@tioelvis/next-template 1.0.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 +42 -0
- package/package.json +31 -0
- package/src/lib/constants.js +28 -0
- package/src/lib/utils.js +141 -0
- package/src/main.js +107 -0
- package/src/template/components.json +21 -0
- package/src/template/eslint.config.mjs +16 -0
- package/src/template/next-env.d.ts +5 -0
- package/src/template/next.config.ts +7 -0
- package/src/template/postcss.config.mjs +5 -0
- package/src/template/public/.gitkeep +0 -0
- package/src/template/src/app/favicon.ico +0 -0
- package/src/template/src/app/globals.css +123 -0
- package/src/template/src/app/layout.tsx +36 -0
- package/src/template/src/app/page.tsx +5 -0
- package/src/template/src/lib/constants.ts +1 -0
- package/src/template/src/lib/custom-axios-error.ts +29 -0
- package/src/template/src/lib/request.ts +10 -0
- package/src/template/src/lib/utils.ts +6 -0
- package/src/template/src/providers/query.provider.tsx +12 -0
- package/src/template/src/providers/theme.provider.tsx +8 -0
- package/src/template/tsconfig.json +27 -0
- package/src/ui/components/accordion.json +1 -0
- package/src/ui/components/accordion.tsx +64 -0
- package/src/ui/lib/utils.ts +6 -0
- package/src/ui/tsconfig.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# UI Project Generator
|
|
2
|
+
|
|
3
|
+
A **command-line tool** to scaffold a modern **Next.js + TailwindCSS** project with optional prebuilt UI components and hooks — inspired by [shadcn/ui](https://ui.shadcn.com/).
|
|
4
|
+
|
|
5
|
+
> ⚠️ This tool does **not** aim to replace or take credit for [shadcn/ui]. It is simply a utility for convenience and faster setup in personal and internal projects. All UI components are sourced directly from shadcn.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🔌 React Query Included
|
|
10
|
+
|
|
11
|
+
React Query ([`@tanstack/react-query`](https://tanstack.com/query/latest)) is pre-configured for efficient and scalable data fetching:
|
|
12
|
+
|
|
13
|
+
- ✅ Includes a `QueryClient` setup in `src/providers/query-provider.tsx`
|
|
14
|
+
- ✅ Wraps the application with `QueryClientProvider` (already wired in your layout)
|
|
15
|
+
- ✅ Enables usage of `useQuery`, `useMutation`, and other React Query hooks out of the box
|
|
16
|
+
|
|
17
|
+
## ✨ Features
|
|
18
|
+
|
|
19
|
+
- 🔧 Choose a **project name** and **package manager** (`npm` or `pnpm`)
|
|
20
|
+
- 🎨 Select UI components to include (e.g., Accordion, Alert, Button)
|
|
21
|
+
- ⚡ Automatically sets up:
|
|
22
|
+
- A **Next.js + TailwindCSS** project
|
|
23
|
+
- Preselected **UI components** and **hooks**
|
|
24
|
+
- Project structure under `src/` (with `components/ui`, `hooks`, `lib`, etc.)
|
|
25
|
+
- **React Query** integration with [`@tanstack/react-query`](https://tanstack.com/query/latest)
|
|
26
|
+
- All required dependencies and devDependencies
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 🛠️ Usage
|
|
31
|
+
|
|
32
|
+
Using npm:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx @tioelvis/next-template
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Using pnpm:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pnpx @tioelvis/next-template
|
|
42
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tioelvis/next-template",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "CLI to scaffold a Next.js + Tailwind project using shadcn/ui components",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"next-template": "./src/main.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"cli",
|
|
11
|
+
"nextjs",
|
|
12
|
+
"shadcn",
|
|
13
|
+
"tailwind",
|
|
14
|
+
"ui",
|
|
15
|
+
"project-generator",
|
|
16
|
+
"scaffolding"
|
|
17
|
+
],
|
|
18
|
+
"author": {
|
|
19
|
+
"name": "Ti0Elvis",
|
|
20
|
+
"email": "elvis.veramijares@gmail.com"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/Ti0Elvis/next-template"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"chalk": "^5.4.1",
|
|
29
|
+
"prompts": "^2.4.2"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const DEPENDENCIES = [
|
|
2
|
+
"@tanstack/react-query",
|
|
3
|
+
"axios",
|
|
4
|
+
"class-variance-authority",
|
|
5
|
+
"clsx",
|
|
6
|
+
"cookies-next",
|
|
7
|
+
"date-fns",
|
|
8
|
+
"lucide-react",
|
|
9
|
+
"next",
|
|
10
|
+
"next-auth",
|
|
11
|
+
"next-themes",
|
|
12
|
+
"react",
|
|
13
|
+
"react-dom",
|
|
14
|
+
"tailwind-merge",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const DEV_DEPENDENCIES = [
|
|
18
|
+
"@eslint/eslintrc",
|
|
19
|
+
"@tailwindcss/postcss",
|
|
20
|
+
"@types/node",
|
|
21
|
+
"@types/react",
|
|
22
|
+
"@types/react-dom",
|
|
23
|
+
"eslint",
|
|
24
|
+
"eslint-config-next",
|
|
25
|
+
"tailwindcss",
|
|
26
|
+
"tw-animate-css",
|
|
27
|
+
"typescript",
|
|
28
|
+
];
|
package/src/lib/utils.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import prompts from "prompts";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
export async function get_prompts() {
|
|
8
|
+
const responses = await prompts([
|
|
9
|
+
{
|
|
10
|
+
type: "text",
|
|
11
|
+
name: "project_name",
|
|
12
|
+
message: "What is your project named?",
|
|
13
|
+
validate: (e) => {
|
|
14
|
+
if (e === undefined || e.trim() === "") {
|
|
15
|
+
return chalk.red("⚠️ The name is required");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!/^[a-zA-Z0-9-_]+$/.test(e.trim())) {
|
|
19
|
+
return chalk.red(
|
|
20
|
+
"⚠️ Use only letters, numbers, dashes, and underscores."
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return true;
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
type: "select",
|
|
29
|
+
name: "package_manager",
|
|
30
|
+
message: "Which package manager would you want to use?",
|
|
31
|
+
choices: [
|
|
32
|
+
{ title: "npm", value: "npm" },
|
|
33
|
+
{ title: "pnpm", value: "pnpm" },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: "multiselect",
|
|
38
|
+
name: "components",
|
|
39
|
+
message: "Which components do you want to install?",
|
|
40
|
+
instructions: false,
|
|
41
|
+
choices: [{ title: "Accordion", value: "accordion" }],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
onCancel: () => {
|
|
45
|
+
console.log(chalk.yellow("\n👋 Exiting..."));
|
|
46
|
+
process.exit(0);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
return responses;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function copy_template_files(src, project_path) {
|
|
55
|
+
try {
|
|
56
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
57
|
+
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
const src_path = path.join(src, entry.name);
|
|
60
|
+
const dest_path = path.join(project_path, entry.name);
|
|
61
|
+
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
fs.mkdirSync(dest_path, { recursive: true });
|
|
64
|
+
copy_template_files(src_path, dest_path);
|
|
65
|
+
} else {
|
|
66
|
+
fs.copyFileSync(src_path, dest_path);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function copy_components_files(src, project_path, components) {
|
|
75
|
+
try {
|
|
76
|
+
components.forEach((e) => {
|
|
77
|
+
const component_path = path.resolve(src, `${e}.tsx`);
|
|
78
|
+
const dest_path = path.resolve(project_path, "src", "components", "ui");
|
|
79
|
+
|
|
80
|
+
if (fs.existsSync(component_path) === false) {
|
|
81
|
+
throw new Error(`Component ${e} not found`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (fs.existsSync(dest_path) === false) {
|
|
85
|
+
fs.mkdirSync(dest_path, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fs.copyFileSync(component_path, path.join(dest_path, `${e}.tsx`));
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function copy_hooks_files(
|
|
96
|
+
src,
|
|
97
|
+
components_path,
|
|
98
|
+
project_path,
|
|
99
|
+
components
|
|
100
|
+
) {
|
|
101
|
+
const map = {};
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
components.forEach((e) => {
|
|
105
|
+
const hook = map[e];
|
|
106
|
+
const component_path = path.resolve(components_path, `${e}.tsx`);
|
|
107
|
+
|
|
108
|
+
if (fs.existsSync(component_path) === false) {
|
|
109
|
+
throw new Error(`Component ${e} not found`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (hook === undefined) {
|
|
113
|
+
console.warn(`No hook mapping found for component ${e}, skipping.`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const hook_path = path.resolve(src, `${hook}.ts`);
|
|
118
|
+
const dest_path = path.resolve(project_path, "src", "hooks");
|
|
119
|
+
|
|
120
|
+
if (fs.existsSync(hook_path) === false) {
|
|
121
|
+
throw new Error(`Hook ${hook} not found for component ${e}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (fs.existsSync(dest_path) === false) {
|
|
125
|
+
fs.mkdirSync(dest_path, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fs.copyFileSync(hook_path, path.join(dest_path, `${hook}.ts`));
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function run(cmd, cwd) {
|
|
136
|
+
try {
|
|
137
|
+
execSync(cmd, { cwd, stdio: "inherit" });
|
|
138
|
+
} catch (error) {
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
package/src/main.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
copy_components_files,
|
|
4
|
+
copy_hooks_files,
|
|
5
|
+
copy_template_files,
|
|
6
|
+
get_prompts,
|
|
7
|
+
run,
|
|
8
|
+
} from "./lib/utils.js";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { DEPENDENCIES, DEV_DEPENDENCIES } from "./lib/constants.js";
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
// Manage Ctrl+C command
|
|
18
|
+
process.on("SIGINT", () => {
|
|
19
|
+
console.log(chalk.yellow("\n👋 Exiting..."));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
let project_path = "";
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const { project_name, package_manager, components } = await get_prompts();
|
|
28
|
+
|
|
29
|
+
// Creating folder and copying files
|
|
30
|
+
project_path = path.resolve(project_name);
|
|
31
|
+
|
|
32
|
+
if (fs.existsSync(project_path)) {
|
|
33
|
+
console.error(chalk.red("❌ A folder with that name already exists"));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(
|
|
38
|
+
chalk.blue("📁 Creating project in:"),
|
|
39
|
+
chalk.bold(project_name)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
fs.mkdirSync(project_path, { recursive: true });
|
|
43
|
+
const template_path = path.join(__dirname, "template");
|
|
44
|
+
const components_path = path.join(__dirname, "ui", "components");
|
|
45
|
+
const hooks_path = path.join(__dirname, "ui", "hooks");
|
|
46
|
+
|
|
47
|
+
copy_template_files(template_path, project_path);
|
|
48
|
+
copy_components_files(components_path, project_path, components);
|
|
49
|
+
copy_hooks_files(hooks_path, components_path, project_path, components);
|
|
50
|
+
|
|
51
|
+
// Initialize package.json
|
|
52
|
+
console.log(chalk.cyan("📦 Initializing and configure package.json..."));
|
|
53
|
+
|
|
54
|
+
const package_json = {
|
|
55
|
+
name: project_name,
|
|
56
|
+
version: "1.0.0",
|
|
57
|
+
private: true,
|
|
58
|
+
scripts: {
|
|
59
|
+
dev: "next dev --turbopack",
|
|
60
|
+
build: "next build",
|
|
61
|
+
start: "next start",
|
|
62
|
+
lint: "next lint",
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const package_json_path = path.join(project_path, "package.json");
|
|
67
|
+
fs.writeFileSync(package_json_path, JSON.stringify(package_json, null, 2));
|
|
68
|
+
|
|
69
|
+
// Install dependencies
|
|
70
|
+
console.log(chalk.cyan("⬇️ Installing dependencies..."));
|
|
71
|
+
|
|
72
|
+
components.forEach((e) => {
|
|
73
|
+
const component_path = path.resolve(components_path, `${e}.json`);
|
|
74
|
+
|
|
75
|
+
if (fs.existsSync(component_path) === false) {
|
|
76
|
+
throw new Error(`Component ${e} not found`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const json = JSON.parse(fs.readFileSync(component_path, "utf-8"));
|
|
80
|
+
|
|
81
|
+
json.forEach((e) => {
|
|
82
|
+
DEPENDENCIES.push(e);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (package_manager === "npm") {
|
|
87
|
+
run(`npm install ${DEPENDENCIES.join(" ")}`, project_path);
|
|
88
|
+
run(`npm install ${DEV_DEPENDENCIES.join(" ")} -D`, project_path);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (package_manager === "pnpm") {
|
|
92
|
+
run(`pnpm add ${DEPENDENCIES.join(" ")}`, project_path);
|
|
93
|
+
run(`pnpm add ${DEV_DEPENDENCIES.join(" ")} -D`, project_path);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(chalk.green("\n✅ Project created successfully! 🚀"));
|
|
97
|
+
process.exit(0);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(chalk.red("❌ Internal error"), error.message);
|
|
100
|
+
if (fs.existsSync(project_path) === true) {
|
|
101
|
+
fs.rmSync(project_path, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
main();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/app/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "@/components",
|
|
15
|
+
"utils": "@/lib/utils",
|
|
16
|
+
"ui": "@/components/ui",
|
|
17
|
+
"lib": "@/lib",
|
|
18
|
+
"hooks": "@/hooks"
|
|
19
|
+
},
|
|
20
|
+
"iconLibrary": "lucide"
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { dirname } from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { FlatCompat } from "@eslint/eslintrc";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
|
|
8
|
+
const compat = new FlatCompat({
|
|
9
|
+
baseDirectory: __dirname,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const eslintConfig = [
|
|
13
|
+
...compat.extends("next/core-web-vitals", "next/typescript"),
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export default eslintConfig;
|
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
|
|
4
|
+
@custom-variant dark (&:is(.dark *));
|
|
5
|
+
|
|
6
|
+
@theme inline {
|
|
7
|
+
--color-background: var(--background);
|
|
8
|
+
--color-foreground: var(--foreground);
|
|
9
|
+
--font-sans: var(--font-geist-sans);
|
|
10
|
+
--font-mono: var(--font-geist-mono);
|
|
11
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
12
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
13
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
14
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
15
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
16
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
17
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
18
|
+
--color-sidebar: var(--sidebar);
|
|
19
|
+
--color-chart-5: var(--chart-5);
|
|
20
|
+
--color-chart-4: var(--chart-4);
|
|
21
|
+
--color-chart-3: var(--chart-3);
|
|
22
|
+
--color-chart-2: var(--chart-2);
|
|
23
|
+
--color-chart-1: var(--chart-1);
|
|
24
|
+
--color-ring: var(--ring);
|
|
25
|
+
--color-input: var(--input);
|
|
26
|
+
--color-border: var(--border);
|
|
27
|
+
--color-destructive: var(--destructive);
|
|
28
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
29
|
+
--color-accent: var(--accent);
|
|
30
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
31
|
+
--color-muted: var(--muted);
|
|
32
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
33
|
+
--color-secondary: var(--secondary);
|
|
34
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
35
|
+
--color-primary: var(--primary);
|
|
36
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
37
|
+
--color-popover: var(--popover);
|
|
38
|
+
--color-card-foreground: var(--card-foreground);
|
|
39
|
+
--color-card: var(--card);
|
|
40
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
41
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
42
|
+
--radius-lg: var(--radius);
|
|
43
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
:root {
|
|
47
|
+
--radius: 0.65rem;
|
|
48
|
+
--background: oklch(1 0 0);
|
|
49
|
+
--foreground: oklch(0.145 0 0);
|
|
50
|
+
--card: oklch(1 0 0);
|
|
51
|
+
--card-foreground: oklch(0.145 0 0);
|
|
52
|
+
--popover: oklch(1 0 0);
|
|
53
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
54
|
+
--primary: oklch(0.205 0 0);
|
|
55
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
56
|
+
--secondary: oklch(0.97 0 0);
|
|
57
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
58
|
+
--muted: oklch(0.97 0 0);
|
|
59
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
60
|
+
--accent: oklch(0.97 0 0);
|
|
61
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
62
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
63
|
+
--border: oklch(0.922 0 0);
|
|
64
|
+
--input: oklch(0.922 0 0);
|
|
65
|
+
--ring: oklch(0.708 0 0);
|
|
66
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
67
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
68
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
69
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
70
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
71
|
+
--radius: 0.625rem;
|
|
72
|
+
--sidebar: oklch(0.985 0 0);
|
|
73
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
74
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
75
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
76
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
77
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
78
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
79
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.dark {
|
|
83
|
+
--background: oklch(0.145 0 0);
|
|
84
|
+
--foreground: oklch(0.985 0 0);
|
|
85
|
+
--card: oklch(0.205 0 0);
|
|
86
|
+
--card-foreground: oklch(0.985 0 0);
|
|
87
|
+
--popover: oklch(0.205 0 0);
|
|
88
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
89
|
+
--primary: oklch(0.922 0 0);
|
|
90
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
91
|
+
--secondary: oklch(0.269 0 0);
|
|
92
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
93
|
+
--muted: oklch(0.269 0 0);
|
|
94
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
95
|
+
--accent: oklch(0.269 0 0);
|
|
96
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
97
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
98
|
+
--border: oklch(1 0 0 / 10%);
|
|
99
|
+
--input: oklch(1 0 0 / 15%);
|
|
100
|
+
--ring: oklch(0.556 0 0);
|
|
101
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
102
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
103
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
104
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
105
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
106
|
+
--sidebar: oklch(0.205 0 0);
|
|
107
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
108
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
109
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
110
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
111
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
112
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
113
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@layer base {
|
|
117
|
+
* {
|
|
118
|
+
@apply border-border outline-ring/50;
|
|
119
|
+
}
|
|
120
|
+
body {
|
|
121
|
+
@apply bg-background text-foreground;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import "./globals.css";
|
|
2
|
+
import type { Metadata } from "next";
|
|
3
|
+
import { Roboto } from "next/font/google";
|
|
4
|
+
import { QueryProvider } from "@/providers/query.provider";
|
|
5
|
+
import { ThemeProvider } from "@/providers/theme.provider";
|
|
6
|
+
|
|
7
|
+
const roboto = Roboto({
|
|
8
|
+
subsets: ["latin"],
|
|
9
|
+
weight: ["500", "600", "700", "800"],
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const metadata: Metadata = {
|
|
13
|
+
title: "Untitled",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function Layout({ children }: Readonly<Props>) {
|
|
21
|
+
return (
|
|
22
|
+
<html lang="en" suppressHydrationWarning>
|
|
23
|
+
<body className={roboto.className}>
|
|
24
|
+
<ThemeProvider
|
|
25
|
+
attribute="class"
|
|
26
|
+
defaultTheme="system"
|
|
27
|
+
enableSystem
|
|
28
|
+
disableTransitionOnChange>
|
|
29
|
+
<QueryProvider>
|
|
30
|
+
<main className="w-full min-h-[calc(100vh-4rem)]">{children}</main>
|
|
31
|
+
</QueryProvider>
|
|
32
|
+
</ThemeProvider>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const API = process.env.NEXT_PUBLIC_API;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isAxiosError } from "axios";
|
|
2
|
+
|
|
3
|
+
export class CustomAxiosError extends Error {
|
|
4
|
+
constructor(error: unknown) {
|
|
5
|
+
let message = "";
|
|
6
|
+
|
|
7
|
+
if (isAxiosError(error) === true) {
|
|
8
|
+
const status = error.response?.status ?? 500;
|
|
9
|
+
const p = error.response?.data?.message;
|
|
10
|
+
|
|
11
|
+
if (status === 401 || status === 403) {
|
|
12
|
+
message = "Access denied";
|
|
13
|
+
} else if (p !== undefined) {
|
|
14
|
+
message =
|
|
15
|
+
Array.isArray(p?.message) === true ? p.message[0] : p.message ?? p;
|
|
16
|
+
} else {
|
|
17
|
+
message = "Server error";
|
|
18
|
+
}
|
|
19
|
+
} else if (error instanceof Error) {
|
|
20
|
+
message = error.message;
|
|
21
|
+
} else {
|
|
22
|
+
message = "Unknown error";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "CustomAxiosError";
|
|
27
|
+
Object.setPrototypeOf(this, CustomAxiosError.prototype);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
3
|
+
|
|
4
|
+
const client = new QueryClient();
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function QueryProvider({ children }: Readonly<Props>) {
|
|
11
|
+
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
3
|
+
|
|
4
|
+
type Props = React.ComponentProps<typeof NextThemesProvider>;
|
|
5
|
+
|
|
6
|
+
export function ThemeProvider({ children, ...props }: Readonly<Props>) {
|
|
7
|
+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./src/*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
26
|
+
"exclude": ["node_modules"]
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
["@radix-ui/react-accordion"]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
|
5
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
function Accordion({
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
|
12
|
+
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function AccordionItem({
|
|
16
|
+
className,
|
|
17
|
+
...props
|
|
18
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
|
19
|
+
return (
|
|
20
|
+
<AccordionPrimitive.Item
|
|
21
|
+
data-slot="accordion-item"
|
|
22
|
+
className={cn("border-b last:border-b-0", className)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function AccordionTrigger({
|
|
29
|
+
className,
|
|
30
|
+
children,
|
|
31
|
+
...props
|
|
32
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
|
33
|
+
return (
|
|
34
|
+
<AccordionPrimitive.Header className="flex">
|
|
35
|
+
<AccordionPrimitive.Trigger
|
|
36
|
+
data-slot="accordion-trigger"
|
|
37
|
+
className={cn(
|
|
38
|
+
"focus-visible:border-ring cursor-pointer focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
{...props}>
|
|
42
|
+
{children}
|
|
43
|
+
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
|
44
|
+
</AccordionPrimitive.Trigger>
|
|
45
|
+
</AccordionPrimitive.Header>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function AccordionContent({
|
|
50
|
+
className,
|
|
51
|
+
children,
|
|
52
|
+
...props
|
|
53
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
|
54
|
+
return (
|
|
55
|
+
<AccordionPrimitive.Content
|
|
56
|
+
data-slot="accordion-content"
|
|
57
|
+
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
|
58
|
+
{...props}>
|
|
59
|
+
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
|
60
|
+
</AccordionPrimitive.Content>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
26
|
+
"exclude": ["node_modules"]
|
|
27
|
+
}
|