create-bdpamke-react-scaffold 1.0.0
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/LICENSE +21 -0
- package/README.md +62 -0
- package/bin/create-bdpamke-react-scaffold.js +101 -0
- package/package.json +39 -0
- package/template/.env.example +6 -0
- package/template/FUNCTIONS_EXAMPLES.md +480 -0
- package/template/HOWTOadd a page.md +166 -0
- package/template/REACT_PROPS_USEEFFECT.md +210 -0
- package/template/REGISTRATION_FLOW.md +268 -0
- package/template/USESTATE_EXAMPLES.md +451 -0
- package/template/components.json +20 -0
- package/template/index.html +13 -0
- package/template/jsconfig.json +19 -0
- package/template/package-lock.json +5988 -0
- package/template/package.json +73 -0
- package/template/postcss.config.cjs +6 -0
- package/template/public/images/BDPA_edited.png +0 -0
- package/template/server/server.js +86 -0
- package/template/server/utils/apiClient.js +59 -0
- package/template/server/utils/password.js +60 -0
- package/template/src/App.jsx +10 -0
- package/template/src/components/layout/Container.jsx +7 -0
- package/template/src/components/layout/Section.jsx +7 -0
- package/template/src/components/ui/accordion.jsx +41 -0
- package/template/src/components/ui/alert-dialog.jsx +99 -0
- package/template/src/components/ui/alert.jsx +47 -0
- package/template/src/components/ui/aspect-ratio.jsx +5 -0
- package/template/src/components/ui/avatar.jsx +35 -0
- package/template/src/components/ui/badge.jsx +34 -0
- package/template/src/components/ui/button.jsx +47 -0
- package/template/src/components/ui/calendar.jsx +173 -0
- package/template/src/components/ui/card.jsx +50 -0
- package/template/src/components/ui/carousel.jsx +194 -0
- package/template/src/components/ui/checkbox.jsx +22 -0
- package/template/src/components/ui/collapsible.jsx +11 -0
- package/template/src/components/ui/command.jsx +116 -0
- package/template/src/components/ui/dialog.jsx +94 -0
- package/template/src/components/ui/drawer.jsx +92 -0
- package/template/src/components/ui/dropdown-menu.jsx +155 -0
- package/template/src/components/ui/form.jsx +138 -0
- package/template/src/components/ui/hover-card.jsx +25 -0
- package/template/src/components/ui/icons.jsx +81 -0
- package/template/src/components/ui/input.jsx +19 -0
- package/template/src/components/ui/label.jsx +16 -0
- package/template/src/components/ui/menubar.jsx +200 -0
- package/template/src/components/ui/navigation-menu.jsx +104 -0
- package/template/src/components/ui/popover.jsx +25 -0
- package/template/src/components/ui/progress.jsx +20 -0
- package/template/src/components/ui/radio-group.jsx +29 -0
- package/template/src/components/ui/scroll-area.jsx +40 -0
- package/template/src/components/ui/select.jsx +120 -0
- package/template/src/components/ui/separator.jsx +25 -0
- package/template/src/components/ui/sheet.jsx +108 -0
- package/template/src/components/ui/skeleton.jsx +10 -0
- package/template/src/components/ui/slider.jsx +23 -0
- package/template/src/components/ui/sonner.jsx +42 -0
- package/template/src/components/ui/switch.jsx +24 -0
- package/template/src/components/ui/table.jsx +83 -0
- package/template/src/components/ui/tabs.jsx +41 -0
- package/template/src/components/ui/textarea.jsx +18 -0
- package/template/src/components/ui/toast.jsx +82 -0
- package/template/src/components/ui/toaster.jsx +33 -0
- package/template/src/components/ui/toggle.jsx +40 -0
- package/template/src/components/ui/tooltip.jsx +24 -0
- package/template/src/hooks/use-toast.js +155 -0
- package/template/src/index.css +61 -0
- package/template/src/index.js +6 -0
- package/template/src/lib/utils.js +11 -0
- package/template/src/main.jsx +15 -0
- package/template/src/pages/Home.jsx +26 -0
- package/template/tailwind.config.cjs +76 -0
- package/template/vite.config.mts +22 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bdpa-react-scaffold-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"preview": "vite preview",
|
|
9
|
+
"server": "node server/server.js",
|
|
10
|
+
"dev:server": "nodemon server/server.js",
|
|
11
|
+
"dev:all": "concurrently \"npm run dev\" \"npm run dev:server\""
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@hookform/resolvers": "^5.2.2",
|
|
15
|
+
"@radix-ui/react-accordion": "^1.2.12",
|
|
16
|
+
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
17
|
+
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
|
18
|
+
"@radix-ui/react-avatar": "^1.1.11",
|
|
19
|
+
"@radix-ui/react-checkbox": "^1.3.3",
|
|
20
|
+
"@radix-ui/react-collapsible": "^1.1.12",
|
|
21
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
22
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
23
|
+
"@radix-ui/react-hover-card": "^1.1.15",
|
|
24
|
+
"@radix-ui/react-label": "^2.1.8",
|
|
25
|
+
"@radix-ui/react-menubar": "^1.1.16",
|
|
26
|
+
"@radix-ui/react-navigation-menu": "^1.2.14",
|
|
27
|
+
"@radix-ui/react-popover": "^1.1.15",
|
|
28
|
+
"@radix-ui/react-progress": "^1.1.8",
|
|
29
|
+
"@radix-ui/react-radio-group": "^1.3.8",
|
|
30
|
+
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
31
|
+
"@radix-ui/react-select": "^2.2.6",
|
|
32
|
+
"@radix-ui/react-separator": "^1.1.8",
|
|
33
|
+
"@radix-ui/react-slider": "^1.3.6",
|
|
34
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
35
|
+
"@radix-ui/react-switch": "^1.2.6",
|
|
36
|
+
"@radix-ui/react-tabs": "^1.1.13",
|
|
37
|
+
"@radix-ui/react-toast": "^1.2.15",
|
|
38
|
+
"@radix-ui/react-toggle": "^1.1.10",
|
|
39
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
40
|
+
"axios": "^1.6.8",
|
|
41
|
+
"bcryptjs": "^2.4.3",
|
|
42
|
+
"class-variance-authority": "^0.7.0",
|
|
43
|
+
"clsx": "^2.1.0",
|
|
44
|
+
"cmdk": "^1.1.1",
|
|
45
|
+
"cors": "^2.8.5",
|
|
46
|
+
"date-fns": "^4.1.0",
|
|
47
|
+
"embla-carousel-react": "^8.6.0",
|
|
48
|
+
"express": "^4.18.2",
|
|
49
|
+
"lucide-react": "^0.575.0",
|
|
50
|
+
"next-themes": "^0.4.6",
|
|
51
|
+
"react": "^18.2.0",
|
|
52
|
+
"react-day-picker": "^9.14.0",
|
|
53
|
+
"react-dom": "^18.2.0",
|
|
54
|
+
"react-hook-form": "^7.72.1",
|
|
55
|
+
"react-router-dom": "^6.20.0",
|
|
56
|
+
"sonner": "^2.0.7",
|
|
57
|
+
"tailwind-merge": "^2.2.1",
|
|
58
|
+
"vaul": "^1.1.2",
|
|
59
|
+
"zod": "^4.3.6"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/react": "^18.2.0",
|
|
63
|
+
"@types/react-dom": "^18.2.0",
|
|
64
|
+
"@vitejs/plugin-react-swc": "^3.5.0",
|
|
65
|
+
"autoprefixer": "^10.4.20",
|
|
66
|
+
"concurrently": "^8.2.2",
|
|
67
|
+
"nodemon": "^3.0.2",
|
|
68
|
+
"postcss": "^8.4.47",
|
|
69
|
+
"tailwindcss": "^3.4.0",
|
|
70
|
+
"tailwindcss-animate": "^1.0.7",
|
|
71
|
+
"vite": "^5.0.0"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const express = require("express");
|
|
3
|
+
const cors = require("cors");
|
|
4
|
+
const bcrypt = require("bcryptjs");
|
|
5
|
+
const { ApiClient } = require("./utils/apiClient");
|
|
6
|
+
|
|
7
|
+
const app = express();
|
|
8
|
+
const PORT = 5000;
|
|
9
|
+
|
|
10
|
+
// ── Middleware ──────────────────────────────────────────────────────────────
|
|
11
|
+
app.use(cors({
|
|
12
|
+
origin: process.env.CLIENT_ORIGIN || "http://localhost:3000",
|
|
13
|
+
credentials: true,
|
|
14
|
+
}));
|
|
15
|
+
app.use(express.json());
|
|
16
|
+
|
|
17
|
+
// ── Password routes ─────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* POST /api/auth/hash
|
|
21
|
+
* Body: { password: string }
|
|
22
|
+
* Returns: { hash: string }
|
|
23
|
+
*/
|
|
24
|
+
app.post("/api/auth/hash", async (req, res) => {
|
|
25
|
+
const { password } = req.body;
|
|
26
|
+
if (!password || typeof password !== "string" || password.trim() === "") {
|
|
27
|
+
return res.status(400).json({ error: "Password must be a non-empty string." });
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const hash = await bcrypt.hash(password, 10);
|
|
31
|
+
return res.json({ hash });
|
|
32
|
+
} catch (err) {
|
|
33
|
+
return res.status(500).json({ error: "Failed to hash password." });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* POST /api/auth/verify
|
|
39
|
+
* Body: { password: string, hash: string }
|
|
40
|
+
* Returns: { valid: boolean }
|
|
41
|
+
*/
|
|
42
|
+
app.post("/api/auth/verify", async (req, res) => {
|
|
43
|
+
const { password, hash } = req.body;
|
|
44
|
+
if (!password || typeof password !== "string") {
|
|
45
|
+
return res.status(400).json({ error: "Password must be a non-empty string." });
|
|
46
|
+
}
|
|
47
|
+
if (!hash || typeof hash !== "string") {
|
|
48
|
+
return res.status(400).json({ error: "Hash must be a valid string." });
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const valid = await bcrypt.compare(password, hash);
|
|
52
|
+
return res.json({ valid });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return res.status(500).json({ error: "Failed to verify password." });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ── Generic external-API proxy ───────────────────────────────────────────────
|
|
59
|
+
// Forwards GET /api/proxy?url=<encoded-url> to an external service.
|
|
60
|
+
// Use for fetching third-party data server-side to avoid CORS issues.
|
|
61
|
+
app.get("/api/proxy", async (req, res) => {
|
|
62
|
+
const targetUrl = req.query.url;
|
|
63
|
+
if (!targetUrl) {
|
|
64
|
+
return res.status(400).json({ error: "Missing required query param: url" });
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const client = new ApiClient("");
|
|
68
|
+
const result = await client.getAll(targetUrl);
|
|
69
|
+
if (!result.success) {
|
|
70
|
+
return res.status(502).json({ error: result.error });
|
|
71
|
+
}
|
|
72
|
+
return res.json(result.data);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return res.status(500).json({ error: "Proxy request failed." });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── Health check ─────────────────────────────────────────────────────────────
|
|
79
|
+
app.get("/api/health", (_req, res) => {
|
|
80
|
+
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
84
|
+
app.listen(PORT, () => {
|
|
85
|
+
console.log(`🚀 Server running on http://localhost:${PORT}`);
|
|
86
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const axios = require("axios");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Server-side Axios helper — mirrors the client ApiClient shape.
|
|
6
|
+
*/
|
|
7
|
+
class ApiClient {
|
|
8
|
+
constructor(baseURL = "") {
|
|
9
|
+
this.instance = axios.create({
|
|
10
|
+
baseURL,
|
|
11
|
+
headers: { "Content-Type": "application/json" },
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
setToken(token) {
|
|
16
|
+
if (token) {
|
|
17
|
+
this.instance.defaults.headers.common["Authorization"] = "Bearer " + token;
|
|
18
|
+
} else {
|
|
19
|
+
delete this.instance.defaults.headers.common["Authorization"];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async request(config) {
|
|
24
|
+
try {
|
|
25
|
+
const res = await this.instance.request(config);
|
|
26
|
+
return { success: true, data: res.data };
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const message =
|
|
29
|
+
error?.response?.data?.message || error?.message || "Request failed";
|
|
30
|
+
return { success: false, error: message };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async getAll(url, config = {}) {
|
|
35
|
+
return this.request({ url, method: "GET", ...config });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getOne(url, config = {}) {
|
|
39
|
+
return this.request({ url, method: "GET", ...config });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async create(url, data, config = {}) {
|
|
43
|
+
return this.request({ url, method: "POST", data, ...config });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async update(url, data, config = {}) {
|
|
47
|
+
return this.request({ url, method: "PUT", data, ...config });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async patch(url, data, config = {}) {
|
|
51
|
+
return this.request({ url, method: "PATCH", data, ...config });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async delete(url, config = {}) {
|
|
55
|
+
return this.request({ url, method: "DELETE", ...config });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { ApiClient };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const bcrypt = require("bcryptjs");
|
|
3
|
+
|
|
4
|
+
const SALT_ROUNDS = 10;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hash a plain-text password.
|
|
8
|
+
* @param {string} password
|
|
9
|
+
* @returns {Promise<string>} bcrypt hash
|
|
10
|
+
*/
|
|
11
|
+
async function hashPassword(password) {
|
|
12
|
+
if (!password || typeof password !== "string" || password.trim().length === 0) {
|
|
13
|
+
throw new Error("Password must be a non-empty string.");
|
|
14
|
+
}
|
|
15
|
+
return bcrypt.hash(password, SALT_ROUNDS);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compare a plain-text password against a bcrypt hash.
|
|
20
|
+
* @param {string} password
|
|
21
|
+
* @param {string} hash
|
|
22
|
+
* @returns {Promise<boolean>}
|
|
23
|
+
*/
|
|
24
|
+
async function verifyPassword(password, hash) {
|
|
25
|
+
if (!password || typeof password !== "string") {
|
|
26
|
+
throw new Error("Password must be a non-empty string.");
|
|
27
|
+
}
|
|
28
|
+
if (!hash || typeof hash !== "string") {
|
|
29
|
+
throw new Error("Hash must be a valid string.");
|
|
30
|
+
}
|
|
31
|
+
return bcrypt.compare(password, hash);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Return a numeric strength score 0-4 for the given password.
|
|
36
|
+
* @param {string} password
|
|
37
|
+
* @returns {number}
|
|
38
|
+
*/
|
|
39
|
+
function getPasswordStrength(password) {
|
|
40
|
+
if (!password) return 0;
|
|
41
|
+
let score = 0;
|
|
42
|
+
if (password.length >= 8) score++;
|
|
43
|
+
if (password.length >= 12) score++;
|
|
44
|
+
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score++;
|
|
45
|
+
if (/d/.test(password)) score++;
|
|
46
|
+
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score++;
|
|
47
|
+
return Math.min(score, 4);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Return a human-readable label for the password strength.
|
|
52
|
+
* @param {string} password
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
function getPasswordStrengthLabel(password) {
|
|
56
|
+
const labels = ["Weak", "Fair", "Good", "Strong", "Very Strong"];
|
|
57
|
+
return labels[getPasswordStrength(password)];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { hashPassword, verifyPassword, getPasswordStrength, getPasswordStrengthLabel };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
|
3
|
+
import { ChevronDown } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const Accordion = AccordionPrimitive.Root
|
|
8
|
+
|
|
9
|
+
const AccordionItem = React.forwardRef(({ className, ...props }, ref) => (
|
|
10
|
+
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
|
|
11
|
+
))
|
|
12
|
+
AccordionItem.displayName = "AccordionItem"
|
|
13
|
+
|
|
14
|
+
const AccordionTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
|
|
15
|
+
<AccordionPrimitive.Header className="flex">
|
|
16
|
+
<AccordionPrimitive.Trigger
|
|
17
|
+
ref={ref}
|
|
18
|
+
className={cn(
|
|
19
|
+
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
{...props}>
|
|
23
|
+
{children}
|
|
24
|
+
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
|
25
|
+
</AccordionPrimitive.Trigger>
|
|
26
|
+
</AccordionPrimitive.Header>
|
|
27
|
+
))
|
|
28
|
+
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
|
29
|
+
|
|
30
|
+
const AccordionContent = React.forwardRef(({ className, children, ...props }, ref) => (
|
|
31
|
+
<AccordionPrimitive.Content
|
|
32
|
+
ref={ref}
|
|
33
|
+
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
34
|
+
{...props}>
|
|
35
|
+
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
|
36
|
+
</AccordionPrimitive.Content>
|
|
37
|
+
))
|
|
38
|
+
|
|
39
|
+
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
|
40
|
+
|
|
41
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import { buttonVariants } from "@/components/ui/button"
|
|
8
|
+
|
|
9
|
+
const AlertDialog = AlertDialogPrimitive.Root
|
|
10
|
+
|
|
11
|
+
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
|
12
|
+
|
|
13
|
+
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
|
14
|
+
|
|
15
|
+
const AlertDialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
|
16
|
+
<AlertDialogPrimitive.Overlay
|
|
17
|
+
className={cn(
|
|
18
|
+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
ref={ref} />
|
|
23
|
+
))
|
|
24
|
+
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
|
25
|
+
|
|
26
|
+
const AlertDialogContent = React.forwardRef(({ className, ...props }, ref) => (
|
|
27
|
+
<AlertDialogPortal>
|
|
28
|
+
<AlertDialogOverlay />
|
|
29
|
+
<AlertDialogPrimitive.Content
|
|
30
|
+
ref={ref}
|
|
31
|
+
className={cn(
|
|
32
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
{...props} />
|
|
36
|
+
</AlertDialogPortal>
|
|
37
|
+
))
|
|
38
|
+
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
|
39
|
+
|
|
40
|
+
const AlertDialogHeader = ({
|
|
41
|
+
className,
|
|
42
|
+
...props
|
|
43
|
+
}) => (
|
|
44
|
+
<div
|
|
45
|
+
className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
|
|
46
|
+
{...props} />
|
|
47
|
+
)
|
|
48
|
+
AlertDialogHeader.displayName = "AlertDialogHeader"
|
|
49
|
+
|
|
50
|
+
const AlertDialogFooter = ({
|
|
51
|
+
className,
|
|
52
|
+
...props
|
|
53
|
+
}) => (
|
|
54
|
+
<div
|
|
55
|
+
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
|
56
|
+
{...props} />
|
|
57
|
+
)
|
|
58
|
+
AlertDialogFooter.displayName = "AlertDialogFooter"
|
|
59
|
+
|
|
60
|
+
const AlertDialogTitle = React.forwardRef(({ className, ...props }, ref) => (
|
|
61
|
+
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
|
|
62
|
+
))
|
|
63
|
+
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
|
64
|
+
|
|
65
|
+
const AlertDialogDescription = React.forwardRef(({ className, ...props }, ref) => (
|
|
66
|
+
<AlertDialogPrimitive.Description
|
|
67
|
+
ref={ref}
|
|
68
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
69
|
+
{...props} />
|
|
70
|
+
))
|
|
71
|
+
AlertDialogDescription.displayName =
|
|
72
|
+
AlertDialogPrimitive.Description.displayName
|
|
73
|
+
|
|
74
|
+
const AlertDialogAction = React.forwardRef(({ className, ...props }, ref) => (
|
|
75
|
+
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
|
|
76
|
+
))
|
|
77
|
+
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
|
78
|
+
|
|
79
|
+
const AlertDialogCancel = React.forwardRef(({ className, ...props }, ref) => (
|
|
80
|
+
<AlertDialogPrimitive.Cancel
|
|
81
|
+
ref={ref}
|
|
82
|
+
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
|
|
83
|
+
{...props} />
|
|
84
|
+
))
|
|
85
|
+
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
AlertDialog,
|
|
89
|
+
AlertDialogPortal,
|
|
90
|
+
AlertDialogOverlay,
|
|
91
|
+
AlertDialogTrigger,
|
|
92
|
+
AlertDialogContent,
|
|
93
|
+
AlertDialogHeader,
|
|
94
|
+
AlertDialogFooter,
|
|
95
|
+
AlertDialogTitle,
|
|
96
|
+
AlertDialogDescription,
|
|
97
|
+
AlertDialogAction,
|
|
98
|
+
AlertDialogCancel,
|
|
99
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
const alertVariants = cva(
|
|
7
|
+
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-background text-foreground",
|
|
12
|
+
destructive:
|
|
13
|
+
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
variant: "default",
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const Alert = React.forwardRef(({ className, variant, ...props }, ref) => (
|
|
23
|
+
<div
|
|
24
|
+
ref={ref}
|
|
25
|
+
role="alert"
|
|
26
|
+
className={cn(alertVariants({ variant }), className)}
|
|
27
|
+
{...props} />
|
|
28
|
+
))
|
|
29
|
+
Alert.displayName = "Alert"
|
|
30
|
+
|
|
31
|
+
const AlertTitle = React.forwardRef(({ className, ...props }, ref) => (
|
|
32
|
+
<h5
|
|
33
|
+
ref={ref}
|
|
34
|
+
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
|
35
|
+
{...props} />
|
|
36
|
+
))
|
|
37
|
+
AlertTitle.displayName = "AlertTitle"
|
|
38
|
+
|
|
39
|
+
const AlertDescription = React.forwardRef(({ className, ...props }, ref) => (
|
|
40
|
+
<div
|
|
41
|
+
ref={ref}
|
|
42
|
+
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
|
43
|
+
{...props} />
|
|
44
|
+
))
|
|
45
|
+
AlertDescription.displayName = "AlertDescription"
|
|
46
|
+
|
|
47
|
+
export { Alert, AlertTitle, AlertDescription }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
const Avatar = React.forwardRef(({ className, ...props }, ref) => (
|
|
9
|
+
<AvatarPrimitive.Root
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
|
|
12
|
+
{...props} />
|
|
13
|
+
))
|
|
14
|
+
Avatar.displayName = AvatarPrimitive.Root.displayName
|
|
15
|
+
|
|
16
|
+
const AvatarImage = React.forwardRef(({ className, ...props }, ref) => (
|
|
17
|
+
<AvatarPrimitive.Image
|
|
18
|
+
ref={ref}
|
|
19
|
+
className={cn("aspect-square h-full w-full", className)}
|
|
20
|
+
{...props} />
|
|
21
|
+
))
|
|
22
|
+
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
|
23
|
+
|
|
24
|
+
const AvatarFallback = React.forwardRef(({ className, ...props }, ref) => (
|
|
25
|
+
<AvatarPrimitive.Fallback
|
|
26
|
+
ref={ref}
|
|
27
|
+
className={cn(
|
|
28
|
+
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
{...props} />
|
|
32
|
+
))
|
|
33
|
+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
|
34
|
+
|
|
35
|
+
export { Avatar, AvatarImage, AvatarFallback }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
const badgeVariants = cva(
|
|
7
|
+
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default:
|
|
12
|
+
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
|
13
|
+
secondary:
|
|
14
|
+
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
15
|
+
destructive:
|
|
16
|
+
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
|
17
|
+
outline: "text-foreground",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: {
|
|
21
|
+
variant: "default",
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
function Badge({
|
|
27
|
+
className,
|
|
28
|
+
variant,
|
|
29
|
+
...props
|
|
30
|
+
}) {
|
|
31
|
+
return (<div className={cn(badgeVariants({ variant }), className)} {...props} />);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { Badge, badgeVariants }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva } from "class-variance-authority";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
13
|
+
destructive:
|
|
14
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
15
|
+
outline:
|
|
16
|
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
17
|
+
secondary:
|
|
18
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
19
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
20
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default: "h-10 px-4 py-2",
|
|
24
|
+
sm: "h-9 rounded-md px-3",
|
|
25
|
+
lg: "h-11 rounded-md px-8",
|
|
26
|
+
icon: "h-10 w-10",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
variant: "default",
|
|
31
|
+
size: "default",
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
37
|
+
const Comp = asChild ? Slot : "button"
|
|
38
|
+
return (
|
|
39
|
+
<Comp
|
|
40
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
41
|
+
ref={ref}
|
|
42
|
+
{...props} />
|
|
43
|
+
);
|
|
44
|
+
})
|
|
45
|
+
Button.displayName = "Button"
|
|
46
|
+
|
|
47
|
+
export { Button, buttonVariants }
|