create-stackkit-app 0.4.2 → 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 +1 -0
- package/bin/create-stackkit.js +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/create-project.js +329 -143
- package/dist/lib/template-composer.js +21 -21
- package/modules/auth/better-auth-express/adapters/mongoose-mongodb.ts +3 -3
- package/modules/auth/better-auth-express/adapters/prisma-mongodb.ts +4 -4
- package/modules/auth/better-auth-express/adapters/prisma-postgresql.ts +4 -4
- 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-nextjs/adapters/mongoose-mongodb.ts +3 -3
- package/modules/auth/better-auth-nextjs/adapters/prisma-mongodb.ts +4 -4
- package/modules/auth/better-auth-nextjs/adapters/prisma-postgresql.ts +4 -4
- 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-react/files/lib/auth-client.ts +2 -2
- package/modules/auth/clerk-express/files/lib/auth.ts +1 -1
- 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-react/files/lib/auth-provider.tsx +2 -2
- package/modules/database/mongoose-mongodb/files/lib/db.ts +3 -3
- package/modules/database/prisma-mongodb/files/lib/db.ts +2 -2
- package/modules/database/prisma-postgresql/files/lib/db.ts +2 -2
- package/package.json +8 -3
- package/templates/express/package.json +1 -0
- package/templates/express/src/app.ts +17 -15
- package/templates/express/src/config/env.ts +8 -8
- package/templates/express/src/middlewares/error.middleware.ts +3 -3
- package/templates/express/src/server.ts +2 -2
- package/templates/express/template.json +38 -1
- package/templates/express/tsconfig.json +17 -0
- package/templates/nextjs/app/layout.tsx +1 -5
- package/templates/nextjs/app/page.tsx +26 -34
- package/templates/nextjs/package.json +2 -1
- package/templates/nextjs/template.json +13 -1
- package/templates/react-vite/eslint.config.js +9 -9
- package/templates/react-vite/package.json +1 -2
- package/templates/react-vite/src/api/client.ts +16 -16
- package/templates/react-vite/src/api/services/user.service.ts +2 -10
- package/templates/react-vite/src/components/ErrorBoundary.tsx +4 -4
- package/templates/react-vite/src/components/Layout.tsx +1 -1
- package/templates/react-vite/src/components/Loading.tsx +1 -1
- package/templates/react-vite/src/components/SEO.tsx +5 -5
- package/templates/react-vite/src/config/constants.ts +3 -3
- package/templates/react-vite/src/hooks/index.ts +5 -5
- package/templates/react-vite/src/lib/queryClient.ts +2 -2
- package/templates/react-vite/src/main.tsx +12 -12
- package/templates/react-vite/src/pages/About.tsx +6 -2
- package/templates/react-vite/src/pages/Home.tsx +8 -4
- package/templates/react-vite/src/pages/NotFound.tsx +2 -2
- package/templates/react-vite/src/pages/UserProfile.tsx +6 -6
- package/templates/react-vite/src/router.tsx +13 -13
- package/templates/react-vite/src/types/{api.ts → api.d.ts} +6 -6
- package/templates/react-vite/src/types/user.d.ts +6 -0
- package/templates/react-vite/src/utils/helpers.ts +11 -11
- package/templates/react-vite/src/utils/storage.ts +4 -4
- package/templates/react-vite/template.json +26 -0
- package/templates/react-vite/tsconfig.json +1 -4
- package/templates/react-vite/vite.config.ts +5 -5
|
@@ -4,59 +4,51 @@ export default function Home() {
|
|
|
4
4
|
return (
|
|
5
5
|
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
|
6
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
|
-
<
|
|
8
|
-
className="dark:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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>
|
|
15
19
|
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
16
20
|
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
17
21
|
To get started, edit the page.tsx file.
|
|
18
22
|
</h1>
|
|
19
23
|
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
20
|
-
|
|
21
|
-
<a
|
|
22
|
-
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
23
|
-
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
24
|
-
>
|
|
25
|
-
Templates
|
|
26
|
-
</a>{' '}
|
|
27
|
-
or the{' '}
|
|
24
|
+
This template includes Next.js, Tailwind CSS, and Stackkit best practices. Check out the{" "}
|
|
28
25
|
<a
|
|
29
|
-
href="https://
|
|
30
|
-
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
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"
|
|
31
30
|
>
|
|
32
|
-
|
|
33
|
-
</a>{
|
|
34
|
-
|
|
31
|
+
Stackkit repository
|
|
32
|
+
</a>{" "}
|
|
33
|
+
for more info.
|
|
35
34
|
</p>
|
|
36
35
|
</div>
|
|
37
36
|
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
38
37
|
<a
|
|
39
|
-
className="flex h-12 w-full items-center justify-center
|
|
40
|
-
href="https://
|
|
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"
|
|
41
40
|
target="_blank"
|
|
42
41
|
rel="noopener noreferrer"
|
|
43
42
|
>
|
|
44
|
-
|
|
45
|
-
className="dark:invert"
|
|
46
|
-
src="/vercel.svg"
|
|
47
|
-
alt="Vercel logomark"
|
|
48
|
-
width={16}
|
|
49
|
-
height={16}
|
|
50
|
-
/>
|
|
51
|
-
Deploy Now
|
|
43
|
+
Documentation
|
|
52
44
|
</a>
|
|
53
45
|
<a
|
|
54
|
-
className="flex h-12 w-full items-center justify-center rounded-full
|
|
55
|
-
href="https://
|
|
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"
|
|
56
48
|
target="_blank"
|
|
57
49
|
rel="noopener noreferrer"
|
|
58
50
|
>
|
|
59
|
-
|
|
51
|
+
Stackkit GitHub
|
|
60
52
|
</a>
|
|
61
53
|
</div>
|
|
62
54
|
</main>
|
|
@@ -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
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import js from
|
|
2
|
-
import globals from
|
|
3
|
-
import reactHooks from
|
|
4
|
-
import reactRefresh from
|
|
5
|
-
import tseslint from
|
|
6
|
-
import { defineConfig, globalIgnores } from
|
|
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
7
|
|
|
8
8
|
export default defineConfig([
|
|
9
|
-
globalIgnores([
|
|
9
|
+
globalIgnores(["dist"]),
|
|
10
10
|
{
|
|
11
|
-
files: [
|
|
11
|
+
files: ["**/*.{ts,tsx}"],
|
|
12
12
|
extends: [
|
|
13
13
|
js.configs.recommended,
|
|
14
14
|
tseslint.configs.recommended,
|
|
@@ -20,4 +20,4 @@ export default defineConfig([
|
|
|
20
20
|
globals: globals.browser,
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
|
-
])
|
|
23
|
+
]);
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import type { AxiosResponse } from
|
|
2
|
-
import axios, { AxiosError } from
|
|
3
|
-
import toast from
|
|
1
|
+
import type { AxiosResponse } from "axios";
|
|
2
|
+
import axios, { AxiosError } from "axios";
|
|
3
|
+
import toast from "react-hot-toast";
|
|
4
4
|
|
|
5
5
|
const api = axios.create({
|
|
6
|
-
baseURL: import.meta.env.VITE_API_URL ||
|
|
6
|
+
baseURL: import.meta.env.VITE_API_URL || "http://localhost:3000/api",
|
|
7
7
|
timeout: 10000,
|
|
8
8
|
headers: {
|
|
9
|
-
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
10
|
},
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
api.interceptors.request.use(
|
|
14
14
|
(config) => {
|
|
15
|
-
const token = localStorage.getItem(
|
|
15
|
+
const token = localStorage.getItem("auth_token");
|
|
16
16
|
if (token) {
|
|
17
17
|
config.headers.Authorization = `Bearer ${token}`;
|
|
18
18
|
}
|
|
@@ -20,28 +20,28 @@ api.interceptors.request.use(
|
|
|
20
20
|
},
|
|
21
21
|
(error: AxiosError) => {
|
|
22
22
|
return Promise.reject(error);
|
|
23
|
-
}
|
|
23
|
+
},
|
|
24
24
|
);
|
|
25
25
|
|
|
26
26
|
api.interceptors.response.use(
|
|
27
27
|
(response: AxiosResponse) => response,
|
|
28
28
|
(error: AxiosError) => {
|
|
29
29
|
if (error.response?.status === 401) {
|
|
30
|
-
localStorage.removeItem(
|
|
31
|
-
toast.error(
|
|
30
|
+
localStorage.removeItem("auth_token");
|
|
31
|
+
toast.error("Session expired. Please login again.");
|
|
32
32
|
} else if (error.response?.status === 403) {
|
|
33
|
-
toast.error(
|
|
33
|
+
toast.error("You do not have permission to perform this action.");
|
|
34
34
|
} else if (error.response?.status === 404) {
|
|
35
|
-
toast.error(
|
|
35
|
+
toast.error("Resource not found.");
|
|
36
36
|
} else if (error.response?.status === 500) {
|
|
37
|
-
toast.error(
|
|
38
|
-
} else if (error.code ===
|
|
39
|
-
toast.error(
|
|
37
|
+
toast.error("Server error. Please try again later.");
|
|
38
|
+
} else if (error.code === "ECONNABORTED") {
|
|
39
|
+
toast.error("Request timeout. Please try again.");
|
|
40
40
|
} else if (!error.response) {
|
|
41
|
-
toast.error(
|
|
41
|
+
toast.error("Network error. Please check your connection.");
|
|
42
42
|
}
|
|
43
43
|
return Promise.reject(error);
|
|
44
|
-
}
|
|
44
|
+
},
|
|
45
45
|
);
|
|
46
46
|
|
|
47
47
|
export default api;
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import api from '../client';
|
|
3
|
-
|
|
4
|
-
export interface User {
|
|
5
|
-
id: string;
|
|
6
|
-
name: string;
|
|
7
|
-
email: string;
|
|
8
|
-
avatar?: string;
|
|
9
|
-
}
|
|
1
|
+
import api from "../client";
|
|
10
2
|
|
|
11
3
|
export const userService = {
|
|
12
4
|
getUser: async (id: string): Promise<User> => {
|
|
@@ -15,7 +7,7 @@ export const userService = {
|
|
|
15
7
|
},
|
|
16
8
|
|
|
17
9
|
getCurrentUser: async (): Promise<User> => {
|
|
18
|
-
const response = await api.get<ApiResponse<User>>(
|
|
10
|
+
const response = await api.get<ApiResponse<User>>("/users/me");
|
|
19
11
|
return response.data.data;
|
|
20
12
|
},
|
|
21
13
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ErrorInfo, ReactNode } from
|
|
2
|
-
import { Component } from
|
|
1
|
+
import type { ErrorInfo, ReactNode } from "react";
|
|
2
|
+
import { Component } from "react";
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
5
|
children?: ReactNode;
|
|
@@ -22,7 +22,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
25
|
-
console.error(
|
|
25
|
+
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
render() {
|
|
@@ -34,7 +34,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
|
|
34
34
|
<div className="p-8 text-center">
|
|
35
35
|
<h2 className="text-2xl font-semibold">Something went wrong</h2>
|
|
36
36
|
<p className="mt-2 text-gray-600">
|
|
37
|
-
{this.state.error?.message ||
|
|
37
|
+
{this.state.error?.message || "An unexpected error occurred"}
|
|
38
38
|
</p>
|
|
39
39
|
<button
|
|
40
40
|
onClick={() => this.setState({ hasError: false, error: undefined })}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Helmet, HelmetProvider } from
|
|
1
|
+
import { Helmet, HelmetProvider } from "react-helmet-async";
|
|
2
2
|
|
|
3
3
|
interface SEOProps {
|
|
4
4
|
title?: string;
|
|
@@ -9,10 +9,10 @@ interface SEOProps {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const defaultSEO = {
|
|
12
|
-
title:
|
|
13
|
-
description:
|
|
14
|
-
keywords:
|
|
15
|
-
image:
|
|
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
16
|
};
|
|
17
17
|
|
|
18
18
|
export function SEOProvider({ children }: { children: React.ReactNode }) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export const APP_NAME = import.meta.env.VITE_APP_NAME ||
|
|
2
|
-
export const APP_VERSION = import.meta.env.VITE_APP_VERSION ||
|
|
3
|
-
export const API_URL = import.meta.env.VITE_API_URL ||
|
|
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
4
|
export const IS_DEV = import.meta.env.DEV;
|
|
5
5
|
export const IS_PROD = import.meta.env.PROD;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState } from
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
2
|
|
|
3
3
|
export function useDebounce<T>(value: T, delay: number): T {
|
|
4
4
|
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
@@ -18,7 +18,7 @@ export function useDebounce<T>(value: T, delay: number): T {
|
|
|
18
18
|
|
|
19
19
|
export function useLocalStorage<T>(
|
|
20
20
|
key: string,
|
|
21
|
-
initialValue: T
|
|
21
|
+
initialValue: T,
|
|
22
22
|
): [T, (value: T | ((val: T) => T)) => void] {
|
|
23
23
|
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
24
24
|
try {
|
|
@@ -45,7 +45,7 @@ export function useLocalStorage<T>(
|
|
|
45
45
|
|
|
46
46
|
export function useMediaQuery(query: string): boolean {
|
|
47
47
|
const [matches, setMatches] = useState(() => {
|
|
48
|
-
if (typeof window !==
|
|
48
|
+
if (typeof window !== "undefined") {
|
|
49
49
|
return window.matchMedia(query).matches;
|
|
50
50
|
}
|
|
51
51
|
return false;
|
|
@@ -55,9 +55,9 @@ export function useMediaQuery(query: string): boolean {
|
|
|
55
55
|
const media = window.matchMedia(query);
|
|
56
56
|
|
|
57
57
|
const listener = () => setMatches(media.matches);
|
|
58
|
-
media.addEventListener(
|
|
58
|
+
media.addEventListener("change", listener);
|
|
59
59
|
|
|
60
|
-
return () => media.removeEventListener(
|
|
60
|
+
return () => media.removeEventListener("change", listener);
|
|
61
61
|
}, [query]);
|
|
62
62
|
|
|
63
63
|
return matches;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { QueryClient } from
|
|
1
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
2
2
|
|
|
3
3
|
export const queryClient = new QueryClient({
|
|
4
4
|
defaultOptions: {
|
|
@@ -9,4 +9,4 @@ export const queryClient = new QueryClient({
|
|
|
9
9
|
refetchOnWindowFocus: false,
|
|
10
10
|
},
|
|
11
11
|
},
|
|
12
|
-
});
|
|
12
|
+
});
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { QueryClientProvider } from
|
|
2
|
-
import { ReactQueryDevtools } from
|
|
3
|
-
import { StrictMode } from
|
|
4
|
-
import { createRoot } from
|
|
5
|
-
import { Toaster } from
|
|
6
|
-
import { RouterProvider } from
|
|
7
|
-
import { SEOProvider } from
|
|
8
|
-
import
|
|
9
|
-
import { queryClient } from
|
|
10
|
-
import { router } from
|
|
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
11
|
|
|
12
|
-
createRoot(document.getElementById(
|
|
12
|
+
createRoot(document.getElementById("root")!).render(
|
|
13
13
|
<StrictMode>
|
|
14
14
|
<SEOProvider>
|
|
15
15
|
<QueryClientProvider client={queryClient}>
|
|
@@ -18,5 +18,5 @@ createRoot(document.getElementById('root')!).render(
|
|
|
18
18
|
<ReactQueryDevtools initialIsOpen={false} />
|
|
19
19
|
</QueryClientProvider>
|
|
20
20
|
</SEOProvider>
|
|
21
|
-
</StrictMode
|
|
21
|
+
</StrictMode>,
|
|
22
22
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SEO } from
|
|
1
|
+
import { SEO } from "../components/SEO";
|
|
2
2
|
|
|
3
3
|
export default function About() {
|
|
4
4
|
return (
|
|
@@ -7,7 +7,11 @@ export default function About() {
|
|
|
7
7
|
|
|
8
8
|
<div className="flex min-h-screen items-center justify-center bg-black">
|
|
9
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="
|
|
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>
|
|
11
15
|
|
|
12
16
|
<div className="flex flex-col gap-12 sm:text-left">
|
|
13
17
|
<div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SEO } from
|
|
1
|
+
import { SEO } from "../components/SEO";
|
|
2
2
|
|
|
3
3
|
export default function Home() {
|
|
4
4
|
return (
|
|
@@ -6,7 +6,11 @@ export default function Home() {
|
|
|
6
6
|
<SEO title="Home" />
|
|
7
7
|
<div className="flex min-h-screen items-center justify-center bg-black">
|
|
8
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="
|
|
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>
|
|
10
14
|
|
|
11
15
|
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
12
16
|
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-zinc-50">
|
|
@@ -14,10 +18,10 @@ export default function Home() {
|
|
|
14
18
|
</h1>
|
|
15
19
|
<p className="max-w-md text-lg leading-8 text-zinc-400">
|
|
16
20
|
This template includes React Router, TanStack Query, Axios, and Tailwind CSS. Check
|
|
17
|
-
out the{
|
|
21
|
+
out the{" "}
|
|
18
22
|
<a href="/about" className="font-medium text-zinc-50 hover:underline">
|
|
19
23
|
About
|
|
20
|
-
</a>{
|
|
24
|
+
</a>{" "}
|
|
21
25
|
page to learn more about the included features.
|
|
22
26
|
</p>
|
|
23
27
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useQuery } from
|
|
2
|
-
import { useLoaderData, useParams } from
|
|
3
|
-
import { userService } from
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { useLoaderData, useParams } from "react-router";
|
|
3
|
+
import { userService } from "../api/services/user.service";
|
|
4
4
|
|
|
5
5
|
type User = { id?: string; name?: string; email?: string; avatar?: string; [key: string]: any };
|
|
6
6
|
|
|
@@ -9,9 +9,9 @@ export default function UserProfile() {
|
|
|
9
9
|
const { userId } = useParams();
|
|
10
10
|
|
|
11
11
|
const { data: user = loaderUser ?? {} } = useQuery({
|
|
12
|
-
queryKey: [
|
|
12
|
+
queryKey: ["user", userId],
|
|
13
13
|
queryFn: async () => {
|
|
14
|
-
if (!userId) throw new Error(
|
|
14
|
+
if (!userId) throw new Error("Missing user id");
|
|
15
15
|
return await userService.getUser(userId);
|
|
16
16
|
},
|
|
17
17
|
initialData: loaderUser,
|
|
@@ -26,7 +26,7 @@ export default function UserProfile() {
|
|
|
26
26
|
<img src={user.avatar} alt={user.name} className="w-16 h-16 rounded-full" />
|
|
27
27
|
) : (
|
|
28
28
|
<div className="w-16 h-16 rounded-full bg-zinc-700 flex items-center justify-center text-xl">
|
|
29
|
-
{user.name?.[0] ??
|
|
29
|
+
{user.name?.[0] ?? "U"}
|
|
30
30
|
</div>
|
|
31
31
|
)}
|
|
32
32
|
<div>
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { createBrowserRouter } from
|
|
2
|
-
import { userService } from
|
|
3
|
-
import { ErrorBoundary } from
|
|
4
|
-
import Layout from
|
|
5
|
-
import About from
|
|
6
|
-
import Home from
|
|
7
|
-
import NotFound from
|
|
8
|
-
import UserProfile from
|
|
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
9
|
|
|
10
10
|
export const router = createBrowserRouter([
|
|
11
11
|
{
|
|
12
|
-
path:
|
|
12
|
+
path: "/",
|
|
13
13
|
Component: Layout,
|
|
14
14
|
errorElement: <ErrorBoundary />,
|
|
15
15
|
children: [
|
|
16
16
|
{ index: true, Component: Home },
|
|
17
|
-
{ path:
|
|
17
|
+
{ path: "about", Component: About },
|
|
18
18
|
{
|
|
19
|
-
path:
|
|
19
|
+
path: "users/:userId",
|
|
20
20
|
loader: async ({ params }) => {
|
|
21
21
|
const id = params.userId;
|
|
22
|
-
if (!id) throw new Response(
|
|
22
|
+
if (!id) throw new Response("Missing user id", { status: 400 });
|
|
23
23
|
const user = await userService.getUser(id);
|
|
24
24
|
return user;
|
|
25
25
|
},
|
|
26
26
|
Component: UserProfile,
|
|
27
27
|
},
|
|
28
|
-
{ path:
|
|
28
|
+
{ path: "*", Component: NotFound },
|
|
29
29
|
],
|
|
30
30
|
},
|
|
31
31
|
]);
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
type ApiResponse<T = unknown> = {
|
|
2
2
|
data: T;
|
|
3
3
|
message?: string;
|
|
4
4
|
status: number;
|
|
5
|
-
}
|
|
5
|
+
};
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
type PaginatedResponse<T> = {
|
|
8
8
|
data: T[];
|
|
9
9
|
total: number;
|
|
10
10
|
page: number;
|
|
11
11
|
pageSize: number;
|
|
12
12
|
totalPages: number;
|
|
13
|
-
}
|
|
13
|
+
};
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
type ApiError = {
|
|
16
16
|
message: string;
|
|
17
17
|
code?: string;
|
|
18
18
|
status?: number;
|
|
19
19
|
errors?: Record<string, string[]>;
|
|
20
|
-
}
|
|
20
|
+
};
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
export function cn(...classes: (string | boolean | undefined | null)[]): string {
|
|
2
|
-
return classes.filter(Boolean).join(
|
|
2
|
+
return classes.filter(Boolean).join(" ");
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
export function formatDate(date: Date | string): string {
|
|
6
|
-
const d = typeof date ===
|
|
7
|
-
return d.toLocaleDateString(
|
|
8
|
-
year:
|
|
9
|
-
month:
|
|
10
|
-
day:
|
|
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
11
|
});
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function truncate(str: string, maxLength: number): string {
|
|
15
15
|
if (str.length <= maxLength) return str;
|
|
16
|
-
return str.slice(0, maxLength) +
|
|
16
|
+
return str.slice(0, maxLength) + "...";
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function delay(ms: number): Promise<void> {
|
|
@@ -22,7 +22,7 @@ export function delay(ms: number): Promise<void> {
|
|
|
22
22
|
|
|
23
23
|
export function debounce<T extends (...args: never[]) => unknown>(
|
|
24
24
|
func: T,
|
|
25
|
-
wait: number
|
|
25
|
+
wait: number,
|
|
26
26
|
): (...args: Parameters<T>) => void {
|
|
27
27
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
28
28
|
|
|
@@ -45,7 +45,7 @@ export function slugify(str: string): string {
|
|
|
45
45
|
return str
|
|
46
46
|
.toLowerCase()
|
|
47
47
|
.trim()
|
|
48
|
-
.replace(/[^\w\s-]/g,
|
|
49
|
-
.replace(/[\s_-]+/g,
|
|
50
|
-
.replace(/^-+|-+$/g,
|
|
48
|
+
.replace(/[^\w\s-]/g, "")
|
|
49
|
+
.replace(/[\s_-]+/g, "-")
|
|
50
|
+
.replace(/^-+|-+$/g, "");
|
|
51
51
|
}
|