@naeil/ui 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/DESIGN.md +186 -0
- package/LICENSE +21 -0
- package/README.md +85 -22
- package/THIRD_PARTY_NOTICES.md +49 -0
- package/dist/chunk-JBTDRVZ7.js +46 -0
- package/dist/chunk-QKVN6Q6C.js +438 -0
- package/dist/components/index.d.ts +87 -0
- package/dist/components/index.js +832 -0
- package/dist/components/ui/index.d.ts +105 -0
- package/dist/components/ui/index.js +736 -0
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +7 -0
- package/dist/source/components/footer.d.ts +15 -0
- package/dist/source/components/footer.js +172 -0
- package/dist/source/components/index.d.ts +11 -0
- package/dist/source/components/index.js +26 -0
- package/dist/source/components/locale-switcher.d.ts +17 -0
- package/dist/source/components/locale-switcher.js +66 -0
- package/dist/source/components/logo.d.ts +9 -0
- package/dist/source/components/logo.js +52 -0
- package/dist/source/components/nav.d.ts +39 -0
- package/dist/source/components/nav.js +412 -0
- package/dist/source/components/theme-provider.d.ts +6 -0
- package/dist/source/components/theme-provider.js +45 -0
- package/dist/source/components/theme-toggle-icon.d.ts +5 -0
- package/dist/source/components/theme-toggle-icon.js +62 -0
- package/dist/source/components/theme-toggle.d.ts +5 -0
- package/dist/source/components/theme-toggle.js +34 -0
- package/dist/source/components/typography.d.ts +12 -0
- package/dist/source/components/typography.js +49 -0
- package/dist/source/components/ui/avatar.d.ts +14 -0
- package/dist/source/components/ui/avatar.js +137 -0
- package/dist/source/components/ui/badge.d.ts +13 -0
- package/dist/source/components/ui/badge.js +76 -0
- package/dist/source/components/ui/button.d.ts +14 -0
- package/dist/source/components/ui/button.js +88 -0
- package/dist/source/components/ui/card.d.ts +13 -0
- package/dist/source/components/ui/card.js +135 -0
- package/dist/source/components/ui/checkbox.d.ts +7 -0
- package/dist/source/components/ui/checkbox.js +58 -0
- package/dist/source/components/ui/dialog.d.ts +20 -0
- package/dist/source/components/ui/dialog.js +182 -0
- package/dist/source/components/ui/dropdown-menu.d.ts +28 -0
- package/dist/source/components/ui/dropdown-menu.js +282 -0
- package/dist/source/components/ui/index.d.ts +19 -0
- package/dist/source/components/ui/index.js +131 -0
- package/dist/source/components/ui/input.d.ts +6 -0
- package/dist/source/components/ui/input.js +49 -0
- package/dist/source/components/ui/select.d.ts +16 -0
- package/dist/source/components/ui/select.js +197 -0
- package/dist/source/components/ui/sonner.d.ts +6 -0
- package/dist/source/components/ui/sonner.js +80 -0
- package/dist/source/components/ui/switch.d.ts +7 -0
- package/dist/source/components/ui/switch.js +54 -0
- package/dist/source/components/ui/tabs.d.ts +10 -0
- package/dist/source/components/ui/tabs.js +57 -0
- package/dist/source/components/ui/textarea.d.ts +6 -0
- package/dist/source/components/ui/textarea.js +46 -0
- package/dist/source/i18n/config.d.ts +6 -0
- package/dist/source/i18n/config.js +12 -0
- package/dist/source/i18n/routing.d.ts +371 -0
- package/dist/source/i18n/routing.js +17 -0
- package/dist/source/lib/design/modal-inert.d.ts +6 -0
- package/dist/source/lib/design/modal-inert.js +73 -0
- package/dist/source/lib/utils.d.ts +5 -0
- package/dist/source/lib/utils.js +8 -0
- package/dist/tokens.ts +217 -0
- package/docs/components/README.md +21 -0
- package/docs/components/avatar.md +47 -0
- package/docs/components/badge.md +29 -0
- package/docs/components/button.md +39 -0
- package/docs/components/card.md +32 -0
- package/docs/components/checkbox.md +35 -0
- package/docs/components/common.md +31 -0
- package/docs/components/composition.md +68 -0
- package/docs/components/dialog.md +41 -0
- package/docs/components/dropdown-menu.md +38 -0
- package/docs/components/input.md +32 -0
- package/docs/components/select.md +39 -0
- package/docs/components/sonner.md +37 -0
- package/docs/components/switch.md +33 -0
- package/docs/components/tabs.md +40 -0
- package/docs/components/textarea.md +35 -0
- package/docs/design/component-demand.md +30 -0
- package/docs/design/public-ui-final-verification.md +96 -0
- package/docs/design/public-ui-release-validation.md +86 -0
- package/docs/design/public-ui-stage3-verification.md +98 -0
- package/docs/design/public-ui-stage4-verification.md +129 -0
- package/docs/design/public-ui-support.md +40 -0
- package/docs/design/v2-migration.md +199 -0
- package/docs/package-release.md +89 -0
- package/licenses/Noto-Sans-JP-OFL.txt +93 -0
- package/licenses/Nunito-Sans-OFL.txt +93 -0
- package/licenses/Pretendard-OFL.txt +94 -0
- package/licenses/React-MIT.txt +21 -0
- package/licenses/Storybook-MIT.txt +21 -0
- package/licenses/popper-MIT.txt +25 -0
- package/licenses/react-remove-scroll-bar-MIT.txt +21 -0
- package/licenses/store2-MIT.txt +22 -0
- package/licenses/toggle-selection-MIT.txt +21 -0
- package/package.json +215 -26
- package/src/styles/components.css +276 -0
- package/src/styles/globals.css +18 -0
- package/src/styles/theme.css +293 -0
- package/messages/en.json +0 -221
- package/messages/ja.json +0 -221
- package/messages/ko.json +0 -221
- package/postcss.config.mjs +0 -7
- package/src/app/[locale]/(content)/baby-agent/page.tsx +0 -30
- package/src/app/[locale]/(content)/blog/page.tsx +0 -68
- package/src/app/[locale]/(content)/cc/page.tsx +0 -216
- package/src/app/[locale]/(content)/design/page.tsx +0 -703
- package/src/app/[locale]/(content)/layout.tsx +0 -7
- package/src/app/[locale]/(content)/pkm/page.tsx +0 -31
- package/src/app/[locale]/(content)/projects/page.tsx +0 -100
- package/src/app/[locale]/(content)/sa/page.tsx +0 -179
- package/src/app/[locale]/(landing)/page.tsx +0 -157
- package/src/app/[locale]/layout.tsx +0 -72
- package/src/app/[locale]/login/actions.ts +0 -10
- package/src/app/[locale]/login/oauth-buttons.tsx +0 -78
- package/src/app/[locale]/login/page.tsx +0 -85
- package/src/app/auth/callback/route.ts +0 -78
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +0 -32
- package/src/app/layout.tsx +0 -22
- package/src/app/sitemap.ts +0 -26
- package/src/components/accent-picker.tsx +0 -62
- package/src/components/auth-slot.tsx +0 -96
- package/src/components/footer-wrapper.tsx +0 -8
- package/src/components/footer.tsx +0 -193
- package/src/components/hero-scene.tsx +0 -948
- package/src/components/hero-section.tsx +0 -57
- package/src/components/locale-switcher.tsx +0 -74
- package/src/components/logo.tsx +0 -51
- package/src/components/nav-server-wrapper.tsx +0 -22
- package/src/components/nav-wrapper.tsx +0 -45
- package/src/components/nav.tsx +0 -550
- package/src/components/paraglider-cursor.tsx +0 -112
- package/src/components/project-layout.tsx +0 -194
- package/src/components/theme-provider.tsx +0 -10
- package/src/components/theme-toggle-icon.tsx +0 -62
- package/src/components/theme-toggle.tsx +0 -39
- package/src/components/typography.tsx +0 -27
- package/src/components/ui/avatar.tsx +0 -109
- package/src/components/ui/badge.tsx +0 -47
- package/src/components/ui/button.tsx +0 -65
- package/src/components/ui/card.tsx +0 -106
- package/src/components/ui/dialog.tsx +0 -158
- package/src/components/ui/dropdown-menu.tsx +0 -257
- package/src/components/ui/input.tsx +0 -21
- package/src/components/ui/sonner.tsx +0 -54
- package/src/components/workflow-diagram.tsx +0 -414
- package/src/i18n/config.ts +0 -8
- package/src/i18n/request.ts +0 -16
- package/src/i18n/routing.ts +0 -12
- package/src/lib/supabase/client.ts +0 -18
- package/src/lib/supabase/middleware.ts +0 -42
- package/src/lib/supabase/server.ts +0 -37
- package/src/lib/utils.ts +0 -6
- package/src/proxy.ts +0 -62
- package/src/tokens/.gitkeep +0 -0
- package/src/tokens/color.json +0 -56
- package/src/tokens/radius.json +0 -9
- package/src/tokens/semantic.json +0 -81
- package/src/tokens/shadow.json +0 -7
- package/src/tokens/spacing.json +0 -16
- package/src/tokens/typography.json +0 -34
- package/src/types/i18n.d.ts +0 -6
- package/src/utils/.gitkeep +0 -0
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { getTranslations } from "next-intl/server";
|
|
2
|
-
import { redirect } from "@/i18n/routing";
|
|
3
|
-
import { createClient } from "@/lib/supabase/server";
|
|
4
|
-
import { OAuthButtons } from "./oauth-buttons";
|
|
5
|
-
|
|
6
|
-
export default async function LoginPage({
|
|
7
|
-
params,
|
|
8
|
-
searchParams,
|
|
9
|
-
}: {
|
|
10
|
-
params: Promise<{ locale: string }>;
|
|
11
|
-
searchParams: Promise<{ next?: string }>;
|
|
12
|
-
}) {
|
|
13
|
-
const { locale } = await params;
|
|
14
|
-
const { next } = await searchParams;
|
|
15
|
-
|
|
16
|
-
const supabase = await createClient();
|
|
17
|
-
const {
|
|
18
|
-
data: { user },
|
|
19
|
-
} = await supabase.auth.getUser();
|
|
20
|
-
|
|
21
|
-
if (user) {
|
|
22
|
-
redirect({ href: next || "/", locale });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const t = await getTranslations("auth");
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<div className="flex min-h-svh items-center justify-center px-4">
|
|
29
|
-
<div className="w-full max-w-sm space-y-8">
|
|
30
|
-
{/* Branding */}
|
|
31
|
-
<div className="text-center">
|
|
32
|
-
<div className="mb-4 flex justify-center">
|
|
33
|
-
<Logo size={40} />
|
|
34
|
-
</div>
|
|
35
|
-
<h1 className="text-foreground text-2xl font-semibold tracking-tight">
|
|
36
|
-
{t("login")}
|
|
37
|
-
</h1>
|
|
38
|
-
<p className="text-muted-foreground mt-2 text-sm">
|
|
39
|
-
{t("loginDescription")}
|
|
40
|
-
</p>
|
|
41
|
-
</div>
|
|
42
|
-
|
|
43
|
-
{/* OAuth buttons */}
|
|
44
|
-
<OAuthButtons next={next} />
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function Logo({ size = 40 }: { size?: number }) {
|
|
51
|
-
const id = "login-logo";
|
|
52
|
-
return (
|
|
53
|
-
<svg
|
|
54
|
-
width={size}
|
|
55
|
-
height={size}
|
|
56
|
-
viewBox="0 0 24 24"
|
|
57
|
-
fill="none"
|
|
58
|
-
aria-label="naeil"
|
|
59
|
-
>
|
|
60
|
-
<path
|
|
61
|
-
d="M6 4 L6 16 L20 16"
|
|
62
|
-
stroke="currentColor"
|
|
63
|
-
strokeWidth="2"
|
|
64
|
-
strokeLinecap="square"
|
|
65
|
-
strokeLinejoin="miter"
|
|
66
|
-
/>
|
|
67
|
-
<circle
|
|
68
|
-
cx="15"
|
|
69
|
-
cy="16"
|
|
70
|
-
r="3.5"
|
|
71
|
-
fill={`url(#${id}-grad)`}
|
|
72
|
-
mask={`url(#${id})`}
|
|
73
|
-
/>
|
|
74
|
-
<defs>
|
|
75
|
-
<linearGradient id={`${id}-grad`} x1="15" y1="12.5" x2="15" y2="16">
|
|
76
|
-
<stop stopColor="#eab308" />
|
|
77
|
-
<stop offset="1" stopColor="#dc2626" />
|
|
78
|
-
</linearGradient>
|
|
79
|
-
<mask id={id}>
|
|
80
|
-
<rect x="0" y="0" width="24" height="16" fill="white" />
|
|
81
|
-
</mask>
|
|
82
|
-
</defs>
|
|
83
|
-
</svg>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import { createServerClient } from "@supabase/ssr";
|
|
3
|
-
import { cookies, headers } from "next/headers";
|
|
4
|
-
|
|
5
|
-
function getCookieDomain(headerStore: Headers): string | undefined {
|
|
6
|
-
const host = headerStore.get("host") || "";
|
|
7
|
-
return host.endsWith(".naeil.dev") || host === "naeil.dev"
|
|
8
|
-
? ".naeil.dev"
|
|
9
|
-
: undefined;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function isAllowedRedirect(url: string): boolean {
|
|
13
|
-
if (url.startsWith("/")) return true;
|
|
14
|
-
try {
|
|
15
|
-
const parsed = new URL(url);
|
|
16
|
-
return (
|
|
17
|
-
parsed.hostname === "naeil.dev" ||
|
|
18
|
-
parsed.hostname.endsWith(".naeil.dev")
|
|
19
|
-
);
|
|
20
|
-
} catch {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function GET(request: Request) {
|
|
26
|
-
const { searchParams, origin } = new URL(request.url);
|
|
27
|
-
const code = searchParams.get("code");
|
|
28
|
-
|
|
29
|
-
if (code) {
|
|
30
|
-
const cookieStore = await cookies();
|
|
31
|
-
const headerStore = await headers();
|
|
32
|
-
const domain = getCookieDomain(headerStore);
|
|
33
|
-
|
|
34
|
-
const supabase = createServerClient(
|
|
35
|
-
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
36
|
-
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
37
|
-
{
|
|
38
|
-
cookies: {
|
|
39
|
-
getAll() {
|
|
40
|
-
return cookieStore.getAll();
|
|
41
|
-
},
|
|
42
|
-
setAll(cookiesToSet) {
|
|
43
|
-
try {
|
|
44
|
-
cookiesToSet.forEach(({ name, value, options }) =>
|
|
45
|
-
cookieStore.set(name, value, { ...options, domain }),
|
|
46
|
-
);
|
|
47
|
-
} catch {
|
|
48
|
-
// Ignore — cookies can only be set in Server Actions or Route Handlers
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const { error } = await supabase.auth.exchangeCodeForSession(code);
|
|
56
|
-
|
|
57
|
-
const redirectCookie = cookieStore.get("auth_redirect_to");
|
|
58
|
-
|
|
59
|
-
if (!error) {
|
|
60
|
-
// decodeURIComponent required: Next.js cookies.set() URL-encodes values,
|
|
61
|
-
// but cookies.get() returns them raw (still encoded)
|
|
62
|
-
const next = decodeURIComponent(redirectCookie?.value || "/");
|
|
63
|
-
|
|
64
|
-
// Clear the redirect cookie
|
|
65
|
-
cookieStore.set("auth_redirect_to", "", { path: "/", maxAge: 0, domain });
|
|
66
|
-
|
|
67
|
-
// Redirect to original page
|
|
68
|
-
if (isAllowedRedirect(next)) {
|
|
69
|
-
const redirectUrl = next.startsWith("http") ? next : `${origin}${next}`;
|
|
70
|
-
return NextResponse.redirect(redirectUrl);
|
|
71
|
-
}
|
|
72
|
-
return NextResponse.redirect(origin);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Auth error — redirect to login
|
|
77
|
-
return NextResponse.redirect(`${origin}/login`);
|
|
78
|
-
}
|
package/src/app/favicon.ico
DELETED
|
Binary file
|
package/src/app/globals.css
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
@import "tailwindcss";
|
|
2
|
-
@import "tw-animate-css";
|
|
3
|
-
@import "shadcn/tailwind.css";
|
|
4
|
-
@import "../../dist/theme.css";
|
|
5
|
-
|
|
6
|
-
@custom-variant dark (&:is(.dark *));
|
|
7
|
-
|
|
8
|
-
/* Register float animation as Tailwind utility */
|
|
9
|
-
@theme inline {
|
|
10
|
-
--animate-float: float 4s ease-in-out infinite;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
@layer base {
|
|
14
|
-
* {
|
|
15
|
-
@apply border-border outline-ring/50;
|
|
16
|
-
}
|
|
17
|
-
body {
|
|
18
|
-
@apply bg-background text-foreground font-sans;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/* Float keyframes */
|
|
23
|
-
@keyframes float {
|
|
24
|
-
0%, 100% { transform: translateY(0); }
|
|
25
|
-
50% { transform: translateY(-12px); }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
@media (prefers-reduced-motion: reduce) {
|
|
29
|
-
.animate-float {
|
|
30
|
-
animation: none;
|
|
31
|
-
}
|
|
32
|
-
}
|
package/src/app/layout.tsx
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { JetBrains_Mono } from "next/font/google";
|
|
2
|
-
import "pretendard/dist/web/variable/pretendardvariable-dynamic-subset.css";
|
|
3
|
-
import "./globals.css";
|
|
4
|
-
|
|
5
|
-
const jetbrainsMono = JetBrains_Mono({
|
|
6
|
-
variable: "--font-jetbrains-mono",
|
|
7
|
-
subsets: ["latin"],
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
export default function RootLayout({
|
|
11
|
-
children,
|
|
12
|
-
}: {
|
|
13
|
-
children: React.ReactNode;
|
|
14
|
-
}) {
|
|
15
|
-
return (
|
|
16
|
-
<html suppressHydrationWarning>
|
|
17
|
-
<body className={`${jetbrainsMono.variable} antialiased`}>
|
|
18
|
-
{children}
|
|
19
|
-
</body>
|
|
20
|
-
</html>
|
|
21
|
-
);
|
|
22
|
-
}
|
package/src/app/sitemap.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { MetadataRoute } from "next";
|
|
2
|
-
import { locales } from "@/i18n/config";
|
|
3
|
-
|
|
4
|
-
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || "https://naeil.dev";
|
|
5
|
-
const staticPages = ["", "/projects", "/design", "/cc", "/pkm"];
|
|
6
|
-
|
|
7
|
-
export default function sitemap(): MetadataRoute.Sitemap {
|
|
8
|
-
const entries: MetadataRoute.Sitemap = [];
|
|
9
|
-
|
|
10
|
-
for (const locale of locales) {
|
|
11
|
-
for (const page of staticPages) {
|
|
12
|
-
const languages: Record<string, string> = {};
|
|
13
|
-
for (const alt of locales) {
|
|
14
|
-
languages[alt] = `${APP_URL}/${alt}${page}`;
|
|
15
|
-
}
|
|
16
|
-
languages["x-default"] = `${APP_URL}/en${page}`;
|
|
17
|
-
|
|
18
|
-
entries.push({
|
|
19
|
-
url: `${APP_URL}/${locale}${page}`,
|
|
20
|
-
lastModified: new Date(),
|
|
21
|
-
alternates: { languages },
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return entries;
|
|
26
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { Button } from "@/components/ui/button";
|
|
5
|
-
|
|
6
|
-
const ACCENT_PRESETS = [
|
|
7
|
-
{ name: "블루 (기본)", hue: 259, chroma: 0.214 },
|
|
8
|
-
{ name: "퍼플", hue: 303, chroma: 0.22 },
|
|
9
|
-
{ name: "그린", hue: 162, chroma: 0.17 },
|
|
10
|
-
{ name: "오렌지", hue: 55, chroma: 0.22 },
|
|
11
|
-
{ name: "레드", hue: 25, chroma: 0.24 },
|
|
12
|
-
] as const;
|
|
13
|
-
|
|
14
|
-
function generateAccentScale(hue: number, chroma: number) {
|
|
15
|
-
// Generate a 50-950 OKLCH scale for the given hue/chroma
|
|
16
|
-
const steps = [
|
|
17
|
-
{ name: "50", l: 0.97, c: chroma * 0.065 },
|
|
18
|
-
{ name: "100", l: 0.932, c: chroma * 0.15 },
|
|
19
|
-
{ name: "200", l: 0.882, c: chroma * 0.275 },
|
|
20
|
-
{ name: "300", l: 0.809, c: chroma * 0.49 },
|
|
21
|
-
{ name: "400", l: 0.707, c: chroma * 0.77 },
|
|
22
|
-
{ name: "500", l: 0.623, c: chroma },
|
|
23
|
-
{ name: "600", l: 0.546, c: chroma * 1.14 },
|
|
24
|
-
{ name: "700", l: 0.488, c: chroma * 1.13 },
|
|
25
|
-
{ name: "800", l: 0.424, c: chroma * 0.93 },
|
|
26
|
-
{ name: "900", l: 0.379, c: chroma * 0.68 },
|
|
27
|
-
{ name: "950", l: 0.282, c: chroma * 0.42 },
|
|
28
|
-
];
|
|
29
|
-
return steps.map((s) => ({
|
|
30
|
-
name: s.name,
|
|
31
|
-
value: `oklch(${s.l} ${Number(s.c.toFixed(3))} ${hue})`,
|
|
32
|
-
}));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function AccentPicker() {
|
|
36
|
-
const [active, setActive] = useState(0);
|
|
37
|
-
|
|
38
|
-
function applyAccent(index: number) {
|
|
39
|
-
setActive(index);
|
|
40
|
-
const preset = ACCENT_PRESETS[index];
|
|
41
|
-
const scale = generateAccentScale(preset.hue, preset.chroma);
|
|
42
|
-
const root = document.documentElement;
|
|
43
|
-
for (const step of scale) {
|
|
44
|
-
root.style.setProperty(`--accent-${step.name}`, step.value);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<div className="flex flex-wrap gap-2">
|
|
50
|
-
{ACCENT_PRESETS.map((preset, i) => (
|
|
51
|
-
<Button
|
|
52
|
-
key={preset.name}
|
|
53
|
-
variant={active === i ? "default" : "outline"}
|
|
54
|
-
size="sm"
|
|
55
|
-
onClick={() => applyAccent(i)}
|
|
56
|
-
>
|
|
57
|
-
{preset.name}
|
|
58
|
-
</Button>
|
|
59
|
-
))}
|
|
60
|
-
</div>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useLocale, useTranslations } from "next-intl";
|
|
4
|
-
import { Link, usePathname } from "@/i18n/routing";
|
|
5
|
-
import Image from "next/image";
|
|
6
|
-
import { Avatar, AvatarImage, AvatarFallback } from "./ui/avatar";
|
|
7
|
-
import {
|
|
8
|
-
DropdownMenu,
|
|
9
|
-
DropdownMenuTrigger,
|
|
10
|
-
DropdownMenuContent,
|
|
11
|
-
DropdownMenuLabel,
|
|
12
|
-
DropdownMenuSeparator,
|
|
13
|
-
DropdownMenuItem,
|
|
14
|
-
} from "./ui/dropdown-menu";
|
|
15
|
-
import { logout } from "@/app/[locale]/login/actions";
|
|
16
|
-
|
|
17
|
-
const ANIMALS = [
|
|
18
|
-
"/images/coral.png",
|
|
19
|
-
"/images/fish.png",
|
|
20
|
-
"/images/jellyfish.png",
|
|
21
|
-
"/images/turtle.png",
|
|
22
|
-
"/images/whale.png",
|
|
23
|
-
"/images/diver.png",
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
function hashUserId(id: string): number {
|
|
27
|
-
let hash = 0;
|
|
28
|
-
for (let i = 0; i < id.length; i++) {
|
|
29
|
-
hash = ((hash << 5) - hash + id.charCodeAt(i)) | 0;
|
|
30
|
-
}
|
|
31
|
-
return Math.abs(hash) % ANIMALS.length;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface AuthUser {
|
|
35
|
-
id: string;
|
|
36
|
-
name?: string;
|
|
37
|
-
email?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function AuthSlot({ user }: { user: AuthUser | null }) {
|
|
41
|
-
const t = useTranslations("auth");
|
|
42
|
-
const locale = useLocale();
|
|
43
|
-
const pathname = usePathname();
|
|
44
|
-
|
|
45
|
-
if (!user) {
|
|
46
|
-
return (
|
|
47
|
-
<Link
|
|
48
|
-
href={`/login?next=${encodeURIComponent(pathname)}`}
|
|
49
|
-
className="text-muted-foreground hover:text-foreground text-sm font-medium transition-colors"
|
|
50
|
-
>
|
|
51
|
-
{t("login")}
|
|
52
|
-
</Link>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const animalSrc = ANIMALS[hashUserId(user.id)];
|
|
57
|
-
const displayName = user.name || user.email?.split("@")[0] || "User";
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<DropdownMenu>
|
|
61
|
-
<DropdownMenuTrigger asChild>
|
|
62
|
-
<button className="cursor-pointer focus:outline-none">
|
|
63
|
-
<Avatar>
|
|
64
|
-
<AvatarImage src={animalSrc} alt={displayName} />
|
|
65
|
-
<AvatarFallback>
|
|
66
|
-
{displayName.charAt(0).toUpperCase()}
|
|
67
|
-
</AvatarFallback>
|
|
68
|
-
</Avatar>
|
|
69
|
-
</button>
|
|
70
|
-
</DropdownMenuTrigger>
|
|
71
|
-
<DropdownMenuContent align="end" className="w-56">
|
|
72
|
-
<DropdownMenuLabel className="flex items-center gap-3 font-normal">
|
|
73
|
-
<Image
|
|
74
|
-
src={animalSrc}
|
|
75
|
-
alt=""
|
|
76
|
-
width={32}
|
|
77
|
-
height={32}
|
|
78
|
-
className="shrink-0 rounded-full"
|
|
79
|
-
/>
|
|
80
|
-
<div className="flex min-w-0 flex-col">
|
|
81
|
-
<span className="truncate text-sm font-medium">{displayName}</span>
|
|
82
|
-
{user.email && (
|
|
83
|
-
<span className="text-muted-foreground truncate text-xs">
|
|
84
|
-
{user.email}
|
|
85
|
-
</span>
|
|
86
|
-
)}
|
|
87
|
-
</div>
|
|
88
|
-
</DropdownMenuLabel>
|
|
89
|
-
<DropdownMenuSeparator />
|
|
90
|
-
<DropdownMenuItem onClick={() => logout(locale)}>
|
|
91
|
-
{t("logout")}
|
|
92
|
-
</DropdownMenuItem>
|
|
93
|
-
</DropdownMenuContent>
|
|
94
|
-
</DropdownMenu>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useTranslations } from "next-intl";
|
|
4
|
-
|
|
5
|
-
interface FooterProps {
|
|
6
|
-
/** Link component from consumer */
|
|
7
|
-
Link?: React.ComponentType<{
|
|
8
|
-
href: string;
|
|
9
|
-
children: React.ReactNode;
|
|
10
|
-
className?: string;
|
|
11
|
-
}>;
|
|
12
|
-
/** Whether links go to naeil.dev (external) or are internal. Default: false */
|
|
13
|
-
linksExternal?: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function Footer({ Link, linksExternal = false }: FooterProps) {
|
|
17
|
-
const t = useTranslations("footer");
|
|
18
|
-
|
|
19
|
-
// Render a navigation link — external <a> or internal Link
|
|
20
|
-
const NavLink = ({
|
|
21
|
-
href,
|
|
22
|
-
children,
|
|
23
|
-
className,
|
|
24
|
-
}: {
|
|
25
|
-
href: string;
|
|
26
|
-
children: React.ReactNode;
|
|
27
|
-
className?: string;
|
|
28
|
-
}) => {
|
|
29
|
-
if (linksExternal) {
|
|
30
|
-
return (
|
|
31
|
-
<a
|
|
32
|
-
href={`https://naeil.dev${href}`}
|
|
33
|
-
target="_blank"
|
|
34
|
-
rel="noopener noreferrer"
|
|
35
|
-
className={className}
|
|
36
|
-
>
|
|
37
|
-
{children}
|
|
38
|
-
</a>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
if (Link) {
|
|
42
|
-
return (
|
|
43
|
-
<Link href={href} className={className}>
|
|
44
|
-
{children}
|
|
45
|
-
</Link>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
return (
|
|
49
|
-
<a href={href} className={className}>
|
|
50
|
-
{children}
|
|
51
|
-
</a>
|
|
52
|
-
);
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<footer className="border-border/40 border-t">
|
|
57
|
-
<div className="mx-auto flex max-w-7xl flex-col gap-10 px-6 py-16 sm:flex-row sm:justify-between">
|
|
58
|
-
{/* Left: Brand */}
|
|
59
|
-
<div className="flex flex-col gap-4">
|
|
60
|
-
<div className="flex items-center gap-3">
|
|
61
|
-
<Logo size={20} />
|
|
62
|
-
<span className="text-foreground text-sm font-semibold tracking-tight">
|
|
63
|
-
.naeil
|
|
64
|
-
</span>
|
|
65
|
-
</div>
|
|
66
|
-
<p className="text-muted-foreground max-w-[240px] text-sm leading-relaxed">
|
|
67
|
-
{t("tagline")}
|
|
68
|
-
</p>
|
|
69
|
-
<div className="flex items-center gap-3">
|
|
70
|
-
<a
|
|
71
|
-
href="https://github.com/naeil-dev"
|
|
72
|
-
target="_blank"
|
|
73
|
-
rel="noopener noreferrer"
|
|
74
|
-
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
75
|
-
aria-label="GitHub"
|
|
76
|
-
>
|
|
77
|
-
<GitHubIcon />
|
|
78
|
-
</a>
|
|
79
|
-
<a
|
|
80
|
-
href="https://x.com"
|
|
81
|
-
target="_blank"
|
|
82
|
-
rel="noopener noreferrer"
|
|
83
|
-
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
84
|
-
aria-label="X"
|
|
85
|
-
>
|
|
86
|
-
<XIcon />
|
|
87
|
-
</a>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
{/* Right: Links */}
|
|
92
|
-
<div className="flex gap-10 sm:gap-16">
|
|
93
|
-
<div className="flex flex-col gap-3">
|
|
94
|
-
<span className="text-foreground text-xs font-medium uppercase tracking-wider">
|
|
95
|
-
{t("navigation")}
|
|
96
|
-
</span>
|
|
97
|
-
<NavLink
|
|
98
|
-
href="/projects"
|
|
99
|
-
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
|
100
|
-
>
|
|
101
|
-
{t("projects")}
|
|
102
|
-
</NavLink>
|
|
103
|
-
<NavLink
|
|
104
|
-
href="/blog"
|
|
105
|
-
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
|
106
|
-
>
|
|
107
|
-
{t("blog")}
|
|
108
|
-
</NavLink>
|
|
109
|
-
</div>
|
|
110
|
-
<div className="flex flex-col gap-3">
|
|
111
|
-
<span className="text-foreground text-xs font-medium uppercase tracking-wider">
|
|
112
|
-
{t("social")}
|
|
113
|
-
</span>
|
|
114
|
-
<a
|
|
115
|
-
href="https://github.com/naeil-dev"
|
|
116
|
-
target="_blank"
|
|
117
|
-
rel="noopener noreferrer"
|
|
118
|
-
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
|
119
|
-
>
|
|
120
|
-
GitHub ↗
|
|
121
|
-
</a>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
</footer>
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/* ── Inline Logo (pure SVG) ── */
|
|
130
|
-
function Logo({ size = 20 }: { size?: number }) {
|
|
131
|
-
const id = `naeil-footer-logo-${size}`;
|
|
132
|
-
return (
|
|
133
|
-
<svg
|
|
134
|
-
width={size}
|
|
135
|
-
height={size}
|
|
136
|
-
viewBox="0 0 24 24"
|
|
137
|
-
fill="none"
|
|
138
|
-
aria-label="naeil"
|
|
139
|
-
>
|
|
140
|
-
<path
|
|
141
|
-
d="M6 4 L6 16 L20 16"
|
|
142
|
-
stroke="currentColor"
|
|
143
|
-
strokeWidth="2"
|
|
144
|
-
strokeLinecap="square"
|
|
145
|
-
strokeLinejoin="miter"
|
|
146
|
-
/>
|
|
147
|
-
<circle
|
|
148
|
-
cx="15"
|
|
149
|
-
cy="16"
|
|
150
|
-
r="3.5"
|
|
151
|
-
fill={`url(#${id}-grad)`}
|
|
152
|
-
mask={`url(#${id})`}
|
|
153
|
-
/>
|
|
154
|
-
<defs>
|
|
155
|
-
<linearGradient id={`${id}-grad`} x1="15" y1="12.5" x2="15" y2="16">
|
|
156
|
-
<stop stopColor="#eab308" />
|
|
157
|
-
<stop offset="1" stopColor="#dc2626" />
|
|
158
|
-
</linearGradient>
|
|
159
|
-
<mask id={id}>
|
|
160
|
-
<rect x="0" y="0" width="24" height="16" fill="white" />
|
|
161
|
-
</mask>
|
|
162
|
-
</defs>
|
|
163
|
-
</svg>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function GitHubIcon() {
|
|
168
|
-
return (
|
|
169
|
-
<svg
|
|
170
|
-
width="16"
|
|
171
|
-
height="16"
|
|
172
|
-
viewBox="0 0 24 24"
|
|
173
|
-
fill="currentColor"
|
|
174
|
-
aria-hidden="true"
|
|
175
|
-
>
|
|
176
|
-
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
|
177
|
-
</svg>
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function XIcon() {
|
|
182
|
-
return (
|
|
183
|
-
<svg
|
|
184
|
-
width="14"
|
|
185
|
-
height="14"
|
|
186
|
-
viewBox="0 0 24 24"
|
|
187
|
-
fill="currentColor"
|
|
188
|
-
aria-hidden="true"
|
|
189
|
-
>
|
|
190
|
-
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
191
|
-
</svg>
|
|
192
|
-
);
|
|
193
|
-
}
|