This commit is contained in:
2026-05-09 01:20:37 +07:00
parent 1711c81754
commit 35a942908b
93 changed files with 83627 additions and 0 deletions
+207
View File
@@ -0,0 +1,207 @@
# Copilot Instructions (Next.js App Router)
## Core Principles
- Clean, readable, maintainable code
- Functional components + hooks only
- Single responsibility per component
- Avoid duplication
- Do NOT break UI or existing behavior
---
## Architecture
- Routing → `src/app/` (Next.js App Router)
- UI → `components/` (render only)
- Feature logic → `features/`
- Shared logic → `utils/`
- API layer → `features/*/api` or `lib/`
- External setup → `lib/`
---
## Folder Structure
```txt
src/
app/ # Next.js routing (page.tsx, layout.tsx)
components/ # reusable UI (atoms, molecules, organisms, templates)
features/ # domain-based logic (blog/, product/, auth/, ...)
<feature>/
api/
components/
hooks/
types/
utils/
lib/ # axios, config, shared setup
utils/ # global helpers (pure functions)
types/ # shared types
constants/ # global constants
styles/ # optional (global styles)
```
---
## Routing Rules (CRITICAL)
- Use **App Router only**
- All routes must be inside `src/app/`
- Do NOT use `src/pages/` (remove if exists)
Example:
```txt
src/app/
layout.tsx
page.tsx
blog/
page.tsx
[slug]/
page.tsx
```
---
## API Rules
- Do NOT use `services/`
- API must be placed in:
- `features/<feature>/api/` (preferred)
- or `lib/` (shared APIs)
- API layer:
- Only handle HTTP
- No UI logic
---
## Data Flow Rules
- Data fetching:
- in **Server Components (app/)** OR
- in **hooks (features/\*/hooks)**
- Components:
- MUST be presentational only
- MUST NOT call API directly
---
## Types Rules
- `features/*/types` → feature-specific types
- `types/` → shared global types
- Do NOT mix API types and UI types
- Keep local types close to usage
---
## Components (Atomic Design)
- atoms → basic UI (Button, Input)
- molecules → small combos (SearchBox, Card)
- organisms → complex UI (Header, ProductSection)
- templates → layouts
➡️ Prefer composition over duplication
---
## Hooks
- Place in `features/<feature>/hooks`
- Used for:
- data fetching
- state logic
- Keep reusable and isolated
---
## Utils
- Pure functions only
- No side effects
- No API calls
---
## Lib
- axios instance
- interceptors
- config
- shared setup
---
## Styling
- Prefer Tailwind CSS + shadcn/ui (recommended)
- If SCSS is used:
- follow BEM
- each component has its own SCSS
- Avoid inline styles
---
## Naming
- components → PascalCase
- folders → kebab-case
- utils/api → camelCase or lowercase
- entry files → `index.tsx` or `index.ts`
---
## Import Rules (CRITICAL)
Use alias:
```ts
@/* src/*
```
Correct:
```ts
import { Button } from '@/components/ui/Button';
```
Forbidden:
```ts
import Button from '../../../components/Button';
```
---
## Code Rules
- No API calls inside UI components
- No mixing UI + business logic
- Keep files small and reusable
- Prefer Server Components when possible
---
## Remove Legacy Structure
- Remove `src/pages/`
- Remove `routes/`
- Remove `services/` if duplicated with API layer
- Remove unused folders/files after verification
---
## Scripts
```bash
npm install
npm run dev
npm run build
npm start
```
+41
View File
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+7
View File
@@ -0,0 +1,7 @@
{
"endOfLine": "lf",
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all"
}
+15
View File
@@ -0,0 +1,15 @@
import type { StorybookConfig } from "@storybook/nextjs";
const config: StorybookConfig = {
stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-essentials",
],
framework: {
name: "@storybook/nextjs",
options: {},
},
staticDirs: ["../public"],
};
export default config;
+15
View File
@@ -0,0 +1,15 @@
import type { Preview } from "@storybook/react";
import "../src/app/globals.css";
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
+14
View File
@@ -0,0 +1,14 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app ./
EXPOSE 3000
CMD ["npm", "start"]
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
+17328
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@ant-design/icons": "^6.1.0",
"@base-ui/react": "^1.3.0",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.562.0",
"next": "16.1.1",
"next-themes": "^0.4.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"shadcn": "^4.1.2",
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@storybook/addon-essentials": "^8.6.14",
"@storybook/nextjs": "^8.6.18",
"@storybook/react": "^8.6.18",
"@tailwindcss/postcss": "^4.1.18",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"ajv": "^8.18.0",
"autoprefixer": "^10.4.23",
"eslint": "^9",
"eslint-config-next": "16.1.1",
"postcss": "^8.5.6",
"storybook": "^8.6.18",
"tailwindcss": "^4.1.18",
"typescript": "^5"
}
}
+6
View File
@@ -0,0 +1,6 @@
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;
+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

+60
View File
@@ -0,0 +1,60 @@
'use client';
import { useState } from 'react';
export default function AdminPage() {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
const values = { title, content };
console.log('Success:', values);
// later: call API POST /api/posts
};
return (
<div className="max-w-2xl space-y-6">
<h1 className="text-2xl font-bold">🛠 Admin Panel</h1>
<form onSubmit={onSubmit} className="space-y-4">
{/* Title */}
<div>
<label className="block text-sm font-medium mb-1">
Title <span className="text-red-500">*</span>
</label>
<input
type="text"
required
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full rounded border px-3 py-2 focus:outline-none focus:ring focus:ring-black/20"
placeholder="Post title"
/>
</div>
{/* Content */}
<div>
<label className="block text-sm font-medium mb-1">Content</label>
<textarea
rows={6}
value={content}
onChange={(e) => setContent(e.target.value)}
className="w-full rounded border px-3 py-2 focus:outline-none focus:ring focus:ring-black/20"
placeholder="Write your post content here..."
/>
</div>
{/* Submit */}
<button
type="submit"
className="rounded bg-black px-5 py-2 text-white hover:bg-gray-800"
>
Save Post
</button>
</form>
</div>
);
}
@@ -0,0 +1,21 @@
import Link from 'next/link';
import { ArrowLeft } from 'lucide-react';
export default function BlogNotFound() {
return (
<div className="flex flex-col items-center justify-center space-y-6 py-20 text-center">
<h1 className="text-4xl font-bold">Post Not Found</h1>
<p className="max-w-md text-muted-foreground">
The blog post you are looking for does not exist or may have been
removed.
</p>
<Link
href="/blog"
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/80"
>
<ArrowLeft className="h-4 w-4" />
Back to Blog
</Link>
</div>
);
}
+108
View File
@@ -0,0 +1,108 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { ArrowLeft, Calendar, Clock } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { getPostBySlug, getAllPosts } from '@/features/blog/data';
interface BlogDetailPageProps {
params: Promise<{ slug: string }>;
}
export async function generateMetadata({
params,
}: BlogDetailPageProps): Promise<Metadata> {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) {
return { title: 'Post Not Found' };
}
return {
title: post.title,
description: post.description,
openGraph: {
title: `${post.title} | Code Journey`,
description: post.description,
type: 'article',
publishedTime: post.publishDate,
tags: post.tags,
},
};
}
export async function generateStaticParams() {
const posts = getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
export default async function BlogDetailPage({ params }: BlogDetailPageProps) {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) {
notFound();
}
return (
<article className="mx-auto max-w-3xl space-y-8">
{/* Back link */}
<Link
href="/blog"
className="-ml-4 inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
<ArrowLeft className="h-4 w-4" />
Back to Blog
</Link>
{/* Header */}
<header className="space-y-4">
<h1 className="text-4xl font-bold tracking-tight">{post.title}</h1>
<p className="text-lg text-muted-foreground">{post.description}</p>
{/* Meta */}
<div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
{formatDate(post.publishDate)}
</span>
<span className="flex items-center gap-1">
<Clock className="h-4 w-4" />
{post.readingTime}
</span>
</div>
{/* Tags */}
<div className="flex flex-wrap gap-2">
{post.tags.map((tag) => (
<Badge key={tag} variant="secondary">
{tag}
</Badge>
))}
</div>
</header>
{/* Divider */}
<hr className="border-border" />
{/* Content */}
<div className="prose prose-neutral dark:prose-invert max-w-none">
{post.content.split('\n\n').map((paragraph, index) => (
<p key={index} className="leading-7 [&:not(:first-child)]:mt-6">
{paragraph}
</p>
))}
</div>
</article>
);
}
+46
View File
@@ -0,0 +1,46 @@
import type { Metadata } from 'next';
import { getAllPosts } from '@/features/blog/data';
import { BlogCard } from '@/components/molecules/BlogCard';
export const metadata: Metadata = {
title: 'Blog',
description:
'Articles about Java, Spring Boot, Next.js, and real-world software engineering practices.',
openGraph: {
title: 'Blog | Code Journey',
description:
'Articles about Java, Spring Boot, Next.js, and real-world software engineering practices.',
},
};
export default function BlogPage() {
const posts = getAllPosts();
return (
<section className="space-y-8">
{/* Page header */}
<div className="space-y-2">
<h1 className="text-3xl font-bold tracking-tight">Blog</h1>
<p className="text-muted-foreground">
Thoughts on software engineering, architecture, and the tools I use
every day.
</p>
</div>
{/* Post grid */}
<div className="grid gap-6 sm:grid-cols-2">
{posts.map((post) => (
<BlogCard
key={post.slug}
slug={post.slug}
title={post.title}
description={post.description}
publishDate={post.publishDate}
tags={post.tags}
readingTime={post.readingTime}
/>
))}
</div>
</section>
);
}
+129
View File
@@ -0,0 +1,129 @@
@import 'tailwindcss';
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-heading: var(--font-sans);
--font-sans: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}
+65
View File
@@ -0,0 +1,65 @@
import './globals.css';
import type { Metadata } from 'next';
import Link from 'next/link';
import { Geist } from 'next/font/google';
import { cn } from '@/lib/utils';
import { ThemeProvider } from '@/components/atoms/ThemeProvider';
import { ThemeToggle } from '@/components/atoms/ThemeToggle';
const geist = Geist({ subsets: ['latin'], variable: '--font-sans' });
export const metadata: Metadata = {
title: {
default: 'Code Journey',
template: '%s | Code Journey',
},
description:
'Personal blog about software engineering — Java, Spring Boot, Next.js, and real-world development practices.',
openGraph: {
title: 'Code Journey',
description:
'Personal blog about software engineering — Java, Spring Boot, Next.js, and real-world development practices.',
type: 'website',
locale: 'en_US',
siteName: 'Code Journey',
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning className={cn('font-sans', geist.variable)}>
<body className="min-h-screen bg-background text-foreground">
<ThemeProvider>
<header className="sticky top-0 z-50 border-b bg-background/80 backdrop-blur-sm">
<nav className="mx-auto flex max-w-5xl items-center justify-between px-6 py-4">
<div className="flex items-center gap-6">
<Link href="/" className="text-lg font-bold">
Code Journey
</Link>
<Link
href="/blog"
className="text-muted-foreground transition-colors hover:text-foreground"
>
Blog
</Link>
<Link
href="/about"
className="text-muted-foreground transition-colors hover:text-foreground"
>
About
</Link>
</div>
<ThemeToggle />
</nav>
</header>
<main className="mx-auto max-w-5xl px-6 py-8">{children}</main>
</ThemeProvider>
</body>
</html>
);
}
+186
View File
@@ -0,0 +1,186 @@
import React from 'react';
import { Mail, Github, Linkedin } from 'lucide-react';
export default function HomePage() {
return (
<main className="max-w-4xl mx-auto px-4 py-12 space-y-20">
{/* Hero Section */}
<section className="space-y-4 text-center">
<h1 className="text-5xl font-bold text-gray-900">👋 Hi, I'm An</h1>
<p className="text-lg text-gray-700 max-w-2xl mx-auto">
I'm a Web Developer passionate about building scalable backends and
beautiful frontends. I write about <strong>Java</strong>,{' '}
<strong>Spring Boot</strong>, <strong>Next.js</strong>, and real-world{' '}
<strong>software engineering</strong> practices.
</p>
<a
href="/blog"
className="inline-block rounded bg-black px-6 py-3 text-white font-medium hover:bg-gray-800 transition"
>
📚 Read My Blog
</a>
</section>
{/* About Section */}
<section className="space-y-4">
<h2 className="text-3xl font-semibold text-gray-900">About Me</h2>
<p className="text-gray-700 text-base">
With a strong foundation in backend development and a deep
appreciation for great UI/UX, I love solving real-world problems
through code. I'm especially interested in clean architecture,
developer experience, and performance optimization.
</p>
</section>
{/* Tech Stack Section */}
<section className="space-y-6">
<h2 className="text-3xl font-semibold text-gray-900">🛠 Tech Stack</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4 text-center">
{[
'Java',
'Spring Boot',
'Next.js',
'React',
'Tailwind CSS',
'PostgreSQL',
'Docker',
'Git',
'REST APIs',
].map((tech) => (
<div
key={tech}
className="bg-gray-100 rounded px-4 py-3 font-medium text-gray-800"
>
{tech}
</div>
))}
</div>
</section>
{/* Contact Section */}
<section className="space-y-6">
<h2 className="text-3xl font-semibold text-gray-900">📬 Contact Me</h2>
<p className="text-gray-700">
Whether you want to collaborate, have a question, or just want to say
hi — my inbox is open!
</p>
<div className="flex space-x-4">
<a
href="mailto:an@example.com"
className="flex items-center space-x-2 text-gray-800 hover:text-black transition"
>
<Mail size={20} />
<span>Email</span>
</a>
<a
href="https://github.com/yourusername"
target="_blank"
rel="noopener noreferrer"
className="flex items-center space-x-2 text-gray-800 hover:text-black transition"
>
<Github size={20} />
<span>GitHub</span>
</a>
<a
href="https://linkedin.com/in/yourusername"
target="_blank"
rel="noopener noreferrer"
className="flex items-center space-x-2 text-gray-800 hover:text-black transition"
>
<Linkedin size={20} />
<span>LinkedIn</span>
</a>
</div>
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-pink-50 to-purple-100 p-4">
<form className="w-full max-w-md bg-white rounded-2xl shadow-xl p-8 space-y-6">
{/* Title */}
<div className="text-center space-y-1">
<h2 className="text-3xl font-bold text-gray-800">
Create Account
</h2>
<p className="text-gray-500 text-sm">
Join us and enjoy a beautiful experience ✨
</p>
</div>
{/* Name */}
<div className="space-y-1">
<label className="text-sm font-medium text-gray-600">
Full Name
</label>
<input
type="text"
placeholder="Your name"
className="w-full px-4 py-3 rounded-xl border border-gray-200
focus:outline-none focus:ring-2 focus:ring-pink-400
transition"
/>
</div>
{/* Email */}
<div className="space-y-1">
<label className="text-sm font-medium text-gray-600">
Email Address
</label>
<input
type="email"
placeholder="you@example.com"
className="w-full px-4 py-3 rounded-xl border border-gray-200
focus:outline-none focus:ring-2 focus:ring-pink-400
transition"
/>
</div>
{/* Password */}
<div className="space-y-1">
<label className="text-sm font-medium text-gray-600">
Password
</label>
<input
type="password"
placeholder="••••••••"
className="w-full px-4 py-3 rounded-xl border border-gray-200
focus:outline-none focus:ring-2 focus:ring-pink-400
transition"
/>
</div>
{/* Remember */}
<div className="flex items-center justify-between text-sm">
<label className="flex items-center gap-2 text-gray-600">
<input
type="checkbox"
className="rounded border-gray-300 text-pink-500 focus:ring-pink-400"
/>
Remember me
</label>
<a href="#" className="text-pink-500 hover:underline">
Forgot password?
</a>
</div>
{/* Submit */}
<button
type="submit"
className="w-full py-3 rounded-xl bg-gradient-to-r
from-pink-500 to-purple-500 text-white font-semibold
hover:opacity-90 transition shadow-lg"
>
Sign Up
</button>
{/* Footer */}
<p className="text-center text-sm text-gray-500">
Already have an account?{' '}
<a href="#" className="text-pink-500 font-medium hover:underline">
Sign in
</a>
</p>
</form>
</div>
</section>
</main>
);
}
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1,21 @@
'use client';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import type { ReactNode } from 'react';
interface ThemeProviderProps {
children: ReactNode;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</NextThemesProvider>
);
}
@@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ThemeToggle } from './ThemeToggle';
import { ThemeProvider } from './ThemeProvider';
const meta = {
title: 'Atoms/ThemeToggle',
component: ThemeToggle,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [
(Story) => (
<ThemeProvider>
<Story />
</ThemeProvider>
),
],
} satisfies Meta<typeof ThemeToggle>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
@@ -0,0 +1,40 @@
'use client';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
import { Moon, Sun } from 'lucide-react';
import { Button } from '@/components/ui/button';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<Button variant="ghost" size="icon" aria-label="Toggle theme">
<Sun className="h-5 w-5" />
</Button>
);
}
const isDark = theme === 'dark';
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(isDark ? 'light' : 'dark')}
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
{isDark ? (
<Sun className="h-5 w-5 transition-transform duration-200" />
) : (
<Moon className="h-5 w-5 transition-transform duration-200" />
)}
</Button>
);
}
@@ -0,0 +1,48 @@
import type { Meta, StoryObj } from '@storybook/react';
import { BlogCard } from './BlogCard';
const meta = {
title: 'Molecules/BlogCard',
component: BlogCard,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof BlogCard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
slug: 'example-post',
title: 'An Example Blog Post',
description: 'This is a brief summary of the example blog post to show how it looks in the card component.',
publishDate: '2026-04-03',
tags: ['Next.js', 'React', 'Storybook'],
readingTime: '5 min read',
},
decorators: [
(Story) => (
<div style={{ width: '400px' }}>
<Story />
</div>
),
],
};
export const LongTitle: Story = {
args: {
...Default.args,
title: 'This is a Very Long Title Which Should Probably Wrap or Be Truncated Depending on the Container Size and CSS',
},
decorators: Default.decorators,
};
export const ManyTags: Story = {
args: {
...Default.args,
tags: ['React', 'Next.js', 'Storybook', 'Tailwind', 'CSS', 'Frontend', 'Web Dev'],
},
decorators: Default.decorators,
};
@@ -0,0 +1,74 @@
import Link from 'next/link';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Calendar, Clock } from 'lucide-react';
interface BlogCardProps {
slug: string;
title: string;
description: string;
publishDate: string;
tags: string[];
readingTime: string;
}
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
export function BlogCard({
slug,
title,
description,
publishDate,
tags,
readingTime,
}: BlogCardProps) {
return (
<Link href={`/blog/${slug}`} className="group block">
<Card className="h-full transition-all duration-200 group-hover:shadow-lg group-hover:border-primary/20">
<CardHeader className="space-y-2">
<CardTitle className="text-xl leading-tight group-hover:text-primary transition-colors">
{title}
</CardTitle>
<CardDescription className="line-clamp-2">
{description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{/* Tags */}
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<Badge key={tag} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
</div>
{/* Meta */}
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Calendar className="h-3.5 w-3.5" />
{formatDate(publishDate)}
</span>
<span className="flex items-center gap-1">
<Clock className="h-3.5 w-3.5" />
{readingTime}
</span>
</div>
</CardContent>
</Card>
</Link>
);
}
@@ -0,0 +1 @@
export { BlogCard } from './BlogCard';
+52
View File
@@ -0,0 +1,52 @@
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
secondary:
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive:
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost:
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
render,
...props
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
return useRender({
defaultTagName: "span",
props: mergeProps<"span">(
{
className: cn(badgeVariants({ variant }), className),
},
props
),
render,
state: {
slot: "badge",
variant,
},
})
}
export { Badge, badgeVariants }
@@ -0,0 +1,42 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './button';
const meta = {
title: 'UI/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'Button',
variant: 'default',
},
};
export const Outline: Story = {
args: {
children: 'Outline Button',
variant: 'outline',
},
};
export const Ghost: Story = {
args: {
children: 'Ghost Button',
variant: 'ghost',
},
};
export const Destructive: Story = {
args: {
children: 'Destructive Button',
variant: 'destructive',
},
};
+60
View File
@@ -0,0 +1,60 @@
"use client"
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
+103
View File
@@ -0,0 +1,103 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
+20
View File
@@ -0,0 +1,20 @@
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }
+1
View File
@@ -0,0 +1 @@
+21
View File
@@ -0,0 +1,21 @@
import { api } from "@/lib/axios";
/**
* Login with email and password.
*/
export async function login(email: string, password: string) {
const response = await api.post("/auth/login", { email, password });
return response.data;
}
/**
* Register a new user account.
*/
export async function register(data: {
name: string;
email: string;
password: string;
}) {
const response = await api.post("/auth/register", data);
return response.data;
}
+150
View File
@@ -0,0 +1,150 @@
import type { BlogPost } from '@/features/blog/types';
/**
* Mock blog data for Phase 1 MVP.
* Will be replaced with API calls in a future phase.
*/
export const MOCK_BLOG_POSTS: BlogPost[] = [
{
slug: 'understanding-react-server-components',
title: 'Understanding React Server Components',
description:
'A deep dive into how React Server Components work in Next.js App Router and why they change the way we think about data fetching.',
publishDate: '2026-03-28',
tags: ['React', 'Next.js', 'Server Components'],
readingTime: '8 min read',
content: `React Server Components (RSC) represent a fundamental shift in how we build React applications. Unlike traditional client components that run in the browser, server components execute on the server and send only the rendered output to the client.
In Next.js App Router, every component is a server component by default. This means you can directly fetch data, access backend resources, and keep sensitive logic on the server without shipping extra JavaScript to the client.
One of the biggest advantages is performance. Because server components do not include their JavaScript in the client bundle, your application ships less code to the browser. This results in faster initial page loads and better Core Web Vitals scores.
To use client-side interactivity, you opt in by adding the 'use client' directive at the top of a file. This tells Next.js to include that component in the client bundle. The key is to push client directives as far down the component tree as possible.
Server components can import and render client components, but not the other way around. This creates a clear boundary between server and client code that helps you reason about where your code executes.
Data fetching in server components is straightforward. You can use async/await directly in the component body, fetch from APIs, query databases, or read from the file system. There is no need for useEffect or client-side data fetching libraries for initial page data.
Understanding this model is essential for any developer working with Next.js App Router. It changes how you structure components, manage state, and think about the boundary between server and client.`,
},
{
slug: 'clean-architecture-spring-boot',
title: 'Clean Architecture with Spring Boot',
description:
'How to structure a Spring Boot application using Clean Architecture principles for maintainability and testability.',
publishDate: '2026-03-20',
tags: ['Java', 'Spring Boot', 'Architecture'],
readingTime: '12 min read',
content: `Clean Architecture, popularized by Robert C. Martin, is a software design philosophy that separates concerns into distinct layers. When applied to Spring Boot, it creates applications that are easy to test, maintain, and evolve over time.
The core idea is the Dependency Rule: source code dependencies must point inward toward higher-level policies. Your business logic should not depend on frameworks, databases, or external services. Instead, those outer layers depend on the inner layers through interfaces.
In a Spring Boot context, the typical layers are: Entities (domain models), Use Cases (application services), Interface Adapters (controllers, repositories), and Frameworks (Spring Boot itself, database drivers, HTTP clients).
The domain layer contains your business entities and rules. These are plain Java objects with no Spring annotations. They represent the core concepts of your application and contain business validation logic.
Use cases orchestrate the flow of data between entities and define application-specific business rules. Each use case class has a single responsibility and is injected with repository interfaces, not concrete implementations.
The adapter layer translates between the format used by use cases and the format needed by external systems. REST controllers convert HTTP requests to use case inputs. Repository implementations convert database results to domain entities.
Spring Boot sits in the outermost layer. It provides dependency injection, auto-configuration, and web server infrastructure. The key insight is that removing Spring Boot should not require changing your business logic.
Testing becomes straightforward with this approach. You can unit test domain logic without Spring. You can test use cases with mock repositories. Integration tests verify the adapters work correctly with real infrastructure.
This architecture requires more upfront effort but pays dividends as the application grows. Changes to the database, API format, or framework version affect only the outer layers, leaving business logic untouched.`,
},
{
slug: 'tailwind-css-best-practices',
title: 'Tailwind CSS Best Practices for Large Projects',
description:
'Practical patterns and conventions for scaling Tailwind CSS in production applications without creating a maintenance nightmare.',
publishDate: '2026-03-15',
tags: ['CSS', 'Tailwind', 'Frontend'],
readingTime: '6 min read',
content: `Tailwind CSS is incredibly productive for building UIs, but without discipline it can lead to hard-to-maintain templates full of long class strings. Here are practical patterns that keep Tailwind projects clean as they scale.
First, extract components aggressively. When you find yourself repeating the same combination of utility classes, create a reusable component. In React, this means creating a component file. In server-rendered apps, this means using partials or includes.
Use a consistent ordering convention for utility classes. A common approach is: layout, sizing, spacing, typography, colors, effects. Tools like the Tailwind Prettier plugin can automate this sorting for you.
Leverage the design token system. Define your color palette, spacing scale, and typography in the Tailwind configuration. Avoid using arbitrary values like w-[347px] unless absolutely necessary. Consistent tokens create visual harmony.
For dark mode, use the class strategy with next-themes in Next.js projects. This gives you full control over when dark mode activates and allows users to choose their preference. Define both light and dark variants using CSS custom properties.
Component libraries like shadcn/ui provide well-designed, accessible components that use Tailwind internally. They give you a head start on common UI patterns while remaining fully customizable since the code lives in your project.
Keep responsive design systematic. Use mobile-first breakpoints and test at each breakpoint during development. Create layout components that handle responsive behavior so page components remain clean.
Finally, use Tailwind Merge and clsx for dynamic class composition. The cn() utility function combines both tools and is essential for components that accept className as a prop. It prevents class conflicts and allows overrides.`,
},
{
slug: 'docker-for-developers',
title: 'Docker Fundamentals for Web Developers',
description:
'Everything you need to know about Docker to containerize your web applications and set up reliable development environments.',
publishDate: '2026-03-10',
tags: ['Docker', 'DevOps', 'Backend'],
readingTime: '10 min read',
content: `Docker has become an essential tool for web developers. It solves the "works on my machine" problem by packaging your application with its exact dependencies into a portable container.
A Docker image is a read-only template that contains your application code, runtime, libraries, and system tools. You define it with a Dockerfile, which is a series of instructions that build the image layer by layer.
For a typical Node.js application, your Dockerfile starts with a base image like node:20-alpine, copies your package files, runs npm install, copies your source code, and sets the start command. Each instruction creates a new layer that Docker caches for faster rebuilds.
Multi-stage builds are essential for production images. You use one stage to build your application and a separate, minimal stage to run it. This keeps your production image small by excluding build tools and devDependencies.
Docker Compose simplifies multi-container setups. A docker-compose.yml file defines your application, database, cache, and other services. Running docker compose up starts everything with the correct networking and environment variables.
Volumes allow you to persist data and share files between your host machine and containers. For development, you mount your source code as a volume so changes are reflected immediately without rebuilding the image.
Environment variables configure your application per environment. Docker supports .env files, command-line variables, and secrets management. Never hardcode configuration values in your Dockerfile or application code.
Health checks tell Docker whether your container is functioning correctly. Define a health check command that verifies your application can serve requests. Docker uses this to restart unhealthy containers and route traffic appropriately.
Understanding Docker fundamentals makes you more effective in modern development teams. Most CI/CD pipelines, cloud platforms, and orchestration tools assume containerized applications.`,
},
{
slug: 'typescript-advanced-patterns',
title: 'Advanced TypeScript Patterns You Should Know',
description:
'Practical TypeScript patterns including discriminated unions, template literal types, and conditional types that improve code safety.',
publishDate: '2026-03-05',
tags: ['TypeScript', 'Frontend', 'Patterns'],
readingTime: '7 min read',
content: `TypeScript offers powerful type system features beyond basic annotations. Mastering these patterns helps you write safer code and catch more bugs at compile time rather than runtime.
Discriminated unions are one of the most useful patterns. By adding a literal type property to each variant of a union, TypeScript can narrow the type in switch statements and if blocks. This is perfect for modeling states like loading, success, and error.
Template literal types let you create string types from combinations. You can define types like event names, CSS class patterns, or API endpoint paths that TypeScript validates at compile time. Combined with mapped types, they enable powerful type transformations.
Conditional types use the extends keyword to create types that depend on other types. The syntax Type extends Condition ? TrueType : FalseType mirrors a ternary expression. This is the foundation for utility types like Extract, Exclude, and ReturnType.
The infer keyword works within conditional types to extract and name parts of a type. You can infer function parameter types, return types, array element types, and promise resolved types. This is essential for building generic utility types.
Generic constraints with extends ensure that type parameters meet certain requirements. Instead of accepting any type, you can require that a generic type has specific properties, extends a base type, or satisfies an interface.
Branded types add compile-time safety to primitive values. By intersecting a primitive with a unique symbol type, you can create distinct types for concepts like UserId, EmailAddress, or Currency that prevent accidental mixing.
The satisfies operator validates that a value matches a type without widening it. This is useful when you want TypeScript to check your object against an interface while preserving the literal types of its properties for autocompletion.
These patterns have real-world applications in API response handling, state management, form validation, and component props. Start with discriminated unions and gradually incorporate more advanced patterns as your comfort grows.`,
},
];
/**
* Get all blog posts, sorted by publish date (newest first).
*/
export function getAllPosts(): BlogPost[] {
return [...MOCK_BLOG_POSTS].sort(
(a, b) =>
new Date(b.publishDate).getTime() - new Date(a.publishDate).getTime()
);
}
/**
* Get a single blog post by slug.
* Returns undefined if not found.
*/
export function getPostBySlug(slug: string): BlogPost | undefined {
return MOCK_BLOG_POSTS.find((post) => post.slug === slug);
}
+22
View File
@@ -0,0 +1,22 @@
/**
* Blog feature type definitions.
*/
export interface BlogPost {
/** URL-friendly unique identifier */
slug: string;
/** Post title */
title: string;
/** Short summary shown in cards and meta descriptions */
description: string;
/** ISO date string of publication */
publishDate: string;
/** Categorization tags */
tags: string[];
/** Estimated reading time (e.g. "5 min read") */
readingTime: string;
/** Optional cover image URL */
coverImage?: string;
/** Full article content (plain text paragraphs) */
content: string;
}
+25
View File
@@ -0,0 +1,25 @@
import { api } from "@/lib/axios";
/**
* Fetch all posts.
*/
export async function getPosts() {
const response = await api.get("/posts");
return response.data;
}
/**
* Fetch a single post by slug.
*/
export async function getPostBySlug(slug: string) {
const response = await api.get(`/posts/${slug}`);
return response.data;
}
/**
* Create a new post.
*/
export async function createPost(data: { title: string; content: string }) {
const response = await api.post("/posts", data);
return response.data;
}
+5
View File
@@ -0,0 +1,5 @@
import axios from "axios";
export const api = axios.create({
baseURL: "http://localhost:8080/api",
});
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+7
View File
@@ -0,0 +1,7 @@
/**
* Shared type definitions used across the application.
*
* Add common interfaces, type aliases, and enums here.
*/
export {};
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svgjs="http://svgjs.com/svgjs" xmlns:xlink="http://www.w3.org/1999/xlink" width="164" height="164" version="1.1"><svg xmlns="http://www.w3.org/2000/svg" width="164" height="164" fill="none" viewBox="0 0 164 164"><path fill="#FF4785" d="M22.467 147.762 17.5 15.402a8.062 8.062 0 0 1 7.553-8.35L137.637.016a8.061 8.061 0 0 1 8.565 8.047v144.23a8.063 8.063 0 0 1-8.424 8.054l-107.615-4.833a8.062 8.062 0 0 1-7.695-7.752Z"/><path fill="#fff" fill-rule="evenodd" d="m128.785.57-15.495.968-.755 18.172a1.203 1.203 0 0 0 1.928 1.008l7.06-5.354 5.962 4.697a1.202 1.202 0 0 0 1.946-.987L128.785.569Zm-12.059 60.856c-2.836 2.203-23.965 3.707-23.965.57.447-11.969-4.912-12.494-7.889-12.494-2.828 0-7.59.855-7.59 7.267 0 6.534 6.96 10.223 15.13 14.553 11.607 6.15 25.654 13.594 25.654 32.326 0 17.953-14.588 27.871-33.194 27.871-19.201 0-35.981-7.769-34.086-34.702.744-3.163 25.156-2.411 25.156 0-.298 11.114 2.232 14.383 8.633 14.383 4.912 0 7.144-2.708 7.144-7.267 0-6.9-7.252-10.973-15.595-15.657C64.827 81.933 51.53 74.468 51.53 57.34c0-17.098 11.76-28.497 32.747-28.497 20.988 0 32.449 11.224 32.449 32.584Z" clip-rule="evenodd"/></svg><style>@media (prefers-color-scheme:light){:root{filter:none}}</style></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+169
View File
@@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>@storybook/core - Storybook</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<style>
@font-face {
font-family: 'Nunito Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('./sb-common-assets/nunito-sans-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Nunito Sans';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('./sb-common-assets/nunito-sans-italic.woff2') format('woff2');
}
@font-face {
font-family: 'Nunito Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('./sb-common-assets/nunito-sans-bold.woff2') format('woff2');
}
@font-face {
font-family: 'Nunito Sans';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('./sb-common-assets/nunito-sans-bold-italic.woff2') format('woff2');
}
</style>
<link href="./sb-manager/runtime.js" rel="modulepreload" />
<link href="./sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/essentials-controls-1/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/essentials-actions-2/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/essentials-docs-3/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/essentials-backgrounds-4/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/essentials-viewport-5/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/essentials-toolbars-6/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/essentials-measure-7/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/essentials-outline-8/manager-bundle.js" rel="modulepreload" />
<style>
#storybook-root[hidden] {
display: none !important;
}
</style>
</head>
<body>
<div id="root"></div>
<script>
window['FEATURES'] = {
"argTypeTargetsV7": true,
"legacyDecoratorFileOrder": false,
"disallowImplicitActionsInRenderV8": true
};
window['REFS'] = {};
window['LOGLEVEL'] = "info";
window['DOCS_OPTIONS'] = {
"defaultName": "Docs",
"autodocs": "tag"
};
window['CONFIG_TYPE'] = "PRODUCTION";
window['TAGS_OPTIONS'] = {
"dev-only": {
"excludeFromDocsStories": true
},
"docs-only": {
"excludeFromSidebar": true
},
"test-only": {
"excludeFromSidebar": true,
"excludeFromDocsStories": true
}
};
window['STORYBOOK_RENDERER'] = "react";
window['STORYBOOK_BUILDER'] = "@storybook/builder-webpack5";
window['STORYBOOK_FRAMEWORK'] = "@storybook/nextjs";
</script>
<script type="module">
import './sb-manager/globals-runtime.js';
import './sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js';
import './sb-addons/essentials-controls-1/manager-bundle.js';
import './sb-addons/essentials-actions-2/manager-bundle.js';
import './sb-addons/essentials-docs-3/manager-bundle.js';
import './sb-addons/essentials-backgrounds-4/manager-bundle.js';
import './sb-addons/essentials-viewport-5/manager-bundle.js';
import './sb-addons/essentials-toolbars-6/manager-bundle.js';
import './sb-addons/essentials-measure-7/manager-bundle.js';
import './sb-addons/essentials-outline-8/manager-bundle.js';
import './sb-manager/runtime.js';
</script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
{"v":5,"entries":{"atoms-themetoggle--docs":{"id":"atoms-themetoggle--docs","title":"Atoms/ThemeToggle","name":"Docs","importPath":"./src/components/atoms/ThemeToggle.stories.tsx","type":"docs","tags":["dev","test","autodocs"],"storiesImports":[]},"atoms-themetoggle--default":{"type":"story","id":"atoms-themetoggle--default","name":"Default","title":"Atoms/ThemeToggle","importPath":"./src/components/atoms/ThemeToggle.stories.tsx","componentPath":"./src/components/atoms/ThemeToggle.tsx","tags":["dev","test","autodocs"]},"molecules-blogcard--docs":{"id":"molecules-blogcard--docs","title":"Molecules/BlogCard","name":"Docs","importPath":"./src/components/molecules/BlogCard.stories.tsx","type":"docs","tags":["dev","test","autodocs"],"storiesImports":[]},"molecules-blogcard--default":{"type":"story","id":"molecules-blogcard--default","name":"Default","title":"Molecules/BlogCard","importPath":"./src/components/molecules/BlogCard.stories.tsx","componentPath":"./src/components/molecules/BlogCard.tsx","tags":["dev","test","autodocs"]},"molecules-blogcard--long-title":{"type":"story","id":"molecules-blogcard--long-title","name":"Long Title","title":"Molecules/BlogCard","importPath":"./src/components/molecules/BlogCard.stories.tsx","componentPath":"./src/components/molecules/BlogCard.tsx","tags":["dev","test","autodocs"]},"molecules-blogcard--many-tags":{"type":"story","id":"molecules-blogcard--many-tags","name":"Many Tags","title":"Molecules/BlogCard","importPath":"./src/components/molecules/BlogCard.stories.tsx","componentPath":"./src/components/molecules/BlogCard.tsx","tags":["dev","test","autodocs"]},"ui-button--docs":{"id":"ui-button--docs","title":"UI/Button","name":"Docs","importPath":"./src/components/ui/button.stories.tsx","type":"docs","tags":["dev","test","autodocs"],"storiesImports":[]},"ui-button--default":{"type":"story","id":"ui-button--default","name":"Default","title":"UI/Button","importPath":"./src/components/ui/button.stories.tsx","componentPath":"./src/components/ui/button.tsx","tags":["dev","test","autodocs"]},"ui-button--outline":{"type":"story","id":"ui-button--outline","name":"Outline","title":"UI/Button","importPath":"./src/components/ui/button.stories.tsx","componentPath":"./src/components/ui/button.tsx","tags":["dev","test","autodocs"]},"ui-button--ghost":{"type":"story","id":"ui-button--ghost","name":"Ghost","title":"UI/Button","importPath":"./src/components/ui/button.stories.tsx","componentPath":"./src/components/ui/button.tsx","tags":["dev","test","autodocs"]},"ui-button--destructive":{"type":"story","id":"ui-button--destructive","name":"Destructive","title":"UI/Button","importPath":"./src/components/ui/button.stories.tsx","componentPath":"./src/components/ui/button.tsx","tags":["dev","test","autodocs"]}}}
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
try{
(()=>{var b=__STORYBOOK_API__,{ActiveTabs:h,Consumer:g,ManagerContext:f,Provider:A,RequestResponseError:v,addons:n,combineParameters:x,controlOrMetaKey:P,controlOrMetaSymbol:R,eventMatchesShortcut:E,eventToShortcut:M,experimental_MockUniversalStore:k,experimental_UniversalStore:B,experimental_requestResponse:C,experimental_useUniversalStore:I,isMacLike:K,isShortcutTaken:L,keyToSymbol:U,merge:Y,mockChannel:w,optionOrAltSymbol:N,shortcutMatchesShortcut:D,shortcutToHumanString:G,types:q,useAddonState:F,useArgTypes:H,useArgs:j,useChannel:V,useGlobalTypes:z,useGlobals:J,useParameter:Q,useSharedState:W,useStoryPrepared:X,useStorybookApi:Z,useStorybookState:$}=__STORYBOOK_API__;var S=(()=>{let e;return typeof window<"u"?e=window:typeof globalThis<"u"?e=globalThis:typeof window<"u"?e=window:typeof self<"u"?e=self:e={},e})(),c="tag-filters",p="static-filter";n.register(c,e=>{let u=Object.entries(S.TAGS_OPTIONS??{}).reduce((t,r)=>{let[o,i]=r;return i.excludeFromSidebar&&(t[o]=!0),t},{});e.experimental_setFilter(p,t=>{let r=t.tags??[];return(r.includes("dev")||t.type==="docs")&&r.filter(o=>u[o]).length===0})});})();
}catch(e){ console.error("[Storybook] One of your manager-entries failed: " + import.meta.url, e); }
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svgjs="http://svgjs.com/svgjs" xmlns:xlink="http://www.w3.org/1999/xlink" width="164" height="164" version="1.1"><svg xmlns="http://www.w3.org/2000/svg" width="164" height="164" fill="none" viewBox="0 0 164 164"><path fill="#FF4785" d="M22.467 147.762 17.5 15.402a8.062 8.062 0 0 1 7.553-8.35L137.637.016a8.061 8.061 0 0 1 8.565 8.047v144.23a8.063 8.063 0 0 1-8.424 8.054l-107.615-4.833a8.062 8.062 0 0 1-7.695-7.752Z"/><path fill="#fff" fill-rule="evenodd" d="m128.785.57-15.495.968-.755 18.172a1.203 1.203 0 0 0 1.928 1.008l7.06-5.354 5.962 4.697a1.202 1.202 0 0 0 1.946-.987L128.785.569Zm-12.059 60.856c-2.836 2.203-23.965 3.707-23.965.57.447-11.969-4.912-12.494-7.889-12.494-2.828 0-7.59.855-7.59 7.267 0 6.534 6.96 10.223 15.13 14.553 11.607 6.15 25.654 13.594 25.654 32.326 0 17.953-14.588 27.871-33.194 27.871-19.201 0-35.981-7.769-34.086-34.702.744-3.163 25.156-2.411 25.156 0-.298 11.114 2.232 14.383 8.633 14.383 4.912 0 7.144-2.708 7.144-7.267 0-6.9-7.252-10.973-15.595-15.657C64.827 81.933 51.53 74.468 51.53 57.34c0-17.098 11.76-28.497 32.747-28.497 20.988 0 32.449 11.224 32.449 32.584Z" clip-rule="evenodd"/></svg><style>@media (prefers-color-scheme:light){:root{filter:none}}</style></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,48 @@
import ESM_COMPAT_Module from "node:module";
import { fileURLToPath as ESM_COMPAT_fileURLToPath } from 'node:url';
import { dirname as ESM_COMPAT_dirname } from 'node:path';
const __filename = ESM_COMPAT_fileURLToPath(import.meta.url);
const __dirname = ESM_COMPAT_dirname(__filename);
const require = ESM_COMPAT_Module.createRequire(import.meta.url);
// src/manager/globals/globals.ts
var _ = {
react: "__REACT__",
"react-dom": "__REACT_DOM__",
"react-dom/client": "__REACT_DOM_CLIENT__",
"@storybook/icons": "__STORYBOOK_ICONS__",
"storybook/internal/manager-api": "__STORYBOOK_API__",
"@storybook/manager-api": "__STORYBOOK_API__",
"@storybook/core/manager-api": "__STORYBOOK_API__",
"storybook/internal/components": "__STORYBOOK_COMPONENTS__",
"@storybook/components": "__STORYBOOK_COMPONENTS__",
"@storybook/core/components": "__STORYBOOK_COMPONENTS__",
"storybook/internal/channels": "__STORYBOOK_CHANNELS__",
"@storybook/channels": "__STORYBOOK_CHANNELS__",
"@storybook/core/channels": "__STORYBOOK_CHANNELS__",
"storybook/internal/core-errors": "__STORYBOOK_CORE_EVENTS__",
"@storybook/core-events": "__STORYBOOK_CORE_EVENTS__",
"@storybook/core/core-events": "__STORYBOOK_CORE_EVENTS__",
"storybook/internal/manager-errors": "__STORYBOOK_CORE_EVENTS_MANAGER_ERRORS__",
"@storybook/core-events/manager-errors": "__STORYBOOK_CORE_EVENTS_MANAGER_ERRORS__",
"@storybook/core/manager-errors": "__STORYBOOK_CORE_EVENTS_MANAGER_ERRORS__",
"storybook/internal/router": "__STORYBOOK_ROUTER__",
"@storybook/router": "__STORYBOOK_ROUTER__",
"@storybook/core/router": "__STORYBOOK_ROUTER__",
"storybook/internal/theming": "__STORYBOOK_THEMING__",
"@storybook/theming": "__STORYBOOK_THEMING__",
"@storybook/core/theming": "__STORYBOOK_THEMING__",
"storybook/internal/theming/create": "__STORYBOOK_THEMING_CREATE__",
"@storybook/theming/create": "__STORYBOOK_THEMING_CREATE__",
"@storybook/core/theming/create": "__STORYBOOK_THEMING_CREATE__",
"storybook/internal/client-logger": "__STORYBOOK_CLIENT_LOGGER__",
"@storybook/client-logger": "__STORYBOOK_CLIENT_LOGGER__",
"@storybook/core/client-logger": "__STORYBOOK_CLIENT_LOGGER__",
"storybook/internal/types": "__STORYBOOK_TYPES__",
"@storybook/types": "__STORYBOOK_TYPES__",
"@storybook/core/types": "__STORYBOOK_TYPES__"
}, o = Object.keys(_);
export {
o as globalPackages,
_ as globalsNameReferenceMap
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,33 @@
import ESM_COMPAT_Module from "node:module";
import { fileURLToPath as ESM_COMPAT_fileURLToPath } from 'node:url';
import { dirname as ESM_COMPAT_dirname } from 'node:path';
const __filename = ESM_COMPAT_fileURLToPath(import.meta.url);
const __dirname = ESM_COMPAT_dirname(__filename);
const require = ESM_COMPAT_Module.createRequire(import.meta.url);
// src/preview/globals/globals.ts
var _ = {
"@storybook/global": "__STORYBOOK_MODULE_GLOBAL__",
"storybook/internal/channels": "__STORYBOOK_MODULE_CHANNELS__",
"@storybook/channels": "__STORYBOOK_MODULE_CHANNELS__",
"@storybook/core/channels": "__STORYBOOK_MODULE_CHANNELS__",
"storybook/internal/client-logger": "__STORYBOOK_MODULE_CLIENT_LOGGER__",
"@storybook/client-logger": "__STORYBOOK_MODULE_CLIENT_LOGGER__",
"@storybook/core/client-logger": "__STORYBOOK_MODULE_CLIENT_LOGGER__",
"storybook/internal/core-events": "__STORYBOOK_MODULE_CORE_EVENTS__",
"@storybook/core-events": "__STORYBOOK_MODULE_CORE_EVENTS__",
"@storybook/core/core-events": "__STORYBOOK_MODULE_CORE_EVENTS__",
"storybook/internal/preview-errors": "__STORYBOOK_MODULE_CORE_EVENTS_PREVIEW_ERRORS__",
"@storybook/core-events/preview-errors": "__STORYBOOK_MODULE_CORE_EVENTS_PREVIEW_ERRORS__",
"@storybook/core/preview-errors": "__STORYBOOK_MODULE_CORE_EVENTS_PREVIEW_ERRORS__",
"storybook/internal/preview-api": "__STORYBOOK_MODULE_PREVIEW_API__",
"@storybook/preview-api": "__STORYBOOK_MODULE_PREVIEW_API__",
"@storybook/core/preview-api": "__STORYBOOK_MODULE_PREVIEW_API__",
"storybook/internal/types": "__STORYBOOK_MODULE_TYPES__",
"@storybook/types": "__STORYBOOK_MODULE_TYPES__",
"@storybook/core/types": "__STORYBOOK_MODULE_TYPES__"
}, O = Object.keys(_);
export {
O as globalPackages,
_ as globalsNameReferenceMap
};
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}