Feature: Add i18n (en/ko/ja), 3-way theme (system/light/dark), and local demo preview mode
This commit is contained in:
@@ -1,19 +1,16 @@
|
||||
"use client";
|
||||
import { useState, useCallback } from "react";
|
||||
"use client";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { TaskList } from "../tasks/TaskList";
|
||||
import { TaskList, Task, List, User } from "../tasks/TaskList";
|
||||
import { TaskDetail } from "../tasks/TaskDetail";
|
||||
import { getDemoStore, saveDemoStore, MockList, MockTask } from "@/lib/mockData";
|
||||
|
||||
interface User { id: string; name?: string | null; email?: string | null; }
|
||||
interface List { id: string; name: string; color: string; icon: string; _count?: { tasks: number } }
|
||||
interface Task {
|
||||
id: string; listId: string; parentId: string | null; title: string; note: string | null;
|
||||
completed: boolean; completedAt: string | null; dueDate: string | null; priority: number;
|
||||
sortOrder: number; createdAt: string; updatedAt: string;
|
||||
children: Task[]; tags: { tag: { id: string; name: string; color: string } }[];
|
||||
interface AppShellProps {
|
||||
user: User;
|
||||
isDemo?: boolean;
|
||||
}
|
||||
|
||||
export function AppShell({ user }: { user: User }) {
|
||||
export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
const [lists, setLists] = useState<List[]>([]);
|
||||
const [selectedListId, setSelectedListId] = useState<string | null>(null);
|
||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
||||
@@ -22,6 +19,28 @@ export function AppShell({ user }: { user: User }) {
|
||||
const [showCompleted, setShowCompleted] = useState(false);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
// Demo store loading
|
||||
useEffect(() => {
|
||||
if (isDemo) {
|
||||
const store = getDemoStore();
|
||||
setLists(store.lists);
|
||||
if (store.lists.length > 0) {
|
||||
setSelectedListId(store.lists[0].id);
|
||||
}
|
||||
}
|
||||
}, [isDemo]);
|
||||
|
||||
// Demo tasks filter
|
||||
useEffect(() => {
|
||||
if (isDemo && selectedListId) {
|
||||
const store = getDemoStore();
|
||||
const listTasks = (store.tasks as Task[]).filter(
|
||||
(t) => t.listId === selectedListId && (showCompleted ? true : !t.completed)
|
||||
);
|
||||
setTasks(listTasks);
|
||||
}
|
||||
}, [isDemo, selectedListId, showCompleted, refreshKey]);
|
||||
|
||||
const refresh = useCallback(() => setRefreshKey((k) => k + 1), []);
|
||||
|
||||
const handleTaskSelect = useCallback((task: Task | null) => {
|
||||
@@ -39,6 +58,89 @@ export function AppShell({ user }: { user: User }) {
|
||||
setSidebarOpen(false);
|
||||
}, []);
|
||||
|
||||
// Demo Handlers
|
||||
const handleDemoCreateList = (name: string, color: string) => {
|
||||
const newList: MockList = {
|
||||
id: "demo-list-" + Date.now(),
|
||||
name,
|
||||
color,
|
||||
icon: "list",
|
||||
};
|
||||
const store = getDemoStore();
|
||||
const newLists = [...store.lists, newList];
|
||||
saveDemoStore(newLists, store.tasks);
|
||||
setLists(newLists);
|
||||
setSelectedListId(newList.id);
|
||||
};
|
||||
|
||||
const handleDemoDeleteList = (id: string) => {
|
||||
const store = getDemoStore();
|
||||
const newLists = store.lists.filter((l) => l.id !== id);
|
||||
const newTasks = store.tasks.filter((t) => t.listId !== id);
|
||||
saveDemoStore(newLists, newTasks);
|
||||
setLists(newLists);
|
||||
if (selectedListId === id) {
|
||||
setSelectedListId(newLists[0]?.id || null);
|
||||
}
|
||||
refresh();
|
||||
};
|
||||
|
||||
const handleDemoAddTask = (title: string, listId: string) => {
|
||||
const newTask: Task = {
|
||||
id: "demo-task-" + Date.now(),
|
||||
listId,
|
||||
parentId: null,
|
||||
title,
|
||||
note: null,
|
||||
completed: false,
|
||||
completedAt: null,
|
||||
dueDate: null,
|
||||
priority: 0,
|
||||
sortOrder: tasks.length,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
children: [],
|
||||
tags: [],
|
||||
};
|
||||
const store = getDemoStore();
|
||||
const newTasks = [...store.tasks, newTask as MockTask];
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks((prev) => [...prev, newTask]);
|
||||
refresh();
|
||||
};
|
||||
|
||||
const handleDemoToggleTask = (id: string, completed: boolean) => {
|
||||
const store = getDemoStore();
|
||||
const newTasks = store.tasks.map((t) => {
|
||||
if (t.id === id) {
|
||||
return { ...t, completed, completedAt: completed ? new Date().toISOString() : null };
|
||||
}
|
||||
return {
|
||||
...t,
|
||||
children: t.children?.map((c) => (c.id === id ? { ...c, completed } : c)) || [],
|
||||
};
|
||||
});
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
refresh();
|
||||
};
|
||||
|
||||
const handleDemoUpdateTask = (updated: Task) => {
|
||||
const store = getDemoStore();
|
||||
const newTasks = store.tasks.map((t) => (t.id === updated.id ? (updated as MockTask) : t));
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks((prev) => prev.map((t) => (t.id === updated.id ? updated : t)));
|
||||
setSelectedTask(updated);
|
||||
};
|
||||
|
||||
const handleDemoDeleteTask = (id: string) => {
|
||||
const store = getDemoStore();
|
||||
const newTasks = store.tasks.filter((t) => t.id !== id);
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks((prev) => prev.filter((t) => t.id !== id));
|
||||
setSelectedTask(null);
|
||||
refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-layout">
|
||||
{/* Mobile overlay */}
|
||||
@@ -58,6 +160,9 @@ export function AppShell({ user }: { user: User }) {
|
||||
onListSelect={handleListSelect}
|
||||
mobileOpen={sidebarOpen}
|
||||
onClose={() => setSidebarOpen(false)}
|
||||
isDemo={isDemo}
|
||||
onDemoCreateList={handleDemoCreateList}
|
||||
onDemoDeleteList={handleDemoDeleteList}
|
||||
/>
|
||||
|
||||
<div className="main-content">
|
||||
@@ -74,6 +179,9 @@ export function AppShell({ user }: { user: User }) {
|
||||
onToggleCompleted={() => setShowCompleted((p) => !p)}
|
||||
onMenuOpen={() => setSidebarOpen(true)}
|
||||
onRefresh={refresh}
|
||||
isDemo={isDemo}
|
||||
onDemoAddTask={handleDemoAddTask}
|
||||
onDemoToggleTask={handleDemoToggleTask}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -82,8 +190,14 @@ export function AppShell({ user }: { user: User }) {
|
||||
task={selectedTask}
|
||||
onClose={() => setSelectedTask(null)}
|
||||
onUpdate={handleTaskUpdate}
|
||||
onDelete={() => { setSelectedTask(null); refresh(); }}
|
||||
onDelete={() => {
|
||||
setSelectedTask(null);
|
||||
refresh();
|
||||
}}
|
||||
listId={selectedTask.listId}
|
||||
isDemo={isDemo}
|
||||
onDemoUpdateTask={handleDemoUpdateTask}
|
||||
onDemoDeleteTask={handleDemoDeleteTask}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -93,7 +207,6 @@ export function AppShell({ user }: { user: User }) {
|
||||
id="mobile-add-task"
|
||||
aria-label="Add task"
|
||||
onClick={() => {
|
||||
// trigger add task from task list — use custom event
|
||||
document.dispatchEvent(new CustomEvent("checkflow:addTask"));
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user