|
@@ -0,0 +1,107 @@
|
|
|
|
|
+import {
|
|
|
|
|
+ branchPath,
|
|
|
|
|
+ yearPath,
|
|
|
|
|
+ monthPath,
|
|
|
|
|
+ dayPath,
|
|
|
|
|
+} from "@/lib/frontend/routes";
|
|
|
|
|
+import { formatMonthLabel } from "@/lib/frontend/explorer/formatters";
|
|
|
|
|
+import { sortNumericStringsDesc } from "@/lib/frontend/explorer/sorters";
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * normalizeNumericOptions
|
|
|
|
|
+ *
|
|
|
|
|
+ * Pure helper:
|
|
|
|
|
+ * - Ensures array input
|
|
|
|
|
+ * - Filters empty values
|
|
|
|
|
+ * - Deduplicates
|
|
|
|
|
+ * - Sorts descending (latest first) for numeric strings
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param {string[]|null|undefined} options
|
|
|
|
|
+ * @returns {string[]|null}
|
|
|
|
|
+ */
|
|
|
|
|
+export function normalizeNumericOptions(options) {
|
|
|
|
|
+ if (!Array.isArray(options) || options.length === 0) return null;
|
|
|
|
|
+
|
|
|
|
|
+ const unique = Array.from(
|
|
|
|
|
+ new Set(options.map((x) => String(x)).filter(Boolean))
|
|
|
|
|
+ );
|
|
|
|
|
+ if (unique.length === 0) return null;
|
|
|
|
|
+
|
|
|
|
|
+ return sortNumericStringsDesc(unique);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * buildExplorerDropdownItems
|
|
|
|
|
+ *
|
|
|
|
|
+ * Builds dropdown menu items for year/month/day breadcrumb segments.
|
|
|
|
|
+ * This function is intentionally pure and React-free to keep it testable.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param {{
|
|
|
|
|
+ * branch: string,
|
|
|
|
|
+ * year?: string|null,
|
|
|
|
|
+ * month?: string|null,
|
|
|
|
|
+ * day?: string|null,
|
|
|
|
|
+ * yearOptions?: string[]|null,
|
|
|
|
|
+ * monthOptions?: string[]|null,
|
|
|
|
|
+ * dayOptions?: string[]|null
|
|
|
|
|
+ * }} input
|
|
|
|
|
+ * @returns {{
|
|
|
|
|
+ * yearItems: Array<{ value: string, label: string, href: string }> | null,
|
|
|
|
|
+ * monthItems: Array<{ value: string, label: string, href: string }> | null,
|
|
|
|
|
+ * dayItems: Array<{ value: string, label: string, href: string }> | null
|
|
|
|
|
+ * }}
|
|
|
|
|
+ */
|
|
|
|
|
+export function buildExplorerDropdownItems({
|
|
|
|
|
+ branch,
|
|
|
|
|
+ year = null,
|
|
|
|
|
+ month = null,
|
|
|
|
|
+ day = null,
|
|
|
|
|
+ yearOptions = null,
|
|
|
|
|
+ monthOptions = null,
|
|
|
|
|
+ dayOptions = null,
|
|
|
|
|
+}) {
|
|
|
|
|
+ const years = normalizeNumericOptions(yearOptions);
|
|
|
|
|
+ const months = normalizeNumericOptions(monthOptions);
|
|
|
|
|
+ const days = normalizeNumericOptions(dayOptions);
|
|
|
|
|
+
|
|
|
|
|
+ const yearItems =
|
|
|
|
|
+ year && years
|
|
|
|
|
+ ? years.map((y) => ({
|
|
|
|
|
+ value: y,
|
|
|
|
|
+ label: y,
|
|
|
|
|
+ href: yearPath(branch, y),
|
|
|
|
|
+ }))
|
|
|
|
|
+ : null;
|
|
|
|
|
+
|
|
|
|
|
+ const monthItems =
|
|
|
|
|
+ year && month && months
|
|
|
|
|
+ ? months.map((m) => ({
|
|
|
|
|
+ value: m,
|
|
|
|
|
+ label: formatMonthLabel(m),
|
|
|
|
|
+ href: monthPath(branch, year, m),
|
|
|
|
|
+ }))
|
|
|
|
|
+ : null;
|
|
|
|
|
+
|
|
|
|
|
+ const dayItems =
|
|
|
|
|
+ year && month && day && days
|
|
|
|
|
+ ? days.map((d) => ({
|
|
|
|
|
+ value: d,
|
|
|
|
|
+ label: d,
|
|
|
|
|
+ href: dayPath(branch, year, month, d),
|
|
|
|
|
+ }))
|
|
|
|
|
+ : null;
|
|
|
|
|
+
|
|
|
|
|
+ return { yearItems, monthItems, dayItems };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * buildBranchCrumbHref
|
|
|
|
|
+ *
|
|
|
|
|
+ * Tiny helper to keep the breadcrumb component simple.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param {string} branch
|
|
|
|
|
+ * @returns {string}
|
|
|
|
|
+ */
|
|
|
|
|
+export function buildBranchCrumbHref(branch) {
|
|
|
|
|
+ return branchPath(branch);
|
|
|
|
|
+}
|