forked from baron/baron-sso
i18n refresh and frontend fixes
This commit is contained in:
@@ -1,123 +1,131 @@
|
||||
import { BadgeCheck, Moon, ShieldHalf, Sun } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NavLink, Outlet } from "react-router-dom";
|
||||
import { t } from "../../lib/i18n";
|
||||
import { Toaster } from "../ui/toaster";
|
||||
|
||||
const navItems = [{ label: "Clients", to: "/clients", icon: ShieldHalf }];
|
||||
const navItems = [
|
||||
{
|
||||
labelKey: "ui.dev.nav.clients",
|
||||
labelFallback: "Clients",
|
||||
to: "/clients",
|
||||
icon: ShieldHalf,
|
||||
},
|
||||
];
|
||||
|
||||
function AppLayout() {
|
||||
const [theme, setTheme] = useState<"light" | "dark">(() => {
|
||||
const stored = window.localStorage.getItem("admin_theme");
|
||||
return stored === "dark" ? "dark" : "light";
|
||||
});
|
||||
const [theme, setTheme] = useState<"light" | "dark">(() => {
|
||||
const stored = window.localStorage.getItem("admin_theme");
|
||||
return stored === "dark" ? "dark" : "light";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.classList.remove("light", "dark");
|
||||
if (theme === "light") {
|
||||
root.classList.add("light");
|
||||
} else {
|
||||
root.classList.add("dark");
|
||||
}
|
||||
window.localStorage.setItem("admin_theme", theme);
|
||||
}, [theme]);
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.classList.remove("light", "dark");
|
||||
if (theme === "light") {
|
||||
root.classList.add("light");
|
||||
} else {
|
||||
root.classList.add("dark");
|
||||
}
|
||||
window.localStorage.setItem("admin_theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme((prev) => (prev === "light" ? "dark" : "light"));
|
||||
};
|
||||
const toggleTheme = () => {
|
||||
setTheme((prev) => (prev === "light" ? "dark" : "light"));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid min-h-screen bg-background text-foreground md:grid-cols-[240px,1fr]">
|
||||
<aside className="border-b border-border bg-card md:sticky md:top-0 md:h-screen md:border-b-0 md:border-r md:bg-card md:backdrop-blur">
|
||||
<div className="flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6">
|
||||
<div className="flex items-center gap-3 md:flex-col md:items-start">
|
||||
<div className="grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]">
|
||||
<ShieldHalf size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
|
||||
Baron 로그인
|
||||
</p>
|
||||
<h1 className="text-lg font-semibold">
|
||||
Developer Console
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2">
|
||||
<BadgeCheck size={14} />
|
||||
Scoped to /dev
|
||||
</div>
|
||||
</div>
|
||||
<nav className="px-2 pb-4 md:px-3 md:pb-8">
|
||||
<div className="flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start">
|
||||
<span className="rounded-full border border-border px-3 py-1">
|
||||
Env: dev
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{navItems.map(({ label, to, icon: Icon }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
className={({ isActive }) =>
|
||||
[
|
||||
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]"
|
||||
: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
|
||||
].join(" ")
|
||||
}
|
||||
>
|
||||
<Icon size={18} />
|
||||
<span>{label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
<div className="hidden space-y-2 px-5 pb-6 text-xs text-[var(--color-muted)] md:block">
|
||||
<p>개발자 전용 콘솔입니다.</p>
|
||||
<p>
|
||||
클라이언트 애플리케이션 등록 및 관리를 수행할 수
|
||||
있습니다.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="relative">
|
||||
<header className="sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur">
|
||||
<div className="flex items-center justify-between px-5 py-4 md:px-8">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-xs uppercase tracking-[0.22em] text-muted-foreground">
|
||||
Dev Plane
|
||||
</p>
|
||||
<span className="text-lg font-semibold">
|
||||
Manage your applications
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20"
|
||||
aria-label="테마 전환"
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<Sun size={16} />
|
||||
) : (
|
||||
<Moon size={16} />
|
||||
)}
|
||||
{theme === "light" ? "Light" : "Dark"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="px-5 py-6 md:px-10 md:py-10">
|
||||
<Outlet />
|
||||
</main>
|
||||
return (
|
||||
<div className="grid min-h-screen bg-background text-foreground md:grid-cols-[240px,1fr]">
|
||||
<aside className="border-b border-border bg-card md:sticky md:top-0 md:h-screen md:border-b-0 md:border-r md:bg-card md:backdrop-blur">
|
||||
<div className="flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6">
|
||||
<div className="flex items-center gap-3 md:flex-col md:items-start">
|
||||
<div className="grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]">
|
||||
<ShieldHalf size={20} />
|
||||
</div>
|
||||
<Toaster />
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
|
||||
{t("ui.dev.brand", "Baron 로그인")}
|
||||
</p>
|
||||
<h1 className="text-lg font-semibold">
|
||||
{t("ui.dev.console_title", "Developer Console")}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2">
|
||||
<BadgeCheck size={14} />
|
||||
{t("ui.dev.scope_badge", "Scoped to /dev")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<nav className="px-2 pb-4 md:px-3 md:pb-8">
|
||||
<div className="flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start">
|
||||
<span className="rounded-full border border-border px-3 py-1">
|
||||
{t("ui.dev.env_badge", "Env: dev")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{navItems.map(({ labelKey, labelFallback, to, icon: Icon }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
className={({ isActive }) =>
|
||||
[
|
||||
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]"
|
||||
: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
|
||||
].join(" ")
|
||||
}
|
||||
>
|
||||
<Icon size={18} />
|
||||
<span>{t(labelKey, labelFallback)}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
<div className="hidden space-y-2 px-5 pb-6 text-xs text-[var(--color-muted)] md:block">
|
||||
<p>{t("msg.dev.sidebar.notice", "개발자 전용 콘솔입니다.")}</p>
|
||||
<p>
|
||||
{t(
|
||||
"msg.dev.sidebar.notice_detail",
|
||||
"클라이언트 애플리케이션 등록 및 관리를 수행할 수 있습니다.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="relative">
|
||||
<header className="sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur">
|
||||
<div className="flex items-center justify-between px-5 py-4 md:px-8">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-xs uppercase tracking-[0.22em] text-muted-foreground">
|
||||
{t("ui.dev.header.plane", "Dev Plane")}
|
||||
</p>
|
||||
<span className="text-lg font-semibold">
|
||||
{t("ui.dev.header.subtitle", "Manage your applications")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20"
|
||||
aria-label={t("ui.common.theme_toggle", "테마 전환")}
|
||||
>
|
||||
{theme === "light" ? <Sun size={16} /> : <Moon size={16} />}
|
||||
{theme === "light"
|
||||
? t("ui.common.theme_light", "Light")
|
||||
: t("ui.common.theme_dark", "Dark")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="px-5 py-6 md:px-10 md:py-10">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppLayout;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { Button, type ButtonProps } from "./button";
|
||||
import * as React from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Button, type ButtonProps } from "./button";
|
||||
|
||||
interface CopyButtonProps extends ButtonProps {
|
||||
value: string;
|
||||
@@ -40,10 +40,10 @@ export function CopyButton({
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (!successful) throw new Error('execCommand copy failed');
|
||||
const successful = document.execCommand("copy");
|
||||
if (!successful) throw new Error("execCommand copy failed");
|
||||
} catch (err) {
|
||||
console.error('Fallback: Oops, unable to copy', err);
|
||||
console.error("Fallback: Oops, unable to copy", err);
|
||||
throw err;
|
||||
} finally {
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { useToastState } from "./use-toast";
|
||||
import { CheckCircle2, AlertCircle, Info, X } from "lucide-react";
|
||||
import { AlertCircle, CheckCircle2, Info } from "lucide-react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { useToastState } from "./use-toast";
|
||||
|
||||
export function Toaster() {
|
||||
const toasts = useToastState();
|
||||
@@ -15,12 +14,17 @@ export function Toaster() {
|
||||
key={t.id}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg border p-4 shadow-lg animate-in slide-in-from-right-full duration-300",
|
||||
t.type === "success" && "bg-emerald-50 border-emerald-200 text-emerald-800 dark:bg-emerald-950 dark:border-emerald-800 dark:text-emerald-200",
|
||||
t.type === "error" && "bg-rose-50 border-rose-200 text-rose-800 dark:bg-rose-950 dark:border-rose-800 dark:text-rose-200",
|
||||
t.type === "info" && "bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-950 dark:border-blue-800 dark:text-blue-200"
|
||||
t.type === "success" &&
|
||||
"bg-emerald-50 border-emerald-200 text-emerald-800 dark:bg-emerald-950 dark:border-emerald-800 dark:text-emerald-200",
|
||||
t.type === "error" &&
|
||||
"bg-rose-50 border-rose-200 text-rose-800 dark:bg-rose-950 dark:border-rose-800 dark:text-rose-200",
|
||||
t.type === "info" &&
|
||||
"bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-950 dark:border-blue-800 dark:text-blue-200",
|
||||
)}
|
||||
>
|
||||
{t.type === "success" && <CheckCircle2 className="h-5 w-5 shrink-0" />}
|
||||
{t.type === "success" && (
|
||||
<CheckCircle2 className="h-5 w-5 shrink-0" />
|
||||
)}
|
||||
{t.type === "error" && <AlertCircle className="h-5 w-5 shrink-0" />}
|
||||
{t.type === "info" && <Info className="h-5 w-5 shrink-0" />}
|
||||
<p className="text-sm font-medium leading-none">{t.message}</p>
|
||||
|
||||
@@ -39,4 +39,4 @@ export const useToastState = () => {
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user