summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2026-03-09 00:00:39 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2026-03-09 00:00:50 +0100
commitd71f0ed16d175285f2e8cbde6091994c2aa1d962 (patch)
treee8b0bcc3f432fa9d5243dd4595af256511643151
parent5288cfbed34f94c4321b8d9dc497cfd0da3ffd26 (diff)
Enhance route details handling and add favorites functionality; improve error logging and response structure
-rw-r--r--src/Enmarcha.Backend/Controllers/TransitController.cs15
-rw-r--r--src/Enmarcha.Backend/Services/OtpService.cs4
-rw-r--r--src/Enmarcha.Backend/Types/Transit/RouteDtos.cs2
-rw-r--r--src/Enmarcha.Sources.OpenTripPlannerGql/Queries/RouteDetailsContent.cs4
-rw-r--r--src/frontend/app/api/schema.ts2
-rw-r--r--src/frontend/app/hooks/useFavorites.ts28
-rw-r--r--src/frontend/app/routes/routes-$id.tsx159
-rw-r--r--src/frontend/app/routes/routes.tsx222
8 files changed, 354 insertions, 82 deletions
diff --git a/src/Enmarcha.Backend/Controllers/TransitController.cs b/src/Enmarcha.Backend/Controllers/TransitController.cs
index 7876dbe..9b13972 100644
--- a/src/Enmarcha.Backend/Controllers/TransitController.cs
+++ b/src/Enmarcha.Backend/Controllers/TransitController.cs
@@ -124,8 +124,21 @@ public class TransitController : ControllerBase
var query = RouteDetailsContent.Query(new RouteDetailsContent.Args(id, serviceDate));
var response = await SendOtpQueryAsync<RouteDetailsResponse>(query);
- if (response?.Data?.Route == null)
+ if (response == null)
{
+ return StatusCode(500, "Failed to connect to OTP.");
+ }
+
+ if (!response.IsSuccess)
+ {
+ var messages = string.Join("; ", response.Errors?.Select(e => e.Message) ?? []);
+ _logger.LogError("OTP returned errors: {Errors}", messages);
+ return StatusCode(500, $"OTP Error: {messages}");
+ }
+
+ if (response.Data?.Route == null)
+ {
+ _logger.LogWarning("Route details not found for {Id} on {ServiceDate}", id, serviceDate);
return NotFound();
}
diff --git a/src/Enmarcha.Backend/Services/OtpService.cs b/src/Enmarcha.Backend/Services/OtpService.cs
index e72877e..0de06bf 100644
--- a/src/Enmarcha.Backend/Services/OtpService.cs
+++ b/src/Enmarcha.Backend/Services/OtpService.cs
@@ -108,7 +108,9 @@ public class OtpService
.Select(t => t.Stoptimes.ElementAtOrDefault(i)?.ScheduledDeparture ?? -1)
.Where(d => d != -1)
.OrderBy(d => d)
- .ToList()
+ .ToList(),
+ PickupType = pattern.TripsForDate.FirstOrDefault()?.Stoptimes.ElementAtOrDefault(i)?.PickupType,
+ DropOffType = pattern.TripsForDate.FirstOrDefault()?.Stoptimes.ElementAtOrDefault(i)?.DropoffType
}).ToList()
};
}
diff --git a/src/Enmarcha.Backend/Types/Transit/RouteDtos.cs b/src/Enmarcha.Backend/Types/Transit/RouteDtos.cs
index 3904555..8427ec7 100644
--- a/src/Enmarcha.Backend/Types/Transit/RouteDtos.cs
+++ b/src/Enmarcha.Backend/Types/Transit/RouteDtos.cs
@@ -43,4 +43,6 @@ public class PatternStopDto
public double Lat { get; set; }
public double Lon { get; set; }
public List<int> ScheduledDepartures { get; set; } = [];
+ public string? PickupType { get; set; }
+ public string? DropOffType { get; set; }
}
diff --git a/src/Enmarcha.Sources.OpenTripPlannerGql/Queries/RouteDetailsContent.cs b/src/Enmarcha.Sources.OpenTripPlannerGql/Queries/RouteDetailsContent.cs
index 59a0991..2891b63 100644
--- a/src/Enmarcha.Sources.OpenTripPlannerGql/Queries/RouteDetailsContent.cs
+++ b/src/Enmarcha.Sources.OpenTripPlannerGql/Queries/RouteDetailsContent.cs
@@ -44,6 +44,8 @@ public class RouteDetailsContent : IGraphRequest<RouteDetailsContent.Args>
tripsForDate(serviceDate: "{{args.ServiceDate}}") {
stoptimes {
scheduledDeparture
+ pickupType
+ dropoffType
}
}
}
@@ -108,5 +110,7 @@ public class RouteDetailsResponse : AbstractGraphResponse
public class StoptimeItem
{
[JsonPropertyName("scheduledDeparture")] public int ScheduledDeparture { get; set; }
+ [JsonPropertyName("pickupType")] public string? PickupType { get; set; }
+ [JsonPropertyName("dropoffType")] public string? DropoffType { get; set; }
}
}
diff --git a/src/frontend/app/api/schema.ts b/src/frontend/app/api/schema.ts
index bc47116..0c55969 100644
--- a/src/frontend/app/api/schema.ts
+++ b/src/frontend/app/api/schema.ts
@@ -107,6 +107,8 @@ export const PatternStopSchema = z.object({
lat: z.number(),
lon: z.number(),
scheduledDepartures: z.array(z.number()),
+ pickupType: z.string().optional().nullable(),
+ dropOffType: z.string().optional().nullable(),
});
export const PatternSchema = z.object({
diff --git a/src/frontend/app/hooks/useFavorites.ts b/src/frontend/app/hooks/useFavorites.ts
new file mode 100644
index 0000000..962ac2d
--- /dev/null
+++ b/src/frontend/app/hooks/useFavorites.ts
@@ -0,0 +1,28 @@
+import { useState } from "react";
+
+/**
+ * A simple hook for managing favorite items in localStorage.
+ * @param key LocalStorage key to use
+ * @returns [favorites, toggleFavorite, isFavorite]
+ */
+export function useFavorites(key: string) {
+ const [favorites, setFavorites] = useState<string[]>(() => {
+ if (typeof window === "undefined") return [];
+ const stored = localStorage.getItem(key);
+ return stored ? JSON.parse(stored) : [];
+ });
+
+ const toggleFavorite = (id: string) => {
+ setFavorites((prev) => {
+ const next = prev.includes(id)
+ ? prev.filter((item) => item !== id)
+ : [...prev, id];
+ localStorage.setItem(key, JSON.stringify(next));
+ return next;
+ });
+ };
+
+ const isFavorite = (id: string) => favorites.includes(id);
+
+ return { favorites, toggleFavorite, isFavorite };
+}
diff --git a/src/frontend/app/routes/routes-$id.tsx b/src/frontend/app/routes/routes-$id.tsx
index 6cc872d..2174244 100644
--- a/src/frontend/app/routes/routes-$id.tsx
+++ b/src/frontend/app/routes/routes-$id.tsx
@@ -1,11 +1,14 @@
import { useQuery } from "@tanstack/react-query";
import {
+ ArrowDownCircle,
+ ArrowUpCircle,
Bus,
ChevronDown,
Clock,
LayoutGrid,
List,
Map as MapIcon,
+ Star,
X,
} from "lucide-react";
import { useMemo, useRef, useState } from "react";
@@ -21,13 +24,38 @@ import { fetchRouteDetails } from "~/api/transit";
import { AppMap } from "~/components/shared/AppMap";
import {
useBackButton,
+ usePageRightNode,
usePageTitle,
usePageTitleNode,
} from "~/contexts/PageTitleContext";
import { useStopArrivals } from "~/hooks/useArrivals";
+import { useFavorites } from "~/hooks/useFavorites";
import { formatHex } from "~/utils/colours";
import "../tailwind-full.css";
+function FavoriteStar({ id }: { id?: string }) {
+ const { isFavorite, toggleFavorite } = useFavorites("favouriteRoutes");
+ const { t } = useTranslation();
+
+ if (!id) return null;
+
+ const isFav = isFavorite(id);
+
+ return (
+ <button
+ type="button"
+ onClick={() => toggleFavorite(id)}
+ className="p-2 rounded-full hover:bg-surface"
+ aria-label={t("routes.toggle_favorite", "Alternar favorita")}
+ >
+ <Star
+ size={20}
+ className={isFav ? "fill-yellow-500 text-yellow-500" : "text-muted"}
+ />
+ </button>
+ );
+}
+
export default function RouteDetailsPage() {
const { id } = useParams();
const { t, i18n } = useTranslation();
@@ -45,6 +73,8 @@ export default function RouteDetailsPage() {
const mapRef = useRef<MapRef>(null);
const stopRefs = useRef<Record<string, HTMLDivElement | null>>({});
+ const { isFavorite, toggleFavorite } = useFavorites("favouriteRoutes");
+
const formatDateKey = (value: Date) => {
const year = value.getFullYear();
const month = String(value.getMonth() + 1).padStart(2, "0");
@@ -138,6 +168,9 @@ export default function RouteDetailsPage() {
usePageTitleNode(titleNode);
+ const rightNode = useMemo(() => <FavoriteStar id={id} />, [id]);
+ usePageRightNode(rightNode);
+
useBackButton({ to: "/routes" });
const weekDays = useMemo(() => {
@@ -169,6 +202,43 @@ export default function RouteDetailsPage() {
});
}, [i18n.language, t]);
+ const activePatterns = useMemo(() => {
+ return route?.patterns.filter((p) => p.tripCount > 0) ?? [];
+ }, [route?.patterns]);
+
+ const patternsByDirection = useMemo(() => {
+ return activePatterns.reduce(
+ (acc, pattern) => {
+ const dir = pattern.directionId;
+ if (!acc[dir]) acc[dir] = [];
+ acc[dir].push(pattern);
+ return acc;
+ },
+ {} as Record<number, typeof route.patterns>
+ );
+ }, [activePatterns, route?.patterns]);
+
+ const selectedPattern = useMemo(() => {
+ if (!route) return null;
+
+ if (selectedPatternId) {
+ const found = activePatterns.find((p) => p.id === selectedPatternId);
+ if (found) return found;
+ }
+
+ // Try to find the most frequent pattern in direction 0 (outbound)
+ const outboundPatterns = (patternsByDirection[0] ?? []).sort(
+ (a, b) => b.tripCount - a.tripCount
+ );
+ if (outboundPatterns.length > 0) return outboundPatterns[0];
+
+ // Fallback to any pattern with trips
+ const anyPatterns = [...activePatterns].sort(
+ (a, b) => b.tripCount - a.tripCount
+ );
+ return anyPatterns[0] || route.patterns[0];
+ }, [activePatterns, patternsByDirection, selectedPatternId, route]);
+
if (isLoading) {
return (
<div className="flex justify-center py-12">
@@ -183,21 +253,6 @@ export default function RouteDetailsPage() {
);
}
- const activePatterns = route.patterns.filter((p) => p.tripCount > 0);
-
- const patternsByDirection = activePatterns.reduce(
- (acc, pattern) => {
- const dir = pattern.directionId;
- if (!acc[dir]) acc[dir] = [];
- acc[dir].push(pattern);
- return acc;
- },
- {} as Record<number, typeof route.patterns>
- );
-
- const selectedPattern =
- activePatterns.find((p) => p.id === selectedPatternId) || activePatterns[0];
-
const selectedPatternLabel = selectedPattern
? selectedPattern.headsign || selectedPattern.name
: t("routes.details", "Detalles de ruta");
@@ -210,6 +265,10 @@ export default function RouteDetailsPage() {
{ departure: number; patternId: string; tripId?: string | null }[]
>();
+ if (selectedPattern?.tripCount === 0) {
+ return byStop;
+ }
+
for (const pattern of sameDirectionPatterns) {
for (const stop of pattern.stops) {
const current = byStop.get(stop.id) ?? [];
@@ -240,16 +299,16 @@ export default function RouteDetailsPage() {
const layoutOptions = [
{
- id: "balanced",
- label: t("routes.layout_balanced", "Equilibrada"),
- icon: LayoutGrid,
- },
- {
id: "map",
label: t("routes.layout_map", "Mapa"),
icon: MapIcon,
},
{
+ id: "balanced",
+ label: t("routes.layout_balanced", "Equilibrada"),
+ icon: LayoutGrid,
+ },
+ {
id: "list",
label: t("routes.layout_list", "Paradas"),
icon: List,
@@ -380,11 +439,19 @@ export default function RouteDetailsPage() {
type="circle"
paint={{
"circle-radius": 6,
- "circle-color": "#ffffff",
+ "circle-color": [
+ "case",
+ ["==", ["get", "id"], selectedStopId ?? ""],
+ route.color ? formatHex(route.color) : "#3b82f6",
+ "#ffffff",
+ ],
"circle-stroke-width": 2,
- "circle-stroke-color": route.color
- ? formatHex(route.color)
- : "#3b82f6",
+ "circle-stroke-color": [
+ "case",
+ ["==", ["get", "id"], selectedStopId ?? ""],
+ "#ffffff",
+ route.color ? formatHex(route.color) : "#3b82f6",
+ ],
}}
/>
</Source>
@@ -595,6 +662,24 @@ export default function RouteDetailsPage() {
<h3 className="text-base font-semibold mb-3 text-text">
{t("routes.stops", "Paradas")}
</h3>
+
+ {selectedPattern?.tripCount === 0 && (
+ <div className="flex flex-col items-center justify-center py-12 px-4 text-center">
+ <div className="bg-surface p-4 rounded-full mb-4 border border-border">
+ <Clock size={32} className="text-muted" />
+ </div>
+ <h4 className="text-lg font-bold text-text mb-1">
+ {t("routes.no_service_today", "Sin servicio hoy")}
+ </h4>
+ <p className="text-sm text-muted max-w-xs">
+ {t(
+ "routes.no_service_today_desc",
+ "Este trayecto no tiene viajes programados para la fecha seleccionada."
+ )}
+ </p>
+ </div>
+ )}
+
<div className="space-y-2">
{selectedPattern?.stops.map((stop, idx) => (
<div
@@ -620,15 +705,37 @@ export default function RouteDetailsPage() {
)}
</div>
<div className="flex-1">
- <p className="font-semibold text-text text-sm">
+ <p
+ className={`font-semibold text-text text-sm ${selectedStopId === stop.id ? "text-primary" : ""}`}
+ >
{stop.name}
{stop.code && (
- <span className="text-[11px] font-normal text-gray-500 ml-2">
+ <span
+ className={`text-[11px] font-normal ml-2 ${selectedStopId === stop.id ? "text-primary/70" : "text-gray-500"}`}
+ >
{stop.code}
</span>
)}
</p>
+ {(stop.pickupType === "NONE" ||
+ stop.dropOffType === "NONE") && (
+ <div className="flex items-center gap-1.5 mt-0.5">
+ {stop.pickupType === "NONE" && (
+ <span className="inline-flex items-center gap-1 text-[10px] font-medium text-amber-600 dark:text-amber-400">
+ <ArrowDownCircle size={10} />
+ {t("routes.drop_off_only", "Solo bajada")}
+ </span>
+ )}
+ {stop.dropOffType === "NONE" && (
+ <span className="inline-flex items-center gap-1 text-[10px] font-medium text-blue-600 dark:text-blue-400">
+ <ArrowUpCircle size={10} />
+ {t("routes.pickup_only", "Solo subida")}
+ </span>
+ )}
+ </div>
+ )}
+
{selectedStopId === stop.id && (
<Link
to={`/stops/${stop.id}`}
diff --git a/src/frontend/app/routes/routes.tsx b/src/frontend/app/routes/routes.tsx
index b33fe58..128bbc4 100644
--- a/src/frontend/app/routes/routes.tsx
+++ b/src/frontend/app/routes/routes.tsx
@@ -1,16 +1,30 @@
import { useQuery } from "@tanstack/react-query";
-import { useState } from "react";
+import { ChevronDown, ChevronRight, Star } from "lucide-react";
+import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { fetchRoutes } from "~/api/transit";
import RouteIcon from "~/components/RouteIcon";
import { usePageTitle } from "~/contexts/PageTitleContext";
+import { useFavorites } from "~/hooks/useFavorites";
import "../tailwind-full.css";
export default function RoutesPage() {
const { t } = useTranslation();
usePageTitle(t("navbar.routes", "Rutas"));
const [searchQuery, setSearchQuery] = useState("");
+ const { toggleFavorite: toggleFavoriteRoute, isFavorite: isFavoriteRoute } =
+ useFavorites("favouriteRoutes");
+ const { toggleFavorite: toggleFavoriteAgency, isFavorite: isFavoriteAgency } =
+ useFavorites("favouriteAgencies");
+
+ const [expandedAgencies, setExpandedAgencies] = useState<
+ Record<string, boolean>
+ >({});
+
+ const toggleAgencyExpanded = (agency: string) => {
+ setExpandedAgencies((prev) => ({ ...prev, [agency]: !prev[agency] }));
+ };
const orderedAgencies = [
"vitrasa",
@@ -26,21 +40,50 @@ export default function RoutesPage() {
queryFn: () => fetchRoutes(orderedAgencies),
});
- const filteredRoutes = routes?.filter(
- (route) =>
- route.shortName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
- route.longName?.toLowerCase().includes(searchQuery.toLowerCase())
- );
+ const filteredRoutes = useMemo(() => {
+ return routes?.filter(
+ (route) =>
+ route.shortName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ route.longName?.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+ }, [routes, searchQuery]);
- const routesByAgency = filteredRoutes?.reduce(
- (acc, route) => {
- const agency = route.agencyName || t("routes.unknown_agency", "Otros");
- if (!acc[agency]) acc[agency] = [];
- acc[agency].push(route);
- return acc;
- },
- {} as Record<string, typeof routes>
- );
+ const routesByAgency = useMemo(() => {
+ return filteredRoutes?.reduce(
+ (acc, route) => {
+ const agency = route.agencyName || t("routes.unknown_agency", "Otros");
+ if (!acc[agency]) acc[agency] = [];
+ acc[agency].push(route);
+ return acc;
+ },
+ {} as Record<string, typeof routes>
+ );
+ }, [filteredRoutes, t]);
+
+ const sortedAgencyEntries = useMemo(() => {
+ if (!routesByAgency) return [];
+ return Object.entries(routesByAgency).sort(([a], [b]) => {
+ // First, sort by favorite status
+ const isFavA = isFavoriteAgency(a);
+ const isFavB = isFavoriteAgency(b);
+ if (isFavA && !isFavB) return -1;
+ if (!isFavA && isFavB) return 1;
+
+ // Then by fixed order
+ const indexA = orderedAgencies.indexOf(a.toLowerCase());
+ const indexB = orderedAgencies.indexOf(b.toLowerCase());
+ if (indexA === -1 && indexB === -1) {
+ return a.localeCompare(b);
+ }
+ if (indexA === -1) return 1;
+ if (indexB === -1) return -1;
+ return indexA - indexB;
+ });
+ }, [routesByAgency, orderedAgencies, isFavoriteAgency]);
+
+ const favoriteRoutes = useMemo(() => {
+ return filteredRoutes?.filter((route) => isFavoriteRoute(route.id)) || [];
+ }, [filteredRoutes, isFavoriteRoute]);
return (
<div className="container mx-auto px-4 py-6">
@@ -48,7 +91,7 @@ export default function RoutesPage() {
<input
type="text"
placeholder={t("routes.search_placeholder", "Buscar rutas...")}
- className="w-full px-4 py-3 rounded-xl border border-border bg-surface text-text focus:outline-none focus:ring-2 focus:ring-primary shadow-sm placeholder-gray-500"
+ className="w-full rounded-xl border border-border bg-surface px-4 py-3 text-text placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-primary"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
@@ -60,47 +103,118 @@ export default function RoutesPage() {
</div>
)}
- <div className="space-y-8">
- {routesByAgency &&
- Object.entries(routesByAgency)
- .sort(([a], [b]) => {
- const indexA = orderedAgencies.indexOf(a.toLowerCase());
- const indexB = orderedAgencies.indexOf(b.toLowerCase());
- if (indexA === -1 && indexB === -1) {
- return a.localeCompare(b);
- }
- if (indexA === -1) return 1;
- if (indexB === -1) return -1;
- return indexA - indexB;
- })
- .map(([agency, agencyRoutes]) => (
- <div key={agency}>
- <h2 className="text-xl font-bold text-text mb-4 border-b border-border pb-2">
- {agency}
- </h2>
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+ <div className="space-y-3">
+ {favoriteRoutes.length > 0 && !searchQuery && (
+ <div className="mb-2">
+ <h2 className="mb-3 flex items-center gap-2 border-b border-border pb-2 text-sm font-semibold uppercase tracking-wide text-muted">
+ <Star size={16} className="fill-yellow-500 text-yellow-500" />
+ {t("routes.favorites", "Favoritas")}
+ </h2>
+ <div className="space-y-2">
+ {favoriteRoutes.map((route) => (
+ <div
+ key={`fav-${route.id}`}
+ className="rounded-xl border border-border bg-surface"
+ >
+ <Link
+ to={`/routes/${route.id}`}
+ className="flex items-center gap-3 px-4 py-3"
+ >
+ <RouteIcon
+ line={route.shortName ?? "?"}
+ mode="pill"
+ colour={route.color ?? undefined}
+ textColour={route.textColor ?? undefined}
+ />
+ <div className="flex-1 min-w-0">
+ <p className="truncate text-sm font-medium text-text">
+ {route.longName}
+ </p>
+ </div>
+ </Link>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {sortedAgencyEntries.map(([agency, agencyRoutes]) => {
+ const isFav = isFavoriteAgency(agency);
+ const isExpanded = searchQuery
+ ? true
+ : (expandedAgencies[agency] ?? false);
+
+ return (
+ <div
+ key={agency}
+ className="overflow-hidden rounded-xl border border-border bg-surface"
+ >
+ <div
+ className={`flex items-center justify-between px-4 py-3 select-none ${isExpanded ? "border-b border-border" : ""}`}
+ >
+ <button
+ type="button"
+ onClick={() => toggleAgencyExpanded(agency)}
+ className="flex flex-1 items-center gap-3 text-left"
+ >
+ <div className="text-muted">
+ {isExpanded ? (
+ <ChevronDown size={18} />
+ ) : (
+ <ChevronRight size={18} />
+ )}
+ </div>
+ <h2 className="text-base font-semibold text-text">
+ {agency}
+ </h2>
+ <span className="rounded-full bg-background px-2 py-0.5 text-xs text-muted">
+ {agencyRoutes.length}
+ </span>
+ </button>
+ <button
+ type="button"
+ onClick={() => toggleFavoriteAgency(agency)}
+ className={`rounded-full p-2 transition-colors ${
+ isFav
+ ? "text-yellow-500"
+ : "text-muted hover:text-yellow-500"
+ }`}
+ aria-label={t(
+ "routes.toggle_favorite_agency",
+ "Alternar agencia favorita"
+ )}
+ >
+ <Star size={16} className={isFav ? "fill-current" : ""} />
+ </button>
+ </div>
+
+ {isExpanded && (
+ <div className="space-y-1 px-3 py-2">
{agencyRoutes.map((route) => (
- <Link
- key={route.id}
- to={`/routes/${route.id}`}
- className="flex items-center gap-3 p-4 bg-surface rounded-lg shadow hover:shadow-lg transition-shadow border border-border"
- >
- <RouteIcon
- line={route.shortName ?? "?"}
- mode="pill"
- colour={route.color ?? undefined}
- textColour={route.textColor ?? undefined}
- />
- <div className="flex-1 min-w-0">
- <p className="text-sm md:text-md font-semibold text-text">
- {route.longName}
- </p>
- </div>
- </Link>
+ <div key={route.id} className="rounded-lg">
+ <Link
+ to={`/routes/${route.id}`}
+ className="flex items-center gap-3 rounded-lg px-3 py-2.5 hover:bg-background"
+ >
+ <RouteIcon
+ line={route.shortName ?? "?"}
+ mode="pill"
+ colour={route.color ?? undefined}
+ textColour={route.textColor ?? undefined}
+ />
+ <div className="flex-1 min-w-0">
+ <p className="truncate text-sm font-medium text-text">
+ {route.longName}
+ </p>
+ </div>
+ </Link>
+ </div>
))}
</div>
- </div>
- ))}
+ )}
+ </div>
+ );
+ })}
</div>
</div>
);