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

This commit is contained in:
Wonhee Han
2026-08-20 14:07:42 +09:00
parent dbfaa0fcb2
commit 3710c313e9
13 changed files with 1611 additions and 655 deletions
+118 -31
View File
@@ -1,11 +1,16 @@
"use client";
"use client";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { useI18n, Language } from "@/lib/i18n";
import { useTheme } from "@/app/providers";
export default function RegisterPage() {
const router = useRouter();
const { t, lang, setLang } = useI18n();
const { theme, toggleTheme } = useTheme();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
@@ -17,41 +22,123 @@ export default function RegisterPage() {
setError("");
setLoading(true);
const res = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email, password }),
});
try {
const res = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email, password }),
});
if (!res.ok) {
let msg = "Registration failed.";
try {
const data = await res.json();
msg = data.error || msg;
} catch {
// non-JSON response (e.g. 500 HTML page)
if (!res.ok) {
let msg = "Registration failed.";
try {
const data = await res.json();
msg = data.error || msg;
} catch {
msg = "Database connection failed. Please ensure PostgreSQL is running or use Demo Mode.";
}
setError(msg);
setLoading(false);
return;
}
setError(msg);
setLoading(false);
return;
}
await signIn("credentials", { email, password, redirect: false });
router.push("/");
await signIn("credentials", { email, password, redirect: false });
router.push("/");
} catch {
setError("Network or server error. Check database status or try Demo Mode.");
setLoading(false);
}
};
return (
<div className="auth-page">
<div className="auth-card">
<div className="auth-logo">
<div className="auth-logo-icon"></div>
<span className="auth-logo-name">CheckFlow</span>
{/* Top bar: Lang & Theme switcher */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 20 }}>
<div className="auth-logo" style={{ marginBottom: 0 }}>
<div className="auth-logo-icon"></div>
<span className="auth-logo-name">{t("appName")}</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<select
aria-label={t("language")}
value={lang}
onChange={(e) => setLang(e.target.value as Language)}
style={{
background: "var(--bg-secondary)",
color: "var(--text-secondary)",
border: "1px solid var(--border)",
borderRadius: "var(--radius-xs)",
fontSize: 11,
padding: "3px 6px",
cursor: "pointer",
fontWeight: 600,
}}
>
<option value="en">EN</option>
<option value="ko"></option>
<option value="ja"></option>
</select>
<button
className="icon-btn"
onClick={toggleTheme}
title={`${t("theme")}: ${theme}`}
style={{ width: 28, height: 28 }}
>
{theme === "system" ? (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" /><line x1="8" y1="21" x2="16" y2="21" /><line x1="12" y1="17" x2="12" y2="21" />
</svg>
) : theme === "light" ? (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="5" /><line x1="12" y1="1" x2="12" y2="3" /><line x1="12" y1="21" x2="12" y2="23" />
</svg>
) : (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
)}
</button>
</div>
</div>
<h1 className="auth-title">Create account</h1>
<p className="auth-subtitle">Start organizing your tasks today</p>
<h1 className="auth-title">{t("createAccount")}</h1>
<p className="auth-subtitle">{t("createAccountSubtitle")}</p>
{/* Demo Mode Action Banner */}
<div
style={{
background: "var(--accent-light)",
border: "1px dashed var(--accent)",
borderRadius: "var(--radius-md)",
padding: "12px 14px",
marginBottom: 20,
display: "flex",
flexDirection: "column",
gap: 6,
}}
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<span style={{ fontSize: 13, fontWeight: 700, color: "var(--accent)" }}> {t("demoBadge")}</span>
<span style={{ fontSize: 11, color: "var(--text-tertiary)" }}>No PostgreSQL needed</span>
</div>
<p style={{ fontSize: 12, color: "var(--text-secondary)", lineHeight: 1.4 }}>
Explore full 3-panel UI, sub-tasks, and markdown notes directly in browser storage.
</p>
<Link
href="/demo"
className="btn btn-primary btn-sm"
style={{ marginTop: 4, width: "100%", textAlign: "center" }}
id="try-demo-btn"
>
🚀 {t("tryDemoMode")}
</Link>
</div>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label className="form-label">Display Name</label>
<label className="form-label">{t("displayName")}</label>
<input
id="name"
type="text"
@@ -64,7 +151,7 @@ export default function RegisterPage() {
/>
</div>
<div className="form-group">
<label className="form-label">Email</label>
<label className="form-label">{t("email")}</label>
<input
id="reg-email"
type="email"
@@ -76,12 +163,12 @@ export default function RegisterPage() {
/>
</div>
<div className="form-group">
<label className="form-label">Password</label>
<label className="form-label">{t("password")}</label>
<input
id="reg-password"
type="password"
className="form-input"
placeholder="Min. 8 characters"
placeholder={t("min8Chars")}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
@@ -90,12 +177,12 @@ export default function RegisterPage() {
</div>
{error && <p className="form-error" style={{ marginBottom: "12px" }}>{error}</p>}
<button id="register-btn" type="submit" className="btn btn-primary w-full" disabled={loading} style={{ height: "44px" }}>
{loading ? "Creating account..." : "Create account"}
{loading ? t("creatingBtn") : t("createBtn")}
</button>
</form>
<p className="auth-footer">
Already have an account?{" "}
<Link href="/login" className="auth-link">Sign in</Link>
{t("alreadyHaveAccount")}{" "}
<Link href="/login" className="auth-link">{t("signIn")}</Link>
</p>
</div>
</div>