mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
Migrate component state to Jotai atoms across settings, explore, and dashboard
Replace ad-hoc useState/useEffect fetch patterns with Jotai atoms and loadables in StatsDisplay, FilterableTitleRow, BackupScheduleSection, IntegrationsSection, and CommandPalette. Each component now gets a scoped Jotai Provider with a pre-initialized store so server-rendered initial values hydrate correctly. Async data fetching moves into atom-level loadables, eliminating manual loading flags and cancellation logic throughout.
This commit is contained in:
@@ -7,8 +7,8 @@ import {
|
||||
IconMovie,
|
||||
IconPlayerPlay,
|
||||
} from "@tabler/icons-react";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { motion } from "motion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -16,6 +16,12 @@ import {
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
episodePeriodAtom,
|
||||
episodeStatsLoadable,
|
||||
moviePeriodAtom,
|
||||
movieStatsLoadable,
|
||||
} from "@/lib/atoms/stats";
|
||||
import type {
|
||||
DashboardStats,
|
||||
HistoryBucket,
|
||||
@@ -122,50 +128,27 @@ function PeriodSelector({
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchStats(
|
||||
type: "movies" | "episodes",
|
||||
period: TimePeriod,
|
||||
): Promise<{ count: number; history: HistoryBucket[] }> {
|
||||
const res = await fetch(
|
||||
`/api/stats?type=${type}&period=${period}&history=true`,
|
||||
);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export function StatsDisplay({ stats }: { stats: DashboardStats }) {
|
||||
const [moviePeriod, setMoviePeriod] = useState<TimePeriod>("this_month");
|
||||
const [episodePeriod, setEpisodePeriod] = useState<TimePeriod>("this_week");
|
||||
const [movieCount, setMovieCount] = useState(stats.moviesThisMonth);
|
||||
const [episodeCount, setEpisodeCount] = useState(stats.episodesThisWeek);
|
||||
const [movieLoading, setMovieLoading] = useState(false);
|
||||
const [episodeLoading, setEpisodeLoading] = useState(false);
|
||||
const [movieHistory, setMovieHistory] = useState<HistoryBucket[]>();
|
||||
const [episodeHistory, setEpisodeHistory] = useState<HistoryBucket[]>();
|
||||
const [moviePeriod, setMoviePeriod] = useAtom(moviePeriodAtom);
|
||||
const [episodePeriod, setEpisodePeriod] = useAtom(episodePeriodAtom);
|
||||
const movieStats = useAtomValue(movieStatsLoadable);
|
||||
const episodeStats = useAtomValue(episodeStatsLoadable);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats("movies", "this_month").then((d) => setMovieHistory(d.history));
|
||||
fetchStats("episodes", "this_week").then((d) =>
|
||||
setEpisodeHistory(d.history),
|
||||
);
|
||||
}, []);
|
||||
const movieLoading = movieStats.state === "loading";
|
||||
const movieCount =
|
||||
movieStats.state === "hasData"
|
||||
? movieStats.data.count
|
||||
: stats.moviesThisMonth;
|
||||
const movieHistory =
|
||||
movieStats.state === "hasData" ? movieStats.data.history : undefined;
|
||||
|
||||
async function handleMoviePeriodChange(period: TimePeriod) {
|
||||
setMoviePeriod(period);
|
||||
setMovieLoading(true);
|
||||
const data = await fetchStats("movies", period);
|
||||
setMovieCount(data.count);
|
||||
setMovieHistory(data.history);
|
||||
setMovieLoading(false);
|
||||
}
|
||||
|
||||
async function handleEpisodePeriodChange(period: TimePeriod) {
|
||||
setEpisodePeriod(period);
|
||||
setEpisodeLoading(true);
|
||||
const data = await fetchStats("episodes", period);
|
||||
setEpisodeCount(data.count);
|
||||
setEpisodeHistory(data.history);
|
||||
setEpisodeLoading(false);
|
||||
}
|
||||
const episodeLoading = episodeStats.state === "loading";
|
||||
const episodeCount =
|
||||
episodeStats.state === "hasData"
|
||||
? episodeStats.data.count
|
||||
: stats.episodesThisWeek;
|
||||
const episodeHistory =
|
||||
episodeStats.state === "hasData" ? episodeStats.data.history : undefined;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
@@ -181,7 +164,7 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
|
||||
<PeriodSelector
|
||||
noun="Movies"
|
||||
period={moviePeriod}
|
||||
onPeriodChange={handleMoviePeriodChange}
|
||||
onPeriodChange={setMoviePeriod}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -197,7 +180,7 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
|
||||
<PeriodSelector
|
||||
noun="Episodes"
|
||||
period={episodePeriod}
|
||||
onPeriodChange={handleEpisodePeriodChange}
|
||||
onPeriodChange={setEpisodePeriod}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures";
|
||||
import { createStore, Provider, useAtom, useAtomValue } from "jotai";
|
||||
import { motion } from "motion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { TitleCardSkeleton } from "@/components/skeletons";
|
||||
import { TitleCard } from "@/components/title-card";
|
||||
import {
|
||||
@@ -10,6 +11,12 @@ import {
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
} from "@/components/ui/carousel";
|
||||
import {
|
||||
defaultItemsAtom,
|
||||
genreResultsLoadable,
|
||||
mediaTypeAtom,
|
||||
selectedGenreAtom,
|
||||
} from "@/lib/atoms/filterable-row";
|
||||
|
||||
interface Genre {
|
||||
id: number;
|
||||
@@ -55,38 +62,40 @@ export function FilterableTitleRow({
|
||||
defaultItems,
|
||||
genres,
|
||||
}: FilterableTitleRowProps) {
|
||||
const [selectedGenre, setSelectedGenre] = useState<number | null>(null);
|
||||
const [results, setResults] = useState<TitleRowItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [store] = useState(() => {
|
||||
const s = createStore();
|
||||
s.set(mediaTypeAtom, mediaType);
|
||||
s.set(defaultItemsAtom, defaultItems);
|
||||
return s;
|
||||
});
|
||||
|
||||
const items = selectedGenre === null ? defaultItems : results;
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<FilterableTitleRowInner heading={heading} icon={icon} genres={genres} />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedGenre === null) {
|
||||
setResults([]);
|
||||
return;
|
||||
}
|
||||
function FilterableTitleRowInner({
|
||||
heading,
|
||||
icon,
|
||||
genres,
|
||||
}: {
|
||||
heading: string;
|
||||
icon: React.ReactNode;
|
||||
genres: Genre[];
|
||||
}) {
|
||||
const [selectedGenre, setSelectedGenre] = useAtom(selectedGenreAtom);
|
||||
const defaults = useAtomValue(defaultItemsAtom);
|
||||
const genreResults = useAtomValue(genreResultsLoadable);
|
||||
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
|
||||
fetch(
|
||||
`/api/explore/discover?type=${mediaType}&genre=${selectedGenre}&sort_by=popularity.desc`,
|
||||
)
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (!cancelled) {
|
||||
setResults(data.results ?? []);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedGenre, mediaType]);
|
||||
const loading = selectedGenre !== null && genreResults.state === "loading";
|
||||
const items =
|
||||
selectedGenre === null
|
||||
? defaults
|
||||
: genreResults.state === "hasData" && genreResults.data !== null
|
||||
? genreResults.data
|
||||
: [];
|
||||
|
||||
function toggleGenre(genreId: number) {
|
||||
setSelectedGenre(selectedGenre === genreId ? null : genreId);
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import { IconCalendarRepeat, IconChevronDown } from "@tabler/icons-react";
|
||||
import { format, formatDistanceToNow } from "date-fns";
|
||||
import { createStore, Provider, useAtomValue } from "jotai";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
@@ -16,10 +16,11 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
setBackupScheduleAction,
|
||||
setMaxBackupsAction,
|
||||
setScheduledBackupAction,
|
||||
} from "@/lib/actions/settings";
|
||||
backupScheduleAtom,
|
||||
savingScheduleAtom,
|
||||
togglingScheduleAtom,
|
||||
useBackupScheduleActions,
|
||||
} from "@/lib/atoms/backup-schedule";
|
||||
import type { BackupFrequency } from "@/lib/cron";
|
||||
|
||||
const FREQUENCY_OPTIONS: { value: BackupFrequency; label: string }[] = [
|
||||
@@ -115,68 +116,33 @@ export function BackupScheduleSection({
|
||||
initialTime: string;
|
||||
initialDow: number;
|
||||
}) {
|
||||
const [scheduledEnabled, setScheduledEnabled] = useState(
|
||||
initialScheduledEnabled,
|
||||
const [store] = useState(() => {
|
||||
const s = createStore();
|
||||
s.set(backupScheduleAtom, {
|
||||
enabled: initialScheduledEnabled,
|
||||
maxRetention: initialMaxRetention,
|
||||
frequency: initialFrequency,
|
||||
time: initialTime,
|
||||
dow: initialDow,
|
||||
});
|
||||
return s;
|
||||
});
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<BackupScheduleInner />
|
||||
</Provider>
|
||||
);
|
||||
const [maxRetention, setMaxRetention] = useState(initialMaxRetention);
|
||||
const [frequency, setFrequency] = useState<BackupFrequency>(initialFrequency);
|
||||
const [time, setTime] = useState(initialTime);
|
||||
const [dow, setDow] = useState(initialDow);
|
||||
const [savingSchedule, setSavingSchedule] = useState(false);
|
||||
const [togglingSchedule, setTogglingSchedule] = useState(false);
|
||||
}
|
||||
|
||||
async function handleToggleScheduled(checked: boolean) {
|
||||
const previous = scheduledEnabled;
|
||||
setScheduledEnabled(checked);
|
||||
setTogglingSchedule(true);
|
||||
try {
|
||||
await setScheduledBackupAction(checked);
|
||||
toast.success(
|
||||
checked ? "Scheduled backups enabled" : "Scheduled backups disabled",
|
||||
);
|
||||
} catch {
|
||||
setScheduledEnabled(previous);
|
||||
toast.error("Failed to update scheduled backup setting");
|
||||
} finally {
|
||||
setTogglingSchedule(false);
|
||||
}
|
||||
}
|
||||
function BackupScheduleInner() {
|
||||
const schedule = useAtomValue(backupScheduleAtom);
|
||||
const savingSchedule = useAtomValue(savingScheduleAtom);
|
||||
const togglingSchedule = useAtomValue(togglingScheduleAtom);
|
||||
const { toggleScheduled, changeMaxRetention, changeSchedule } =
|
||||
useBackupScheduleActions();
|
||||
|
||||
async function handleMaxRetentionChange(value: number) {
|
||||
const previous = maxRetention;
|
||||
setMaxRetention(value);
|
||||
try {
|
||||
await setMaxBackupsAction(value);
|
||||
} catch {
|
||||
setMaxRetention(previous);
|
||||
toast.error("Failed to update retention setting");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleScheduleChange(
|
||||
newFrequency: BackupFrequency,
|
||||
newTime: string,
|
||||
newDow = dow,
|
||||
) {
|
||||
const prevFreq = frequency;
|
||||
const prevTime = time;
|
||||
const prevDow = dow;
|
||||
setFrequency(newFrequency);
|
||||
setTime(newTime);
|
||||
setDow(newDow);
|
||||
setSavingSchedule(true);
|
||||
try {
|
||||
await setBackupScheduleAction(newFrequency, newTime, newDow);
|
||||
toast.success("Schedule updated");
|
||||
} catch {
|
||||
setFrequency(prevFreq);
|
||||
setTime(prevTime);
|
||||
setDow(prevDow);
|
||||
toast.error("Failed to update schedule");
|
||||
} finally {
|
||||
setSavingSchedule(false);
|
||||
}
|
||||
}
|
||||
const { enabled, maxRetention, frequency, time, dow } = schedule;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -189,7 +155,7 @@ export function BackupScheduleSection({
|
||||
<div>
|
||||
<CardTitle>Backup schedule</CardTitle>
|
||||
<CardDescription>
|
||||
{scheduledEnabled ? (
|
||||
{enabled ? (
|
||||
<span className="inline-flex flex-wrap items-baseline gap-1">
|
||||
<span suppressHydrationWarning>
|
||||
{formatNextBackup(frequency, time, dow)}.
|
||||
@@ -205,9 +171,7 @@ export function BackupScheduleSection({
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuRadioGroup
|
||||
value={String(maxRetention)}
|
||||
onValueChange={(v) =>
|
||||
handleMaxRetentionChange(Number(v))
|
||||
}
|
||||
onValueChange={(v) => changeMaxRetention(Number(v))}
|
||||
>
|
||||
{[3, 5, 7, 14, 30, 0].map((n) => (
|
||||
<DropdownMenuRadioItem key={n} value={String(n)}>
|
||||
@@ -226,15 +190,15 @@ export function BackupScheduleSection({
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={scheduledEnabled}
|
||||
onCheckedChange={handleToggleScheduled}
|
||||
checked={enabled}
|
||||
onCheckedChange={toggleScheduled}
|
||||
disabled={togglingSchedule}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{scheduledEnabled && (
|
||||
{enabled && (
|
||||
<CardContent className="border-t border-border/30 pt-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
@@ -256,7 +220,7 @@ export function BackupScheduleSection({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={savingSchedule}
|
||||
onClick={() => handleScheduleChange(opt.value, time)}
|
||||
onClick={() => changeSchedule(opt.value, time)}
|
||||
className={
|
||||
frequency === opt.value
|
||||
? "border-primary/50 bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 hover:text-primary-foreground"
|
||||
@@ -291,7 +255,7 @@ export function BackupScheduleSection({
|
||||
<DropdownMenuRadioGroup
|
||||
value={String(dow)}
|
||||
onValueChange={(v) =>
|
||||
handleScheduleChange(frequency, time, Number(v))
|
||||
changeSchedule(frequency, time, Number(v))
|
||||
}
|
||||
>
|
||||
{DAYS_OF_WEEK.map((day, i) => (
|
||||
@@ -341,9 +305,7 @@ export function BackupScheduleSection({
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuRadioGroup
|
||||
value={time}
|
||||
onValueChange={(v) =>
|
||||
handleScheduleChange(frequency, v)
|
||||
}
|
||||
onValueChange={(v) => changeSchedule(frequency, v)}
|
||||
>
|
||||
{HOURS.map((h) => {
|
||||
const val = `${String(h).padStart(2, "0")}:00`;
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { IconWebhook } from "@tabler/icons-react";
|
||||
import { createStore, Provider } from "jotai";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
deleteWebhookConnection,
|
||||
regenerateWebhookToken,
|
||||
saveWebhookConnection,
|
||||
} from "@/lib/actions/settings";
|
||||
import { connectionsAtom } from "@/lib/atoms/integrations";
|
||||
import { WebhookCard, type WebhookConnection } from "./webhook-card";
|
||||
|
||||
export function IntegrationsSection({
|
||||
@@ -15,109 +11,27 @@ export function IntegrationsSection({
|
||||
}: {
|
||||
initialConnections: WebhookConnection[];
|
||||
}) {
|
||||
const [connections, setConnections] =
|
||||
useState<WebhookConnection[]>(initialConnections);
|
||||
|
||||
const plexConnection = connections.find((c) => c.provider === "plex") ?? null;
|
||||
const jellyfinConnection =
|
||||
connections.find((c) => c.provider === "jellyfin") ?? null;
|
||||
const embyConnection = connections.find((c) => c.provider === "emby") ?? null;
|
||||
|
||||
async function handleConnect(provider: "plex" | "jellyfin" | "emby") {
|
||||
const label =
|
||||
provider === "plex" ? "Plex" : provider === "emby" ? "Emby" : "Jellyfin";
|
||||
try {
|
||||
const result = await saveWebhookConnection(provider);
|
||||
setConnections((prev) => [...prev, { ...result, recentEvents: [] }]);
|
||||
toast.success(`${label} connected`);
|
||||
} catch {
|
||||
toast.error(`Failed to connect ${label}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(provider: "plex" | "jellyfin" | "emby") {
|
||||
const label =
|
||||
provider === "plex" ? "Plex" : provider === "emby" ? "Emby" : "Jellyfin";
|
||||
const previous = connections;
|
||||
setConnections((prev) => prev.filter((c) => c.provider !== provider));
|
||||
try {
|
||||
await deleteWebhookConnection(provider);
|
||||
toast.success(`${label} disconnected`);
|
||||
} catch {
|
||||
setConnections(previous);
|
||||
toast.error(`Failed to disconnect ${label}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRegenerateToken(provider: "plex" | "jellyfin" | "emby") {
|
||||
const label =
|
||||
provider === "plex" ? "Plex" : provider === "emby" ? "Emby" : "Jellyfin";
|
||||
try {
|
||||
const result = await regenerateWebhookToken(provider);
|
||||
setConnections((prev) =>
|
||||
prev.map((c) =>
|
||||
c.provider === provider ? { ...c, token: result.token } : c,
|
||||
),
|
||||
);
|
||||
toast.success(`${label} webhook URL regenerated`);
|
||||
} catch {
|
||||
toast.error(`Failed to regenerate ${label} URL`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggle(
|
||||
provider: "plex" | "jellyfin" | "emby",
|
||||
enabled: boolean,
|
||||
) {
|
||||
const label =
|
||||
provider === "plex" ? "Plex" : provider === "emby" ? "Emby" : "Jellyfin";
|
||||
const previous = connections;
|
||||
setConnections((prev) =>
|
||||
prev.map((c) => (c.provider === provider ? { ...c, enabled } : c)),
|
||||
);
|
||||
try {
|
||||
await saveWebhookConnection(provider, enabled);
|
||||
toast.success(`${label} webhook ${enabled ? "enabled" : "disabled"}`);
|
||||
} catch {
|
||||
setConnections(previous);
|
||||
toast.error(`Failed to update ${label}`);
|
||||
}
|
||||
}
|
||||
const [store] = useState(() => {
|
||||
const s = createStore();
|
||||
s.set(connectionsAtom, initialConnections);
|
||||
return s;
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<IconWebhook className="size-4 text-muted-foreground" />
|
||||
<h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
Integrations
|
||||
</h2>
|
||||
<Provider store={store}>
|
||||
<div>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<IconWebhook className="size-4 text-muted-foreground" />
|
||||
<h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
Integrations
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<WebhookCard provider="plex" />
|
||||
<WebhookCard provider="jellyfin" />
|
||||
<WebhookCard provider="emby" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<WebhookCard
|
||||
provider="plex"
|
||||
connection={plexConnection}
|
||||
onConnect={handleConnect}
|
||||
onDelete={handleDelete}
|
||||
onRegenerateToken={handleRegenerateToken}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
<WebhookCard
|
||||
provider="jellyfin"
|
||||
connection={jellyfinConnection}
|
||||
onConnect={handleConnect}
|
||||
onDelete={handleDelete}
|
||||
onRegenerateToken={handleRegenerateToken}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
<WebhookCard
|
||||
provider="emby"
|
||||
connection={embyConnection}
|
||||
onConnect={handleConnect}
|
||||
onDelete={handleDelete}
|
||||
onRegenerateToken={handleRegenerateToken}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useConnectionActions } from "@/lib/atoms/integrations";
|
||||
import { EmbyIcon, JellyfinIcon, PlexIcon } from "./icons";
|
||||
|
||||
export interface WebhookConnection {
|
||||
@@ -51,22 +52,16 @@ export interface WebhookConnection {
|
||||
|
||||
export function WebhookCard({
|
||||
provider,
|
||||
connection,
|
||||
onConnect,
|
||||
onDelete,
|
||||
onRegenerateToken,
|
||||
onToggle,
|
||||
}: {
|
||||
provider: "plex" | "jellyfin" | "emby";
|
||||
connection: WebhookConnection | null;
|
||||
onConnect: (provider: "plex" | "jellyfin" | "emby") => Promise<void>;
|
||||
onDelete: (provider: "plex" | "jellyfin" | "emby") => Promise<void>;
|
||||
onRegenerateToken: (provider: "plex" | "jellyfin" | "emby") => Promise<void>;
|
||||
onToggle: (
|
||||
provider: "plex" | "jellyfin" | "emby",
|
||||
enabled: boolean,
|
||||
) => Promise<void>;
|
||||
}) {
|
||||
const {
|
||||
connection,
|
||||
handleConnect,
|
||||
handleDelete,
|
||||
handleRegenerateToken,
|
||||
handleToggle,
|
||||
} = useConnectionActions(provider);
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [setupOpen, setSetupOpen] = useState(false);
|
||||
@@ -81,10 +76,10 @@ export function WebhookCard({
|
||||
? `${window.location.origin}/api/webhooks/${connection.token}`
|
||||
: null;
|
||||
|
||||
async function handleConnect() {
|
||||
async function onConnect() {
|
||||
setConnecting(true);
|
||||
try {
|
||||
await onConnect(provider);
|
||||
await handleConnect();
|
||||
} finally {
|
||||
setConnecting(false);
|
||||
}
|
||||
@@ -132,7 +127,7 @@ export function WebhookCard({
|
||||
</span>
|
||||
<Switch
|
||||
checked={connection.enabled}
|
||||
onCheckedChange={(checked) => onToggle(provider, checked)}
|
||||
onCheckedChange={(checked) => handleToggle(checked)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -177,7 +172,7 @@ export function WebhookCard({
|
||||
|
||||
{!connection ? (
|
||||
<Button
|
||||
onClick={handleConnect}
|
||||
onClick={onConnect}
|
||||
disabled={connecting}
|
||||
className="w-full"
|
||||
>
|
||||
@@ -231,7 +226,7 @@ export function WebhookCard({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onRegenerateToken(provider)}
|
||||
onClick={() => handleRegenerateToken()}
|
||||
>
|
||||
<IconRefresh />
|
||||
Regenerate URL
|
||||
@@ -239,7 +234,7 @@ export function WebhookCard({
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => onDelete(provider)}
|
||||
onClick={() => handleDelete()}
|
||||
>
|
||||
<IconTrash />
|
||||
Disconnect
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
"name": "sofa",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "1.2.0",
|
||||
"@better-auth/drizzle-adapter": "1.5.3",
|
||||
"@tabler/icons-react": "3.38.0",
|
||||
"@tanstack/react-hotkeys": "0.3.1",
|
||||
"better-auth": "1.5.2",
|
||||
"better-auth": "1.5.3",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
@@ -18,13 +19,13 @@
|
||||
"embla-carousel-react": "8.6.0",
|
||||
"embla-carousel-wheel-gestures": "8.1.0",
|
||||
"jotai": "2.18.0",
|
||||
"motion": "12.34.5",
|
||||
"motion": "12.35.0",
|
||||
"next": "16.1.6",
|
||||
"node-vibrant": "4.0.4",
|
||||
"react": "19.2.4",
|
||||
"react-day-picker": "9.14.0",
|
||||
"react-dom": "19.2.4",
|
||||
"react-resizable-panels": "4.7.0",
|
||||
"react-resizable-panels": "4.7.1",
|
||||
"recharts": "3.7.0",
|
||||
"shadcn": "3.8.5",
|
||||
"sonner": "2.0.7",
|
||||
@@ -34,7 +35,7 @@
|
||||
"zod": "4.3.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.5",
|
||||
"@biomejs/biome": "2.4.6",
|
||||
"@tailwindcss/postcss": "4.2.1",
|
||||
"@types/bun": "1.3.10",
|
||||
"@types/node": "25.3.3",
|
||||
@@ -148,41 +149,37 @@
|
||||
|
||||
"@base-ui/utils": ["@base-ui/utils@0.2.5", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-oYC7w0gp76RI5MxprlGLV0wze0SErZaRl3AAkeP3OnNB/UBMb6RqNf6ZSIlxOc9Qp68Ab3C2VOcJQyRs7Xc7Vw=="],
|
||||
|
||||
"@better-auth/core": ["@better-auth/core@1.5.2", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-svaKRVN/p3+g++kljLEedHC+RgDlGsVr87tKiATr5xIE7xqLO1If906pMTNMfhF08N5r7pMbix/mRYdObuPKHA=="],
|
||||
"@better-auth/core": ["@better-auth/core@1.5.3", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-fORsQjNZ6BQ7o96xMe7elz3Y4Y8DsqXmQrdyzt289G9rmzX4auwBCPTtE2cXTRTYGiVvH9bv0b97t1Uo/OWynQ=="],
|
||||
|
||||
"@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.2", "", { "peerDependencies": { "@better-auth/core": "1.5.2", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" } }, "sha512-29e7UCwqTriIuDdEr1xbSx4qGg6Ag3aTopzRavPyOCYJyzTwePw8iZ9zaJF1fsLmLeany7LW069NMDf6+3tz/w=="],
|
||||
"@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.3", "", { "peerDependencies": { "@better-auth/core": "1.5.3", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" } }, "sha512-dib9V1vpwDu+TKLC+L+8Q5bLNS0uE3JCT4pGotw52pnpiQF8msoMK4eEfri19f8DtNltpb2F2yzyIsTugBBYNQ=="],
|
||||
|
||||
"@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.5.2", "", { "peerDependencies": { "@better-auth/core": "1.5.2", "@better-auth/utils": "^0.3.0", "kysely": "^0.27.0 || ^0.28.0" } }, "sha512-PaP+KPJ6Cw0DxzuZLH6eR0oFhS5Iq/KjYvmZt76fCl5Q7Ys9Cct0kXVVlrD1bftiznTpV+UKMfwjcCWvtc1l4w=="],
|
||||
"@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.5.3", "", { "peerDependencies": { "@better-auth/core": "1.5.3", "@better-auth/utils": "^0.3.0", "kysely": "^0.27.0 || ^0.28.0" } }, "sha512-eAm1KPrlPXkH/qXUXnGBcHPDgCX153b6BSlc2QJ2IeqmiWym9D/6XORqBIZOl71JiP0Cifzocr2GLpnz0gt31Q=="],
|
||||
|
||||
"@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.5.2", "", { "peerDependencies": { "@better-auth/core": "1.5.2", "@better-auth/utils": "^0.3.0" } }, "sha512-vCbmMpUAisISRVxzNFIxkvO3wm63fzwSxAOO9Xktl77VyEyh8zliBNcW8S6+9DOeolVRKhjWEg7UaBOtIhFX/Q=="],
|
||||
"@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.5.3", "", { "peerDependencies": { "@better-auth/core": "1.5.3", "@better-auth/utils": "^0.3.0" } }, "sha512-QdeTI3bvUmaPkHsjcSMfroXyuGsgnxobv7wZVl57e+ox6yQVR1j4VKbqmCILP6PL6Rr2gpcBH/liHr8v5gqY5Q=="],
|
||||
|
||||
"@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.5.2", "", { "peerDependencies": { "@better-auth/core": "1.5.2", "@better-auth/utils": "^0.3.0", "mongodb": "^6.0.0 || ^7.0.0" } }, "sha512-fJd+Z7fgRFgK2W7oxbFYM9K10yEUs/hlALzihCdDgSyRg9XFLBMJkMcECRqBsop/cZUjwZxLBTQPsIpgx9ix1g=="],
|
||||
|
||||
"@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.5.2", "", { "dependencies": { "@prisma/client": "^7.4.1" }, "peerDependencies": { "@better-auth/core": "1.5.2", "@better-auth/utils": "^0.3.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-9GB4q91Q16wSSsi1vFP/RZNW0GNmdxTiDcE2MX1fC4Hlmo6UuT4WRv+94YPYs3/tPhsJ17k5Lt+rKTOK8S6Kbw=="],
|
||||
|
||||
"@better-auth/telemetry": ["@better-auth/telemetry@1.5.2", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.2" } }, "sha512-Cukyj5yciFNGtx5SkQCop2wOjpHuq2ZHL3V7ydVFe51WUNCXd+v1Sbed2sIfT/8hOcSB+NefALXNv1yTSxKkAQ=="],
|
||||
"@better-auth/telemetry": ["@better-auth/telemetry@1.5.3", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.3" } }, "sha512-ZX/r8AsWdB6BwH+Rb7H/SyJnGtPN6EDWrNxBQEDsqRrBJVcDLwAIz165P57RXci0WwtY872T0guKq+XVyy5rkA=="],
|
||||
|
||||
"@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="],
|
||||
|
||||
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.4.5", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.5", "@biomejs/cli-darwin-x64": "2.4.5", "@biomejs/cli-linux-arm64": "2.4.5", "@biomejs/cli-linux-arm64-musl": "2.4.5", "@biomejs/cli-linux-x64": "2.4.5", "@biomejs/cli-linux-x64-musl": "2.4.5", "@biomejs/cli-win32-arm64": "2.4.5", "@biomejs/cli-win32-x64": "2.4.5" }, "bin": { "biome": "bin/biome" } }, "sha512-OWNCyMS0Q011R6YifXNOg6qsOg64IVc7XX6SqGsrGszPbkVCoaO7Sr/lISFnXZ9hjQhDewwZ40789QmrG0GYgQ=="],
|
||||
"@biomejs/biome": ["@biomejs/biome@2.4.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lGS4Nd5O3KQJ6TeWv10mElnx1phERhBxqGP/IKq0SvZl78kcWDFMaTtVK+w3v3lusRFxJY78n07PbKplirsU5g=="],
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-6MoH4tyISIBNkZ2Q5T1R7dLd5BsITb2yhhhrU9jHZxnNSNMWl+s2Mxu7NBF8Y3a7JJcqq9nsk8i637z4gqkJxQ=="],
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-U1GAG6FTjhAO04MyH4xn23wRNBkT6H7NentHh+8UxD6ShXKBm5SY4RedKJzkUThANxb9rUKIPc7B8ew9Xo/cWg=="],
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-iqLDgpzobG7gpBF0fwEVS/LT8kmN7+S0E2YKFDtqliJfzNLnAiV2Nnyb+ehCDCJgAZBASkYHR2o60VQWikpqIg=="],
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-NdODlSugMzTlENPTa4z0xB82dTUlCpsrOxc43///aNkTLblIYH4XpYflBbf5ySlQuP8AA4AZd1qXhV07IdrHdQ=="],
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-NlKa7GpbQmNhZf9kakQeddqZyT7itN7jjWdakELeXyTU3pg/83fTysRRDPJD0akTfKDl6vZYNT9Zqn4MYZVBOA=="],
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-EBfrTqRIWOFSd7CQb/0ttjHMR88zm3hGravnDwUA9wHAaCAYsULKDebWcN5RmrEo1KBtl/gDVJMrFjNR0pdGUw=="],
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-Pmhv9zT95YzECfjEHNl3mN9Vhusw9VA5KHY0ZvlGsxsjwS5cb7vpRnHzJIv0vG7jB0JI7xEaMH9ddfZm/RozBw=="],
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.6", "", { "os": "win32", "cpu": "x64" }, "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg=="],
|
||||
|
||||
"@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="],
|
||||
|
||||
@@ -366,8 +363,6 @@
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
|
||||
|
||||
"@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.6", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g=="],
|
||||
|
||||
"@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="],
|
||||
|
||||
"@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="],
|
||||
@@ -562,10 +557,6 @@
|
||||
|
||||
"@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="],
|
||||
|
||||
"@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="],
|
||||
|
||||
"@types/whatwg-url": ["@types/whatwg-url@13.0.0", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q=="],
|
||||
|
||||
"@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA=="],
|
||||
|
||||
"@vibrant/color": ["@vibrant/color@4.0.4", "", {}, "sha512-Fq2tAszz4QOPWfHZ+KuEAchXUD8i594BM2fOJt8dI/fvYbiVoBycBF/BlNH6F4IWBubxXoPqD4JmmAHvFYbNew=="],
|
||||
@@ -624,7 +615,7 @@
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
|
||||
|
||||
"better-auth": ["better-auth@1.5.2", "", { "dependencies": { "@better-auth/core": "1.5.2", "@better-auth/drizzle-adapter": "1.5.2", "@better-auth/kysely-adapter": "1.5.2", "@better-auth/memory-adapter": "1.5.2", "@better-auth/mongo-adapter": "1.5.2", "@better-auth/prisma-adapter": "1.5.2", "@better-auth/telemetry": "1.5.2", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-xEnt/RBsu/cjcN+IEsgeBqka/6QRaGzPFl5hnnJfZU/JxRGnDsVUDHXvJQRfecj8qA/1nPJOQjZ6qQL1Z7aN+Q=="],
|
||||
"better-auth": ["better-auth@1.5.3", "", { "dependencies": { "@better-auth/core": "1.5.3", "@better-auth/kysely-adapter": "1.5.3", "@better-auth/memory-adapter": "1.5.3", "@better-auth/telemetry": "1.5.3", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/drizzle-adapter": "1.5.3", "@better-auth/mongo-adapter": "1.5.3", "@better-auth/prisma-adapter": "1.5.3", "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@better-auth/drizzle-adapter", "@better-auth/mongo-adapter", "@better-auth/prisma-adapter", "@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-E+9kA9GMX1+gT3FfMCqRz0NufT4X/+tNhpOsHW1jLmyPZKinkHtfZkUffSBnG5qGkvfBaH/slT5c1fKttnmF5w=="],
|
||||
|
||||
"better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="],
|
||||
|
||||
@@ -640,8 +631,6 @@
|
||||
|
||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
|
||||
"bson": ["bson@7.2.0", "", {}, "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ=="],
|
||||
|
||||
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
@@ -882,7 +871,7 @@
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"framer-motion": ["framer-motion@12.34.5", "", { "dependencies": { "motion-dom": "^12.34.5", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Z2dQ+o7BsfpJI3+u0SQUNCrN+ajCKJen1blC4rCHx1Ta2EOHs+xKJegLT2aaD9iSMbU3OoX+WabQXkloUbZmJQ=="],
|
||||
"framer-motion": ["framer-motion@12.35.0", "", { "dependencies": { "motion-dom": "^12.35.0", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-w8hghCMQ4oq10j6aZh3U2yeEQv5K69O/seDI/41PK4HtgkLrcBovUNc0ayBC3UyyU7V1mrY2yLzvYdWJX9pGZQ=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
@@ -1104,8 +1093,6 @@
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||
@@ -1126,13 +1113,9 @@
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"mongodb": ["mongodb@7.1.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^7.1.1", "mongodb-connection-string-url": "^7.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.806.0", "@mongodb-js/zstd": "^7.0.0", "gcp-metadata": "^7.0.1", "kerberos": "^7.0.0", "mongodb-client-encryption": ">=7.0.0 <7.1.0", "snappy": "^7.3.2", "socks": "^2.8.6" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg=="],
|
||||
"motion": ["motion@12.35.0", "", { "dependencies": { "framer-motion": "^12.35.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-BQUhNUIGvUcwXCzwmnT1JpjUqab34lIwxHnXUyWRht1WC1vAyp7/4qgMiUXxN3K6hgUhyoR+HNnLeQMwUZjVjw=="],
|
||||
|
||||
"mongodb-connection-string-url": ["mongodb-connection-string-url@7.0.1", "", { "dependencies": { "@types/whatwg-url": "^13.0.0", "whatwg-url": "^14.1.0" } }, "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ=="],
|
||||
|
||||
"motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="],
|
||||
|
||||
"motion-dom": ["motion-dom@12.34.5", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-k33CsnxO2K3gBRMUZT+vPmc4Utlb5menKdG0RyVNLtlqRaaJPRWlE9fXl8NTtfZ5z3G8TDvqSu0MENLqSTaHZA=="],
|
||||
"motion-dom": ["motion-dom@12.35.0", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-FFMLEnIejK/zDABn+vqGVAUN4T0+3fw+cVAY8MMT65yR+j5uMuvWdd4npACWhh94OVWQs79CrBBuwOwGRZAQiA=="],
|
||||
|
||||
"motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="],
|
||||
|
||||
@@ -1250,8 +1233,6 @@
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
||||
|
||||
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||
@@ -1278,7 +1259,7 @@
|
||||
|
||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||
|
||||
"react-resizable-panels": ["react-resizable-panels@4.7.0", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-QJTeh8KJR6lKzDwVez+WGPPUPuarIjR3ptg23thJ0G5dAMYJH4iSN1AnNY0DuSP+PgH8s9eYfwrR7AlmX6TvYA=="],
|
||||
"react-resizable-panels": ["react-resizable-panels@4.7.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-RYBRgvdZhnUds5jJWYr1up0hYFofgN1dqSwhxfBl9Savoxms0gyGF0AfaXskhxTYkXrlwc+TlQPe5UkoV+1neg=="],
|
||||
|
||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||
|
||||
@@ -1372,8 +1353,6 @@
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
||||
|
||||
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
||||
@@ -1436,7 +1415,7 @@
|
||||
|
||||
"tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="],
|
||||
|
||||
"tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="],
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="],
|
||||
|
||||
@@ -1488,11 +1467,11 @@
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="],
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"wheel-gestures": ["wheel-gestures@2.2.48", "", {}, "sha512-f+Gy33Oa5Z14XY9679Zze+7VFhbsQfBFXodnU2x589l4kxGM9L5Y8zETTmcMR5pWOPQyRv4Z0lNax6xCO0NSlA=="],
|
||||
|
||||
@@ -1646,8 +1625,6 @@
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"isomorphic-fetch/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"node-vibrant/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
@@ -1658,10 +1635,6 @@
|
||||
|
||||
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"isomorphic-fetch/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"isomorphic-fetch/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ import { useDebounce } from "@/hooks/use-debounce";
|
||||
import {
|
||||
commandPaletteOpenAtom,
|
||||
helpOpenAtom,
|
||||
MAX_RECENT,
|
||||
recentSearchesAtom,
|
||||
} from "@/lib/atoms/command-palette";
|
||||
import { SHORTCUT_DESCRIPTIONS } from "@/lib/constants/shortcuts";
|
||||
|
||||
@@ -48,43 +50,16 @@ interface SearchResult {
|
||||
voteAverage: number;
|
||||
}
|
||||
|
||||
const RECENT_KEY = "cp:recent-searches";
|
||||
const MAX_RECENT = 5;
|
||||
|
||||
function getRecentSearches(): string[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(RECENT_KEY) ?? "[]");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function addRecentSearch(query: string) {
|
||||
const recent = getRecentSearches().filter((q) => q !== query);
|
||||
recent.unshift(query);
|
||||
localStorage.setItem(RECENT_KEY, JSON.stringify(recent.slice(0, MAX_RECENT)));
|
||||
}
|
||||
|
||||
function removeRecentSearch(query: string) {
|
||||
const recent = getRecentSearches().filter((q) => q !== query);
|
||||
localStorage.setItem(RECENT_KEY, JSON.stringify(recent));
|
||||
}
|
||||
|
||||
function clearRecentSearches() {
|
||||
localStorage.removeItem(RECENT_KEY);
|
||||
}
|
||||
|
||||
export function CommandPalette() {
|
||||
const router = useRouter();
|
||||
const [commandPaletteOpen, setCommandPaletteOpen] = useAtom(
|
||||
commandPaletteOpenAtom,
|
||||
);
|
||||
const [helpOpen, setHelpOpen] = useAtom(helpOpenAtom);
|
||||
const [recentSearches, setRecentSearches] = useAtom(recentSearchesAtom);
|
||||
const [query, setQuery] = useState("");
|
||||
const [results, setResults] = useState<SearchResult[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [recentSearches, setRecentSearches] = useState<string[]>([]);
|
||||
const debouncedQuery = useDebounce(query, 300);
|
||||
const enabled = !commandPaletteOpen;
|
||||
|
||||
@@ -100,10 +75,9 @@ export function CommandPalette() {
|
||||
timeout: 500,
|
||||
});
|
||||
|
||||
// Load recent searches when palette opens
|
||||
// Reset ephemeral state when palette opens
|
||||
useEffect(() => {
|
||||
if (commandPaletteOpen) {
|
||||
setRecentSearches(getRecentSearches());
|
||||
setQuery("");
|
||||
setResults([]);
|
||||
}
|
||||
@@ -122,7 +96,11 @@ export function CommandPalette() {
|
||||
.then((data) => {
|
||||
if (!cancelled) {
|
||||
setResults((data.results ?? []).slice(0, 8));
|
||||
addRecentSearch(debouncedQuery.trim());
|
||||
const trimmed = debouncedQuery.trim();
|
||||
setRecentSearches((prev) => {
|
||||
const filtered = prev.filter((q) => q !== trimmed);
|
||||
return [trimmed, ...filtered].slice(0, MAX_RECENT);
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -131,7 +109,7 @@ export function CommandPalette() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [debouncedQuery]);
|
||||
}, [debouncedQuery, setRecentSearches]);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(result: SearchResult) => {
|
||||
@@ -145,15 +123,16 @@ export function CommandPalette() {
|
||||
setQuery(q);
|
||||
}, []);
|
||||
|
||||
const handleRemoveRecent = useCallback((q: string) => {
|
||||
removeRecentSearch(q);
|
||||
setRecentSearches((prev) => prev.filter((s) => s !== q));
|
||||
}, []);
|
||||
const handleRemoveRecent = useCallback(
|
||||
(q: string) => {
|
||||
setRecentSearches((prev) => prev.filter((s) => s !== q));
|
||||
},
|
||||
[setRecentSearches],
|
||||
);
|
||||
|
||||
const handleClearRecent = useCallback(() => {
|
||||
clearRecentSearches();
|
||||
setRecentSearches([]);
|
||||
}, []);
|
||||
}, [setRecentSearches]);
|
||||
|
||||
const hasQuery = query.trim().length > 0;
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { useCallback } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
setBackupScheduleAction,
|
||||
setMaxBackupsAction,
|
||||
setScheduledBackupAction,
|
||||
} from "@/lib/actions/settings";
|
||||
import type { BackupFrequency } from "@/lib/cron";
|
||||
|
||||
export interface BackupScheduleState {
|
||||
enabled: boolean;
|
||||
maxRetention: number;
|
||||
frequency: BackupFrequency;
|
||||
time: string;
|
||||
dow: number;
|
||||
}
|
||||
|
||||
export const backupScheduleAtom = atom<BackupScheduleState>({
|
||||
enabled: false,
|
||||
maxRetention: 7,
|
||||
frequency: "1d",
|
||||
time: "03:00",
|
||||
dow: 0,
|
||||
});
|
||||
|
||||
export const savingScheduleAtom = atom(false);
|
||||
export const togglingScheduleAtom = atom(false);
|
||||
|
||||
export function useBackupScheduleActions() {
|
||||
const [schedule, setSchedule] = useAtom(backupScheduleAtom);
|
||||
const [, setSavingSchedule] = useAtom(savingScheduleAtom);
|
||||
const [, setTogglingSchedule] = useAtom(togglingScheduleAtom);
|
||||
|
||||
const toggleScheduled = useCallback(
|
||||
async (checked: boolean) => {
|
||||
const previous = schedule.enabled;
|
||||
setSchedule((prev) => ({ ...prev, enabled: checked }));
|
||||
setTogglingSchedule(true);
|
||||
try {
|
||||
await setScheduledBackupAction(checked);
|
||||
toast.success(
|
||||
checked ? "Scheduled backups enabled" : "Scheduled backups disabled",
|
||||
);
|
||||
} catch {
|
||||
setSchedule((prev) => ({ ...prev, enabled: previous }));
|
||||
toast.error("Failed to update scheduled backup setting");
|
||||
} finally {
|
||||
setTogglingSchedule(false);
|
||||
}
|
||||
},
|
||||
[schedule.enabled, setSchedule, setTogglingSchedule],
|
||||
);
|
||||
|
||||
const changeMaxRetention = useCallback(
|
||||
async (value: number) => {
|
||||
const previous = schedule.maxRetention;
|
||||
setSchedule((prev) => ({ ...prev, maxRetention: value }));
|
||||
try {
|
||||
await setMaxBackupsAction(value);
|
||||
} catch {
|
||||
setSchedule((prev) => ({ ...prev, maxRetention: previous }));
|
||||
toast.error("Failed to update retention setting");
|
||||
}
|
||||
},
|
||||
[schedule.maxRetention, setSchedule],
|
||||
);
|
||||
|
||||
const changeSchedule = useCallback(
|
||||
async (
|
||||
newFrequency: BackupFrequency,
|
||||
newTime: string,
|
||||
newDow = schedule.dow,
|
||||
) => {
|
||||
const prev = {
|
||||
frequency: schedule.frequency,
|
||||
time: schedule.time,
|
||||
dow: schedule.dow,
|
||||
};
|
||||
setSchedule((s) => ({
|
||||
...s,
|
||||
frequency: newFrequency,
|
||||
time: newTime,
|
||||
dow: newDow,
|
||||
}));
|
||||
setSavingSchedule(true);
|
||||
try {
|
||||
await setBackupScheduleAction(newFrequency, newTime, newDow);
|
||||
toast.success("Schedule updated");
|
||||
} catch {
|
||||
setSchedule((s) => ({ ...s, ...prev }));
|
||||
toast.error("Failed to update schedule");
|
||||
} finally {
|
||||
setSavingSchedule(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
schedule.frequency,
|
||||
schedule.time,
|
||||
schedule.dow,
|
||||
setSchedule,
|
||||
setSavingSchedule,
|
||||
],
|
||||
);
|
||||
|
||||
return { toggleScheduled, changeMaxRetention, changeSchedule };
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
import { atom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
|
||||
export const commandPaletteOpenAtom = atom(false);
|
||||
export const helpOpenAtom = atom(false);
|
||||
|
||||
const RECENT_KEY = "cp:recent-searches";
|
||||
export const MAX_RECENT = 5;
|
||||
export const recentSearchesAtom = atomWithStorage<string[]>(RECENT_KEY, []);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { atom } from "jotai";
|
||||
import { loadable } from "jotai/utils";
|
||||
|
||||
interface TitleRowItem {
|
||||
tmdbId: number;
|
||||
type: "movie" | "tv";
|
||||
title: string;
|
||||
posterPath: string | null;
|
||||
releaseDate: string | null;
|
||||
voteAverage: number;
|
||||
}
|
||||
|
||||
export const selectedGenreAtom = atom<number | null>(null);
|
||||
export const mediaTypeAtom = atom<"movie" | "tv">("movie");
|
||||
export const defaultItemsAtom = atom<TitleRowItem[]>([]);
|
||||
|
||||
const genreResultsAsyncAtom = atom(async (get) => {
|
||||
const genre = get(selectedGenreAtom);
|
||||
const mediaType = get(mediaTypeAtom);
|
||||
if (genre === null) return null;
|
||||
const res = await fetch(
|
||||
`/api/explore/discover?type=${mediaType}&genre=${genre}&sort_by=popularity.desc`,
|
||||
);
|
||||
const data = await res.json();
|
||||
return (data.results ?? []) as TitleRowItem[];
|
||||
});
|
||||
|
||||
export const genreResultsLoadable = loadable(genreResultsAsyncAtom);
|
||||
@@ -0,0 +1,86 @@
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { useCallback } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { WebhookConnection } from "@/app/(pages)/settings/_components/webhook-card";
|
||||
import {
|
||||
deleteWebhookConnection,
|
||||
regenerateWebhookToken,
|
||||
saveWebhookConnection,
|
||||
} from "@/lib/actions/settings";
|
||||
|
||||
export const connectionsAtom = atom<WebhookConnection[]>([]);
|
||||
|
||||
function providerLabel(provider: "plex" | "jellyfin" | "emby") {
|
||||
return provider === "plex"
|
||||
? "Plex"
|
||||
: provider === "emby"
|
||||
? "Emby"
|
||||
: "Jellyfin";
|
||||
}
|
||||
|
||||
export function useConnectionActions(provider: "plex" | "jellyfin" | "emby") {
|
||||
const [connections, setConnections] = useAtom(connectionsAtom);
|
||||
const label = providerLabel(provider);
|
||||
const connection = connections.find((c) => c.provider === provider) ?? null;
|
||||
|
||||
const handleConnect = useCallback(async () => {
|
||||
try {
|
||||
const result = await saveWebhookConnection(provider);
|
||||
setConnections((prev) => [...prev, { ...result, recentEvents: [] }]);
|
||||
toast.success(`${label} connected`);
|
||||
} catch {
|
||||
toast.error(`Failed to connect ${label}`);
|
||||
}
|
||||
}, [provider, label, setConnections]);
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
const previous = connections;
|
||||
setConnections((prev) => prev.filter((c) => c.provider !== provider));
|
||||
try {
|
||||
await deleteWebhookConnection(provider);
|
||||
toast.success(`${label} disconnected`);
|
||||
} catch {
|
||||
setConnections(previous);
|
||||
toast.error(`Failed to disconnect ${label}`);
|
||||
}
|
||||
}, [provider, label, connections, setConnections]);
|
||||
|
||||
const handleRegenerateToken = useCallback(async () => {
|
||||
try {
|
||||
const result = await regenerateWebhookToken(provider);
|
||||
setConnections((prev) =>
|
||||
prev.map((c) =>
|
||||
c.provider === provider ? { ...c, token: result.token } : c,
|
||||
),
|
||||
);
|
||||
toast.success(`${label} webhook URL regenerated`);
|
||||
} catch {
|
||||
toast.error(`Failed to regenerate ${label} URL`);
|
||||
}
|
||||
}, [provider, label, setConnections]);
|
||||
|
||||
const handleToggle = useCallback(
|
||||
async (enabled: boolean) => {
|
||||
const previous = connections;
|
||||
setConnections((prev) =>
|
||||
prev.map((c) => (c.provider === provider ? { ...c, enabled } : c)),
|
||||
);
|
||||
try {
|
||||
await saveWebhookConnection(provider, enabled);
|
||||
toast.success(`${label} webhook ${enabled ? "enabled" : "disabled"}`);
|
||||
} catch {
|
||||
setConnections(previous);
|
||||
toast.error(`Failed to update ${label}`);
|
||||
}
|
||||
},
|
||||
[provider, label, connections, setConnections],
|
||||
);
|
||||
|
||||
return {
|
||||
connection,
|
||||
handleConnect,
|
||||
handleDelete,
|
||||
handleRegenerateToken,
|
||||
handleToggle,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { atom } from "jotai";
|
||||
import { loadable } from "jotai/utils";
|
||||
import type { HistoryBucket, TimePeriod } from "@/lib/services/discovery";
|
||||
|
||||
export const moviePeriodAtom = atom<TimePeriod>("this_month");
|
||||
export const episodePeriodAtom = atom<TimePeriod>("this_week");
|
||||
|
||||
async function fetchStats(
|
||||
type: "movies" | "episodes",
|
||||
period: TimePeriod,
|
||||
): Promise<{ count: number; history: HistoryBucket[] }> {
|
||||
const res = await fetch(
|
||||
`/api/stats?type=${type}&period=${period}&history=true`,
|
||||
);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
const movieStatsAsyncAtom = atom(async (get) => {
|
||||
const period = get(moviePeriodAtom);
|
||||
return fetchStats("movies", period);
|
||||
});
|
||||
|
||||
const episodeStatsAsyncAtom = atom(async (get) => {
|
||||
const period = get(episodePeriodAtom);
|
||||
return fetchStats("episodes", period);
|
||||
});
|
||||
|
||||
export const movieStatsLoadable = loadable(movieStatsAsyncAtom);
|
||||
export const episodeStatsLoadable = loadable(episodeStatsAsyncAtom);
|
||||
+5
-4
@@ -16,9 +16,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "1.2.0",
|
||||
"@better-auth/drizzle-adapter": "1.5.3",
|
||||
"@tabler/icons-react": "3.38.0",
|
||||
"@tanstack/react-hotkeys": "0.3.1",
|
||||
"better-auth": "1.5.2",
|
||||
"better-auth": "1.5.3",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
@@ -28,13 +29,13 @@
|
||||
"embla-carousel-react": "8.6.0",
|
||||
"embla-carousel-wheel-gestures": "8.1.0",
|
||||
"jotai": "2.18.0",
|
||||
"motion": "12.34.5",
|
||||
"motion": "12.35.0",
|
||||
"next": "16.1.6",
|
||||
"node-vibrant": "4.0.4",
|
||||
"react": "19.2.4",
|
||||
"react-day-picker": "9.14.0",
|
||||
"react-dom": "19.2.4",
|
||||
"react-resizable-panels": "4.7.0",
|
||||
"react-resizable-panels": "4.7.1",
|
||||
"recharts": "3.7.0",
|
||||
"shadcn": "3.8.5",
|
||||
"sonner": "2.0.7",
|
||||
@@ -44,7 +45,7 @@
|
||||
"zod": "4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.5",
|
||||
"@biomejs/biome": "2.4.6",
|
||||
"@types/bun": "1.3.10",
|
||||
"@tailwindcss/postcss": "4.2.1",
|
||||
"@types/node": "25.3.3",
|
||||
|
||||
Reference in New Issue
Block a user