Aktueller Stand
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
|
||||
export default function SettingsPage() {
|
||||
@@ -17,6 +17,9 @@ export default function SettingsPage() {
|
||||
const [profileStatus, setProfileStatus] = useState<string | null>(null);
|
||||
const [theme, setTheme] = useState<"light" | "dark">("light");
|
||||
const [copyStatus, setCopyStatus] = useState<"success" | "error" | null>(null);
|
||||
const [appName, setAppName] = useState("Vereinskalender");
|
||||
const [icalPastDays, setIcalPastDays] = useState(14);
|
||||
const icalReadyRef = useRef(false);
|
||||
|
||||
const loadView = async () => {
|
||||
try {
|
||||
@@ -25,6 +28,10 @@ export default function SettingsPage() {
|
||||
const payload = await response.json();
|
||||
setViewToken(payload.token);
|
||||
setViewId(payload.id);
|
||||
setIcalPastDays(
|
||||
typeof payload.icalPastDays === "number" ? payload.icalPastDays : 14
|
||||
);
|
||||
icalReadyRef.current = true;
|
||||
const ids = new Set<string>(
|
||||
(payload.categories || []).map((item: { categoryId: string }) => item.categoryId)
|
||||
);
|
||||
@@ -44,10 +51,23 @@ export default function SettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadAppName = async () => {
|
||||
try {
|
||||
const nameResponse = await fetch("/api/settings/app-name");
|
||||
if (nameResponse.ok) {
|
||||
const payload = await nameResponse.json();
|
||||
setAppName(payload.name || "Vereinskalender");
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.user) {
|
||||
loadView();
|
||||
loadCategories();
|
||||
loadAppName();
|
||||
}
|
||||
}, [data?.user]);
|
||||
|
||||
@@ -106,7 +126,39 @@ export default function SettingsPage() {
|
||||
};
|
||||
|
||||
const baseUrl = typeof window === "undefined" ? "" : window.location.origin;
|
||||
const icalUrl = viewToken ? `${baseUrl}/api/ical/${viewToken}` : "";
|
||||
const toFilename = (value: string) =>
|
||||
value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)/g, "") || "kalender";
|
||||
|
||||
const icalQuery = icalPastDays > 0 ? `?pastDays=${icalPastDays}` : "";
|
||||
const icalUrl = viewToken
|
||||
? `${baseUrl}/api/ical/${viewToken}/${toFilename(appName)}.ical${icalQuery}`
|
||||
: "";
|
||||
|
||||
const updateIcalPastDays = async (value: number) => {
|
||||
setError(null);
|
||||
setStatus(null);
|
||||
const response = await fetch("/api/views/default", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ icalPastDays: value })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
setError(data.error || "Einstellung konnte nicht gespeichert werden.");
|
||||
return;
|
||||
}
|
||||
setStatus("iCal-Einstellung gespeichert.");
|
||||
window.setTimeout(() => setStatus(null), 2500);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!icalReadyRef.current || !viewId) return;
|
||||
updateIcalPastDays(icalPastDays);
|
||||
}, [icalPastDays, viewId]);
|
||||
|
||||
const applyTheme = (next: "light" | "dark") => {
|
||||
setTheme(next);
|
||||
@@ -247,43 +299,73 @@ export default function SettingsPage() {
|
||||
Dein Link kann in externen Kalender-Apps abonniert werden.
|
||||
</p>
|
||||
{viewToken ? (
|
||||
<div className="rounded-xl border border-slate-200 bg-slate-50 p-3 text-sm">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="font-medium">iCal URL</p>
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyIcalUrl}
|
||||
aria-label="iCal-Link kopieren"
|
||||
className="rounded-full border border-slate-200 p-2 text-slate-600 transition hover:bg-slate-100"
|
||||
<div className="ical-link flex items-center gap-3 rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm">
|
||||
<span className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500">
|
||||
iCal
|
||||
</span>
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate text-slate-700"
|
||||
title={icalUrl}
|
||||
>
|
||||
{icalUrl}
|
||||
</span>
|
||||
<div className="relative flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyIcalUrl}
|
||||
aria-label="iCal-Link kopieren"
|
||||
className="rounded-full border border-slate-200 p-2 text-slate-600 transition hover:bg-slate-100"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" />
|
||||
<rect x="3" y="3" width="13" height="13" rx="2" />
|
||||
</svg>
|
||||
</button>
|
||||
{copyStatus && (
|
||||
<div
|
||||
className={`absolute right-0 top-full mt-2 rounded-full px-3 py-1 text-[11px] font-semibold shadow ${
|
||||
copyStatus === "success"
|
||||
? "bg-emerald-100 text-emerald-700"
|
||||
: "bg-rose-100 text-rose-700"
|
||||
}`}
|
||||
>
|
||||
{copyStatus === "success" ? "Kopiert" : "Fehler"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" />
|
||||
<rect x="3" y="3" width="13" height="13" rx="2" />
|
||||
</svg>
|
||||
</button>
|
||||
{copyStatus && (
|
||||
<div
|
||||
className={`absolute right-0 top-full mt-2 rounded-full px-3 py-1 text-[11px] font-semibold shadow ${
|
||||
copyStatus === "success"
|
||||
? "bg-emerald-100 text-emerald-700"
|
||||
: "bg-rose-100 text-rose-700"
|
||||
}`}
|
||||
>
|
||||
{copyStatus === "success" ? "Kopiert" : "Fehler"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="break-all text-slate-700">{icalUrl}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-slate-600">iCal-Link wird geladen...</p>
|
||||
)}
|
||||
<button type="button" className="btn-ghost" onClick={rotateToken}>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<label className="flex items-center gap-3 text-sm text-slate-700">
|
||||
<span className="relative inline-flex h-6 w-11 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="peer sr-only"
|
||||
checked={icalPastDays > 0}
|
||||
onChange={(event) => setIcalPastDays(event.target.checked ? 14 : 0)}
|
||||
/>
|
||||
<span className="h-6 w-11 rounded-full bg-slate-200 transition peer-checked:bg-emerald-500"></span>
|
||||
<span className="absolute left-1 top-1 h-4 w-4 rounded-full bg-white transition peer-checked:translate-x-5"></span>
|
||||
</span>
|
||||
Rückblick der letzten 14 Tage aktivieren
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" className="hidden" onClick={rotateToken}>
|
||||
Link erneuern
|
||||
</button>
|
||||
{status && <p className="text-sm text-emerald-600">{status}</p>}
|
||||
{status && (
|
||||
<div className="fixed bottom-6 right-6 z-40 rounded-full bg-emerald-600 px-4 py-2 text-sm font-semibold text-white shadow-lg">
|
||||
{status}
|
||||
</div>
|
||||
)}
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user