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:
2026-08-20 22:51:43 +09:00
parent fc90ca7d0b
commit 36c8d690d9
12 changed files with 1820 additions and 273 deletions
+258
View File
@@ -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>
);
}