Infrastructure Sync: Initial migration to custom self-hosted Gitea
This commit is contained in:
@@ -125,17 +125,20 @@ function PrefSlider({
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsModal({ isOpen, onClose, user, isDemo = false }: SettingsModalProps) {
|
||||
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 || "Demo User");
|
||||
const [email, setEmail] = useState(user.email || "demo@checkflow.local");
|
||||
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(30);
|
||||
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) {
|
||||
@@ -146,6 +149,23 @@ export function SettingsModal({ isOpen, onClose, user, isDemo = false }: Setting
|
||||
}
|
||||
}, [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 = () => {
|
||||
@@ -267,7 +287,7 @@ export function SettingsModal({ isOpen, onClose, user, isDemo = false }: Setting
|
||||
</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 any)}>
|
||||
<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>
|
||||
@@ -428,7 +448,7 @@ export function SettingsModal({ isOpen, onClose, user, isDemo = false }: Setting
|
||||
min={0}
|
||||
max={360}
|
||||
value={prefs.accentHue}
|
||||
onChange={(e) => updatePrefs({ accentHue: parseInt(e.target.value) })}
|
||||
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 }} />
|
||||
@@ -490,30 +510,149 @@ export function SettingsModal({ isOpen, onClose, user, isDemo = false }: Setting
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab: CalDAV / CardDAV Integrations */}
|
||||
{/* Tab: CalDAV / External Sync */}
|
||||
{activeTab === "sync" && (
|
||||
<div className="settings-tab-content">
|
||||
<h3 style={{ fontSize: 14, fontWeight: 700, marginBottom: 8 }}>📱 Galaxy & Mobile Sync via DAVx⁵</h3>
|
||||
<p style={{ fontSize: 13, color: "var(--text-secondary)", lineHeight: 1.5, marginBottom: 12 }}>
|
||||
CheckFlow supports native two-way synchronization with Samsung Galaxy Reminder, Apple Reminders, and Thunderbird via CalDAV/CardDAV protocols.
|
||||
<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>
|
||||
<div className="form-group">
|
||||
<label className="form-label">CalDAV Base Endpoint</label>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<input className="form-input" readOnly value={calDavUrl} style={{ fontFamily: "monospace", fontSize: 12 }} />
|
||||
<button type="button" className="btn btn-ghost btn-sm" onClick={() => { navigator.clipboard.writeText(calDavUrl); }}>
|
||||
📋 Copy
|
||||
|
||||
{/* 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>
|
||||
<div style={{ background: "var(--bg-secondary)", padding: 12, borderRadius: "var(--radius-sm)", fontSize: 12, color: "var(--text-secondary)" }}>
|
||||
<strong>Setup Instructions for DAVx⁵ (Android):</strong>
|
||||
<ol style={{ paddingLeft: 18, marginTop: 6, lineHeight: 1.6 }}>
|
||||
<li>Install <strong>DAVx⁵</strong> on your Samsung Galaxy from Google Play or F-Droid.</li>
|
||||
<li>Add account → <strong>Login with URL and user name</strong>.</li>
|
||||
<li>Base URL: <code>{calDavUrl}</code></li>
|
||||
<li>User / Password: Your CheckFlow email & password.</li>
|
||||
</ol>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user