OhMyDitzzy commited on
Commit
6d9f36a
·
1 Parent(s): 0247e61

Feat: add project

Browse files
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .DS_Store
4
+ vite.config.ts.*
5
+ *.tar.gz
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package.json ./
6
+ COPY tsconfig.json ./
7
+ COPY vite.config.ts ./
8
+ COPY tailwind.config.ts ./
9
+ COPY postcss.config.js ./
10
+
11
+ RUN npm ci
12
+
13
+ COPY src ./src
14
+ COPY components.json ./
15
+
16
+ RUN npm run build
17
+
18
+ EXPOSE 7860
19
+ ENV PORT=7860
20
+ ENV NODE_ENV=production
21
+
22
+ CMD ["node", "dist/index.cjs"]
_build/script.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { rmSync } from "node:fs";
2
+ import { build as viteBuild } from "vite";
3
+ import { build as esBuild } from "esbuild";
4
+
5
+ async function buildAll() {
6
+ rmSync("dist", { recursive: true, force: true });
7
+
8
+ console.info("[INFO] Building client...");
9
+ await viteBuild();
10
+
11
+ console.info("[INFO] Building server...");
12
+ await esBuild({
13
+ entryPoints: ["src/server/index.ts"],
14
+ platform: "node",
15
+ bundle: true,
16
+ format: "cjs",
17
+ outfile: "dist/index.cjs",
18
+ define: {
19
+ "process.env.NODE_ENV": '"production"',
20
+ },
21
+ minify: true,
22
+ logLevel: "info"
23
+ });
24
+ }
25
+
26
+ buildAll().catch((err) => {
27
+ console.error(err);
28
+ process.exit(1);
29
+ });
components.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "src/client/index.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "utils": "@/lib/utils",
17
+ "ui": "@/components/ui",
18
+ "lib": "@/lib",
19
+ "hooks": "@/hooks"
20
+ },
21
+ "registries": {}
22
+ }
package.json ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ditzzy_api",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "scripts": {
6
+ "dev": "NODE_ENV=development tsx src/server/index.ts",
7
+ "start": "NODE_ENV=production node dist/index.cjs",
8
+ "check": "tsc",
9
+ "build": "tsx _build/script.ts"
10
+ },
11
+ "author": "Ditzzy",
12
+ "license": "MIT",
13
+ "type": "module",
14
+ "devDependencies": {
15
+ "@tailwindcss/typography": "^0.5.19",
16
+ "@tailwindcss/vite": "^4.1.18",
17
+ "@types/express": "^5.0.6",
18
+ "@types/node": "^25.0.7",
19
+ "@types/prismjs": "^1.26.5",
20
+ "@types/react": "^19.2.8",
21
+ "@types/react-dom": "^19.2.3",
22
+ "@vitejs/plugin-react": "^5.1.2",
23
+ "autoprefixer": "^10.4.23",
24
+ "chokidar": "^5.0.0",
25
+ "esbuild": "^0.27.2",
26
+ "glob": "^13.0.0",
27
+ "postcss": "^8.4.47",
28
+ "tailwindcss": "^3.4.17",
29
+ "tsx": "^4.21.0",
30
+ "typescript": "^5.9.3",
31
+ "vite": "^7.3.1"
32
+ },
33
+ "dependencies": {
34
+ "@radix-ui/react-dialog": "^1.1.15",
35
+ "@radix-ui/react-select": "^2.2.6",
36
+ "@radix-ui/react-separator": "^1.1.8",
37
+ "@radix-ui/react-slot": "^1.2.4",
38
+ "@radix-ui/react-tabs": "^1.1.13",
39
+ "axios": "^1.13.2",
40
+ "class-variance-authority": "^0.7.1",
41
+ "clsx": "^2.1.1",
42
+ "express": "^5.2.1",
43
+ "framer-motion": "^12.26.2",
44
+ "lucide-react": "^0.562.0",
45
+ "prismjs": "^1.30.0",
46
+ "react": "^19.2.3",
47
+ "react-dom": "^19.2.3",
48
+ "react-router-dom": "^7.12.0",
49
+ "recharts": "^3.6.0",
50
+ "tailwind-merge": "^3.4.0",
51
+ "tailwindcss-animate": "^1.0.7"
52
+ }
53
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
src/client/App.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Route, Routes } from "react-router-dom";
2
+ import { lazy, Suspense } from "react";
3
+ import { ErrorBoundary } from "@/components/ErrorBoundary";
4
+ import { Loader2 } from "lucide-react";
5
+
6
+ const Home = lazy(() => import("./pages/Home"));
7
+ const Docs = lazy(() => import("./pages/Docs"));
8
+ const NotFound = lazy(() => import("./pages/not-found"));
9
+
10
+ function PageLoader() {
11
+ return (
12
+ <div className="min-h-screen flex items-center justify-center bg-background">
13
+ <div className="text-center">
14
+ <Loader2 className="w-8 h-8 text-purple-400 animate-spin mx-auto mb-4" />
15
+ <p className="text-gray-400">Loading...</p>
16
+ </div>
17
+ </div>
18
+ );
19
+ }
20
+
21
+ export default function App() {
22
+ return (
23
+ <ErrorBoundary>
24
+ <Suspense fallback={<PageLoader />}>
25
+ <Routes>
26
+ <Route path="/" element={<Home />} />
27
+ <Route path="/docs" element={<Docs />} />
28
+ <Route path="*" element={<NotFound />} />
29
+ </Routes>
30
+ </Suspense>
31
+ </ErrorBoundary>
32
+ );
33
+ }
src/client/hooks/usePlugin.ts ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from "react";
2
+
3
+ export interface PluginParameter {
4
+ name: string;
5
+ type: "string" | "number" | "boolean" | "array" | "object";
6
+ required: boolean;
7
+ description: string;
8
+ example?: any;
9
+ default?: any;
10
+ enum?: any[];
11
+ pattern?: string;
12
+ }
13
+
14
+ export interface PluginResponse {
15
+ status: number;
16
+ description: string;
17
+ example: any;
18
+ }
19
+
20
+ export interface PluginParameters {
21
+ query?: PluginParameter[];
22
+ body?: PluginParameter[];
23
+ headers?: PluginParameter[];
24
+ path?: PluginParameter[];
25
+ }
26
+
27
+ export interface PluginMetadata {
28
+ name: string;
29
+ description: string;
30
+ version: string;
31
+ category: string[];
32
+ method: string;
33
+ endpoint: string;
34
+ aliases: string[];
35
+ tags?: string[];
36
+ parameters?: PluginParameters;
37
+ responses?: {
38
+ [statusCode: number]: PluginResponse;
39
+ };
40
+ }
41
+
42
+ export interface ApiStats {
43
+ totalRequests: number;
44
+ totalSuccess: number;
45
+ totalFailed: number;
46
+ uniqueVisitors: number;
47
+ successRate: string;
48
+ uptime: {
49
+ ms: number;
50
+ hours: number;
51
+ days: number;
52
+ formatted: string;
53
+ };
54
+ }
55
+
56
+ export interface Category {
57
+ name: string;
58
+ count: number;
59
+ }
60
+
61
+ export function usePlugins() {
62
+ const [plugins, setPlugins] = useState<PluginMetadata[]>([]);
63
+ const [loading, setLoading] = useState(true);
64
+ const [error, setError] = useState<string | null>(null);
65
+
66
+ useEffect(() => {
67
+ fetchPlugins();
68
+ }, []);
69
+
70
+ const fetchPlugins = async () => {
71
+ try {
72
+ setLoading(true);
73
+ const response = await fetch("/api/plugins");
74
+ const data = await response.json();
75
+
76
+ if (data.success) {
77
+ setPlugins(data.plugins);
78
+ } else {
79
+ setError("Failed to load plugins");
80
+ }
81
+ } catch (err) {
82
+ setError(err instanceof Error ? err.message : "Unknown error");
83
+ } finally {
84
+ setLoading(false);
85
+ }
86
+ };
87
+
88
+ return { plugins, loading, error, refetch: fetchPlugins };
89
+ }
90
+
91
+ export function useStats() {
92
+ const [stats, setStats] = useState<ApiStats | null>(null);
93
+ const [loading, setLoading] = useState(true);
94
+ const [error, setError] = useState<string | null>(null);
95
+
96
+ useEffect(() => {
97
+ fetchStats();
98
+ const interval = setInterval(fetchStats, 30000);
99
+ return () => clearInterval(interval);
100
+ }, []);
101
+
102
+ const fetchStats = async () => {
103
+ try {
104
+ setLoading(true);
105
+ const response = await fetch("/api/stats");
106
+ const data = await response.json();
107
+
108
+ if (data.success) {
109
+ setStats(data.stats.global);
110
+ } else {
111
+ setError("Failed to load stats");
112
+ }
113
+ } catch (err) {
114
+ setError(err instanceof Error ? err.message : "Unknown error");
115
+ } finally {
116
+ setLoading(false);
117
+ }
118
+ };
119
+
120
+ return { stats, loading, error, refetch: fetchStats };
121
+ }
122
+
123
+ export function useCategories() {
124
+ const [categories, setCategories] = useState<Category[]>([]);
125
+ const [loading, setLoading] = useState(true);
126
+ const [error, setError] = useState<string | null>(null);
127
+
128
+ useEffect(() => {
129
+ fetchCategories();
130
+ }, []);
131
+
132
+ const fetchCategories = async () => {
133
+ try {
134
+ setLoading(true);
135
+ const response = await fetch("/api/categories");
136
+ const data = await response.json();
137
+
138
+ if (data.success) {
139
+ setCategories(data.categories);
140
+ } else {
141
+ setError("Failed to load categories");
142
+ }
143
+ } catch (err) {
144
+ setError(err instanceof Error ? err.message : "Unknown error");
145
+ } finally {
146
+ setLoading(false);
147
+ }
148
+ };
149
+
150
+ return { categories, loading, error, refetch: fetchCategories };
151
+ }
152
+
153
+ export function usePluginsByCategory(category: string | null) {
154
+ const [plugins, setPlugins] = useState<PluginMetadata[]>([]);
155
+ const [loading, setLoading] = useState(true);
156
+ const [error, setError] = useState<string | null>(null);
157
+
158
+ useEffect(() => {
159
+ if (!category) {
160
+ fetchAllPlugins();
161
+ } else {
162
+ fetchPluginsByCategory(category);
163
+ }
164
+ }, [category]);
165
+
166
+ const fetchAllPlugins = async () => {
167
+ try {
168
+ setLoading(true);
169
+ const response = await fetch("/api/plugins");
170
+ const data = await response.json();
171
+
172
+ if (data.success) {
173
+ setPlugins(data.plugins);
174
+ } else {
175
+ setError("Failed to load plugins");
176
+ }
177
+ } catch (err) {
178
+ setError(err instanceof Error ? err.message : "Unknown error");
179
+ } finally {
180
+ setLoading(false);
181
+ }
182
+ };
183
+
184
+ const fetchPluginsByCategory = async (cat: string) => {
185
+ try {
186
+ setLoading(true);
187
+ const response = await fetch(`/api/plugins/category/${cat}`);
188
+ const data = await response.json();
189
+
190
+ if (data.success) {
191
+ setPlugins(data.plugins);
192
+ } else {
193
+ setError("Failed to load plugins");
194
+ }
195
+ } catch (err) {
196
+ setError(err instanceof Error ? err.message : "Unknown error");
197
+ } finally {
198
+ setLoading(false);
199
+ }
200
+ };
201
+
202
+ return { plugins, loading, error };
203
+ }
src/client/index.css ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
2
+
3
+ @tailwind base;
4
+ @tailwind components;
5
+ @tailwind utilities;
6
+
7
+ :root {
8
+ /* Dark mode default */
9
+ --background: 240 10% 4%;
10
+ --foreground: 0 0% 98%;
11
+
12
+ --card: 240 10% 6%;
13
+ --card-foreground: 0 0% 98%;
14
+
15
+ --popover: 240 10% 6%;
16
+ --popover-foreground: 0 0% 98%;
17
+
18
+ --primary: 250 100% 65%;
19
+ --primary-foreground: 0 0% 100%;
20
+
21
+ --secondary: 240 5% 15%;
22
+ --secondary-foreground: 0 0% 98%;
23
+
24
+ --muted: 240 5% 15%;
25
+ --muted-foreground: 240 5% 65%;
26
+
27
+ --accent: 250 100% 65%;
28
+ --accent-foreground: 0 0% 100%;
29
+
30
+ --destructive: 0 62.8% 30.6%;
31
+ --destructive-foreground: 0 0% 98%;
32
+
33
+ --border: 240 5% 15%;
34
+ --input: 240 5% 15%;
35
+ --ring: 250 100% 65%;
36
+
37
+ --radius: 0.75rem;
38
+
39
+ --font-sans: 'Inter', sans-serif;
40
+ --font-display: 'Space Grotesk', sans-serif;
41
+ --font-mono: 'JetBrains Mono', monospace;
42
+ }
43
+
44
+ @layer base {
45
+ * {
46
+ @apply border-border;
47
+ }
48
+ body {
49
+ @apply bg-background text-foreground font-sans antialiased selection:bg-primary/20 selection:text-primary;
50
+ }
51
+ h1, h2, h3, h4, h5, h6 {
52
+ @apply font-display tracking-tight;
53
+ }
54
+ }
55
+
56
+ /* Custom Utilities */
57
+ .glass {
58
+ @apply bg-background/60 backdrop-blur-xl border border-white/5;
59
+ }
60
+
61
+ .text-gradient {
62
+ @apply bg-clip-text text-transparent bg-gradient-to-r from-white via-white/80 to-white/60;
63
+ }
64
+
65
+ .text-gradient-primary {
66
+ @apply bg-clip-text text-transparent bg-gradient-to-r from-primary via-purple-400 to-pink-500;
67
+ }
68
+
69
+ .card-hover {
70
+ @apply transition-all duration-300 hover:border-primary/50 hover:shadow-lg hover:shadow-primary/5 hover:-translate-y-1;
71
+ }
72
+
73
+ /* Scrollbar */
74
+ ::-webkit-scrollbar {
75
+ width: 8px;
76
+ height: 8px;
77
+ }
78
+ ::-webkit-scrollbar-track {
79
+ background: transparent;
80
+ }
81
+ ::-webkit-scrollbar-thumb {
82
+ @apply bg-muted rounded-full hover:bg-muted-foreground/50 transition-colors;
83
+ }
84
+
85
+ /* Syntax Highlighting overrides */
86
+ code[class*="language-"],
87
+ pre[class*="language-"] {
88
+ @apply text-sm font-mono !bg-transparent !text-sm;
89
+ text-shadow: none !important;
90
+ }
src/client/main.tsx ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createRoot } from "react-dom/client";
2
+ import React from "react";
3
+ import App from "./App";
4
+ import "./index.css";
5
+ import { BrowserRouter } from "react-router-dom";
6
+
7
+ createRoot(document.getElementById('root')!).render(
8
+ <React.StrictMode>
9
+ <BrowserRouter>
10
+ <App />
11
+ </BrowserRouter>
12
+ </React.StrictMode>
13
+ )
src/client/pages/Docs.tsx ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useMemo } from "react";
2
+ import { Navbar } from "@/components/Navbar";
3
+ import { PluginCard } from "@/components/PluginCard";
4
+ import { Footer } from "@/components/Footer";
5
+ import { StatsCard } from "@/components/StatsCard";
6
+ import { VisitorChart } from "@/components/VisitorChart";
7
+ import { usePlugins, useStats } from "@/client/hooks/usePlugin";
8
+ import { Activity, CheckCircle2, XCircle, TrendingUp, Loader2, Search, X } from "lucide-react";
9
+ import { Input } from "@/components/ui/input";
10
+ import { Badge } from "@/components/ui/badge";
11
+ import { Button } from "@/components/ui/button";
12
+
13
+ export default function Docs() {
14
+ const { plugins, loading: pluginsLoading } = usePlugins();
15
+ const { stats, loading: statsLoading } = useStats();
16
+
17
+ const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
18
+ const [searchQuery, setSearchQuery] = useState("");
19
+ const [selectedTags, setSelectedTags] = useState<string[]>([]);
20
+
21
+ const allTags = useMemo(() => {
22
+ const tagsSet = new Set<string>();
23
+ plugins.forEach(plugin => {
24
+ plugin.tags?.forEach(tag => tagsSet.add(tag));
25
+ });
26
+ return Array.from(tagsSet).sort();
27
+ }, [plugins]);
28
+
29
+ const filteredPlugins = useMemo(() => {
30
+ let filtered = plugins;
31
+
32
+ if (selectedCategory) {
33
+ filtered = filtered.filter((p) => p.category.includes(selectedCategory));
34
+ }
35
+
36
+ if (searchQuery.trim()) {
37
+ const query = searchQuery.toLowerCase();
38
+ filtered = filtered.filter((p) =>
39
+ p.name.toLowerCase().includes(query) ||
40
+ p.description.toLowerCase().includes(query) ||
41
+ p.endpoint.toLowerCase().includes(query) ||
42
+ p.tags?.some(tag => tag.toLowerCase().includes(query))
43
+ );
44
+ }
45
+
46
+ if (selectedTags.length > 0) {
47
+ filtered = filtered.filter((p) =>
48
+ selectedTags.every(tag => p.tags?.includes(tag))
49
+ );
50
+ }
51
+
52
+ return filtered;
53
+ }, [plugins, selectedCategory, searchQuery, selectedTags]);
54
+
55
+ const toggleTag = (tag: string) => {
56
+ setSelectedTags(prev =>
57
+ prev.includes(tag)
58
+ ? prev.filter(t => t !== tag)
59
+ : [...prev, tag]
60
+ );
61
+ };
62
+
63
+ const clearAllFilters = () => {
64
+ setSearchQuery("");
65
+ setSelectedTags([]);
66
+ setSelectedCategory(null);
67
+ };
68
+
69
+ return (
70
+ <div className="min-h-screen bg-background flex flex-col font-sans selection:bg-primary/30">
71
+ {/* Navbar with Categories in Hamburger Menu */}
72
+ <Navbar onCategorySelect={setSelectedCategory} selectedCategory={selectedCategory} />
73
+
74
+ {/* Main Content */}
75
+ <main className="flex-grow">
76
+ <div className="max-w-7xl mx-auto px-4 py-8">
77
+
78
+ {/* Statistics Cards */}
79
+ <div className="mb-8">
80
+ <h2 className="text-2xl font-bold text-white mb-4">API Statistics</h2>
81
+ {statsLoading ? (
82
+ <div className="flex items-center justify-center py-12">
83
+ <Loader2 className="w-8 h-8 text-purple-400 animate-spin" />
84
+ </div>
85
+ ) : stats ? (
86
+ <>
87
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
88
+ <StatsCard
89
+ title="Total Requests"
90
+ value={stats.totalRequests.toLocaleString()}
91
+ icon={Activity}
92
+ color="purple"
93
+ />
94
+ <StatsCard
95
+ title="Successful"
96
+ value={stats.totalSuccess.toLocaleString()}
97
+ icon={CheckCircle2}
98
+ color="green"
99
+ />
100
+ <StatsCard
101
+ title="Failed"
102
+ value={stats.totalFailed.toLocaleString()}
103
+ icon={XCircle}
104
+ color="red"
105
+ />
106
+ <StatsCard
107
+ title="Success Rate"
108
+ value={`${stats.successRate}%`}
109
+ icon={TrendingUp}
110
+ color="blue"
111
+ />
112
+ </div>
113
+
114
+ {/* Visitor Chart */}
115
+ <VisitorChart />
116
+ </>
117
+ ) : (
118
+ <div className="text-sm text-gray-500">Failed to load statistics</div>
119
+ )}
120
+ </div>
121
+
122
+ {/* Search and Filter Section */}
123
+ <div className="mb-6 space-y-4">
124
+ {/* Search Bar */}
125
+ <div className="relative">
126
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
127
+ <Input
128
+ type="text"
129
+ placeholder="Search endpoints, descriptions, or tags..."
130
+ value={searchQuery}
131
+ onChange={(e) => setSearchQuery(e.target.value)}
132
+ className="pl-10 bg-white/[0.02] border-white/10 text-white placeholder:text-gray-500 focus:border-purple-500 h-12"
133
+ />
134
+ {searchQuery && (
135
+ <button
136
+ onClick={() => setSearchQuery("")}
137
+ className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white"
138
+ >
139
+ <X className="w-4 h-4" />
140
+ </button>
141
+ )}
142
+ </div>
143
+
144
+ {/* Tags Filter */}
145
+ {allTags.length > 0 && (
146
+ <div>
147
+ <div className="flex items-center justify-between mb-2">
148
+ <h3 className="text-sm font-semibold text-gray-400">Filter by Tags</h3>
149
+ {selectedTags.length > 0 && (
150
+ <button
151
+ onClick={() => setSelectedTags([])}
152
+ className="text-xs text-purple-400 hover:text-purple-300"
153
+ >
154
+ Clear tags
155
+ </button>
156
+ )}
157
+ </div>
158
+ <div className="flex flex-wrap gap-2">
159
+ {allTags.map((tag) => (
160
+ <Badge
161
+ key={tag}
162
+ onClick={() => toggleTag(tag)}
163
+ className={`cursor-pointer transition-colors ${
164
+ selectedTags.includes(tag)
165
+ ? "bg-purple-500/30 text-purple-300 border-purple-500 hover:bg-purple-500/40"
166
+ : "bg-white/5 text-gray-400 border-white/10 hover:bg-white/10"
167
+ } border`}
168
+ >
169
+ {tag}
170
+ </Badge>
171
+ ))}
172
+ </div>
173
+ </div>
174
+ )}
175
+
176
+ {/* Active Filters Summary */}
177
+ {(selectedCategory || searchQuery || selectedTags.length > 0) && (
178
+ <div className="flex items-center gap-2 flex-wrap">
179
+ <span className="text-sm text-gray-400">Active filters:</span>
180
+ {selectedCategory && (
181
+ <Badge className="bg-blue-500/20 text-blue-400 border-blue-500/50">
182
+ Category: {selectedCategory}
183
+ </Badge>
184
+ )}
185
+ {searchQuery && (
186
+ <Badge className="bg-green-500/20 text-green-400 border-green-500/50">
187
+ Search: "{searchQuery}"
188
+ </Badge>
189
+ )}
190
+ {selectedTags.map(tag => (
191
+ <Badge key={tag} className="bg-purple-500/20 text-purple-400 border-purple-500/50">
192
+ Tag: {tag}
193
+ </Badge>
194
+ ))}
195
+ <Button
196
+ variant="ghost"
197
+ size="sm"
198
+ onClick={clearAllFilters}
199
+ className="text-xs text-gray-400 hover:text-white"
200
+ >
201
+ Clear all
202
+ </Button>
203
+ </div>
204
+ )}
205
+ </div>
206
+
207
+ {/* Results Count */}
208
+ <div className="mb-6">
209
+ <h2 className="text-2xl font-bold text-white capitalize">
210
+ {selectedCategory ? `${selectedCategory} Endpoints` : "All Endpoints"}
211
+ </h2>
212
+ <p className="text-gray-400 text-sm mt-1">
213
+ Showing {filteredPlugins.length} of {plugins.length} endpoint{filteredPlugins.length !== 1 ? 's' : ''}
214
+ </p>
215
+ </div>
216
+
217
+ {/* Plugins List */}
218
+ <div className="space-y-6">
219
+ {pluginsLoading ? (
220
+ <div className="flex items-center justify-center py-20">
221
+ <Loader2 className="w-8 h-8 text-purple-400 animate-spin" />
222
+ </div>
223
+ ) : filteredPlugins.length > 0 ? (
224
+ filteredPlugins.map((plugin) => (
225
+ <PluginCard key={plugin.endpoint} plugin={plugin} />
226
+ ))
227
+ ) : (
228
+ <div className="text-center py-20">
229
+ <div className="text-gray-400 text-lg mb-2">No endpoints found</div>
230
+ <div className="text-gray-600 text-sm mb-4">
231
+ {searchQuery || selectedTags.length > 0
232
+ ? "Try adjusting your search or filters"
233
+ : selectedCategory
234
+ ? "No plugins available in this category"
235
+ : "No plugins available"}
236
+ </div>
237
+ {(searchQuery || selectedTags.length > 0 || selectedCategory) && (
238
+ <Button
239
+ onClick={clearAllFilters}
240
+ variant="outline"
241
+ className="border-white/10 text-purple-400 hover:bg-purple-500/10"
242
+ >
243
+ Clear all filters
244
+ </Button>
245
+ )}
246
+ </div>
247
+ )}
248
+ </div>
249
+
250
+ </div>
251
+ </main>
252
+
253
+ <Footer />
254
+ </div>
255
+ );
256
+ }
src/client/pages/Home.tsx ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { CodeSnippet } from "@/components/CodeSnippet";
2
+ import { Footer } from "@/components/Footer";
3
+ import { Button } from "@/components/ui/button";
4
+ import { motion } from "framer-motion";
5
+ import { DollarSign, Code2, Key, ChevronDown } from "lucide-react";
6
+ import { Link } from "react-router-dom";
7
+ import { useState } from "react";
8
+ import { getBaseUrl } from "@/lib/api-url";
9
+
10
+ export default function Home() {
11
+ const exampleFetchCode = `// Completely easy to use API!
12
+ const res = await fetch("${getBaseUrl()}/api/data");
13
+ const json = await res.json();
14
+
15
+ console.log(json)
16
+ // Check out your console!
17
+ `;
18
+
19
+ const [openFaq, setOpenFaq] = useState<number | null>(null);
20
+
21
+ const faqs = [
22
+ {
23
+ question: "Do I need an API key to use DitzzyAPI?",
24
+ answer: "No! DitzzyAPI is completely free and doesn't require any API key. Just start making requests right away."
25
+ },
26
+ {
27
+ question: "Is there really no usage limit?",
28
+ answer: "DitzzyAPI is free and unlimited for everyone. However, we implement rate limiting to ensure fair usage and keep our servers stable for all users."
29
+ },
30
+ {
31
+ question: "What are the rate limits?",
32
+ answer: "We apply reasonable rate limits per IP address to prevent abuse and maintain server stability. Normal usage patterns are well within these limits."
33
+ },
34
+ {
35
+ question: "How do you keep the service free?",
36
+ answer: "We're passionate about supporting the developer community. Rate limits help us manage costs while keeping the service free for everyone."
37
+ }
38
+ ];
39
+
40
+ return (
41
+ <div className="min-h-screen bg-background flex flex-col font-sans selection:bg-primary/30">
42
+ <main className="flex-grow pt-8">
43
+ {/* Hero Section */}
44
+ <section className="relative overflow-hidden py-24 sm:py-32">
45
+ <div className="absolute top-0 left-1/2 -translate-x-1/2 w-[1000px] h-[600px] bg-primary/20 blur-[120px] rounded-full opacity-50 pointer-events-none" />
46
+ <div className="absolute bottom-0 right-0 w-[800px] h-[600px] bg-purple-500/10 blur-[100px] rounded-full opacity-30 pointer-events-none" />
47
+
48
+ <div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col items-center text-center">
49
+ <motion.div
50
+ initial={{ opacity: 0, y: 20 }}
51
+ animate={{ opacity: 1, y: 0 }}
52
+ transition={{ duration: 0.5 }}
53
+ className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10 text-xs font-medium text-primary mb-8 backdrop-blur-md"
54
+ >
55
+ <span className="relative flex h-2 w-2">
56
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
57
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
58
+ </span>
59
+ <a href="https://github.com/OhMyDitzzy/Yuki">DitzzyAPI has an official bot script, Click here to check now!</a>
60
+ </motion.div>
61
+
62
+ <motion.h1
63
+ initial={{ opacity: 0, y: 20 }}
64
+ animate={{ opacity: 1, y: 0 }}
65
+ transition={{ duration: 0.5, delay: 0.1 }}
66
+ className="text-5xl sm:text-7xl font-display font-bold tracking-tight text-white mb-6 max-w-4xl"
67
+ >
68
+ Build faster with the <br />
69
+ <span className="text-gradient-primary">ultimate developer API</span>
70
+ </motion.h1>
71
+
72
+ <motion.p
73
+ initial={{ opacity: 0, y: 20 }}
74
+ animate={{ opacity: 1, y: 0 }}
75
+ transition={{ duration: 0.5, delay: 0.2 }}
76
+ className="text-lg sm:text-xl text-muted-foreground max-w-2xl mb-10 leading-relaxed"
77
+ >
78
+ Free, unlimited, open-source API, and no API key required. Start building instantly with our
79
+ developer-friendly API.
80
+ </motion.p>
81
+
82
+ <motion.div
83
+ initial={{ opacity: 0, y: 20 }}
84
+ animate={{ opacity: 1, y: 0 }}
85
+ transition={{ duration: 0.5, delay: 0.3 }}
86
+ className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto"
87
+ >
88
+ <Link to="/docs">
89
+ <Button size="lg" className="w-full sm:w-auto h-12 px-8 text-base rounded-xl">
90
+ View Documentation
91
+ </Button>
92
+ </Link>
93
+ <a href="https://github.com/OhMyDitzzy/DitzzyAPI" target="_blank" rel="noopener noreferrer">
94
+ <Button variant="outline" size="lg" className="w-full sm:w-auto h-12 px-8 text-base rounded-xl border-white/10 hover:bg-white/5">
95
+ View on GitHub
96
+ </Button>
97
+ </a>
98
+ </motion.div>
99
+
100
+ <div className="mt-20 w-full flex justify-center">
101
+ <CodeSnippet
102
+ filename="api_example.ts"
103
+ language="typescript"
104
+ code={exampleFetchCode}
105
+ delay={0.5}
106
+ showLineNumbers={true}
107
+ copyable={true}
108
+ />
109
+ </div>
110
+ </div>
111
+ </section>
112
+
113
+ {/* Stats Section */}
114
+ <section className="py-16 border-b border-white/5">
115
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
116
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-8">
117
+ {[
118
+ { value: "100%", label: "Free Forever" },
119
+ { value: "No Keys", label: "Just Start Coding" },
120
+ { value: "<100ms", label: "Avg Response" },
121
+ { value: "24/7", label: "Always Online" }
122
+ ].map((stat, i) => (
123
+ <motion.div
124
+ key={i}
125
+ initial={{ opacity: 0, y: 20 }}
126
+ whileInView={{ opacity: 1, y: 0 }}
127
+ viewport={{ once: true }}
128
+ transition={{ delay: i * 0.1 }}
129
+ className="text-center"
130
+ >
131
+ <div className="text-4xl sm:text-5xl font-bold text-gradient-primary mb-2">
132
+ {stat.value}
133
+ </div>
134
+ <div className="text-sm text-muted-foreground">
135
+ {stat.label}
136
+ </div>
137
+ </motion.div>
138
+ ))}
139
+ </div>
140
+ </div>
141
+ </section>
142
+
143
+ {/* Features Section */}
144
+ <section className="py-24 bg-black/20 border-b border-white/5">
145
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
146
+ <motion.div
147
+ initial={{ opacity: 0, y: 20 }}
148
+ whileInView={{ opacity: 1, y: 0 }}
149
+ viewport={{ once: true }}
150
+ className="text-center mb-16"
151
+ >
152
+ <h2 className="text-3xl sm:text-4xl font-bold text-white mb-4">
153
+ Why choose DitzzyAPI?
154
+ </h2>
155
+ <p className="text-lg text-muted-foreground max-w-2xl mx-auto">
156
+ No registration, no API keys, no hidden fees. Just pure simplicity.
157
+ </p>
158
+ </motion.div>
159
+
160
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
161
+ {[
162
+ { icon: Key, title: "No API Key Needed", desc: "Start using immediately. No sign-ups, no authentication hassles." },
163
+ { icon: DollarSign, title: "Free Unlimited", desc: "Completely free with unlimited requests. No credit card required." },
164
+ { icon: Code2, title: "Developer Friendly", desc: "Simple endpoints, clear documentation, and 24/7 availability." }
165
+ ].map((feature, i) => (
166
+ <motion.div
167
+ key={i}
168
+ initial={{ opacity: 0, y: 20 }}
169
+ whileInView={{ opacity: 1, y: 0 }}
170
+ viewport={{ once: true }}
171
+ transition={{ delay: i * 0.1 }}
172
+ className="p-6 rounded-2xl bg-white/5 border border-white/5 hover:border-primary/50 transition-colors group"
173
+ >
174
+ <div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
175
+ <feature.icon className="w-6 h-6 text-primary" />
176
+ </div>
177
+ <h3 className="text-xl font-bold text-white mb-2">{feature.title}</h3>
178
+ <p className="text-muted-foreground">{feature.desc}</p>
179
+ </motion.div>
180
+ ))}
181
+ </div>
182
+ </div>
183
+ </section>
184
+
185
+ {/* FAQ Section */}
186
+ <section className="py-24 border-b border-white/5">
187
+ <div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
188
+ <motion.div
189
+ initial={{ opacity: 0, y: 20 }}
190
+ whileInView={{ opacity: 1, y: 0 }}
191
+ viewport={{ once: true }}
192
+ className="text-center mb-16"
193
+ >
194
+ <h2 className="text-3xl sm:text-4xl font-bold text-white mb-4">
195
+ Frequently Asked Questions
196
+ </h2>
197
+ <p className="text-lg text-muted-foreground">
198
+ Everything you need to know about DitzzyAPI
199
+ </p>
200
+ </motion.div>
201
+
202
+ <div className="space-y-4">
203
+ {faqs.map((faq, i) => (
204
+ <motion.div
205
+ key={i}
206
+ initial={{ opacity: 0, y: 20 }}
207
+ whileInView={{ opacity: 1, y: 0 }}
208
+ viewport={{ once: true }}
209
+ transition={{ delay: i * 0.1 }}
210
+ className="rounded-xl bg-white/5 border border-white/5 overflow-hidden"
211
+ >
212
+ <button
213
+ onClick={() => setOpenFaq(openFaq === i ? null : i)}
214
+ className="w-full px-6 py-5 flex items-center justify-between text-left hover:bg-white/5 transition-colors"
215
+ >
216
+ <span className="text-lg font-semibold text-white">
217
+ {faq.question}
218
+ </span>
219
+ <ChevronDown
220
+ className={`w-5 h-5 text-primary transition-transform ${
221
+ openFaq === i ? "rotate-180" : ""
222
+ }`}
223
+ />
224
+ </button>
225
+ {openFaq === i && (
226
+ <div className="px-6 pb-5 text-muted-foreground">
227
+ {faq.answer}
228
+ </div>
229
+ )}
230
+ </motion.div>
231
+ ))}
232
+ </div>
233
+ </div>
234
+ </section>
235
+
236
+ {/* CTA Section */}
237
+ <section className="py-24 relative overflow-hidden">
238
+ <div className="absolute inset-0 bg-gradient-to-b from-primary/10 to-transparent pointer-events-none blur-[120px] opacity-50" />
239
+ <div className="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
240
+ <motion.div
241
+ initial={{ opacity: 0, y: 20 }}
242
+ whileInView={{ opacity: 1, y: 0 }}
243
+ viewport={{ once: true }}
244
+ >
245
+ <h2 className="text-3xl sm:text-5xl font-bold text-white mb-6">
246
+ Ready to get started?
247
+ </h2>
248
+ <p className="text-lg text-muted-foreground mb-8 max-w-2xl mx-auto">
249
+ Join thousands of developers using DitzzyAPI. No registration needed,
250
+ just pick an endpoint and start coding!
251
+ </p>
252
+ <div className="flex flex-col sm:flex-row gap-4 justify-center">
253
+ <Link to="/docs">
254
+ <Button size="lg" className="w-full sm:w-auto h-12 px-8 text-base rounded-xl">
255
+ Start Building Now
256
+ </Button>
257
+ </Link>
258
+ </div>
259
+ </motion.div>
260
+ </div>
261
+ </section>
262
+ </main>
263
+
264
+ <Footer />
265
+ </div>
266
+ );
267
+ }
src/client/pages/not-found.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from "@/components/ui/button";
2
+ import { AlertCircle } from "lucide-react";
3
+ import { Link } from "react-router-dom"
4
+
5
+ export default function NotFound() {
6
+ return (
7
+ <div className="min-h-screen w-full flex flex-col items-center justify-center bg-background text-center p-4">
8
+ <div className="w-16 h-16 bg-red-500/10 rounded-full flex items-center justify-center mb-6">
9
+ <AlertCircle className="w-8 h-8 text-red-500" />
10
+ </div>
11
+
12
+ <h1 className="text-4xl font-display font-bold text-white mb-2">404 Page Not Found</h1>
13
+ <p className="text-muted-foreground max-w-md mb-8">
14
+ The page you are looking for doesn't exist or has been moved.
15
+ </p>
16
+ <Link to="/">
17
+ <Button>Go back</Button>
18
+ </Link>
19
+ </div>
20
+ )
21
+ }
src/components/CodeBlock.tsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef } from "react";
2
+ import Prism from "prismjs";
3
+ import "prismjs/themes/prism-tomorrow.css";
4
+ import "prismjs/components/prism-bash";
5
+ import "prismjs/components/prism-javascript";
6
+ import "prismjs/components/prism-typescript";
7
+ import "prismjs/components/prism-json";
8
+ import { Button } from "@/components/ui/button";
9
+ import { Copy, Check } from "lucide-react";
10
+ import { useState } from "react";
11
+
12
+ interface CodeBlockProps {
13
+ code: string;
14
+ language: "bash" | "javascript" | "typescript" | "json";
15
+ showCopy?: boolean;
16
+ }
17
+
18
+ export function CodeBlock({ code, language, showCopy = true }: CodeBlockProps) {
19
+ const codeRef = useRef<HTMLElement>(null);
20
+ const [copied, setCopied] = useState(false);
21
+
22
+ useEffect(() => {
23
+ if (codeRef.current) {
24
+ Prism.highlightElement(codeRef.current);
25
+ }
26
+ }, [code, language]);
27
+
28
+ const handleCopy = () => {
29
+ navigator.clipboard.writeText(code);
30
+ setCopied(true);
31
+ setTimeout(() => setCopied(false), 2000);
32
+ };
33
+
34
+ return (
35
+ <div className="relative group">
36
+ {showCopy && (
37
+ <Button
38
+ variant="ghost"
39
+ size="sm"
40
+ onClick={handleCopy}
41
+ className="absolute top-2 right-2 h-8 opacity-0 group-hover:opacity-100 transition-opacity bg-white/10 hover:bg-white/20 z-10"
42
+ >
43
+ {copied ? (
44
+ <>
45
+ <Check className="w-3 h-3 mr-1" />
46
+ Copied
47
+ </>
48
+ ) : (
49
+ <>
50
+ <Copy className="w-3 h-3 mr-1" />
51
+ Copy
52
+ </>
53
+ )}
54
+ </Button>
55
+ )}
56
+ <pre className="!bg-[#1e1e1e] !border !border-white/10 !rounded-lg !p-4 !m-0 overflow-x-auto">
57
+ <code ref={codeRef} className={`language-${language} !text-sm`}>
58
+ {code}
59
+ </code>
60
+ </pre>
61
+ </div>
62
+ );
63
+ }
src/components/CodeSnippet.tsx ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react"
2
+ import { motion } from "framer-motion"
3
+ import { Copy, Check } from "lucide-react"
4
+ import { Button } from "@/components/ui/button"
5
+ import Prism from "prismjs"
6
+ import "prismjs/themes/prism-tomorrow.css"
7
+ import "prismjs/components/prism-javascript"
8
+ import "prismjs/components/prism-typescript"
9
+ import "prismjs/components/prism-python"
10
+ import "prismjs/components/prism-jsx"
11
+ import "prismjs/components/prism-tsx"
12
+ import "prismjs/components/prism-json"
13
+ import "prismjs/components/prism-bash"
14
+ import "prismjs/components/prism-markup"
15
+ import "prismjs/components/prism-css"
16
+
17
+ interface CodeSnippetProps {
18
+ filename?: string
19
+ language?: string
20
+ code: string
21
+ delay?: number
22
+ className?: string
23
+ showLineNumbers?: boolean
24
+ copyable?: boolean
25
+ }
26
+
27
+ const LANGUAGE_MAP: Record<string, string> = {
28
+ js: "javascript",
29
+ javascript: "javascript",
30
+ ts: "typescript",
31
+ typescript: "typescript",
32
+ py: "python",
33
+ python: "python",
34
+ jsx: "jsx",
35
+ tsx: "tsx",
36
+ json: "json",
37
+ bash: "bash",
38
+ sh: "bash",
39
+ html: "markup",
40
+ css: "css",
41
+ sql: "sql",
42
+ go: "go",
43
+ rust: "rust",
44
+ java: "java",
45
+ };
46
+
47
+ export function CodeSnippet({
48
+ filename = "example.js",
49
+ language = "javascript",
50
+ code,
51
+ delay = 0.5,
52
+ className = "",
53
+ showLineNumbers = false,
54
+ copyable = true,
55
+ }: CodeSnippetProps) {
56
+ const [copied, setCopied] = useState(false);
57
+ const prismLanguage = LANGUAGE_MAP[language.toLowerCase()] || language;
58
+
59
+ useEffect(() => {
60
+ // Highlight code whenever component mounts or code changes
61
+ Prism.highlightAll();
62
+ }, [code, language]);
63
+
64
+ const handleCopy = async () => {
65
+ try {
66
+ await navigator.clipboard.writeText(code);
67
+ setCopied(true);
68
+ setTimeout(() => setCopied(false), 2000);
69
+ } catch (err) {
70
+ console.error("Failed to copy:", err);
71
+ }
72
+ };
73
+
74
+ const displayLanguage = language.toUpperCase();
75
+
76
+ return (
77
+ <motion.div
78
+ initial={{ opacity: 0, y: 40 }}
79
+ animate={{ opacity: 1, y: 0 }}
80
+ transition={{ duration: 0.7, delay }}
81
+ className={`group relative w-full max-w-3xl rounded-xl overflow-hidden border border-white/10 shadow-2xl bg-[#0d1117]/80 backdrop-blur-sm ${className}`}
82
+ >
83
+ {/* Header */}
84
+ <div className="flex items-center justify-between px-4 py-3 border-b border-white/5 bg-gradient-to-r from-white/[0.03] to-white/[0.01]">
85
+ <div className="flex items-center gap-2">
86
+ <div className="flex gap-1.5">
87
+ <div className="w-3 h-3 rounded-full bg-red-500/80" />
88
+ <div className="w-3 h-3 rounded-full bg-yellow-500/80" />
89
+ <div className="w-3 h-3 rounded-full bg-green-500/80" />
90
+ </div>
91
+ <div className="text-xs text-muted-foreground ml-2 font-mono truncate">
92
+ {filename}
93
+ </div>
94
+ </div>
95
+
96
+ <div className="flex items-center gap-3">
97
+ {copyable && (
98
+ <Button
99
+ variant="ghost"
100
+ size="sm"
101
+ onClick={handleCopy}
102
+ className="h-7 px-2 hover:bg-white/10 transition-colors"
103
+ >
104
+ {copied ? (
105
+ <Check className="w-3.5 h-3.5 text-green-400" />
106
+ ) : (
107
+ <Copy className="w-3.5 h-3.5 text-gray-400" />
108
+ )}
109
+ <span className="ml-1 text-xs">
110
+ {copied ? "Copied!" : "Copy"}
111
+ </span>
112
+ </Button>
113
+ )}
114
+ </div>
115
+ </div>
116
+
117
+ {/* Code Area */}
118
+ <div className="relative">
119
+ <pre className={`font-mono text-sm leading-relaxed m-0 overflow-x-auto p-6 ${showLineNumbers ? 'line-numbers' : ''}`}>
120
+ <code className={`language-${prismLanguage}`}>
121
+ {code}
122
+ </code>
123
+ </pre>
124
+
125
+ <div className="absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-[#0d1117] to-transparent pointer-events-none" />
126
+ </div>
127
+ </motion.div>
128
+ );
129
+ }
src/components/ErrorBoundary.tsx ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { Component, ReactNode } from "react";
2
+ import { Button } from "./ui/button";
3
+ import { AlertTriangle, RefreshCw, Home, Code } from "lucide-react";
4
+
5
+ interface Props {
6
+ children: ReactNode;
7
+ fallback?: ReactNode;
8
+ }
9
+
10
+ interface State {
11
+ hasError: boolean;
12
+ error: Error | null;
13
+ errorInfo: React.ErrorInfo | null;
14
+ }
15
+
16
+ export class ErrorBoundary extends Component<Props, State> {
17
+ constructor(props: Props) {
18
+ super(props);
19
+ this.state = {
20
+ hasError: false,
21
+ error: null,
22
+ errorInfo: null,
23
+ };
24
+ }
25
+
26
+ static getDerivedStateFromError(error: Error): Partial<State> {
27
+ return { hasError: true };
28
+ }
29
+
30
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
31
+ console.error("Error caught by boundary:", error, errorInfo);
32
+ this.setState({
33
+ error,
34
+ errorInfo,
35
+ });
36
+ }
37
+
38
+ handleReset = () => {
39
+ this.setState({
40
+ hasError: false,
41
+ error: null,
42
+ errorInfo: null,
43
+ });
44
+ };
45
+
46
+ render() {
47
+ if (this.state.hasError) {
48
+ if (this.props.fallback) {
49
+ return this.props.fallback;
50
+ }
51
+
52
+ return (
53
+ <div className="min-h-screen flex items-center justify-center bg-background px-4 py-8">
54
+ <div className="max-w-3xl w-full">
55
+ {/* Main Error Card */}
56
+ <div className="bg-white/[0.02] border border-white/10 rounded-xl overflow-hidden backdrop-blur-sm">
57
+ {/* Header with Gradient */}
58
+ <div className="bg-gradient-to-r from-red-500/20 via-orange-500/20 to-yellow-500/20 border-b border-white/10 p-8">
59
+ <div className="flex items-start gap-4">
60
+ {/* Animated Icon */}
61
+ <div className="flex-shrink-0">
62
+ <div className="w-16 h-16 bg-red-500/20 rounded-2xl flex items-center justify-center border border-red-500/30 backdrop-blur-sm animate-pulse">
63
+ <AlertTriangle className="w-8 h-8 text-red-400" />
64
+ </div>
65
+ </div>
66
+
67
+ <div className="flex-1 min-w-0">
68
+ <h1 className="text-3xl font-bold text-white mb-2 flex items-center gap-2">
69
+ Oops! Something went wrong
70
+ </h1>
71
+ <p className="text-gray-400 text-base">
72
+ Don't worry, your data is safe. The error has been logged and we'll look into it.
73
+ </p>
74
+ </div>
75
+ </div>
76
+ </div>
77
+
78
+ {/* Error Details */}
79
+ <div className="p-8 space-y-6">
80
+ {this.state.error && (
81
+ <div className="space-y-4">
82
+ {/* Error Message */}
83
+ <div>
84
+ <h3 className="text-sm font-semibold text-gray-400 mb-2 flex items-center gap-2">
85
+ <Code className="w-4 h-4" />
86
+ Error Message
87
+ </h3>
88
+ <div className="bg-black/50 border border-red-500/30 rounded-lg p-4 overflow-x-auto">
89
+ <pre className="text-sm text-red-300 whitespace-pre-wrap break-words">
90
+ {this.state.error.toString()}
91
+ </pre>
92
+ </div>
93
+ </div>
94
+
95
+ {/* Stack Trace (Dev Only) */}
96
+ {import.meta.env.DEV && this.state.errorInfo && (
97
+ <details className="group">
98
+ <summary className="cursor-pointer text-sm font-semibold text-gray-400 hover:text-gray-300 transition-colors select-none flex items-center gap-2 mb-2">
99
+ <svg
100
+ className="w-4 h-4 transition-transform group-open:rotate-90"
101
+ fill="none"
102
+ stroke="currentColor"
103
+ viewBox="0 0 24 24"
104
+ >
105
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
106
+ </svg>
107
+ Stack Trace (Development Mode)
108
+ </summary>
109
+ <div className="bg-black/50 border border-white/10 rounded-lg p-4 overflow-x-auto">
110
+ <pre className="text-xs text-gray-400 whitespace-pre-wrap">
111
+ {this.state.errorInfo.componentStack}
112
+ </pre>
113
+ </div>
114
+ </details>
115
+ )}
116
+ </div>
117
+ )}
118
+
119
+ {/* Action Buttons */}
120
+ <div className="flex flex-wrap gap-3 pt-4">
121
+ <Button
122
+ onClick={this.handleReset}
123
+ className="bg-purple-500 hover:bg-purple-600 text-white gap-2"
124
+ >
125
+ <RefreshCw className="w-4 h-4" />
126
+ Try Again
127
+ </Button>
128
+ <Button
129
+ onClick={() => window.location.reload()}
130
+ variant="outline"
131
+ className="border-white/10 text-gray-300 hover:bg-white/5 gap-2"
132
+ >
133
+ <RefreshCw className="w-4 h-4" />
134
+ Refresh Page
135
+ </Button>
136
+ <Button
137
+ onClick={() => (window.location.href = "/")}
138
+ variant="outline"
139
+ className="border-white/10 text-gray-300 hover:bg-white/5 gap-2"
140
+ >
141
+ <Home className="w-4 h-4" />
142
+ Go Home
143
+ </Button>
144
+ </div>
145
+
146
+ {/* Help Text */}
147
+ <div className="pt-4 border-t border-white/10">
148
+ <p className="text-xs text-gray-500">
149
+ If this problem persists, please contact support or check the console for more details.
150
+ </p>
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ {/* Additional Help Card */}
156
+ <div className="mt-6 bg-white/[0.02] border border-white/10 rounded-xl p-6">
157
+ <h3 className="text-sm font-semibold text-white mb-3">Quick Troubleshooting</h3>
158
+ <ul className="space-y-2 text-sm text-gray-400">
159
+ <li className="flex items-start gap-2">
160
+ <span className="text-purple-400 mt-0.5">•</span>
161
+ <span>Try clearing your browser cache and cookies</span>
162
+ </li>
163
+ <li className="flex items-start gap-2">
164
+ <span className="text-purple-400 mt-0.5">•</span>
165
+ <span>Check your internet connection</span>
166
+ </li>
167
+ <li className="flex items-start gap-2">
168
+ <span className="text-purple-400 mt-0.5">•</span>
169
+ <span>Make sure you're using a supported browser</span>
170
+ </li>
171
+ <li className="flex items-start gap-2">
172
+ <span className="text-purple-400 mt-0.5">•</span>
173
+ <span>Disable browser extensions that might interfere</span>
174
+ </li>
175
+ </ul>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ );
180
+ }
181
+
182
+ return this.props.children;
183
+ }
184
+ }
src/components/Footer.tsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Terminal } from "lucide-react";
2
+
3
+ export function Footer() {
4
+ return (
5
+ <footer className="bg-background border-t border-border/40 py-12">
6
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
7
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-8">
8
+ <div className="col-span-1 md:col-span-2">
9
+ <div className="flex items-center gap-2 mb-4">
10
+ <div className="w-8 h-8 rounded-lg bg-primary/20 flex items-center justify-center">
11
+ <Terminal className="w-5 h-5 text-primary" />
12
+ </div>
13
+ <span className="font-display font-bold text-xl tracking-tight text-white">
14
+ Ditzzy<span className="text-primary">API</span>
15
+ </span>
16
+ </div>
17
+ <p className="text-muted-foreground max-w-sm">
18
+ The developer-first API platform. Secure, scalable, and effortless integration for modern applications.
19
+ </p>
20
+ </div>
21
+
22
+ <div>
23
+ <h4 className="font-semibold text-white mb-4">Links</h4>
24
+ <ul className="space-y-2 text-sm text-muted-foreground">
25
+ <li><a href="/docs" className="hover:text-primary transition-colors">Documentation</a></li>
26
+ <li><a href="#" className="hover:text-primary transition-colors">Status</a></li>
27
+ </ul>
28
+ </div>
29
+
30
+ <div>
31
+ <h4 className="font-semibold text-white mb-4">Legal</h4>
32
+ <ul className="space-y-2 text-sm text-muted-foreground">
33
+ <li><a href="#" className="hover:text-primary transition-colors">Privacy Policy</a></li>
34
+ <li><a href="#" className="hover:text-primary transition-colors">Terms of Service</a></li>
35
+ </ul>
36
+ </div>
37
+ </div>
38
+
39
+ <div className="mt-12 pt-8 border-t border-border/40 text-center text-sm text-muted-foreground">
40
+ © {new Date().getFullYear()} DitzzyAPI. All rights reserved.
41
+ </div>
42
+ </div>
43
+ </footer>
44
+ );
45
+ }
src/components/Navbar.tsx ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { Menu, Terminal, X, Home, BookOpen, ChevronRight } from "lucide-react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { useCategories } from "@/client/hooks/usePlugin";
6
+ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
7
+
8
+ interface NavbarProps {
9
+ onCategorySelect?: (category: string | null) => void;
10
+ selectedCategory?: string | null;
11
+ }
12
+
13
+ export function Navbar({ onCategorySelect, selectedCategory }: NavbarProps) {
14
+ const [isOpen, setIsOpen] = useState(false);
15
+ const { categories } = useCategories();
16
+
17
+ const handleCategoryClick = (category: string) => {
18
+ onCategorySelect?.(category);
19
+ setIsOpen(false);
20
+ };
21
+
22
+ return (
23
+ <nav className="border-b border-white/10 bg-black/50 backdrop-blur-xl sticky top-0 z-50">
24
+ <div className="max-w-7xl mx-auto px-4 py-4">
25
+ <div className="flex items-center justify-between">
26
+ {/* Logo */}
27
+ <Link to="/" className="text-xl font-bold text-white flex items-center gap-2">
28
+ <Terminal className="w-5 h-5 text-primary" />
29
+ DitzzyAPI
30
+ </Link>
31
+
32
+ {/* Desktop Navigation */}
33
+ <div className="hidden md:flex items-center gap-6">
34
+ <Link
35
+ to="/"
36
+ className="text-gray-400 hover:text-white transition flex items-center gap-2"
37
+ >
38
+ <Home className="w-4 h-4" />
39
+ Home
40
+ </Link>
41
+ <Link to="/docs" className="text-purple-400 font-medium flex items-center gap-2">
42
+ <BookOpen className="w-4 h-4" />
43
+ Documentation
44
+ </Link>
45
+ </div>
46
+
47
+ {/* Hamburger Menu - Categories */}
48
+ <Sheet open={isOpen} onOpenChange={setIsOpen}>
49
+ <SheetTrigger asChild>
50
+ <Button variant="ghost" size="icon" className="text-white">
51
+ {isOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
52
+ </Button>
53
+ </SheetTrigger>
54
+ <SheetContent side="right" className="w-80 bg-background border-white/10">
55
+ <SheetHeader>
56
+ <SheetTitle className="text-white">Categories</SheetTitle>
57
+ </SheetHeader>
58
+
59
+ <div className="mt-6 space-y-2">
60
+ {/* All Endpoints */}
61
+ <button
62
+ onClick={() => handleCategoryClick("")}
63
+ className={`w-full flex items-center justify-between px-4 py-3 rounded-lg text-left transition ${
64
+ !selectedCategory
65
+ ? "bg-purple-500/20 text-purple-400 border border-purple-500/50"
66
+ : "bg-white/5 text-gray-300 hover:bg-white/10"
67
+ }`}
68
+ >
69
+ <span className="font-medium">All Endpoints</span>
70
+ <ChevronRight className="w-4 h-4" />
71
+ </button>
72
+
73
+ {/* Categories */}
74
+ {categories.map((cat) => (
75
+ <button
76
+ key={cat.name}
77
+ onClick={() => handleCategoryClick(cat.name)}
78
+ className={`w-full flex items-center justify-between px-4 py-3 rounded-lg text-left transition ${
79
+ selectedCategory === cat.name
80
+ ? "bg-purple-500/20 text-purple-400 border border-purple-500/50"
81
+ : "bg-white/5 text-gray-300 hover:bg-white/10"
82
+ }`}
83
+ >
84
+ <span className="capitalize">{cat.name}</span>
85
+ <div className="flex items-center gap-2">
86
+ <span className="text-xs bg-white/10 px-2 py-1 rounded">{cat.count}</span>
87
+ <ChevronRight className="w-4 h-4" />
88
+ </div>
89
+ </button>
90
+ ))}
91
+ </div>
92
+
93
+ {/* Mobile Navigation Links */}
94
+ <div className="md:hidden mt-8 space-y-2 pt-6 border-t border-white/10">
95
+ <Link
96
+ to="/"
97
+ onClick={() => setIsOpen(false)}
98
+ className="flex items-center gap-2 px-4 py-3 rounded-lg text-gray-300 hover:bg-white/10 transition"
99
+ >
100
+ <Home className="w-4 h-4" />
101
+ Home
102
+ </Link>
103
+ <Link
104
+ to="/docs"
105
+ onClick={() => setIsOpen(false)}
106
+ className="flex items-center gap-2 px-4 py-3 rounded-lg text-purple-400 hover:bg-white/10 transition"
107
+ >
108
+ <BookOpen className="w-4 h-4" />
109
+ Documentation
110
+ </Link>
111
+ </div>
112
+ </SheetContent>
113
+ </Sheet>
114
+ </div>
115
+ </div>
116
+ </nav>
117
+ );
118
+ }
src/components/PluginCard.tsx ADDED
@@ -0,0 +1,506 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { Card } from "@/components/ui/card";
3
+ import { Badge } from "@/components/ui/badge";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Input } from "@/components/ui/input";
6
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
7
+ import { PluginMetadata } from "@/client/hooks/usePlugin";
8
+ import { Play, ChevronDown, ChevronUp, Copy, Check } from "lucide-react";
9
+ import { CodeBlock } from "@/components/CodeBlock";
10
+ import { getApiUrl } from "@/lib/api-url";
11
+
12
+ interface PluginCardProps {
13
+ plugin: PluginMetadata;
14
+ }
15
+
16
+ const methodColors: Record<string, string> = {
17
+ GET: "bg-green-500/20 text-green-400 border-green-500/50",
18
+ POST: "bg-blue-500/20 text-blue-400 border-blue-500/50",
19
+ PUT: "bg-yellow-500/20 text-yellow-400 border-yellow-500/50",
20
+ DELETE: "bg-red-500/20 text-red-400 border-red-500/50",
21
+ PATCH: "bg-purple-500/20 text-purple-400 border-purple-500/50",
22
+ };
23
+
24
+ export function PluginCard({ plugin }: PluginCardProps) {
25
+ const [paramValues, setParamValues] = useState<Record<string, string>>({});
26
+ const [response, setResponse] = useState<any>(null);
27
+ const [responseHeaders, setResponseHeaders] = useState<Record<string, string>>({});
28
+ const [requestUrl, setRequestUrl] = useState<string>("");
29
+ const [loading, setLoading] = useState(false);
30
+ const [isExpanded, setIsExpanded] = useState(false);
31
+ const [copiedUrl, setCopiedUrl] = useState(false);
32
+ const [copiedRequestUrl, setCopiedRequestUrl] = useState(false);
33
+
34
+ const handleParamChange = (paramName: string, value: string) => {
35
+ setParamValues((prev) => ({ ...prev, [paramName]: value }));
36
+ };
37
+
38
+ const handleExecute = async () => {
39
+ setLoading(true);
40
+
41
+ try {
42
+ let url = "/api" + plugin.endpoint;
43
+ let fullUrl = getApiUrl(plugin.endpoint);
44
+
45
+ if (plugin.method === "GET" && plugin.parameters?.query) {
46
+ const queryParams = new URLSearchParams();
47
+ plugin.parameters.query.forEach((param) => {
48
+ const value = paramValues[param.name];
49
+ if (value) {
50
+ queryParams.append(param.name, value);
51
+ }
52
+ });
53
+
54
+ if (queryParams.toString()) {
55
+ url += "?" + queryParams.toString();
56
+ fullUrl += "?" + queryParams.toString();
57
+ }
58
+ }
59
+
60
+ // Store the request URL for display
61
+ setRequestUrl(fullUrl);
62
+
63
+ const fetchOptions: RequestInit = {
64
+ method: plugin.method,
65
+ };
66
+
67
+ // Add body for POST/PUT/PATCH
68
+ if (["POST", "PUT", "PATCH"].includes(plugin.method) && plugin.parameters?.body) {
69
+ const bodyData: Record<string, any> = {};
70
+ plugin.parameters.body.forEach((param) => {
71
+ const value = paramValues[param.name];
72
+ if (value) {
73
+ bodyData[param.name] = value;
74
+ }
75
+ });
76
+ fetchOptions.body = JSON.stringify(bodyData);
77
+ fetchOptions.headers = {
78
+ "Content-Type": "application/json",
79
+ };
80
+ }
81
+
82
+ const res = await fetch(url, fetchOptions);
83
+ const data = await res.json();
84
+
85
+ // Capture response headers
86
+ const headers: Record<string, string> = {};
87
+ res.headers.forEach((value, key) => {
88
+ headers[key] = value;
89
+ });
90
+
91
+ setResponseHeaders(headers);
92
+ setResponse({
93
+ status: res.status,
94
+ statusText: res.statusText,
95
+ data,
96
+ });
97
+ } catch (error) {
98
+ setResponse({
99
+ status: 500,
100
+ statusText: "Error",
101
+ data: { error: error instanceof Error ? error.message : "Unknown error" },
102
+ });
103
+ setResponseHeaders({});
104
+ } finally {
105
+ setLoading(false);
106
+ }
107
+ };
108
+
109
+ const copyApiUrl = () => {
110
+ const fullUrl = getApiUrl(plugin.endpoint);
111
+ navigator.clipboard.writeText(fullUrl);
112
+ setCopiedUrl(true);
113
+ setTimeout(() => setCopiedUrl(false), 2000);
114
+ };
115
+
116
+ const copyRequestUrl = () => {
117
+ navigator.clipboard.writeText(requestUrl);
118
+ setCopiedRequestUrl(true);
119
+ setTimeout(() => setCopiedRequestUrl(false), 2000);
120
+ };
121
+
122
+ const hasQueryParams = plugin.parameters?.query && plugin.parameters.query.length > 0;
123
+ const hasBodyParams = plugin.parameters?.body && plugin.parameters.body.length > 0;
124
+ const hasPathParams = plugin.parameters?.path && plugin.parameters.path.length > 0;
125
+ const hasAnyParams = hasQueryParams || hasBodyParams || hasPathParams;
126
+
127
+ const generateCurlExample = () => {
128
+ let curl = `curl -X ${plugin.method} "${getApiUrl(plugin.endpoint)}`;
129
+
130
+ if (hasQueryParams) {
131
+ const exampleParams = plugin.parameters!.query!
132
+ .map((p) => `${p.name}=${p.example || 'value'}`)
133
+ .join('&');
134
+ curl += `?${exampleParams}`;
135
+ }
136
+
137
+ curl += '"';
138
+
139
+ if (hasBodyParams) {
140
+ curl += ' \\\n -H "Content-Type: application/json" \\\n -d \'';
141
+ const bodyExample: Record<string, any> = {};
142
+ plugin.parameters!.body!.forEach((p) => {
143
+ bodyExample[p.name] = p.example || 'value';
144
+ });
145
+ curl += JSON.stringify(bodyExample, null, 2);
146
+ curl += "'";
147
+ }
148
+
149
+ return curl;
150
+ };
151
+
152
+ const generateNodeExample = () => {
153
+ let code = `const response = await fetch("${getApiUrl(plugin.endpoint)}`;
154
+
155
+ if (hasQueryParams) {
156
+ const exampleParams = plugin.parameters!.query!
157
+ .map((p) => `${p.name}=${p.example || 'value'}`)
158
+ .join('&');
159
+ code += `?${exampleParams}`;
160
+ }
161
+
162
+ code += '", {\n method: "' + plugin.method + '"';
163
+
164
+ if (hasBodyParams) {
165
+ code += ',\n headers: {\n "Content-Type": "application/json"\n },\n body: JSON.stringify(';
166
+ const bodyExample: Record<string, any> = {};
167
+ plugin.parameters!.body!.forEach((p) => {
168
+ bodyExample[p.name] = p.example || 'value';
169
+ });
170
+ code += JSON.stringify(bodyExample, null, 2);
171
+ code += ')';
172
+ }
173
+
174
+ code += '\n});\n\nconst data = await response.json();\nconsole.log(data);';
175
+ return code;
176
+ };
177
+
178
+ return (
179
+ <Card className="bg-white/[0.02] border-white/10 overflow-hidden">
180
+ {/* Collapsible Header */}
181
+ <div
182
+ className="p-6 border-b border-white/10 cursor-pointer hover:bg-white/[0.02] transition-colors"
183
+ onClick={() => setIsExpanded(!isExpanded)}
184
+ >
185
+ <div className="flex items-start justify-between gap-4">
186
+ <div className="flex-1 min-w-0">
187
+ <div className="flex items-center gap-3 mb-3 flex-wrap">
188
+ <Badge className={`${methodColors[plugin.method]} border font-bold px-3 py-1 flex-shrink-0`}>
189
+ {plugin.method}
190
+ </Badge>
191
+ <code className="text-sm text-purple-400 font-mono break-all">{plugin.endpoint}</code>
192
+ <button
193
+ onClick={(e) => {
194
+ e.stopPropagation();
195
+ copyApiUrl();
196
+ }}
197
+ className="text-gray-400 hover:text-white transition-colors p-1 flex-shrink-0"
198
+ title="Copy API URL"
199
+ >
200
+ {copiedUrl ? (
201
+ <Check className="w-4 h-4 text-green-400" />
202
+ ) : (
203
+ <Copy className="w-4 h-4" />
204
+ )}
205
+ </button>
206
+ </div>
207
+ <h3 className="text-xl font-bold text-white mb-2 break-words">{plugin.name}</h3>
208
+ <p className="text-gray-400 text-sm break-words">{plugin.description}</p>
209
+
210
+ {/* Tags */}
211
+ {plugin.tags && plugin.tags.length > 0 && (
212
+ <div className="flex flex-wrap gap-2 mt-3">
213
+ {plugin.tags.map((tag) => (
214
+ <Badge key={tag} variant="outline" className="bg-white/5 text-gray-400 border-white/10 text-xs">
215
+ {tag}
216
+ </Badge>
217
+ ))}
218
+ </div>
219
+ )}
220
+
221
+ {/* API URL Display */}
222
+ <div className="mt-3 flex items-start gap-2">
223
+ <span className="text-xs text-gray-500 flex-shrink-0">API URL:</span>
224
+ <code className="text-xs text-gray-300 bg-black/30 px-2 py-1 rounded break-all">
225
+ {getApiUrl(plugin.endpoint)}
226
+ </code>
227
+ </div>
228
+ </div>
229
+
230
+ <button
231
+ className="text-gray-400 hover:text-white transition-colors flex-shrink-0 p-2 hover:bg-white/5 rounded-lg"
232
+ onClick={(e) => {
233
+ e.stopPropagation();
234
+ setIsExpanded(!isExpanded);
235
+ }}
236
+ >
237
+ {isExpanded ? (
238
+ <ChevronUp className="w-6 h-6" />
239
+ ) : (
240
+ <ChevronDown className="w-6 h-6" />
241
+ )}
242
+ </button>
243
+ </div>
244
+ </div>
245
+
246
+ {/* Expandable Content */}
247
+ {isExpanded && (
248
+ <Tabs defaultValue="try" className="w-full">
249
+ <TabsList className="w-full justify-start rounded-none border-b border-white/10 bg-transparent p-0">
250
+ <TabsTrigger
251
+ value="documentation"
252
+ className="rounded-none data-[state=active]:bg-transparent data-[state=active]:border-b-2 data-[state=active]:border-purple-500 data-[state=active]:text-purple-400 px-6 py-3"
253
+ >
254
+ Documentation
255
+ </TabsTrigger>
256
+ <TabsTrigger
257
+ value="try"
258
+ className="rounded-none data-[state=active]:bg-transparent data-[state=active]:border-b-2 data-[state=active]:border-purple-500 data-[state=active]:text-purple-400 px-6 py-3"
259
+ >
260
+ Try It Out
261
+ </TabsTrigger>
262
+ </TabsList>
263
+
264
+ {/* Documentation Tab */}
265
+ <TabsContent value="documentation" className="p-6 space-y-6">
266
+ {/* Parameters Table */}
267
+ {hasAnyParams && (
268
+ <div>
269
+ <h4 className="text-purple-400 font-semibold mb-3">Parameters</h4>
270
+ <div className="overflow-x-auto">
271
+ <table className="w-full text-sm">
272
+ <thead>
273
+ <tr className="border-b border-white/10">
274
+ <th className="text-left text-gray-400 font-medium pb-2 pr-4">Name</th>
275
+ <th className="text-left text-gray-400 font-medium pb-2 pr-4">Type</th>
276
+ <th className="text-left text-gray-400 font-medium pb-2 pr-4">Required</th>
277
+ <th className="text-left text-gray-400 font-medium pb-2">Description</th>
278
+ </tr>
279
+ </thead>
280
+ <tbody>
281
+ {/* Path Parameters */}
282
+ {plugin.parameters?.path?.map((param) => (
283
+ <tr key={param.name} className="border-b border-white/5">
284
+ <td className="py-3 pr-4 text-white font-mono">{param.name}</td>
285
+ <td className="py-3 pr-4 text-blue-400 font-mono text-xs">{param.type}</td>
286
+ <td className="py-3 pr-4">
287
+ <span className={param.required ? "text-red-400" : "text-gray-500"}>
288
+ {param.required ? "Yes" : "No"}
289
+ </span>
290
+ </td>
291
+ <td className="py-3 text-gray-400">{param.description}</td>
292
+ </tr>
293
+ ))}
294
+ {/* Query Parameters */}
295
+ {plugin.parameters?.query?.map((param) => (
296
+ <tr key={param.name} className="border-b border-white/5">
297
+ <td className="py-3 pr-4 text-white font-mono">{param.name}</td>
298
+ <td className="py-3 pr-4 text-blue-400 font-mono text-xs">{param.type}</td>
299
+ <td className="py-3 pr-4">
300
+ <span className={param.required ? "text-red-400" : "text-gray-500"}>
301
+ {param.required ? "Yes" : "No"}
302
+ </span>
303
+ </td>
304
+ <td className="py-3 text-gray-400">{param.description}</td>
305
+ </tr>
306
+ ))}
307
+ {/* Body Parameters */}
308
+ {plugin.parameters?.body?.map((param) => (
309
+ <tr key={param.name} className="border-b border-white/5">
310
+ <td className="py-3 pr-4 text-white font-mono">{param.name}</td>
311
+ <td className="py-3 pr-4 text-blue-400 font-mono text-xs">{param.type}</td>
312
+ <td className="py-3 pr-4">
313
+ <span className={param.required ? "text-red-400" : "text-gray-500"}>
314
+ {param.required ? "Yes" : "No"}
315
+ </span>
316
+ </td>
317
+ <td className="py-3 text-gray-400">{param.description}</td>
318
+ </tr>
319
+ ))}
320
+ </tbody>
321
+ </table>
322
+ </div>
323
+ </div>
324
+ )}
325
+
326
+ {/* Responses */}
327
+ {plugin.responses && Object.keys(plugin.responses).length > 0 && (
328
+ <div>
329
+ <h4 className="text-purple-400 font-semibold mb-3">Responses</h4>
330
+ <div className="space-y-3">
331
+ {Object.entries(plugin.responses).map(([status, response]) => (
332
+ <div key={status} className="border border-white/10 rounded-lg overflow-hidden">
333
+ <div className={`px-4 py-2 flex items-center gap-3 ${
334
+ parseInt(status) >= 200 && parseInt(status) < 300
335
+ ? "bg-green-500/10"
336
+ : parseInt(status) >= 400 && parseInt(status) < 500
337
+ ? "bg-yellow-500/10"
338
+ : "bg-red-500/10"
339
+ }`}>
340
+ <Badge
341
+ className={`${
342
+ parseInt(status) >= 200 && parseInt(status) < 300
343
+ ? "bg-green-500/20 text-green-400 border-green-500/50"
344
+ : parseInt(status) >= 400 && parseInt(status) < 500
345
+ ? "bg-yellow-500/20 text-yellow-400 border-yellow-500/50"
346
+ : "bg-red-500/20 text-red-400 border-red-500/50"
347
+ } border font-bold`}
348
+ >
349
+ {status}
350
+ </Badge>
351
+ <span className="text-sm text-white">{response.description}</span>
352
+ </div>
353
+ <pre className="p-4 bg-black/50 text-xs overflow-x-auto">
354
+ <code className="text-gray-300">{JSON.stringify(response.example, null, 2)}</code>
355
+ </pre>
356
+ </div>
357
+ ))}
358
+ </div>
359
+ </div>
360
+ )}
361
+
362
+ {/* Code Examples */}
363
+ <div>
364
+ <h4 className="text-purple-400 font-semibold mb-3">Code Example</h4>
365
+ <div className="space-y-3">
366
+ <div>
367
+ <div className="mb-2">
368
+ <span className="text-xs text-gray-400">cURL</span>
369
+ </div>
370
+ <CodeBlock code={generateCurlExample()} language="bash" />
371
+ </div>
372
+
373
+ <div>
374
+ <div className="mb-2">
375
+ <span className="text-xs text-gray-400">Node.js (fetch)</span>
376
+ </div>
377
+ <CodeBlock code={generateNodeExample()} language="javascript" />
378
+ </div>
379
+ </div>
380
+ </div>
381
+ </TabsContent>
382
+
383
+ {/* Try It Out Tab */}
384
+ <TabsContent value="try" className="p-6">
385
+ {/* Parameters Input */}
386
+ {hasAnyParams ? (
387
+ <div className="space-y-4 mb-4">
388
+ {/* Query Parameters */}
389
+ {plugin.parameters?.query?.map((param) => (
390
+ <div key={param.name}>
391
+ <label className="block text-sm text-gray-300 mb-2">
392
+ {param.name}
393
+ {param.required && <span className="text-red-400 ml-1">*</span>}
394
+ <span className="text-xs text-gray-500 ml-2">({param.type})</span>
395
+ </label>
396
+ <Input
397
+ type="text"
398
+ placeholder={param.example?.toString() || param.description}
399
+ value={paramValues[param.name] || ""}
400
+ onChange={(e) => handleParamChange(param.name, e.target.value)}
401
+ className="bg-black/50 border-white/10 text-white focus:border-purple-500"
402
+ />
403
+ <p className="text-xs text-gray-500 mt-1">{param.description}</p>
404
+ </div>
405
+ ))}
406
+
407
+ {/* Body Parameters */}
408
+ {plugin.parameters?.body?.map((param) => (
409
+ <div key={param.name}>
410
+ <label className="block text-sm text-gray-300 mb-2">
411
+ {param.name}
412
+ {param.required && <span className="text-red-400 ml-1">*</span>}
413
+ <span className="text-xs text-gray-500 ml-2">({param.type})</span>
414
+ </label>
415
+ <Input
416
+ type="text"
417
+ placeholder={param.example?.toString() || param.description}
418
+ value={paramValues[param.name] || ""}
419
+ onChange={(e) => handleParamChange(param.name, e.target.value)}
420
+ className="bg-black/50 border-white/10 text-white focus:border-purple-500"
421
+ />
422
+ <p className="text-xs text-gray-500 mt-1">{param.description}</p>
423
+ </div>
424
+ ))}
425
+ </div>
426
+ ) : (
427
+ <p className="text-sm text-gray-400 mb-4">No parameters required</p>
428
+ )}
429
+
430
+ {/* Execute Button */}
431
+ <Button
432
+ onClick={handleExecute}
433
+ disabled={loading}
434
+ className="w-full bg-purple-500 hover:bg-purple-600 text-white py-6 text-base font-semibold"
435
+ >
436
+ <Play className="w-5 h-5 mr-2" />
437
+ {loading ? "Executing..." : "Execute"}
438
+ </Button>
439
+
440
+ {/* Response Display */}
441
+ {response && (
442
+ <div className="mt-6 space-y-4">
443
+ {/* Request URL */}
444
+ <div>
445
+ <div className="flex items-center justify-between mb-2">
446
+ <span className="text-sm text-gray-400">Request URL</span>
447
+ <button
448
+ onClick={copyRequestUrl}
449
+ className="text-gray-400 hover:text-white transition-colors p-1"
450
+ title="Copy Request URL"
451
+ >
452
+ {copiedRequestUrl ? (
453
+ <Check className="w-4 h-4 text-green-400" />
454
+ ) : (
455
+ <Copy className="w-4 h-4" />
456
+ )}
457
+ </button>
458
+ </div>
459
+ <div className="bg-black/50 border border-white/10 rounded p-3 overflow-x-auto">
460
+ <code className="text-xs text-purple-300 break-all">{requestUrl}</code>
461
+ </div>
462
+ </div>
463
+
464
+ {/* Response Status */}
465
+ <div className="flex items-center justify-between">
466
+ <span className="text-sm text-gray-400">Response Status</span>
467
+ <Badge className={`${
468
+ response.status >= 200 && response.status < 300
469
+ ? "bg-green-500/20 text-green-400"
470
+ : "bg-red-500/20 text-red-400"
471
+ }`}>
472
+ {response.status} {response.statusText}
473
+ </Badge>
474
+ </div>
475
+
476
+ {/* Response Headers */}
477
+ {Object.keys(responseHeaders).length > 0 && (
478
+ <div>
479
+ <h5 className="text-sm text-gray-400 mb-2">Response Headers</h5>
480
+ <div className="bg-black/50 border border-white/10 rounded p-4 space-y-1 overflow-x-auto">
481
+ {Object.entries(responseHeaders).map(([key, value]) => (
482
+ <div key={key} className="text-xs">
483
+ <span className="text-purple-400">{key}:</span>{" "}
484
+ <span className="text-gray-300">{value}</span>
485
+ </div>
486
+ ))}
487
+ </div>
488
+ </div>
489
+ )}
490
+
491
+ {/* Response Body with Syntax Highlighting */}
492
+ <div>
493
+ <h5 className="text-sm text-gray-400 mb-2">Response Body</h5>
494
+ <CodeBlock
495
+ code={JSON.stringify(response.data, null, 2)}
496
+ language="json"
497
+ />
498
+ </div>
499
+ </div>
500
+ )}
501
+ </TabsContent>
502
+ </Tabs>
503
+ )}
504
+ </Card>
505
+ );
506
+ }
src/components/StatsCard.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card } from "@/components/ui/card";
2
+ import { LucideIcon } from "lucide-react";
3
+
4
+ interface StatsCardProps {
5
+ title: string;
6
+ value: string | number;
7
+ icon: LucideIcon;
8
+ color: "purple" | "green" | "red" | "blue";
9
+ }
10
+
11
+ const colorClasses = {
12
+ purple: "text-purple-400 bg-purple-500/10 border-purple-500/20",
13
+ green: "text-green-400 bg-green-500/10 border-green-500/20",
14
+ red: "text-red-400 bg-red-500/10 border-red-500/20",
15
+ blue: "text-blue-400 bg-blue-500/10 border-blue-500/20",
16
+ };
17
+
18
+ export function StatsCard({ title, value, icon: Icon, color }: StatsCardProps) {
19
+ return (
20
+ <Card className={`p-4 backdrop-blur-sm ${colorClasses[color]}`}>
21
+ <div className="flex items-center justify-between mb-2">
22
+ <Icon className="w-5 h-5" />
23
+ </div>
24
+ <div className={`text-2xl font-bold ${colorClasses[color].split(" ")[0]}`}>
25
+ {value}
26
+ </div>
27
+ <div className="text-xs text-muted-foreground mt-1">{title}</div>
28
+ </Card>
29
+ );
30
+ }
src/components/VisitorChart.tsx ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import { Card } from "@/components/ui/card";
3
+ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
4
+ import { Users, Loader2 } from "lucide-react";
5
+
6
+ interface VisitorData {
7
+ timestamp: number;
8
+ count: number;
9
+ }
10
+
11
+ export function VisitorChart() {
12
+ const [data, setData] = useState<VisitorData[]>([]);
13
+ const [loading, setLoading] = useState(true);
14
+
15
+ useEffect(() => {
16
+ fetchVisitorData();
17
+ const interval = setInterval(fetchVisitorData, 5 * 60 * 1000);
18
+ return () => clearInterval(interval);
19
+ }, []);
20
+
21
+ const fetchVisitorData = async () => {
22
+ try {
23
+ const res = await fetch("/api/stats/visitors");
24
+ const json = await res.json();
25
+ if (json.success) {
26
+ setData(json.data);
27
+ }
28
+ } catch (error) {
29
+ console.error("Failed to fetch visitor data:", error);
30
+ } finally {
31
+ setLoading(false);
32
+ }
33
+ };
34
+
35
+ const formatXAxis = (timestamp: number) => {
36
+ const date = new Date(timestamp);
37
+ return date.getHours().toString().padStart(2, '0') + ':00';
38
+ };
39
+
40
+ const CustomTooltip = ({ active, payload }: any) => {
41
+ if (active && payload && payload.length) {
42
+ const data = payload[0].payload;
43
+ const date = new Date(data.timestamp);
44
+ const timeStr = date.toLocaleString('en-US', {
45
+ month: 'short',
46
+ day: 'numeric',
47
+ hour: '2-digit',
48
+ minute: '2-digit',
49
+ });
50
+
51
+ return (
52
+ <div className="bg-black/90 border border-white/20 rounded-lg p-3 backdrop-blur-sm">
53
+ <p className="text-xs text-gray-400 mb-1">{timeStr}</p>
54
+ <p className="text-sm font-semibold text-purple-400">
55
+ {data.count} visitor{data.count !== 1 ? 's' : ''}
56
+ </p>
57
+ </div>
58
+ );
59
+ }
60
+ return null;
61
+ };
62
+
63
+ return (
64
+ <Card className="p-6 bg-white/[0.02] border-white/10">
65
+ <div className="flex items-center gap-2 mb-4">
66
+ <Users className="w-5 h-5 text-purple-400" />
67
+ <h3 className="text-lg font-semibold text-white">Visitor Activity (Last 24 Hours)</h3>
68
+ </div>
69
+
70
+ {loading ? (
71
+ <div className="h-64 flex items-center justify-center">
72
+ <Loader2 className="w-6 h-6 text-purple-400 animate-spin" />
73
+ </div>
74
+ ) : (
75
+ <ResponsiveContainer width="100%" height={300}>
76
+ <LineChart data={data} margin={{ top: 5, right: 30, left: 0, bottom: 5 }}>
77
+ <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
78
+ <XAxis
79
+ dataKey="timestamp"
80
+ tickFormatter={formatXAxis}
81
+ stroke="rgba(255,255,255,0.5)"
82
+ style={{ fontSize: '12px' }}
83
+ interval="preserveStartEnd"
84
+ />
85
+ <YAxis
86
+ stroke="rgba(255,255,255,0.5)"
87
+ style={{ fontSize: '12px' }}
88
+ allowDecimals={false}
89
+ />
90
+ <Tooltip content={<CustomTooltip />} />
91
+ <Line
92
+ type="monotone"
93
+ dataKey="count"
94
+ stroke="#a855f7"
95
+ strokeWidth={2}
96
+ dot={{ fill: '#a855f7', r: 3 }}
97
+ activeDot={{ r: 5 }}
98
+ />
99
+ </LineChart>
100
+ </ResponsiveContainer>
101
+ )}
102
+ </Card>
103
+ );
104
+ }
src/components/ui/badge.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-md 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 shadow 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 shadow hover:bg-destructive/80",
17
+ outline: "text-foreground",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "default",
22
+ },
23
+ }
24
+ )
25
+
26
+ export interface BadgeProps
27
+ extends React.HTMLAttributes<HTMLDivElement>,
28
+ VariantProps<typeof badgeVariants> {}
29
+
30
+ function Badge({ className, variant, ...props }: BadgeProps) {
31
+ return (
32
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
33
+ )
34
+ }
35
+
36
+ export { Badge, badgeVariants }
src/components/ui/button.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } 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 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14
+ destructive:
15
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16
+ outline:
17
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18
+ secondary:
19
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20
+ ghost: "hover:bg-accent hover:text-accent-foreground",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2",
25
+ sm: "h-8 rounded-md px-3 text-xs",
26
+ lg: "h-10 rounded-md px-8",
27
+ icon: "h-9 w-9",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ size: "default",
33
+ },
34
+ }
35
+ )
36
+
37
+ export interface ButtonProps
38
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39
+ VariantProps<typeof buttonVariants> {
40
+ asChild?: boolean
41
+ }
42
+
43
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
45
+ const Comp = asChild ? Slot : "button"
46
+ return (
47
+ <Comp
48
+ className={cn(buttonVariants({ variant, size, className }))}
49
+ ref={ref}
50
+ {...props}
51
+ />
52
+ )
53
+ }
54
+ )
55
+ Button.displayName = "Button"
56
+
57
+ export { Button, buttonVariants }
src/components/ui/card.tsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ "rounded-xl border bg-card text-card-foreground shadow",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ ))
18
+ Card.displayName = "Card"
19
+
20
+ const CardHeader = React.forwardRef<
21
+ HTMLDivElement,
22
+ React.HTMLAttributes<HTMLDivElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
27
+ {...props}
28
+ />
29
+ ))
30
+ CardHeader.displayName = "CardHeader"
31
+
32
+ const CardTitle = React.forwardRef<
33
+ HTMLDivElement,
34
+ React.HTMLAttributes<HTMLDivElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <div
37
+ ref={ref}
38
+ className={cn("font-semibold leading-none tracking-tight", className)}
39
+ {...props}
40
+ />
41
+ ))
42
+ CardTitle.displayName = "CardTitle"
43
+
44
+ const CardDescription = React.forwardRef<
45
+ HTMLDivElement,
46
+ React.HTMLAttributes<HTMLDivElement>
47
+ >(({ className, ...props }, ref) => (
48
+ <div
49
+ ref={ref}
50
+ className={cn("text-sm text-muted-foreground", className)}
51
+ {...props}
52
+ />
53
+ ))
54
+ CardDescription.displayName = "CardDescription"
55
+
56
+ const CardContent = React.forwardRef<
57
+ HTMLDivElement,
58
+ React.HTMLAttributes<HTMLDivElement>
59
+ >(({ className, ...props }, ref) => (
60
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
61
+ ))
62
+ CardContent.displayName = "CardContent"
63
+
64
+ const CardFooter = React.forwardRef<
65
+ HTMLDivElement,
66
+ React.HTMLAttributes<HTMLDivElement>
67
+ >(({ className, ...props }, ref) => (
68
+ <div
69
+ ref={ref}
70
+ className={cn("flex items-center p-6 pt-0", className)}
71
+ {...props}
72
+ />
73
+ ))
74
+ CardFooter.displayName = "CardFooter"
75
+
76
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
src/components/ui/input.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
6
+ ({ className, type, ...props }, ref) => {
7
+ return (
8
+ <input
9
+ type={type}
10
+ className={cn(
11
+ "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ className
13
+ )}
14
+ ref={ref}
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+ )
20
+ Input.displayName = "Input"
21
+
22
+ export { Input }
src/components/ui/select.tsx ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SelectPrimitive from "@radix-ui/react-select"
5
+ import { Check, ChevronDown, ChevronUp } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const Select = SelectPrimitive.Root
10
+
11
+ const SelectGroup = SelectPrimitive.Group
12
+
13
+ const SelectValue = SelectPrimitive.Value
14
+
15
+ const SelectTrigger = React.forwardRef<
16
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
17
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
18
+ >(({ className, children, ...props }, ref) => (
19
+ <SelectPrimitive.Trigger
20
+ ref={ref}
21
+ className={cn(
22
+ "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
23
+ className
24
+ )}
25
+ {...props}
26
+ >
27
+ {children}
28
+ <SelectPrimitive.Icon asChild>
29
+ <ChevronDown className="h-4 w-4 opacity-50" />
30
+ </SelectPrimitive.Icon>
31
+ </SelectPrimitive.Trigger>
32
+ ))
33
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34
+
35
+ const SelectScrollUpButton = React.forwardRef<
36
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
37
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
38
+ >(({ className, ...props }, ref) => (
39
+ <SelectPrimitive.ScrollUpButton
40
+ ref={ref}
41
+ className={cn(
42
+ "flex cursor-default items-center justify-center py-1",
43
+ className
44
+ )}
45
+ {...props}
46
+ >
47
+ <ChevronUp className="h-4 w-4" />
48
+ </SelectPrimitive.ScrollUpButton>
49
+ ))
50
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51
+
52
+ const SelectScrollDownButton = React.forwardRef<
53
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
54
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
55
+ >(({ className, ...props }, ref) => (
56
+ <SelectPrimitive.ScrollDownButton
57
+ ref={ref}
58
+ className={cn(
59
+ "flex cursor-default items-center justify-center py-1",
60
+ className
61
+ )}
62
+ {...props}
63
+ >
64
+ <ChevronDown className="h-4 w-4" />
65
+ </SelectPrimitive.ScrollDownButton>
66
+ ))
67
+ SelectScrollDownButton.displayName =
68
+ SelectPrimitive.ScrollDownButton.displayName
69
+
70
+ const SelectContent = React.forwardRef<
71
+ React.ElementRef<typeof SelectPrimitive.Content>,
72
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
73
+ >(({ className, children, position = "popper", ...props }, ref) => (
74
+ <SelectPrimitive.Portal>
75
+ <SelectPrimitive.Content
76
+ ref={ref}
77
+ className={cn(
78
+ "relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
79
+ position === "popper" &&
80
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
81
+ className
82
+ )}
83
+ position={position}
84
+ {...props}
85
+ >
86
+ <SelectScrollUpButton />
87
+ <SelectPrimitive.Viewport
88
+ className={cn(
89
+ "p-1",
90
+ position === "popper" &&
91
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
92
+ )}
93
+ >
94
+ {children}
95
+ </SelectPrimitive.Viewport>
96
+ <SelectScrollDownButton />
97
+ </SelectPrimitive.Content>
98
+ </SelectPrimitive.Portal>
99
+ ))
100
+ SelectContent.displayName = SelectPrimitive.Content.displayName
101
+
102
+ const SelectLabel = React.forwardRef<
103
+ React.ElementRef<typeof SelectPrimitive.Label>,
104
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
105
+ >(({ className, ...props }, ref) => (
106
+ <SelectPrimitive.Label
107
+ ref={ref}
108
+ className={cn("px-2 py-1.5 text-sm font-semibold", className)}
109
+ {...props}
110
+ />
111
+ ))
112
+ SelectLabel.displayName = SelectPrimitive.Label.displayName
113
+
114
+ const SelectItem = React.forwardRef<
115
+ React.ElementRef<typeof SelectPrimitive.Item>,
116
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
117
+ >(({ className, children, ...props }, ref) => (
118
+ <SelectPrimitive.Item
119
+ ref={ref}
120
+ className={cn(
121
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
122
+ className
123
+ )}
124
+ {...props}
125
+ >
126
+ <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
127
+ <SelectPrimitive.ItemIndicator>
128
+ <Check className="h-4 w-4" />
129
+ </SelectPrimitive.ItemIndicator>
130
+ </span>
131
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
132
+ </SelectPrimitive.Item>
133
+ ))
134
+ SelectItem.displayName = SelectPrimitive.Item.displayName
135
+
136
+ const SelectSeparator = React.forwardRef<
137
+ React.ElementRef<typeof SelectPrimitive.Separator>,
138
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
139
+ >(({ className, ...props }, ref) => (
140
+ <SelectPrimitive.Separator
141
+ ref={ref}
142
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
143
+ {...props}
144
+ />
145
+ ))
146
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName
147
+
148
+ export {
149
+ Select,
150
+ SelectGroup,
151
+ SelectValue,
152
+ SelectTrigger,
153
+ SelectContent,
154
+ SelectLabel,
155
+ SelectItem,
156
+ SelectSeparator,
157
+ SelectScrollUpButton,
158
+ SelectScrollDownButton,
159
+ }
src/components/ui/separator.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as SeparatorPrimitive from "@radix-ui/react-separator"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const Separator = React.forwardRef<
7
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
8
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
9
+ >(
10
+ (
11
+ { className, orientation = "horizontal", decorative = true, ...props },
12
+ ref
13
+ ) => (
14
+ <SeparatorPrimitive.Root
15
+ ref={ref}
16
+ decorative={decorative}
17
+ orientation={orientation}
18
+ className={cn(
19
+ "shrink-0 bg-border",
20
+ orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ )
26
+ )
27
+ Separator.displayName = SeparatorPrimitive.Root.displayName
28
+
29
+ export { Separator }
src/components/ui/sheet.tsx ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as SheetPrimitive from "@radix-ui/react-dialog"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+ import { X } from "lucide-react"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Sheet = SheetPrimitive.Root
9
+
10
+ const SheetTrigger = SheetPrimitive.Trigger
11
+
12
+ const SheetClose = SheetPrimitive.Close
13
+
14
+ const SheetPortal = SheetPrimitive.Portal
15
+
16
+ const SheetOverlay = React.forwardRef<
17
+ React.ElementRef<typeof SheetPrimitive.Overlay>,
18
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
19
+ >(({ className, ...props }, ref) => (
20
+ <SheetPrimitive.Overlay
21
+ className={cn(
22
+ "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",
23
+ className
24
+ )}
25
+ {...props}
26
+ ref={ref}
27
+ />
28
+ ))
29
+ SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
30
+
31
+ const sheetVariants = cva(
32
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
33
+ {
34
+ variants: {
35
+ side: {
36
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
37
+ bottom:
38
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
39
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
40
+ right:
41
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
42
+ },
43
+ },
44
+ defaultVariants: {
45
+ side: "right",
46
+ },
47
+ }
48
+ )
49
+
50
+ interface SheetContentProps
51
+ extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
52
+ VariantProps<typeof sheetVariants> {}
53
+
54
+ const SheetContent = React.forwardRef<
55
+ React.ElementRef<typeof SheetPrimitive.Content>,
56
+ SheetContentProps
57
+ >(({ side = "right", className, children, ...props }, ref) => (
58
+ <SheetPortal>
59
+ <SheetOverlay />
60
+ <SheetPrimitive.Content
61
+ ref={ref}
62
+ className={cn(sheetVariants({ side }), className)}
63
+ {...props}
64
+ >
65
+ <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
66
+ <X className="h-4 w-4" />
67
+ <span className="sr-only">Close</span>
68
+ </SheetPrimitive.Close>
69
+ {children}
70
+ </SheetPrimitive.Content>
71
+ </SheetPortal>
72
+ ))
73
+ SheetContent.displayName = SheetPrimitive.Content.displayName
74
+
75
+ const SheetHeader = ({
76
+ className,
77
+ ...props
78
+ }: React.HTMLAttributes<HTMLDivElement>) => (
79
+ <div
80
+ className={cn(
81
+ "flex flex-col space-y-2 text-center sm:text-left",
82
+ className
83
+ )}
84
+ {...props}
85
+ />
86
+ )
87
+ SheetHeader.displayName = "SheetHeader"
88
+
89
+ const SheetFooter = ({
90
+ className,
91
+ ...props
92
+ }: React.HTMLAttributes<HTMLDivElement>) => (
93
+ <div
94
+ className={cn(
95
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
96
+ className
97
+ )}
98
+ {...props}
99
+ />
100
+ )
101
+ SheetFooter.displayName = "SheetFooter"
102
+
103
+ const SheetTitle = React.forwardRef<
104
+ React.ElementRef<typeof SheetPrimitive.Title>,
105
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
106
+ >(({ className, ...props }, ref) => (
107
+ <SheetPrimitive.Title
108
+ ref={ref}
109
+ className={cn("text-lg font-semibold text-foreground", className)}
110
+ {...props}
111
+ />
112
+ ))
113
+ SheetTitle.displayName = SheetPrimitive.Title.displayName
114
+
115
+ const SheetDescription = React.forwardRef<
116
+ React.ElementRef<typeof SheetPrimitive.Description>,
117
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
118
+ >(({ className, ...props }, ref) => (
119
+ <SheetPrimitive.Description
120
+ ref={ref}
121
+ className={cn("text-sm text-muted-foreground", className)}
122
+ {...props}
123
+ />
124
+ ))
125
+ SheetDescription.displayName = SheetPrimitive.Description.displayName
126
+
127
+ export {
128
+ Sheet,
129
+ SheetPortal,
130
+ SheetOverlay,
131
+ SheetTrigger,
132
+ SheetClose,
133
+ SheetContent,
134
+ SheetHeader,
135
+ SheetFooter,
136
+ SheetTitle,
137
+ SheetDescription,
138
+ }
src/components/ui/tabs.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as TabsPrimitive from "@radix-ui/react-tabs"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const Tabs = TabsPrimitive.Root
7
+
8
+ const TabsList = React.forwardRef<
9
+ React.ElementRef<typeof TabsPrimitive.List>,
10
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
11
+ >(({ className, ...props }, ref) => (
12
+ <TabsPrimitive.List
13
+ ref={ref}
14
+ className={cn(
15
+ "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ ))
21
+ TabsList.displayName = TabsPrimitive.List.displayName
22
+
23
+ const TabsTrigger = React.forwardRef<
24
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
25
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
26
+ >(({ className, ...props }, ref) => (
27
+ <TabsPrimitive.Trigger
28
+ ref={ref}
29
+ className={cn(
30
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
31
+ className
32
+ )}
33
+ {...props}
34
+ />
35
+ ))
36
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
37
+
38
+ const TabsContent = React.forwardRef<
39
+ React.ElementRef<typeof TabsPrimitive.Content>,
40
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
41
+ >(({ className, ...props }, ref) => (
42
+ <TabsPrimitive.Content
43
+ ref={ref}
44
+ className={cn(
45
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ ))
51
+ TabsContent.displayName = TabsPrimitive.Content.displayName
52
+
53
+ export { Tabs, TabsList, TabsTrigger, TabsContent }
src/index.html ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
6
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ <script type="module" src="/client/main.tsx"></script>
13
+ </body>
14
+ </html>
src/lib/api-url.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function getBaseUrl(): string {
2
+ // Check if we're in browser
3
+ if (typeof window !== 'undefined') {
4
+ if (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
5
+ return process.env.DOMAIN_URL;
6
+ }
7
+ return `${window.location.protocol}//${window.location.host}`;
8
+ }
9
+
10
+ return process.env.NODE_ENV === 'production'
11
+ ? process.env.DOMAIN_URL
12
+ : 'http://localhost:5000';
13
+ }
14
+
15
+ export function getApiUrl(endpoint: string): string {
16
+ const baseUrl = getBaseUrl();
17
+ // Remove leading slash if present to avoid double slashes
18
+ const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
19
+ return `${baseUrl}/api${cleanEndpoint}`;
20
+ }
21
+
22
+ export function getDisplayUrl(): string {
23
+ const baseUrl = getBaseUrl();
24
+ return baseUrl.replace(/^https?:\/\//, '');
25
+ }
src/lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
src/public/favicon.svg ADDED
src/server/index.ts ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express, { type Request, Response, NextFunction } from "express";
2
+ import { serveStatic } from "./static";
3
+ import { createServer } from "http";
4
+ import { initPluginLoader, getPluginLoader } from "./plugin-loader";
5
+ import { join } from "path";
6
+ import { initStatsTracker, getStatsTracker } from "./lib/stats-tracker";
7
+
8
+ const app = express();
9
+ const httpServer = createServer(app);
10
+
11
+ declare module "http" {
12
+ interface IncomingMessage {
13
+ rawBody: unknown;
14
+ }
15
+ }
16
+
17
+ app.use(
18
+ express.json({
19
+ verify: (req, _res, buf) => {
20
+ req.rawBody = buf;
21
+ },
22
+ }),
23
+ );
24
+
25
+ app.use(express.urlencoded({ extended: false }));
26
+
27
+ export function log(message: string, source = "express") {
28
+ const formattedTime = new Date().toLocaleTimeString("en-US", {
29
+ hour: "numeric",
30
+ minute: "2-digit",
31
+ second: "2-digit",
32
+ hour12: true,
33
+ });
34
+
35
+ console.log(`${formattedTime} [${source}] ${message}`);
36
+ }
37
+
38
+ interface RateLimitStore {
39
+ [key: string]: {
40
+ count: number;
41
+ resetTime: number;
42
+ };
43
+ }
44
+
45
+ const rateLimitStore: RateLimitStore = {};
46
+ const RATE_LIMIT = 25;
47
+ const WINDOW_MS = 60 * 1000;
48
+
49
+ setInterval(() => {
50
+ const now = Date.now();
51
+ Object.keys(rateLimitStore).forEach((key) => {
52
+ if (rateLimitStore[key].resetTime < now) {
53
+ delete rateLimitStore[key];
54
+ }
55
+ });
56
+ }, 5 * 60 * 1000);
57
+
58
+ app.use("/api", (req: Request, res: Response, next: NextFunction) => {
59
+ const clientIp = req.ip || req.socket.remoteAddress || "unknown";
60
+ const now = Date.now();
61
+
62
+ if (!rateLimitStore[clientIp]) {
63
+ rateLimitStore[clientIp] = {
64
+ count: 1,
65
+ resetTime: now + WINDOW_MS,
66
+ };
67
+ return next();
68
+ }
69
+
70
+ const clientData = rateLimitStore[clientIp];
71
+
72
+ if (now > clientData.resetTime) {
73
+ clientData.count = 1;
74
+ clientData.resetTime = now + WINDOW_MS;
75
+ return next();
76
+ }
77
+
78
+ clientData.count++;
79
+
80
+ const remaining = Math.max(0, RATE_LIMIT - clientData.count);
81
+ const resetInSeconds = Math.ceil((clientData.resetTime - now) / 1000);
82
+
83
+ res.setHeader("X-RateLimit-Limit", RATE_LIMIT.toString());
84
+ res.setHeader("X-RateLimit-Remaining", remaining.toString());
85
+ res.setHeader("X-RateLimit-Reset", resetInSeconds.toString());
86
+
87
+ if (clientData.count > RATE_LIMIT) {
88
+ log(`Rate limit exceeded for IP: ${clientIp}`, "rate-limit");
89
+ return res.status(429).json({
90
+ message: "Too many requests, please try again later.",
91
+ retryAfter: resetInSeconds,
92
+ });
93
+ }
94
+
95
+ next();
96
+ });
97
+
98
+ app.use((req, res, next) => {
99
+ const start = Date.now();
100
+ const path = req.path;
101
+ let capturedJsonResponse: Record<string, any> | undefined = undefined;
102
+
103
+ const originalResJson = res.json;
104
+ res.json = function (bodyJson, ...args) {
105
+ capturedJsonResponse = bodyJson;
106
+ return originalResJson.apply(res, [bodyJson, ...args]);
107
+ };
108
+
109
+ res.on("finish", () => {
110
+ const duration = Date.now() - start;
111
+ if (path.startsWith("/api")) {
112
+ let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
113
+ if (capturedJsonResponse) {
114
+ logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
115
+ }
116
+
117
+ log(logLine);
118
+
119
+ const excludedPaths = [
120
+ '/api/plugins',
121
+ '/api/stats',
122
+ '/api/categories',
123
+ '/docs'
124
+ ];
125
+
126
+ const isPluginEndpoint = !excludedPaths.some(excluded => path.startsWith(excluded));
127
+
128
+ if (isPluginEndpoint) {
129
+ const clientIp = req.ip || req.socket.remoteAddress || "unknown";
130
+ getStatsTracker().trackRequest(path, res.statusCode, clientIp);
131
+ }
132
+ }
133
+ });
134
+
135
+ next();
136
+ });
137
+
138
+ (async () => {
139
+ initStatsTracker();
140
+ log("Stats tracker initialized");
141
+
142
+ const pluginsDir = join(process.cwd(), "src/server/plugins");
143
+ const pluginLoader = initPluginLoader(pluginsDir);
144
+
145
+ const isDev = process.env.NODE_ENV === "development";
146
+ await pluginLoader.loadPlugins(app, isDev);
147
+
148
+ app.get("/api/plugins", (req, res) => {
149
+ const metadata = getPluginLoader().getPluginMetadata();
150
+ res.json({
151
+ success: true,
152
+ count: metadata.length,
153
+ plugins: metadata,
154
+ });
155
+ });
156
+
157
+ app.get("/api/plugins/category/:category", (req, res) => {
158
+ const { category } = req.params;
159
+ const allPlugins = getPluginLoader().getPluginMetadata();
160
+ const filtered = allPlugins.filter(p =>
161
+ p.category.includes(category)
162
+ );
163
+
164
+ res.json({
165
+ success: true,
166
+ category,
167
+ count: filtered.length,
168
+ plugins: filtered,
169
+ });
170
+ });
171
+
172
+ app.get("/api/stats", (req, res) => {
173
+ const globalStats = getStatsTracker().getGlobalStats();
174
+ const topEndpoints = getStatsTracker().getTopEndpoints(5);
175
+
176
+ res.json({
177
+ success: true,
178
+ stats: {
179
+ global: globalStats,
180
+ topEndpoints,
181
+ },
182
+ });
183
+ });
184
+
185
+ app.get("/api/stats/visitors", (req, res) => {
186
+ const chartData = getStatsTracker().getVisitorChartData();
187
+
188
+ res.json({
189
+ success: true,
190
+ data: chartData,
191
+ });
192
+ });
193
+
194
+ app.get("/api/categories", (req, res) => {
195
+ const allPlugins = getPluginLoader().getPluginMetadata();
196
+ const categoriesMap = new Map<string, number>();
197
+
198
+ allPlugins.forEach(plugin => {
199
+ plugin.category.forEach(cat => {
200
+ categoriesMap.set(cat, (categoriesMap.get(cat) || 0) + 1);
201
+ });
202
+ });
203
+
204
+ const categories = Array.from(categoriesMap.entries()).map(([name, count]) => ({
205
+ name,
206
+ count,
207
+ }));
208
+
209
+ res.json({
210
+ success: true,
211
+ categories,
212
+ });
213
+ });
214
+
215
+ app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
216
+ const status = err.status || err.statusCode || 500;
217
+ const message = err.message || "Internal Server Error";
218
+
219
+ res.status(status).json({ message });
220
+ throw err;
221
+ });
222
+
223
+ if (process.env.NODE_ENV === "production") {
224
+ serveStatic(app);
225
+ } else {
226
+ const { setupVite } = await import("./vite");
227
+ await setupVite(httpServer, app);
228
+ }
229
+
230
+ app.use((req: Request, res: Response, next: NextFunction) => {
231
+ if (req.path.startsWith("/api")) {
232
+ return res.status(404).json({
233
+ message: "API endpoint not found",
234
+ path: req.path,
235
+ });
236
+ }
237
+ next();
238
+ });
239
+
240
+ const port = parseInt(process.env.PORT || "7860", 10);
241
+ httpServer.listen(
242
+ {
243
+ port,
244
+ host: "0.0.0.0",
245
+ reusePort: true,
246
+ },
247
+ () => {
248
+ log(`serving on port ${port}`);
249
+ },
250
+ );
251
+
252
+ process.on('uncaughtException', (error: Error) => {
253
+ log(`Uncaught Exception: ${error.message}`, 'error');
254
+ console.error(error.stack);
255
+ });
256
+
257
+ process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
258
+ log(`Unhandled Rejection at: ${promise}, reason: ${reason}`, 'error');
259
+ console.error(reason);
260
+ });
261
+ })();
src/server/lib/response-helper.js ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Send standardized success response
3
+ */
4
+ export function sendSuccess(
5
+ res,
6
+ data,
7
+ message,
8
+ statusCode = 200
9
+ ) {
10
+ const response = {
11
+ status: statusCode,
12
+ author: "Ditzzy",
13
+ note: "Thank you for using this API!",
14
+ results: data,
15
+ };
16
+
17
+ if (message) {
18
+ response.message = message;
19
+ }
20
+
21
+ return res.status(statusCode).json(response);
22
+ }
23
+
24
+ /**
25
+ * Send standardized error response
26
+ */
27
+ export function sendError(
28
+ res,
29
+ statusCode,
30
+ message,
31
+ error
32
+ ) {
33
+ const response = {
34
+ status: statusCode,
35
+ message,
36
+ };
37
+
38
+ if (error) {
39
+ response.error = error;
40
+ }
41
+
42
+ return res.status(statusCode).json(response);
43
+ }
44
+
45
+ /**
46
+ * Common error responses
47
+ */
48
+ export const ErrorResponses = {
49
+ badRequest: (res, message = "Bad request") =>
50
+ sendError(res, 400, message),
51
+
52
+ invalidUrl: (res, message = "Invalid URL") =>
53
+ sendError(res, 400, message),
54
+
55
+ missingParameter: (res, param) =>
56
+ sendError(res, 400, `Missing required parameter: ${param}`),
57
+
58
+ invalidParameter: (res, param, reason) =>
59
+ sendError(
60
+ res,
61
+ 400,
62
+ `Invalid parameter: ${param}${reason ? ` - ${reason}` : ""}`
63
+ ),
64
+
65
+ notFound: (res, message = "Resource not found") =>
66
+ sendError(res, 404, message),
67
+
68
+ serverError: (
69
+ res,
70
+ message = "An error occurred, please try again later."
71
+ ) =>
72
+ sendError(res, 500, message),
73
+
74
+ tooManyRequests: (
75
+ res,
76
+ message = "Too many requests, please slow down."
77
+ ) =>
78
+ sendError(res, 429, message),
79
+
80
+ serviceUnavailable: (
81
+ res,
82
+ message = "Service temporarily unavailable"
83
+ ) =>
84
+ sendError(res, 503, message),
85
+ };
src/server/lib/stats-tracker.ts ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interface EndpointStats {
2
+ totalRequests: number;
3
+ successRequests: number;
4
+ failedRequests: number;
5
+ lastAccessed: number;
6
+ }
7
+
8
+ interface VisitorData {
9
+ timestamp: number;
10
+ count: number;
11
+ }
12
+
13
+ interface GlobalStats {
14
+ totalRequests: number;
15
+ totalSuccess: number;
16
+ totalFailed: number;
17
+ uniqueVisitors: Set<string>;
18
+ endpoints: Map<string, EndpointStats>;
19
+ startTime: number;
20
+ visitorsByHour: Map<number, Set<string>>;
21
+ }
22
+
23
+ class StatsTracker {
24
+ private stats: GlobalStats;
25
+
26
+ constructor() {
27
+ this.stats = {
28
+ totalRequests: 0,
29
+ totalSuccess: 0,
30
+ totalFailed: 0,
31
+ uniqueVisitors: new Set(),
32
+ endpoints: new Map(),
33
+ startTime: Date.now(),
34
+ visitorsByHour: new Map(),
35
+ };
36
+ }
37
+
38
+ trackRequest(endpoint: string, statusCode: number, clientIp: string) {
39
+ this.stats.totalRequests++;
40
+ this.stats.uniqueVisitors.add(clientIp);
41
+
42
+ const currentHour = Math.floor(Date.now() / (1000 * 60 * 60));
43
+ if (!this.stats.visitorsByHour.has(currentHour)) {
44
+ this.stats.visitorsByHour.set(currentHour, new Set());
45
+ }
46
+ this.stats.visitorsByHour.get(currentHour)!.add(clientIp);
47
+
48
+ const cutoffHour = currentHour - 24;
49
+ Array.from(this.stats.visitorsByHour.keys()).forEach(hour => {
50
+ if (hour < cutoffHour) {
51
+ this.stats.visitorsByHour.delete(hour);
52
+ }
53
+ });
54
+
55
+ if (statusCode >= 200 && statusCode < 400) {
56
+ this.stats.totalSuccess++;
57
+ } else {
58
+ this.stats.totalFailed++;
59
+ }
60
+
61
+ if (!this.stats.endpoints.has(endpoint)) {
62
+ this.stats.endpoints.set(endpoint, {
63
+ totalRequests: 0,
64
+ successRequests: 0,
65
+ failedRequests: 0,
66
+ lastAccessed: Date.now(),
67
+ });
68
+ }
69
+
70
+ const endpointStats = this.stats.endpoints.get(endpoint)!;
71
+ endpointStats.totalRequests++;
72
+ endpointStats.lastAccessed = Date.now();
73
+
74
+ if (statusCode >= 200 && statusCode < 400) {
75
+ endpointStats.successRequests++;
76
+ } else {
77
+ endpointStats.failedRequests++;
78
+ }
79
+ }
80
+
81
+ getGlobalStats() {
82
+ const uptime = Date.now() - this.stats.startTime;
83
+ const uptimeHours = Math.floor(uptime / (1000 * 60 * 60));
84
+ const uptimeDays = Math.floor(uptimeHours / 24);
85
+
86
+ return {
87
+ totalRequests: this.stats.totalRequests,
88
+ totalSuccess: this.stats.totalSuccess,
89
+ totalFailed: this.stats.totalFailed,
90
+ uniqueVisitors: this.stats.uniqueVisitors.size,
91
+ successRate: this.stats.totalRequests > 0
92
+ ? ((this.stats.totalSuccess / this.stats.totalRequests) * 100).toFixed(2)
93
+ : "0.00",
94
+ uptime: {
95
+ ms: uptime,
96
+ hours: uptimeHours,
97
+ days: uptimeDays,
98
+ formatted: uptimeDays > 0
99
+ ? `${uptimeDays}d ${uptimeHours % 24}h`
100
+ : `${uptimeHours}h`,
101
+ },
102
+ };
103
+ }
104
+
105
+ getVisitorChartData(): VisitorData[] {
106
+ const currentHour = Math.floor(Date.now() / (1000 * 60 * 60));
107
+ const data: VisitorData[] = [];
108
+
109
+ for (let i = 23; i >= 0; i--) {
110
+ const hour = currentHour - i;
111
+ const visitors = this.stats.visitorsByHour.get(hour);
112
+ const timestamp = hour * 1000 * 60 * 60;
113
+
114
+ data.push({
115
+ timestamp,
116
+ count: visitors ? visitors.size : 0,
117
+ });
118
+ }
119
+
120
+ return data;
121
+ }
122
+
123
+ getEndpointStats(endpoint: string) {
124
+ return this.stats.endpoints.get(endpoint) || null;
125
+ }
126
+
127
+ getAllEndpointStats() {
128
+ const result: Record<string, EndpointStats> = {};
129
+ this.stats.endpoints.forEach((stats, endpoint) => {
130
+ result[endpoint] = stats;
131
+ });
132
+ return result;
133
+ }
134
+
135
+ getTopEndpoints(limit: number = 10) {
136
+ return Array.from(this.stats.endpoints.entries())
137
+ .map(([endpoint, stats]) => ({ endpoint, ...stats }))
138
+ .sort((a, b) => b.totalRequests - a.totalRequests)
139
+ .slice(0, limit);
140
+ }
141
+
142
+ reset() {
143
+ this.stats = {
144
+ totalRequests: 0,
145
+ totalSuccess: 0,
146
+ totalFailed: 0,
147
+ uniqueVisitors: new Set(),
148
+ endpoints: new Map(),
149
+ startTime: Date.now(),
150
+ visitorsByHour: new Map(),
151
+ };
152
+ }
153
+ }
154
+
155
+ let statsTracker: StatsTracker;
156
+
157
+ export function initStatsTracker() {
158
+ statsTracker = new StatsTracker();
159
+ return statsTracker;
160
+ }
161
+
162
+ export function getStatsTracker() {
163
+ if (!statsTracker) {
164
+ throw new Error("StatsTracker not initialized. Call initStatsTracker() first.");
165
+ }
166
+ return statsTracker;
167
+ }
src/server/plugin-loader.ts ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Express, Router } from "express";
2
+ import { readdirSync, statSync, existsSync } from "fs";
3
+ import { join, extname, relative } from "path";
4
+ import { watch } from "chokidar";
5
+ import { pathToFileURL } from "url";
6
+ import { ApiPluginHandler, PluginMetadata, PluginRegistry } from "./types/plugin";
7
+
8
+ export class PluginLoader {
9
+ private pluginRegistry: PluginRegistry = {};
10
+ private pluginsDir: string;
11
+ private router: Router | null = null;
12
+ private app: Express | null = null;
13
+ private watcher: any = null;
14
+
15
+ constructor(pluginsDir: string) {
16
+ this.pluginsDir = pluginsDir;
17
+ }
18
+
19
+ async loadPlugins(app: Express, enableHotReload = false) {
20
+ this.app = app;
21
+ this.router = Router();
22
+
23
+ await this.scanDirectory(this.pluginsDir, this.router);
24
+ app.use("/api", this.router);
25
+
26
+ console.log(`✅ Loaded ${Object.keys(this.pluginRegistry).length} plugins`);
27
+
28
+ if (enableHotReload) {
29
+ this.enableHotReload();
30
+ }
31
+
32
+ return this.pluginRegistry;
33
+ }
34
+
35
+ private enableHotReload() {
36
+ if (this.watcher) {
37
+ console.log("Hot reload already enabled");
38
+ return;
39
+ }
40
+
41
+ console.log("🔥 Hot reload enabled for plugins");
42
+
43
+ let reloadTimeout: NodeJS.Timeout | null = null;
44
+
45
+ this.watcher = watch(this.pluginsDir, {
46
+ ignored: /(^|[\/\\])\../, // ignore dotfiles
47
+ persistent: true,
48
+ ignoreInitial: true,
49
+ awaitWriteFinish: {
50
+ stabilityThreshold: 500,
51
+ pollInterval: 100,
52
+ },
53
+ });
54
+
55
+ const handleChange = (eventType: string, path: string) => {
56
+ console.log(`📝 Plugin ${eventType}: ${relative(this.pluginsDir, path)}`);
57
+
58
+ if (reloadTimeout) {
59
+ clearTimeout(reloadTimeout);
60
+ }
61
+
62
+ reloadTimeout = setTimeout(() => {
63
+ this.reloadPlugins();
64
+ }, 200);
65
+ };
66
+
67
+ this.watcher
68
+ .on("add", (path: string) => handleChange("added", path))
69
+ .on("change", (path: string) => handleChange("changed", path))
70
+ .on("unlink", (path: string) => {
71
+ console.log(`🗑️ Plugin removed: ${relative(this.pluginsDir, path)}`);
72
+ this.reloadPlugins();
73
+ });
74
+ }
75
+
76
+ private async reloadPlugins() {
77
+ if (!this.app || !this.router) return;
78
+
79
+ try {
80
+ console.log("🔄 Reloading plugins...");
81
+ const oldRegistry = { ...this.pluginRegistry };
82
+ const oldRouter = this.router;
83
+ this.pluginRegistry = {};
84
+ const newRouter = Router();
85
+ this.clearModuleCache(this.pluginsDir);
86
+
87
+ try {
88
+ await this.scanDirectory(this.pluginsDir, newRouter);
89
+
90
+ // If successful, replace old router with new one
91
+ this.removeOldRouter();
92
+ this.router = newRouter;
93
+ this.app.use("/api", this.router);
94
+
95
+ console.log(`✅ Successfully reloaded ${Object.keys(this.pluginRegistry).length} plugins`);
96
+ } catch (scanError) {
97
+ console.error("❌ Error scanning plugins, rolling back...");
98
+ this.pluginRegistry = oldRegistry;
99
+ this.router = oldRouter;
100
+ throw scanError;
101
+ }
102
+ } catch (error) {
103
+ console.error("❌ Error reloading plugins:", error);
104
+ console.log("⚠️ Keeping previous plugin configuration");
105
+ }
106
+ }
107
+
108
+ private removeOldRouter() {
109
+ if (!this.app) return;
110
+
111
+ try {
112
+ // Express 5 uses app._router differently
113
+ const stack = (this.app as any)._router?.stack || [];
114
+
115
+ for (let i = stack.length - 1; i >= 0; i--) {
116
+ const layer = stack[i];
117
+ if (layer.name === 'router' && layer.regexp.test('/api')) {
118
+ stack.splice(i, 1);
119
+ }
120
+ }
121
+ } catch (error) {
122
+ // if _router structure is different, just log warning
123
+ console.warn("⚠️ Could not remove old router, continuing anyway...");
124
+ }
125
+ }
126
+
127
+ private clearModuleCache(dirPath: string) {
128
+ if (!existsSync(dirPath)) return;
129
+
130
+ const items = readdirSync(dirPath);
131
+
132
+ for (const item of items) {
133
+ const fullPath = join(dirPath, item);
134
+ const stat = statSync(fullPath);
135
+
136
+ if (stat.isDirectory()) {
137
+ this.clearModuleCache(fullPath);
138
+ } else if (stat.isFile() && (extname(item) === ".ts" || extname(item) === ".js")) {
139
+ // In ES modules, we can't clear cache like CommonJS.
140
+ // Hot Reload also doesn't seem to have any effect on API serving.
141
+ // For now, Just log and mark as reload, We have to restart the server in "development" mode.
142
+ // TODO: Find another way. If hot reloading doesn't work, try restarting automatically.
143
+ const relativePath = relative(process.cwd(), fullPath);
144
+ console.log(`♻️ Marked for reload: ${relativePath}`);
145
+ }
146
+ }
147
+ }
148
+
149
+ private async scanDirectory(dir: string, router: Router, categoryPath: string[] = []) {
150
+ try {
151
+ const items = readdirSync(dir);
152
+
153
+ for (const item of items) {
154
+ const fullPath = join(dir, item);
155
+ const stat = statSync(fullPath);
156
+
157
+ if (stat.isDirectory()) {
158
+ await this.scanDirectory(fullPath, router, [...categoryPath, item]);
159
+ } else if (stat.isFile() && (extname(item) === ".ts" || extname(item) === ".js")) {
160
+ await this.loadPlugin(fullPath, router, categoryPath);
161
+ }
162
+ }
163
+ } catch (error) {
164
+ console.error(`❌ Error scanning directory ${dir}:`, error);
165
+ }
166
+ }
167
+
168
+ private isValidPluginMetadata(handler: ApiPluginHandler, fileName: string): { valid: boolean; reason?: string } {
169
+ if (!handler.category || !Array.isArray(handler.category) || handler.category.length === 0) {
170
+ return { valid: false, reason: 'category is missing or empty' };
171
+ }
172
+
173
+ if (!handler.name || typeof handler.name !== 'string' || handler.name.trim() === '') {
174
+ return { valid: false, reason: 'name is missing or empty' };
175
+ }
176
+
177
+ if (!handler.description || typeof handler.description !== 'string' || handler.description.trim() === '') {
178
+ return { valid: false, reason: 'description is missing or empty' };
179
+ }
180
+
181
+ return { valid: true };
182
+ }
183
+
184
+ private async loadPlugin(filePath: string, router: Router, categoryPath: string[]) {
185
+ const fileName = relative(this.pluginsDir, filePath);
186
+
187
+ try {
188
+ const fileUrl = pathToFileURL(filePath).href;
189
+ const cacheBuster = `?update=${Date.now()}`;
190
+ const module = await import(fileUrl + cacheBuster);
191
+
192
+ const handler: ApiPluginHandler = module.default;
193
+
194
+ if (!handler || !handler.exec) {
195
+ console.warn(`⚠️ Skipping plugin '${fileName}': missing handler or exec function`);
196
+ return;
197
+ }
198
+
199
+ if (!handler.method) {
200
+ console.warn(`⚠️ Skipping plugin '${fileName}': missing 'method' field`);
201
+ return;
202
+ }
203
+
204
+ if (!handler.alias || handler.alias.length === 0) {
205
+ console.warn(`⚠️ Skipping plugin '${fileName}': missing 'alias' array`);
206
+ return;
207
+ }
208
+
209
+ if (typeof handler.exec !== 'function') {
210
+ console.warn(`⚠️ Skipping plugin '${fileName}': 'exec' must be a function`);
211
+ return;
212
+ }
213
+
214
+ const metadataValidation = this.isValidPluginMetadata(handler, fileName);
215
+ const shouldShowInDocs = metadataValidation.valid;
216
+
217
+ if (!shouldShowInDocs) {
218
+ console.warn(`⚠️ Plugin '${fileName}' will be hidden from docs: ${metadataValidation.reason}`);
219
+ }
220
+
221
+ const basePath = handler.category && handler.category.length > 0
222
+ ? `/${handler.category.join("/")}`
223
+ : "";
224
+
225
+ const primaryAlias = handler.alias[0];
226
+ const primaryEndpoint = basePath ? `${basePath}/${primaryAlias}` : `/${primaryAlias}`;
227
+ const method = handler.method.toLowerCase() as "get" | "post" | "put" | "delete" | "patch";
228
+
229
+ const wrappedExec = async (req: any, res: any, next: any) => {
230
+ try {
231
+ await handler.exec(req, res, next);
232
+ } catch (error) {
233
+ console.error(`❌ Error in plugin ${handler.name || 'unknown'}:`, error);
234
+ if (!res.headersSent) {
235
+ res.status(500).json({
236
+ success: false,
237
+ message: "Plugin execution error",
238
+ plugin: handler.name || 'unknown',
239
+ error: error instanceof Error ? error.message : "Unknown error",
240
+ });
241
+ }
242
+ }
243
+ };
244
+
245
+ for (const alias of handler.alias) {
246
+ const endpoint = basePath ? `${basePath}/${alias}` : `/${alias}`;
247
+ router[method](endpoint, wrappedExec);
248
+ console.log(`✓ [${handler.method}] ${endpoint} -> ${handler.name || 'unnamed'}`);
249
+ }
250
+
251
+ if (shouldShowInDocs) {
252
+ const metadata: PluginMetadata = {
253
+ name: handler.name,
254
+ description: handler.description,
255
+ version: handler.version || "1.0.0",
256
+ category: handler.category,
257
+ method: handler.method,
258
+ endpoint: primaryEndpoint,
259
+ aliases: handler.alias,
260
+ tags: handler.tags || [],
261
+ parameters: handler.parameters || {
262
+ query: [],
263
+ body: [],
264
+ headers: [],
265
+ path: []
266
+ },
267
+ responses: handler.responses || {}
268
+ };
269
+
270
+ this.pluginRegistry[primaryEndpoint] = { handler, metadata };
271
+ }
272
+ } catch (error) {
273
+ console.error(`❌ Failed to load plugin '${fileName}':`, error instanceof Error ? error.message : error);
274
+ }
275
+ }
276
+
277
+ getPluginMetadata(): PluginMetadata[] {
278
+ return Object.values(this.pluginRegistry).map(p => p.metadata);
279
+ }
280
+
281
+ getPluginRegistry(): PluginRegistry {
282
+ return this.pluginRegistry;
283
+ }
284
+
285
+ stopHotReload() {
286
+ if (this.watcher) {
287
+ this.watcher.close();
288
+ this.watcher = null;
289
+ console.log("🛑 Hot reload stopped");
290
+ }
291
+ }
292
+ }
293
+
294
+ let pluginLoader: PluginLoader;
295
+
296
+ export function initPluginLoader(pluginsDir: string) {
297
+ pluginLoader = new PluginLoader(pluginsDir);
298
+ return pluginLoader;
299
+ }
300
+
301
+ export function getPluginLoader() {
302
+ return pluginLoader;
303
+ }
src/server/plugins/data.js ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const handler = {
2
+ name: "Greet user",
3
+ description: "Greet the user",
4
+ method: "GET",
5
+ category: [],
6
+ alias: ["data"],
7
+ exec: async (req, res) => {
8
+ res.json({
9
+ status: 200,
10
+ message: "Welcome to DitzzyAPI, Lets get started by visit our documentation on: https://api.ditzzy.my.id/docs"
11
+ })
12
+ }
13
+ }
14
+
15
+ export default handler
src/server/plugins/downloader/tiktok.js ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from "axios";
2
+ import { sendSuccess, ErrorResponses } from "../../lib/response-helper.js";
3
+
4
+ /** @type {import("../../types/plugin").ApiPluginHandler} */
5
+ const handler = {
6
+ name: "TikTok Downloader",
7
+ description: "Download videos or slide photos from TikTok URLs. Supports both standard and HD quality downloads.",
8
+ version: "1.0.0",
9
+ method: "GET",
10
+ category: ["downloader"],
11
+ alias: ["tiktok", "tt"],
12
+ tags: ["social-media", "video", "downloader"],
13
+
14
+ parameters: {
15
+ query: [
16
+ {
17
+ name: "url",
18
+ type: "string",
19
+ required: true,
20
+ description: "TikTok video URL to download",
21
+ example: "https://www.tiktok.com/@username/video/1234567890",
22
+ pattern: "^https?:\\/\\/(www\\.|vm\\.)?tiktok\\.com\\/.+$"
23
+ }
24
+ ],
25
+ body: [],
26
+ headers: []
27
+ },
28
+
29
+ responses: {
30
+ 200: {
31
+ status: 200,
32
+ description: "Successfully retrieved TikTok video data",
33
+ example: {
34
+ status: 200,
35
+ author: "Ditzzy",
36
+ note: "Thank you for using this API!",
37
+ results: {
38
+ id: "1234567890",
39
+ title: "Video Title",
40
+ author: {
41
+ nickname: "Username",
42
+ unique_id: "username"
43
+ },
44
+ play: "https://video-url.com/video.mp4",
45
+ wmplay: "https://video-url.com/video-watermark.mp4",
46
+ hdplay: "https://video-url.com/video-hd.mp4",
47
+ music: "https://music-url.com/audio.mp3",
48
+ duration: 15,
49
+ create_time: 1234567890
50
+ }
51
+ }
52
+ },
53
+ 400: {
54
+ status: 400,
55
+ description: "Invalid TikTok URL provided",
56
+ example: {
57
+ status: 400,
58
+ message: "Invalid URL - must be a valid TikTok URL"
59
+ }
60
+ },
61
+ 404: {
62
+ status: 404,
63
+ description: "Missing required parameter",
64
+ example: {
65
+ status: 404,
66
+ message: "Missing required parameter: url"
67
+ }
68
+ },
69
+ 500: {
70
+ status: 500,
71
+ description: "Server error or TikTok API unavailable",
72
+ example: {
73
+ status: 500,
74
+ message: "An error occurred, please try again later."
75
+ }
76
+ }
77
+ },
78
+
79
+ exec: async (req, res) => {
80
+ const { url } = req.query;
81
+
82
+ if (!url) {
83
+ return ErrorResponses.missingParameter(res, "url");
84
+ }
85
+
86
+ if (!url.match(/tiktok/gi)) {
87
+ return ErrorResponses.invalidUrl(res, "Invalid URL - must be a valid TikTok URL");
88
+ }
89
+
90
+ try {
91
+ const videoData = await fetchTikTokVideo(url);
92
+ return sendSuccess(res, videoData);
93
+ } catch (error) {
94
+ console.error("TikTok download error:", error);
95
+ return ErrorResponses.serverError(res);
96
+ }
97
+ }
98
+ };
99
+
100
+ export default handler;
101
+
102
+ async function fetchTikTokVideo(url) {
103
+ const encodedParams = new URLSearchParams();
104
+ encodedParams.set("url", url);
105
+ encodedParams.set("hd", "1");
106
+
107
+ const response = await axios({
108
+ method: "POST",
109
+ url: "https://tikwm.com/api/",
110
+ headers: {
111
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
112
+ "Cookie": "current_language=en",
113
+ "User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36"
114
+ },
115
+ data: encodedParams
116
+ });
117
+
118
+ if (!response.data || !response.data.data) {
119
+ throw new Error("Invalid response from TikTok API");
120
+ }
121
+
122
+ return response.data.data;
123
+ }
src/server/static.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express, { type Express } from "express";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ export function serveStatic(app: Express) {
6
+ const distPath = path.resolve(__dirname, "public");
7
+ if (!fs.existsSync(distPath)) {
8
+ throw new Error(
9
+ `Could not find the build directory: ${distPath}, make sure to build the client first`,
10
+ );
11
+ }
12
+
13
+ app.use(express.static(distPath));
14
+
15
+ // fall through to index.html if the file doesn't exist
16
+ app.use((req, res) => {
17
+ res.sendFile(path.resolve(distPath, "index.html"));
18
+ });
19
+ }
src/server/types/plugin.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from "express";
2
+
3
+ export interface PluginParameter {
4
+ name: string;
5
+ type: "string" | "number" | "boolean" | "array" | "object";
6
+ required: boolean;
7
+ description: string;
8
+ example?: any;
9
+ default?: any;
10
+ enum?: any[];
11
+ pattern?: string;
12
+ }
13
+
14
+ export interface PluginResponse {
15
+ status: number;
16
+ description: string;
17
+ example: any;
18
+ }
19
+
20
+ export interface PluginParameters {
21
+ query?: PluginParameter[];
22
+ body?: PluginParameter[];
23
+ headers?: PluginParameter[];
24
+ path?: PluginParameter[];
25
+ }
26
+
27
+ export interface ApiPluginHandler {
28
+ name: string;
29
+ description: string;
30
+ version: string;
31
+ category: string[];
32
+ method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
33
+ alias: string[];
34
+ tags?: string[];
35
+ parameters?: PluginParameters;
36
+ responses?: {
37
+ [statusCode: number]: PluginResponse;
38
+ };
39
+
40
+ exec: (req: Request, res: Response, next: NextFunction) => Promise<any> | any;
41
+ }
42
+
43
+ export interface PluginMetadata {
44
+ name: string;
45
+ description: string;
46
+ version: string;
47
+ category: string[];
48
+ method: string;
49
+ endpoint: string;
50
+ aliases: string[];
51
+ tags?: string[];
52
+ parameters?: PluginParameters;
53
+ responses?: {
54
+ [statusCode: number]: PluginResponse;
55
+ };
56
+ }
57
+
58
+ export interface PluginRegistry {
59
+ [endpoint: string]: {
60
+ handler: ApiPluginHandler;
61
+ metadata: PluginMetadata;
62
+ };
63
+ }
64
+
65
+ export interface ApiResponse<T = any> {
66
+ status: number;
67
+ message?: string;
68
+ author?: string;
69
+ note?: string;
70
+ results?: T;
71
+ error?: string;
72
+ }
src/server/vite.ts ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type Express } from "express";
2
+ import { createServer as createViteServer, createLogger } from "vite";
3
+ import { type Server } from "http";
4
+ import viteConfig from "../../vite.config";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { nanoid } from "nanoid";
8
+
9
+ const viteLogger = createLogger();
10
+
11
+ export async function setupVite(server: Server, app: Express) {
12
+ const serverOptions = {
13
+ middlewareMode: true,
14
+ hmr: { server, path: "/vite-hmr" },
15
+ allowedHosts: true as const,
16
+ };
17
+
18
+ const vite = await createViteServer({
19
+ ...viteConfig,
20
+ configFile: false,
21
+ customLogger: {
22
+ ...viteLogger,
23
+ error: (msg, options) => {
24
+ viteLogger.error(msg, options);
25
+ process.exit(1);
26
+ },
27
+ },
28
+ server: serverOptions,
29
+ appType: "custom",
30
+ });
31
+
32
+ app.use(vite.middlewares);
33
+
34
+ app.use(async (req, res, next) => {
35
+ const url = req.originalUrl;
36
+
37
+ try {
38
+ const clientTemplate = path.resolve(
39
+ import.meta.dirname,
40
+ "..",
41
+ "index.html",
42
+ );
43
+
44
+ // always reload the index.html file from disk incase it changes
45
+ let template = await fs.promises.readFile(clientTemplate, "utf-8");
46
+ template = template.replace(
47
+ `client="/client/main.tsx"`,
48
+ `client="/client/main.tsx?v=${nanoid()}"`,
49
+ );
50
+ const page = await vite.transformIndexHtml(url, template);
51
+ res.status(200).set({ "Content-Type": "text/html" }).end(page);
52
+ } catch (e) {
53
+ vite.ssrFixStacktrace(e as Error);
54
+ next(e);
55
+ }
56
+ });
57
+ }
tailwind.config.ts ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Config } from "tailwindcss";
2
+
3
+ export default {
4
+ darkMode: ["class"],
5
+ content: ["./src/index.html", "./src/**/*.{js,jsx,ts,tsx}"],
6
+ theme: {
7
+ extend: {
8
+ borderRadius: {
9
+ lg: ".5625rem", /* 9px */
10
+ md: ".375rem", /* 6px */
11
+ sm: ".1875rem", /* 3px */
12
+ },
13
+ colors: {
14
+ // Flat
15
+ background: "hsl(var(--background) / <alpha-value>)",
16
+ foreground: "hsl(var(--foreground) / <alpha-value>)",
17
+ border: "hsl(var(--border) / <alpha-value>)",
18
+ input: "hsl(var(--input) / <alpha-value>)",
19
+ card: {
20
+ DEFAULT: "hsl(var(--card) / <alpha-value>)",
21
+ foreground: "hsl(var(--card-foreground) / <alpha-value>)",
22
+ border: "hsl(var(--card-border) / <alpha-value>)",
23
+ },
24
+ popover: {
25
+ DEFAULT: "hsl(var(--popover) / <alpha-value>)",
26
+ foreground: "hsl(var(--popover-foreground) / <alpha-value>)",
27
+ border: "hsl(var(--popover-border) / <alpha-value>)",
28
+ },
29
+ primary: {
30
+ DEFAULT: "hsl(var(--primary) / <alpha-value>)",
31
+ foreground: "hsl(var(--primary-foreground) / <alpha-value>)",
32
+ border: "var(--primary-border)",
33
+ },
34
+ secondary: {
35
+ DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
36
+ foreground: "hsl(var(--secondary-foreground) / <alpha-value>)",
37
+ border: "var(--secondary-border)",
38
+ },
39
+ muted: {
40
+ DEFAULT: "hsl(var(--muted) / <alpha-value>)",
41
+ foreground: "hsl(var(--muted-foreground) / <alpha-value>)",
42
+ border: "var(--muted-border)",
43
+ },
44
+ accent: {
45
+ DEFAULT: "hsl(var(--accent) / <alpha-value>)",
46
+ foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
47
+ border: "var(--accent-border)",
48
+ },
49
+ destructive: {
50
+ DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
51
+ foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
52
+ border: "var(--destructive-border)",
53
+ },
54
+ ring: "hsl(var(--ring) / <alpha-value>)",
55
+ chart: {
56
+ "1": "hsl(var(--chart-1) / <alpha-value>)",
57
+ "2": "hsl(var(--chart-2) / <alpha-value>)",
58
+ "3": "hsl(var(--chart-3) / <alpha-value>)",
59
+ "4": "hsl(var(--chart-4) / <alpha-value>)",
60
+ "5": "hsl(var(--chart-5) / <alpha-value>)",
61
+ },
62
+ sidebar: {
63
+ ring: "hsl(var(--sidebar-ring) / <alpha-value>)",
64
+ DEFAULT: "hsl(var(--sidebar) / <alpha-value>)",
65
+ foreground: "hsl(var(--sidebar-foreground) / <alpha-value>)",
66
+ border: "hsl(var(--sidebar-border) / <alpha-value>)",
67
+ },
68
+ "sidebar-primary": {
69
+ DEFAULT: "hsl(var(--sidebar-primary) / <alpha-value>)",
70
+ foreground: "hsl(var(--sidebar-primary-foreground) / <alpha-value>)",
71
+ border: "var(--sidebar-primary-border)",
72
+ },
73
+ "sidebar-accent": {
74
+ DEFAULT: "hsl(var(--sidebar-accent) / <alpha-value>)",
75
+ foreground: "hsl(var(--sidebar-accent-foreground) / <alpha-value>)",
76
+ border: "var(--sidebar-accent-border)"
77
+ },
78
+ status: {
79
+ online: "rgb(34 197 94)",
80
+ away: "rgb(245 158 11)",
81
+ busy: "rgb(239 68 68)",
82
+ offline: "rgb(156 163 175)",
83
+ },
84
+ },
85
+ fontFamily: {
86
+ sans: ["Inter", "sans-serif"],
87
+ display: ["Space Grotesk", "sans-serif"],
88
+ mono: ["JetBrains Mono", "monospace"],
89
+ },
90
+ keyframes: {
91
+ "accordion-down": {
92
+ from: { height: "0" },
93
+ to: { height: "var(--radix-accordion-content-height)" },
94
+ },
95
+ "accordion-up": {
96
+ from: { height: "var(--radix-accordion-content-height)" },
97
+ to: { height: "0" },
98
+ },
99
+ },
100
+ animation: {
101
+ "accordion-down": "accordion-down 0.2s ease-out",
102
+ "accordion-up": "accordion-up 0.2s ease-out",
103
+ },
104
+ },
105
+ },
106
+ plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
107
+ } satisfies Config;
tsconfig.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "include": ["src/**/*"],
3
+ "exclude": ["node_modules", "build", "dist", "**/*.test.ts"],
4
+ "compilerOptions": {
5
+ "incremental": true,
6
+ "tsBuildInfoFile": "./node_modules/typescript/tsbuildinfo",
7
+ "noEmit": true,
8
+ "module": "ESNext",
9
+ "strict": true,
10
+ "lib": ["esnext", "dom", "dom.iterable"],
11
+ "jsx": "preserve",
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "allowImportingTsExtensions": true,
15
+ "allowSyntheticDefaultImports": true,
16
+ "moduleResolution": "bundler",
17
+ "baseUrl": ".",
18
+ "types": ["node", "vite/client"],
19
+ "paths": {
20
+ "@/*": ["./src/*"]
21
+ }
22
+ }
23
+ }
vite.config.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite"
2
+ import react from "@vitejs/plugin-react"
3
+ import path from "node:path"
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ resolve: {
8
+ alias: {
9
+ "@": path.resolve(import.meta.dirname, "src"),
10
+ },
11
+ },
12
+ root: path.resolve(import.meta.dirname, "src"),
13
+ build: {
14
+ outDir: path.resolve(import.meta.dirname, "dist/public"),
15
+ emptyOutDir: true,
16
+ },
17
+ server: {
18
+ fs: {
19
+ strict: true,
20
+ deny: ["**/.*"],
21
+ },
22
+ },
23
+ })