@naeil/ui 0.1.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/README.md +36 -0
- package/messages/en.json +221 -0
- package/messages/ja.json +221 -0
- package/messages/ko.json +221 -0
- package/package.json +73 -0
- package/postcss.config.mjs +7 -0
- package/src/app/[locale]/(content)/baby-agent/page.tsx +30 -0
- package/src/app/[locale]/(content)/blog/page.tsx +68 -0
- package/src/app/[locale]/(content)/cc/page.tsx +216 -0
- package/src/app/[locale]/(content)/design/page.tsx +703 -0
- package/src/app/[locale]/(content)/layout.tsx +7 -0
- package/src/app/[locale]/(content)/pkm/page.tsx +31 -0
- package/src/app/[locale]/(content)/projects/page.tsx +100 -0
- package/src/app/[locale]/(content)/sa/page.tsx +179 -0
- package/src/app/[locale]/(landing)/page.tsx +157 -0
- package/src/app/[locale]/layout.tsx +72 -0
- package/src/app/[locale]/login/actions.ts +10 -0
- package/src/app/[locale]/login/oauth-buttons.tsx +78 -0
- package/src/app/[locale]/login/page.tsx +85 -0
- package/src/app/auth/callback/route.ts +78 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +32 -0
- package/src/app/layout.tsx +22 -0
- package/src/app/sitemap.ts +26 -0
- package/src/components/accent-picker.tsx +62 -0
- package/src/components/auth-slot.tsx +96 -0
- package/src/components/footer-wrapper.tsx +8 -0
- package/src/components/footer.tsx +193 -0
- package/src/components/hero-scene.tsx +948 -0
- package/src/components/hero-section.tsx +57 -0
- package/src/components/locale-switcher.tsx +74 -0
- package/src/components/logo.tsx +51 -0
- package/src/components/nav-server-wrapper.tsx +22 -0
- package/src/components/nav-wrapper.tsx +45 -0
- package/src/components/nav.tsx +550 -0
- package/src/components/paraglider-cursor.tsx +112 -0
- package/src/components/project-layout.tsx +194 -0
- package/src/components/theme-provider.tsx +10 -0
- package/src/components/theme-toggle-icon.tsx +62 -0
- package/src/components/theme-toggle.tsx +39 -0
- package/src/components/typography.tsx +27 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +47 -0
- package/src/components/ui/button.tsx +65 -0
- package/src/components/ui/card.tsx +106 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/sonner.tsx +54 -0
- package/src/components/workflow-diagram.tsx +414 -0
- package/src/i18n/config.ts +8 -0
- package/src/i18n/request.ts +16 -0
- package/src/i18n/routing.ts +12 -0
- package/src/lib/supabase/client.ts +18 -0
- package/src/lib/supabase/middleware.ts +42 -0
- package/src/lib/supabase/server.ts +37 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +62 -0
- package/src/tokens/.gitkeep +0 -0
- package/src/tokens/color.json +56 -0
- package/src/tokens/radius.json +9 -0
- package/src/tokens/semantic.json +81 -0
- package/src/tokens/shadow.json +7 -0
- package/src/tokens/spacing.json +16 -0
- package/src/tokens/typography.json +34 -0
- package/src/types/i18n.d.ts +6 -0
- package/src/utils/.gitkeep +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { setRequestLocale } from "next-intl/server";
|
|
2
|
+
import { useTranslations } from "next-intl";
|
|
3
|
+
import { ProjectLayout } from "@/components/project-layout";
|
|
4
|
+
import type { ProjectData } from "@/components/project-layout";
|
|
5
|
+
|
|
6
|
+
export default async function PKMPage({
|
|
7
|
+
params,
|
|
8
|
+
}: {
|
|
9
|
+
params: Promise<{ locale: string }>;
|
|
10
|
+
}) {
|
|
11
|
+
const { locale } = await params;
|
|
12
|
+
setRequestLocale(locale);
|
|
13
|
+
|
|
14
|
+
return <PKMContent />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function PKMContent() {
|
|
18
|
+
const t = useTranslations("pkm");
|
|
19
|
+
|
|
20
|
+
const data: ProjectData = {
|
|
21
|
+
icon: "/images/jellyfish.png",
|
|
22
|
+
name: t("name"),
|
|
23
|
+
description: t("description"),
|
|
24
|
+
accent: "oklch(0.627 0.265 303)",
|
|
25
|
+
tags: ["Python", "Embeddings", "Semantic Search"],
|
|
26
|
+
github: "https://github.com/naeil-dev/pkm",
|
|
27
|
+
comingSoon: true,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return <ProjectLayout data={data} />;
|
|
31
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { setRequestLocale } from "next-intl/server";
|
|
2
|
+
import { useTranslations } from "next-intl";
|
|
3
|
+
import { Link } from "@/i18n/routing";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardTitle,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardContent,
|
|
10
|
+
} from "@/components/ui/card";
|
|
11
|
+
import { Badge } from "@/components/ui/badge";
|
|
12
|
+
import { PageTitle } from "@/components/typography";
|
|
13
|
+
|
|
14
|
+
export default async function ProjectsPage({
|
|
15
|
+
params,
|
|
16
|
+
}: {
|
|
17
|
+
params: Promise<{ locale: string }>;
|
|
18
|
+
}) {
|
|
19
|
+
const { locale } = await params;
|
|
20
|
+
setRequestLocale(locale);
|
|
21
|
+
|
|
22
|
+
return <ProjectsContent />;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ProjectsContent() {
|
|
26
|
+
const t = useTranslations();
|
|
27
|
+
|
|
28
|
+
const PROJECTS = [
|
|
29
|
+
{
|
|
30
|
+
name: t("projects.cc.name"),
|
|
31
|
+
description: t("projects.cc.description"),
|
|
32
|
+
href: "/cc" as const,
|
|
33
|
+
tags: ["Python", "RSS", "AI"],
|
|
34
|
+
status: t("projects.status.active"),
|
|
35
|
+
coming: false,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: t("projects.pkm.name"),
|
|
39
|
+
description: t("projects.pkm.description"),
|
|
40
|
+
href: "/pkm" as const,
|
|
41
|
+
tags: ["Python", "Embeddings", "Search"],
|
|
42
|
+
status: t("projects.status.coming"),
|
|
43
|
+
coming: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: t("projects.naeilUi.name"),
|
|
47
|
+
description: t("projects.naeilUi.description"),
|
|
48
|
+
href: "/design" as const,
|
|
49
|
+
tags: ["React", "Tailwind", "Design Tokens"],
|
|
50
|
+
status: t("projects.status.active"),
|
|
51
|
+
coming: false,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: t("projects.babyAgent.name"),
|
|
55
|
+
description: t("projects.babyAgent.description"),
|
|
56
|
+
href: "/baby-agent" as const,
|
|
57
|
+
tags: ["OpenClaw", "LINE", "AI"],
|
|
58
|
+
status: t("projects.status.coming"),
|
|
59
|
+
coming: true,
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<section className="mx-auto max-w-4xl px-6 pb-24">
|
|
65
|
+
<div className="mb-10">
|
|
66
|
+
<PageTitle className="mb-3">{t("projects.title")}</PageTitle>
|
|
67
|
+
<p className="text-muted-foreground text-lg">
|
|
68
|
+
{t("projects.subtitle")}
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div className="grid gap-4">
|
|
73
|
+
{PROJECTS.map((project) => (
|
|
74
|
+
<Link key={project.name} href={project.href}>
|
|
75
|
+
<Card className="transition-colors hover:border-foreground/20">
|
|
76
|
+
<CardHeader>
|
|
77
|
+
<div className="flex items-center justify-between">
|
|
78
|
+
<CardTitle className="text-base">{project.name}</CardTitle>
|
|
79
|
+
<Badge variant={project.coming ? "outline" : "secondary"}>
|
|
80
|
+
{project.status}
|
|
81
|
+
</Badge>
|
|
82
|
+
</div>
|
|
83
|
+
<CardDescription>{project.description}</CardDescription>
|
|
84
|
+
</CardHeader>
|
|
85
|
+
<CardContent>
|
|
86
|
+
<div className="flex flex-wrap gap-1.5">
|
|
87
|
+
{project.tags.map((tag) => (
|
|
88
|
+
<Badge key={tag} variant="outline" className="text-xs">
|
|
89
|
+
{tag}
|
|
90
|
+
</Badge>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
</CardContent>
|
|
94
|
+
</Card>
|
|
95
|
+
</Link>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
</section>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { setRequestLocale } from "next-intl/server";
|
|
2
|
+
import { useTranslations } from "next-intl";
|
|
3
|
+
import { ProjectLayout } from "@/components/project-layout";
|
|
4
|
+
import type { ProjectData } from "@/components/project-layout";
|
|
5
|
+
|
|
6
|
+
export default async function SAPage({
|
|
7
|
+
params,
|
|
8
|
+
}: {
|
|
9
|
+
params: Promise<{ locale: string }>;
|
|
10
|
+
}) {
|
|
11
|
+
const { locale } = await params;
|
|
12
|
+
setRequestLocale(locale);
|
|
13
|
+
|
|
14
|
+
return <SAContent />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function SAContent() {
|
|
18
|
+
const t = useTranslations("sa");
|
|
19
|
+
|
|
20
|
+
const data: ProjectData = {
|
|
21
|
+
icon: "/images/fish.png",
|
|
22
|
+
name: t("name"),
|
|
23
|
+
description: t("description"),
|
|
24
|
+
accent: "oklch(0.723 0.219 149)",
|
|
25
|
+
tags: ["Python", "PostgreSQL", "pgvector", "BGE-M3", "Claude", "Docling"],
|
|
26
|
+
github: "https://github.com/naeil-dev/sustainability-analyzer",
|
|
27
|
+
|
|
28
|
+
overview: t("overview"),
|
|
29
|
+
|
|
30
|
+
features: [
|
|
31
|
+
{
|
|
32
|
+
icon: "📄",
|
|
33
|
+
title: t("features.ingest.title"),
|
|
34
|
+
description: t("features.ingest.desc"),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
icon: "🔍",
|
|
38
|
+
title: t("features.embed.title"),
|
|
39
|
+
description: t("features.embed.desc"),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
icon: "🗂️",
|
|
43
|
+
title: t("features.topics.title"),
|
|
44
|
+
description: t("features.topics.desc"),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
icon: "📊",
|
|
48
|
+
title: t("features.assess.title"),
|
|
49
|
+
description: t("features.assess.desc"),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
icon: "⚖️",
|
|
53
|
+
title: t("features.compare.title"),
|
|
54
|
+
description: t("features.compare.desc"),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
icon: "⚡",
|
|
58
|
+
title: t("features.zeroCost.title"),
|
|
59
|
+
description: t("features.zeroCost.desc"),
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
|
|
63
|
+
workflow: {
|
|
64
|
+
nodes: [
|
|
65
|
+
{
|
|
66
|
+
id: "pdf",
|
|
67
|
+
label: "ESG Reports",
|
|
68
|
+
type: "source",
|
|
69
|
+
position: { x: 50, y: 5 },
|
|
70
|
+
detail: {
|
|
71
|
+
description: t("workflow.pdf.desc"),
|
|
72
|
+
tech: ["PDF"],
|
|
73
|
+
meta: t("workflow.pdf.meta"),
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "docling",
|
|
78
|
+
label: "Docling Parser",
|
|
79
|
+
type: "process",
|
|
80
|
+
position: { x: 50, y: 18 },
|
|
81
|
+
detail: {
|
|
82
|
+
description: t("workflow.docling.desc"),
|
|
83
|
+
tech: ["Docling", "Python"],
|
|
84
|
+
meta: t("workflow.docling.meta"),
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "chunk",
|
|
89
|
+
label: "Hierarchical Chunker",
|
|
90
|
+
type: "process",
|
|
91
|
+
position: { x: 50, y: 32 },
|
|
92
|
+
detail: {
|
|
93
|
+
description: t("workflow.chunk.desc"),
|
|
94
|
+
tech: ["Parent-Child", "Layout-aware"],
|
|
95
|
+
meta: t("workflow.chunk.meta"),
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "bgem3",
|
|
100
|
+
label: "BGE-M3",
|
|
101
|
+
type: "ai",
|
|
102
|
+
position: { x: 25, y: 46 },
|
|
103
|
+
detail: {
|
|
104
|
+
description: t("workflow.bgem3.desc"),
|
|
105
|
+
tech: ["FlagEmbedding", "PyTorch"],
|
|
106
|
+
meta: t("workflow.bgem3.meta"),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "pgvector",
|
|
111
|
+
label: "PostgreSQL",
|
|
112
|
+
type: "storage",
|
|
113
|
+
position: { x: 75, y: 46 },
|
|
114
|
+
detail: {
|
|
115
|
+
description: t("workflow.pgvector.desc"),
|
|
116
|
+
tech: ["PostgreSQL 17", "pgvector", "Docker"],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "search",
|
|
121
|
+
label: "Hybrid Search",
|
|
122
|
+
type: "process",
|
|
123
|
+
position: { x: 50, y: 60 },
|
|
124
|
+
detail: {
|
|
125
|
+
description: t("workflow.search.desc"),
|
|
126
|
+
tech: ["Dense", "Sparse", "pgvector"],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "claude",
|
|
131
|
+
label: "Claude Sonnet",
|
|
132
|
+
type: "ai",
|
|
133
|
+
position: { x: 50, y: 74 },
|
|
134
|
+
detail: {
|
|
135
|
+
description: t("workflow.claude.desc"),
|
|
136
|
+
tech: ["Claude Sonnet", "Anthropic API"],
|
|
137
|
+
meta: t("workflow.claude.meta"),
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: "report",
|
|
142
|
+
label: "Comparison Report",
|
|
143
|
+
type: "output",
|
|
144
|
+
position: { x: 50, y: 90 },
|
|
145
|
+
detail: {
|
|
146
|
+
description: t("workflow.report.desc"),
|
|
147
|
+
tech: ["Jinja2", "HTML"],
|
|
148
|
+
meta: t("workflow.report.meta"),
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
edges: [
|
|
153
|
+
{ from: "pdf", to: "docling" },
|
|
154
|
+
{ from: "docling", to: "chunk" },
|
|
155
|
+
{ from: "chunk", to: "bgem3" },
|
|
156
|
+
{ from: "chunk", to: "pgvector" },
|
|
157
|
+
{ from: "bgem3", to: "pgvector" },
|
|
158
|
+
{ from: "pgvector", to: "search" },
|
|
159
|
+
{ from: "search", to: "claude" },
|
|
160
|
+
{ from: "claude", to: "pgvector" },
|
|
161
|
+
{ from: "claude", to: "report" },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
stack: [
|
|
166
|
+
{ icon: "🐍", name: "Python 3.12" },
|
|
167
|
+
{ icon: "🐘", name: "PostgreSQL 17" },
|
|
168
|
+
{ icon: "🧮", name: "pgvector 0.8" },
|
|
169
|
+
{ icon: "🔤", name: "BGE-M3" },
|
|
170
|
+
{ icon: "🧠", name: "Claude Sonnet" },
|
|
171
|
+
{ icon: "📄", name: "Docling" },
|
|
172
|
+
{ icon: "🔥", name: "PyTorch" },
|
|
173
|
+
{ icon: "🐳", name: "Docker Compose" },
|
|
174
|
+
{ icon: "📊", name: "Jinja2" },
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return <ProjectLayout data={data} />;
|
|
179
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { setRequestLocale } from "next-intl/server";
|
|
2
|
+
import { useTranslations } from "next-intl";
|
|
3
|
+
import { Link } from "@/i18n/routing";
|
|
4
|
+
import { Badge } from "@/components/ui/badge";
|
|
5
|
+
import { HeroSection } from "@/components/hero-section";
|
|
6
|
+
import Image from "next/image";
|
|
7
|
+
|
|
8
|
+
export default async function HubPage({
|
|
9
|
+
params,
|
|
10
|
+
}: {
|
|
11
|
+
params: Promise<{ locale: string }>;
|
|
12
|
+
}) {
|
|
13
|
+
const { locale } = await params;
|
|
14
|
+
setRequestLocale(locale);
|
|
15
|
+
|
|
16
|
+
return <HubContent />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PROJECTS = [
|
|
20
|
+
{
|
|
21
|
+
key: "cc" as const,
|
|
22
|
+
href: "/cc" as const,
|
|
23
|
+
icon: "/images/coral.png",
|
|
24
|
+
accent: "oklch(0.704 0.140 181)",
|
|
25
|
+
tags: ["Python", "RSS", "AI"],
|
|
26
|
+
status: "active" as const,
|
|
27
|
+
featured: true,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: "pkm" as const,
|
|
31
|
+
href: "/pkm" as const,
|
|
32
|
+
icon: "/images/jellyfish.png",
|
|
33
|
+
accent: "oklch(0.702 0.183 293)",
|
|
34
|
+
tags: ["Python", "Embeddings", "Search"],
|
|
35
|
+
status: "coming" as const,
|
|
36
|
+
featured: false,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: "naeilUi" as const,
|
|
40
|
+
href: "/design" as const,
|
|
41
|
+
icon: "/images/whale.png",
|
|
42
|
+
accent: "oklch(0.623 0.214 259)",
|
|
43
|
+
tags: ["React", "Tailwind", "OKLCH"],
|
|
44
|
+
status: "active" as const,
|
|
45
|
+
featured: false,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: "sa" as const,
|
|
49
|
+
href: "/sa" as const,
|
|
50
|
+
icon: "/images/fish.png",
|
|
51
|
+
accent: "oklch(0.723 0.219 149)",
|
|
52
|
+
tags: ["Python", "pgvector", "Claude"],
|
|
53
|
+
status: "active" as const,
|
|
54
|
+
featured: false,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: "babyAgent" as const,
|
|
58
|
+
href: "/baby-agent" as const,
|
|
59
|
+
icon: "/images/turtle.png",
|
|
60
|
+
accent: "oklch(0.769 0.188 70.08)",
|
|
61
|
+
tags: ["LINE", "AI"],
|
|
62
|
+
status: "coming" as const,
|
|
63
|
+
featured: false,
|
|
64
|
+
},
|
|
65
|
+
] as const;
|
|
66
|
+
|
|
67
|
+
function HubContent() {
|
|
68
|
+
const t = useTranslations();
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
{/* Hero */}
|
|
73
|
+
<HeroSection />
|
|
74
|
+
|
|
75
|
+
{/* Projects — Bento Grid */}
|
|
76
|
+
<section className="mx-auto max-w-4xl px-6 py-24">
|
|
77
|
+
<h2 className="text-foreground mb-2 text-2xl font-bold tracking-tight">
|
|
78
|
+
{t("projects.title")}
|
|
79
|
+
</h2>
|
|
80
|
+
<p className="text-muted-foreground mb-10 text-sm">
|
|
81
|
+
{t("projects.subtitle")}
|
|
82
|
+
</p>
|
|
83
|
+
|
|
84
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
85
|
+
{PROJECTS.map((project) => (
|
|
86
|
+
<Link
|
|
87
|
+
key={project.key}
|
|
88
|
+
href={project.href}
|
|
89
|
+
className={[
|
|
90
|
+
"group relative rounded-2xl border border-border-subtle bg-surface-subtle p-7 transition-all duration-250",
|
|
91
|
+
"hover:-translate-y-0.5 hover:border-border-subtle-hover hover:shadow-[0_8px_32px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_8px_32px_rgba(0,0,0,0.3)]",
|
|
92
|
+
project.featured ? "sm:col-span-2 sm:flex sm:items-center sm:gap-8" : "",
|
|
93
|
+
].join(" ")}
|
|
94
|
+
style={{ "--accent": project.accent } as React.CSSProperties}
|
|
95
|
+
>
|
|
96
|
+
{/* Icon */}
|
|
97
|
+
<div
|
|
98
|
+
className={[
|
|
99
|
+
"flex shrink-0 items-center justify-center rounded-[14px]",
|
|
100
|
+
project.featured
|
|
101
|
+
? "mb-5 h-14 w-14 sm:mb-0 sm:h-20 sm:w-20"
|
|
102
|
+
: "mb-5 h-14 w-14",
|
|
103
|
+
].join(" ")}
|
|
104
|
+
style={{
|
|
105
|
+
background: `color-mix(in oklch, ${project.accent} 8%, transparent)`,
|
|
106
|
+
border: `1px solid color-mix(in oklch, ${project.accent} 20%, transparent)`,
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<Image
|
|
110
|
+
src={project.icon}
|
|
111
|
+
alt=""
|
|
112
|
+
width={project.featured ? 52 : 36}
|
|
113
|
+
height={project.featured ? 52 : 36}
|
|
114
|
+
className="object-contain"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Text */}
|
|
119
|
+
<div className={project.featured ? "flex-1" : ""}>
|
|
120
|
+
<div className="text-foreground mb-1.5 text-[15px] font-semibold">
|
|
121
|
+
{t(`projects.${project.key}.name`)}
|
|
122
|
+
{project.status === "coming" && (
|
|
123
|
+
<Badge
|
|
124
|
+
variant="outline"
|
|
125
|
+
className="text-muted-foreground ml-2 px-1.5 py-0 text-[9px] align-middle"
|
|
126
|
+
>
|
|
127
|
+
{t("projects.status.coming")}
|
|
128
|
+
</Badge>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
<p className="text-muted-foreground mb-3 text-[13px] leading-relaxed">
|
|
132
|
+
{t(`projects.${project.key}.description`)}
|
|
133
|
+
</p>
|
|
134
|
+
<div className="flex flex-wrap gap-1.5">
|
|
135
|
+
{project.tags.map((tag) => (
|
|
136
|
+
<span
|
|
137
|
+
key={tag}
|
|
138
|
+
className="rounded-full border border-border-subtle px-2 py-0.5 text-[10px] text-zinc-400"
|
|
139
|
+
>
|
|
140
|
+
{tag}
|
|
141
|
+
</span>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* Arrow */}
|
|
147
|
+
<span className="text-muted-foreground/40 group-hover:text-foreground absolute right-6 top-6 text-sm transition-colors">
|
|
148
|
+
↗
|
|
149
|
+
</span>
|
|
150
|
+
</Link>
|
|
151
|
+
))}
|
|
152
|
+
</div>
|
|
153
|
+
</section>
|
|
154
|
+
|
|
155
|
+
</>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { NextIntlClientProvider } from "next-intl";
|
|
3
|
+
import {
|
|
4
|
+
getMessages,
|
|
5
|
+
getTranslations,
|
|
6
|
+
setRequestLocale,
|
|
7
|
+
} from "next-intl/server";
|
|
8
|
+
import { notFound } from "next/navigation";
|
|
9
|
+
import { ThemeProvider } from "@/components/theme-provider";
|
|
10
|
+
import { Toaster } from "@/components/ui/sonner";
|
|
11
|
+
import { NavServerWrapper } from "@/components/nav-server-wrapper";
|
|
12
|
+
import { FooterWrapper } from "@/components/footer-wrapper";
|
|
13
|
+
import { locales, type Locale } from "@/i18n/config";
|
|
14
|
+
|
|
15
|
+
export function generateStaticParams() {
|
|
16
|
+
return locales.map((locale) => ({ locale }));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function generateMetadata({
|
|
20
|
+
params,
|
|
21
|
+
}: {
|
|
22
|
+
params: Promise<{ locale: string }>;
|
|
23
|
+
}): Promise<Metadata> {
|
|
24
|
+
const { locale } = await params;
|
|
25
|
+
const t = await getTranslations({ locale, namespace: "common" });
|
|
26
|
+
return {
|
|
27
|
+
title: t("appName"),
|
|
28
|
+
description: "Personal projects by Jay",
|
|
29
|
+
alternates: {
|
|
30
|
+
canonical: `https://naeil.dev/${locale}`,
|
|
31
|
+
languages: {
|
|
32
|
+
en: "https://naeil.dev/en",
|
|
33
|
+
ko: "https://naeil.dev/ko",
|
|
34
|
+
ja: "https://naeil.dev/ja",
|
|
35
|
+
"x-default": "https://naeil.dev/en",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default async function LocaleLayout({
|
|
42
|
+
children,
|
|
43
|
+
params,
|
|
44
|
+
}: {
|
|
45
|
+
children: React.ReactNode;
|
|
46
|
+
params: Promise<{ locale: string }>;
|
|
47
|
+
}) {
|
|
48
|
+
const { locale } = await params;
|
|
49
|
+
|
|
50
|
+
if (!locales.includes(locale as Locale)) {
|
|
51
|
+
notFound();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setRequestLocale(locale);
|
|
55
|
+
const messages = await getMessages();
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<NextIntlClientProvider messages={messages}>
|
|
59
|
+
<ThemeProvider
|
|
60
|
+
attribute="class"
|
|
61
|
+
defaultTheme="dark"
|
|
62
|
+
enableSystem
|
|
63
|
+
disableTransitionOnChange
|
|
64
|
+
>
|
|
65
|
+
<NavServerWrapper />
|
|
66
|
+
<main>{children}</main>
|
|
67
|
+
<FooterWrapper />
|
|
68
|
+
<Toaster />
|
|
69
|
+
</ThemeProvider>
|
|
70
|
+
</NextIntlClientProvider>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import { redirect } from "@/i18n/routing";
|
|
4
|
+
import { createClient } from "@/lib/supabase/server";
|
|
5
|
+
|
|
6
|
+
export async function logout(locale: string) {
|
|
7
|
+
const supabase = await createClient();
|
|
8
|
+
await supabase.auth.signOut();
|
|
9
|
+
redirect({ href: "/", locale });
|
|
10
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { createClient } from "@/lib/supabase/client";
|
|
6
|
+
|
|
7
|
+
export function OAuthButtons({ next }: { next?: string }) {
|
|
8
|
+
const t = useTranslations("auth");
|
|
9
|
+
|
|
10
|
+
const handleOAuth = async (provider: "google" | "github") => {
|
|
11
|
+
const supabase = createClient();
|
|
12
|
+
await supabase.auth.signInWithOAuth({
|
|
13
|
+
provider,
|
|
14
|
+
options: {
|
|
15
|
+
redirectTo: `${window.location.origin}/auth/callback`,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex flex-col gap-3">
|
|
22
|
+
<Button
|
|
23
|
+
variant="outline"
|
|
24
|
+
size="lg"
|
|
25
|
+
className="w-full"
|
|
26
|
+
onClick={() => handleOAuth("google")}
|
|
27
|
+
>
|
|
28
|
+
<GoogleIcon />
|
|
29
|
+
{t("loginWithGoogle")}
|
|
30
|
+
</Button>
|
|
31
|
+
<Button
|
|
32
|
+
variant="outline"
|
|
33
|
+
size="lg"
|
|
34
|
+
className="w-full"
|
|
35
|
+
onClick={() => handleOAuth("github")}
|
|
36
|
+
>
|
|
37
|
+
<GitHubIcon />
|
|
38
|
+
{t("loginWithGithub")}
|
|
39
|
+
</Button>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function GoogleIcon() {
|
|
45
|
+
return (
|
|
46
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
|
47
|
+
<path
|
|
48
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
|
|
49
|
+
fill="#4285F4"
|
|
50
|
+
/>
|
|
51
|
+
<path
|
|
52
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
53
|
+
fill="#34A853"
|
|
54
|
+
/>
|
|
55
|
+
<path
|
|
56
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
57
|
+
fill="#FBBC05"
|
|
58
|
+
/>
|
|
59
|
+
<path
|
|
60
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
61
|
+
fill="#EA4335"
|
|
62
|
+
/>
|
|
63
|
+
</svg>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function GitHubIcon() {
|
|
68
|
+
return (
|
|
69
|
+
<svg
|
|
70
|
+
width="16"
|
|
71
|
+
height="16"
|
|
72
|
+
viewBox="0 0 24 24"
|
|
73
|
+
fill="currentColor"
|
|
74
|
+
>
|
|
75
|
+
<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" />
|
|
76
|
+
</svg>
|
|
77
|
+
);
|
|
78
|
+
}
|