Feature: Add i18n (en/ko/ja), 3-way theme (system/light/dark), and local demo preview mode

This commit is contained in:
2026-08-20 14:07:42 +09:00
parent 3e56120b00
commit 76f33de773
13 changed files with 1611 additions and 655 deletions
+126 -13
View File
@@ -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"));
}}
>