690 lines
28 KiB
TypeScript
690 lines
28 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 };
|
|
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: _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") || "Galaxy & External Sync (CalDAV)"}
|
|
</h3>
|
|
<p style={{ fontSize: 13, color: "var(--text-secondary)", lineHeight: 1.5, marginBottom: 14 }}>
|
|
{t("syncDesc") || "CheckFlow supports native two-way synchronization with Samsung Galaxy Reminder, Apple Reminders, and Thunderbird via 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") || "Galaxy / 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)" }}>
|
|
📱 Samsung Galaxy & Android (DAVx⁵ + Reminder / 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">
|
|
<h3 style={{ fontSize: 14, fontWeight: 700, marginBottom: 8 }}>👑 Multi-User Admin Console</h3>
|
|
<p style={{ fontSize: 13, color: "var(--text-secondary)", marginBottom: 14 }}>
|
|
Manage registered users, user roles (User/Admin), system statistics, and storage allocations.
|
|
</p>
|
|
<a href="/admin" className="btn btn-primary" style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
|
|
🚀 Open Admin Dashboard →
|
|
</a>
|
|
</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>
|
|
);
|
|
} |