kmod-cli 1.0.10
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 +53 -0
- package/bin/gen-components.js +68 -0
- package/bin/index.js +153 -0
- package/component-templates/components/access-denied.tsx +130 -0
- package/component-templates/components/breadcumb.tsx +42 -0
- package/component-templates/components/count-down.tsx +94 -0
- package/component-templates/components/count-input.tsx +221 -0
- package/component-templates/components/date-range-calendar/button.tsx +61 -0
- package/component-templates/components/date-range-calendar/calendar.tsx +132 -0
- package/component-templates/components/date-range-calendar/date-input.tsx +259 -0
- package/component-templates/components/date-range-calendar/date-range-picker.tsx +594 -0
- package/component-templates/components/date-range-calendar/label.tsx +31 -0
- package/component-templates/components/date-range-calendar/popover.tsx +32 -0
- package/component-templates/components/date-range-calendar/select.tsx +125 -0
- package/component-templates/components/date-range-calendar/switch.tsx +30 -0
- package/component-templates/components/datetime-picker/button.tsx +61 -0
- package/component-templates/components/datetime-picker/calendar.tsx +156 -0
- package/component-templates/components/datetime-picker/datetime-picker.tsx +75 -0
- package/component-templates/components/datetime-picker/input.tsx +20 -0
- package/component-templates/components/datetime-picker/label.tsx +18 -0
- package/component-templates/components/datetime-picker/period-input.tsx +62 -0
- package/component-templates/components/datetime-picker/popover.tsx +32 -0
- package/component-templates/components/datetime-picker/select.tsx +125 -0
- package/component-templates/components/datetime-picker/time-picker-input.tsx +131 -0
- package/component-templates/components/datetime-picker/time-picker-utils.tsx +204 -0
- package/component-templates/components/datetime-picker/time-picker.tsx +59 -0
- package/component-templates/components/gradient-outline.tsx +233 -0
- package/component-templates/components/gradient-svg.tsx +157 -0
- package/component-templates/components/grid-layout.tsx +69 -0
- package/component-templates/components/hydrate-guard.tsx +40 -0
- package/component-templates/components/image.tsx +92 -0
- package/component-templates/components/loader-slash-gradient.tsx +85 -0
- package/component-templates/components/masonry-gallery.tsx +221 -0
- package/component-templates/components/modal.tsx +110 -0
- package/component-templates/components/multi-select.tsx +447 -0
- package/component-templates/components/non-hydration.tsx +27 -0
- package/component-templates/components/portal.tsx +34 -0
- package/component-templates/components/segments-circle.tsx +235 -0
- package/component-templates/components/single-select.tsx +248 -0
- package/component-templates/components/stroke-circle.tsx +57 -0
- package/component-templates/components/table/column-table.tsx +15 -0
- package/component-templates/components/table/data-table.tsx +339 -0
- package/component-templates/components/table/readme.tsx +95 -0
- package/component-templates/components/table/table.tsx +60 -0
- package/component-templates/components/text-hover-effect.tsx +120 -0
- package/component-templates/components/timout-loader.tsx +52 -0
- package/component-templates/components/toast.tsx +994 -0
- package/component-templates/configs/config.ts +33 -0
- package/component-templates/configs/feature-config.tsx +432 -0
- package/component-templates/configs/keys.ts +7 -0
- package/component-templates/core/api-service.ts +202 -0
- package/component-templates/core/calculate.ts +18 -0
- package/component-templates/core/idb.ts +166 -0
- package/component-templates/core/storage.ts +213 -0
- package/component-templates/hooks/count-down.ts +38 -0
- package/component-templates/hooks/fade-on-scroll.ts +52 -0
- package/component-templates/hooks/safe-action.ts +59 -0
- package/component-templates/hooks/spam-guard.ts +31 -0
- package/component-templates/lib/utils.ts +6 -0
- package/component-templates/providers/feature-guard.tsx +432 -0
- package/component-templates/queries/query.tsx +775 -0
- package/component-templates/utils/colors/color-by-text.ts +307 -0
- package/component-templates/utils/colors/stripe-effect.ts +100 -0
- package/component-templates/utils/hash/hash-aes.ts +35 -0
- package/components.json +348 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# kmod-cli
|
|
2
|
+
|
|
3
|
+
Module install fast to setup into project by command.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Install all/single components and package need to fast setup in projects of React/Next.js
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Using npm
|
|
13
|
+
npm install kmod-cli
|
|
14
|
+
#Or
|
|
15
|
+
yarn add kmod-cli
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx kmod add [command]
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Common Commands
|
|
27
|
+
|
|
28
|
+
- `--all` — Get all coomponent
|
|
29
|
+
- `<component_name> - Get a component
|
|
30
|
+
|
|
31
|
+
### Examples
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx kumod add # empty <command> that show select menu easy use
|
|
35
|
+
#Or
|
|
36
|
+
npx kumod add --all # add all component
|
|
37
|
+
#Or
|
|
38
|
+
npx kumod add button # add button component
|
|
39
|
+
#...any other command
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Contributing
|
|
44
|
+
|
|
45
|
+
Contributions are welcome! Please open issues or pull requests.
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT License
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
**Author:** [KumoD](https://github.com/datnt19213)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
const templatesDir = path.join(__dirname, "../component-templates");
|
|
10
|
+
const outPath = path.join(__dirname, "../components.json");
|
|
11
|
+
|
|
12
|
+
// ================= Helpers =================
|
|
13
|
+
function extractDepsFromFile(filePath) {
|
|
14
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
15
|
+
const importRegex = /from\s+['"]([^'"]+)['"]/g;
|
|
16
|
+
const deps = new Set();
|
|
17
|
+
let match;
|
|
18
|
+
while ((match = importRegex.exec(content))) {
|
|
19
|
+
const dep = match[1];
|
|
20
|
+
if (!dep.startsWith(".") && !dep.startsWith("/")) {
|
|
21
|
+
if (dep === "react" || dep.startsWith("next") || dep === "@/lib/utils") {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
deps.add(dep);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...deps];
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
function normalizePath(p) {
|
|
33
|
+
return p.split(path.sep).join("/"); // thay \ bằng /
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function scanTemplates(dir, base = "component-templates") {
|
|
37
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
38
|
+
const results = {};
|
|
39
|
+
|
|
40
|
+
for (const item of items) {
|
|
41
|
+
const relPath = normalizePath(path.join(base, item.name));
|
|
42
|
+
const fullPath = path.join(dir, item.name);
|
|
43
|
+
|
|
44
|
+
if (item.isDirectory()) {
|
|
45
|
+
// đệ quy scan folder con
|
|
46
|
+
Object.assign(results, scanTemplates(fullPath, relPath));
|
|
47
|
+
} else if (item.isFile() && (item.name.endsWith(".tsx") || item.name.endsWith(".ts"))) {
|
|
48
|
+
// mỗi file = 1 component
|
|
49
|
+
const compName = path.parse(item.name).name;
|
|
50
|
+
const deps = extractDepsFromFile(fullPath);
|
|
51
|
+
|
|
52
|
+
results[compName] = {
|
|
53
|
+
path: relPath,
|
|
54
|
+
dependencies: deps,
|
|
55
|
+
devDependencies: [],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ================= Run =================
|
|
64
|
+
const components = scanTemplates(templatesDir);
|
|
65
|
+
|
|
66
|
+
fs.writeFileSync(outPath, JSON.stringify(components, null, 2));
|
|
67
|
+
|
|
68
|
+
console.log(`✅ Generated components.json with ${Object.keys(components).length} entries`);
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
const rootDir = path.join(__dirname, "..");
|
|
14
|
+
const templatesDir = path.join(rootDir, "templates");
|
|
15
|
+
const componentsConfig = fs.readJsonSync(
|
|
16
|
+
path.join(rootDir, "components.json")
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// ====================== Helpers ======================
|
|
20
|
+
function detectPackageManager() {
|
|
21
|
+
if (fs.existsSync("pnpm-lock.yaml")) return "pnpm";
|
|
22
|
+
if (fs.existsSync("yarn.lock")) return "yarn";
|
|
23
|
+
return "npm";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function installDeps(deps, dev = false) {
|
|
27
|
+
if (!deps || deps.length === 0) return;
|
|
28
|
+
|
|
29
|
+
const pm = detectPackageManager();
|
|
30
|
+
const cmd =
|
|
31
|
+
pm === "yarn"
|
|
32
|
+
? `yarn add ${deps.join(" ")} ${dev ? "-D" : ""}`
|
|
33
|
+
: pm === "pnpm"
|
|
34
|
+
? `pnpm add ${deps.join(" ")} ${dev ? "-D" : ""}`
|
|
35
|
+
: `npm install ${deps.join(" ")} ${dev ? "--save-dev" : ""}`;
|
|
36
|
+
|
|
37
|
+
console.log(`📦 Installing: ${deps.join(", ")} ...`);
|
|
38
|
+
execSync(cmd, { stdio: "inherit" });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getUserPackageJson() {
|
|
42
|
+
const pjPath = path.join(process.cwd(), "package.json");
|
|
43
|
+
if (!fs.existsSync(pjPath)) return null;
|
|
44
|
+
return fs.readJsonSync(pjPath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getMissingDeps(userPkg, deps = [], devDeps = []) {
|
|
48
|
+
const installed = {
|
|
49
|
+
...userPkg.dependencies,
|
|
50
|
+
...userPkg.devDependencies,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const missingDeps = deps.filter((d) => !installed[d]);
|
|
54
|
+
const missingDevDeps = devDeps.filter((d) => !installed[d]);
|
|
55
|
+
|
|
56
|
+
return { missingDeps, missingDevDeps };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function copyComponent(name, collectedDeps) {
|
|
60
|
+
const comp = componentsConfig[name];
|
|
61
|
+
if (!comp) {
|
|
62
|
+
console.error(`❌ Component not found: ${name}`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const src = path.join(rootDir, comp.path);
|
|
67
|
+
|
|
68
|
+
// Base: src/custom
|
|
69
|
+
const destBase = path.join(process.cwd(), "src", "custom");
|
|
70
|
+
await fs.ensureDir(destBase);
|
|
71
|
+
|
|
72
|
+
// Giữ nguyên cấu trúc bên trong component-templates
|
|
73
|
+
const relPath = comp.path.replace(/^component-templates[\\/]/, "");
|
|
74
|
+
const dest = path.join(destBase, relPath);
|
|
75
|
+
|
|
76
|
+
await fs.copy(src, dest, { overwrite: false });
|
|
77
|
+
console.log(`✅ Copied to src/custom/${relPath}`);
|
|
78
|
+
|
|
79
|
+
// Collect deps
|
|
80
|
+
const userPkg = getUserPackageJson();
|
|
81
|
+
if (!userPkg) return;
|
|
82
|
+
|
|
83
|
+
const { missingDeps, missingDevDeps } = getMissingDeps(
|
|
84
|
+
userPkg,
|
|
85
|
+
comp.dependencies,
|
|
86
|
+
comp.devDependencies
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (missingDeps.length) collectedDeps.deps.push(...missingDeps);
|
|
90
|
+
if (missingDevDeps.length) collectedDeps.devDeps.push(...missingDevDeps);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function addComponents() {
|
|
94
|
+
const choices = Object.keys(componentsConfig);
|
|
95
|
+
|
|
96
|
+
const answers = await inquirer.prompt([
|
|
97
|
+
{
|
|
98
|
+
type: "checkbox",
|
|
99
|
+
name: "selected",
|
|
100
|
+
message: "Select components to add:",
|
|
101
|
+
choices: [{ name: "✨ All", value: "--all" }, ...choices],
|
|
102
|
+
},
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
const collectedDeps = { deps: [], devDeps: [] };
|
|
106
|
+
|
|
107
|
+
if (answers.selected.includes("--all")) {
|
|
108
|
+
for (const c of choices) {
|
|
109
|
+
await copyComponent(c, collectedDeps);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
for (const c of answers.selected) {
|
|
113
|
+
await copyComponent(c, collectedDeps);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Remove duplicates
|
|
118
|
+
collectedDeps.deps = [...new Set(collectedDeps.deps)];
|
|
119
|
+
collectedDeps.devDeps = [...new Set(collectedDeps.devDeps)];
|
|
120
|
+
|
|
121
|
+
if (collectedDeps.deps.length || collectedDeps.devDeps.length) {
|
|
122
|
+
console.log(`⚠️ Missing packages detected:`);
|
|
123
|
+
|
|
124
|
+
if (collectedDeps.deps.length)
|
|
125
|
+
console.log(" - deps:", collectedDeps.deps.join(", "));
|
|
126
|
+
if (collectedDeps.devDeps.length)
|
|
127
|
+
console.log(" - devDeps:", collectedDeps.devDeps.join(", "));
|
|
128
|
+
|
|
129
|
+
const { confirm } = await inquirer.prompt([
|
|
130
|
+
{
|
|
131
|
+
type: "confirm",
|
|
132
|
+
name: "confirm",
|
|
133
|
+
message: "Install all missing packages now?",
|
|
134
|
+
default: true,
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
if (confirm) {
|
|
139
|
+
if (collectedDeps.deps.length) installDeps(collectedDeps.deps, false);
|
|
140
|
+
if (collectedDeps.devDeps.length) installDeps(collectedDeps.devDeps, true);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ====================== CLI Commands ======================
|
|
146
|
+
program
|
|
147
|
+
.name("kumod")
|
|
148
|
+
.description("CLI to copy component/templates into project")
|
|
149
|
+
.version("1.0.0");
|
|
150
|
+
|
|
151
|
+
program.command("add").description("Select components to add").action(addComponents);
|
|
152
|
+
|
|
153
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ArrowLeft,
|
|
5
|
+
Ban,
|
|
6
|
+
Mail,
|
|
7
|
+
X,
|
|
8
|
+
} from 'lucide-react';
|
|
9
|
+
import dynamic from 'next/dynamic';
|
|
10
|
+
|
|
11
|
+
import { Button } from '../ui/button';
|
|
12
|
+
import {
|
|
13
|
+
Card,
|
|
14
|
+
CardContent,
|
|
15
|
+
} from '../ui/card';
|
|
16
|
+
|
|
17
|
+
const Lottie = dynamic(() => import("lottie-react"), {
|
|
18
|
+
ssr: false,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
interface AccessDeniedProps {
|
|
22
|
+
title?: string;
|
|
23
|
+
message?: string;
|
|
24
|
+
showBackButton?: boolean;
|
|
25
|
+
showContactButton?: boolean;
|
|
26
|
+
contactButtonLabel?: string;
|
|
27
|
+
backButtonLabel?: string;
|
|
28
|
+
onBack?: () => void;
|
|
29
|
+
onContact?: () => void;
|
|
30
|
+
mailAddress?: string;
|
|
31
|
+
lottieSrc?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function AccessDenied({
|
|
35
|
+
title = "Không có quyền truy cập",
|
|
36
|
+
message = "Bạn không có quyền xem nội dung này. Vui lòng liên hệ người quản trị để được cấp quyền.",
|
|
37
|
+
contactButtonLabel = "Liên hệ",
|
|
38
|
+
backButtonLabel = "Quay lại",
|
|
39
|
+
showBackButton = true,
|
|
40
|
+
showContactButton = true,
|
|
41
|
+
mailAddress = "support@yourdomain.com",
|
|
42
|
+
lottieSrc,
|
|
43
|
+
onBack,
|
|
44
|
+
onContact,
|
|
45
|
+
}: AccessDeniedProps) {
|
|
46
|
+
const handleBack = () => {
|
|
47
|
+
if (onBack) {
|
|
48
|
+
onBack();
|
|
49
|
+
} else {
|
|
50
|
+
window.history.back();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleContact = () => {
|
|
55
|
+
if (onContact) {
|
|
56
|
+
onContact();
|
|
57
|
+
} else {
|
|
58
|
+
// Default contact action - could be email or support page
|
|
59
|
+
window.location.href = `mailto:${mailAddress}`;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex min-h-[400px] items-center justify-center p-4">
|
|
65
|
+
<Card className="w-full max-w-md">
|
|
66
|
+
<CardContent className="p-8 text-center">
|
|
67
|
+
{/* Icon */}
|
|
68
|
+
<div className="mb-6">
|
|
69
|
+
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-red-100">
|
|
70
|
+
<Lottie animationData={lottieSrc} className="h-8 w-8 text-red-600" />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Title */}
|
|
75
|
+
<h2 className="mb-3 text-xl font-semibold text-gray-900">{title}</h2>
|
|
76
|
+
|
|
77
|
+
{/* Message */}
|
|
78
|
+
<p className="mb-6 text-sm leading-relaxed text-gray-600">{message}</p>
|
|
79
|
+
|
|
80
|
+
{/* Actions */}
|
|
81
|
+
<div className="flex flex-col justify-center gap-3 sm:flex-row">
|
|
82
|
+
{showBackButton && (
|
|
83
|
+
<Button variant="outline" onClick={handleBack} className="flex items-center gap-2 bg-transparent">
|
|
84
|
+
<ArrowLeft className="h-4 w-4" />
|
|
85
|
+
{backButtonLabel}
|
|
86
|
+
</Button>
|
|
87
|
+
)}
|
|
88
|
+
{showContactButton && (
|
|
89
|
+
<Button onClick={handleContact} className="flex items-center gap-2">
|
|
90
|
+
<Mail className="h-4 w-4" />
|
|
91
|
+
{contactButtonLabel}
|
|
92
|
+
</Button>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
</CardContent>
|
|
96
|
+
</Card>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Compact version for inline use
|
|
102
|
+
export function AccessDeniedInline({ message = "Bạn không có quyền xem nội dung này" }: { message?: string }) {
|
|
103
|
+
return (
|
|
104
|
+
<div className="flex items-center justify-center rounded-lg border border-red-200 bg-red-50 p-6">
|
|
105
|
+
<div className="flex items-center gap-3 text-red-700">
|
|
106
|
+
<Ban className="h-5 w-5" />
|
|
107
|
+
<span className="text-sm font-medium">{message}</span>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Banner version for top of page
|
|
114
|
+
export function AccessDeniedBanner({ message = "Quyền truy cập bị hạn chế", onDismiss }: { message?: string; onDismiss?: () => void }) {
|
|
115
|
+
return (
|
|
116
|
+
<div className="mb-4 border-l-4 border-red-400 bg-red-50 p-4">
|
|
117
|
+
<div className="flex items-center justify-between">
|
|
118
|
+
<div className="flex items-center">
|
|
119
|
+
<Ban className="mr-3 h-5 w-5 text-red-400" />
|
|
120
|
+
<p className="text-sm text-red-700">{message}</p>
|
|
121
|
+
</div>
|
|
122
|
+
{onDismiss && (
|
|
123
|
+
<Button variant="ghost" size="icon" onClick={onDismiss} className="text-red-700 hover:text-red-800">
|
|
124
|
+
<X className="h-4 w-4" />
|
|
125
|
+
</Button>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// components/Breadcrumb.tsx
|
|
2
|
+
import React, { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
import { ChevronRight } from 'lucide-react';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
|
|
7
|
+
import { cn } from '../lib/utils';
|
|
8
|
+
|
|
9
|
+
type BreadcrumbItem = {
|
|
10
|
+
label: ReactNode;
|
|
11
|
+
as?: "link" | "a";
|
|
12
|
+
href?: string;
|
|
13
|
+
isCurrent?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
interface BreadcrumbProps {
|
|
17
|
+
items: BreadcrumbItem[];
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Breadcrumbs: React.FC<BreadcrumbProps> = ({ items, className }) => {
|
|
22
|
+
return (
|
|
23
|
+
<nav className={cn("text-muted-foreground flex items-center space-x-1 text-sm", className)} aria-label="Breadcrumb">
|
|
24
|
+
{items.map((item, index) => (
|
|
25
|
+
<React.Fragment key={index}>
|
|
26
|
+
{index !== 0 && <ChevronRight className="text-muted-foreground h-4 w-4" />}
|
|
27
|
+
{item.href && !item.isCurrent && item.as === "link" ? (
|
|
28
|
+
<Link href={item.href} className="hover:text-foreground transition-colors">
|
|
29
|
+
{item.label}
|
|
30
|
+
</Link>
|
|
31
|
+
) : item.href && !item.isCurrent && item.as === "a" ? (
|
|
32
|
+
<a href={item.href} className="hover:text-foreground transition-colors">
|
|
33
|
+
{item.label}
|
|
34
|
+
</a>
|
|
35
|
+
) : (
|
|
36
|
+
<span className="text-foreground font-medium">{item.label}</span>
|
|
37
|
+
)}
|
|
38
|
+
</React.Fragment>
|
|
39
|
+
))}
|
|
40
|
+
</nav>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
ReactElement,
|
|
3
|
+
useEffect,
|
|
4
|
+
} from 'react';
|
|
5
|
+
|
|
6
|
+
import { useCountdownStore } from '../hooks/count-down';
|
|
7
|
+
|
|
8
|
+
export type CountdownProps = {
|
|
9
|
+
initialTime?: number;
|
|
10
|
+
onComplete?: () => void;
|
|
11
|
+
onCountDown?: () => void;
|
|
12
|
+
blockInitialTime?: boolean; // Optional prop to control initial time setting
|
|
13
|
+
freeze?: boolean; // Optional prop to freeze the countdown
|
|
14
|
+
freezeAfter?: number; // Optional prop to freeze countdown after a certain time
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Countdown component
|
|
19
|
+
* @param {number} initialTime - Initial time in seconds, defaults to 0
|
|
20
|
+
* @param {boolean} blockInitialTime - If true, the initial time will not be set automatically
|
|
21
|
+
* @param {boolean} freeze - If true, the countdown will freeze after it reaches 0
|
|
22
|
+
* @param {number} freezeAfter - The time in seconds after which the countdown will freeze
|
|
23
|
+
* @param {() => void} onComplete - Callback function that will be called when the countdown is completed
|
|
24
|
+
* @param {() => void} onCountDown - Callback function that will be called every second while the countdown is running
|
|
25
|
+
* @returns {ReactElement} The countdown component
|
|
26
|
+
*/
|
|
27
|
+
export const Countdown: React.FC<CountdownProps> = ({
|
|
28
|
+
initialTime,
|
|
29
|
+
blockInitialTime = false,
|
|
30
|
+
freeze = false,
|
|
31
|
+
freezeAfter,
|
|
32
|
+
onComplete,
|
|
33
|
+
onCountDown,
|
|
34
|
+
}: CountdownProps): ReactElement => {
|
|
35
|
+
const {
|
|
36
|
+
decrement,
|
|
37
|
+
isRunning,
|
|
38
|
+
setTime,
|
|
39
|
+
stop,
|
|
40
|
+
timeLeft,
|
|
41
|
+
freezing,
|
|
42
|
+
freeze: doFreeze,
|
|
43
|
+
} = useCountdownStore();
|
|
44
|
+
|
|
45
|
+
// Init time if not blocked and timeLeft is 0
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!blockInitialTime && timeLeft === 0 && initialTime) {
|
|
48
|
+
setTime(initialTime);
|
|
49
|
+
}
|
|
50
|
+
}, [blockInitialTime, initialTime]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (blockInitialTime) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (timeLeft === 0 && initialTime) {
|
|
57
|
+
setTime(initialTime);
|
|
58
|
+
}
|
|
59
|
+
}, [initialTime]);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (timeLeft === 0 && isRunning) {
|
|
63
|
+
stop();
|
|
64
|
+
if (onComplete) {
|
|
65
|
+
onComplete();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}, [timeLeft, isRunning, onComplete]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!isRunning || freeze || freezing) return;
|
|
72
|
+
|
|
73
|
+
// 🚦 Auto freeze
|
|
74
|
+
if (freezeAfter !== undefined && timeLeft === freezeAfter) {
|
|
75
|
+
doFreeze();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (isRunning && timeLeft > 0) {
|
|
79
|
+
const timer = setInterval(() => {
|
|
80
|
+
decrement();
|
|
81
|
+
onCountDown?.();
|
|
82
|
+
}, 1000);
|
|
83
|
+
|
|
84
|
+
return () => clearInterval(timer);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (timeLeft === 0 && isRunning) {
|
|
88
|
+
stop();
|
|
89
|
+
onComplete?.();
|
|
90
|
+
}
|
|
91
|
+
}, [isRunning, freeze, freezing, timeLeft, freezeAfter]);
|
|
92
|
+
|
|
93
|
+
return <>{timeLeft}</>;
|
|
94
|
+
};
|