Checkpoint: Initial stable CheckFlow base before i18n and demo mode

This commit is contained in:
Wonhee Han
2026-08-20 14:01:12 +09:00
parent ed076cf5ab
commit dbfaa0fcb2
42 changed files with 4727 additions and 167 deletions
+41
View File
@@ -0,0 +1,41 @@
import { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { prisma } from "@/lib/prisma";
import bcrypt from "bcryptjs";
export const authOptions: NextAuthOptions = {
session: { strategy: "jwt" },
pages: {
signIn: "/login",
newUser: "/register",
},
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null;
const user = await prisma.user.findUnique({
where: { email: credentials.email },
});
if (!user) return null;
const valid = await bcrypt.compare(credentials.password, user.passwordHash);
if (!valid) return null;
return { id: user.id, email: user.email, name: user.name };
},
}),
],
callbacks: {
jwt({ token, user }) {
if (user) token.id = user.id;
return token;
},
session({ session, token }) {
if (session.user) session.user.id = token.id as string;
return session;
},
},
};
+288
View File
@@ -0,0 +1,288 @@
"use client";
import React, { createContext, useContext, useState, useEffect } from "react";
export type Locale = "en" | "ko" | "ja";
// ============================================================
// TRANSLATIONS
// ============================================================
const translations = {
en: {
auth: {
welcome: "Welcome back",
signIn: "Sign in",
signInSub: "Sign in to your account",
createAccount: "Create account",
createAccountSub: "Start organizing your tasks today",
email: "Email",
password: "Password",
passwordHint: "Min. 8 characters",
displayName: "Display Name",
namePlaceholder: "Your name",
noAccount: "Don't have an account?",
hasAccount: "Already have an account?",
createOne: "Create one",
signInLink: "Sign in",
signingIn: "Signing in...",
creating: "Creating account...",
invalidCredentials: "Invalid email or password.",
registrationFailed: "Registration failed.",
},
sidebar: {
lists: "Lists",
newList: "New List",
create: "Create",
cancel: "Cancel",
listName: "List name",
importTasks: "Import Tasks",
signOut: "Sign out",
deleteListConfirm: "Delete this list and all its tasks?",
},
tasks: {
addTask: "Add a task...",
add: "Add",
showDone: "Show done",
hideDone: "Hide done",
noTasks: "No tasks yet. Add one below!",
selectList: "Select a list to get started",
completed: "Completed",
loading: "Loading...",
},
detail: {
priority: "Priority",
dueDate: "Due Date",
notes: "Notes",
notesPlaceholder: "Add notes, details, or anything you need to remember\u2026\n\nMarkdown: **bold**, *italic*, # heading, - list, - [ ] checkbox",
subtasks: "Sub-tasks",
addSubtask: "Add sub-task\u2026",
autoSaved: "Auto-saved",
saving: "Saving\u2026",
delete: "Delete task",
deleteConfirm: "Delete this task?",
close: "Close",
clear: "Clear",
created: "Created",
},
priority: { none: "None", low: "Low", medium: "Medium", high: "High" },
settings: {
theme: "Theme",
language: "Language",
system: "System",
light: "Light",
dark: "Dark",
},
import: {
title: "Import Tasks",
targetList: "Target List",
file: "File (CSV or ICS)",
tip: "TickTick: Settings \u2192 Export \u2192 Export as CSV or iCalendar",
cancel: "Cancel",
import: "Import",
importing: "Importing...",
error: "Error",
},
list: { create: "Create list", color: "Color" },
},
ko: {
auth: {
welcome: "다시 오셨군요",
signIn: "로그인",
signInSub: "계정에 로그인하세요",
createAccount: "계정 만들기",
createAccountSub: "오늘부터 할 일을 정리해보세요",
email: "이메일",
password: "비밀번호",
passwordHint: "최소 8자",
displayName: "이름",
namePlaceholder: "표시될 이름",
noAccount: "계정이 없으신가요?",
hasAccount: "이미 계정이 있으신가요?",
createOne: "만들기",
signInLink: "로그인",
signingIn: "로그인 중...",
creating: "계정 생성 중...",
invalidCredentials: "이메일 또는 비밀번호가 올바르지 않습니다.",
registrationFailed: "회원가입에 실패했습니다.",
},
sidebar: {
lists: "목록",
newList: "새 목록",
create: "만들기",
cancel: "취소",
listName: "목록 이름",
importTasks: "가져오기",
signOut: "로그아웃",
deleteListConfirm: "이 목록과 모든 할 일을 삭제할까요?",
},
tasks: {
addTask: "할 일 추가...",
add: "추가",
showDone: "완료 표시",
hideDone: "완료 숨기기",
noTasks: "할 일이 없습니다. 아래에서 추가하세요!",
selectList: "목록을 선택하여 시작하세요",
completed: "완료됨",
loading: "불러오는 중...",
},
detail: {
priority: "우선순위",
dueDate: "마감일",
notes: "메모",
notesPlaceholder: "메모, 세부 정보, 기억해야 할 내용을 추가하세요\u2026\n\nMarkdown: **굵게**, *기울임*, # 제목, - 목록, - [ ] 체크박스",
subtasks: "하위 할 일",
addSubtask: "하위 항목 추가\u2026",
autoSaved: "자동 저장됨",
saving: "저장 중\u2026",
delete: "삭제",
deleteConfirm: "이 할 일을 삭제할까요?",
close: "닫기",
clear: "초기화",
created: "생성일",
},
priority: { none: "없음", low: "낮음", medium: "보통", high: "높음" },
settings: {
theme: "테마",
language: "언어",
system: "시스템",
light: "라이트",
dark: "다크",
},
import: {
title: "할 일 가져오기",
targetList: "대상 목록",
file: "파일 (CSV 또는 ICS)",
tip: "TickTick: 설정 \u2192 내보내기 \u2192 CSV 또는 iCalendar로 내보내기",
cancel: "취소",
import: "가져오기",
importing: "가져오는 중...",
error: "오류",
},
list: { create: "목록 만들기", color: "색상" },
},
ja: {
auth: {
welcome: "おかえりなさい",
signIn: "サインイン",
signInSub: "アカウントにサインイン",
createAccount: "アカウント作成",
createAccountSub: "今日からタスクを整理しましょう",
email: "メールアドレス",
password: "パスワード",
passwordHint: "8文字以上",
displayName: "表示名",
namePlaceholder: "あなたの名前",
noAccount: "アカウントをお持ちでない方は",
hasAccount: "すでにアカウントをお持ちですか?",
createOne: "作成する",
signInLink: "サインイン",
signingIn: "サインイン中...",
creating: "アカウント作成中...",
invalidCredentials: "メールアドレスまたはパスワードが正しくありません。",
registrationFailed: "登録に失敗しました。",
},
sidebar: {
lists: "リスト",
newList: "新しいリスト",
create: "作成",
cancel: "キャンセル",
listName: "リスト名",
importTasks: "インポート",
signOut: "サインアウト",
deleteListConfirm: "このリストとすべてのタスクを削除しますか?",
},
tasks: {
addTask: "タスクを追加...",
add: "追加",
showDone: "完了を表示",
hideDone: "完了を非表示",
noTasks: "タスクがありません。下から追加してください!",
selectList: "リストを選択して始めましょう",
completed: "完了済み",
loading: "読み込み中...",
},
detail: {
priority: "優先度",
dueDate: "期限",
notes: "メモ",
notesPlaceholder: "メモ、詳細、覚えておくべきことを追加\u2026\n\nMarkdown: **太字**, *斜体*, # 見出し, - リスト, - [ ] チェックボックス",
subtasks: "サブタスク",
addSubtask: "サブタスクを追加\u2026",
autoSaved: "自動保存済み",
saving: "保存中\u2026",
delete: "削除",
deleteConfirm: "このタスクを削除しますか?",
close: "閉じる",
clear: "クリア",
created: "作成日",
},
priority: { none: "なし", low: "低", medium: "中", high: "高" },
settings: {
theme: "テーマ",
language: "言語",
system: "システム",
light: "ライト",
dark: "ダーク",
},
import: {
title: "タスクをインポート",
targetList: "対象リスト",
file: "ファイル (CSV または ICS)",
tip: "TickTick: 設定 \u2192 エクスポート \u2192 CSVまたはiCalendarでエクスポート",
cancel: "キャンセル",
import: "インポート",
importing: "インポート中...",
error: "エラー",
},
list: { create: "リスト作成", color: "カラー" },
},
} as const;
export type Translations = typeof translations.en;
export type TranslationKey = keyof Translations;
// ============================================================
// CONTEXT
// ============================================================
interface I18nContextValue {
locale: Locale;
setLocale: (l: Locale) => void;
t: Translations;
}
const I18nContext = createContext<I18nContextValue>({
locale: "en",
setLocale: () => {},
t: translations.en,
});
export function I18nProvider({ children }: { children: React.ReactNode }) {
const [locale, setLocaleState] = useState<Locale>("en");
useEffect(() => {
const saved = localStorage.getItem("locale") as Locale | null;
if (saved && saved in translations) setLocaleState(saved);
}, []);
const setLocale = (l: Locale) => {
setLocaleState(l);
localStorage.setItem("locale", l);
};
return (
<I18nContext.Provider value={{ locale, setLocale, t: translations[locale] }}>
{children}
</I18nContext.Provider>
);
}
export function useI18n() {
return useContext(I18nContext);
}
export const LOCALES: { value: Locale; label: string; flag: string }[] = [
{ value: "en", label: "English", flag: "🇺🇸" },
{ value: "ko", label: "한국어", flag: "🇰🇷" },
{ value: "ja", label: "日本語", flag: "🇯🇵" },
];
+13
View File
@@ -0,0 +1,13 @@
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;