Feature: Mobile bottom-sheet drawer (slide-up from bottom, swipe-down to close, overlay), update AGENTS.md to v4.0
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
"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";
|
||||
|
||||
interface SettingsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
user: { id: string; name?: string | null; email?: string | null };
|
||||
isDemo?: boolean;
|
||||
}
|
||||
|
||||
export function SettingsModal({ isOpen, onClose, user, isDemo = false }: SettingsModalProps) {
|
||||
const { t, lang, setLang } = useI18n();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<"profile" | "preferences" | "sync" | "admin">("profile");
|
||||
const [displayName, setDisplayName] = useState(user.name || "Demo User");
|
||||
const [email, setEmail] = useState(user.email || "demo@checkflow.local");
|
||||
const [password, setPassword] = useState("");
|
||||
const [trashRetention, setTrashRetention] = useState(30);
|
||||
const [savedMsg, setSavedMsg] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const s = getUserSettings();
|
||||
setDisplayName(user.name || s.displayName);
|
||||
setEmail(user.email || s.email);
|
||||
setTrashRetention(s.trashRetentionDays ?? 30);
|
||||
}
|
||||
}, [isOpen, user]);
|
||||
|
||||
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";
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="modal settings-modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 640, width: "90%" }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
|
||||
<h2 className="modal-title" style={{ marginBottom: 0 }}>⚙️ {t("settingsModalTitle") || "Settings"}</h2>
|
||||
<button className="icon-btn" onClick={onClose} aria-label="Close" type="button">✕</button>
|
||||
</div>
|
||||
|
||||
{/* Tab Bar */}
|
||||
<div className="settings-tab-bar" style={{ display: "flex", gap: 6, borderBottom: "1px solid var(--border)", marginBottom: 18, paddingBottom: 6 }}>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${activeTab === "profile" ? "btn-primary" : "btn-ghost"}`}
|
||||
onClick={() => setActiveTab("profile")}
|
||||
>
|
||||
👤 {t("profile") || "Profile"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${activeTab === "preferences" ? "btn-primary" : "btn-ghost"}`}
|
||||
onClick={() => setActiveTab("preferences")}
|
||||
>
|
||||
⚙️ {t("preferences") || "Preferences"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${activeTab === "sync" ? "btn-primary" : "btn-ghost"}`}
|
||||
onClick={() => setActiveTab("sync")}
|
||||
>
|
||||
📱 {t("syncIntegrations") || "Integrations (DAVx⁵)"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${activeTab === "admin" ? "btn-primary" : "btn-ghost"}`}
|
||||
onClick={() => setActiveTab("admin")}
|
||||
>
|
||||
👑 {t("admin") || "Admin"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 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 (Trash Retention, Theme, Language) */}
|
||||
{activeTab === "preferences" && (
|
||||
<div className="settings-tab-content">
|
||||
{/* Trash Retention Days */}
|
||||
<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>
|
||||
|
||||
{/* Language Selection */}
|
||||
<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)}>
|
||||
<option value="en">English (Default)</option>
|
||||
<option value="ko">한국어 (Korean)</option>
|
||||
<option value="ja">日本語 (Japanese)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<div className="form-group">
|
||||
<label className="form-label">{t("theme")}</label>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${theme === "system" ? "btn-primary" : "btn-ghost"}`}
|
||||
onClick={() => { if (theme !== "system") toggleTheme(); }}
|
||||
>
|
||||
💻 {t("themeSystem")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${theme === "light" ? "btn-primary" : "btn-ghost"}`}
|
||||
onClick={() => { if (theme !== "light") toggleTheme(); }}
|
||||
>
|
||||
☀️ {t("themeLight")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${theme === "dark" ? "btn-primary" : "btn-ghost"}`}
|
||||
onClick={() => { if (theme !== "dark") toggleTheme(); }}
|
||||
>
|
||||
🌙 {t("themeDark")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab 3: CalDAV / CardDAV Integrations */}
|
||||
{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.
|
||||
</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);
|
||||
alert("Copied to clipboard!");
|
||||
}}
|
||||
>
|
||||
📋 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>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="modal-footer" style={{ marginTop: 20, 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user