Files
checkflow/src/components/settings/SettingsModal.tsx
T
Neru_Han 04fef885a8
Build and Push Docker Image / build-and-push (push) Successful in 13m7s
feat(i18n,admin): modular localization dictionary & admin console demo preview
2026-08-21 20:00:01 +09:00

751 lines
31 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { useI18n } from "@/lib/i18n";
import { useTheme } from "@/app/providers";
import { getUserSettings, saveUserSettings, UserSettings } from "@/lib/mockData";
import { useUserPrefs } from "@/lib/useUserPrefs";
interface SettingsModalProps {
isOpen: boolean;
onClose: () => void;
user: { id: string; name?: string | null; email?: string | null; role?: string | null };
isDemo?: boolean;
}
/** A toggle-row component used throughout Labs tab */
function LabsRow({
label,
desc,
children,
}: {
label: string;
desc?: string;
children: React.ReactNode;
}) {
return (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "10px 14px",
background: "var(--bg-secondary)",
borderRadius: "var(--radius-md)",
border: "1px solid var(--border)",
marginBottom: 8,
gap: 16,
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 600, color: "var(--text-primary)" }}>{label}</div>
{desc && (
<div style={{ fontSize: 12, color: "var(--text-secondary)", marginTop: 2, lineHeight: 1.4 }}>
{desc}
</div>
)}
</div>
<div style={{ flexShrink: 0 }}>{children}</div>
</div>
);
}
/** Segmented control (radio-style) */
function SegmentedControl<T extends string>({
value,
options,
onChange,
}: {
value: T;
options: { label: string; value: T }[];
onChange: (v: T) => void;
}) {
return (
<div
style={{
display: "inline-flex",
background: "var(--bg-primary)",
border: "1px solid var(--border)",
borderRadius: "var(--radius-sm)",
padding: 2,
gap: 2,
}}
>
{options.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => onChange(opt.value)}
style={{
padding: "4px 10px",
fontSize: 12,
fontWeight: value === opt.value ? 700 : 500,
borderRadius: "var(--radius-xs)",
background: value === opt.value ? "var(--accent)" : "transparent",
color: value === opt.value ? "#fff" : "var(--text-secondary)",
border: "none",
cursor: "pointer",
transition: "all var(--dur-fast)",
}}
>
{opt.label}
</button>
))}
</div>
);
}
/** Slider with live value display */
function PrefSlider({
min,
max,
value,
onChange,
unit,
}: {
min: number;
max: number;
value: number;
onChange: (v: number) => void;
unit?: string;
}) {
return (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<input
type="range"
min={min}
max={max}
value={value}
onChange={(e) => onChange(parseInt(e.target.value, 10))}
style={{ width: 120, accentColor: "var(--accent)" }}
/>
<span style={{ fontSize: 12, fontWeight: 600, color: "var(--text-secondary)", minWidth: 40 }}>
{value}{unit}
</span>
</div>
);
}
export function SettingsModal({ isOpen, onClose, user, isDemo = false }: SettingsModalProps) {
const { t, lang, setLang } = useI18n();
const { theme, toggleTheme } = useTheme();
const { prefs, updatePrefs, resetToDefaults } = useUserPrefs();
const [activeTab, setActiveTab] = useState<"profile" | "preferences" | "labs" | "sync" | "admin">("profile");
const [displayName, setDisplayName] = useState(() => user.name || (typeof window !== "undefined" ? getUserSettings().displayName : "Demo User"));
const [email, setEmail] = useState(() => user.email || (typeof window !== "undefined" ? getUserSettings().email : "demo@checkflow.local"));
const [password, setPassword] = useState("");
const [trashRetention, setTrashRetention] = useState(() => (typeof window !== "undefined" ? (getUserSettings().trashRetentionDays ?? 30) : 30));
const [savedMsg, setSavedMsg] = useState("");
const [syncPlatform, setSyncPlatform] = useState<"android" | "apple" | "thunderbird">("android");
const [copiedCalDav, setCopiedCalDav] = useState(false);
const [testSyncStatus, setTestSyncStatus] = useState<"idle" | "testing" | "success" | "error">("idle");
useEffect(() => {
if (isOpen) {
const s = getUserSettings();
setDisplayName(user.name || s.displayName);
setEmail(user.email || s.email);
setTrashRetention(s.trashRetentionDays ?? 30);
}
}, [isOpen, user]);
const handleTestConnection = async () => {
setTestSyncStatus("testing");
try {
const res = await fetch("/api/dav", { method: "OPTIONS" });
if (res.ok || res.status === 401 || res.status === 207) {
setTestSyncStatus("success");
} else {
setTestSyncStatus("error");
}
} catch {
setTestSyncStatus("error");
}
setTimeout(() => {
setTestSyncStatus("idle");
}, 4000);
};
if (!isOpen) return null;
const handleSave = () => {
const newSettings: UserSettings = {
displayName,
email,
trashRetentionDays: trashRetention,
theme,
language: lang,
};
saveUserSettings(newSettings);
setSavedMsg("✓ " + (lang === "ko" ? "설정이 저장되었습니다" : lang === "ja" ? "設定が保存されました" : "Settings saved"));
setTimeout(() => {
setSavedMsg("");
onClose();
}, 1000);
};
const calDavUrl = typeof window !== "undefined" ? `${window.location.origin}/api/dav` : "https://todo.yourdomain.com/api/dav";
// Accent hue preview
const hue = prefs.accentHue;
const sat = Math.round(prefs.saturation * 0.9);
const accentPreview = `hsl(${hue}, ${sat}%, 54%)`;
const tabs = [
{ id: "profile", label: `👤 ${t("profile") || "Profile"}` },
{ id: "preferences", label: `⚙️ ${t("preferences") || "Preferences"}` },
{ id: "labs", label: `🧪 ${t("labs") || "Labs"}` },
{ id: "sync", label: `📱 ${t("syncIntegrations") || "Integrations"}` },
{ id: "admin", label: `👑 ${t("admin") || "Admin"}` },
] as const;
return (
<div className="modal-overlay" onClick={onClose}>
<div
className="modal settings-modal"
onClick={(e) => e.stopPropagation()}
style={{ maxWidth: 660, width: "92%", padding: "24px 24px 20px", maxHeight: "90vh", display: "flex", flexDirection: "column" }}
>
{/* Header */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 14 }}>
<h2 className="modal-title" style={{ marginBottom: 0, fontSize: 17 }}>⚙️ {t("settingsModalTitle") || "Settings"}</h2>
<button className="icon-btn" onClick={onClose} aria-label="Close" type="button"></button>
</div>
{/* Tab Bar */}
<div
style={{
display: "flex",
gap: 2,
borderBottom: "1px solid var(--border)",
marginBottom: 16,
paddingBottom: 0,
overflowX: "auto",
}}
>
{tabs.map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => setActiveTab(tab.id)}
style={{
padding: "7px 14px",
fontSize: 12.5,
fontWeight: activeTab === tab.id ? 700 : 500,
background: "transparent",
color: activeTab === tab.id ? "var(--accent)" : "var(--text-secondary)",
border: "none",
borderBottom: activeTab === tab.id ? "2px solid var(--accent)" : "2px solid transparent",
borderRadius: 0,
cursor: "pointer",
whiteSpace: "nowrap",
transition: "all var(--dur-fast)",
}}
>
{tab.label}
</button>
))}
</div>
{/* Scrollable tab content */}
<div style={{ flex: 1, overflowY: "auto", paddingRight: 2 }}>
{/* Tab 1: Profile */}
{activeTab === "profile" && (
<div className="settings-tab-content">
<div className="form-group">
<label className="form-label">{t("displayName")}</label>
<input className="form-input" value={displayName} onChange={(e) => setDisplayName(e.target.value)} placeholder="Your Name" />
</div>
<div className="form-group">
<label className="form-label">{t("email")}</label>
<input className="form-input" type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="your.email@example.com" />
</div>
<div className="form-group">
<label className="form-label">{t("password")} (Change)</label>
<input className="form-input" type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="New password (leave blank to keep current)" />
</div>
</div>
)}
{/* Tab 2: Preferences */}
{activeTab === "preferences" && (
<div className="settings-tab-content">
<div className="form-group">
<label className="form-label" style={{ fontWeight: 600 }}>
🗑️ {t("trashRetention") || "Trash Auto-Delete Retention Period"}
</label>
<p style={{ fontSize: 12, color: "var(--text-tertiary)", marginBottom: 8 }}>
{t("trashRetentionHint") || "Deleted tasks will be permanently removed after the specified period."}
</p>
<select className="form-input" value={trashRetention} onChange={(e) => setTrashRetention(parseInt(e.target.value, 10))}>
<option value={7}>{t("days7") || "7 Days"}</option>
<option value={14}>{t("days14") || "14 Days"}</option>
<option value={30}>{t("days30") || "30 Days (Recommended)"}</option>
<option value={0}>{t("neverDelete") || "Never Auto-Delete (Manual empty only)"}</option>
</select>
</div>
<div className="form-group">
<label className="form-label">{t("language")}</label>
<select className="form-input" value={lang} onChange={(e) => setLang(e.target.value as "en" | "ko" | "ja")}>
<option value="en">English (Default)</option>
<option value="ko">한국어 (Korean)</option>
<option value="ja">日本語 (Japanese)</option>
</select>
</div>
<div className="form-group">
<label className="form-label">{t("theme")}</label>
<div style={{ display: "flex", gap: 8 }}>
{[
{ key: "system", icon: "💻", label: t("themeSystem") },
{ key: "light", icon: "☀️", label: t("themeLight") },
{ key: "dark", icon: "🌙", label: t("themeDark") },
].map(({ key, icon, label }) => (
<button
key={key}
type="button"
className={`btn btn-sm ${theme === key ? "btn-primary" : "btn-ghost"}`}
onClick={() => { if (theme !== key) toggleTheme(); }}
>
{icon} {label}
</button>
))}
</div>
</div>
</div>
)}
{/* Tab 3: 🧪 CheckFlow Labs */}
{activeTab === "labs" && (
<div className="settings-tab-content">
{/* Section: View */}
<div style={{ fontSize: 11, fontWeight: 700, color: "var(--text-tertiary)", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: 8 }}>
View
</div>
<LabsRow
label="📊 Kanban Board"
desc="Switch between list view and 3-column Kanban board (To Do / In Progress / Done)."
>
<SegmentedControl
value={prefs.viewMode}
options={[
{ label: "List", value: "list" },
{ label: "Kanban", value: "kanban" },
]}
onChange={(v) => updatePrefs({ viewMode: v })}
/>
</LabsRow>
{/* Section: Layout */}
<div style={{ fontSize: 11, fontWeight: 700, color: "var(--text-tertiary)", letterSpacing: "0.08em", textTransform: "uppercase", margin: "16px 0 8px" }}>
Layout
</div>
<LabsRow
label="📐 Content Density"
desc="Controls the vertical spacing of task items."
>
<SegmentedControl
value={prefs.density}
options={[
{ label: "Compact", value: "compact" },
{ label: "Default", value: "default" },
{ label: "Airy", value: "comfortable" },
]}
onChange={(v) => updatePrefs({ density: v })}
/>
</LabsRow>
<LabsRow
label="↔️ Sidebar Width"
desc={`Drag the sidebar edge or adjust here. (${prefs.sidebarWidth}px)`}
>
<PrefSlider
min={160}
max={420}
value={prefs.sidebarWidth}
onChange={(v) => updatePrefs({ sidebarWidth: v })}
unit="px"
/>
</LabsRow>
<LabsRow
label="↔️ Detail Panel Width"
desc={`Drag the panel edge or adjust here. (${prefs.detailWidth}px)`}
>
<PrefSlider
min={300}
max={760}
value={prefs.detailWidth}
onChange={(v) => updatePrefs({ detailWidth: v })}
unit="px"
/>
</LabsRow>
{/* Section: Appearance */}
<div style={{ fontSize: 11, fontWeight: 700, color: "var(--text-tertiary)", letterSpacing: "0.08em", textTransform: "uppercase", margin: "16px 0 8px" }}>
Appearance
</div>
<LabsRow
label="🔤 Font Size"
desc="Base font size across the app."
>
<SegmentedControl
value={prefs.fontSize}
options={[
{ label: "S", value: "small" },
{ label: "M", value: "default" },
{ label: "L", value: "large" },
]}
onChange={(v) => updatePrefs({ fontSize: v })}
/>
</LabsRow>
<LabsRow
label="🎨 Accent Color"
desc="Choose the hue of your accent color. Saturation controls vibrancy."
>
<div style={{ display: "flex", flexDirection: "column", gap: 6, alignItems: "flex-end" }}>
{/* Hue ring preview */}
<div style={{ display: "flex", gap: 5, flexWrap: "wrap", justifyContent: "flex-end" }}>
{[210, 250, 340, 10, 45, 145, 185].map((h) => (
<div
key={h}
onClick={() => updatePrefs({ accentHue: h })}
title={`Hue ${h}°`}
style={{
width: 20,
height: 20,
borderRadius: "50%",
background: `hsl(${h}, ${sat}%, 54%)`,
cursor: "pointer",
border: prefs.accentHue === h ? "2.5px solid var(--text-primary)" : "2px solid transparent",
outline: prefs.accentHue === h ? `3px solid ${accentPreview}` : "none",
outlineOffset: 1,
transition: "transform var(--dur-fast)",
transform: prefs.accentHue === h ? "scale(1.2)" : "scale(1)",
}}
/>
))}
</div>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<span style={{ fontSize: 11, color: "var(--text-tertiary)" }}>Saturation</span>
<PrefSlider
min={20}
max={100}
value={prefs.saturation}
onChange={(v) => updatePrefs({ saturation: v })}
unit="%"
/>
</div>
{/* Custom hue input */}
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<span style={{ fontSize: 11, color: "var(--text-tertiary)" }}>Hue °</span>
<input
type="range"
min={0}
max={360}
value={prefs.accentHue}
onChange={(e) => updatePrefs({ accentHue: parseInt(e.target.value, 10) })}
style={{ width: 100, accentColor: accentPreview }}
/>
<div style={{ width: 18, height: 18, borderRadius: "50%", background: accentPreview, flexShrink: 0 }} />
</div>
</div>
</LabsRow>
<LabsRow
label="⬛ Border Roundness"
desc="Controls the roundness of cards, buttons, and UI elements."
>
<SegmentedControl
value={prefs.roundness}
options={[
{ label: "Sharp", value: "sharp" },
{ label: "Default", value: "default" },
{ label: "Round", value: "round" },
]}
onChange={(v) => updatePrefs({ roundness: v })}
/>
</LabsRow>
<LabsRow
label="⚡ Animation Speed"
desc="Controls the speed of transitions and hover effects."
>
<SegmentedControl
value={prefs.animationSpeed}
options={[
{ label: "Off", value: "none" },
{ label: "Fast", value: "fast" },
{ label: "Default", value: "default" },
{ label: "Slow", value: "slow" },
]}
onChange={(v) => updatePrefs({ animationSpeed: v })}
/>
</LabsRow>
{/* Reset to Defaults */}
<div style={{ marginTop: 20, borderTop: "1px solid var(--border)", paddingTop: 16 }}>
<LabsRow
label="🔄 Reset to Defaults"
desc="Restores all layout, appearance, and view settings to their factory defaults."
>
<button
type="button"
className="btn btn-sm"
style={{ color: "var(--danger)", border: "1px solid var(--danger)", background: "transparent" }}
onClick={() => {
if (confirm(t("resetConfirm") || "Reset all customizations to defaults?")) {
resetToDefaults();
}
}}
>
{t("resetDefaults") || "Reset"}
</button>
</LabsRow>
</div>
</div>
)}
{/* Tab: CalDAV / External Sync */}
{activeTab === "sync" && (
<div className="settings-tab-content">
<h3 style={{ fontSize: 15, fontWeight: 700, marginBottom: 6 }}>
📱 {t("syncTitle") || "CalDAV & Mobile Sync (Android / Apple)"}
</h3>
<p style={{ fontSize: 13, color: "var(--text-secondary)", lineHeight: 1.5, marginBottom: 14 }}>
{t("syncDesc") || "CheckFlow supports native two-way synchronization with Android, Apple Reminders, and Thunderbird via standard CalDAV."}
</p>
{/* Endpoint bar & Action buttons */}
<div className="form-group" style={{ marginBottom: 14 }}>
<label className="form-label" style={{ fontWeight: 600 }}>{t("syncBaseUrl") || "CalDAV Server Base URL"}</label>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<input
className="form-input"
readOnly
value={calDavUrl}
style={{ fontFamily: "monospace", fontSize: 12.5, background: "var(--bg-secondary)", flex: 1 }}
onClick={(e) => (e.target as HTMLInputElement).select()}
/>
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={() => {
navigator.clipboard.writeText(calDavUrl);
setCopiedCalDav(true);
setTimeout(() => setCopiedCalDav(false), 2000);
}}
style={{ minWidth: 80, fontWeight: 600 }}
>
{copiedCalDav ? `✓ ${t("syncCopied") || "Copied!"}` : `📋 ${t("syncCopyUrl") || "Copy"}`}
</button>
</div>
</div>
{/* Actions row: Test & Download */}
<div style={{ display: "flex", gap: 10, marginBottom: 16, flexWrap: "wrap", alignItems: "center" }}>
<button
type="button"
className="btn btn-sm btn-ghost"
onClick={handleTestConnection}
disabled={testSyncStatus === "testing"}
style={{ display: "inline-flex", alignItems: "center", gap: 6 }}
>
{testSyncStatus === "testing" ? `⏳ ${t("syncTesting") || "Testing..."}` : `🔍 ${t("syncTestConnection") || "Test Endpoint"}`}
</button>
<a
href="/api/dav"
target="_blank"
rel="noreferrer"
className="btn btn-sm btn-ghost"
style={{ display: "inline-flex", alignItems: "center", gap: 6, textDecoration: "none" }}
>
📥 {t("syncDownloadIcs") || "Download .ICS Feed"}
</a>
{testSyncStatus === "success" && (
<span style={{ fontSize: 12, color: "var(--success)", fontWeight: 600 }}>
{t("syncTestSuccess") || "✓ CalDAV endpoint responded successfully"}
</span>
)}
{testSyncStatus === "error" && (
<span style={{ fontSize: 12, color: "var(--danger)", fontWeight: 600 }}>
Endpoint test failed
</span>
)}
</div>
{/* Client setup guides segmented selector */}
<div style={{ marginBottom: 10 }}>
<SegmentedControl
value={syncPlatform}
options={[
{ label: `🤖 ${t("syncTabAndroid") || "Android (DAVx⁵)"}`, value: "android" },
{ label: `🍎 ${t("syncTabApple") || "Apple Reminders"}`, value: "apple" },
{ label: `🦅 ${t("syncTabThunderbird") || "Thunderbird"}`, value: "thunderbird" },
]}
onChange={(v) => setSyncPlatform(v)}
/>
</div>
{/* Step by step cards */}
<div
style={{
background: "var(--bg-secondary)",
padding: "14px 16px",
borderRadius: "var(--radius-md)",
border: "1px solid var(--border)",
fontSize: 12.5,
color: "var(--text-primary)",
lineHeight: 1.6,
}}
>
{syncPlatform === "android" && (
<div>
<div style={{ fontWeight: 700, marginBottom: 6, color: "var(--accent)" }}>
📱 Android (DAVx + Tasks.org / OpenTasks)
</div>
<ol style={{ paddingLeft: 20, margin: 0, display: "flex", flexDirection: "column", gap: 4 }}>
<li>{t("syncAndroidStep1")}</li>
<li>{t("syncAndroidStep2")}</li>
<li>{t("syncAndroidStep3")}</li>
<li>{t("syncAndroidStep4")}</li>
</ol>
<div style={{ marginTop: 10, fontSize: 11.5, color: "var(--text-tertiary)", borderTop: "1px dashed var(--border)", paddingTop: 8 }}>
💡 <strong>Tip:</strong> In DAVx account settings, set Sync Interval to <strong>15 minutes</strong> for battery efficiency and near real-time sync.
</div>
</div>
)}
{syncPlatform === "apple" && (
<div>
<div style={{ fontWeight: 700, marginBottom: 6, color: "var(--accent)" }}>
🍎 Apple Reminders & Calendar (iOS / iPadOS / macOS)
</div>
<ol style={{ paddingLeft: 20, margin: 0, display: "flex", flexDirection: "column", gap: 4 }}>
<li>{t("syncAppleStep1")}</li>
<li>{t("syncAppleStep2")}</li>
<li>{t("syncAppleStep3")}</li>
<li>{t("syncAppleStep4")}</li>
</ol>
<div style={{ marginTop: 10, fontSize: 11.5, color: "var(--text-tertiary)", borderTop: "1px dashed var(--border)", paddingTop: 8 }}>
💡 <strong>Tip:</strong> If using HTTPS behind a reverse proxy (Nginx/Caddy), ensure valid SSL certificates are trusted by Apple devices.
</div>
</div>
)}
{syncPlatform === "thunderbird" && (
<div>
<div style={{ fontWeight: 700, marginBottom: 6, color: "var(--accent)" }}>
💻 Mozilla Thunderbird (Windows / Mac / Linux)
</div>
<ol style={{ paddingLeft: 20, margin: 0, display: "flex", flexDirection: "column", gap: 4 }}>
<li>{t("syncThunderbirdStep1")}</li>
<li>{t("syncThunderbirdStep2")}</li>
<li>{t("syncThunderbirdStep3")}</li>
<li>{t("syncThunderbirdStep4")}</li>
</ol>
<div style={{ marginTop: 10, fontSize: 11.5, color: "var(--text-tertiary)", borderTop: "1px dashed var(--border)", paddingTop: 8 }}>
💡 <strong>Tip:</strong> Thunderbird Tasks view will display CheckFlow priority tags, due dates, and completion status.
</div>
</div>
)}
</div>
</div>
)}
{/* Tab 4: Admin Quick Access */}
{activeTab === "admin" && (
<div className="settings-tab-content">
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }}>
<span style={{ fontSize: 24 }}>👑</span>
<div>
<h3 style={{ fontSize: 15, fontWeight: 700, margin: 0 }}>
{t("admin") || "Admin Console"}
</h3>
<p style={{ fontSize: 12, color: "var(--text-tertiary)", margin: "2px 0 0" }}>
Multi-user administration, account control, and system telemetry
</p>
</div>
</div>
<div
style={{
background: "var(--bg-secondary)",
borderRadius: "var(--radius-md)",
padding: 16,
border: "1px solid var(--border)",
marginBottom: 16,
fontSize: 13,
lineHeight: 1.6,
}}
>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 10, flexWrap: "wrap", gap: 8 }}>
<div>
<strong style={{ color: "var(--text-primary)" }}>Current Account:</strong>{" "}
<code style={{ background: "var(--bg-primary)", padding: "2px 6px", borderRadius: 4, border: "1px solid var(--border)" }}>
{user.email || "demo@checkflow.local"}
</code>
</div>
<div>
<span
style={{
padding: "2px 8px",
borderRadius: "var(--radius-sm)",
fontSize: 11,
fontWeight: 700,
background: "rgba(75, 123, 245, 0.15)",
color: "var(--accent)",
border: "1px solid var(--border)",
}}
>
{user.role || (isDemo ? "DEMO_ADMIN" : "USER")}
</span>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginTop: 12 }}>
<div style={{ background: "var(--bg-primary)", padding: "10px 12px", borderRadius: "var(--radius-sm)", border: "1px solid var(--border)" }}>
<div style={{ fontSize: 11, color: "var(--text-tertiary)" }}>Isolation Mode</div>
<div style={{ fontSize: 13, fontWeight: 600, marginTop: 2, color: "var(--success)" }}> Multi-User Privacy</div>
</div>
<div style={{ background: "var(--bg-primary)", padding: "10px 12px", borderRadius: "var(--radius-sm)", border: "1px solid var(--border)" }}>
<div style={{ fontSize: 11, color: "var(--text-tertiary)" }}>CalDAV Endpoint</div>
<div style={{ fontSize: 13, fontWeight: 600, marginTop: 2, color: "var(--accent)" }}> Active / Enabled</div>
</div>
</div>
</div>
<div style={{ display: "flex", gap: 10 }}>
<a
href="/admin"
className="btn btn-primary"
style={{ display: "inline-flex", alignItems: "center", gap: 8, textDecoration: "none" }}
>
🚀 Open Admin Dashboard
</a>
</div>
</div>
)}
</div>
{/* Footer */}
<div
className="modal-footer"
style={{ marginTop: 16, paddingTop: 12, borderTop: "1px solid var(--border)", display: "flex", justifyContent: "space-between", alignItems: "center" }}
>
{savedMsg && <span style={{ fontSize: 13, color: "var(--success)", fontWeight: 600 }}>{savedMsg}</span>}
<div style={{ display: "flex", gap: 8, marginLeft: "auto" }}>
<button className="btn btn-ghost" onClick={onClose} type="button">{t("cancel")}</button>
<button className="btn btn-primary" onClick={handleSave} type="button">
{t("save") || "Save Changes"}
</button>
</div>
</div>
</div>
</div>
);
}