尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗Mobilní utilita: jedním klepnutím zjisti 2–3 nejbližší konkrétní vstupy do pražského metra a spusť k nim pěší navigaci v Google/Apple Maps. Plus vlastní čitelná, přibližovatelná schematická mapa metra (linky A, B, C) a jedna nenápadná reklamní pozice (zatím jen placeholder).
navigator.geolocation) a ukáže 3 nejbližší vstupy
do metra — s barevnými odznaky linek, orientační vzdáleností/časem
chůze a tlačítkem na pěší navigaci k přesným GPS souřadnicím vstupu./ (čeština) a /en (angličtina), viz sekce
SEO a jazykové URL níže. Přepínač v hlavičce
je skutečný odkaz mezi oběma routami, žádné klientské přepnutí ani
detekce/uložení preferovaného jazyka v localStorage — URL je jediný
zdroj pravdy.localStorage.npm install
npm run data:refresh # stáhne aktuální PID GTFS a vygeneruje data/*.json (viz níže)
npm run dev
# → http://localhost:3000
data/metro-entrances.json a data/metro-line-order.json jsou
verzované v repozitáři, takže npm run dev/npm run build fungují i
bez spuštění importu — data:refresh stačí pouštět jen když chceš
data aktualizovat.
npm run data:refreshStáhne oficiální feed https://data.pid.cz/PID_GTFS.zip, rozbalí jen
potřebné CSV soubory (routes.txt, trips.txt, stops.txt,
stop_times.txt, calendar.txt, calendar_dates.txt — vyžaduje
systémový příkaz unzip), odvodí skutečné vstupy do metra, jejich
linky a plánované odjezdy, a zapíše:
data/metro-entrances.json — všechny vstupy (souřadnice, linky,
bezbariérovost),data/metro-line-order.json — reálné pořadí stanic na každé lince
(podle stop_sequence), ze kterého vychází schematická mapa,public/data/departures/{stationId}.json — jeden kompaktní soubor
odjezdů na stanici (viz Odjezdy níže).Import selže s chybou (nenulový návratový kód), pokud by výsledek byl nápadně malý (méně než 50 stanic nebo 200 vstupů), nebo pokud by některá stanice appky neměla v GTFS odpovídající naplánované odjezdy — ochrana proti omylem nasazeným prázdným/změněným/nekonzistentním datům. Appka za běhu žádné GTFS/ZIP nezná, jen tyhle hotové JSONy.
Dopravní data pocházejí z oficiálního feedu Pražské integrované dopravy (PID), licence CC BY 4.0 — atribuce je vždy v patičce appky.
Vzdálenosti zobrazené u výsledků jsou primárně vzdušné (Haversinova formule) — okamžitě po získání polohy, a pro uživatele mimo Prahu vždy. V Praze appka navíc na pozadí dopočítá skutečnou pěší vzdálenost/čas přes Mapy.com Matrix Routing (viz níže) a výsledky podle toho přeřadí; appka vždy transparentně označí, o který typ vzdálenosti jde. Přesnou trasu (geometrii) i tak spočítá až externí navigace (Google/Apple Maps/Mapy.com), na kterou appka odkazuje.
Projekt je běžná Next.js 15 aplikace, žádná databáze ani env proměnné nejsou potřeba:
npm i -g vercel # pokud ještě není nainstalované
vercel --prod
Vercel přidělí *.vercel.app URL — to stačí pro MVP. Vlastní doména se
připojí později (Project → Settings → Domains), kód se kvůli tomu měnit
nemusí (app/config/site.ts si SITE_URL sám odvodí z
NEXT_PUBLIC_SITE_URL/VERCEL_URL).
Čeština (/) a angličtina (/en) jsou dvě samostatné, serverově
vykreslené a nezávisle indexovatelné routy — ne jedna URL s klientským
přepínáním jazyka. Jazyk stránky určuje výhradně URL; appka nikdy
needetekuje jazyk prohlížeče ani needetekuje/nepřepisuje jazyk z
localStorage (jediná výjimka je nezávislý 18+ přepínač, který
zůstává v localStorage a jazyk nijak neovlivňuje).
app/(cs)/ (route group, na URL
se neprojeví) obsahuje layout.tsx (<html lang="cs">) a page.tsx
(route /). app/en/ obsahuje vlastní layout.tsx
(<html lang="en">) a page.tsx (route /en). Jde o standardní
Next.js vzorec "multiple root layouts" — proto už neexistuje jeden
sdílený app/layout.tsx. app/not-found.tsx je fallback pro cestu,
která nepatří ani pod jednu z těchto dvou skupin.components/HomePage.tsx (Server Component) s locale propem —
hledání/mapa/reklamy zůstávají shodné, mění se jen jazyk textů
(lib/i18n/dictionary.ts) a SEO obsah (lib/seo/content.ts).I18nProvider (components/i18n/I18nProvider.tsx) dostává
locale jako povinný prop odvozený serverem — nikdy sám nedetekuje
ani neukládá jazyk. LanguageToggle je skutečný next/link mezi
/ a /en, ne klientské přepnutí stavu.robots: index,follow) jsou u obou stránek nastavená přímo v
app/(cs)/page.tsx / app/en/page.tsx přes lib/seo/content.ts.
alternates.languages na obou stránkách odkazuje obousměrně na cs,
en i x-default. metadataBase (v obou layout.tsx) je postavené
nad SITE_URL, takže se canonical/hreflang v preview prostředí nikdy
neprosáknou k produkční doméně.WebApplication JSON-LD (jen ověřitelné
údaje — žádné hodnocení, cena, autor) na obou stránkách a FAQPage
JSON-LD sestavené přímo z pole viditelných FAQ otázek
(lib/seo/structured-data.ts), takže nikdy neujede od toho, co
appka skutečně zobrazuje.components/seo/SeoContent.tsx
(čistě serverová komponenta, žádné "use client") vykresluje úvod,
"Jak to funguje", tematický rozcestník ("Nejčastěji hledané" /
"Explore the Prague Metro") a FAQ — vše je součástí prvotního HTML,
žádná z těchto sekcí nevyžaduje JavaScript.lib/metro/brno.ts (,
) — čistě lokální Haversinův výpočet nad
existující , žádný geocoding, poloha nikam
neodchází. Kontroluje se AŽ po existující hranici "mimo Prahu" (25 km).Malé sekundární tlačítko Odjezdy / Departures na každé výsledkové kartě (v pomocném řádku pod trojicí navigačních tlačítek, vedle textu s vysvětlením vzdušné vzdálenosti — nikdy jako čtvrté tlačítko v jejich řádku) otevře přístupný panel s plánovanými odjezdy podle GTFS jízdního řádu. Nejde o polohu vlaků v reálném čase.
https://data.pid.cz/PID_GTFS.zip), navíc trips.txt (service_id,
trip_headsign, direction_id), stop_times.txt
(departure_time, stop_sequence), calendar.txt a
calendar_dates.txt.stationId, jaký už mají vstupy v
data/metro-entrances.json (GTFS parent_station) — žádné párování
podle názvu. scripts/import-pid-gtfs.ts po vygenerování odjezdů
ověří, že KAŽDÁ stanice appky má odpovídající soubor odjezdů
(lib/gtfs/validate-departures-coverage.ts) — chybějící/nejednoznačná
vazba je fatální chyba importu, appka nikdy tiše nezobrazí odjezdy
cizí stanice.route_type=1 +
route_short_name identifikace jako u vstupů
(lib/gtfs/metro-routes.ts, sdílené s importem vstupů). Směr podle
GTFS direction_id (0/1) — popisek volby směru je nejčastější
trip_headsign v tom směru, ale každý jednotlivý odjezd si nese
VLASTNÍ headsign, takže krátce ukončený spoj (jiná konečná než
"hlavní" směr) ukáže svůj skutečný cíl, ne zavádějící společný
popisek. Poslední zastávka spoje (GTFS ji vždy vyplní, i když vlak
dál nepokračuje) se do odjezdů nepočítá — jinak by se na konečné
stanici objevovaly "odjezdy" směrem k ní samé (příjezdy).Europe/Prague (lib/time/prague-time.ts, sdílené se svátkovým
přáním). Aktivní service_id pro dané datum řeší
lib/departures/service-calendar.ts přesně podle GTFS sémantiky
(calendar.txt týdenní vzor, calendar_dates.txt výjimky mají vždy
přednost). Nejbližší odjezdy (lib/departures/next-departures.ts)
posuzují aktivní služby DNEŠNÍHO i VČEREJŠÍHO provozního dne — spoj
zapsaný v GTFS jako např. 24:35:00 (čas > 24:00:00, spoj přes
půlnoc) se tak správně objeví jako dnešní brzké ráno, pokud mu
odpovídající služba platila včera.getLastDeparture) — nikdy jako pevně uložená hodnota.lib/departures/freshness.ts — starší než 3 dny (GTFS
kalendáře PID typicky pokrývají jen pár týdnů dopředu) zobrazí
upozornění "Jízdní řád nemusí být aktuální." místo tvrzení o
posledním vlaku. Selhání načtení po kliknutí zobrazí samostatnou
chybovou hlášku — appka nikdy nevymýšlí náhradní časy.Reklamy se od 2026-09 spravují výhradně přes
content-api.darbujan.com/admin/promotions
(project "kdejemetro"), NE v tomto repozitáři. Appka je jen ČTE — žádná
kampaň se sem nezapisuje natvrdo, přidání/úprava/deaktivace nové reklamy
nikdy nevyžaduje nový deploy KdeJeMetro.cz. Podrobná architektura
Content API (Project → Collection → Record, per-projekt tokeny) je
zdokumentovaná v repozitáři universal-content-api
(docs/KDEJEMETRO_INTEGRATION.md).
Appka má dvě reklamní pozice (finder_results pod výsledky hledání
nejbližšího metra, night_finder_results pod výsledky noční dopravy) —
každá se stahuje NEZÁVISLE, výpadek jedné neovlivní druhou.
components/HomePage.tsx a
components/night/NightPage.tsx (Server Components) volají
lib/promotions/get-promotions.ts::getActivePromotionCampaigns(placement)
a výsledek posílají dolů jako obyčejnou prop (HomeClient →
FinderSection → AdSlot, resp. NightFinder → AdSlot) — žádná z
klientských komponent neví, že Content API existuje, a browser samotné
Content API nikdy nevolá přímo. Fetch je cachovaný Next.js
revalidate: 300 (~5 minut) — změna v adminu se tedy neprojeví
okamžitě, jen s tímhle zpožděním.getRecords(...).catch(() => [])
— chybějící env proměnné, timeout, HTTP chyba i nevalidní JSON vždy
skončí jako prázdné pole, appka nikdy nespadne kvůli reklamě. Prázdné
pole se pak chová stejně jako "žádná způsobilá kampaň" (viz České
přání se jmeninami níže).lib/promotions/get-promotions.ts
převádí syrovou odpověď Content API (UcaRecord) na existující,
beze změny ponechaný typ AdCampaign (lib/ads/types.ts) — CELÁ
výběrová logika (lib/ads/filter-campaigns.ts,
lib/ads/weighted-select.ts, lib/ads/select-ad.ts,
lib/ads/language-fallback.ts, lib/ads/validate-url.ts,
lib/ads/resolve-slot-content.ts) zůstala z dřívějška nezměněná a
plně otestovaná — jen dřív dostávala natvrdo zapsané pole
(lib/ads/campaigns.ts, smazáno), teď dostává pole staené server-side
z Content API. data.locale (nepovinné pole v adminu) se mapuje na
languages — bez vyplnění platí kampaň pro všechny 4 jazyky, s
vyplněním jen pro ten jeden (s de/uk fallbackem na en, viz níže).
advertiser a ikona podle kategorie nemají v Content API schématu
odpovídající pole (obecné schéma sdílené s HowToFish.cz/Gembl.cz) —
karta teď vždy ukazuje obecnou výchozí ikonu bez jména partnera.valid_from/valid_until v adminu), A MÁ
href s platnou absolutní https:// URL (lib/ads/validate-url.ts
→ hasValidAffiliateUrl). href prázdné, http:, javascript:,
data:, i relativní cesta se vždy vyřadí.Když pro aktuální jazyk (cs) neexistuje žádná způsobilá reklama s
platným https:// affiliate odkazem, appka pod výsledky hledání
zobrazí místo reklamy jednoduché přání se jmeninami — není to
reklama: žádný štítek "Reklama", žádné CTA, žádný odkaz, žádný název
partnera, žádné reklamní tracking. V angličtině se přání nikdy
nezobrazuje — bez způsobilé anglické reklamy appka pod výsledky
nezobrazí nic.
lib/ads/resolve-slot-content.ts (čistá
funkce, AdResolutionState → pending/ad/nameday/none) použitá
v components/ads/AdSlot.tsx. Dokud výběr reklamy na klientovi ještě
neproběhl (pending), nevykreslí se ani reklama, ani přání — ať
jedno neprobliskne pod druhým.lib/namedays/czech-namedays.ts — statická data
převzatá z OzzyCzech/namedays-cs
(soubor lib/names.json), licence MIT (© Roman Ožana), plný text
licence je přímo v hlavičce souboru. Appka žádné externí API pro
jmeniny nevolá — data jsou uložená lokálně v repozitáři a načtou se
jako běžný TS modul.Europe/Prague
(lib/namedays/get-czech-nameday.ts, getPragueCalendarDate přes
Intl.DateTimeFormat), ne podle UTC — důležité, protože Vercel běží
v UTC a prosté new Date().getDate() by kolem půlnoci dávalo špatný
den. Když appka zůstane otevřená přes pražskou půlnoc, přání se samo
aktualizuje (jeden setTimeout naplánovaný přesně na příští pražskou
půlnoc, žádný interval tikající každou sekundu/minutu).formatNamedaySentence() — jedno jméno "Dnes má
svátek X.", dvě jména "Dnes mají svátek X a Y.", tři a více
"Dnes mají svátek X, Y a Z." (přirozený český výčet, nikdy lomítko).
Pro dny bez jména v datech (v civilním kalendáři jde o 5 státních
svátků bez jmenin — 1. leden, 1. a 8. květen, 6. červenec, 25.
prosinec) bezpečný obecný fallback "Ať se vám dnes daří.".lib/names.json z OzzyCzech/namedays-cs
a nahraď obsah CZECH_NAMEDAYS v lib/namedays/czech-namedays.ts —
formát je stejný ("MM-DD": ["Jméno", ...]), jen zachovej hlavičku s
licencí a zdrojem.locale: cs
a platným href) se přání automaticky přestane zobrazovat, aniž by
bylo potřeba cokoliv v NamedayGreeting.tsx nebo AdSlot.tsx měnit
nebo appku znovu nasazovat.Výhradně přes content-api.darbujan.com/admin/promotions
— vyfiltruj projekt "KdeJeMetro.cz", zvol umístění ("KdeJeMetro —
výsledky hledání" / "KdeJeMetro — noční doprava"), vyplň title/text/CTA/
odkaz/váhu, volitelně jazyk (bez výběru platí pro všechny 4). Nový
záznam je hned "Aktivní" ve výchozím stavu — appka ho vyzvedne do ~5
minut (revalidate: 300), bez nutnosti nového deploy. Deaktivace =
přepnutí "Aktivní" na vypnuto (nebo tlačítko "Deaktivovat" v seznamu),
appka přestane kampaň nabízet ve stejném cache okně.
Affiliate URL se vkládá přesně tak, jak ji partner poskytl — appka ji
nijak neupravuje, nezkracuje, nepřejmenovává parametry ani nepřidává
vlastní. Datový model je připravený i na budoucí Dognet parametry —
celou už hotovou schválenou affiliate URL (klidně včetně d1/d2
apod., nebo partner_id/utm_* jako u GetYourGuide) prostě vlož do
pole "Odkaz (href)" tak, jak je.
Poloha uživatele ani ID stanice se reklamním partnerům ani Content API
nikdy neposílají — appka jen otevře cílovou href URL v nové kartě.
V sessionStorage se ukládá výhradně ID vybrané kampaně, nic víc.
Systém má připravené typy pro budoucí měření (lib/ads/events.ts,
AdEvent), ale v této iteraci se žádná reklamní data nikam neodesílají
— jen volitelný console.debug v development režimu. Content API token
je jen server-side (env proměnná UCA_API_TOKEN, scope pouze
records:read) a nikdy neopustí server.
Podrobná implementace, ověřené API parametry a reálné srovnání před/po jsou v docs/ROUTING.md. Provozní shrnutí:
developer.mapy.com/my-account/financial-settings/ (placenou
spotřebu tam lze i nechat vypnutou — appka zůstává plně funkční jen s
bezplatným limitem, viz níže).lib/routing/matrix-circuit-breaker.ts).Appka jde na Androidu i iPhonu přidat na plochu a spustit skoro jako
samostatná appka (display: standalone, vlastní ikona, žádná adresní
lišta). Cíl je jen instalovatelná ikonka, ne offline appka — proto
žádný service worker (appka pracuje s aktuální polohou/tras/dopravními
daty a agresivní offline cache by snadno vedla k zastaralým informacím,
viz zadání). Bez service workeru appka splňuje instalovatelnost jak v
Chrome/Androidu (beforeinstallprompt), tak přes iOS Safari "Přidat na
plochu" — obojí funguje čistě nad Web App Manifestem a metadaty.
app/manifest.ts (Next.js App Router file convention,
vystavené na /manifest.webmanifest). Branding (název, popis, barvy)
je v lib/pwa/config.ts — jediné místo, kde se mění.app/icon.tsx) je v
lib/pwa/icon-graphic.tsx, vykreslovaná přes next/og ImageResponse
— žádné binární soubory v repu:
/pwa-icon-192, /pwa-icon-512 — běžné ikony (purpose: "any")./pwa-icon-512-maskable — maskable varianta (obsah zmenšený do
bezpečné zóny, aby ho Android adaptivní ikona neoříznula).app/apple-icon.tsx — Next.js file-convention Apple touch icon
(180×180), automaticky polinkovaný ve všech jazykových layoutech.app/icon.tsx) zůstal beze změny.viewport.themeColor, metadata.appleWebApp):
sdílené konstanty v lib/pwa/config.ts, znovupoužité ve všech čtyřech
jazykových root layoutech (app/(cs)/layout.tsx, app/en/layout.tsx,
app/de/layout.tsx, app/ua/layout.tsx) — žádný sdílený
app/layout.tsx neexistuje (viz "multiple root layouts" výše), proto
se importují zvlášť do každého z nich, stejně jako metadataBase.components/pwa/InstallPrompt.tsx — nenápadná
karta pod hlavní funkční částí homepage (components/HomePage.tsx, až
po <HomeClient>), viditelná jen do sm: šířky (telefon, ne
tablet/desktop). Na Androidu tlačítko spustí nativní install dialog
(hooks/usePwaInstall.ts), na iOS otevře krátký návod (2 kroky:
Sdílet → Přidat na plochu) ve spodním panelu — nikdy ne fake
"instalační" tlačítko. Karta se schová v standalone režimu a po
zavření (X, po odpovědi na Android prompt, nebo po zavření iOS
návodu) na 30 dní (DEFAULT_REMIND_AFTER_MS v InstallPrompt.tsx).Texty (a jazykové mutace cs/en/de/uk) jsou v lib/i18n/dictionary.ts
pod klíčem pwa — stejný slovník jako zbytek appky, žádný zvláštní i18n
systém jen pro PWA.
Uprav lib/pwa/config.ts (PWA_NAME, PWA_SHORT_NAME,
PWA_DESCRIPTION, PWA_THEME_COLOR, PWA_BACKGROUND_COLOR) — hodnoty
se automaticky promítnou do manifestu i do <meta name="theme-color">/
Apple Web App metadat. Manifest má vědomě jen jeden start_url/scope
(/, česká homepage) bez ohledu na to, z které jazykové verze appku
uživatel nainstaluje — kvůli jednoduchosti (jeden manifest pro celý
web), ne přehlédnutím.
hooks/usePwaInstall.ts, hooks/usePersistentDismiss.ts a
components/pwa/InstallPrompt.tsx jsou napsané bez KdeJeMetro brandingu
— žádná barva, text ani ikona natvrdo zapsaná v logice. Pro nasazení v
jiném Next.js projektu (KdeJeHospoda.cz, Hlasuju.cz, NechciVolit.cz,
KdeJeMHD.cz…) stačí:
hooks/usePwaInstall.ts, hooks/usePersistentDismiss.ts,
lib/pwa/detect-platform.ts, lib/storage/dismiss-window.ts,
lib/storage/safe-storage.ts (pokud tam ještě není) a
components/pwa/InstallPrompt.tsx beze změny.<InstallPrompt texts={...} ctaClassName="..." /> s vlastními
texty/barvou daného projektu (typ InstallPromptTexts).app/manifest.ts + ikony podle vzoru
lib/pwa/config.ts/lib/pwa/icon-graphic.tsx (barvy a motiv jsou
samozřejmě jiné pro každý projekt)./stanice)Osm ručně psaných, indexovatelných průvodců (/stanice/mustek, muzeum,
florenc, cerny-most, nadrazi-veleslavin, zlicin, haje, letnany)
a přehled /stanice. Jen česky, staticky generované (generateStaticParams,
dynamicParams = false — jiný slug je 404). Na homepage (jen /) na ně
odkazuje nenápadný blok pod mapou.
lib/stations/guides.ts: texty, „Kam potřebujete?“,
FAQ a VŠECHNY externí URL (poskytovatel, typ, přímý odkaz, datum ověření).
Faktická tvrzení mají u sekce sourceIds (hlavně bezbariérové stránky DPP).
Odjezdy, výluky a stav výtahů se nikam neopisují, jen se na ně odkazuje.data/metro-entrances.json (stejná data jako homepage).data/station-area-stops.json, generuje
npm run data:refresh z PID GTFS (stejný uzel asw_node_id, kód
platform_code, linky, které ze stanoviště podle jízdního řádu skutečně
odjíždějí). Díky tomu „linka 59 → stanoviště 2“ drží krok s denní
aktualizací dat bez ruční údržby.lib/stations/place-links.ts. Vstupy a stanoviště (přesná
GPS z PID) = pěší navigace v Google/Apple Maps bez výchozí polohy (start
doplní mapová aplikace), Mapy.com bod ukáže na mapě. Cíle bez ověřených
souřadnic (Národní muzeum, autobusové nádraží…) = jen vyhledání místa
v mapách podle názvu, žádná předstíraně přesná navigace.lib/stations/events.ts posílá přes track() z
@vercel/analytics (Vercel Web Analytics je na projektu zapnutá, tým má
plán Pro) události station_departures_click, station_navigation_click,
station_destination_click a station_source_click. Plán Pro dovoluje
jen 2 vlastnosti na událost, proto: station_slug + provider /
destination_id / destination_provider (u navigace spojené
„id-cíle:služba“). Nikdy poloha uživatele. Zobrazení stránek měří
<Analytics /> automaticky. Události najdete ve Vercelu: Project →
Analytics → Events.data/station-area-stops.json; import metra a noční dopravy kvůli tomu
nespadne (decideAreaStopsWrite). Denní workflow soubor commituje spolu
s ostatními daty.Přidání další stanice = nový záznam v STATION_GUIDES + GUIDE_ORDER;
test test/station-guides.test.ts ohlídá odkazy, kotvy, unikátní SEO a vazbu
na data PID.
/jizdenky-praha)Stránka o jízdenkách PID (ceny, SMS jízdenka, PID Lítačka, automaty a
terminály, označování, FAQ) ve 4 jazycích: /jizdenky-praha,
/en/prague-tickets, /de/fahrkarten-prag, /ua/kvytky-praha
(TICKETS_LOCALE_TO_ROUTE, vlastní hreflang klastr, x-default = en).
Odkaz „Jízdenky“ v hlavičce vede na verzi v aktuálním jazyce; česká
stránka má navíc odkaz z homepage a ze všech průvodců stanicemi.
Texty — lib/tickets/content.ts (stránka, server) a
lib/tickets/picker-text.ts (SMS komponenta, klient). Fakta berou vždy
z lib/tickets/tariff.ts; testy hlídají, že ceny a kódy sedí ve všech
jazycích.
Tarif — ručně ověřený v lib/tickets/tariff.ts (zdroje a datum
ověření v komentáři i na stránce). Při změně tarifu PID upravte ceny,
TARIFF_YEAR a TARIFF_VERIFIED_AT; rok v titulku se sám nemění.
Varianta SMS s verified: false se nenabídne k přípravě.
SMS — components/tickets/SmsTicketPicker.tsx jen otevře SMS
aplikaci (sms:90206?body=… pro Android, sms:90206&body=… pro iOS),
nic neodesílá ani neúčtuje. Číslo a text jsou vždy vidět pro ruční odeslání.
Analytika — lib/tickets/events.ts (Vercel track(), max. 2
vlastnosti): ticket_sms_option_select, ticket_sms_prepare_click,
ticket_sms_copy_click (ticket_duration, ticket_price) a
ticket_pid_app_click (ticket_method, placement). Zobrazení stránky
měří <Analytics /> automaticky.
Strukturovaná data — jen BreadcrumbList, žádné Product/Offer (web jízdenky neprodává).
E1, E2, …) je přímo z GTFS feedu — pokud feed
nemá lidštější název (např. název ulice), appka si nic nevymýšlí a
zobrazí jen tohle technické označení.npm run data:refresh mírně lišit (např. dočasně
uzavřená stanice kvůli rekonstrukci se v datech krátkodobě neobjeví).start_url: "/"
v manifestu) bez ohledu na to, ze které jazykové verze si ji uživatel
nainstaloval — vědomé zjednodušení, viz PWA / Přidat na
plochu.sm: a výš) — na iPadu v landscape orientaci se tak nemusí zobrazit,
i když je zařízení technicky rozpoznané jako iOS (zadání cílí na
Android/iPhone, ne konkrétně iPad).isNearBrnoclassifyOutsidePraguehaversineDistanceMetersapp/sitemap.ts obsahuje obě jazykové URL s obousměrnými
alternates.languages.public/data/departures/{stationId}.json — jeden kompaktní
soubor na stanici (~35–115 kB nekomprimovaně, cca 5–10 kB po gzipu),
fetchovaný AŽ při otevření panelu. Homepage//en ho nikdy nestahuje,
obě jazykové verze sdílejí stejné soubory (jazykově neutrální data).hooks/useFocusTrap.ts — focus trap, zavření
klávesou Escape, návrat focusu na tlačítko, které panel otevřelo.
components/DeparturesPanel.tsx (role="dialog", aria-modal,
aria-labelledby) je na mobilu spodní panel, od sm: výš
vycentrovaný dialog.stop_url pro žádnou stanici
metra a appka nechtěla vymýšlet neověřenou URL. Text pro budoucí
použití zůstává v lib/i18n/dictionary.ts
(dict.departures.checkInPidLitacka).file:lib/ads/weighted-select.ts) prostě pracuje se skutečným součtem vah
aktuálně způsobilé množiny.hooks/useSelectedAd.ts (beze změny) uloží ID vybrané kampaně do
sessionStorage pod klíčem kdejemetro:selected-ad:{jazyk} — reklama
se tak během jedné návštěvy a stejného jazyka nemění. Pokud uložené ID
patří kampani, která mezitím přestala existovat/být způsobilá
(deaktivovaná v adminu, mimo platnost, jiný jazyk), stará hodnota se
automaticky zahodí a vybere se nová způsobilá kampaň. Do
sessionStorage se nikdy neukládá poloha, souřadnice ani žádný
osobní údaj — a poloha uživatele se ani nikdy neposílá Content API.components/ads/AdSlot.tsx (beze změny logiky, jen
nový povinný campaigns prop) rozhoduje mezi reklamou a fallbackem —
rozhodovací logika je čistá testovatelná funkce
lib/ads/resolve-slot-content.ts. Samotná reklamní karta
(components/ads/AdCard.tsx + AdIcon.tsx) dostává už vybranou
kampaň jako prop a žádnou výběrovou logiku sama neřeší.