stackkit-cli 0.4.2 → 0.4.4
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 +17 -10
- package/bin/stackkit.js +10 -1
- package/dist/commands/add.js +164 -25
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +34 -29
- package/dist/commands/list.js +12 -12
- package/dist/index.js +25 -23
- package/dist/types/index.d.ts +28 -16
- package/dist/utils/code-inject.d.ts +1 -1
- package/dist/utils/code-inject.js +6 -6
- package/dist/utils/detect.d.ts +1 -1
- package/dist/utils/detect.js +48 -44
- package/dist/utils/env-editor.js +20 -20
- package/dist/utils/files.js +4 -4
- package/dist/utils/json-editor.d.ts +3 -3
- package/dist/utils/json-editor.js +10 -14
- package/dist/utils/logger.d.ts +1 -1
- package/dist/utils/logger.js +8 -8
- package/dist/utils/package-manager.d.ts +2 -2
- package/dist/utils/package-manager.js +33 -26
- package/modules/auth/better-auth/files/api/auth/[...all]/route.ts +4 -0
- package/modules/auth/better-auth/files/lib/auth.ts +13 -0
- package/modules/auth/better-auth/files/schemas/prisma-schema.prisma +63 -0
- package/modules/auth/better-auth/module.json +54 -0
- package/modules/auth/{clerk-express/files/lib → clerk/files/express}/auth.ts +1 -1
- package/modules/auth/{clerk-nextjs/files/lib → clerk/files/nextjs}/auth-provider.tsx +1 -1
- package/modules/auth/clerk/files/nextjs/middleware.ts +9 -0
- package/modules/auth/{clerk-react/files/lib → clerk/files/react}/auth-provider.tsx +2 -2
- package/modules/auth/clerk/module.json +115 -0
- package/modules/database/mongoose-mongodb/files/lib/db.ts +45 -7
- package/modules/database/mongoose-mongodb/files/models/User.ts +39 -0
- package/modules/database/mongoose-mongodb/module.json +59 -7
- package/modules/database/prisma/files/lib/prisma.ts +6 -0
- package/modules/database/prisma/files/prisma/schema.prisma +8 -0
- package/modules/database/prisma/files/prisma.config.ts +12 -0
- package/modules/database/prisma/module.json +140 -0
- package/package.json +1 -1
- package/templates/express/.env.example +3 -0
- package/templates/express/eslint.config.cjs +42 -0
- package/templates/express/package.json +33 -0
- package/templates/express/src/app.ts +51 -0
- package/templates/express/src/config/env.ts +12 -0
- package/templates/express/src/features/auth/auth.controller.ts +48 -0
- package/templates/express/src/features/auth/auth.route.ts +10 -0
- package/templates/express/src/features/auth/auth.service.ts +21 -0
- package/templates/express/src/middlewares/error.middleware.ts +18 -0
- package/templates/{bases/express-base → express}/src/server.ts +3 -3
- package/templates/express/template.json +40 -0
- package/templates/express/tsconfig.json +30 -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/auth/better-auth-express/files/lib/auth.ts +0 -16
- package/modules/auth/better-auth-express/files/routes/auth.ts +0 -12
- package/modules/auth/better-auth-express/module.json +0 -38
- package/modules/auth/better-auth-nextjs/files/api/auth/[...all]/route.ts +0 -5
- package/modules/auth/better-auth-nextjs/files/lib/auth.ts +0 -26
- package/modules/auth/better-auth-nextjs/module.json +0 -41
- package/modules/auth/better-auth-react/files/lib/auth-client.ts +0 -9
- package/modules/auth/better-auth-react/module.json +0 -26
- package/modules/auth/clerk-express/module.json +0 -20
- package/modules/auth/clerk-nextjs/files/middleware.ts +0 -9
- package/modules/auth/clerk-nextjs/module.json +0 -28
- package/modules/auth/clerk-react/module.json +0 -19
- 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 -35
- package/modules/database/prisma-mongodb/files/lib/db.ts +0 -9
- package/modules/database/prisma-mongodb/files/prisma/schema.prisma +0 -17
- package/modules/database/prisma-mongodb/module.json +0 -36
- package/modules/database/prisma-postgresql/files/lib/db.ts +0 -9
- package/modules/database/prisma-postgresql/files/prisma/schema.prisma +0 -17
- package/modules/database/prisma-postgresql/module.json +0 -36
- 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,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";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
export const queryClient = new QueryClient({
|
|
4
|
+
defaultOptions: {
|
|
5
|
+
queries: {
|
|
6
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
7
|
+
gcTime: 1000 * 60 * 10, // 10 minutes (formerly cacheTime)
|
|
8
|
+
retry: 1,
|
|
9
|
+
refetchOnWindowFocus: false,
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { QueryClientProvider } from "@tanstack/react-query";
|
|
2
|
+
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|
3
|
+
import { StrictMode } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { Toaster } from "react-hot-toast";
|
|
6
|
+
import { RouterProvider } from "react-router";
|
|
7
|
+
import { SEOProvider } from "./components/SEO";
|
|
8
|
+
import "./index.css";
|
|
9
|
+
import { queryClient } from "./lib/queryClient";
|
|
10
|
+
import { router } from "./router";
|
|
11
|
+
|
|
12
|
+
createRoot(document.getElementById("root")!).render(
|
|
13
|
+
<StrictMode>
|
|
14
|
+
<SEOProvider>
|
|
15
|
+
<QueryClientProvider client={queryClient}>
|
|
16
|
+
<RouterProvider router={router} />
|
|
17
|
+
<Toaster position="top-right" />
|
|
18
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
19
|
+
</QueryClientProvider>
|
|
20
|
+
</SEOProvider>
|
|
21
|
+
</StrictMode>,
|
|
22
|
+
);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { SEO } from "../components/SEO";
|
|
2
|
+
|
|
3
|
+
export default function About() {
|
|
4
|
+
return (
|
|
5
|
+
<>
|
|
6
|
+
<SEO title="About" description="About Stackkit - A production-ready React starter template" />
|
|
7
|
+
|
|
8
|
+
<div className="flex min-h-screen items-center justify-center bg-black">
|
|
9
|
+
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-black sm:items-start">
|
|
10
|
+
<div className="flex items-center gap-4 mb-8">
|
|
11
|
+
<div className="text-2xl font-bold text-white">Stackkit</div>
|
|
12
|
+
<span className="text-xl text-zinc-400">+</span>
|
|
13
|
+
<img src="https://react.dev/favicon.ico" alt="React logo" width={32} height={32} />
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div className="flex flex-col gap-12 sm:text-left">
|
|
17
|
+
<div>
|
|
18
|
+
<h1 className="text-3xl font-semibold leading-10 tracking-tight text-zinc-50 mb-6">
|
|
19
|
+
About this template
|
|
20
|
+
</h1>
|
|
21
|
+
<p className="text-lg leading-8 text-zinc-400 mb-8">
|
|
22
|
+
Stackkit is a production-ready React starter template that includes all the
|
|
23
|
+
essential tools you need to build modern web applications.
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div>
|
|
28
|
+
<h2 className="text-xl font-semibold text-zinc-50 mb-4">What's included</h2>
|
|
29
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-zinc-400">
|
|
30
|
+
<div className="p-4 border border-zinc-800 rounded-lg">
|
|
31
|
+
<div className="font-medium text-zinc-50 mb-1">React 19</div>
|
|
32
|
+
<div className="text-sm">Latest React with TypeScript</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="p-4 border border-zinc-800 rounded-lg">
|
|
35
|
+
<div className="font-medium text-zinc-50 mb-1">Vite 7</div>
|
|
36
|
+
<div className="text-sm">Fast build tool and dev server</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="p-4 border border-zinc-800 rounded-lg">
|
|
39
|
+
<div className="font-medium text-zinc-50 mb-1">React Router</div>
|
|
40
|
+
<div className="text-sm">Client-side routing</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div className="p-4 border border-zinc-800 rounded-lg">
|
|
43
|
+
<div className="font-medium text-zinc-50 mb-1">TanStack Query</div>
|
|
44
|
+
<div className="text-sm">Data fetching and caching</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="p-4 border border-zinc-800 rounded-lg">
|
|
47
|
+
<div className="font-medium text-zinc-50 mb-1">Axios</div>
|
|
48
|
+
<div className="text-sm">HTTP client with interceptors</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="p-4 border border-zinc-800 rounded-lg">
|
|
51
|
+
<div className="font-medium text-zinc-50 mb-1">Tailwind CSS</div>
|
|
52
|
+
<div className="text-sm">Utility-first CSS framework</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row mt-8">
|
|
59
|
+
<a
|
|
60
|
+
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-39.5"
|
|
61
|
+
href="/"
|
|
62
|
+
>
|
|
63
|
+
Back to Home
|
|
64
|
+
</a>
|
|
65
|
+
<a
|
|
66
|
+
className="flex h-12 w-full items-center justify-center rounded-full px-5 transition-colors hover:border-transparent bg-zinc-900 md:w-39.5 dark:text-white text-black"
|
|
67
|
+
href="https://stackkit.tariqul.dev"
|
|
68
|
+
target="_blank"
|
|
69
|
+
rel="noopener noreferrer"
|
|
70
|
+
>
|
|
71
|
+
Website
|
|
72
|
+
</a>
|
|
73
|
+
</div>
|
|
74
|
+
</main>
|
|
75
|
+
</div>
|
|
76
|
+
</>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { SEO } from "../components/SEO";
|
|
2
|
+
|
|
3
|
+
export default function Home() {
|
|
4
|
+
return (
|
|
5
|
+
<>
|
|
6
|
+
<SEO title="Home" />
|
|
7
|
+
<div className="flex min-h-screen items-center justify-center bg-black">
|
|
8
|
+
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-black sm:items-start">
|
|
9
|
+
<div className="flex items-center gap-4 mb-8">
|
|
10
|
+
<div className="text-2xl font-bold text-white">Stackkit</div>
|
|
11
|
+
<span className="text-xl text-zinc-400">+</span>
|
|
12
|
+
<img src="https://react.dev/favicon.ico" alt="React logo" width={32} height={32} />
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
16
|
+
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-zinc-50">
|
|
17
|
+
To get started, edit the Home.tsx file.
|
|
18
|
+
</h1>
|
|
19
|
+
<p className="max-w-md text-lg leading-8 text-zinc-400">
|
|
20
|
+
This template includes React Router, TanStack Query, Axios, and Tailwind CSS. Check
|
|
21
|
+
out the{" "}
|
|
22
|
+
<a href="/about" className="font-medium text-zinc-50 hover:underline">
|
|
23
|
+
About
|
|
24
|
+
</a>{" "}
|
|
25
|
+
page to learn more about the included features.
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
30
|
+
<a
|
|
31
|
+
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-39.5"
|
|
32
|
+
href="https://react.dev"
|
|
33
|
+
target="_blank"
|
|
34
|
+
rel="noopener noreferrer"
|
|
35
|
+
>
|
|
36
|
+
Get Started
|
|
37
|
+
</a>
|
|
38
|
+
<a
|
|
39
|
+
className="flex h-12 w-full items-center justify-center rounded-full px-5 transition-colors hover:border-transparent bg-zinc-900 md:w-39.5 dark:text-white text-black"
|
|
40
|
+
href="/about"
|
|
41
|
+
>
|
|
42
|
+
Documentation
|
|
43
|
+
</a>
|
|
44
|
+
</div>
|
|
45
|
+
</main>
|
|
46
|
+
</div>
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Link } from "react-router";
|
|
2
|
+
import { SEO } from "../components/SEO";
|
|
3
|
+
|
|
4
|
+
export default function NotFound() {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
<SEO title="404 - Page Not Found" description="The page you're looking for doesn't exist" />
|
|
8
|
+
|
|
9
|
+
<div className="flex min-h-screen items-center justify-center bg-black">
|
|
10
|
+
<div className="text-center px-6">
|
|
11
|
+
<h1 className="text-8xl font-bold text-white mb-4">404</h1>
|
|
12
|
+
<h2 className="text-3xl font-semibold text-zinc-50 mb-4">Page Not Found</h2>
|
|
13
|
+
<p className="text-lg text-zinc-400 mb-8">The page you're looking for doesn't exist.</p>
|
|
14
|
+
<Link
|
|
15
|
+
to="/"
|
|
16
|
+
className="inline-flex h-12 items-center justify-center rounded-full bg-white text-black px-8 font-medium transition-colors hover:bg-zinc-200"
|
|
17
|
+
>
|
|
18
|
+
Go Home
|
|
19
|
+
</Link>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { useLoaderData, useParams } from "react-router";
|
|
3
|
+
import { userService } from "../api/services/user.service";
|
|
4
|
+
|
|
5
|
+
type User = { id?: string; name?: string; email?: string; avatar?: string; [key: string]: any };
|
|
6
|
+
|
|
7
|
+
export default function UserProfile() {
|
|
8
|
+
const loaderUser = useLoaderData() as User | undefined;
|
|
9
|
+
const { userId } = useParams();
|
|
10
|
+
|
|
11
|
+
const { data: user = loaderUser ?? {} } = useQuery({
|
|
12
|
+
queryKey: ["user", userId],
|
|
13
|
+
queryFn: async () => {
|
|
14
|
+
if (!userId) throw new Error("Missing user id");
|
|
15
|
+
return await userService.getUser(userId);
|
|
16
|
+
},
|
|
17
|
+
initialData: loaderUser,
|
|
18
|
+
staleTime: 1000 * 60,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="min-h-screen bg-black text-white flex items-center justify-center">
|
|
23
|
+
<div className="max-w-xl p-8 bg-zinc-900 rounded-md shadow">
|
|
24
|
+
<div className="flex items-center gap-4">
|
|
25
|
+
{user.avatar ? (
|
|
26
|
+
<img src={user.avatar} alt={user.name} className="w-16 h-16 rounded-full" />
|
|
27
|
+
) : (
|
|
28
|
+
<div className="w-16 h-16 rounded-full bg-zinc-700 flex items-center justify-center text-xl">
|
|
29
|
+
{user.name?.[0] ?? "U"}
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
32
|
+
<div>
|
|
33
|
+
<h2 className="text-2xl font-semibold">{user.name}</h2>
|
|
34
|
+
<p className="text-sm text-zinc-400">{user.email}</p>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createBrowserRouter } from "react-router";
|
|
2
|
+
import { userService } from "./api/services/user.service";
|
|
3
|
+
import { ErrorBoundary } from "./components/ErrorBoundary";
|
|
4
|
+
import Layout from "./components/Layout";
|
|
5
|
+
import About from "./pages/About";
|
|
6
|
+
import Home from "./pages/Home";
|
|
7
|
+
import NotFound from "./pages/NotFound";
|
|
8
|
+
import UserProfile from "./pages/UserProfile";
|
|
9
|
+
|
|
10
|
+
export const router = createBrowserRouter([
|
|
11
|
+
{
|
|
12
|
+
path: "/",
|
|
13
|
+
Component: Layout,
|
|
14
|
+
errorElement: <ErrorBoundary />,
|
|
15
|
+
children: [
|
|
16
|
+
{ index: true, Component: Home },
|
|
17
|
+
{ path: "about", Component: About },
|
|
18
|
+
{
|
|
19
|
+
path: "users/:userId",
|
|
20
|
+
loader: async ({ params }) => {
|
|
21
|
+
const id = params.userId;
|
|
22
|
+
if (!id) throw new Response("Missing user id", { status: 400 });
|
|
23
|
+
const user = await userService.getUser(id);
|
|
24
|
+
return user;
|
|
25
|
+
},
|
|
26
|
+
Component: UserProfile,
|
|
27
|
+
},
|
|
28
|
+
{ path: "*", Component: NotFound },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export default router;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type ApiResponse<T = unknown> = {
|
|
2
|
+
data: T;
|
|
3
|
+
message?: string;
|
|
4
|
+
status: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type PaginatedResponse<T> = {
|
|
8
|
+
data: T[];
|
|
9
|
+
total: number;
|
|
10
|
+
page: number;
|
|
11
|
+
pageSize: number;
|
|
12
|
+
totalPages: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type ApiError = {
|
|
16
|
+
message: string;
|
|
17
|
+
code?: string;
|
|
18
|
+
status?: number;
|
|
19
|
+
errors?: Record<string, string[]>;
|
|
20
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function cn(...classes: (string | boolean | undefined | null)[]): string {
|
|
2
|
+
return classes.filter(Boolean).join(" ");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function formatDate(date: Date | string): string {
|
|
6
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
7
|
+
return d.toLocaleDateString("en-US", {
|
|
8
|
+
year: "numeric",
|
|
9
|
+
month: "long",
|
|
10
|
+
day: "numeric",
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function truncate(str: string, maxLength: number): string {
|
|
15
|
+
if (str.length <= maxLength) return str;
|
|
16
|
+
return str.slice(0, maxLength) + "...";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function delay(ms: number): Promise<void> {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function debounce<T extends (...args: never[]) => unknown>(
|
|
24
|
+
func: T,
|
|
25
|
+
wait: number,
|
|
26
|
+
): (...args: Parameters<T>) => void {
|
|
27
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
28
|
+
|
|
29
|
+
return function executedFunction(...args: Parameters<T>) {
|
|
30
|
+
const later = () => {
|
|
31
|
+
timeout = null;
|
|
32
|
+
func(...args);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (timeout) clearTimeout(timeout);
|
|
36
|
+
timeout = setTimeout(later, wait);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function capitalize(str: string): string {
|
|
41
|
+
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function slugify(str: string): string {
|
|
45
|
+
return str
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.trim()
|
|
48
|
+
.replace(/[^\w\s-]/g, "")
|
|
49
|
+
.replace(/[\s_-]+/g, "-")
|
|
50
|
+
.replace(/^-+|-+$/g, "");
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const storage = {
|
|
2
|
+
get: <T>(key: string, defaultValue?: T): T | null => {
|
|
3
|
+
try {
|
|
4
|
+
const item = localStorage.getItem(key);
|
|
5
|
+
return item ? JSON.parse(item) : (defaultValue ?? null);
|
|
6
|
+
} catch (error) {
|
|
7
|
+
console.error("Error reading from localStorage:", error);
|
|
8
|
+
return defaultValue ?? null;
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
set: <T>(key: string, value: T): void => {
|
|
13
|
+
try {
|
|
14
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error("Error writing to localStorage:", error);
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
remove: (key: string): void => {
|
|
21
|
+
try {
|
|
22
|
+
localStorage.removeItem(key);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error("Error removing from localStorage:", error);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
clear: (): void => {
|
|
29
|
+
try {
|
|
30
|
+
localStorage.clear();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error("Error clearing localStorage:", error);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-vite-base",
|
|
3
|
+
"displayName": "React (Vite)",
|
|
4
|
+
"framework": "react-vite",
|
|
5
|
+
"description": "Production-ready React 19 + Vite with TypeScript, Router, TanStack Query, and more",
|
|
6
|
+
"files": [
|
|
7
|
+
"src/",
|
|
8
|
+
"public/",
|
|
9
|
+
".env.example",
|
|
10
|
+
".gitignore",
|
|
11
|
+
"eslint.config.js",
|
|
12
|
+
"index.html",
|
|
13
|
+
"package.json",
|
|
14
|
+
"README.md",
|
|
15
|
+
"tsconfig.json",
|
|
16
|
+
"tsconfig.app.json",
|
|
17
|
+
"tsconfig.node.json",
|
|
18
|
+
"vite.config.ts"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"dev": "vite",
|
|
22
|
+
"build": "tsc -b && vite build",
|
|
23
|
+
"lint": "eslint .",
|
|
24
|
+
"lint:fix": "eslint . --fix",
|
|
25
|
+
"preview": "vite preview"
|
|
26
|
+
},
|
|
27
|
+
"jsScripts": {
|
|
28
|
+
"dev": "vite",
|
|
29
|
+
"build": "vite build",
|
|
30
|
+
"lint": "eslint .",
|
|
31
|
+
"lint:fix": "eslint . --fix",
|
|
32
|
+
"preview": "vite preview"
|
|
33
|
+
},
|
|
34
|
+
"fileReplacements": [
|
|
35
|
+
{
|
|
36
|
+
"file": "index.html",
|
|
37
|
+
"from": "/src/main.tsx",
|
|
38
|
+
"to": "/src/main.jsx"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"file": "vite.config.ts",
|
|
42
|
+
"from": "main.tsx",
|
|
43
|
+
"to": "main.jsx"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { defineConfig } from "vite";
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react(), tailwindcss()],
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
"@": path.resolve(__dirname, "./src"),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
});
|