Abstract related object sections #93
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
gtfs.zone/coloring-book#93
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
This refactor introduces a centralized
src/utils/entity-references.tsutility with two pure render functions —renderRouteReferenceandrenderServiceReference— that become the canonical way to display a related object row anywhere in the Browse tab. All four view contexts (home, agency, route, service) will consume these functions, replacing the scattered, inconsistent HTML today. The key behavioral changes: on the service page the related-routes list becomes a "Timetables" list where row click → timetable and a "View Route" button navigates to the route; on the route page the same inversion applies with "View Service". Service reference rows gain rich calendar metadata (days of week formatted as "Mon–Fri" or "Mon, Wed, Fri", date range, counts). Route reference rows get a color dot, name, agency, and trip count.Relevant Context
Files to create:
src/utils/entity-references.ts— new shared render functions + CSS class constantsFiles to modify:
src/modules/page-content-renderer.tsrenderRoute()(ln 477–621): replace "Services" section withrenderServiceReferencerowsrenderHome()(ln 273–384): replace bare service cards withrenderServiceReferencerowsaddEventListeners()(ln 667–765): swap class selectors for new CSS constantssrc/modules/service-view-controller.tsrenderRelatedRoutes()(ln 172–297): replace DaisyUI table withrenderRouteReferencerowsaddEventListeners()(ln 317–353): swap class selectorssrc/modules/agency-view-controller.tsrenderRouteItem()(ln 134–147): replace withrenderRouteReferenceaddEventListeners()(ln 206–217): swap.route-itemforROUTE_REF_ROWconstantKey existing utilities:
src/utils/entity-display.ts—getRouteDisplay(),getServiceDisplay(),renderCardLabel()— continue to use thesesrc/types/gtfs.ts—Routes,Calendar,CalendarDatestypessrc/utils/agency-helpers.ts—normalizeAgencyId()Event wiring pattern: all click handlers live in
addEventListeners()methods; HTML is rendered as template strings withdata-*attributes and CSS class selectors. The new CSS class constants must be imported fromentity-references.tsrather than hard-coded as strings.Phase 1: Create
src/utils/entity-references.tsAll other phases depend on this. The functions are pure (no async, no DB access) — callers pre-fetch everything and pass it in.
Define and export CSS class constants:
Define
RouteReferenceOpts:Define
ServiceReferenceOpts:Implement
formatDaysOfWeek(service: Record<string, unknown>): stringmonday,tuesday,wednesday,thursday,friday,saturday,sunday'Mon–Fri''Mon, Wed, Fri''Specific Days''No regular days'Implement
formatDateRange(service: Record<string, unknown>, calendarDates?: Array<{date: string; exception_type: string | number}>): stringservice.start_dateexists: return'${start_date} – ${end_date}'calendarDatesprovided with positive exceptions (exception_type === '1'or=== 1): derive min/max date and return'${min} – ${max}'''Implement
renderRouteReference(route: Record<string, unknown>, opts: RouteReferenceOpts): stringflex items-center gap-3 p-3 rounded-lg hover:bg-base-200 cursor-pointer transition-colors ${ROUTE_REF_ROW}data-route-id="${route.route_id}"on the containerdata-service-id="${opts.service_id}"on the container whenservice_idpresent<div class="w-3 h-3 rounded-full flex-shrink-0" style="background-color: #${route.route_color || '6366f1'}"></div>renderCardLabel(getRouteDisplay(route as Record<string, string>))— primary = short/long name, secondary = IDagencyNameprovided, show it as a small muted line below the route labeltripCount !== undefined, show<div class="badge badge-outline badge-sm">${tripCount} trip${tripCount !== 1 ? 's' : ''}</div>service_idpresent: add<button class="btn btn-xs btn-ghost ${ENTITY_REF_BTN}" data-route-id="${route.route_id}">View Route</button>. The timetable link lives on the row itself (row click).service_idabsent: no secondary button; clicking the row navigates to the route page.Implement
renderServiceReference(service: Record<string, unknown>, opts: ServiceReferenceOpts): stringflex items-center gap-3 p-3 rounded-lg hover:bg-base-200 cursor-pointer transition-colors ${SERVICE_REF_ROW}data-service-id="${service.service_id}"on the containerdata-route-id="${opts.route_id}"when presentrenderCardLabel(getServiceDisplay(service as Record<string, string>)))formatDaysOfWeek(service)— shown as a small muted text if non-emptyformatDateRange(service, opts.calendarDates)— shown as small muted text if non-emptyroute_idpresent: add<button class="btn btn-xs btn-ghost ${ENTITY_REF_BTN}" data-service-id="${service.service_id}">View Service</button>. Timetable link lives on the row.route_idabsent: no secondary button; clicking the row navigates to the service page.Gotchas:
route_colorin GTFS has no#prefix; always prepend itroute.route_coloris empty/undefined, fall back to a neutral color (e.g.,#6366f1)formatDaysOfWeekmust handle records where day fields are completely absent (calendar_dates-only services have only{ service_id })Phase 2: Refactor Route Page timetables (
page-content-renderer.ts::renderRoute)Replace the "Services" section with rich service reference rows. Row click → timetable; "View Service" button → service page.
renderServiceReference,SERVICE_REF_ROW,TIMETABLE_REF_BTN,ENTITY_REF_BTNfromentity-references.tsallServicesfetch (calendar rows), build a lookup map:const calendarByServiceId = new Map(allServices.map(s => [s.service_id as string, s]))serviceGroups, call:Object.entries(serviceGroups).map(...)block with the above calls<h2>from "Services" to "Timetables"#new-service-selectdropdown (for adding new timetables) exactly as-is — it sits above the reference rowsaddEventListeners():.service-rowclick handler (old: navigates to service page).route-timetable-btnclick handlerSERVICE_REF_ROWclick handler:onTimetableClick(route_id, service_id)— get both IDs fromdata-*attributesENTITY_REF_BTNclick handler (scoped to route page): navigate to service viaonServiceClick(service_id)— getservice_idfromdata-service-idon the buttonGotchas:
{ service_id }—renderServiceReferencehandles this gracefully viaformatDaysOfWeek's absent-fields path.onServiceClickis currentlyoptionalinContentRendererDependencies. Keep defensive guard in the event handler.ENTITY_REF_BTNclass will also appear on the service page (Phase 3) after DOM insertion. SinceaddEventListenersscopes queries to the providedcontainer, there's no cross-page interference..service-linkhandler inaddEventListeners()(ln 727–737) should also be removed as it's superseded.Phase 3: Refactor Service Page timetables (
service-view-controller.ts)Replace the DaisyUI
table table-pin-rows(grouped by agency) with route reference rows. Section renamed to "Timetables". Row click → timetable; "View Route" button → route page.renderRouteReference,ROUTE_REF_ROW,ENTITY_REF_BTNfromentity-references.tsrenderServiceView(), fetch trips for this service and build a trip count map:getAgenciesForRoutes(routes)call, then buildMap<normalizedAgencyId, agencyName>from itrenderRelatedRoutestorenderTimetablesSection; change signature to(routes: Routes[], agencyNameByNormalizedId: Map<string, string>, tripCountByRoute: Map<string, number>, service_id: string): stringtableHTML/table.table-pin-rowswith route reference rows wrapped in<div class="space-y-2">addEventListeners():.route-rowclick handler.agency-linkclick handler.timetable-btnclick handlerROUTE_REF_ROWclick handler:onTimetableClick(route_id, service_id)— read bothdata-route-idanddata-service-idfrom the rowENTITY_REF_BTNclick handler:onRouteClick(route_id)— readdata-route-idfrom the buttonGotchas:
agency_id(single-agency GTFS feed):agencyNamewill beundefined, which is fine —renderRouteReferenceomits it silentlytable table-pin-rowswith agency group headers is removed entirely; agency name appears inline in each row instead — this is a deliberate simplification per the issueServiceViewDependencies.onAgencyClickis currently optional and only used for agency header links. After this phase it is no longer wired up to any rendered element. Leave the dependency in place (don't remove it from the interface) to avoid breaking callers, but it is effectively unused in rendering.Phase 4: Enrich Home Page service cards (
page-content-renderer.ts::renderHome)Replace bare service name cards with
renderServiceReferencerows that include days, date range, and counts.renderServiceReference,SERVICE_REF_ROWat the top ofpage-content-renderer.ts(already importing from Phase 2)renderHome(), after fetching services, fetch all trips once:serviceItemsmap with calls torenderServiceReference(serviceData, { tripCount, routeCount })— noroute_id(home context)addEventListeners(): replace.service-cardclick handler withSERVICE_REF_ROWclick handler →onServiceClick(service_id)(updated the existingSERVICE_REF_ROWhandler to handle both route-page context withroute_id→ timetable, and home-page context withoutroute_id→ service page)Gotchas:
getAllRows('trips')adds one extra async DB call on the home page render. This is acceptable given dataset sizes typical of GTFS.extraServices(calendar_dates-only, shaped as{ service_id }) will havetripCountfrom the trips map but no day/date fields —renderServiceReferencehandles this gracefullydata-inline-create="service"input for creating new services stays in place above the listPhase 5: Enrich Agency Page route items (
agency-view-controller.ts)Replace
renderRouteItemwithrenderRouteReference. No service context (row click → route page). Add trip count per route.renderRouteReference,ROUTE_REF_ROWfromentity-references.tsrenderAgencyView(), after fetching routes, fetch trip counts viaqueryRows('trips')(no filter = all rows;QueryOnlyDatabaseonly exposesqueryRows, notgetAllRows)renderRoutesList(), replace.map(route => this.renderRouteItem(route))with.map(route => renderRouteReference(route as Record<string, unknown>, { tripCount: tripCountByRoute.get(route.route_id) }))tripCountByRouteintorenderRoutesList()— updated method signature accordinglyrenderRouteItem()methodaddEventListeners(): replace.route-itemclick handler withROUTE_REF_ROWclick handler →onRouteClick(route_id)Gotchas:
agencyNamepassed torenderRouteReference(we're on the agency page — redundant)service_idpassed — row click → route page, no timetable buttongetAllRows('trips')filtered client-side is efficient enough for typical GTFS datasets; avoids one query per routeAgencyViewControlleronly hasgtfsDatabaseandonRouteClickin its deps — both are already availableOriginal Issue
These can be more consistent and a bit more descriptive. We should make proper abstractions so that a browse page is basically just what is described here, then we describe how to display each corresponding item.
Pages
Feed Information
Agency Properties
Service Schedule
Route Page
References
To a Service
To a Route