create-stackkit-app 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -7
- package/bin/create-stackkit.js +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/create-project.js +408 -139
- package/dist/lib/template-composer.js +22 -22
- package/modules/auth/better-auth-express/adapters/mongoose-mongodb.ts +13 -0
- package/modules/auth/better-auth-express/adapters/prisma-mongodb.ts +15 -0
- package/modules/auth/better-auth-express/adapters/prisma-postgresql.ts +15 -0
- package/modules/auth/better-auth-express/files/lib/auth.ts +1 -1
- package/modules/auth/better-auth-express/files/routes/auth.ts +3 -3
- package/modules/auth/better-auth-express/files/schemas/prisma-mongodb-schema.prisma +72 -0
- package/modules/auth/better-auth-express/files/schemas/prisma-postgresql-schema.prisma +72 -0
- package/modules/auth/better-auth-express/module.json +26 -3
- package/modules/auth/better-auth-nextjs/adapters/mongoose-mongodb.ts +24 -0
- package/modules/auth/better-auth-nextjs/adapters/prisma-mongodb.ts +26 -0
- package/modules/auth/better-auth-nextjs/adapters/prisma-postgresql.ts +26 -0
- package/modules/auth/better-auth-nextjs/files/api/auth/[...all]/route.ts +2 -3
- package/modules/auth/better-auth-nextjs/files/lib/auth.ts +4 -4
- package/modules/auth/better-auth-nextjs/files/schemas/prisma-mongodb-schema.prisma +72 -0
- package/modules/auth/better-auth-nextjs/files/schemas/prisma-postgresql-schema.prisma +72 -0
- package/modules/auth/better-auth-nextjs/module.json +26 -5
- package/modules/auth/better-auth-react/files/lib/auth-client.ts +2 -2
- package/modules/auth/better-auth-react/module.json +7 -5
- package/modules/auth/clerk-express/files/lib/auth.ts +1 -1
- package/modules/auth/clerk-express/module.json +23 -8
- package/modules/auth/clerk-nextjs/files/lib/auth-provider.tsx +1 -1
- package/modules/auth/clerk-nextjs/files/middleware.ts +3 -3
- package/modules/auth/clerk-nextjs/module.json +51 -14
- package/modules/auth/clerk-react/files/lib/auth-provider.tsx +2 -2
- package/modules/auth/clerk-react/module.json +17 -7
- package/modules/database/mongoose-mongodb/files/lib/db.ts +3 -3
- package/modules/database/mongoose-mongodb/module.json +44 -6
- package/modules/database/prisma-mongodb/files/lib/db.ts +2 -2
- package/modules/database/prisma-mongodb/files/prisma/schema.prisma +1 -1
- package/modules/database/prisma-mongodb/module.json +28 -4
- package/modules/database/prisma-postgresql/files/lib/db.ts +2 -2
- package/modules/database/prisma-postgresql/files/prisma/schema.prisma +1 -1
- package/modules/database/prisma-postgresql/module.json +28 -4
- package/package.json +8 -3
- package/templates/express/.env.example +11 -0
- package/templates/express/eslint.config.cjs +42 -0
- package/templates/express/package.json +39 -0
- package/templates/express/src/app.ts +71 -0
- package/templates/express/src/config/env.ts +23 -0
- package/templates/express/src/middlewares/error.middleware.ts +18 -0
- package/templates/{bases/express-base → express}/src/server.ts +2 -2
- package/templates/express/template.json +44 -0
- package/templates/express/tsconfig.json +31 -0
- package/templates/{bases/nextjs-base → nextjs}/app/layout.tsx +1 -5
- package/templates/nextjs/app/page.tsx +57 -0
- package/templates/{bases/nextjs-base → nextjs}/package.json +2 -1
- package/templates/{bases/nextjs-base → nextjs}/template.json +13 -1
- package/templates/react-vite/.env.example +2 -0
- package/templates/react-vite/README.md +85 -0
- package/templates/react-vite/eslint.config.js +23 -0
- package/templates/{bases/react-vite-base → react-vite}/index.html +1 -0
- package/templates/{bases/react-vite-base → react-vite}/package.json +16 -2
- package/templates/react-vite/src/api/client.ts +47 -0
- package/templates/react-vite/src/api/services/user.service.ts +18 -0
- package/templates/react-vite/src/components/ErrorBoundary.tsx +51 -0
- package/templates/react-vite/src/components/Layout.tsx +13 -0
- package/templates/react-vite/src/components/Loading.tsx +8 -0
- package/templates/react-vite/src/components/SEO.tsx +49 -0
- package/templates/react-vite/src/config/constants.ts +5 -0
- package/templates/react-vite/src/hooks/index.ts +64 -0
- package/templates/react-vite/src/index.css +1 -0
- package/templates/react-vite/src/lib/queryClient.ts +12 -0
- package/templates/react-vite/src/main.tsx +22 -0
- package/templates/react-vite/src/pages/About.tsx +78 -0
- package/templates/react-vite/src/pages/Home.tsx +49 -0
- package/templates/react-vite/src/pages/NotFound.tsx +24 -0
- package/templates/react-vite/src/pages/UserProfile.tsx +40 -0
- package/templates/react-vite/src/router.tsx +33 -0
- package/templates/react-vite/src/types/api.d.ts +20 -0
- package/templates/react-vite/src/types/user.d.ts +6 -0
- package/templates/react-vite/src/utils/helpers.ts +51 -0
- package/templates/react-vite/src/utils/storage.ts +35 -0
- package/templates/react-vite/src/vite-env.d.ts +11 -0
- package/templates/react-vite/template.json +46 -0
- package/templates/react-vite/tsconfig.json +4 -0
- package/templates/react-vite/vite.config.ts +13 -0
- package/modules/database/drizzle-postgresql/files/drizzle.config.ts +0 -10
- package/modules/database/drizzle-postgresql/files/lib/db.ts +0 -7
- package/modules/database/drizzle-postgresql/files/lib/schema.ts +0 -8
- package/modules/database/drizzle-postgresql/module.json +0 -34
- package/templates/bases/express-base/.env.example +0 -2
- package/templates/bases/express-base/package.json +0 -23
- package/templates/bases/express-base/src/app.ts +0 -34
- package/templates/bases/express-base/src/config/env.ts +0 -14
- package/templates/bases/express-base/src/middlewares/error.middleware.ts +0 -12
- package/templates/bases/express-base/template.json +0 -7
- package/templates/bases/express-base/tsconfig.json +0 -14
- package/templates/bases/nextjs-base/app/page.tsx +0 -65
- package/templates/bases/react-vite-base/README.md +0 -73
- package/templates/bases/react-vite-base/eslint.config.js +0 -23
- package/templates/bases/react-vite-base/src/App.css +0 -42
- package/templates/bases/react-vite-base/src/App.tsx +0 -35
- package/templates/bases/react-vite-base/src/index.css +0 -68
- package/templates/bases/react-vite-base/src/main.tsx +0 -10
- package/templates/bases/react-vite-base/template.json +0 -19
- package/templates/bases/react-vite-base/tsconfig.json +0 -7
- package/templates/bases/react-vite-base/vite.config.ts +0 -7
- /package/templates/{bases/nextjs-base → nextjs}/README.md +0 -0
- /package/templates/{bases/nextjs-base → nextjs}/app/favicon.ico +0 -0
- /package/templates/{bases/nextjs-base → nextjs}/app/globals.css +0 -0
- /package/templates/{bases/nextjs-base → nextjs}/eslint.config.mjs +0 -0
- /package/templates/{bases/nextjs-base → nextjs}/next.config.ts +0 -0
- /package/templates/{bases/nextjs-base → nextjs}/postcss.config.mjs +0 -0
- /package/templates/{bases/nextjs-base → nextjs}/public/file.svg +0 -0
- /package/templates/{bases/nextjs-base → nextjs}/public/globe.svg +0 -0
- /package/templates/{bases/nextjs-base → nextjs}/public/next.svg +0 -0
- /package/templates/{bases/nextjs-base → nextjs}/public/vercel.svg +0 -0
- /package/templates/{bases/nextjs-base → nextjs}/public/window.svg +0 -0
- /package/templates/{bases/nextjs-base → nextjs}/tsconfig.json +0 -0
- /package/templates/{bases/react-vite-base → react-vite}/public/vite.svg +0 -0
- /package/templates/{bases/react-vite-base → react-vite}/src/assets/react.svg +0 -0
- /package/templates/{bases/react-vite-base → react-vite}/tsconfig.app.json +0 -0
- /package/templates/{bases/react-vite-base → react-vite}/tsconfig.node.json +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
dotenv.config({ path: path.join(process.cwd(), ".env") });
|
|
5
|
+
|
|
6
|
+
const env = {
|
|
7
|
+
app: {
|
|
8
|
+
port: Number(process.env.PORT) || 3000,
|
|
9
|
+
url: process.env.APP_URL || "http://localhost:3000",
|
|
10
|
+
site_url: process.env.SITE_URL || "http://localhost:5173",
|
|
11
|
+
trust_proxy: (process.env.TRUST_PROXY || "false") === "true",
|
|
12
|
+
rateLimit: {
|
|
13
|
+
max: Number(process.env.RATE_LIMIT_MAX) || 100,
|
|
14
|
+
windowMs: Number(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
node: {
|
|
18
|
+
env: process.env.NODE_ENV || "development",
|
|
19
|
+
isProduction: (process.env.NODE_ENV || "development") === "production",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export { env };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from "express";
|
|
2
|
+
import { env } from "../config/env";
|
|
3
|
+
|
|
4
|
+
export const errorHandler = (err: any, req: Request, res: Response, next: NextFunction) => {
|
|
5
|
+
const statusCode = err.status || 500;
|
|
6
|
+
const errorMessage = err?.message || "Internal server error!";
|
|
7
|
+
|
|
8
|
+
const payload: any = {
|
|
9
|
+
success: false,
|
|
10
|
+
message: errorMessage,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
if (!env.node.isProduction) {
|
|
14
|
+
payload.errors = err?.stack || err;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
res.status(statusCode).json(payload);
|
|
18
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "express-base",
|
|
3
|
+
"displayName": "Express.js",
|
|
4
|
+
"framework": "express",
|
|
5
|
+
"description": "Express.js REST API with TypeScript",
|
|
6
|
+
"files": ["src/", ".env.example", ".gitignore", "package.json", "tsconfig.json"],
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "tsx watch src/server.ts",
|
|
9
|
+
"clean": "rimraf dist",
|
|
10
|
+
"prebuild": "npm run clean",
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"lint": "eslint src --ext .ts",
|
|
13
|
+
"lint:fix": "eslint src --ext .ts --fix",
|
|
14
|
+
"start": "node dist/server.js",
|
|
15
|
+
"start:prod": "cross-env NODE_ENV=production node dist/server.js"
|
|
16
|
+
},
|
|
17
|
+
"jsScripts": {
|
|
18
|
+
"dev": "tsx --watch src/server.js",
|
|
19
|
+
"clean": "rimraf dist",
|
|
20
|
+
"prebuild": "npm run clean",
|
|
21
|
+
"build": "echo 'No build step for JavaScript'",
|
|
22
|
+
"lint": "eslint src --ext .js",
|
|
23
|
+
"lint:fix": "eslint src --ext .js --fix",
|
|
24
|
+
"start": "node src/server.js",
|
|
25
|
+
"start:prod": "cross-env NODE_ENV=production node src/server.js"
|
|
26
|
+
},
|
|
27
|
+
"fileReplacements": [
|
|
28
|
+
{
|
|
29
|
+
"file": "src/server.js",
|
|
30
|
+
"from": "import app from './app'",
|
|
31
|
+
"to": "import app from './app.js'"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"file": "src/app.js",
|
|
35
|
+
"from": "import { env } from './config/env'",
|
|
36
|
+
"to": "import { env } from './config/env.js'"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"file": "src/app.js",
|
|
40
|
+
"from": "import { errorHandler } from './middlewares/error.middleware'",
|
|
41
|
+
"to": "import { errorHandler } from './middlewares/error.middleware.js'"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"rootDir": "./src",
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"target": "ES2023",
|
|
8
|
+
"lib": ["ES2023"],
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"declaration": false,
|
|
17
|
+
"declarationMap": false,
|
|
18
|
+
"removeComments": false,
|
|
19
|
+
"noImplicitAny": true,
|
|
20
|
+
"noImplicitReturns": true,
|
|
21
|
+
"noImplicitThis": true,
|
|
22
|
+
"noUnusedLocals": true,
|
|
23
|
+
"noUnusedParameters": true,
|
|
24
|
+
"exactOptionalPropertyTypes": true,
|
|
25
|
+
"noEmitOnError": true,
|
|
26
|
+
"incremental": false,
|
|
27
|
+
"ignoreDeprecations": "5.0"
|
|
28
|
+
},
|
|
29
|
+
"include": ["src/**/*"],
|
|
30
|
+
"exclude": ["node_modules", "dist"]
|
|
31
|
+
}
|
|
@@ -24,11 +24,7 @@ export default function RootLayout({
|
|
|
24
24
|
}>) {
|
|
25
25
|
return (
|
|
26
26
|
<html lang="en">
|
|
27
|
-
<body
|
|
28
|
-
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
29
|
-
>
|
|
30
|
-
{children}
|
|
31
|
-
</body>
|
|
27
|
+
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
|
|
32
28
|
</html>
|
|
33
29
|
);
|
|
34
30
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import Image from "next/image";
|
|
2
|
+
|
|
3
|
+
export default function Home() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
|
6
|
+
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
|
7
|
+
<div className="flex items-center gap-4 mb-8">
|
|
8
|
+
<div className="text-2xl font-bold text-black dark:text-white">Stackkit</div>
|
|
9
|
+
<span className="text-xl text-zinc-400">+</span>
|
|
10
|
+
<Image
|
|
11
|
+
className="dark:invert"
|
|
12
|
+
src="/next.svg"
|
|
13
|
+
alt="Next.js logo"
|
|
14
|
+
width={100}
|
|
15
|
+
height={20}
|
|
16
|
+
priority
|
|
17
|
+
/>
|
|
18
|
+
</div>
|
|
19
|
+
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
20
|
+
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
21
|
+
To get started, edit the page.tsx file.
|
|
22
|
+
</h1>
|
|
23
|
+
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
24
|
+
This template includes Next.js, Tailwind CSS, and Stackkit best practices. Check out the{" "}
|
|
25
|
+
<a
|
|
26
|
+
href="https://github.com/tariqul420/stackkit"
|
|
27
|
+
className="font-medium text-zinc-950 dark:text-zinc-50 hover:underline"
|
|
28
|
+
target="_blank"
|
|
29
|
+
rel="noopener noreferrer"
|
|
30
|
+
>
|
|
31
|
+
Stackkit repository
|
|
32
|
+
</a>{" "}
|
|
33
|
+
for more info.
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
37
|
+
<a
|
|
38
|
+
className="flex h-12 w-full items-center justify-center rounded-full bg-white text-black px-5 transition-colors hover:bg-zinc-200 md:w-40"
|
|
39
|
+
href="https://nextjs.org/docs"
|
|
40
|
+
target="_blank"
|
|
41
|
+
rel="noopener noreferrer"
|
|
42
|
+
>
|
|
43
|
+
Documentation
|
|
44
|
+
</a>
|
|
45
|
+
<a
|
|
46
|
+
className="flex h-12 w-full items-center justify-center rounded-full bg-black text-white px-5 transition-colors hover:bg-zinc-900 md:w-40"
|
|
47
|
+
href="https://github.com/tariqul420/stackkit"
|
|
48
|
+
target="_blank"
|
|
49
|
+
rel="noopener noreferrer"
|
|
50
|
+
>
|
|
51
|
+
Stackkit GitHub
|
|
52
|
+
</a>
|
|
53
|
+
</div>
|
|
54
|
+
</main>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -14,5 +14,17 @@
|
|
|
14
14
|
"postcss.config.mjs",
|
|
15
15
|
"README.md",
|
|
16
16
|
"tsconfig.json"
|
|
17
|
-
]
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "next dev",
|
|
20
|
+
"build": "next build",
|
|
21
|
+
"start": "next start",
|
|
22
|
+
"lint": "eslint"
|
|
23
|
+
},
|
|
24
|
+
"jsScripts": {
|
|
25
|
+
"dev": "next dev",
|
|
26
|
+
"build": "next build",
|
|
27
|
+
"start": "next start",
|
|
28
|
+
"lint": "eslint"
|
|
29
|
+
}
|
|
18
30
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# React + Vite Template
|
|
2
|
+
|
|
3
|
+
A production-ready React starter template with TypeScript, Vite, and essential libraries pre-configured.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **React 19** with TypeScript
|
|
8
|
+
- **Vite 7** for fast development
|
|
9
|
+
- **React Router v7** for client-side routing
|
|
10
|
+
- **TanStack Query v5** for data fetching and caching
|
|
11
|
+
- **Axios** with interceptors
|
|
12
|
+
- **Tailwind CSS** for styling
|
|
13
|
+
- **React Hot Toast** for notifications
|
|
14
|
+
- **SEO Ready** with React Helmet Async
|
|
15
|
+
- **Error Boundaries** for graceful error handling
|
|
16
|
+
- **ESLint** for code quality
|
|
17
|
+
- **Custom Hooks** included
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Install dependencies
|
|
23
|
+
pnpm install
|
|
24
|
+
|
|
25
|
+
# Start development server
|
|
26
|
+
pnpm dev
|
|
27
|
+
|
|
28
|
+
# Build for production
|
|
29
|
+
pnpm build
|
|
30
|
+
|
|
31
|
+
# Preview production build
|
|
32
|
+
pnpm preview
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Project Structure
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
src/
|
|
39
|
+
├── api/ # API client & interceptors
|
|
40
|
+
├── components/ # Reusable UI components
|
|
41
|
+
├── config/ # App configuration
|
|
42
|
+
├── hooks/ # Custom React hooks
|
|
43
|
+
├── lib/ # Library configurations
|
|
44
|
+
├── pages/ # Route pages
|
|
45
|
+
├── types/ # TypeScript types
|
|
46
|
+
├── utils/ # Helper functions
|
|
47
|
+
├── App.tsx # Main app component
|
|
48
|
+
├── main.tsx # Entry point
|
|
49
|
+
└── index.css # Global styles
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Environment Variables
|
|
53
|
+
|
|
54
|
+
Create a `.env` file:
|
|
55
|
+
|
|
56
|
+
```env
|
|
57
|
+
VITE_API_URL=http://localhost:3000/api
|
|
58
|
+
VITE_APP_NAME=My App
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Production Build
|
|
62
|
+
|
|
63
|
+
- Code splitting with vendor chunks
|
|
64
|
+
- Tree shaking to remove unused code
|
|
65
|
+
- Minified and compressed output
|
|
66
|
+
- TypeScript strict mode enabled
|
|
67
|
+
- ESLint configured for code quality
|
|
68
|
+
|
|
69
|
+
## Deployment
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pnpm build
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Deploy the `dist` folder to Vercel, Netlify, or any static hosting service.
|
|
76
|
+
|
|
77
|
+
## Tech Stack
|
|
78
|
+
|
|
79
|
+
- React 19 - UI library
|
|
80
|
+
- Vite 7 - Build tool
|
|
81
|
+
- TypeScript - Type safety
|
|
82
|
+
- React Router v7 - Routing
|
|
83
|
+
- TanStack Query v5 - Data fetching
|
|
84
|
+
- Axios - HTTP client
|
|
85
|
+
- Tailwind CSS - Styling
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import globals from "globals";
|
|
3
|
+
import reactHooks from "eslint-plugin-react-hooks";
|
|
4
|
+
import reactRefresh from "eslint-plugin-react-refresh";
|
|
5
|
+
import tseslint from "typescript-eslint";
|
|
6
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
7
|
+
|
|
8
|
+
export default defineConfig([
|
|
9
|
+
globalIgnores(["dist"]),
|
|
10
|
+
{
|
|
11
|
+
files: ["**/*.{ts,tsx}"],
|
|
12
|
+
extends: [
|
|
13
|
+
js.configs.recommended,
|
|
14
|
+
tseslint.configs.recommended,
|
|
15
|
+
reactHooks.configs.flat.recommended,
|
|
16
|
+
reactRefresh.configs.vite,
|
|
17
|
+
],
|
|
18
|
+
languageOptions: {
|
|
19
|
+
ecmaVersion: 2020,
|
|
20
|
+
globals: globals.browser,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
]);
|
|
@@ -7,11 +7,22 @@
|
|
|
7
7
|
"dev": "vite",
|
|
8
8
|
"build": "tsc -b && vite build",
|
|
9
9
|
"lint": "eslint .",
|
|
10
|
+
"lint:fix": "eslint . --fix",
|
|
10
11
|
"preview": "vite preview"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
14
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
15
|
+
"@tanstack/react-query": "^5.62.0",
|
|
16
|
+
"@tanstack/react-query-devtools": "^5.62.0",
|
|
17
|
+
"axios": "^1.7.7",
|
|
18
|
+
"class-variance-authority": "^0.7.1",
|
|
19
|
+
"clsx": "^2.1.1",
|
|
13
20
|
"react": "^19.2.0",
|
|
14
|
-
"react-dom": "^19.2.0"
|
|
21
|
+
"react-dom": "^19.2.0",
|
|
22
|
+
"react-helmet-async": "^2.0.5",
|
|
23
|
+
"react-hot-toast": "^2.4.1",
|
|
24
|
+
"react-router": "^7.12.0",
|
|
25
|
+
"tailwind-merge": "^3.4.0"
|
|
15
26
|
},
|
|
16
27
|
"devDependencies": {
|
|
17
28
|
"@eslint/js": "^9.39.1",
|
|
@@ -19,12 +30,15 @@
|
|
|
19
30
|
"@types/react": "^19.2.5",
|
|
20
31
|
"@types/react-dom": "^19.2.3",
|
|
21
32
|
"@vitejs/plugin-react": "^5.1.1",
|
|
33
|
+
"autoprefixer": "^10.4.23",
|
|
22
34
|
"eslint": "^9.39.1",
|
|
23
35
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
24
36
|
"eslint-plugin-react-refresh": "^0.4.24",
|
|
25
37
|
"globals": "^16.5.0",
|
|
38
|
+
"postcss": "^8.5.6",
|
|
39
|
+
"tailwindcss": "^4.1.18",
|
|
26
40
|
"typescript": "~5.9.3",
|
|
27
41
|
"typescript-eslint": "^8.46.4",
|
|
28
42
|
"vite": "^7.2.4"
|
|
29
43
|
}
|
|
30
|
-
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AxiosResponse } from "axios";
|
|
2
|
+
import axios, { AxiosError } from "axios";
|
|
3
|
+
import toast from "react-hot-toast";
|
|
4
|
+
|
|
5
|
+
const api = axios.create({
|
|
6
|
+
baseURL: import.meta.env.VITE_API_URL || "http://localhost:3000/api",
|
|
7
|
+
timeout: 10000,
|
|
8
|
+
headers: {
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
api.interceptors.request.use(
|
|
14
|
+
(config) => {
|
|
15
|
+
const token = localStorage.getItem("auth_token");
|
|
16
|
+
if (token) {
|
|
17
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
18
|
+
}
|
|
19
|
+
return config;
|
|
20
|
+
},
|
|
21
|
+
(error: AxiosError) => {
|
|
22
|
+
return Promise.reject(error);
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
api.interceptors.response.use(
|
|
27
|
+
(response: AxiosResponse) => response,
|
|
28
|
+
(error: AxiosError) => {
|
|
29
|
+
if (error.response?.status === 401) {
|
|
30
|
+
localStorage.removeItem("auth_token");
|
|
31
|
+
toast.error("Session expired. Please login again.");
|
|
32
|
+
} else if (error.response?.status === 403) {
|
|
33
|
+
toast.error("You do not have permission to perform this action.");
|
|
34
|
+
} else if (error.response?.status === 404) {
|
|
35
|
+
toast.error("Resource not found.");
|
|
36
|
+
} else if (error.response?.status === 500) {
|
|
37
|
+
toast.error("Server error. Please try again later.");
|
|
38
|
+
} else if (error.code === "ECONNABORTED") {
|
|
39
|
+
toast.error("Request timeout. Please try again.");
|
|
40
|
+
} else if (!error.response) {
|
|
41
|
+
toast.error("Network error. Please check your connection.");
|
|
42
|
+
}
|
|
43
|
+
return Promise.reject(error);
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export default api;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import api from "../client";
|
|
2
|
+
|
|
3
|
+
export const userService = {
|
|
4
|
+
getUser: async (id: string): Promise<User> => {
|
|
5
|
+
const response = await api.get<ApiResponse<User>>(`/users/${id}`);
|
|
6
|
+
return response.data.data;
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
getCurrentUser: async (): Promise<User> => {
|
|
10
|
+
const response = await api.get<ApiResponse<User>>("/users/me");
|
|
11
|
+
return response.data.data;
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
updateUser: async (id: string, data: Partial<User>): Promise<User> => {
|
|
15
|
+
const response = await api.patch<ApiResponse<User>>(`/users/${id}`, data);
|
|
16
|
+
return response.data.data;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ErrorInfo, ReactNode } from "react";
|
|
2
|
+
import { Component } from "react";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
fallback?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface State {
|
|
10
|
+
hasError: boolean;
|
|
11
|
+
error?: Error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
15
|
+
constructor(props: Props) {
|
|
16
|
+
super(props);
|
|
17
|
+
this.state = { hasError: false };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static getDerivedStateFromError(error: Error): State {
|
|
21
|
+
return { hasError: true, error };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
25
|
+
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
render() {
|
|
29
|
+
if (this.state.hasError) {
|
|
30
|
+
if (this.props.fallback) {
|
|
31
|
+
return this.props.fallback;
|
|
32
|
+
}
|
|
33
|
+
return (
|
|
34
|
+
<div className="p-8 text-center">
|
|
35
|
+
<h2 className="text-2xl font-semibold">Something went wrong</h2>
|
|
36
|
+
<p className="mt-2 text-gray-600">
|
|
37
|
+
{this.state.error?.message || "An unexpected error occurred"}
|
|
38
|
+
</p>
|
|
39
|
+
<button
|
|
40
|
+
onClick={() => this.setState({ hasError: false, error: undefined })}
|
|
41
|
+
className="mt-4 flex h-12 w-full items-center justify-center rounded-full bg-white text-black px-5 transition-colors hover:bg-zinc-200 md:w-39.5"
|
|
42
|
+
>
|
|
43
|
+
Try again
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return this.props.children;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Outlet } from "react-router";
|
|
2
|
+
|
|
3
|
+
export function Layout() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="min-h-screen flex flex-col bg-black">
|
|
6
|
+
<main className="flex-1 max-w-7xl w-full mx-auto p-4">
|
|
7
|
+
<Outlet />
|
|
8
|
+
</main>
|
|
9
|
+
</div>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default Layout;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Helmet, HelmetProvider } from "react-helmet-async";
|
|
2
|
+
|
|
3
|
+
interface SEOProps {
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
keywords?: string;
|
|
7
|
+
image?: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const defaultSEO = {
|
|
12
|
+
title: "React App",
|
|
13
|
+
description: "A modern React application built with Vite",
|
|
14
|
+
keywords: "react, vite, typescript, spa",
|
|
15
|
+
image: "/og-image.png",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function SEOProvider({ children }: { children: React.ReactNode }) {
|
|
19
|
+
return <HelmetProvider>{children}</HelmetProvider>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function SEO({ title, description, keywords, image, url }: SEOProps) {
|
|
23
|
+
const siteTitle = title ? `${title} | ${defaultSEO.title}` : defaultSEO.title;
|
|
24
|
+
const siteDescription = description || defaultSEO.description;
|
|
25
|
+
const siteKeywords = keywords || defaultSEO.keywords;
|
|
26
|
+
const siteImage = image || defaultSEO.image;
|
|
27
|
+
const siteUrl = url || window.location.href;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Helmet>
|
|
31
|
+
<title>{siteTitle}</title>
|
|
32
|
+
<meta name="description" content={siteDescription} />
|
|
33
|
+
<meta name="keywords" content={siteKeywords} />
|
|
34
|
+
|
|
35
|
+
<meta property="og:type" content="website" />
|
|
36
|
+
<meta property="og:title" content={siteTitle} />
|
|
37
|
+
<meta property="og:description" content={siteDescription} />
|
|
38
|
+
<meta property="og:image" content={siteImage} />
|
|
39
|
+
<meta property="og:url" content={siteUrl} />
|
|
40
|
+
|
|
41
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
42
|
+
<meta name="twitter:title" content={siteTitle} />
|
|
43
|
+
<meta name="twitter:description" content={siteDescription} />
|
|
44
|
+
<meta name="twitter:image" content={siteImage} />
|
|
45
|
+
|
|
46
|
+
<link rel="canonical" href={siteUrl} />
|
|
47
|
+
</Helmet>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const APP_NAME = import.meta.env.VITE_APP_NAME || "React App";
|
|
2
|
+
export const APP_VERSION = import.meta.env.VITE_APP_VERSION || "1.0.0";
|
|
3
|
+
export const API_URL = import.meta.env.VITE_API_URL || "http://localhost:3000/api";
|
|
4
|
+
export const IS_DEV = import.meta.env.DEV;
|
|
5
|
+
export const IS_PROD = import.meta.env.PROD;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function useDebounce<T>(value: T, delay: number): T {
|
|
4
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const handler = setTimeout(() => {
|
|
8
|
+
setDebouncedValue(value);
|
|
9
|
+
}, delay);
|
|
10
|
+
|
|
11
|
+
return () => {
|
|
12
|
+
clearTimeout(handler);
|
|
13
|
+
};
|
|
14
|
+
}, [value, delay]);
|
|
15
|
+
|
|
16
|
+
return debouncedValue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useLocalStorage<T>(
|
|
20
|
+
key: string,
|
|
21
|
+
initialValue: T,
|
|
22
|
+
): [T, (value: T | ((val: T) => T)) => void] {
|
|
23
|
+
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
24
|
+
try {
|
|
25
|
+
const item = window.localStorage.getItem(key);
|
|
26
|
+
return item ? JSON.parse(item) : initialValue;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(error);
|
|
29
|
+
return initialValue;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const setValue = (value: T | ((val: T) => T)) => {
|
|
34
|
+
try {
|
|
35
|
+
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
36
|
+
setStoredValue(valueToStore);
|
|
37
|
+
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(error);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return [storedValue, setValue];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useMediaQuery(query: string): boolean {
|
|
47
|
+
const [matches, setMatches] = useState(() => {
|
|
48
|
+
if (typeof window !== "undefined") {
|
|
49
|
+
return window.matchMedia(query).matches;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const media = window.matchMedia(query);
|
|
56
|
+
|
|
57
|
+
const listener = () => setMatches(media.matches);
|
|
58
|
+
media.addEventListener("change", listener);
|
|
59
|
+
|
|
60
|
+
return () => media.removeEventListener("change", listener);
|
|
61
|
+
}, [query]);
|
|
62
|
+
|
|
63
|
+
return matches;
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|