1
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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"]
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import axios from "axios";
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: "http://localhost:8080/api",
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Shared type definitions used across the application.
|
||||
*
|
||||
* Add common interfaces, type aliases, and enums here.
|
||||
*/
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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"]}}}
|
||||
@@ -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 |
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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"]
|
||||
}
|
||||