feat: reorganize API around operation domains

Restructure the oRPC contract from 14 resource-oriented routers to 8
domain-oriented routers for a cleaner public API surface.

- Consolidate 7 watch procedures into unified tracking.watch/unwatch
  with scope + ids input (movie, episode, season, series)
- Split dashboard across tracking (stats, history), library
  (continueWatching, upcoming), and discover (recommendations)
- Merge explore + search + discover into single discover router
- Absorb integrations into account.integrations
- Merge system.authConfig into system.publicInfo
- Collapse 6 admin setting endpoints into admin.settings.get/update
- Deduplicate platforms.list + explore.watchProviders into
  discover.platforms
- Add symmetric unwatchMovie/unwatchSeries core functions
- Rename titles.detail→get, titles.recommendations→similar,
  people.detail→get

BREAKING CHANGE: All client API paths have changed. REST paths now
mirror router structure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 12:36:49 -04:00
co-authored by Claude Opus 4.6
parent 3906f6021d
commit 73e5c96bd8
112 changed files with 3291 additions and 4015 deletions
+1 -1
View File
@@ -101,7 +101,7 @@ export function CommandPalette() {
const debouncedQuery = useDebounce(query, 300);
const trimmedQuery = debouncedQuery.trim();
const { data: searchData, isLoading: loading } = useQuery(
orpc.search.queryOptions({
orpc.discover.search.queryOptions({
input: trimmedQuery ? { query: trimmedQuery } : skipToken,
}),
);
@@ -8,7 +8,7 @@ import { ContinueWatchingList, ContinueWatchingSectionSkeleton } from "./continu
import { FeedSection } from "./feed-section";
export function ContinueWatchingSection() {
const { data, isPending } = useQuery(orpc.dashboard.continueWatching.queryOptions());
const { data, isPending } = useQuery(orpc.library.continueWatching.queryOptions());
const { t } = useLingui();
@@ -8,7 +8,7 @@ import { FeedSection } from "./feed-section";
import { TitleGrid, TitleGridSectionSkeleton } from "./title-grid";
export function RecommendationsSection() {
const { data, isPending } = useQuery(orpc.dashboard.recommendations.queryOptions());
const { data, isPending } = useQuery(orpc.discover.recommendations.queryOptions());
const { t } = useLingui();
@@ -158,12 +158,12 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
const [episodePeriod, setEpisodePeriod] = useState<TimePeriod>("this_week");
const { data: movieStats } = useQuery(
orpc.dashboard.watchHistory.queryOptions({
orpc.tracking.history.queryOptions({
input: { type: "movie", period: moviePeriod },
}),
);
const { data: episodeStats } = useQuery(
orpc.dashboard.watchHistory.queryOptions({
orpc.tracking.history.queryOptions({
input: { type: "episode", period: episodePeriod },
}),
);
@@ -8,7 +8,7 @@ import { orpc } from "@/lib/orpc/client";
import { StatsDisplay, StatsSectionSkeleton } from "./stats-display";
export function StatsSection() {
const { data: stats, isPending } = useQuery(orpc.dashboard.stats.queryOptions());
const { data: stats, isPending } = useQuery(orpc.tracking.stats.queryOptions());
if (isPending) return <StatsSectionSkeleton />;
if (!stats) return null;
@@ -9,7 +9,7 @@ import { UpcomingRow } from "./upcoming-item";
export function UpcomingSection() {
const { data, isPending } = useQuery(
orpc.dashboard.upcoming.queryOptions({ input: { days: 7, limit: 5 } }),
orpc.library.upcoming.queryOptions({ input: { days: 7, limit: 5 } }),
);
const { t } = useLingui();
@@ -69,11 +69,11 @@ export function DiscoverSection() {
const [language, setLanguage] = useState<string | undefined>(undefined);
const [platformId, setPlatformId] = useState<string | undefined>(undefined);
const { data: genreData } = useQuery(orpc.explore.genres.queryOptions({ input: { type } }));
const { data: providerData } = useQuery(orpc.platforms.list.queryOptions());
const { data: genreData } = useQuery(orpc.discover.genres.queryOptions({ input: { type } }));
const { data: providerData } = useQuery(orpc.discover.platforms.queryOptions());
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isPending } = useInfiniteQuery(
orpc.discover.infiniteOptions({
orpc.discover.browse.infiniteOptions({
input: (pageParam: number) => ({
type,
genreId,
@@ -54,7 +54,7 @@ export function FilterableTitleRow({
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery(
orpc.discover.infiniteOptions({
orpc.discover.browse.infiniteOptions({
input:
selectedGenre != null
? (pageParam: number) => ({
@@ -33,7 +33,7 @@ export function PersonDetailSkeleton() {
export function PersonDetailClient({ id }: { id: string }) {
const { data, isPending, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery(
orpc.people.detail.infiniteOptions({
orpc.people.get.infiniteOptions({
input: (pageParam: number) => ({ id, page: pageParam, limit: 20 }),
initialPageParam: 1,
getNextPageParam: (lastPage) =>
@@ -83,7 +83,7 @@ export function IntegrationCard({
const providerInput = provider as "plex" | "jellyfin" | "emby" | "sonarr" | "radarr";
const connectMutation = useMutation(
orpc.integrations.create.mutationOptions({
orpc.account.integrations.create.mutationOptions({
onSuccess: (result) => {
setConnections((prev) => [...prev, { ...result, recentEvents: [] }]);
toast.success(t`${label} connected`);
@@ -93,7 +93,7 @@ export function IntegrationCard({
);
const deleteMutation = useMutation(
orpc.integrations.delete.mutationOptions({
orpc.account.integrations.delete.mutationOptions({
onMutate: () => {
let previous: IntegrationConnection[] = [];
setConnections((prev) => {
@@ -111,7 +111,7 @@ export function IntegrationCard({
);
const regenerateTokenMutation = useMutation(
orpc.integrations.regenerateToken.mutationOptions({
orpc.account.integrations.regenerateToken.mutationOptions({
onSuccess: (result) => {
setConnections((prev) =>
prev.map((c) => (c.provider === provider ? { ...c, token: result.token } : c)),
@@ -8,7 +8,7 @@ import { IntegrationCard, type IntegrationConnection } from "./integration-card"
import { INTEGRATION_CONFIGS } from "./integration-configs";
export function IntegrationsSection() {
const { data, isPending } = useQuery(orpc.integrations.list.queryOptions());
const { data, isPending } = useQuery(orpc.account.integrations.list.queryOptions());
const [localConnections, setLocalConnections] = useState<IntegrationConnection[] | null>(null);
// Use local state if user has modified connections, else use query data
@@ -11,12 +11,12 @@ import { orpc } from "@/lib/orpc/client";
export function RegistrationSection() {
const { t } = useLingui();
const { data, isPending: isLoading } = useQuery(orpc.admin.registration.queryOptions());
const { data, isPending: isLoading } = useQuery(orpc.admin.settings.get.queryOptions());
const [registrationOpen, setRegistrationOpen] = useState<boolean | null>(null);
const currentOpen = registrationOpen ?? data?.open ?? false;
const currentOpen = registrationOpen ?? data?.registration.open ?? false;
const [optimisticOpen, setOptimisticOpen] = useOptimistic(currentOpen);
const [isPending, startTransition] = useTransition();
const toggleMutation = useMutation(orpc.admin.toggleRegistration.mutationOptions());
const toggleMutation = useMutation(orpc.admin.settings.update.mutationOptions());
if (isLoading) {
return (
@@ -30,7 +30,7 @@ export function RegistrationSection() {
startTransition(async () => {
setOptimisticOpen(checked);
try {
await toggleMutation.mutateAsync({ open: checked });
await toggleMutation.mutateAsync({ registration: { open: checked } });
setRegistrationOpen(checked);
toast.success(checked ? t`Registration opened` : t`Registration closed`);
} catch {
@@ -20,7 +20,7 @@ export function StreamingServicesSection() {
const saveCounterRef = useRef(0);
const initialized = useRef(false);
const platformsQuery = useQuery(orpc.platforms.list.queryOptions());
const platformsQuery = useQuery(orpc.discover.platforms.queryOptions());
const userPlatformsQuery = useQuery(orpc.account.platforms.queryOptions());
// Initialize selected IDs from server data
@@ -11,12 +11,12 @@ import { orpc } from "@/lib/orpc/client";
export function UpdateCheckSection() {
const { t } = useLingui();
const { data, isPending: isLoading } = useQuery(orpc.admin.updateCheck.queryOptions());
const { data, isPending: isLoading } = useQuery(orpc.admin.settings.get.queryOptions());
const [localEnabled, setLocalEnabled] = useState<boolean | null>(null);
const currentEnabled = localEnabled ?? data?.enabled ?? true;
const currentEnabled = localEnabled ?? data?.updateCheck.enabled ?? true;
const [optimisticEnabled, setOptimisticEnabled] = useOptimistic(currentEnabled);
const [isPending, startTransition] = useTransition();
const toggleMutation = useMutation(orpc.admin.toggleUpdateCheck.mutationOptions());
const toggleMutation = useMutation(orpc.admin.settings.update.mutationOptions());
if (isLoading) {
return (
@@ -30,7 +30,7 @@ export function UpdateCheckSection() {
startTransition(async () => {
setOptimisticEnabled(checked);
try {
await toggleMutation.mutateAsync({ enabled: checked });
await toggleMutation.mutateAsync({ updateCheck: { enabled: checked } });
setLocalEnabled(checked);
toast.success(checked ? t`Update checks enabled` : t`Update checks disabled`);
} catch {
+1 -1
View File
@@ -89,7 +89,7 @@ function QuickAddButton({ id, userStatus }: { id: string; userStatus?: TitleStat
const [optimisticStatus, setOptimisticStatus] = useState<TitleStatus | null>(null);
const quickAddMutation = useMutation(
orpc.titles.quickAdd.mutationOptions({
orpc.tracking.quickAdd.mutationOptions({
onSuccess: () => setOptimisticStatus("in_watchlist"),
}),
);
@@ -27,7 +27,7 @@ export function useTitleUserInfo() {
const { titleId } = useTitleContext();
const { data: session } = useSession();
const { data } = useQuery({
...orpc.titles.userInfo.queryOptions({ input: { id: titleId } }),
...orpc.tracking.userInfo.queryOptions({ input: { id: titleId } }),
enabled: !!session,
});
return {
@@ -27,7 +27,7 @@ function RecommendationsSkeleton() {
export function TitleRecommendations({ titleId }: { titleId: string }) {
const { data, isLoading } = useQuery(
orpc.titles.recommendations.queryOptions({ input: { id: titleId } }),
orpc.titles.similar.queryOptions({ input: { id: titleId } }),
);
if (isLoading) return <RecommendationsSkeleton />;
@@ -19,7 +19,7 @@ export function useTitleActions() {
const { t } = useLingui();
const { titleId, titleName, seasons, setWatchingEp } = useTitleContext();
const queryClient = useQueryClient();
const userInfoKey = orpc.titles.userInfo.queryKey({ input: { id: titleId } });
const userInfoKey = orpc.tracking.userInfo.queryKey({ input: { id: titleId } });
const getUserInfo = useCallback(
() =>
@@ -40,15 +40,10 @@ export function useTitleActions() {
[queryClient, userInfoKey],
);
const { mutateAsync: batchWatch } = useMutation(orpc.episodes.batchWatch.mutationOptions());
const { mutateAsync: updateStatus } = useMutation(orpc.titles.updateStatus.mutationOptions());
const { mutateAsync: updateRating } = useMutation(orpc.titles.updateRating.mutationOptions());
const { mutateAsync: watchMovie } = useMutation(orpc.titles.watchMovie.mutationOptions());
const { mutateAsync: unwatchEp } = useMutation(orpc.episodes.unwatch.mutationOptions());
const { mutateAsync: watchEp } = useMutation(orpc.episodes.watch.mutationOptions());
const { mutateAsync: watchSeason } = useMutation(orpc.seasons.watch.mutationOptions());
const { mutateAsync: unwatchSeason } = useMutation(orpc.seasons.unwatch.mutationOptions());
const { mutateAsync: watchAll } = useMutation(orpc.titles.watchAll.mutationOptions());
const { mutateAsync: watch } = useMutation(orpc.tracking.watch.mutationOptions());
const { mutateAsync: unwatch } = useMutation(orpc.tracking.unwatch.mutationOptions());
const { mutateAsync: updateStatus } = useMutation(orpc.tracking.updateStatus.mutationOptions());
const { mutateAsync: updateRating } = useMutation(orpc.tracking.rate.mutationOptions());
const catchUp = useCallback(
async (episodeIds: string[]) => {
@@ -64,7 +59,7 @@ export function useTitleActions() {
}));
try {
await batchWatch({ episodeIds });
await watch({ scope: "episode", ids: episodeIds });
await queryClient.invalidateQueries({ queryKey: userInfoKey });
const count = episodeIds.length;
toast.success(
@@ -79,7 +74,7 @@ export function useTitleActions() {
toast.error(t`Failed to catch up`);
}
},
[getUserInfo, setUserInfo, batchWatch, queryClient, userInfoKey, t],
[getUserInfo, setUserInfo, watch, queryClient, userInfoKey, t],
);
const handleStatusChange = useCallback(
@@ -132,13 +127,13 @@ export function useTitleActions() {
const prevStatus = getUserInfo().status;
setUserInfo((old) => ({ ...old, status: "completed" }));
try {
await watchMovie({ id: titleId });
await watch({ scope: "movie", ids: [titleId] });
toast.success(t`Marked "${titleName}" as watched`);
} catch {
setUserInfo((old) => ({ ...old, status: prevStatus }));
toast.error(t`Failed to mark as watched`);
}
}, [getUserInfo, setUserInfo, queryClient, userInfoKey, titleId, titleName, watchMovie, t]);
}, [getUserInfo, setUserInfo, queryClient, userInfoKey, titleId, titleName, watch, t]);
const handleWatchEpisode = useCallback(
async (episodeId: string, seasonNum: number, epNum: number, isWatched: boolean) => {
@@ -156,7 +151,7 @@ export function useTitleActions() {
}));
try {
await unwatchEp({ id: episodeId });
await unwatch({ scope: "episode", ids: [episodeId] });
toast.success(t`Unwatched S${seasonNum} E${epNum}`);
} catch {
setUserInfo((old) => ({
@@ -181,7 +176,7 @@ export function useTitleActions() {
}));
try {
await watchEp({ id: episodeId });
await watch({ scope: "episode", ids: [episodeId] });
const watchedSet = new Set(getUserInfo().episodeWatches);
const previousUnwatched: string[] = [];
@@ -231,8 +226,8 @@ export function useTitleActions() {
setWatchingEp,
seasons,
catchUp,
unwatchEp,
watchEp,
unwatch,
watch,
t,
],
);
@@ -257,7 +252,7 @@ export function useTitleActions() {
}));
try {
await watchSeason({ id: season.id });
await watch({ scope: "season", ids: [season.id] });
await queryClient.invalidateQueries({ queryKey: userInfoKey });
const currentWatchSet = new Set(getUserInfo().episodeWatches);
@@ -296,7 +291,7 @@ export function useTitleActions() {
toast.error(t`Failed to mark some episodes`);
}
},
[getUserInfo, setUserInfo, seasons, catchUp, watchSeason, queryClient, userInfoKey, t],
[getUserInfo, setUserInfo, seasons, catchUp, watch, queryClient, userInfoKey, t],
);
const handleUnmarkSeason = useCallback(
@@ -312,7 +307,7 @@ export function useTitleActions() {
}));
try {
await unwatchSeason({ id: season.id });
await unwatch({ scope: "season", ids: [season.id] });
const seasonNumber = season.seasonNumber;
const seasonLabel = season.name ?? t`Season ${seasonNumber}`;
toast.success(t`Unwatched all of ${seasonLabel}`);
@@ -325,7 +320,7 @@ export function useTitleActions() {
toast.error(t`Failed to unmark some episodes`);
}
},
[getUserInfo, setUserInfo, queryClient, userInfoKey, unwatchSeason, t],
[getUserInfo, setUserInfo, queryClient, userInfoKey, unwatch, t],
);
const handleMarkAllWatched = useCallback(async () => {
@@ -339,7 +334,7 @@ export function useTitleActions() {
status: old.status ?? "watching",
}));
try {
await watchAll({ id: titleId });
await watch({ scope: "series", ids: [titleId] });
// Refresh to get server-derived display status (caught_up / completed)
await queryClient.invalidateQueries({ queryKey: userInfoKey });
toast.success(t`Marked all episodes as watched`);
@@ -351,7 +346,7 @@ export function useTitleActions() {
}));
toast.error(t`Failed to mark all episodes as watched`);
}
}, [getUserInfo, setUserInfo, seasons, titleId, watchAll, queryClient, userInfoKey, t]);
}, [getUserInfo, setUserInfo, seasons, titleId, watch, queryClient, userInfoKey, t]);
return {
handleStatusChange,
+2 -2
View File
@@ -4,9 +4,9 @@ import { useEffect } from "react";
import { toast } from "sonner";
import { updateToastDismissedVersionAtom } from "@/lib/atoms/update-check";
import type { UpdateCheckResult } from "@sofa/api/schemas";
import type { AdminSettings } from "@sofa/api/schemas";
export function UpdateToast({ data }: { data: UpdateCheckResult | null }) {
export function UpdateToast({ data }: { data: AdminSettings["updateCheck"] | null }) {
const { t } = useLingui();
const [dismissedVersion, setDismissedVersion] = useAtom(updateToastDismissedVersionAtom);
+2 -1
View File
@@ -15,7 +15,8 @@ export const Route = createFileRoute("/_app")({
let updateCheck = null;
if (session.user.role === "admin") {
try {
({ updateCheck } = await client.admin.updateCheck({}));
const settings = await client.admin.settings.get({});
updateCheck = settings.updateCheck;
} catch {
// Silently ignore — update check is non-critical
}
+4 -4
View File
@@ -17,11 +17,11 @@ export const Route = createFileRoute("/_app/dashboard")({
staleTime: 30_000,
loader: async ({ context }) => {
await Promise.all([
context.queryClient.ensureQueryData(orpc.dashboard.stats.queryOptions()),
context.queryClient.ensureQueryData(orpc.dashboard.continueWatching.queryOptions()),
context.queryClient.ensureQueryData(orpc.dashboard.recommendations.queryOptions()),
context.queryClient.ensureQueryData(orpc.tracking.stats.queryOptions()),
context.queryClient.ensureQueryData(orpc.library.continueWatching.queryOptions()),
context.queryClient.ensureQueryData(orpc.discover.recommendations.queryOptions()),
context.queryClient.ensureQueryData(
orpc.dashboard.upcoming.queryOptions({ input: { days: 7, limit: 5 } }),
orpc.library.upcoming.queryOptions({ input: { days: 7, limit: 5 } }),
),
context.queryClient.ensureQueryData(
orpc.library.list.queryOptions({ input: { page: 1, limit: 10 } }),
+10 -10
View File
@@ -17,7 +17,7 @@ export const Route = createFileRoute("/_app/explore")({
loader: async ({ context }) => {
await Promise.all([
context.queryClient.ensureInfiniteQueryData(
orpc.explore.trending.infiniteOptions({
orpc.discover.trending.infiniteOptions({
input: (pageParam: number) => ({ type: "all" as const, page: pageParam }),
initialPageParam: 1,
getNextPageParam: (lastPage) =>
@@ -26,16 +26,16 @@ export const Route = createFileRoute("/_app/explore")({
}),
),
context.queryClient.ensureQueryData(
orpc.explore.popular.queryOptions({ input: { type: "movie" } }),
orpc.discover.popular.queryOptions({ input: { type: "movie" } }),
),
context.queryClient.ensureQueryData(
orpc.explore.popular.queryOptions({ input: { type: "tv" } }),
orpc.discover.popular.queryOptions({ input: { type: "tv" } }),
),
context.queryClient.ensureQueryData(
orpc.explore.genres.queryOptions({ input: { type: "movie" } }),
orpc.discover.genres.queryOptions({ input: { type: "movie" } }),
),
context.queryClient.ensureQueryData(
orpc.explore.genres.queryOptions({ input: { type: "tv" } }),
orpc.discover.genres.queryOptions({ input: { type: "tv" } }),
),
]);
},
@@ -81,7 +81,7 @@ function ExplorePage() {
hasNextPage: hasNextTrending,
isFetchingNextPage: isFetchingNextTrending,
} = useInfiniteQuery(
orpc.explore.trending.infiniteOptions({
orpc.discover.trending.infiniteOptions({
input: (pageParam: number) => ({ type: "all" as const, page: pageParam }),
initialPageParam: 1,
getNextPageParam: (lastPage) =>
@@ -91,16 +91,16 @@ function ExplorePage() {
);
const { data: popularMoviesData, isPending: moviesPending } = useQuery(
orpc.explore.popular.queryOptions({ input: { type: "movie" } }),
orpc.discover.popular.queryOptions({ input: { type: "movie" } }),
);
const { data: popularTvData, isPending: tvPending } = useQuery(
orpc.explore.popular.queryOptions({ input: { type: "tv" } }),
orpc.discover.popular.queryOptions({ input: { type: "tv" } }),
);
const { data: movieGenreData } = useQuery(
orpc.explore.genres.queryOptions({ input: { type: "movie" } }),
orpc.discover.genres.queryOptions({ input: { type: "movie" } }),
);
const { data: tvGenreData } = useQuery(
orpc.explore.genres.queryOptions({ input: { type: "tv" } }),
orpc.discover.genres.queryOptions({ input: { type: "tv" } }),
);
const isPending = trendingPending || moviesPending || tvPending;
+2 -2
View File
@@ -11,7 +11,7 @@ import { client, orpc } from "@/lib/orpc/client";
export const Route = createFileRoute("/_app/onboarding")({
loader: async ({ context }) => {
await context.queryClient.ensureQueryData(orpc.platforms.list.queryOptions());
await context.queryClient.ensureQueryData(orpc.discover.platforms.queryOptions());
},
head: () => ({ meta: [{ title: "Get Started — Sofa" }] }),
errorComponent: RouteError,
@@ -23,7 +23,7 @@ function OnboardingPage() {
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [saving, setSaving] = useState(false);
const platformsQuery = useQuery(orpc.platforms.list.queryOptions());
const platformsQuery = useQuery(orpc.discover.platforms.queryOptions());
const platforms = platformsQuery.data?.platforms ?? [];
const handleToggle = useCallback((id: string) => {
+1 -1
View File
@@ -11,7 +11,7 @@ export const Route = createFileRoute("/_app/people/$id")({
loader: async ({ params, context }) => {
try {
const data = await context.queryClient.ensureInfiniteQueryData(
orpc.people.detail.infiniteOptions({
orpc.people.get.infiniteOptions({
input: (pageParam: number) => ({
id: params.id,
page: pageParam,
+2 -2
View File
@@ -35,9 +35,9 @@ export const Route = createFileRoute("/_app/settings")({
staleTime: 30_000,
loader: async ({ context }) => {
const promises: Promise<unknown>[] = [
context.queryClient.ensureQueryData(orpc.integrations.list.queryOptions()),
context.queryClient.ensureQueryData(orpc.account.integrations.list.queryOptions()),
context.queryClient.ensureQueryData(orpc.system.status.queryOptions()),
context.queryClient.ensureQueryData(orpc.platforms.list.queryOptions()),
context.queryClient.ensureQueryData(orpc.discover.platforms.queryOptions()),
context.queryClient.ensureQueryData(orpc.account.platforms.queryOptions()),
];
const isAdmin = context.session.user.role === "admin";
+2 -2
View File
@@ -21,10 +21,10 @@ export const Route = createFileRoute("/_app/titles/$id")({
try {
const [titleResult, userInfo] = await Promise.all([
context.queryClient.ensureQueryData(
orpc.titles.detail.queryOptions({ input: { id: params.id } }),
orpc.titles.get.queryOptions({ input: { id: params.id } }),
),
context.queryClient
.ensureQueryData(orpc.titles.userInfo.queryOptions({ input: { id: params.id } }))
.ensureQueryData(orpc.tracking.userInfo.queryOptions({ input: { id: params.id } }))
.catch(() => null),
]);
return { ...titleResult, userInfo };
+2 -2
View File
@@ -23,7 +23,7 @@ export const Route = createFileRoute("/_app/upcoming")({
staleTime: 30_000,
loader: async ({ context }) => {
await context.queryClient.ensureInfiniteQueryData(
orpc.dashboard.upcoming.infiniteOptions({
orpc.library.upcoming.infiniteOptions({
input: (pageParam: string | undefined) => ({
days: 90,
limit: 20,
@@ -70,7 +70,7 @@ function UpcomingPage() {
const statusFilter = search.status ?? "all";
const { data, isPending, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery(
orpc.dashboard.upcoming.infiniteOptions({
orpc.library.upcoming.infiniteOptions({
input: (pageParam: string | undefined) => ({
days: 90,
limit: 20,
+8 -8
View File
@@ -5,25 +5,25 @@ import { client } from "@/lib/orpc/client";
export const Route = createFileRoute("/_auth/login")({
beforeLoad: async () => {
const authConfig = await client.system.authConfig({});
if (authConfig.userCount === 0) throw redirect({ to: "/register" });
const publicInfo = await client.system.publicInfo({});
if (publicInfo.userCount === 0) throw redirect({ to: "/register" });
return { authConfig };
return { publicInfo };
},
head: () => ({ meta: [{ title: "Sign in — Sofa" }] }),
component: LoginPage,
});
function LoginPage() {
const { authConfig } = Route.useRouteContext();
const { publicInfo } = Route.useRouteContext();
return (
<AuthForm
mode="login"
authConfig={{
oidcEnabled: authConfig.oidcEnabled,
oidcProviderName: authConfig.oidcProviderName,
passwordLoginDisabled: authConfig.passwordLoginDisabled,
registrationOpen: authConfig.registrationOpen,
oidcEnabled: publicInfo.oidcEnabled,
oidcProviderName: publicInfo.oidcProviderName,
passwordLoginDisabled: publicInfo.passwordLoginDisabled,
registrationOpen: publicInfo.registrationOpen,
}}
/>
);
+7 -7
View File
@@ -7,17 +7,17 @@ import { client } from "@/lib/orpc/client";
export const Route = createFileRoute("/_auth/register")({
beforeLoad: async () => {
const authConfig = await client.system.authConfig({});
return { authConfig };
const publicInfo = await client.system.publicInfo({});
return { publicInfo };
},
head: () => ({ meta: [{ title: "Create account — Sofa" }] }),
component: RegisterPage,
});
function RegisterPage() {
const { authConfig } = Route.useRouteContext();
const { publicInfo } = Route.useRouteContext();
if (!authConfig.registrationOpen) {
if (!publicInfo.registrationOpen) {
return (
<div className="relative mx-auto w-full max-w-sm">
<div className="bg-primary/3 absolute -inset-4 rounded-2xl blur-2xl" />
@@ -50,9 +50,9 @@ function RegisterPage() {
<AuthForm
mode="register"
authConfig={{
oidcEnabled: authConfig.oidcEnabled,
oidcProviderName: authConfig.oidcProviderName,
passwordLoginDisabled: authConfig.passwordLoginDisabled,
oidcEnabled: publicInfo.oidcEnabled,
oidcProviderName: publicInfo.oidcProviderName,
passwordLoginDisabled: publicInfo.passwordLoginDisabled,
}}
/>
);