feat: add transcripts with audio links
This commit is contained in:
+12
-8
@@ -7,6 +7,7 @@ import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { TextSelectionToolbar } from "@/components/text-selection-toolbar";
|
||||
import { TextHighlighter } from "@/components/text-highlighter";
|
||||
import { AudioPlayerProvider, FloatingPlayer } from "@/components/audio-player";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
@@ -44,14 +45,17 @@ export default function RootLayout({
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<TooltipProvider>
|
||||
{children}
|
||||
<TextSelectionToolbar />
|
||||
<Suspense fallback={null}>
|
||||
<TextHighlighter />
|
||||
</Suspense>
|
||||
<SiteFooter />
|
||||
</TooltipProvider>
|
||||
<AudioPlayerProvider>
|
||||
<TooltipProvider>
|
||||
{children}
|
||||
<TextSelectionToolbar />
|
||||
<Suspense fallback={null}>
|
||||
<TextHighlighter />
|
||||
</Suspense>
|
||||
<FloatingPlayer />
|
||||
<SiteFooter />
|
||||
</TooltipProvider>
|
||||
</AudioPlayerProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+8
-15
@@ -44,15 +44,13 @@ export default function Home() {
|
||||
Structured verse-by-verse editions
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant="secondary">Recommended</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Bhagavad-gītā, Śrīmad-Bhāgavatam, and Caitanya-caritāmṛta with
|
||||
Devanagari, Bengali, transliteration, synonyms, translations
|
||||
& purports. Extraction is still in progress but offers a
|
||||
refined, searchable reading experience.
|
||||
Bhagavad-gītā, Śrīmad-Bhāgavatam, Caitanya-caritāmṛta and
|
||||
other books with Devanagari, Bengali, transliteration,
|
||||
synonyms, translations & purports.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -69,7 +67,7 @@ export default function Home() {
|
||||
<div className="flex-1 min-w-0">
|
||||
<CardTitle className="text-xl font-bold">Essays</CardTitle>
|
||||
<CardDescription>
|
||||
Standalone essays by Śrīla Prabhupāda
|
||||
Essays by Śrīla Prabhupāda
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,18 +94,15 @@ export default function Home() {
|
||||
<CardTitle className="text-xl font-bold">
|
||||
Magazines
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Back to Godhead & other periodicals
|
||||
</CardDescription>
|
||||
<CardDescription>Back to Godhead magazines</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
The original Back to Godhead magazine founded by Śrīla
|
||||
Prabhupāda in 1944 — 29 issues across 3 volumes with 89
|
||||
articles on Kṛṣṇa consciousness, Vedic philosophy, and
|
||||
spiritual life.
|
||||
Prabhupāda with articles on Kṛṣṇa consciousness, Vedic
|
||||
philosophy, and spiritual life.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -155,7 +150,6 @@ export default function Home() {
|
||||
Lectures & conversations
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant="secondary">Coming Soon</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -274,8 +268,7 @@ export default function Home() {
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Miscellaneous documents, analyses, and writings by Śrīla
|
||||
Prabhupāda that don't fit neatly into the books or essays
|
||||
collections — including structured analyses with verse
|
||||
references.
|
||||
collections.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
getTranscription,
|
||||
getAdjacentTranscriptions,
|
||||
} from "@/lib/transcriptions";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChevronLeft, ChevronRight, Clock, MapPin, Users } from "lucide-react";
|
||||
import { PlayAudioButton } from "./play-audio-button";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const t = await getTranscription(slug);
|
||||
if (!t) return {};
|
||||
|
||||
const detail = [t.date, t.place].filter(Boolean).join(" · ");
|
||||
return buildOgMeta({
|
||||
title: t.name,
|
||||
detail: detail || undefined,
|
||||
quote:
|
||||
t.content.length > 0
|
||||
? t.content
|
||||
.find((b) => b.speaker === "Prabhupāda" && b.text.length > 50)
|
||||
?.text?.replace(/<[^>]+>/g, "")
|
||||
.slice(0, 200)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function TranscriptionPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const t = await getTranscription(slug);
|
||||
if (!t) notFound();
|
||||
|
||||
const { prev, next } = await getAdjacentTranscriptions(slug, t.year);
|
||||
|
||||
const backHref = t.year
|
||||
? `/transcriptions/year/${t.year}`
|
||||
: "/transcriptions";
|
||||
const backLabel = t.year ? `Transcriptions from ${t.year}` : "Transcriptions";
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href={backHref}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
← {backLabel}
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">{t.name}</h1>
|
||||
<div className="mt-2 flex items-center gap-3 flex-wrap text-sm text-muted-foreground">
|
||||
<Badge variant={t.type === "lecture" ? "default" : "secondary"}>
|
||||
{t.type === "lecture" ? "Lecture" : "Conversation"}
|
||||
</Badge>
|
||||
{t.date && <span>{t.date}</span>}
|
||||
{t.place && (
|
||||
<span className="flex items-center gap-1">
|
||||
<MapPin className="size-3.5" />
|
||||
{t.place}
|
||||
</span>
|
||||
)}
|
||||
{t.reference && (
|
||||
<span className="text-primary font-medium">{t.reference}</span>
|
||||
)}
|
||||
{t.durationMinutes && t.durationMinutes > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="size-3.5" />
|
||||
{t.durationMinutes} min
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-6 py-8 space-y-8">
|
||||
{/* Top nav */}
|
||||
<TranscriptionNav prev={prev} next={next} />
|
||||
|
||||
{/* Audio + Participants row */}
|
||||
<div className="w-full flex flex-col md:flex-row items-start md:items-end justify-between gap-4">
|
||||
{/* Participants */}
|
||||
{t.participants.length > 0 && (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex pl-2 items-center gap-2 flex-wrap text-sm text-muted-foreground">
|
||||
<Users className="size-3.5" />
|
||||
<span>Participants:</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap text-sm text-muted-foreground">
|
||||
{t.participants.map((p, i) => (
|
||||
<Badge
|
||||
key={i}
|
||||
variant="outline"
|
||||
className="text-xs font-normal"
|
||||
>
|
||||
{p}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Play Audio button */}
|
||||
{t.audioUrl && (
|
||||
<div className="w-full md:w-auto flex justify-end">
|
||||
<PlayAudioButton
|
||||
audioUrl={t.audioUrl}
|
||||
title={t.name}
|
||||
subtitle={[t.date, t.place].filter(Boolean).join(" · ")}
|
||||
sourceUrl={`/transcriptions/${t.slug}`}
|
||||
type={t.type}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Transcription content */}
|
||||
{t.content.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center text-muted-foreground">
|
||||
<p className="text-lg font-medium">No transcription available</p>
|
||||
<p className="text-sm mt-1">
|
||||
This recording has not been transcribed into text.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-3">
|
||||
{t.content.map((block, i) => (
|
||||
<ContentBlockView key={i} block={block} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Bottom nav */}
|
||||
<TranscriptionNav prev={prev} next={next} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ContentBlockView({
|
||||
block,
|
||||
}: {
|
||||
block: {
|
||||
text: string;
|
||||
format: string;
|
||||
alignment: string;
|
||||
speaker: string | null;
|
||||
};
|
||||
}) {
|
||||
const isVerse = block.format === "verse";
|
||||
const isQuote = block.format === "quote";
|
||||
const isCenter = block.alignment === "center";
|
||||
|
||||
const className = cn(
|
||||
"text-base leading-relaxed",
|
||||
isVerse && "text-center italic my-4",
|
||||
isQuote && "border-l-2 border-primary/30 pl-4 text-muted-foreground italic",
|
||||
!isVerse && !isQuote && isCenter && "text-center",
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{block.speaker && (
|
||||
<span className="font-bold text-foreground">{block.speaker}: </span>
|
||||
)}
|
||||
<span dangerouslySetInnerHTML={{ __html: block.text }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TranscriptionNav({
|
||||
prev,
|
||||
next,
|
||||
}: {
|
||||
prev: { slug: string; name: string } | null;
|
||||
next: { slug: string; name: string } | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
{prev ? (
|
||||
<Link
|
||||
href={`/transcriptions/${prev.slug}`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline", size: "sm" }),
|
||||
"gap-1.5",
|
||||
)}
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
<span className="hidden sm:inline truncate max-w-[180px]">
|
||||
{prev.name}
|
||||
</span>
|
||||
<span className="sm:hidden">Previous</span>
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
||||
{next ? (
|
||||
<Link
|
||||
href={`/transcriptions/${next.slug}`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline", size: "sm" }),
|
||||
"gap-1.5",
|
||||
)}
|
||||
>
|
||||
<span className="hidden sm:inline truncate max-w-[180px]">
|
||||
{next.name}
|
||||
</span>
|
||||
<span className="sm:hidden">Next</span>
|
||||
<ChevronRight className="size-4" />
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { useAudioPlayer, type AudioTrack } from "@/components/audio-player";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Play, Pause, Radio } from "lucide-react";
|
||||
|
||||
type PlayAudioButtonProps = {
|
||||
audioUrl: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
sourceUrl: string;
|
||||
type?: string;
|
||||
className?: string;
|
||||
/** Compact icon-only variant for list items */
|
||||
variant?: "default" | "compact";
|
||||
};
|
||||
|
||||
export function PlayAudioButton({
|
||||
audioUrl,
|
||||
title,
|
||||
subtitle,
|
||||
sourceUrl,
|
||||
type,
|
||||
className,
|
||||
variant = "default",
|
||||
}: PlayAudioButtonProps) {
|
||||
const { currentTrack, isPlaying, play, togglePlayPause } = useAudioPlayer();
|
||||
|
||||
const isThisTrack = currentTrack?.url === audioUrl;
|
||||
const isThisPlaying = isThisTrack && isPlaying;
|
||||
|
||||
function handleClick() {
|
||||
if (isThisTrack) {
|
||||
togglePlayPause();
|
||||
} else {
|
||||
const track: AudioTrack = {
|
||||
url: audioUrl,
|
||||
title,
|
||||
subtitle,
|
||||
sourceUrl,
|
||||
type: type ?? "lecture",
|
||||
};
|
||||
play(track);
|
||||
}
|
||||
}
|
||||
|
||||
if (variant === "compact") {
|
||||
return (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleClick();
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-center size-8 rounded-full transition-all shrink-0 cursor-pointer!",
|
||||
isThisPlaying
|
||||
? "bg-primary text-primary-foreground shadow-sm"
|
||||
: "bg-primary/10 text-primary hover:bg-primary/20",
|
||||
className,
|
||||
)}
|
||||
title={isThisPlaying ? "Pause" : "Play audio"}
|
||||
aria-label={isThisPlaying ? "Pause" : "Play audio"}
|
||||
>
|
||||
{isThisPlaying ? (
|
||||
<Pause className="size-3.5" />
|
||||
) : (
|
||||
<Play className="size-3.5 ml-0.5" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
"inline-flex cursor-pointer items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-semibold transition-all shadow-sm",
|
||||
isThisPlaying
|
||||
? "bg-primary text-primary-foreground shadow-md"
|
||||
: "bg-primary text-primary-foreground hover:bg-primary/90 hover:shadow-md",
|
||||
className,
|
||||
)}
|
||||
aria-label={isThisPlaying ? "Pause audio" : "Play audio"}
|
||||
>
|
||||
{isThisPlaying ? (
|
||||
<>
|
||||
<Radio className="size-4 animate-pulse" />
|
||||
<span>Now Playing</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="size-4" />
|
||||
<span>Play Audio</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
+36
-24
@@ -1,19 +1,41 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { Mic, Clock } from "lucide-react";
|
||||
import {
|
||||
getTranscriptionYears,
|
||||
getTranscriptionPlaces,
|
||||
getTotalTranscriptionCount,
|
||||
getTranscriptionIndex,
|
||||
} from "@/lib/transcriptions";
|
||||
import { TranscriptionsNav } from "./transcriptions-nav";
|
||||
import { TranscriptionsBrowser } from "./transcriptions-browser";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
return buildOgMeta({
|
||||
title: "Transcriptions",
|
||||
detail: "Lectures & conversations",
|
||||
detail: "3,700+ lectures & conversations (1966–1977)",
|
||||
quote:
|
||||
"Transcriptions of Śrīla Prabhupāda's lectures, morning walks, room conversations, and dialogues — the living voice of the Founder-Ācārya preserved in text.",
|
||||
});
|
||||
}
|
||||
|
||||
export default function TranscriptionsPage() {
|
||||
export default async function TranscriptionsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ view?: string }>;
|
||||
}) {
|
||||
const { view = "year" } = await searchParams;
|
||||
|
||||
const [years, places, totalCount, index] = await Promise.all([
|
||||
getTranscriptionYears(),
|
||||
getTranscriptionPlaces(),
|
||||
getTotalTranscriptionCount(),
|
||||
getTranscriptionIndex(),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<PageHeader>
|
||||
@@ -27,31 +49,21 @@ export default function TranscriptionsPage() {
|
||||
Transcriptions
|
||||
</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Lectures & conversations of Śrīla Prabhupāda
|
||||
{totalCount.toLocaleString()} lectures & conversations of Śrīla
|
||||
Prabhupāda (1966–1977)
|
||||
</p>
|
||||
</PageHeader>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-6 py-8">
|
||||
<div className="flex flex-col items-center justify-center py-24 text-center">
|
||||
<div className="flex items-center justify-center size-20 rounded-2xl bg-primary/10 text-primary mb-6">
|
||||
<Mic className="size-10" />
|
||||
</div>
|
||||
<main className="max-w-4xl mx-auto px-6 py-8 space-y-6">
|
||||
<TranscriptionsNav currentView={view} />
|
||||
|
||||
<h2 className="text-2xl font-bold text-foreground mb-2">
|
||||
Coming Soon
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground max-w-md mb-8">
|
||||
We are working on indexing thousands of transcriptions of Śrīla
|
||||
Prabhupāda's lectures, morning walks, room conversations, and
|
||||
dialogues into our database. This section will be available shortly.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground/60">
|
||||
<Clock className="size-4" />
|
||||
<span>Under active development</span>
|
||||
</div>
|
||||
</div>
|
||||
<TranscriptionsBrowser
|
||||
currentView={view}
|
||||
years={years}
|
||||
places={places}
|
||||
lectureCount={index.typeDistribution.lecture}
|
||||
conversationCount={index.typeDistribution.conversation}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getTranscriptionsByPlace } from "@/lib/transcriptions";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Clock } from "lucide-react";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ place: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { place: encodedPlace } = await params;
|
||||
const place = decodeURIComponent(encodedPlace);
|
||||
const transcriptions = await getTranscriptionsByPlace(place);
|
||||
return buildOgMeta({
|
||||
title: "Transcriptions",
|
||||
detail: `${transcriptions.length} transcriptions from ${place}`,
|
||||
quote: `Lectures and conversations of Śrīla Prabhupāda in ${place}.`,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function TranscriptionsByPlacePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ place: string }>;
|
||||
}) {
|
||||
const { place: encodedPlace } = await params;
|
||||
const place = decodeURIComponent(encodedPlace);
|
||||
|
||||
const transcriptions = await getTranscriptionsByPlace(place);
|
||||
if (transcriptions.length === 0) notFound();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/transcriptions?view=place"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
← Transcriptions by Place
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
Transcriptions in {place}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{transcriptions.length} transcriptions
|
||||
</p>
|
||||
</PageHeader>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-6 py-8">
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-0 divide-y divide-border">
|
||||
{transcriptions.map((t) => (
|
||||
<Link
|
||||
key={t.slug}
|
||||
href={`/transcriptions/${t.slug}`}
|
||||
className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-5 py-3.5 gap-1 sm:gap-4 hover:bg-muted transition-colors"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<p className="font-medium text-foreground text-sm">
|
||||
{t.name}
|
||||
</p>
|
||||
<Badge
|
||||
variant={t.type === "lecture" ? "default" : "secondary"}
|
||||
className="text-[10px] px-1.5 py-0"
|
||||
>
|
||||
{t.type === "lecture" ? "Lecture" : "Conversation"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
|
||||
{t.date && <span>{t.date}</span>}
|
||||
{t.reference && (
|
||||
<span className="text-primary/70">{t.reference}</span>
|
||||
)}
|
||||
{t.durationMinutes && t.durationMinutes > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="size-3" />
|
||||
{t.durationMinutes} min
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useRef, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Search,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ArrowUpDown,
|
||||
ArrowDownAZ,
|
||||
ArrowUpAZ,
|
||||
Hash,
|
||||
Check,
|
||||
BookOpen,
|
||||
MessageCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type YearGroup = { year: number; count: number };
|
||||
export type PlaceGroup = { name: string; count: number };
|
||||
|
||||
type SortMode = "count" | "asc" | "desc";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Diacritic-aware normalize (matches books-grid pattern)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function normalize(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/ṛ/g, "r")
|
||||
.replace(/ṝ/g, "r")
|
||||
.replace(/ṣ/g, "s")
|
||||
.replace(/ś/g, "s")
|
||||
.replace(/ṭ/g, "t")
|
||||
.replace(/ḍ/g, "d")
|
||||
.replace(/ṇ/g, "n")
|
||||
.replace(/ñ/g, "n")
|
||||
.replace(/ṅ/g, "n")
|
||||
.replace(/ḥ/g, "h")
|
||||
.replace(/ṁ/g, "m")
|
||||
.replace(/ṃ/g, "m")
|
||||
.replace(/ḷ/g, "l")
|
||||
.replace(/ā/g, "a")
|
||||
.replace(/ī/g, "i")
|
||||
.replace(/ū/g, "u")
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sort Popover
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SORT_OPTIONS: { key: SortMode; label: string; icon: typeof Hash }[] = [
|
||||
{ key: "count", label: "Most transcriptions first", icon: Hash },
|
||||
{ key: "asc", label: "A → Z", icon: ArrowDownAZ },
|
||||
{ key: "desc", label: "Z → A", icon: ArrowUpAZ },
|
||||
];
|
||||
|
||||
const SORT_OPTIONS_YEAR: { key: SortMode; label: string; icon: typeof Hash }[] =
|
||||
[
|
||||
{ key: "count", label: "Most transcriptions first", icon: Hash },
|
||||
{ key: "asc", label: "Oldest first", icon: ArrowDownAZ },
|
||||
{ key: "desc", label: "Newest first", icon: ArrowUpAZ },
|
||||
];
|
||||
|
||||
function SortPopover({
|
||||
sort,
|
||||
onSortChange,
|
||||
isYear,
|
||||
}: {
|
||||
sort: SortMode;
|
||||
onSortChange: (s: SortMode) => void;
|
||||
isYear?: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close on outside click
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, [open]);
|
||||
|
||||
const options = isYear ? SORT_OPTIONS_YEAR : SORT_OPTIONS;
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className={cn(
|
||||
"flex items-center justify-center size-10 rounded-lg border transition-colors",
|
||||
open
|
||||
? "bg-primary/10 border-primary/30 text-primary"
|
||||
: "border-border text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
title="Sort options"
|
||||
aria-label="Sort options"
|
||||
>
|
||||
<ArrowUpDown className="size-4" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute right-0 top-12 z-50 w-52 rounded-lg border border-border bg-card shadow-lg py-1 animate-in fade-in-0 zoom-in-95">
|
||||
{options.map((opt) => {
|
||||
const Icon = opt.icon;
|
||||
const isActive = sort === opt.key;
|
||||
return (
|
||||
<button
|
||||
key={opt.key}
|
||||
onClick={() => {
|
||||
onSortChange(opt.key);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-3 w-full px-3 py-2.5 text-sm transition-colors text-left",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary font-medium"
|
||||
: "text-foreground hover:bg-muted",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4 shrink-0" />
|
||||
<span className="flex-1">{opt.label}</span>
|
||||
{isActive && <Check className="size-3.5 shrink-0" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pagination
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const PAGE_SIZE_PLACES = 60;
|
||||
const PAGE_SIZE_YEARS = 100;
|
||||
|
||||
function Pagination({
|
||||
page,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
}: {
|
||||
page: number;
|
||||
totalPages: number;
|
||||
onPageChange: (p: number) => void;
|
||||
}) {
|
||||
if (totalPages <= 1) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-3 pt-4">
|
||||
<button
|
||||
onClick={() => onPageChange(page - 1)}
|
||||
disabled={page <= 1}
|
||||
className={cn(
|
||||
"flex items-center gap-1 px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
|
||||
page <= 1
|
||||
? "text-muted-foreground/40 cursor-not-allowed"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
Previous
|
||||
</button>
|
||||
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Page {page} of {totalPages}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
disabled={page >= totalPages}
|
||||
className={cn(
|
||||
"flex items-center gap-1 px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
|
||||
page >= totalPages
|
||||
? "text-muted-foreground/40 cursor-not-allowed"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sorting helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function sortYears(data: YearGroup[], mode: SortMode): YearGroup[] {
|
||||
const sorted = [...data];
|
||||
switch (mode) {
|
||||
case "count":
|
||||
return sorted.sort((a, b) => b.count - a.count);
|
||||
case "asc":
|
||||
return sorted.sort((a, b) => a.year - b.year);
|
||||
case "desc":
|
||||
return sorted.sort((a, b) => b.year - a.year);
|
||||
}
|
||||
}
|
||||
|
||||
function sortPlaces(data: PlaceGroup[], mode: SortMode): PlaceGroup[] {
|
||||
const sorted = [...data];
|
||||
switch (mode) {
|
||||
case "count":
|
||||
return sorted.sort((a, b) => b.count - a.count);
|
||||
case "asc":
|
||||
return sorted.sort((a, b) => a.name.localeCompare(b.name));
|
||||
case "desc":
|
||||
return sorted.sort((a, b) => b.name.localeCompare(a.name));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function TranscriptionsBrowser({
|
||||
currentView,
|
||||
years,
|
||||
places,
|
||||
lectureCount,
|
||||
conversationCount,
|
||||
}: {
|
||||
currentView: string;
|
||||
years: YearGroup[];
|
||||
places: PlaceGroup[];
|
||||
lectureCount: number;
|
||||
conversationCount: number;
|
||||
}) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [sortYear, setSortYear] = useState<SortMode>("asc");
|
||||
const [sortPlace, setSortPlace] = useState<SortMode>("count");
|
||||
|
||||
const handleQueryChange = (q: string) => {
|
||||
setQuery(q);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handleSortChange = (mode: SortMode) => {
|
||||
if (currentView === "year") setSortYear(mode);
|
||||
else setSortPlace(mode);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const currentSort = currentView === "year" ? sortYear : sortPlace;
|
||||
|
||||
// --- Filtered + sorted data ---
|
||||
|
||||
const filteredYears = useMemo(() => {
|
||||
let data = years;
|
||||
if (query.trim()) {
|
||||
const q = normalize(query.trim());
|
||||
data = data.filter((yg) => String(yg.year).includes(q));
|
||||
}
|
||||
return sortYears(data, sortYear);
|
||||
}, [years, query, sortYear]);
|
||||
|
||||
const filteredPlaces = useMemo(() => {
|
||||
let data = places;
|
||||
if (query.trim()) {
|
||||
const q = normalize(query.trim());
|
||||
data = data.filter((pg) => normalize(pg.name).includes(q));
|
||||
}
|
||||
return sortPlaces(data, sortPlace);
|
||||
}, [places, query, sortPlace]);
|
||||
|
||||
// --- Paginated data ---
|
||||
|
||||
const activeData =
|
||||
currentView === "year" ? filteredYears : filteredPlaces;
|
||||
|
||||
const pageSize =
|
||||
currentView === "year" ? PAGE_SIZE_YEARS : PAGE_SIZE_PLACES;
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(activeData.length / pageSize));
|
||||
const safePage = Math.min(page, totalPages);
|
||||
const startIdx = (safePage - 1) * pageSize;
|
||||
const endIdx = startIdx + pageSize;
|
||||
|
||||
const placeholder =
|
||||
currentView === "year" ? "Search years..." : "Search places...";
|
||||
|
||||
const resultCount = activeData.length;
|
||||
const totalCount =
|
||||
currentView === "year" ? years.length : places.length;
|
||||
|
||||
// --- Type view (no search/sort/pagination needed) ---
|
||||
|
||||
if (currentView === "type") {
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Link href="/transcriptions/type/lecture">
|
||||
<Card className="transition-all hover:shadow-md hover:ring-foreground/20">
|
||||
<CardContent className="py-8 px-6 text-center">
|
||||
<BookOpen className="size-8 mx-auto mb-3 text-primary" />
|
||||
<p className="text-xl font-bold text-foreground">Lectures</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{lectureCount.toLocaleString()} transcriptions
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/60 mt-2">
|
||||
Bhagavad-gītā, Śrīmad-Bhāgavatam, Caitanya-caritāmṛta &
|
||||
more
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
<Link href="/transcriptions/type/conversation">
|
||||
<Card className="transition-all hover:shadow-md hover:ring-foreground/20">
|
||||
<CardContent className="py-8 px-6 text-center">
|
||||
<MessageCircle className="size-8 mx-auto mb-3 text-primary" />
|
||||
<p className="text-xl font-bold text-foreground">Conversations</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{conversationCount.toLocaleString()} transcriptions
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/60 mt-2">
|
||||
Morning walks, room conversations, interviews & more
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Year / Place views ---
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Search + Sort */}
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
value={query}
|
||||
onChange={(e) => handleQueryChange(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<SortPopover
|
||||
sort={currentSort}
|
||||
onSortChange={handleSortChange}
|
||||
isYear={currentView === "year"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Result count */}
|
||||
{query.trim() && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{resultCount === 0
|
||||
? "No results found"
|
||||
: `${resultCount} of ${totalCount} ${currentView === "year" ? "years" : "places"}`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Year view */}
|
||||
{currentView === "year" && (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
||||
{(filteredYears.slice(startIdx, endIdx) as YearGroup[]).map((yg) => (
|
||||
<Link key={yg.year} href={`/transcriptions/year/${yg.year}`}>
|
||||
<Card className="transition-all hover:shadow-md hover:ring-foreground/20">
|
||||
<CardContent className="py-4 px-4 text-center">
|
||||
<p className="text-lg font-bold text-foreground">{yg.year}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{yg.count}{" "}
|
||||
{yg.count === 1 ? "transcription" : "transcriptions"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Place view */}
|
||||
{currentView === "place" && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{(filteredPlaces.slice(startIdx, endIdx) as PlaceGroup[]).map(
|
||||
(pg) => (
|
||||
<Link
|
||||
key={pg.name}
|
||||
href={`/transcriptions/place/${encodeURIComponent(pg.name)}`}
|
||||
>
|
||||
<Card className="transition-all hover:shadow-md hover:ring-foreground/20">
|
||||
<CardContent className="py-4 px-4 text-center">
|
||||
<p className="font-medium text-foreground">{pg.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{pg.count}{" "}
|
||||
{pg.count === 1 ? "transcription" : "transcriptions"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
<Pagination
|
||||
page={safePage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Calendar, MapPin, Mic } from "lucide-react";
|
||||
|
||||
const views = [
|
||||
{ key: "year", label: "Year", icon: Calendar },
|
||||
{ key: "place", label: "Place", icon: MapPin },
|
||||
{ key: "type", label: "Type", icon: Mic },
|
||||
];
|
||||
|
||||
export function TranscriptionsNav({ currentView }: { currentView: string }) {
|
||||
return (
|
||||
<div className="flex gap-2 border-b border-border pb-3">
|
||||
{views.map((v) => {
|
||||
const Icon = v.icon;
|
||||
const isActive = currentView === v.key;
|
||||
return (
|
||||
<Link
|
||||
key={v.key}
|
||||
href={`/transcriptions?view=${v.key}`}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
{v.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getTranscriptionsByType } from "@/lib/transcriptions";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { TranscriptionTypeBrowser } from "./type-browser";
|
||||
|
||||
const TYPE_LABELS: Record<string, string> = {
|
||||
lecture: "Lectures",
|
||||
conversation: "Conversations",
|
||||
};
|
||||
|
||||
const TYPE_DESCRIPTIONS: Record<string, string> = {
|
||||
lecture: "Bhagavad-gītā, Śrīmad-Bhāgavatam, Caitanya-caritāmṛta & more",
|
||||
conversation: "Morning walks, room conversations, interviews & more",
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ type: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { type } = await params;
|
||||
if (!TYPE_LABELS[type]) return {};
|
||||
const transcriptions = await getTranscriptionsByType(type);
|
||||
return buildOgMeta({
|
||||
title: TYPE_LABELS[type],
|
||||
detail: `${transcriptions.length} transcriptions`,
|
||||
quote: `${TYPE_LABELS[type]} of Śrīla Prabhupāda — ${TYPE_DESCRIPTIONS[type]}.`,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function TranscriptionsByTypePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ type: string }>;
|
||||
}) {
|
||||
const { type } = await params;
|
||||
if (!TYPE_LABELS[type]) notFound();
|
||||
|
||||
const transcriptions = await getTranscriptionsByType(type);
|
||||
if (transcriptions.length === 0) notFound();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<TranscriptionTypeBrowser
|
||||
transcriptions={transcriptions}
|
||||
type={type}
|
||||
typeLabel={TYPE_LABELS[type]}
|
||||
totalCount={transcriptions.length}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { ReadingSettings } from "@/components/reading-settings";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChevronLeft, ChevronRight, Menu, Clock, MapPin } from "lucide-react";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type TranscriptionItem = {
|
||||
slug: string;
|
||||
name: string;
|
||||
type: string;
|
||||
date: string | null;
|
||||
year: number | null;
|
||||
month: number | null;
|
||||
day: number | null;
|
||||
place: string | null;
|
||||
context: string | null;
|
||||
reference: string | null;
|
||||
durationMinutes: number | null;
|
||||
participants: string[];
|
||||
};
|
||||
|
||||
type YearGroup = {
|
||||
year: number;
|
||||
label: string;
|
||||
count: number;
|
||||
transcriptions: TranscriptionItem[];
|
||||
};
|
||||
|
||||
type MonthGroup = {
|
||||
month: number;
|
||||
label: string;
|
||||
transcriptions: TranscriptionItem[];
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const MONTH_NAMES = [
|
||||
"",
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Grouping helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildYearGroups(transcriptions: TranscriptionItem[]): YearGroup[] {
|
||||
const map = new Map<number, TranscriptionItem[]>();
|
||||
|
||||
for (const t of transcriptions) {
|
||||
const y = t.year ?? 0;
|
||||
if (!map.has(y)) map.set(y, []);
|
||||
map.get(y)!.push(t);
|
||||
}
|
||||
|
||||
const groups: YearGroup[] = [];
|
||||
|
||||
const years = Array.from(map.keys()).sort((a, b) => a - b);
|
||||
for (const y of years) {
|
||||
if (y === 0) continue;
|
||||
const items = map.get(y)!;
|
||||
groups.push({
|
||||
year: y,
|
||||
label: String(y),
|
||||
count: items.length,
|
||||
transcriptions: items,
|
||||
});
|
||||
}
|
||||
const undated = map.get(0);
|
||||
if (undated && undated.length > 0) {
|
||||
groups.push({
|
||||
year: 0,
|
||||
label: "Undated",
|
||||
count: undated.length,
|
||||
transcriptions: undated,
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function buildMonthGroups(transcriptions: TranscriptionItem[]): MonthGroup[] {
|
||||
const map = new Map<number, TranscriptionItem[]>();
|
||||
|
||||
for (const t of transcriptions) {
|
||||
const m = t.month && t.month >= 1 && t.month <= 12 ? t.month : 0;
|
||||
if (!map.has(m)) map.set(m, []);
|
||||
map.get(m)!.push(t);
|
||||
}
|
||||
|
||||
const groups: MonthGroup[] = [];
|
||||
for (let m = 1; m <= 12; m++) {
|
||||
const items = map.get(m);
|
||||
if (items && items.length > 0) {
|
||||
groups.push({ month: m, label: MONTH_NAMES[m], transcriptions: items });
|
||||
}
|
||||
}
|
||||
const unknown = map.get(0);
|
||||
if (unknown && unknown.length > 0) {
|
||||
groups.push({ month: 0, label: "Unknown Month", transcriptions: unknown });
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Year Sidebar
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function YearSidebar({
|
||||
groups,
|
||||
activeYear,
|
||||
onSelect,
|
||||
}: {
|
||||
groups: YearGroup[];
|
||||
activeYear: number;
|
||||
onSelect: (year: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
|
||||
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
||||
Years
|
||||
</h2>
|
||||
<nav className="flex flex-col gap-0.5">
|
||||
{groups.map((g) => {
|
||||
const isActive = g.year === activeYear;
|
||||
return (
|
||||
<button
|
||||
key={g.year}
|
||||
onClick={() => onSelect(g.year)}
|
||||
className={cn(
|
||||
"flex items-center justify-between gap-2 px-2 py-1.5 rounded-md text-sm transition-colors text-left",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary font-medium"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{g.label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs tabular-nums shrink-0",
|
||||
isActive ? "text-primary/70" : "text-muted-foreground/60",
|
||||
)}
|
||||
>
|
||||
{g.count}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mobile drawer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function YearMobileNav({
|
||||
groups,
|
||||
activeYear,
|
||||
onSelect,
|
||||
title,
|
||||
}: {
|
||||
groups: YearGroup[];
|
||||
activeYear: number;
|
||||
onSelect: (year: number) => void;
|
||||
title: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={setOpen} direction="left">
|
||||
<DrawerTrigger
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline", size: "sm" }),
|
||||
"lg:hidden gap-1.5",
|
||||
)}
|
||||
>
|
||||
<Menu className="size-4" />
|
||||
<span className="hidden sm:inline">Years</span>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent direction="left" className="h-full">
|
||||
<DrawerHeader className="text-left border-b border-border pb-4 mb-2">
|
||||
<DrawerTitle>{title}</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<nav className="overflow-y-auto px-4 pb-6 pt-2">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{groups.map((g) => {
|
||||
const isActive = g.year === activeYear;
|
||||
return (
|
||||
<DrawerClose key={g.year} asChild>
|
||||
<button
|
||||
onClick={() => {
|
||||
onSelect(g.year);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-between gap-2 px-2 py-2 rounded-md text-sm transition-colors text-left",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary font-medium"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<span>{g.label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs tabular-nums",
|
||||
isActive
|
||||
? "text-primary/70"
|
||||
: "text-muted-foreground/60",
|
||||
)}
|
||||
>
|
||||
{g.count}
|
||||
</span>
|
||||
</button>
|
||||
</DrawerClose>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Prev / Next nav
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function YearNav({
|
||||
prev,
|
||||
next,
|
||||
groups,
|
||||
activeYear,
|
||||
onSelect,
|
||||
drawerTitle,
|
||||
}: {
|
||||
prev: YearGroup | null;
|
||||
next: YearGroup | null;
|
||||
groups: YearGroup[];
|
||||
activeYear: number;
|
||||
onSelect: (year: number) => void;
|
||||
drawerTitle: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<YearMobileNav
|
||||
groups={groups}
|
||||
activeYear={activeYear}
|
||||
onSelect={onSelect}
|
||||
title={drawerTitle}
|
||||
/>
|
||||
|
||||
{prev ? (
|
||||
<button
|
||||
onClick={() => onSelect(prev.year)}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline", size: "sm" }),
|
||||
"gap-1.5",
|
||||
)}
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
<span className="hidden sm:inline truncate max-w-[180px]">
|
||||
{prev.label}
|
||||
</span>
|
||||
<span className="sm:hidden">Previous</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="hidden lg:block" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{next ? (
|
||||
<button
|
||||
onClick={() => onSelect(next.year)}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline", size: "sm" }),
|
||||
"gap-1.5",
|
||||
)}
|
||||
>
|
||||
<span className="hidden sm:inline truncate max-w-[180px]">
|
||||
{next.label}
|
||||
</span>
|
||||
<span className="sm:hidden">Next</span>
|
||||
<ChevronRight className="size-4" />
|
||||
</button>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Month section — collapsible group of transcriptions for a single month
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function MonthSection({
|
||||
group,
|
||||
defaultOpen,
|
||||
}: {
|
||||
group: MonthGroup;
|
||||
defaultOpen: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex items-center gap-3 w-full px-5 py-3 text-left hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"size-4 text-muted-foreground shrink-0 transition-transform",
|
||||
open && "rotate-90",
|
||||
)}
|
||||
/>
|
||||
<span className="font-semibold text-foreground text-sm">
|
||||
{group.label}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground tabular-nums">
|
||||
{group.transcriptions.length}{" "}
|
||||
{group.transcriptions.length === 1
|
||||
? "transcription"
|
||||
: "transcriptions"}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<ul className="divide-y divide-border">
|
||||
{group.transcriptions.map((t) => (
|
||||
<li key={t.slug}>
|
||||
<Link
|
||||
href={`/transcriptions/${t.slug}`}
|
||||
className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-5 pl-12 py-3.5 gap-1 sm:gap-4 hover:bg-muted transition-colors"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<p className="font-medium text-foreground text-sm">
|
||||
{t.name}
|
||||
</p>
|
||||
<Badge
|
||||
variant={t.type === "lecture" ? "default" : "secondary"}
|
||||
className="text-[10px] px-1.5 py-0"
|
||||
>
|
||||
{t.type === "lecture" ? "Lecture" : "Conversation"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
|
||||
{t.place && (
|
||||
<span className="flex items-center gap-1">
|
||||
<MapPin className="size-3" />
|
||||
{t.place}
|
||||
</span>
|
||||
)}
|
||||
{t.reference && (
|
||||
<span className="text-primary/70">{t.reference}</span>
|
||||
)}
|
||||
{t.durationMinutes && t.durationMinutes > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="size-3" />
|
||||
{t.durationMinutes} min
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function TranscriptionTypeBrowser({
|
||||
transcriptions,
|
||||
type,
|
||||
typeLabel,
|
||||
totalCount,
|
||||
}: {
|
||||
transcriptions: TranscriptionItem[];
|
||||
type: string;
|
||||
typeLabel: string;
|
||||
totalCount: number;
|
||||
}) {
|
||||
const yearGroups = useMemo(
|
||||
() => buildYearGroups(transcriptions),
|
||||
[transcriptions],
|
||||
);
|
||||
|
||||
const [activeYearRaw, setActiveYear] = useState<number>(
|
||||
yearGroups.length > 0 ? yearGroups[0].year : 0,
|
||||
);
|
||||
|
||||
// Derive a safe active year
|
||||
const activeYear = useMemo(() => {
|
||||
if (yearGroups.length === 0) return activeYearRaw;
|
||||
if (yearGroups.find((g) => g.year === activeYearRaw)) return activeYearRaw;
|
||||
return yearGroups[0].year;
|
||||
}, [yearGroups, activeYearRaw]);
|
||||
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleYearSelect = (year: number) => {
|
||||
setActiveYear(year);
|
||||
contentRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
};
|
||||
|
||||
// Active group + neighbors
|
||||
const activeIdx = yearGroups.findIndex((g) => g.year === activeYear);
|
||||
const activeGroup = activeIdx >= 0 ? yearGroups[activeIdx] : null;
|
||||
const prev = activeIdx > 0 ? yearGroups[activeIdx - 1] : null;
|
||||
const next =
|
||||
activeIdx >= 0 && activeIdx < yearGroups.length - 1
|
||||
? yearGroups[activeIdx + 1]
|
||||
: null;
|
||||
|
||||
// Month groups within the active year
|
||||
const monthGroups = useMemo(
|
||||
() => buildMonthGroups(activeGroup?.transcriptions ?? []),
|
||||
[activeGroup],
|
||||
);
|
||||
|
||||
const activeYearLabel = activeGroup?.label ?? "Unknown";
|
||||
const drawerTitle = typeLabel;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Header */}
|
||||
<header className="border-b border-border bg-card px-6 py-4 sticky top-0 z-20">
|
||||
<div className="max-w-5xl mx-auto flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<Link
|
||||
href="/transcriptions?view=type"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
← Transcriptions by Type
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<span className="text-primary">{typeLabel}: </span>
|
||||
{activeYearLabel}
|
||||
</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{activeGroup?.count ?? 0}{" "}
|
||||
{(activeGroup?.count ?? 0) === 1
|
||||
? "transcription"
|
||||
: "transcriptions"}{" "}
|
||||
in {activeYearLabel} · {totalCount} total
|
||||
</p>
|
||||
</div>
|
||||
<div className="shrink-0 pt-1">
|
||||
<ReadingSettings />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Sidebar + Main */}
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
{/* Left sidebar — years */}
|
||||
<YearSidebar
|
||||
groups={yearGroups}
|
||||
activeYear={activeYear}
|
||||
onSelect={handleYearSelect}
|
||||
/>
|
||||
|
||||
{/* Main content */}
|
||||
<main ref={contentRef} className="flex-1 min-w-0 px-6 py-8 space-y-8">
|
||||
{/* Top navigation */}
|
||||
<YearNav
|
||||
prev={prev}
|
||||
next={next}
|
||||
groups={yearGroups}
|
||||
activeYear={activeYear}
|
||||
onSelect={handleYearSelect}
|
||||
drawerTitle={drawerTitle}
|
||||
/>
|
||||
|
||||
{/* Content — transcriptions grouped by month */}
|
||||
{monthGroups.length === 0 ? (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
<p className="text-lg font-medium">
|
||||
No transcriptions for this year
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-0 divide-y divide-border">
|
||||
{monthGroups.map((mg, i) => (
|
||||
<MonthSection
|
||||
key={mg.month}
|
||||
group={mg}
|
||||
defaultOpen={i === 0 || monthGroups.length <= 3}
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Bottom navigation */}
|
||||
<YearNav
|
||||
prev={prev}
|
||||
next={next}
|
||||
groups={yearGroups}
|
||||
activeYear={activeYear}
|
||||
onSelect={handleYearSelect}
|
||||
drawerTitle={drawerTitle}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getTranscriptionsByYear } from "@/lib/transcriptions";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { TranscriptionYearBrowser } from "./year-browser";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ year: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { year: yearStr } = await params;
|
||||
const year = parseInt(yearStr, 10);
|
||||
if (isNaN(year)) return {};
|
||||
const transcriptions = await getTranscriptionsByYear(year);
|
||||
return buildOgMeta({
|
||||
title: "Transcriptions",
|
||||
detail: `${transcriptions.length} transcriptions from ${year}`,
|
||||
quote: `Lectures and conversations of Śrīla Prabhupāda from ${year}.`,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function TranscriptionsByYearPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ year: string }>;
|
||||
}) {
|
||||
const { year: yearStr } = await params;
|
||||
const year = parseInt(yearStr, 10);
|
||||
if (isNaN(year)) notFound();
|
||||
|
||||
const transcriptions = await getTranscriptionsByYear(year);
|
||||
if (transcriptions.length === 0) notFound();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<TranscriptionYearBrowser
|
||||
transcriptions={transcriptions}
|
||||
year={year}
|
||||
totalCount={transcriptions.length}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChevronRight, Clock, MapPin } from "lucide-react";
|
||||
import { PlayAudioButton } from "@/app/transcriptions/[slug]/play-audio-button";
|
||||
|
||||
type TranscriptionItem = {
|
||||
slug: string;
|
||||
name: string;
|
||||
type: string;
|
||||
date: string | null;
|
||||
year: number | null;
|
||||
month: number | null;
|
||||
day: number | null;
|
||||
place: string | null;
|
||||
context: string | null;
|
||||
reference: string | null;
|
||||
durationMinutes: number | null;
|
||||
participants: string[];
|
||||
audioUrl: string | null;
|
||||
};
|
||||
|
||||
const MONTH_NAMES = [
|
||||
"",
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
|
||||
function MonthSection({
|
||||
month,
|
||||
label,
|
||||
items,
|
||||
defaultOpen,
|
||||
}: {
|
||||
month: number;
|
||||
label: string;
|
||||
items: TranscriptionItem[];
|
||||
defaultOpen: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex items-center gap-3 w-full px-5 py-3 text-left hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"size-4 text-muted-foreground shrink-0 transition-transform",
|
||||
open && "rotate-90",
|
||||
)}
|
||||
/>
|
||||
<span className="font-semibold text-foreground text-sm">{label}</span>
|
||||
<span className="text-xs text-muted-foreground tabular-nums">
|
||||
{items.length}{" "}
|
||||
{items.length === 1 ? "transcription" : "transcriptions"}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<ul className="divide-y divide-border">
|
||||
{items.map((t) => (
|
||||
<li key={t.slug}>
|
||||
<Link
|
||||
href={`/transcriptions/${t.slug}`}
|
||||
className="flex items-center px-5 pl-12 py-3.5 gap-3 hover:bg-muted transition-colors"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<p className="font-medium text-foreground text-sm">
|
||||
{t.name}
|
||||
</p>
|
||||
<Badge
|
||||
variant={t.type === "lecture" ? "default" : "secondary"}
|
||||
className="text-[10px] px-1.5 py-0"
|
||||
>
|
||||
{t.type === "lecture" ? "Lecture" : "Conversation"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
|
||||
{t.place && (
|
||||
<span className="flex items-center gap-1">
|
||||
<MapPin className="size-3" />
|
||||
{t.place}
|
||||
</span>
|
||||
)}
|
||||
{t.reference && (
|
||||
<span className="text-primary/70">{t.reference}</span>
|
||||
)}
|
||||
{t.durationMinutes && t.durationMinutes > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="size-3" />
|
||||
{t.durationMinutes} min
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{t.audioUrl && (
|
||||
<PlayAudioButton
|
||||
audioUrl={t.audioUrl}
|
||||
title={t.name}
|
||||
subtitle={[t.date, t.place].filter(Boolean).join(" · ")}
|
||||
sourceUrl={`/transcriptions/${t.slug}`}
|
||||
type={t.type}
|
||||
variant="compact"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TranscriptionYearBrowser({
|
||||
transcriptions,
|
||||
year,
|
||||
totalCount,
|
||||
}: {
|
||||
transcriptions: TranscriptionItem[];
|
||||
year: number;
|
||||
totalCount: number;
|
||||
}) {
|
||||
const monthGroups = useMemo(() => {
|
||||
const map = new Map<number, TranscriptionItem[]>();
|
||||
for (const t of transcriptions) {
|
||||
const m = t.month && t.month >= 1 && t.month <= 12 ? t.month : 0;
|
||||
if (!map.has(m)) map.set(m, []);
|
||||
map.get(m)!.push(t);
|
||||
}
|
||||
const groups: {
|
||||
month: number;
|
||||
label: string;
|
||||
items: TranscriptionItem[];
|
||||
}[] = [];
|
||||
for (let m = 1; m <= 12; m++) {
|
||||
const items = map.get(m);
|
||||
if (items && items.length > 0) {
|
||||
groups.push({ month: m, label: MONTH_NAMES[m], items });
|
||||
}
|
||||
}
|
||||
const unknown = map.get(0);
|
||||
if (unknown && unknown.length > 0) {
|
||||
groups.push({ month: 0, label: "Unknown Month", items: unknown });
|
||||
}
|
||||
return groups;
|
||||
}, [transcriptions]);
|
||||
|
||||
const lectureCount = transcriptions.filter(
|
||||
(t) => t.type === "lecture",
|
||||
).length;
|
||||
const conversationCount = transcriptions.filter(
|
||||
(t) => t.type === "conversation",
|
||||
).length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/transcriptions"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
← Transcriptions
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
Transcriptions from {year}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{totalCount} transcriptions · {lectureCount} lectures ·{" "}
|
||||
{conversationCount} conversations
|
||||
</p>
|
||||
</PageHeader>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-6 py-8 space-y-6">
|
||||
{monthGroups.length === 0 ? (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
<p className="text-lg font-medium">
|
||||
No transcriptions for this year
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-0 divide-y divide-border">
|
||||
{monthGroups.map((mg, i) => (
|
||||
<MonthSection
|
||||
key={mg.month}
|
||||
month={mg.month}
|
||||
label={mg.label}
|
||||
items={mg.items}
|
||||
defaultOpen={i === 0 || monthGroups.length <= 3}
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type AudioTrack = {
|
||||
url: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
coverImage?: string;
|
||||
sourceUrl?: string;
|
||||
type?: "lecture" | "conversation" | "kirtan" | "music" | string;
|
||||
};
|
||||
|
||||
type AudioPlayerState = {
|
||||
currentTrack: AudioTrack | null;
|
||||
isPlaying: boolean;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
volume: number;
|
||||
isMuted: boolean;
|
||||
isLoading: boolean;
|
||||
playlist: AudioTrack[];
|
||||
currentIndex: number;
|
||||
};
|
||||
|
||||
type AudioPlayerActions = {
|
||||
play: (track: AudioTrack) => void;
|
||||
playPlaylist: (tracks: AudioTrack[], startIndex?: number) => void;
|
||||
pause: () => void;
|
||||
resume: () => void;
|
||||
togglePlayPause: () => void;
|
||||
stop: () => void;
|
||||
seekTo: (time: number) => void;
|
||||
setVolume: (vol: number) => void;
|
||||
toggleMute: () => void;
|
||||
next: () => void;
|
||||
previous: () => void;
|
||||
};
|
||||
|
||||
type AudioPlayerContextValue = AudioPlayerState & AudioPlayerActions;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const AudioPlayerContext = createContext<AudioPlayerContextValue | null>(null);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// localStorage helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const VOLUME_KEY = "audio-player-volume";
|
||||
const MUTED_KEY = "audio-player-muted";
|
||||
|
||||
function readStoredVolume(): number {
|
||||
if (typeof window === "undefined") return 0.75;
|
||||
try {
|
||||
const raw = localStorage.getItem(VOLUME_KEY);
|
||||
if (raw !== null) {
|
||||
const v = parseFloat(raw);
|
||||
if (isFinite(v) && v >= 0 && v <= 1) return v;
|
||||
}
|
||||
} catch {
|
||||
// localStorage unavailable
|
||||
}
|
||||
return 0.75;
|
||||
}
|
||||
|
||||
function readStoredMuted(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
try {
|
||||
return localStorage.getItem(MUTED_KEY) === "true";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function persistVolume(vol: number) {
|
||||
try {
|
||||
localStorage.setItem(VOLUME_KEY, String(vol));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function persistMuted(muted: boolean) {
|
||||
try {
|
||||
localStorage.setItem(MUTED_KEY, String(muted));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function AudioPlayerProvider({ children }: { children: ReactNode }) {
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
|
||||
// --- State ---
|
||||
const [currentTrack, setCurrentTrack] = useState<AudioTrack | null>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [volume, setVolumeState] = useState(readStoredVolume);
|
||||
const [isMuted, setIsMuted] = useState(readStoredMuted);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [playlist, setPlaylist] = useState<AudioTrack[]>([]);
|
||||
const [currentIndex, setCurrentIndex] = useState(-1);
|
||||
|
||||
// Keep refs for values accessed in callbacks to avoid stale closures
|
||||
const playlistRef = useRef(playlist);
|
||||
const currentIndexRef = useRef(currentIndex);
|
||||
|
||||
// Sync refs in an effect to avoid "cannot access refs during render"
|
||||
useEffect(() => {
|
||||
playlistRef.current = playlist;
|
||||
}, [playlist]);
|
||||
useEffect(() => {
|
||||
currentIndexRef.current = currentIndex;
|
||||
}, [currentIndex]);
|
||||
|
||||
// --- Sync volume/muted to the <audio> element ---
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
audio.volume = isMuted ? 0 : volume;
|
||||
}, [volume, isMuted]);
|
||||
|
||||
// --- Audio event listeners ---
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
|
||||
function handleTimeUpdate() {
|
||||
setCurrentTime(audio!.currentTime);
|
||||
}
|
||||
|
||||
function handleLoadedMetadata() {
|
||||
setDuration(audio!.duration);
|
||||
}
|
||||
|
||||
function handleDurationChange() {
|
||||
if (isFinite(audio!.duration)) {
|
||||
setDuration(audio!.duration);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCanPlay() {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
function handleWaiting() {
|
||||
setIsLoading(true);
|
||||
}
|
||||
|
||||
function handlePlaying() {
|
||||
setIsPlaying(true);
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
function handlePause() {
|
||||
setIsPlaying(false);
|
||||
}
|
||||
|
||||
function handleEnded() {
|
||||
setIsPlaying(false);
|
||||
// Auto-play next track if playlist has more
|
||||
const idx = currentIndexRef.current;
|
||||
const pl = playlistRef.current;
|
||||
if (idx >= 0 && idx < pl.length - 1) {
|
||||
const nextIdx = idx + 1;
|
||||
const nextTrack = pl[nextIdx];
|
||||
setCurrentIndex(nextIdx);
|
||||
setCurrentTrack(nextTrack);
|
||||
setCurrentTime(0);
|
||||
setDuration(0);
|
||||
setIsLoading(true);
|
||||
if (audio) {
|
||||
audio.src = nextTrack.url;
|
||||
audio.load();
|
||||
audio.play().catch((err) => {
|
||||
console.error("[AudioPlayer] Auto-play next failed:", err);
|
||||
setIsPlaying(false);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleError() {
|
||||
console.error("[AudioPlayer] Audio error:", audio!.error);
|
||||
setIsPlaying(false);
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
audio.addEventListener("timeupdate", handleTimeUpdate);
|
||||
audio.addEventListener("loadedmetadata", handleLoadedMetadata);
|
||||
audio.addEventListener("durationchange", handleDurationChange);
|
||||
audio.addEventListener("canplay", handleCanPlay);
|
||||
audio.addEventListener("waiting", handleWaiting);
|
||||
audio.addEventListener("playing", handlePlaying);
|
||||
audio.addEventListener("pause", handlePause);
|
||||
audio.addEventListener("ended", handleEnded);
|
||||
audio.addEventListener("error", handleError);
|
||||
|
||||
return () => {
|
||||
audio.removeEventListener("timeupdate", handleTimeUpdate);
|
||||
audio.removeEventListener("loadedmetadata", handleLoadedMetadata);
|
||||
audio.removeEventListener("durationchange", handleDurationChange);
|
||||
audio.removeEventListener("canplay", handleCanPlay);
|
||||
audio.removeEventListener("waiting", handleWaiting);
|
||||
audio.removeEventListener("playing", handlePlaying);
|
||||
audio.removeEventListener("pause", handlePause);
|
||||
audio.removeEventListener("ended", handleEnded);
|
||||
audio.removeEventListener("error", handleError);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
const play = useCallback((track: AudioTrack) => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
|
||||
setCurrentTrack(track);
|
||||
setPlaylist([track]);
|
||||
setCurrentIndex(0);
|
||||
setCurrentTime(0);
|
||||
setDuration(0);
|
||||
setIsLoading(true);
|
||||
|
||||
audio.src = track.url;
|
||||
audio.load();
|
||||
audio.play().catch((err) => {
|
||||
console.error("[AudioPlayer] Play failed:", err);
|
||||
setIsPlaying(false);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const playPlaylist = useCallback(
|
||||
(tracks: AudioTrack[], startIndex: number = 0) => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio || tracks.length === 0) return;
|
||||
|
||||
const idx = Math.max(0, Math.min(startIndex, tracks.length - 1));
|
||||
const track = tracks[idx];
|
||||
|
||||
setPlaylist(tracks);
|
||||
setCurrentIndex(idx);
|
||||
setCurrentTrack(track);
|
||||
setCurrentTime(0);
|
||||
setDuration(0);
|
||||
setIsLoading(true);
|
||||
|
||||
audio.src = track.url;
|
||||
audio.load();
|
||||
audio.play().catch((err) => {
|
||||
console.error("[AudioPlayer] PlayPlaylist failed:", err);
|
||||
setIsPlaying(false);
|
||||
setIsLoading(false);
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const pause = useCallback(() => {
|
||||
audioRef.current?.pause();
|
||||
}, []);
|
||||
|
||||
const resume = useCallback(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio || !audio.src) return;
|
||||
audio.play().catch((err) => {
|
||||
console.error("[AudioPlayer] Resume failed:", err);
|
||||
setIsPlaying(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const togglePlayPause = useCallback(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio || !audio.src) return;
|
||||
if (audio.paused) {
|
||||
audio.play().catch((err) => {
|
||||
console.error("[AudioPlayer] Toggle play failed:", err);
|
||||
setIsPlaying(false);
|
||||
});
|
||||
} else {
|
||||
audio.pause();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
const audio = audioRef.current;
|
||||
if (audio) {
|
||||
audio.pause();
|
||||
audio.removeAttribute("src");
|
||||
audio.load();
|
||||
}
|
||||
setCurrentTrack(null);
|
||||
setIsPlaying(false);
|
||||
setCurrentTime(0);
|
||||
setDuration(0);
|
||||
setIsLoading(false);
|
||||
setPlaylist([]);
|
||||
setCurrentIndex(-1);
|
||||
}, []);
|
||||
|
||||
const seekTo = useCallback((time: number) => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
const clamped = Math.max(0, Math.min(time, audio.duration || 0));
|
||||
audio.currentTime = clamped;
|
||||
setCurrentTime(clamped);
|
||||
}, []);
|
||||
|
||||
const setVolume = useCallback(
|
||||
(vol: number) => {
|
||||
const clamped = Math.max(0, Math.min(1, vol));
|
||||
setVolumeState(clamped);
|
||||
persistVolume(clamped);
|
||||
// If setting volume while muted, unmute
|
||||
if (isMuted && clamped > 0) {
|
||||
setIsMuted(false);
|
||||
persistMuted(false);
|
||||
}
|
||||
},
|
||||
[isMuted],
|
||||
);
|
||||
|
||||
const toggleMute = useCallback(() => {
|
||||
setIsMuted((prev) => {
|
||||
const next = !prev;
|
||||
persistMuted(next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const next = useCallback(() => {
|
||||
const audio = audioRef.current;
|
||||
const idx = currentIndexRef.current;
|
||||
const pl = playlistRef.current;
|
||||
|
||||
if (!audio || idx < 0 || idx >= pl.length - 1) return;
|
||||
|
||||
const nextIdx = idx + 1;
|
||||
const nextTrack = pl[nextIdx];
|
||||
|
||||
setCurrentIndex(nextIdx);
|
||||
setCurrentTrack(nextTrack);
|
||||
setCurrentTime(0);
|
||||
setDuration(0);
|
||||
setIsLoading(true);
|
||||
|
||||
audio.src = nextTrack.url;
|
||||
audio.load();
|
||||
audio.play().catch((err) => {
|
||||
console.error("[AudioPlayer] Next failed:", err);
|
||||
setIsPlaying(false);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const previous = useCallback(() => {
|
||||
const audio = audioRef.current;
|
||||
const idx = currentIndexRef.current;
|
||||
const pl = playlistRef.current;
|
||||
|
||||
if (!audio || idx < 0) return;
|
||||
|
||||
// If more than 3 seconds in, restart the current track instead
|
||||
if (audio.currentTime > 3) {
|
||||
audio.currentTime = 0;
|
||||
setCurrentTime(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (idx <= 0) {
|
||||
// At start of playlist — just restart
|
||||
audio.currentTime = 0;
|
||||
setCurrentTime(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const prevIdx = idx - 1;
|
||||
const prevTrack = pl[prevIdx];
|
||||
|
||||
setCurrentIndex(prevIdx);
|
||||
setCurrentTrack(prevTrack);
|
||||
setCurrentTime(0);
|
||||
setDuration(0);
|
||||
setIsLoading(true);
|
||||
|
||||
audio.src = prevTrack.url;
|
||||
audio.load();
|
||||
audio.play().catch((err) => {
|
||||
console.error("[AudioPlayer] Previous failed:", err);
|
||||
setIsPlaying(false);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// --- Context value ---
|
||||
|
||||
const value: AudioPlayerContextValue = {
|
||||
currentTrack,
|
||||
isPlaying,
|
||||
currentTime,
|
||||
duration,
|
||||
volume,
|
||||
isMuted,
|
||||
isLoading,
|
||||
playlist,
|
||||
currentIndex,
|
||||
play,
|
||||
playPlaylist,
|
||||
pause,
|
||||
resume,
|
||||
togglePlayPause,
|
||||
stop,
|
||||
seekTo,
|
||||
setVolume,
|
||||
toggleMute,
|
||||
next,
|
||||
previous,
|
||||
};
|
||||
|
||||
return (
|
||||
<AudioPlayerContext.Provider value={value}>
|
||||
{children}
|
||||
<audio ref={audioRef} preload="metadata" style={{ display: "none" }} />
|
||||
</AudioPlayerContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useAudioPlayer(): AudioPlayerContextValue {
|
||||
const ctx = useContext(AudioPlayerContext);
|
||||
if (!ctx) {
|
||||
throw new Error(
|
||||
"useAudioPlayer must be used within an <AudioPlayerProvider>",
|
||||
);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,560 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useCallback, useEffect } from "react";
|
||||
import { useAudioPlayer } from "./audio-context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
Play,
|
||||
Pause,
|
||||
SkipBack,
|
||||
SkipForward,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
Volume1,
|
||||
X,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const FALLBACK_COVER = "/images/cover.png";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
if (!isFinite(seconds) || seconds < 0) return "0:00";
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
if (h > 0)
|
||||
return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
||||
return `${m}:${String(s).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function VolumeIcon({
|
||||
volume,
|
||||
isMuted,
|
||||
className,
|
||||
}: {
|
||||
volume: number;
|
||||
isMuted: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
if (isMuted || volume === 0) return <VolumeX className={className} />;
|
||||
if (volume < 0.5) return <Volume1 className={className} />;
|
||||
return <Volume2 className={className} />;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Progress Bar
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ProgressBar({
|
||||
currentTime,
|
||||
duration,
|
||||
onSeek,
|
||||
thin = false,
|
||||
}: {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
onSeek: (time: number) => void;
|
||||
thin?: boolean;
|
||||
}) {
|
||||
const barRef = useRef<HTMLDivElement>(null);
|
||||
const draggingRef = useRef(false);
|
||||
|
||||
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
|
||||
|
||||
const calculateSeekTime = useCallback(
|
||||
(clientX: number) => {
|
||||
const bar = barRef.current;
|
||||
if (!bar || duration <= 0) return;
|
||||
const rect = bar.getBoundingClientRect();
|
||||
const fraction = Math.max(
|
||||
0,
|
||||
Math.min(1, (clientX - rect.left) / rect.width),
|
||||
);
|
||||
onSeek(fraction * duration);
|
||||
},
|
||||
[duration, onSeek],
|
||||
);
|
||||
|
||||
const handlePointerDown = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
e.preventDefault();
|
||||
draggingRef.current = true;
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
calculateSeekTime(e.clientX);
|
||||
},
|
||||
[calculateSeekTime],
|
||||
);
|
||||
|
||||
const handlePointerMove = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!draggingRef.current) return;
|
||||
calculateSeekTime(e.clientX);
|
||||
},
|
||||
[calculateSeekTime],
|
||||
);
|
||||
|
||||
const handlePointerUp = useCallback(() => {
|
||||
draggingRef.current = false;
|
||||
}, []);
|
||||
|
||||
if (thin) {
|
||||
return (
|
||||
<div
|
||||
ref={barRef}
|
||||
className="w-full h-2 bg-white/15 cursor-pointer shrink-0 relative"
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
role="slider"
|
||||
aria-label="Playback progress"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={duration || 0}
|
||||
aria-valuenow={currentTime}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="h-full bg-primary transition-[width] duration-100 ease-linear"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<span className="text-[10px] tabular-nums text-muted-foreground min-w-[32px] text-right select-none">
|
||||
{formatTime(currentTime)}
|
||||
</span>
|
||||
<div
|
||||
ref={barRef}
|
||||
className="relative flex-1 h-1 bg-muted rounded-full cursor-pointer group/progress"
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
role="slider"
|
||||
aria-label="Playback progress"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={duration || 0}
|
||||
aria-valuenow={currentTime}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="h-full bg-primary rounded-full transition-[width] duration-100 ease-linear"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
<div
|
||||
className="absolute top-1/2 -translate-y-1/2 -translate-x-1/2 size-2.5 bg-primary rounded-full opacity-0 group-hover/progress:opacity-100 transition-opacity shadow-sm pointer-events-none"
|
||||
style={{ left: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[10px] tabular-nums text-muted-foreground min-w-[32px] select-none">
|
||||
{formatTime(duration)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Transport Button
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function TransportButton({
|
||||
onClick,
|
||||
disabled = false,
|
||||
label,
|
||||
size = "default",
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
label: string;
|
||||
size?: "default" | "large";
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
aria-label={label}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center rounded-full transition-colors outline-none",
|
||||
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card",
|
||||
"disabled:opacity-30 disabled:pointer-events-none",
|
||||
size === "large"
|
||||
? "size-9 bg-foreground text-background hover:bg-foreground/90"
|
||||
: "size-8 text-foreground hover:bg-muted",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Floating Player
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function FloatingPlayer() {
|
||||
const {
|
||||
currentTrack,
|
||||
isPlaying,
|
||||
currentTime,
|
||||
duration,
|
||||
volume,
|
||||
isMuted,
|
||||
isLoading,
|
||||
playlist,
|
||||
currentIndex,
|
||||
togglePlayPause,
|
||||
stop,
|
||||
seekTo,
|
||||
setVolume,
|
||||
toggleMute,
|
||||
next,
|
||||
previous,
|
||||
} = useAudioPlayer();
|
||||
|
||||
const playerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Add bottom padding to body when player is visible
|
||||
useEffect(() => {
|
||||
if (!currentTrack) return;
|
||||
document.body.style.paddingBottom = "96px";
|
||||
return () => {
|
||||
document.body.style.paddingBottom = "";
|
||||
};
|
||||
}, [currentTrack]);
|
||||
|
||||
// Keyboard shortcuts when player is focused
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case " ":
|
||||
e.preventDefault();
|
||||
togglePlayPause();
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
e.preventDefault();
|
||||
seekTo(Math.max(0, currentTime - 10));
|
||||
break;
|
||||
case "ArrowRight":
|
||||
e.preventDefault();
|
||||
seekTo(Math.min(duration, currentTime + 10));
|
||||
break;
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
setVolume(Math.min(1, volume + 0.05));
|
||||
break;
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
setVolume(Math.max(0, volume - 0.05));
|
||||
break;
|
||||
case "m":
|
||||
case "M":
|
||||
e.preventDefault();
|
||||
toggleMute();
|
||||
break;
|
||||
case "Escape":
|
||||
e.preventDefault();
|
||||
stop();
|
||||
break;
|
||||
}
|
||||
},
|
||||
[
|
||||
togglePlayPause,
|
||||
seekTo,
|
||||
setVolume,
|
||||
toggleMute,
|
||||
stop,
|
||||
currentTime,
|
||||
duration,
|
||||
volume,
|
||||
],
|
||||
);
|
||||
|
||||
if (!currentTrack) return null;
|
||||
|
||||
const hasNext = currentIndex >= 0 && currentIndex < playlist.length - 1;
|
||||
const hasPrev = currentIndex > 0;
|
||||
const coverSrc = currentTrack.coverImage || FALLBACK_COVER;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={playerRef}
|
||||
data-audio-player
|
||||
role="region"
|
||||
aria-label="Audio player"
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={cn(
|
||||
// Centered floating bar
|
||||
"fixed z-40 bottom-5 left-1/2 -translate-x-1/2",
|
||||
// Narrower width
|
||||
"w-[calc(100%-2rem)] max-w-[680px]",
|
||||
// Forced dark surface for contrast regardless of theme
|
||||
"bg-[#1c1c1e] text-white",
|
||||
"border border-white/10",
|
||||
"rounded-2xl",
|
||||
"shadow-2xl shadow-black/25",
|
||||
// Animation
|
||||
"animate-in fade-in-0 slide-in-from-bottom-4 duration-300",
|
||||
// Clip seeker to rounded top corners
|
||||
"overflow-hidden",
|
||||
)}
|
||||
>
|
||||
{/* Thin progress bar — absolutely positioned at top edge */}
|
||||
<ProgressBar
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
onSeek={seekTo}
|
||||
thin
|
||||
/>
|
||||
|
||||
{/* Time row — desktop (hidden on mobile, mobile has its own) */}
|
||||
<div className="hidden md:flex items-center justify-between px-5 pt-1.5">
|
||||
<span className="text-[10px] tabular-nums text-white/40 select-none">
|
||||
{formatTime(currentTime)}
|
||||
</span>
|
||||
<span className="text-[10px] tabular-nums text-white/40 select-none">
|
||||
{formatTime(duration)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* ── Desktop layout ── */}
|
||||
<div className="hidden md:flex items-center h-[64px] px-5 gap-5">
|
||||
{/* Left: Cover + Track info */}
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
{/* Cover image */}
|
||||
<div className="size-12 rounded-lg overflow-hidden bg-muted shrink-0 relative">
|
||||
<Image
|
||||
src={coverSrc}
|
||||
alt=""
|
||||
fill
|
||||
className="object-cover"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate leading-tight">
|
||||
{currentTrack.title}
|
||||
</p>
|
||||
{currentTrack.subtitle && (
|
||||
<p className="text-[11px] text-white/50 truncate leading-tight mt-0.5">
|
||||
{currentTrack.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center: Transport controls */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<TransportButton
|
||||
onClick={previous}
|
||||
disabled={!hasPrev && currentTime <= 3}
|
||||
label="Previous track"
|
||||
className="text-white/60 hover:text-white hover:bg-white/10"
|
||||
>
|
||||
<SkipBack className="size-4" />
|
||||
</TransportButton>
|
||||
|
||||
<TransportButton
|
||||
onClick={togglePlayPause}
|
||||
label={isPlaying ? "Pause" : "Play"}
|
||||
size="large"
|
||||
className="bg-white text-black hover:bg-white/90"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : isPlaying ? (
|
||||
<Pause className="size-4" />
|
||||
) : (
|
||||
<Play className="size-4 ml-0.5" />
|
||||
)}
|
||||
</TransportButton>
|
||||
|
||||
<TransportButton
|
||||
onClick={next}
|
||||
disabled={!hasNext}
|
||||
label="Next track"
|
||||
className="text-white/60 hover:text-white hover:bg-white/10"
|
||||
>
|
||||
<SkipForward className="size-4" />
|
||||
</TransportButton>
|
||||
</div>
|
||||
|
||||
{/* Right: Volume + Source link + Close */}
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
{/* Volume control */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleMute}
|
||||
aria-label={isMuted ? "Unmute" : "Mute"}
|
||||
className="size-7 inline-flex items-center justify-center rounded-md text-white/50 hover:text-white hover:bg-white/10 transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<VolumeIcon
|
||||
volume={volume}
|
||||
isMuted={isMuted}
|
||||
className="size-4"
|
||||
/>
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={isMuted ? 0 : Math.round(volume * 100)}
|
||||
onChange={(e) => setVolume(Number(e.target.value) / 100)}
|
||||
aria-label="Volume"
|
||||
className={cn(
|
||||
"w-16 h-1 appearance-none bg-white/15 rounded-full cursor-pointer",
|
||||
"[&::-webkit-slider-thumb]:appearance-none",
|
||||
"[&::-webkit-slider-thumb]:size-3",
|
||||
"[&::-webkit-slider-thumb]:rounded-full",
|
||||
"[&::-webkit-slider-thumb]:bg-white",
|
||||
"[&::-webkit-slider-thumb]:cursor-pointer",
|
||||
"[&::-moz-range-thumb]:size-3",
|
||||
"[&::-moz-range-thumb]:rounded-full",
|
||||
"[&::-moz-range-thumb]:bg-white",
|
||||
"[&::-moz-range-thumb]:border-0",
|
||||
"[&::-moz-range-thumb]:cursor-pointer",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Source link */}
|
||||
{currentTrack.sourceUrl && (
|
||||
<Link
|
||||
href={currentTrack.sourceUrl}
|
||||
aria-label="Go to source page"
|
||||
className="size-7 inline-flex items-center justify-center rounded-md text-white/50 hover:text-white hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<ExternalLink className="size-3.5" />
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Close */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={stop}
|
||||
aria-label="Close player"
|
||||
className="size-7 inline-flex items-center justify-center rounded-md text-white/50 hover:text-white hover:bg-white/10 transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Mobile layout ── */}
|
||||
<div className="flex md:hidden flex-col px-4 py-3 gap-2.5">
|
||||
{/* Time row — right below seeker */}
|
||||
<div className="flex items-center justify-between px-0.5">
|
||||
<span className="text-[10px] tabular-nums text-white/40 select-none">
|
||||
{formatTime(currentTime)}
|
||||
</span>
|
||||
<span className="text-[10px] tabular-nums text-white/40 select-none">
|
||||
{formatTime(duration)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Row 1: Cover + Title + Source + Close */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Cover image */}
|
||||
<div className="size-10 rounded-lg overflow-hidden bg-muted shrink-0 relative">
|
||||
<Image
|
||||
src={coverSrc}
|
||||
alt=""
|
||||
fill
|
||||
className="object-cover"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Track info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate leading-tight">
|
||||
{currentTrack.title}
|
||||
</p>
|
||||
{currentTrack.subtitle && (
|
||||
<p className="text-[11px] text-white/50 truncate leading-tight mt-0.5">
|
||||
{currentTrack.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Source link */}
|
||||
{currentTrack.sourceUrl && (
|
||||
<Link
|
||||
href={currentTrack.sourceUrl}
|
||||
aria-label="Go to source page"
|
||||
className="size-8 inline-flex items-center justify-center rounded-full text-white/50 hover:text-white hover:bg-white/10 transition-colors shrink-0"
|
||||
>
|
||||
<ExternalLink className="size-4" />
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Close */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={stop}
|
||||
aria-label="Close player"
|
||||
className="size-8 inline-flex items-center justify-center rounded-full text-white/50 hover:text-white hover:bg-white/10 transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring shrink-0"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Prev + Play/Pause + Next — centered */}
|
||||
<div className="flex items-center justify-center gap-4 py-1">
|
||||
<TransportButton
|
||||
onClick={previous}
|
||||
disabled={!hasPrev && currentTime <= 3}
|
||||
label="Previous track"
|
||||
className="text-white/60 hover:text-white hover:bg-white/10 !size-10"
|
||||
>
|
||||
<SkipBack className="size-5" />
|
||||
</TransportButton>
|
||||
|
||||
<TransportButton
|
||||
onClick={togglePlayPause}
|
||||
label={isPlaying ? "Pause" : "Play"}
|
||||
size="large"
|
||||
className="bg-white text-black hover:bg-white/90 !size-12"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="size-5 animate-spin" />
|
||||
) : isPlaying ? (
|
||||
<Pause className="size-5" />
|
||||
) : (
|
||||
<Play className="size-5 ml-0.5" />
|
||||
)}
|
||||
</TransportButton>
|
||||
|
||||
<TransportButton
|
||||
onClick={next}
|
||||
disabled={!hasNext}
|
||||
label="Next track"
|
||||
className="text-white/60 hover:text-white hover:bg-white/10 !size-10"
|
||||
>
|
||||
<SkipForward className="size-5" />
|
||||
</TransportButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { AudioPlayerProvider, useAudioPlayer } from "./audio-context";
|
||||
export type { AudioTrack } from "./audio-context";
|
||||
export { FloatingPlayer } from "./floating-player";
|
||||
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* Data access layer for Transcriptions.
|
||||
*
|
||||
* Uses Prisma to query the `transcriptions` collection in MongoDB.
|
||||
* Replaces the previous local-JSON-file approach so that data is always
|
||||
* served from the database (seeded via seed-transcriptions.ts).
|
||||
*
|
||||
* Some documents store their content as a JSON string in `contentJson`
|
||||
* (a BSON wire-protocol workaround for very large nested arrays).
|
||||
* The `getTranscription()` helper transparently parses that fallback.
|
||||
*/
|
||||
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type TranscriptionSummary = {
|
||||
slug: string;
|
||||
name: string;
|
||||
type: string;
|
||||
date: string | null;
|
||||
year: number | null;
|
||||
month: number | null;
|
||||
day: number | null;
|
||||
place: string | null;
|
||||
context: string | null;
|
||||
reference: string | null;
|
||||
durationMinutes: number | null;
|
||||
participants: string[];
|
||||
audioUrl: string | null;
|
||||
};
|
||||
|
||||
export type ContentBlock = {
|
||||
text: string;
|
||||
format: string;
|
||||
alignment: string;
|
||||
speaker: string | null;
|
||||
};
|
||||
|
||||
export type TranscriptionFull = TranscriptionSummary & {
|
||||
content: ContentBlock[];
|
||||
};
|
||||
|
||||
export type YearGroup = {
|
||||
year: number;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type PlaceGroup = {
|
||||
name: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Prisma select objects (keep queries lightweight)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Fields needed for summary views (no content). */
|
||||
const SUMMARY_SELECT = {
|
||||
slug: true,
|
||||
name: true,
|
||||
type: true,
|
||||
date: true,
|
||||
year: true,
|
||||
month: true,
|
||||
day: true,
|
||||
place: true,
|
||||
context: true,
|
||||
reference: true,
|
||||
durationMinutes: true,
|
||||
participants: true,
|
||||
audioUrl: true,
|
||||
} as const;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sanitize
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function sanitizeHtml(html: string): string {
|
||||
let clean = html.replace(/<script[\s>][\s\S]*?<\/script>/gi, "");
|
||||
clean = clean.replace(/<style[\s>][\s\S]*?<\/style>/gi, "");
|
||||
clean = clean.replace(/<iframe[\s>][\s\S]*?<\/iframe>/gi, "");
|
||||
clean = clean.replace(
|
||||
/<\/?(?:script|style|iframe|object|embed|form|input|textarea|select|button)[^>]*>/gi,
|
||||
"",
|
||||
);
|
||||
clean = clean.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
|
||||
return clean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Index / overview queries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function getTranscriptionYears(): Promise<YearGroup[]> {
|
||||
const transcriptions = await prisma.transcription.findMany({
|
||||
where: { year: { not: null }, isPublished: true },
|
||||
select: { year: true },
|
||||
});
|
||||
|
||||
const counts: Record<number, number> = {};
|
||||
for (const t of transcriptions) {
|
||||
if (t.year) {
|
||||
counts[t.year] = (counts[t.year] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.entries(counts)
|
||||
.map(([year, count]) => ({ year: Number(year), count }))
|
||||
.filter((g) => g.year >= 1960 && g.year <= 1980)
|
||||
.sort((a, b) => a.year - b.year);
|
||||
}
|
||||
|
||||
export async function getTranscriptionPlaces(): Promise<PlaceGroup[]> {
|
||||
const transcriptions = await prisma.transcription.findMany({
|
||||
where: { place: { not: null }, isPublished: true },
|
||||
select: { place: true },
|
||||
});
|
||||
|
||||
const counts: Record<string, number> = {};
|
||||
for (const t of transcriptions) {
|
||||
if (t.place) {
|
||||
counts[t.place] = (counts[t.place] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.entries(counts)
|
||||
.map(([name, count]) => ({ name, count }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
export async function getTotalTranscriptionCount(): Promise<number> {
|
||||
return prisma.transcription.count({ where: { isPublished: true } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a lightweight index object with type distribution counts.
|
||||
* Used by the index page to show lecture/conversation totals.
|
||||
*/
|
||||
export async function getTranscriptionIndex(): Promise<{
|
||||
totalTranscriptions: number;
|
||||
typeDistribution: Record<string, number>;
|
||||
}> {
|
||||
const [total, lectures, conversations] = await Promise.all([
|
||||
prisma.transcription.count({ where: { isPublished: true } }),
|
||||
prisma.transcription.count({
|
||||
where: { isPublished: true, type: "lecture" },
|
||||
}),
|
||||
prisma.transcription.count({
|
||||
where: { isPublished: true, type: "conversation" },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
totalTranscriptions: total,
|
||||
typeDistribution: {
|
||||
lecture: lectures,
|
||||
conversation: conversations,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// List queries (return summaries without content)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function getTranscriptionsByYear(
|
||||
year: number,
|
||||
): Promise<TranscriptionSummary[]> {
|
||||
const transcriptions = await prisma.transcription.findMany({
|
||||
where: { year, isPublished: true },
|
||||
orderBy: [{ month: "asc" }, { day: "asc" }],
|
||||
select: SUMMARY_SELECT,
|
||||
});
|
||||
return transcriptions;
|
||||
}
|
||||
|
||||
export async function getTranscriptionsByPlace(
|
||||
place: string,
|
||||
): Promise<TranscriptionSummary[]> {
|
||||
const transcriptions = await prisma.transcription.findMany({
|
||||
where: { place, isPublished: true },
|
||||
orderBy: [{ year: "asc" }, { month: "asc" }, { day: "asc" }],
|
||||
select: SUMMARY_SELECT,
|
||||
});
|
||||
return transcriptions;
|
||||
}
|
||||
|
||||
export async function getTranscriptionsByType(
|
||||
type: string,
|
||||
): Promise<TranscriptionSummary[]> {
|
||||
const transcriptions = await prisma.transcription.findMany({
|
||||
where: { type, isPublished: true },
|
||||
orderBy: [{ year: "asc" }, { month: "asc" }, { day: "asc" }],
|
||||
select: SUMMARY_SELECT,
|
||||
});
|
||||
return transcriptions;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Single transcription
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function getTranscription(
|
||||
slug: string,
|
||||
): Promise<TranscriptionFull | null> {
|
||||
const t = await prisma.transcription.findUnique({ where: { slug } });
|
||||
if (!t || !t.isPublished) return null;
|
||||
|
||||
// Content may be stored as embedded array or as a JSON string fallback
|
||||
let content: ContentBlock[] = [];
|
||||
|
||||
if (t.content && t.content.length > 0) {
|
||||
content = t.content.map((block) => ({
|
||||
text: sanitizeHtml(block.text),
|
||||
format: block.format,
|
||||
alignment: block.alignment,
|
||||
speaker: block.speaker ?? null,
|
||||
}));
|
||||
} else {
|
||||
// Check for contentJson fallback (BSON workaround for large documents)
|
||||
const raw = t as unknown as Record<string, unknown>;
|
||||
const contentJson = raw.contentJson;
|
||||
if (typeof contentJson === "string" && contentJson.length > 2) {
|
||||
try {
|
||||
const parsed = JSON.parse(contentJson) as ContentBlock[];
|
||||
content = parsed.map((block) => ({
|
||||
text: sanitizeHtml(block.text),
|
||||
format: block.format,
|
||||
alignment: block.alignment,
|
||||
speaker: block.speaker ?? null,
|
||||
}));
|
||||
} catch {
|
||||
// Ignore parse errors — return empty content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
slug: t.slug,
|
||||
name: t.name,
|
||||
type: t.type,
|
||||
date: t.date,
|
||||
year: t.year,
|
||||
month: t.month,
|
||||
day: t.day,
|
||||
place: t.place,
|
||||
context: t.context,
|
||||
reference: t.reference,
|
||||
durationMinutes: t.durationMinutes,
|
||||
participants: t.participants,
|
||||
audioUrl: t.audioUrl,
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Navigation helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function getAdjacentTranscriptions(
|
||||
slug: string,
|
||||
year: number | null,
|
||||
): Promise<{
|
||||
prev: TranscriptionSummary | null;
|
||||
next: TranscriptionSummary | null;
|
||||
}> {
|
||||
if (!year) return { prev: null, next: null };
|
||||
|
||||
const yearTranscriptions = await prisma.transcription.findMany({
|
||||
where: { year, isPublished: true },
|
||||
orderBy: [{ month: "asc" }, { day: "asc" }, { slug: "asc" }],
|
||||
select: SUMMARY_SELECT,
|
||||
});
|
||||
|
||||
const idx = yearTranscriptions.findIndex((t) => t.slug === slug);
|
||||
return {
|
||||
prev: idx > 0 ? yearTranscriptions[idx - 1] : null,
|
||||
next:
|
||||
idx < yearTranscriptions.length - 1 ? yearTranscriptions[idx + 1] : null,
|
||||
};
|
||||
}
|
||||
Generated
+1414
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1045.0",
|
||||
"@base-ui/react": "^1.4.0",
|
||||
"@prisma/client": "^6.19.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
@@ -382,6 +382,7 @@ model Transcription {
|
||||
language String @default("en")
|
||||
source String?
|
||||
durationMinutes Int?
|
||||
audioUrl String?
|
||||
isPublished Boolean @default(false)
|
||||
notes String?
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 504 KiB |
@@ -7,6 +7,557 @@
|
||||
resolved "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz"
|
||||
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
|
||||
|
||||
"@aws-crypto/crc32@5.2.0":
|
||||
version "5.2.0"
|
||||
resolved "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz"
|
||||
integrity sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==
|
||||
dependencies:
|
||||
"@aws-crypto/util" "^5.2.0"
|
||||
"@aws-sdk/types" "^3.222.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-crypto/crc32c@5.2.0":
|
||||
version "5.2.0"
|
||||
resolved "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz"
|
||||
integrity sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==
|
||||
dependencies:
|
||||
"@aws-crypto/util" "^5.2.0"
|
||||
"@aws-sdk/types" "^3.222.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-crypto/sha1-browser@5.2.0":
|
||||
version "5.2.0"
|
||||
resolved "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz"
|
||||
integrity sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==
|
||||
dependencies:
|
||||
"@aws-crypto/supports-web-crypto" "^5.2.0"
|
||||
"@aws-crypto/util" "^5.2.0"
|
||||
"@aws-sdk/types" "^3.222.0"
|
||||
"@aws-sdk/util-locate-window" "^3.0.0"
|
||||
"@smithy/util-utf8" "^2.0.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-crypto/sha256-browser@5.2.0":
|
||||
version "5.2.0"
|
||||
resolved "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz"
|
||||
integrity sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==
|
||||
dependencies:
|
||||
"@aws-crypto/sha256-js" "^5.2.0"
|
||||
"@aws-crypto/supports-web-crypto" "^5.2.0"
|
||||
"@aws-crypto/util" "^5.2.0"
|
||||
"@aws-sdk/types" "^3.222.0"
|
||||
"@aws-sdk/util-locate-window" "^3.0.0"
|
||||
"@smithy/util-utf8" "^2.0.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-crypto/sha256-js@^5.2.0", "@aws-crypto/sha256-js@5.2.0":
|
||||
version "5.2.0"
|
||||
resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz"
|
||||
integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==
|
||||
dependencies:
|
||||
"@aws-crypto/util" "^5.2.0"
|
||||
"@aws-sdk/types" "^3.222.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-crypto/supports-web-crypto@^5.2.0":
|
||||
version "5.2.0"
|
||||
resolved "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz"
|
||||
integrity sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==
|
||||
dependencies:
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-crypto/util@^5.2.0", "@aws-crypto/util@5.2.0":
|
||||
version "5.2.0"
|
||||
resolved "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz"
|
||||
integrity sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==
|
||||
dependencies:
|
||||
"@aws-sdk/types" "^3.222.0"
|
||||
"@smithy/util-utf8" "^2.0.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/client-s3@^3.1045.0":
|
||||
version "3.1045.0"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1045.0.tgz"
|
||||
integrity sha512-fsuO3Y6t+3Ro9Bsg41DKj4Sfy53CGSrhnMldNplWmG8Tx0UbYk+YDa4RD1hVlJpERw4JBmPkl0+J9qlxMh1pcA==
|
||||
dependencies:
|
||||
"@aws-crypto/sha1-browser" "5.2.0"
|
||||
"@aws-crypto/sha256-browser" "5.2.0"
|
||||
"@aws-crypto/sha256-js" "5.2.0"
|
||||
"@aws-sdk/core" "^3.974.8"
|
||||
"@aws-sdk/credential-provider-node" "^3.972.39"
|
||||
"@aws-sdk/middleware-bucket-endpoint" "^3.972.10"
|
||||
"@aws-sdk/middleware-expect-continue" "^3.972.10"
|
||||
"@aws-sdk/middleware-flexible-checksums" "^3.974.16"
|
||||
"@aws-sdk/middleware-host-header" "^3.972.10"
|
||||
"@aws-sdk/middleware-location-constraint" "^3.972.10"
|
||||
"@aws-sdk/middleware-logger" "^3.972.10"
|
||||
"@aws-sdk/middleware-recursion-detection" "^3.972.11"
|
||||
"@aws-sdk/middleware-sdk-s3" "^3.972.37"
|
||||
"@aws-sdk/middleware-ssec" "^3.972.10"
|
||||
"@aws-sdk/middleware-user-agent" "^3.972.38"
|
||||
"@aws-sdk/region-config-resolver" "^3.972.13"
|
||||
"@aws-sdk/signature-v4-multi-region" "^3.996.25"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@aws-sdk/util-endpoints" "^3.996.8"
|
||||
"@aws-sdk/util-user-agent-browser" "^3.972.10"
|
||||
"@aws-sdk/util-user-agent-node" "^3.973.24"
|
||||
"@smithy/config-resolver" "^4.4.17"
|
||||
"@smithy/core" "^3.23.17"
|
||||
"@smithy/eventstream-serde-browser" "^4.2.14"
|
||||
"@smithy/eventstream-serde-config-resolver" "^4.3.14"
|
||||
"@smithy/eventstream-serde-node" "^4.2.14"
|
||||
"@smithy/fetch-http-handler" "^5.3.17"
|
||||
"@smithy/hash-blob-browser" "^4.2.15"
|
||||
"@smithy/hash-node" "^4.2.14"
|
||||
"@smithy/hash-stream-node" "^4.2.14"
|
||||
"@smithy/invalid-dependency" "^4.2.14"
|
||||
"@smithy/md5-js" "^4.2.14"
|
||||
"@smithy/middleware-content-length" "^4.2.14"
|
||||
"@smithy/middleware-endpoint" "^4.4.32"
|
||||
"@smithy/middleware-retry" "^4.5.7"
|
||||
"@smithy/middleware-serde" "^4.2.20"
|
||||
"@smithy/middleware-stack" "^4.2.14"
|
||||
"@smithy/node-config-provider" "^4.3.14"
|
||||
"@smithy/node-http-handler" "^4.6.1"
|
||||
"@smithy/protocol-http" "^5.3.14"
|
||||
"@smithy/smithy-client" "^4.12.13"
|
||||
"@smithy/types" "^4.14.1"
|
||||
"@smithy/url-parser" "^4.2.14"
|
||||
"@smithy/util-base64" "^4.3.2"
|
||||
"@smithy/util-body-length-browser" "^4.2.2"
|
||||
"@smithy/util-body-length-node" "^4.2.3"
|
||||
"@smithy/util-defaults-mode-browser" "^4.3.49"
|
||||
"@smithy/util-defaults-mode-node" "^4.2.54"
|
||||
"@smithy/util-endpoints" "^3.4.2"
|
||||
"@smithy/util-middleware" "^4.2.14"
|
||||
"@smithy/util-retry" "^4.3.6"
|
||||
"@smithy/util-stream" "^4.5.25"
|
||||
"@smithy/util-utf8" "^4.2.2"
|
||||
"@smithy/util-waiter" "^4.3.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/core@^3.974.8":
|
||||
version "3.974.8"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.8.tgz"
|
||||
integrity sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==
|
||||
dependencies:
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@aws-sdk/xml-builder" "^3.972.22"
|
||||
"@smithy/core" "^3.23.17"
|
||||
"@smithy/node-config-provider" "^4.3.14"
|
||||
"@smithy/property-provider" "^4.2.14"
|
||||
"@smithy/protocol-http" "^5.3.14"
|
||||
"@smithy/signature-v4" "^5.3.14"
|
||||
"@smithy/smithy-client" "^4.12.13"
|
||||
"@smithy/types" "^4.14.1"
|
||||
"@smithy/util-base64" "^4.3.2"
|
||||
"@smithy/util-middleware" "^4.2.14"
|
||||
"@smithy/util-retry" "^4.3.6"
|
||||
"@smithy/util-utf8" "^4.2.2"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/crc64-nvme@^3.972.7":
|
||||
version "3.972.7"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.7.tgz"
|
||||
integrity sha512-QUagVVBbC8gODCF6e1aV0mE2TXWB9Opz4k8EJFdNrujUVQm5R4AjJa1mpOqzwOuROBzqJU9zawzig7M96L8Ejg==
|
||||
dependencies:
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/credential-provider-env@^3.972.34":
|
||||
version "3.972.34"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.34.tgz"
|
||||
integrity sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==
|
||||
dependencies:
|
||||
"@aws-sdk/core" "^3.974.8"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/property-provider" "^4.2.14"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/credential-provider-http@^3.972.36":
|
||||
version "3.972.36"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.36.tgz"
|
||||
integrity sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==
|
||||
dependencies:
|
||||
"@aws-sdk/core" "^3.974.8"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/fetch-http-handler" "^5.3.17"
|
||||
"@smithy/node-http-handler" "^4.6.1"
|
||||
"@smithy/property-provider" "^4.2.14"
|
||||
"@smithy/protocol-http" "^5.3.14"
|
||||
"@smithy/smithy-client" "^4.12.13"
|
||||
"@smithy/types" "^4.14.1"
|
||||
"@smithy/util-stream" "^4.5.25"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/credential-provider-ini@^3.972.38":
|
||||
version "3.972.38"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.38.tgz"
|
||||
integrity sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==
|
||||
dependencies:
|
||||
"@aws-sdk/core" "^3.974.8"
|
||||
"@aws-sdk/credential-provider-env" "^3.972.34"
|
||||
"@aws-sdk/credential-provider-http" "^3.972.36"
|
||||
"@aws-sdk/credential-provider-login" "^3.972.38"
|
||||
"@aws-sdk/credential-provider-process" "^3.972.34"
|
||||
"@aws-sdk/credential-provider-sso" "^3.972.38"
|
||||
"@aws-sdk/credential-provider-web-identity" "^3.972.38"
|
||||
"@aws-sdk/nested-clients" "^3.997.6"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/credential-provider-imds" "^4.2.14"
|
||||
"@smithy/property-provider" "^4.2.14"
|
||||
"@smithy/shared-ini-file-loader" "^4.4.9"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/credential-provider-login@^3.972.38":
|
||||
version "3.972.38"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.38.tgz"
|
||||
integrity sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==
|
||||
dependencies:
|
||||
"@aws-sdk/core" "^3.974.8"
|
||||
"@aws-sdk/nested-clients" "^3.997.6"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/property-provider" "^4.2.14"
|
||||
"@smithy/protocol-http" "^5.3.14"
|
||||
"@smithy/shared-ini-file-loader" "^4.4.9"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/credential-provider-node@^3.972.39":
|
||||
version "3.972.39"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.39.tgz"
|
||||
integrity sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==
|
||||
dependencies:
|
||||
"@aws-sdk/credential-provider-env" "^3.972.34"
|
||||
"@aws-sdk/credential-provider-http" "^3.972.36"
|
||||
"@aws-sdk/credential-provider-ini" "^3.972.38"
|
||||
"@aws-sdk/credential-provider-process" "^3.972.34"
|
||||
"@aws-sdk/credential-provider-sso" "^3.972.38"
|
||||
"@aws-sdk/credential-provider-web-identity" "^3.972.38"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/credential-provider-imds" "^4.2.14"
|
||||
"@smithy/property-provider" "^4.2.14"
|
||||
"@smithy/shared-ini-file-loader" "^4.4.9"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/credential-provider-process@^3.972.34":
|
||||
version "3.972.34"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.34.tgz"
|
||||
integrity sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==
|
||||
dependencies:
|
||||
"@aws-sdk/core" "^3.974.8"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/property-provider" "^4.2.14"
|
||||
"@smithy/shared-ini-file-loader" "^4.4.9"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/credential-provider-sso@^3.972.38":
|
||||
version "3.972.38"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.38.tgz"
|
||||
integrity sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==
|
||||
dependencies:
|
||||
"@aws-sdk/core" "^3.974.8"
|
||||
"@aws-sdk/nested-clients" "^3.997.6"
|
||||
"@aws-sdk/token-providers" "3.1041.0"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/property-provider" "^4.2.14"
|
||||
"@smithy/shared-ini-file-loader" "^4.4.9"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/credential-provider-web-identity@^3.972.38":
|
||||
version "3.972.38"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz"
|
||||
integrity sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==
|
||||
dependencies:
|
||||
"@aws-sdk/core" "^3.974.8"
|
||||
"@aws-sdk/nested-clients" "^3.997.6"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/property-provider" "^4.2.14"
|
||||
"@smithy/shared-ini-file-loader" "^4.4.9"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/middleware-bucket-endpoint@^3.972.10":
|
||||
version "3.972.10"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.10.tgz"
|
||||
integrity sha512-Vbc2frZH7wXlMNd+ZZSXUEs/l1Sv8Jj4zUnIfwrYF5lwaLdXHZ9xx4U3rjUcaye3HRhFVc+E5DbBxpRAbB16BA==
|
||||
dependencies:
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@aws-sdk/util-arn-parser" "^3.972.3"
|
||||
"@smithy/node-config-provider" "^4.3.14"
|
||||
"@smithy/protocol-http" "^5.3.14"
|
||||
"@smithy/types" "^4.14.1"
|
||||
"@smithy/util-config-provider" "^4.2.2"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/middleware-expect-continue@^3.972.10":
|
||||
version "3.972.10"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.10.tgz"
|
||||
integrity sha512-2Yn0f1Qiq/DjxYR3wfI3LokXnjOhFM7Ssn4LTdFDIxRMCE6I32MAsVnhPX1cUZsuVA9tiZtwwhlSLAtFGxAZlQ==
|
||||
dependencies:
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/protocol-http" "^5.3.14"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/middleware-flexible-checksums@^3.974.16":
|
||||
version "3.974.16"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.16.tgz"
|
||||
integrity sha512-6ru8doI0/XzszqLIPXf0E/V7HhAw1Pu94010XCKYtBUfD0LxF0BuOzrUf8OQGR6j2o6wgKTHUniOmndQycHwCA==
|
||||
dependencies:
|
||||
"@aws-crypto/crc32" "5.2.0"
|
||||
"@aws-crypto/crc32c" "5.2.0"
|
||||
"@aws-crypto/util" "5.2.0"
|
||||
"@aws-sdk/core" "^3.974.8"
|
||||
"@aws-sdk/crc64-nvme" "^3.972.7"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/is-array-buffer" "^4.2.2"
|
||||
"@smithy/node-config-provider" "^4.3.14"
|
||||
"@smithy/protocol-http" "^5.3.14"
|
||||
"@smithy/types" "^4.14.1"
|
||||
"@smithy/util-middleware" "^4.2.14"
|
||||
"@smithy/util-stream" "^4.5.25"
|
||||
"@smithy/util-utf8" "^4.2.2"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/middleware-host-header@^3.972.10":
|
||||
version "3.972.10"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz"
|
||||
integrity sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==
|
||||
dependencies:
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/protocol-http" "^5.3.14"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/middleware-location-constraint@^3.972.10":
|
||||
version "3.972.10"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.10.tgz"
|
||||
integrity sha512-rI3NZvJcEvjoD0+0PI0iUAwlPw2IlSlhyvgBK/3WkKJQE/YiKFedd9dMN2lVacdNxPNhxL/jzQaKQdrGtQagjQ==
|
||||
dependencies:
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/middleware-logger@^3.972.10":
|
||||
version "3.972.10"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz"
|
||||
integrity sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==
|
||||
dependencies:
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/middleware-recursion-detection@^3.972.11":
|
||||
version "3.972.11"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz"
|
||||
integrity sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==
|
||||
dependencies:
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@aws/lambda-invoke-store" "^0.2.2"
|
||||
"@smithy/protocol-http" "^5.3.14"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/middleware-sdk-s3@^3.972.37":
|
||||
version "3.972.37"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.37.tgz"
|
||||
integrity sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==
|
||||
dependencies:
|
||||
"@aws-sdk/core" "^3.974.8"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@aws-sdk/util-arn-parser" "^3.972.3"
|
||||
"@smithy/core" "^3.23.17"
|
||||
"@smithy/node-config-provider" "^4.3.14"
|
||||
"@smithy/protocol-http" "^5.3.14"
|
||||
"@smithy/signature-v4" "^5.3.14"
|
||||
"@smithy/smithy-client" "^4.12.13"
|
||||
"@smithy/types" "^4.14.1"
|
||||
"@smithy/util-config-provider" "^4.2.2"
|
||||
"@smithy/util-middleware" "^4.2.14"
|
||||
"@smithy/util-stream" "^4.5.25"
|
||||
"@smithy/util-utf8" "^4.2.2"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/middleware-ssec@^3.972.10":
|
||||
version "3.972.10"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.10.tgz"
|
||||
integrity sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw==
|
||||
dependencies:
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/middleware-user-agent@^3.972.38":
|
||||
version "3.972.38"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.38.tgz"
|
||||
integrity sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==
|
||||
dependencies:
|
||||
"@aws-sdk/core" "^3.974.8"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@aws-sdk/util-endpoints" "^3.996.8"
|
||||
"@smithy/core" "^3.23.17"
|
||||
"@smithy/protocol-http" "^5.3.14"
|
||||
"@smithy/types" "^4.14.1"
|
||||
"@smithy/util-retry" "^4.3.6"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/nested-clients@^3.997.6":
|
||||
version "3.997.6"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.6.tgz"
|
||||
integrity sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==
|
||||
dependencies:
|
||||
"@aws-crypto/sha256-browser" "5.2.0"
|
||||
"@aws-crypto/sha256-js" "5.2.0"
|
||||
"@aws-sdk/core" "^3.974.8"
|
||||
"@aws-sdk/middleware-host-header" "^3.972.10"
|
||||
"@aws-sdk/middleware-logger" "^3.972.10"
|
||||
"@aws-sdk/middleware-recursion-detection" "^3.972.11"
|
||||
"@aws-sdk/middleware-user-agent" "^3.972.38"
|
||||
"@aws-sdk/region-config-resolver" "^3.972.13"
|
||||
"@aws-sdk/signature-v4-multi-region" "^3.996.25"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@aws-sdk/util-endpoints" "^3.996.8"
|
||||
"@aws-sdk/util-user-agent-browser" "^3.972.10"
|
||||
"@aws-sdk/util-user-agent-node" "^3.973.24"
|
||||
"@smithy/config-resolver" "^4.4.17"
|
||||
"@smithy/core" "^3.23.17"
|
||||
"@smithy/fetch-http-handler" "^5.3.17"
|
||||
"@smithy/hash-node" "^4.2.14"
|
||||
"@smithy/invalid-dependency" "^4.2.14"
|
||||
"@smithy/middleware-content-length" "^4.2.14"
|
||||
"@smithy/middleware-endpoint" "^4.4.32"
|
||||
"@smithy/middleware-retry" "^4.5.7"
|
||||
"@smithy/middleware-serde" "^4.2.20"
|
||||
"@smithy/middleware-stack" "^4.2.14"
|
||||
"@smithy/node-config-provider" "^4.3.14"
|
||||
"@smithy/node-http-handler" "^4.6.1"
|
||||
"@smithy/protocol-http" "^5.3.14"
|
||||
"@smithy/smithy-client" "^4.12.13"
|
||||
"@smithy/types" "^4.14.1"
|
||||
"@smithy/url-parser" "^4.2.14"
|
||||
"@smithy/util-base64" "^4.3.2"
|
||||
"@smithy/util-body-length-browser" "^4.2.2"
|
||||
"@smithy/util-body-length-node" "^4.2.3"
|
||||
"@smithy/util-defaults-mode-browser" "^4.3.49"
|
||||
"@smithy/util-defaults-mode-node" "^4.2.54"
|
||||
"@smithy/util-endpoints" "^3.4.2"
|
||||
"@smithy/util-middleware" "^4.2.14"
|
||||
"@smithy/util-retry" "^4.3.6"
|
||||
"@smithy/util-utf8" "^4.2.2"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/region-config-resolver@^3.972.13":
|
||||
version "3.972.13"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.13.tgz"
|
||||
integrity sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==
|
||||
dependencies:
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/config-resolver" "^4.4.17"
|
||||
"@smithy/node-config-provider" "^4.3.14"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/signature-v4-multi-region@^3.996.25":
|
||||
version "3.996.25"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.25.tgz"
|
||||
integrity sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==
|
||||
dependencies:
|
||||
"@aws-sdk/middleware-sdk-s3" "^3.972.37"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/protocol-http" "^5.3.14"
|
||||
"@smithy/signature-v4" "^5.3.14"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/token-providers@3.1041.0":
|
||||
version "3.1041.0"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz"
|
||||
integrity sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==
|
||||
dependencies:
|
||||
"@aws-sdk/core" "^3.974.8"
|
||||
"@aws-sdk/nested-clients" "^3.997.6"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/property-provider" "^4.2.14"
|
||||
"@smithy/shared-ini-file-loader" "^4.4.9"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/types@^3.222.0", "@aws-sdk/types@^3.973.8":
|
||||
version "3.973.8"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz"
|
||||
integrity sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==
|
||||
dependencies:
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/util-arn-parser@^3.972.3":
|
||||
version "3.972.3"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz"
|
||||
integrity sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==
|
||||
dependencies:
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/util-endpoints@^3.996.8":
|
||||
version "3.996.8"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz"
|
||||
integrity sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==
|
||||
dependencies:
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/types" "^4.14.1"
|
||||
"@smithy/url-parser" "^4.2.14"
|
||||
"@smithy/util-endpoints" "^3.4.2"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/util-locate-window@^3.0.0":
|
||||
version "3.965.5"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz"
|
||||
integrity sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==
|
||||
dependencies:
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/util-user-agent-browser@^3.972.10":
|
||||
version "3.972.10"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz"
|
||||
integrity sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==
|
||||
dependencies:
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/types" "^4.14.1"
|
||||
bowser "^2.11.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/util-user-agent-node@^3.973.24":
|
||||
version "3.973.24"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.24.tgz"
|
||||
integrity sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==
|
||||
dependencies:
|
||||
"@aws-sdk/middleware-user-agent" "^3.972.38"
|
||||
"@aws-sdk/types" "^3.973.8"
|
||||
"@smithy/node-config-provider" "^4.3.14"
|
||||
"@smithy/types" "^4.14.1"
|
||||
"@smithy/util-config-provider" "^4.2.2"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws-sdk/xml-builder@^3.972.22":
|
||||
version "3.972.22"
|
||||
resolved "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz"
|
||||
integrity sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==
|
||||
dependencies:
|
||||
"@nodable/entities" "2.1.0"
|
||||
"@smithy/types" "^4.14.1"
|
||||
fast-xml-parser "5.7.2"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@aws/lambda-invoke-store@^0.2.2":
|
||||
version "0.2.4"
|
||||
resolved "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz"
|
||||
integrity sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==
|
||||
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz"
|
||||
@@ -591,6 +1142,11 @@
|
||||
resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz"
|
||||
integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==
|
||||
|
||||
"@nodable/entities@^2.1.0", "@nodable/entities@2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz"
|
||||
integrity sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
|
||||
@@ -830,6 +1386,346 @@
|
||||
resolved "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz"
|
||||
integrity sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==
|
||||
|
||||
"@smithy/config-resolver@^4.4.17":
|
||||
version "4.5.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.5.0.tgz"
|
||||
integrity sha512-m5PNfr7xKdIegNG8DlLz+Gf/DlAhHWFGmFbe0DZo9pnvBwuZ3P/9OMtQU0UyWMYy8zjl+HDFVS7rdD9p2xEFjQ==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/core@^3.23.17", "@smithy/core@^3.24.0":
|
||||
version "3.24.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/core/-/core-3.24.0.tgz"
|
||||
integrity sha512-rZ5YfycIXX6puoGjthnDiMpUgtKNOq3c7CndQYkCNYQTv26AiCrZQOJPy7ANSfZ6Okk3UvCRnmO1OYWlLnYZgg==
|
||||
dependencies:
|
||||
"@aws-crypto/crc32" "5.2.0"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/credential-provider-imds@^4.2.14":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.0.tgz"
|
||||
integrity sha512-5gi+28FH+RurB2+tcRH1CK7KiLJ0dVnabjWLY3DgeFLiU45dbyrsq7NOYvMUcHgu9LVZH5F7G+Qk1GdXF0y6jg==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/eventstream-serde-browser@^4.2.14":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.3.0.tgz"
|
||||
integrity sha512-JlY17/ZwBJ2O7FK/bKt8PZR+HBkyFwvgssgT6LiB0xYtz5/E5XG/HeKr5q2NMaVm8u8xjFfGk/6DVlbBe1qNkA==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/eventstream-serde-config-resolver@^4.3.14":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.4.0.tgz"
|
||||
integrity sha512-1Pg7aqxIdMilTbGJKCHTx0toIkKSrHdO6VHCh9oCncWJG+1wkJa90O/xb9mmRPuoOFCg2DLZAqnRyuBiUQnNIA==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/eventstream-serde-node@^4.2.14":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.3.0.tgz"
|
||||
integrity sha512-Xte1Td6CQpc/D0WnPZ2k98CvF7y1GopylMoGY/r26a9wbRHV5xusRbT6O9vouSeZlvtxoVb4ON/1fLRofO7m4Q==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/fetch-http-handler@^5.3.17":
|
||||
version "5.4.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.0.tgz"
|
||||
integrity sha512-yxurumLvHfgYgM0FVtjOVIyBSJXfno4xKKOgD43wOk9Qh+2lTKfP9Qhu4JHU7IUwrqVPa888byUzomHMgvKVMg==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/hash-blob-browser@^4.2.15":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.3.0.tgz"
|
||||
integrity sha512-xOQ7w5hSzTe8IAwQ6BAbX+1d9s9SRwWUbU8cYYh5DHgkYOi131Q5+B9Blelg9u0zoPfM4VYOuuBRAmregPkzpQ==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/hash-node@^4.2.14":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.3.0.tgz"
|
||||
integrity sha512-4a+KoVqr1SZtw7cZvY24XU1S5OL+c23MdDQ3jFmMCQ5s9diBFdMG/UIgp5dNqlwvDrWA0U5KO+z3Gzq1ize+LA==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/hash-stream-node@^4.2.14":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.3.0.tgz"
|
||||
integrity sha512-DPDT0UyOREMPwipO7BzSJfD8z2YYp/x1FMpHqEppBF+gmGr7FoqUUFlXfK+YPOfHhlr8HDJRQpmShNb9C9po5Q==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/invalid-dependency@^4.2.14":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.3.0.tgz"
|
||||
integrity sha512-TaoGtqi2ZNdGzxUgYcLczjW8rb/h5DQ8vlCMYDSdZ4LRzGQrrEYgUjlZVM9dAagTsLK5gZx1f7+44sFTjz5vuQ==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/is-array-buffer@^2.2.0":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz"
|
||||
integrity sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==
|
||||
dependencies:
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/is-array-buffer@^4.2.2":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.3.0.tgz"
|
||||
integrity sha512-V9ZCT5mHNteWOKtu3LkGHTheEyBWzTU3XydeSOHrqG6znEoCARjxFZ0XP9JJyKV9jxEMx7hNbzGlQaXtKkmvSQ==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/md5-js@^4.2.14":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.3.0.tgz"
|
||||
integrity sha512-bODwWrXILREpCL7XZq1/QxiMqFxdWadw3noiKGjNKsPl1/nvWtLVFInNjK6pmHjM4BUOgfgZkrl9+V56ez2FNA==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/middleware-content-length@^4.2.14":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.3.0.tgz"
|
||||
integrity sha512-IbSiS/3nOxsimCthzElEoBrjQo+Na4bsQ63qyC8qSI8lkMjOv9+VlosDQd8gfNolAD9XmC5tLqYTI0bJGJsscg==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/middleware-endpoint@^4.4.32":
|
||||
version "4.5.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.5.0.tgz"
|
||||
integrity sha512-ux8LgN/m/X7ET2ISRc8G4aKFI1QhINZtkKpoayNPTrhwpsCVxb47mlpYFuWceTlesc0Wmb0S9y6DP195ReQoXA==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/middleware-retry@^4.5.7":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.6.0.tgz"
|
||||
integrity sha512-8CtxY9aHT4f3UvZUbU2O0bccRckqTDfTKk3t1DawUZa5DWRZdV2AMABLsdMTdj7KE1uumhzEaT0X7/jTcOtoBw==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/middleware-serde@^4.2.20":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.3.0.tgz"
|
||||
integrity sha512-c+V02hZlIStscI4ie2VllJjM4DLxdI2SymIBvXmqCqicrNb0NAbgDXDTBiwcMiruaBOqEFYxpKXbz6JjsNEN3Q==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/middleware-stack@^4.2.14":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.3.0.tgz"
|
||||
integrity sha512-KtYcs+sJn7AiT0YdM53/6MT0dKsaW2MSAr9MpprRVSfwN9qyKQf2dBIuCXt18/nEZaWerol/bGaQ63G949aovw==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/node-config-provider@^4.3.14":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.4.0.tgz"
|
||||
integrity sha512-5RutFJsYoqK4tWYZOjGQrPLowGf2Ku8rbNuVeGkNJ5axIDO4LV/fydBojPtwcDz2zf87YNCOXfNyuEyAwYgI7A==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/node-http-handler@^4.6.1":
|
||||
version "4.7.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.0.tgz"
|
||||
integrity sha512-PxF57Jr3dPm+RgZWekOL+o96FPdaT62xZUyDfi47uMRFi5rHpwO/ewFbrztrASQ/7H8moNi1sspIHihHpfoKsQ==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/property-provider@^4.2.14":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.3.0.tgz"
|
||||
integrity sha512-/YBWtO2SdvPSAUk/Ke1Xpdg1E1lfaNGblla7mnIVGtaGkSQ5bK7KBZqpuj5IokHlU9UcLDvt2QwTLV7oRzBUTA==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/protocol-http@^5.3.14":
|
||||
version "5.4.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.4.0.tgz"
|
||||
integrity sha512-WG0LgSZg+WbvWYD04uwIYVyMEpyd0cPx1lkqx61JxunxiFti+wGoFiDKr6wswun1r25Z2f8yUoMQWyxjMnnXtw==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/shared-ini-file-loader@^4.4.9":
|
||||
version "4.5.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.5.0.tgz"
|
||||
integrity sha512-xATpw6gcurFztdsUrMNaKb2ugqk3545Whhqg7ZD4sxTg+zI27THjg3IY+InXsVWturOWdCdV+UHQx11g9Sp5Kw==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/signature-v4@^5.3.14":
|
||||
version "5.4.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.0.tgz"
|
||||
integrity sha512-nkdB9T8JS6iD5PukE5TB8KqcvMEPVPHVUY7J0odYJgyIM40Du2msUhBdoPNRqRArDDcGQqVQcbzu0CZA7b+Nkw==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/smithy-client@^4.12.13":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.13.0.tgz"
|
||||
integrity sha512-lysfoRCr7PdD9CsPp9VQuJYRGI5mWYb8FRkbdBSQttxpQmW7tZsFgmpBNKVcgvBsAgBCkYX/UQs0NmznuBcZQQ==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
"@smithy/types" "^4.14.1"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/types@^4.14.1":
|
||||
version "4.14.1"
|
||||
resolved "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz"
|
||||
integrity sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==
|
||||
dependencies:
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/url-parser@^4.2.14":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.3.0.tgz"
|
||||
integrity sha512-I5tCWs/ndLrJrbvlnsN1cOt8PVAbQEqg0nNeQqebD5ynQcbhgch9uA7KmpX9vfq/vEudq0iVYAOxt+4aBkUlWA==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/util-base64@^4.3.2":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.4.0.tgz"
|
||||
integrity sha512-puJITyefgQ9a5F+wKylCLkf0VCwesWbaN4O3YCEalRin4N0CTPQu/XA3kz/QsMOTgd3knhd0BQwGCBm/tv0Y1A==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/util-body-length-browser@^4.2.2":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.3.0.tgz"
|
||||
integrity sha512-83U8xa8EmdExGzFuqBzgXvtmbLQIYcCuCNm5no4rlPqpGdOPGUufzMvLdlw+sPTb01qHIsDDNwOecm4s8ROOPw==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/util-body-length-node@^4.2.3":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.3.0.tgz"
|
||||
integrity sha512-Ok2v9zPFfd6uOJMTIIJ8HFdCpARD77q4OHYhwhG9y5X1Y9oeQ0CHUQVJD6LhT6l8FUkFYisqcUaZSg7SArFUTA==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/util-buffer-from@^2.2.0":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz"
|
||||
integrity sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==
|
||||
dependencies:
|
||||
"@smithy/is-array-buffer" "^2.2.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/util-config-provider@^4.2.2":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.3.0.tgz"
|
||||
integrity sha512-kAC6/UB9qW9r2xQAOko2iDxAXmRD2VGMZjnXSEacAhQySdJs58CwvoOE0tHWdtc/lWF4g78X6Z9ucLanJnuVUw==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/util-defaults-mode-browser@^4.3.49":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.4.0.tgz"
|
||||
integrity sha512-jKezW5Taa+N2gbkB02UVijH1rFlEJC+cskZzwasFqFJMBBi/bcVgHqcYOX0WOnUk6MDZfHf0gEsr5Br4XMHiAg==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/util-defaults-mode-node@^4.2.54":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.3.0.tgz"
|
||||
integrity sha512-xYRuNHHIztu5AzruMJ8kTyA1JsBL/yZKvX5z/A7OHUxsf+rkEESZFZWJDcAj5dDWSu6brWFe5KH6qJNTVztX/w==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/util-endpoints@^3.4.2":
|
||||
version "3.5.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.5.0.tgz"
|
||||
integrity sha512-pcvTCp9Wch/9UnWWfRGoG5GJogDXFPjevE+CqALxtPFGA4GqFQRD6eUtgJhHN+NPtohcozI12u1skF2/iubGrQ==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/util-middleware@^4.2.14":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.3.0.tgz"
|
||||
integrity sha512-X/DNQxgUCbjjs3HosLmt5Yi1NocxjRFiiOgHml4tVV3w4mIbqZxPR8kq7apGPEMnhIpyxeTgFyypMrfxfn2DlQ==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/util-retry@^4.3.6":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.4.0.tgz"
|
||||
integrity sha512-pV/Kq4jUuP9raOqwSPeBiut2IWmwbc9vM+nE3ly4YUkzPHbBZvfhikwMOyudER+KHPjakuc8r4TecEPMsI7nVg==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/util-stream@^4.5.25":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.6.0.tgz"
|
||||
integrity sha512-BlWg46UASokl3O5YqWmbLpINE5stmAxynXlyOe1nE4dx+tvwgqtT4ug/rPcRg0xVcBnj68XlcOqbXeaGGcH0DA==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/util-utf8@^2.0.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz"
|
||||
integrity sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==
|
||||
dependencies:
|
||||
"@smithy/util-buffer-from" "^2.2.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/util-utf8@^4.2.2":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.3.0.tgz"
|
||||
integrity sha512-5hrmCc+dTgZkiFhX72Q16LemYPkvZ1M4pFMOhk0X9tQnLY7dn7zC1+C+aAJn0dw6CXldbqY/KMbMYCwm8yw14g==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@smithy/util-waiter@^4.3.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.4.0.tgz"
|
||||
integrity sha512-7kAlrB3n7/BHyw+uLq83d5jdadPUcDkdMOUSGxvpXjrJ++G0hTedTnoNChjybIxhZ/Gk7sCrfIOLkMAB0LhRBA==
|
||||
dependencies:
|
||||
"@smithy/core" "^3.24.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@standard-schema/spec@^1.0.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz"
|
||||
@@ -1294,6 +2190,11 @@ body-parser@^2.2.1:
|
||||
raw-body "^3.0.1"
|
||||
type-is "^2.0.1"
|
||||
|
||||
bowser@^2.11.0:
|
||||
version "2.14.1"
|
||||
resolved "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz"
|
||||
integrity sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.13"
|
||||
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz"
|
||||
@@ -2367,6 +3268,24 @@ fast-uri@^3.0.1:
|
||||
resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz"
|
||||
integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==
|
||||
|
||||
fast-xml-builder@^1.1.5:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz"
|
||||
integrity sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==
|
||||
dependencies:
|
||||
path-expression-matcher "^1.5.0"
|
||||
xml-naming "^0.1.0"
|
||||
|
||||
fast-xml-parser@5.7.2:
|
||||
version "5.7.2"
|
||||
resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz"
|
||||
integrity sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==
|
||||
dependencies:
|
||||
"@nodable/entities" "^2.1.0"
|
||||
fast-xml-builder "^1.1.5"
|
||||
path-expression-matcher "^1.5.0"
|
||||
strnum "^2.2.3"
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.20.1"
|
||||
resolved "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz"
|
||||
@@ -3747,6 +4666,11 @@ path-exists@^4.0.0:
|
||||
resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
|
||||
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
|
||||
|
||||
path-expression-matcher@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz"
|
||||
integrity sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==
|
||||
|
||||
path-key@^3.0.0, path-key@^3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz"
|
||||
@@ -4545,6 +5469,11 @@ strip-json-comments@^3.1.1:
|
||||
resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
strnum@^2.2.3:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz"
|
||||
integrity sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==
|
||||
|
||||
styled-jsx@5.1.6:
|
||||
version "5.1.6"
|
||||
resolved "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz"
|
||||
@@ -4672,7 +5601,7 @@ tsconfig-paths@^4.2.0:
|
||||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.8.0:
|
||||
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.6.2, tslib@^2.8.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
@@ -5015,6 +5944,11 @@ wsl-utils@^0.3.0:
|
||||
is-wsl "^3.1.0"
|
||||
powershell-utils "^0.1.0"
|
||||
|
||||
xml-naming@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz"
|
||||
integrity sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz"
|
||||
|
||||
Reference in New Issue
Block a user