feat: SSR pour toutes les cards de la page Settings
Toutes les configurations (Prowlarr, qBittorrent, Telegram, Anilist, Komga, metadata providers, status mappings) sont maintenant récupérées côté serveur dans page.tsx et passées en props aux cards. Supprime ~10 fetchs client useEffect au chargement, élimine les layout shifts et réduit le temps de rendu initial. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,19 +8,21 @@ import { useTranslation } from "@/lib/i18n/context";
|
||||
export function AnilistTab({
|
||||
handleUpdateSetting,
|
||||
users,
|
||||
initialData,
|
||||
}: {
|
||||
handleUpdateSetting: (key: string, value: unknown) => Promise<void>;
|
||||
users: UserDto[];
|
||||
initialData: Record<string, unknown> | null;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [origin, setOrigin] = useState("");
|
||||
useEffect(() => { setOrigin(window.location.origin); }, []);
|
||||
|
||||
const [clientId, setClientId] = useState("");
|
||||
const [token, setToken] = useState("");
|
||||
const [userId, setUserId] = useState("");
|
||||
const [localUserId, setLocalUserId] = useState("");
|
||||
const [clientId, setClientId] = useState(initialData?.client_id ? String(initialData.client_id) : "");
|
||||
const [token, setToken] = useState(initialData?.access_token ? String(initialData.access_token) : "");
|
||||
const [userId, setUserId] = useState(initialData?.user_id ? String(initialData.user_id) : "");
|
||||
const [localUserId, setLocalUserId] = useState(initialData?.local_user_id ? String(initialData.local_user_id) : "");
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [viewer, setViewer] = useState<AnilistStatusDto | null>(null);
|
||||
const [testError, setTestError] = useState<string | null>(null);
|
||||
@@ -33,21 +35,6 @@ export function AnilistTab({
|
||||
const [isPreviewing, setIsPreviewing] = useState(false);
|
||||
const [previewItems, setPreviewItems] = useState<AnilistSyncPreviewItemDto[] | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/settings/anilist")
|
||||
.then((r) => r.ok ? r.json() : null)
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
if (data.client_id) setClientId(String(data.client_id));
|
||||
if (data.access_token) setToken(data.access_token);
|
||||
if (data.user_id) setUserId(String(data.user_id));
|
||||
if (data.local_user_id) setLocalUserId(String(data.local_user_id));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
function buildAnilistSettings() {
|
||||
return {
|
||||
client_id: clientId || undefined,
|
||||
|
||||
@@ -5,12 +5,12 @@ import { Card, CardHeader, CardTitle, CardDescription, CardContent, Button, Form
|
||||
import { KomgaSyncResponse, KomgaSyncReportSummary, UserDto } from "@/lib/api";
|
||||
import { useTranslation } from "@/lib/i18n/context";
|
||||
|
||||
export function KomgaSyncCard({ users }: { users: UserDto[] }) {
|
||||
export function KomgaSyncCard({ users, initialData }: { users: UserDto[]; initialData: Record<string, unknown> | null }) {
|
||||
const { t, locale } = useTranslation();
|
||||
const [komgaUrl, setKomgaUrl] = useState("");
|
||||
const [komgaUsername, setKomgaUsername] = useState("");
|
||||
const [komgaUrl, setKomgaUrl] = useState(initialData?.url ? String(initialData.url) : "");
|
||||
const [komgaUsername, setKomgaUsername] = useState(initialData?.username ? String(initialData.username) : "");
|
||||
const [komgaPassword, setKomgaPassword] = useState("");
|
||||
const [komgaUserId, setKomgaUserId] = useState(users[0]?.id ?? "");
|
||||
const [komgaUserId, setKomgaUserId] = useState(initialData?.user_id ? String(initialData.user_id) : (users[0]?.id ?? ""));
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [syncResult, setSyncResult] = useState<KomgaSyncResponse | null>(null);
|
||||
const [syncError, setSyncError] = useState<string | null>(null);
|
||||
@@ -39,13 +39,6 @@ export function KomgaSyncCard({ users }: { users: UserDto[] }) {
|
||||
|
||||
useEffect(() => {
|
||||
fetchReports();
|
||||
fetch("/api/settings/komga").then(r => r.ok ? r.json() : null).then(data => {
|
||||
if (data) {
|
||||
if (data.url) setKomgaUrl(data.url);
|
||||
if (data.username) setKomgaUsername(data.username);
|
||||
if (data.user_id) setKomgaUserId(data.user_id);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}, [fetchReports]);
|
||||
|
||||
async function handleViewReport(id: string) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, FormField, FormInput, FormSelect, Icon } from "@/app/components/ui";
|
||||
import { ProviderIcon } from "@/app/components/ProviderIcon";
|
||||
import { useTranslation } from "@/lib/i18n/context";
|
||||
@@ -11,25 +11,22 @@ export const METADATA_LANGUAGES = [
|
||||
{ value: "es", label: "Español" },
|
||||
] as const;
|
||||
|
||||
export function MetadataProvidersCard({ handleUpdateSetting }: { handleUpdateSetting: (key: string, value: unknown) => Promise<void> }) {
|
||||
const { t } = useTranslation();
|
||||
const [defaultProvider, setDefaultProvider] = useState("google_books");
|
||||
const [metadataLanguage, setMetadataLanguage] = useState("en");
|
||||
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
||||
function extractInitialApiKeys(data: Record<string, unknown> | null): Record<string, string> {
|
||||
const keys: Record<string, string> = {};
|
||||
if (data) {
|
||||
const comicvine = data.comicvine as Record<string, unknown> | undefined;
|
||||
const googleBooks = data.google_books as Record<string, unknown> | undefined;
|
||||
if (comicvine?.api_key) keys.comicvine = String(comicvine.api_key);
|
||||
if (googleBooks?.api_key) keys.google_books = String(googleBooks.api_key);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/settings/metadata_providers")
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
if (data.default_provider) setDefaultProvider(data.default_provider);
|
||||
if (data.metadata_language) setMetadataLanguage(data.metadata_language);
|
||||
if (data.comicvine?.api_key) setApiKeys((prev) => ({ ...prev, comicvine: data.comicvine.api_key }));
|
||||
if (data.google_books?.api_key) setApiKeys((prev) => ({ ...prev, google_books: data.google_books.api_key }));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
export function MetadataProvidersCard({ handleUpdateSetting, initialData }: { handleUpdateSetting: (key: string, value: unknown) => Promise<void>; initialData: Record<string, unknown> | null }) {
|
||||
const { t } = useTranslation();
|
||||
const [defaultProvider, setDefaultProvider] = useState(initialData?.default_provider ? String(initialData.default_provider) : "google_books");
|
||||
const [metadataLanguage, setMetadataLanguage] = useState(initialData?.metadata_language ? String(initialData.metadata_language) : "en");
|
||||
const [apiKeys, setApiKeys] = useState<Record<string, string>>(extractInitialApiKeys(initialData));
|
||||
|
||||
function save(provider: string, lang: string, keys: Record<string, string>) {
|
||||
const value: Record<string, unknown> = {
|
||||
|
||||
@@ -1,30 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, Button, FormField, FormInput, Icon } from "@/app/components/ui";
|
||||
import { useTranslation } from "@/lib/i18n/context";
|
||||
|
||||
export function ProwlarrCard({ handleUpdateSetting }: { handleUpdateSetting: (key: string, value: unknown) => Promise<void> }) {
|
||||
export function ProwlarrCard({ handleUpdateSetting, initialData }: { handleUpdateSetting: (key: string, value: unknown) => Promise<void>; initialData: Record<string, unknown> | null }) {
|
||||
const { t } = useTranslation();
|
||||
const [prowlarrUrl, setProwlarrUrl] = useState("");
|
||||
const [prowlarrApiKey, setProwlarrApiKey] = useState("");
|
||||
const [prowlarrCategories, setProwlarrCategories] = useState("7030, 7020");
|
||||
const [prowlarrUrl, setProwlarrUrl] = useState(initialData?.url ? String(initialData.url) : "");
|
||||
const [prowlarrApiKey, setProwlarrApiKey] = useState(initialData?.api_key ? String(initialData.api_key) : "");
|
||||
const [prowlarrCategories, setProwlarrCategories] = useState(
|
||||
Array.isArray(initialData?.categories) ? (initialData.categories as number[]).join(", ") : "7030, 7020"
|
||||
);
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/settings/prowlarr")
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
if (data.url) setProwlarrUrl(data.url);
|
||||
if (data.api_key) setProwlarrApiKey(data.api_key);
|
||||
if (data.categories) setProwlarrCategories(data.categories.join(", "));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
function saveProwlarr(url?: string, apiKey?: string, cats?: string) {
|
||||
const categories = (cats ?? prowlarrCategories)
|
||||
.split(",")
|
||||
|
||||
@@ -1,34 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, Button, FormField, FormInput, FormSelect, Icon } from "@/app/components/ui";
|
||||
import { useTranslation } from "@/lib/i18n/context";
|
||||
|
||||
export function QBittorrentCard({ handleUpdateSetting }: { handleUpdateSetting: (key: string, value: unknown) => Promise<void> }) {
|
||||
export function QBittorrentCard({ handleUpdateSetting, initialQbittorrent, initialTorrentImport }: { handleUpdateSetting: (key: string, value: unknown) => Promise<void>; initialQbittorrent: Record<string, unknown> | null; initialTorrentImport: Record<string, unknown> | null }) {
|
||||
const { t } = useTranslation();
|
||||
const [qbUrl, setQbUrl] = useState("");
|
||||
const [qbUsername, setQbUsername] = useState("");
|
||||
const [qbPassword, setQbPassword] = useState("");
|
||||
const [qbUrl, setQbUrl] = useState(initialQbittorrent?.url ? String(initialQbittorrent.url) : "");
|
||||
const [qbUsername, setQbUsername] = useState(initialQbittorrent?.username ? String(initialQbittorrent.username) : "");
|
||||
const [qbPassword, setQbPassword] = useState(initialQbittorrent?.password ? String(initialQbittorrent.password) : "");
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||
const [importEnabled, setImportEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/settings/qbittorrent")
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
if (data.url) setQbUrl(data.url);
|
||||
if (data.username) setQbUsername(data.username);
|
||||
if (data.password) setQbPassword(data.password);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
fetch("/api/settings/torrent_import")
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data) => { if (data?.enabled !== undefined) setImportEnabled(data.enabled); })
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
const [importEnabled, setImportEnabled] = useState(initialTorrentImport?.enabled === true);
|
||||
|
||||
function saveQbittorrent() {
|
||||
handleUpdateSetting("qbittorrent", {
|
||||
|
||||
@@ -1,36 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, Button, FormField, FormInput, FormSelect, Icon } from "@/app/components/ui";
|
||||
import { StatusMappingDto } from "@/lib/api";
|
||||
import { useTranslation } from "@/lib/i18n/context";
|
||||
|
||||
export function StatusMappingsCard() {
|
||||
export function StatusMappingsCard({ initialStatusMappings, initialSeriesStatuses, initialProviderStatuses }: { initialStatusMappings: Record<string, unknown>[]; initialSeriesStatuses: string[]; initialProviderStatuses: string[] }) {
|
||||
const { t } = useTranslation();
|
||||
const [mappings, setMappings] = useState<StatusMappingDto[]>([]);
|
||||
const [targetStatuses, setTargetStatuses] = useState<string[]>([]);
|
||||
const [providerStatuses, setProviderStatuses] = useState<string[]>([]);
|
||||
const [mappings, setMappings] = useState<StatusMappingDto[]>(initialStatusMappings as unknown as StatusMappingDto[]);
|
||||
const [targetStatuses, setTargetStatuses] = useState<string[]>(initialSeriesStatuses);
|
||||
const [providerStatuses] = useState<string[]>(initialProviderStatuses);
|
||||
const [newTargetName, setNewTargetName] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
const [mRes, sRes, pRes] = await Promise.all([
|
||||
fetch("/api/settings/status-mappings").then((r) => r.ok ? r.json() : []),
|
||||
fetch("/api/series/statuses").then((r) => r.ok ? r.json() : []),
|
||||
fetch("/api/series/provider-statuses").then((r) => r.ok ? r.json() : []),
|
||||
]);
|
||||
setMappings(mRes);
|
||||
setTargetStatuses(sRes);
|
||||
setProviderStatuses(pRes);
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { loadData(); }, [loadData]);
|
||||
|
||||
// Group mappings by target status (only those with a non-null mapped_status)
|
||||
const grouped = useMemo(() => {
|
||||
@@ -108,14 +88,6 @@ export function StatusMappingsCard() {
|
||||
return translated !== key ? translated : status;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="mb-6">
|
||||
<CardContent><p className="text-muted-foreground py-4">{t("common.loading")}</p></CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, Button, FormField, FormInput, Icon } from "@/app/components/ui";
|
||||
import { useTranslation } from "@/lib/i18n/context";
|
||||
|
||||
@@ -25,30 +25,18 @@ export const DEFAULT_EVENTS = {
|
||||
download_detection_failed: true,
|
||||
};
|
||||
|
||||
export function TelegramCard({ handleUpdateSetting }: { handleUpdateSetting: (key: string, value: unknown) => Promise<void> }) {
|
||||
export function TelegramCard({ handleUpdateSetting, initialData }: { handleUpdateSetting: (key: string, value: unknown) => Promise<void>; initialData: Record<string, unknown> | null }) {
|
||||
const { t } = useTranslation();
|
||||
const [botToken, setBotToken] = useState("");
|
||||
const [chatId, setChatId] = useState("");
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [events, setEvents] = useState(DEFAULT_EVENTS);
|
||||
const [botToken, setBotToken] = useState(initialData?.bot_token ? String(initialData.bot_token) : "");
|
||||
const [chatId, setChatId] = useState(initialData?.chat_id ? String(initialData.chat_id) : "");
|
||||
const [enabled, setEnabled] = useState(initialData?.enabled === true);
|
||||
const [events, setEvents] = useState(
|
||||
initialData?.events ? { ...DEFAULT_EVENTS, ...(initialData.events as Record<string, boolean>) } : DEFAULT_EVENTS
|
||||
);
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/settings/telegram")
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
if (data.bot_token) setBotToken(data.bot_token);
|
||||
if (data.chat_id) setChatId(data.chat_id);
|
||||
if (data.enabled !== undefined) setEnabled(data.enabled);
|
||||
if (data.events) setEvents({ ...DEFAULT_EVENTS, ...data.events });
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
function saveTelegram(token?: string, chat?: string, en?: boolean, ev?: typeof events) {
|
||||
handleUpdateSetting("telegram", {
|
||||
bot_token: token ?? botToken,
|
||||
|
||||
Reference in New Issue
Block a user