├── .gitignore
├── .replit
├── LICENSE
├── README.md
├── client
├── index.html
└── src
│ ├── App.tsx
│ ├── components
│ ├── api-test
│ │ ├── request-builder.tsx
│ │ └── response-viewer.tsx
│ ├── tasks
│ │ ├── status-chart.tsx
│ │ ├── task-form.tsx
│ │ └── task-list.tsx
│ └── ui
│ │ ├── accordion.tsx
│ │ ├── alert-dialog.tsx
│ │ ├── alert.tsx
│ │ ├── aspect-ratio.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── button.tsx
│ │ ├── calendar.tsx
│ │ ├── card.tsx
│ │ ├── carousel.tsx
│ │ ├── chart.tsx
│ │ ├── checkbox.tsx
│ │ ├── collapsible.tsx
│ │ ├── command.tsx
│ │ ├── context-menu.tsx
│ │ ├── dialog.tsx
│ │ ├── drawer.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── hover-card.tsx
│ │ ├── input-otp.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── menubar.tsx
│ │ ├── navigation-menu.tsx
│ │ ├── pagination.tsx
│ │ ├── popover.tsx
│ │ ├── progress.tsx
│ │ ├── radio-group.tsx
│ │ ├── resizable.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── sidebar.tsx
│ │ ├── skeleton.tsx
│ │ ├── slider.tsx
│ │ ├── switch.tsx
│ │ ├── table.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ ├── toggle-group.tsx
│ │ ├── toggle.tsx
│ │ └── tooltip.tsx
│ ├── hooks
│ ├── use-mobile.tsx
│ └── use-toast.ts
│ ├── index.css
│ ├── lib
│ ├── api.ts
│ ├── queryClient.ts
│ ├── utils.ts
│ └── websocket.ts
│ ├── main.tsx
│ └── pages
│ ├── dashboard.tsx
│ ├── metrics-dashboard.tsx
│ ├── not-found.tsx
│ └── test-dashboard.tsx
├── db
├── index.ts
└── schema.ts
├── drizzle.config.ts
├── generated-icon.png
├── package-lock.json
├── package.json
├── postcss.config.js
├── server
├── index.ts
├── routes.ts
└── vite.ts
├── tailwind.config.ts
├── theme.json
├── tsconfig.json
└── vite.config.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .DS_Store
4 | server/public
5 | vite.config.ts.*
6 | *.tar.gz
--------------------------------------------------------------------------------
/.replit:
--------------------------------------------------------------------------------
1 | modules = ["nodejs-20", "web", "postgresql-16"]
2 | run = "npm run dev"
3 | hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"]
4 |
5 | [nix]
6 | channel = "stable-24_05"
7 |
8 | [deployment]
9 | deploymentTarget = "cloudrun"
10 | build = ["npm", "run", "build"]
11 | run = ["npm", "run", "start"]
12 |
13 | [[ports]]
14 | localPort = 5000
15 | externalPort = 80
16 |
17 | [workflows]
18 | runButton = "Project"
19 |
20 | [[workflows.workflow]]
21 | name = "Project"
22 | mode = "parallel"
23 | author = "agent"
24 |
25 | [[workflows.workflow.tasks]]
26 | task = "workflow.run"
27 | args = "Start application"
28 |
29 | [[workflows.workflow]]
30 | name = "Start application"
31 | author = "agent"
32 |
33 | [workflows.workflow.metadata]
34 | agentRequireRestartOnSave = false
35 |
36 | [[workflows.workflow.tasks]]
37 | task = "packager.installForAll"
38 |
39 | [[workflows.workflow.tasks]]
40 | task = "shell.exec"
41 | args = "npm run dev"
42 | waitForPort = 5000
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Pippin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | *This tool was entirely built, tested, and open-sourced on Github using a combination of OpenAI's Operator using Replit Agent ([video](https://x.com/yoheinakajima/status/1882707264936845329)).*
2 |
3 | # Pippin Tasks
4 |
5 | A comprehensive API-based task management system designed for AI agents and teams, featuring advanced real-time monitoring, robust performance metrics tracking, and dynamic task tracking functionalities.
6 |
7 | ## Features
8 |
9 | - 📋 **Task Management**
10 | - Create, update, and delete tasks
11 | - Real-time status updates via WebSocket
12 | - Priority and assignment tracking
13 | - Task status visualization
14 |
15 | - 🧪 **API Testing Dashboard**
16 | - Interactive API request builder
17 | - Response viewer with syntax highlighting
18 | - Historical test results tracking
19 | - Support for all HTTP methods
20 |
21 | - 📊 **Performance Metrics**
22 | - Real-time API response time tracking
23 | - Error rate monitoring
24 | - Endpoint performance visualization
25 | - Request volume analytics
26 |
27 | ## Tech Stack
28 |
29 | - **Frontend**
30 | - React.js with TypeScript
31 | - TanStack Query for data fetching
32 | - Recharts for data visualization
33 | - Tailwind CSS with shadcn/ui components
34 | - WebSocket for real-time updates
35 |
36 | - **Backend**
37 | - Express.js
38 | - PostgreSQL database
39 | - Drizzle ORM
40 | - WebSocket server
41 | - Performance metrics middleware
42 |
43 | ## Getting Started
44 |
45 | 1. Clone the repository:
46 | ```bash
47 | git clone https://github.com/pippinlovesyou/pippin-tasks.git
48 | cd pippin-tasks
49 | ```
50 |
51 | 2. Install dependencies:
52 | ```bash
53 | npm install
54 | ```
55 |
56 | 3. Set up the database:
57 | ```bash
58 | # Create a PostgreSQL database and set the DATABASE_URL environment variable
59 | npm run db:push
60 | ```
61 |
62 | 4. Start the development server:
63 | ```bash
64 | npm run dev
65 | ```
66 |
67 | ## API Documentation
68 |
69 | ### Tasks
70 |
71 | ```typescript
72 | // Task schema
73 | interface Task {
74 | id: number;
75 | title: string;
76 | description?: string;
77 | status: 'pending' | 'in-progress' | 'completed' | 'cancelled';
78 | assignedTo?: string;
79 | priority: 'low' | 'medium' | 'high';
80 | createdAt: Date;
81 | updatedAt: Date;
82 | }
83 | ```
84 |
85 | #### Endpoints
86 |
87 | - `GET /api/tasks` - Get all tasks
88 | - `POST /api/tasks` - Create a new task
89 | - `PUT /api/tasks/:id` - Update a task
90 | - `DELETE /api/tasks/:id` - Delete a task
91 |
92 | ### API Tests
93 |
94 | ```typescript
95 | // API Test schema
96 | interface ApiTest {
97 | id: number;
98 | endpoint: string;
99 | method: string;
100 | requestBody?: string;
101 | responseStatus: number;
102 | responseBody?: string;
103 | createdAt: Date;
104 | }
105 | ```
106 |
107 | #### Endpoints
108 |
109 | - `GET /api/tests` - Get all API tests
110 | - `POST /api/tests` - Create a new API test
111 |
112 | ### Metrics
113 |
114 | ```typescript
115 | // Metric schema
116 | interface Metric {
117 | id: number;
118 | endpoint: string;
119 | method: string;
120 | responseTime: number;
121 | responseStatus: number;
122 | timestamp: Date;
123 | }
124 | ```
125 |
126 | #### Endpoints
127 |
128 | - `GET /api/metrics` - Get performance metrics
129 |
130 | ## Real-time Updates
131 |
132 | The application uses WebSocket connections for real-time updates. The WebSocket server broadcasts task changes to all connected clients, ensuring that the UI stays synchronized with the latest data.
133 |
134 | ## Development Guidelines
135 |
136 | - Follow the established code structure and patterns
137 | - Use TypeScript for type safety
138 | - Keep the components modular and reusable
139 | - Add appropriate error handling
140 | - Write clear commit messages
141 | - Update documentation when adding new features
142 |
143 | ## Screenshots
144 |
145 | ### Task Dashboard
146 | 
147 |
148 | ### API Test Dashboard
149 | 
150 |
151 | ### Metrics Dashboard
152 | 
153 |
154 | ## Contributing
155 |
156 | 1. Fork the repository
157 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
158 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
159 | 4. Push to the branch (`git push origin feature/amazing-feature`)
160 | 5. Open a Pull Request
161 |
162 | ## License
163 |
164 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
165 |
166 | ## Acknowledgments
167 |
168 | - Built with ❤️ by the Pippin team
169 | - Special thanks to all contributors
170 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/client/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Switch, Route } from "wouter";
2 | import { QueryClientProvider } from "@tanstack/react-query";
3 | import { queryClient } from "./lib/queryClient";
4 | import { Toaster } from "@/components/ui/toaster";
5 | import Dashboard from "@/pages/dashboard";
6 | import TestDashboard from "@/pages/test-dashboard";
7 | import MetricsDashboard from "@/pages/metrics-dashboard";
8 | import NotFound from "@/pages/not-found";
9 |
10 | function Router() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | function App() {
22 | return (
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | export default App;
--------------------------------------------------------------------------------
/client/src/components/api-test/request-builder.tsx:
--------------------------------------------------------------------------------
1 | import { useForm } from "react-hook-form";
2 | import { Button } from "@/components/ui/button";
3 | import { Input } from "@/components/ui/input";
4 | import {
5 | Form,
6 | FormControl,
7 | FormField,
8 | FormItem,
9 | FormLabel,
10 | FormMessage,
11 | } from "@/components/ui/form";
12 | import {
13 | Select,
14 | SelectContent,
15 | SelectItem,
16 | SelectTrigger,
17 | SelectValue,
18 | } from "@/components/ui/select";
19 | import { Textarea } from "@/components/ui/textarea";
20 | import { useMutation, useQueryClient } from "@tanstack/react-query";
21 | import { createApiTest } from "@/lib/api";
22 | import { useToast } from "@/hooks/use-toast";
23 |
24 | export function RequestBuilder() {
25 | const queryClient = useQueryClient();
26 | const { toast } = useToast();
27 |
28 | const form = useForm({
29 | defaultValues: {
30 | endpoint: "/api/tasks",
31 | method: "GET",
32 | requestBody: "",
33 | },
34 | });
35 |
36 | const mutation = useMutation({
37 | mutationFn: async (values: {
38 | endpoint: string;
39 | method: string;
40 | requestBody: string;
41 | }) => {
42 | try {
43 | const response = await fetch(values.endpoint, {
44 | method: values.method,
45 | headers: {
46 | "Content-Type": "application/json",
47 | },
48 | body:
49 | values.method !== "GET" && values.requestBody
50 | ? values.requestBody
51 | : undefined,
52 | });
53 |
54 | const responseBody = await response.text();
55 | const test = {
56 | endpoint: values.endpoint,
57 | method: values.method,
58 | requestBody: values.requestBody,
59 | responseStatus: response.status,
60 | responseBody: responseBody,
61 | };
62 |
63 | return createApiTest(test);
64 | } catch (error) {
65 | throw new Error("Failed to execute API test");
66 | }
67 | },
68 | onSuccess: () => {
69 | queryClient.invalidateQueries({ queryKey: ["/api/tests"] });
70 | toast({
71 | title: "API Test Executed",
72 | description: "The API test has been successfully recorded.",
73 | });
74 | },
75 | });
76 |
77 | return (
78 |
154 |
155 | );
156 | }
157 |
--------------------------------------------------------------------------------
/client/src/components/api-test/response-viewer.tsx:
--------------------------------------------------------------------------------
1 | import { ApiTest } from "@db/schema";
2 | import {
3 | Accordion,
4 | AccordionContent,
5 | AccordionItem,
6 | AccordionTrigger,
7 | } from "@/components/ui/accordion";
8 | import { Badge } from "@/components/ui/badge";
9 | import { ScrollArea } from "@/components/ui/scroll-area";
10 | import { format } from "date-fns";
11 |
12 | interface ResponseViewerProps {
13 | tests: ApiTest[];
14 | }
15 |
16 | const statusColors: Record = {
17 | "2": "bg-green-500",
18 | "3": "bg-blue-500",
19 | "4": "bg-yellow-500",
20 | "5": "bg-red-500",
21 | };
22 |
23 | export function ResponseViewer({ tests }: ResponseViewerProps) {
24 | return (
25 |
26 |
27 | {tests.map((test) => (
28 |
29 |
30 |
31 |
40 | {test.responseStatus || 500}
41 |
42 |
43 | {test.method} {test.endpoint}
44 |
45 |
46 | {format(new Date(test.createdAt), "MMM d, yyyy HH:mm:ss")}
47 |
48 |
49 |
50 |
51 |
52 | {test.requestBody && (
53 |
54 |
Request Body
55 |
56 |
57 | {JSON.stringify(
58 | JSON.parse(test.requestBody),
59 | null,
60 | 2
61 | )}
62 |
63 |
64 |
65 | )}
66 | {test.responseBody && (
67 |
68 |
Response Body
69 |
70 |
71 | {JSON.stringify(
72 | JSON.parse(test.responseBody),
73 | null,
74 | 2
75 | )}
76 |
77 |
78 |
79 | )}
80 |
81 |
82 |
83 | ))}
84 |
85 |
86 | );
87 | }
--------------------------------------------------------------------------------
/client/src/components/tasks/status-chart.tsx:
--------------------------------------------------------------------------------
1 | import { Task } from "@db/schema";
2 | import { PieChart, Pie, Cell, ResponsiveContainer, Legend } from "recharts";
3 | import { useEffect } from "react";
4 | import { taskWebSocket } from "@/lib/websocket";
5 | import { useQueryClient } from "@tanstack/react-query";
6 |
7 | interface StatusChartProps {
8 | tasks: Task[];
9 | }
10 |
11 | const COLORS = {
12 | pending: "#EAB308",
13 | "in-progress": "#3B82F6",
14 | completed: "#22C55E",
15 | cancelled: "#EF4444",
16 | };
17 |
18 | export function StatusChart({ tasks }: StatusChartProps) {
19 | const queryClient = useQueryClient();
20 |
21 | // Subscribe to WebSocket updates
22 | useEffect(() => {
23 | const unsubscribe = taskWebSocket.onMessage(() => {
24 | // Invalidate tasks query to trigger a refresh
25 | queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
26 | });
27 |
28 | return () => unsubscribe();
29 | }, [queryClient]);
30 |
31 | const statusCounts = tasks.reduce(
32 | (acc, task) => {
33 | acc[task.status] = (acc[task.status] || 0) + 1;
34 | return acc;
35 | },
36 | {} as Record
37 | );
38 |
39 | const data = Object.entries(statusCounts).map(([name, value]) => ({
40 | name,
41 | value,
42 | }));
43 |
44 | return (
45 |
46 |
47 |
48 | {
64 | const RADIAN = Math.PI / 180;
65 | const radius = 25 + innerRadius + (outerRadius - innerRadius);
66 | const x = cx + radius * Math.cos(-midAngle * RADIAN);
67 | const y = cy + radius * Math.sin(-midAngle * RADIAN);
68 |
69 | return (
70 | cx ? "start" : "end"}
75 | dominantBaseline="central"
76 | >
77 | {`${value}`}
78 |
79 | );
80 | }}
81 | >
82 | {data.map((entry, index) => (
83 | |
87 | ))}
88 |
89 |
97 |
98 |
99 | );
100 | }
--------------------------------------------------------------------------------
/client/src/components/tasks/task-form.tsx:
--------------------------------------------------------------------------------
1 | import { useForm } from "react-hook-form";
2 | import { Task } from "@db/schema";
3 | import { Button } from "@/components/ui/button";
4 | import { Input } from "@/components/ui/input";
5 | import {
6 | Form,
7 | FormControl,
8 | FormField,
9 | FormItem,
10 | FormLabel,
11 | FormMessage,
12 | } from "@/components/ui/form";
13 | import {
14 | Select,
15 | SelectContent,
16 | SelectItem,
17 | SelectTrigger,
18 | SelectValue,
19 | } from "@/components/ui/select";
20 | import { Textarea } from "@/components/ui/textarea";
21 | import { useMutation, useQueryClient } from "@tanstack/react-query";
22 | import { createTask, updateTask } from "@/lib/api";
23 | import { useToast } from "@/hooks/use-toast";
24 |
25 | interface TaskFormProps {
26 | initialData?: Task;
27 | }
28 |
29 | export function TaskForm({ initialData }: TaskFormProps) {
30 | const queryClient = useQueryClient();
31 | const { toast } = useToast();
32 |
33 | const form = useForm({
34 | defaultValues: initialData || {
35 | title: "",
36 | description: "",
37 | status: "pending",
38 | priority: "medium",
39 | assignedTo: "",
40 | },
41 | });
42 |
43 | const mutation = useMutation({
44 | mutationFn: (data: Partial) => {
45 | if (initialData) {
46 | return updateTask(initialData.id, data);
47 | }
48 | return createTask(data as Omit);
49 | },
50 | onSuccess: () => {
51 | queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
52 | form.reset();
53 | toast({
54 | title: initialData ? "Task updated" : "Task created",
55 | description: `The task has been successfully ${
56 | initialData ? "updated" : "created"
57 | }.`,
58 | });
59 | },
60 | });
61 |
62 | return (
63 |
167 |
168 | );
169 | }
--------------------------------------------------------------------------------
/client/src/components/tasks/task-list.tsx:
--------------------------------------------------------------------------------
1 | import { Task } from "@db/schema";
2 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
3 | import { Badge } from "@/components/ui/badge";
4 | import { Button } from "@/components/ui/button";
5 | import { Trash2, Edit2 } from "lucide-react";
6 | import { useMutation, useQueryClient } from "@tanstack/react-query";
7 | import { deleteTask, updateTask } from "@/lib/api";
8 | import { useToast } from "@/hooks/use-toast";
9 | import {
10 | Dialog,
11 | DialogContent,
12 | DialogHeader,
13 | DialogTitle,
14 | DialogTrigger,
15 | } from "@/components/ui/dialog";
16 | import { TaskForm } from "./task-form";
17 |
18 | interface TaskListProps {
19 | tasks: Task[];
20 | }
21 |
22 | const statusColors = {
23 | pending: "bg-yellow-500",
24 | "in-progress": "bg-blue-500",
25 | completed: "bg-green-500",
26 | cancelled: "bg-red-500",
27 | };
28 |
29 | const priorityColors = {
30 | low: "bg-gray-500",
31 | medium: "bg-orange-500",
32 | high: "bg-red-500",
33 | };
34 |
35 | export function TaskList({ tasks }: TaskListProps) {
36 | const queryClient = useQueryClient();
37 | const { toast } = useToast();
38 |
39 | const deleteMutation = useMutation({
40 | mutationFn: deleteTask,
41 | onSuccess: () => {
42 | queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
43 | toast({
44 | title: "Task deleted",
45 | description: "The task has been successfully deleted.",
46 | });
47 | },
48 | });
49 |
50 | const updateMutation = useMutation({
51 | mutationFn: ({ id, task }: { id: number; task: Partial }) =>
52 | updateTask(id, task),
53 | onSuccess: () => {
54 | queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
55 | toast({
56 | title: "Task updated",
57 | description: "The task has been successfully updated.",
58 | });
59 | },
60 | });
61 |
62 | return (
63 |
64 |
65 |
66 | Title
67 | Status
68 | Priority
69 | Assigned To
70 | Actions
71 |
72 |
73 |
74 | {tasks.map((task) => (
75 |
76 | {task.title}
77 |
78 |
83 | {task.status}
84 |
85 |
86 |
87 |
92 | {task.priority}
93 |
94 |
95 | {task.assignedTo || "Unassigned"}
96 |
97 |
110 |
117 |
118 |
119 | ))}
120 |
121 |
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/client/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
3 | import { ChevronDown } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Accordion = AccordionPrimitive.Root
8 |
9 | const AccordionItem = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
18 | ))
19 | AccordionItem.displayName = "AccordionItem"
20 |
21 | const AccordionTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, children, ...props }, ref) => (
25 |
26 | svg]:rotate-180",
30 | className
31 | )}
32 | {...props}
33 | >
34 | {children}
35 |
36 |
37 |
38 | ))
39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
40 |
41 | const AccordionContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
50 | {children}
51 |
52 | ))
53 |
54 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
55 |
56 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
57 |
--------------------------------------------------------------------------------
/client/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { buttonVariants } from "@/components/ui/button"
6 |
7 | const AlertDialog = AlertDialogPrimitive.Root
8 |
9 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
10 |
11 | const AlertDialogPortal = AlertDialogPrimitive.Portal
12 |
13 | const AlertDialogOverlay = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef
16 | >(({ className, ...props }, ref) => (
17 |
25 | ))
26 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
27 |
28 | const AlertDialogContent = React.forwardRef<
29 | React.ElementRef,
30 | React.ComponentPropsWithoutRef
31 | >(({ className, ...props }, ref) => (
32 |
33 |
34 |
42 |
43 | ))
44 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
45 |
46 | const AlertDialogHeader = ({
47 | className,
48 | ...props
49 | }: React.HTMLAttributes) => (
50 |
57 | )
58 | AlertDialogHeader.displayName = "AlertDialogHeader"
59 |
60 | const AlertDialogFooter = ({
61 | className,
62 | ...props
63 | }: React.HTMLAttributes) => (
64 |
71 | )
72 | AlertDialogFooter.displayName = "AlertDialogFooter"
73 |
74 | const AlertDialogTitle = React.forwardRef<
75 | React.ElementRef,
76 | React.ComponentPropsWithoutRef
77 | >(({ className, ...props }, ref) => (
78 |
83 | ))
84 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
85 |
86 | const AlertDialogDescription = React.forwardRef<
87 | React.ElementRef,
88 | React.ComponentPropsWithoutRef
89 | >(({ className, ...props }, ref) => (
90 |
95 | ))
96 | AlertDialogDescription.displayName =
97 | AlertDialogPrimitive.Description.displayName
98 |
99 | const AlertDialogAction = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
110 |
111 | const AlertDialogCancel = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
124 | ))
125 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
126 |
127 | export {
128 | AlertDialog,
129 | AlertDialogPortal,
130 | AlertDialogOverlay,
131 | AlertDialogTrigger,
132 | AlertDialogContent,
133 | AlertDialogHeader,
134 | AlertDialogFooter,
135 | AlertDialogTitle,
136 | AlertDialogDescription,
137 | AlertDialogAction,
138 | AlertDialogCancel,
139 | }
140 |
--------------------------------------------------------------------------------
/client/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
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 alertVariants = cva(
7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/client/src/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
2 |
3 | const AspectRatio = AspectRatioPrimitive.Root
4 |
5 | export { AspectRatio }
6 |
--------------------------------------------------------------------------------
/client/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Avatar = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 | ))
19 | Avatar.displayName = AvatarPrimitive.Root.displayName
20 |
21 | const AvatarImage = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => (
25 |
30 | ))
31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
32 |
33 | const AvatarFallback = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef
36 | >(({ className, ...props }, ref) => (
37 |
45 | ))
46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
47 |
48 | export { Avatar, AvatarImage, AvatarFallback }
49 |
--------------------------------------------------------------------------------
/client/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
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-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/client/src/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { ChevronRight, MoreHorizontal } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<"nav"> & {
10 | separator?: React.ReactNode
11 | }
12 | >(({ ...props }, ref) => )
13 | Breadcrumb.displayName = "Breadcrumb"
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<"ol">
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | BreadcrumbList.displayName = "BreadcrumbList"
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<"li">
33 | >(({ className, ...props }, ref) => (
34 |
39 | ))
40 | BreadcrumbItem.displayName = "BreadcrumbItem"
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<"a"> & {
45 | asChild?: boolean
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : "a"
49 |
50 | return (
51 |
56 | )
57 | })
58 | BreadcrumbLink.displayName = "BreadcrumbLink"
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<"span">
63 | >(({ className, ...props }, ref) => (
64 |
72 | ))
73 | BreadcrumbPage.displayName = "BreadcrumbPage"
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<"li">) => (
80 | svg]:w-3.5 [&>svg]:h-3.5", className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | )
89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"span">) => (
95 |
101 |
102 | More
103 |
104 | )
105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | }
116 |
--------------------------------------------------------------------------------
/client/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
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 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/client/src/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronLeft, ChevronRight } from "lucide-react"
3 | import { DayPicker } from "react-day-picker"
4 |
5 | import { cn } from "@/lib/utils"
6 | import { buttonVariants } from "@/components/ui/button"
7 |
8 | export type CalendarProps = React.ComponentProps
9 |
10 | function Calendar({
11 | className,
12 | classNames,
13 | showOutsideDays = true,
14 | ...props
15 | }: CalendarProps) {
16 | return (
17 | ,
56 | IconRight: ({ ...props }) => ,
57 | }}
58 | {...props}
59 | />
60 | )
61 | }
62 | Calendar.displayName = "Calendar"
63 |
64 | export { Calendar }
65 |
--------------------------------------------------------------------------------
/client/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/client/src/components/ui/carousel.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import useEmblaCarousel, {
3 | type UseEmblaCarouselType,
4 | } from "embla-carousel-react"
5 | import { ArrowLeft, ArrowRight } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { Button } from "@/components/ui/button"
9 |
10 | type CarouselApi = UseEmblaCarouselType[1]
11 | type UseCarouselParameters = Parameters
12 | type CarouselOptions = UseCarouselParameters[0]
13 | type CarouselPlugin = UseCarouselParameters[1]
14 |
15 | type CarouselProps = {
16 | opts?: CarouselOptions
17 | plugins?: CarouselPlugin
18 | orientation?: "horizontal" | "vertical"
19 | setApi?: (api: CarouselApi) => void
20 | }
21 |
22 | type CarouselContextProps = {
23 | carouselRef: ReturnType[0]
24 | api: ReturnType[1]
25 | scrollPrev: () => void
26 | scrollNext: () => void
27 | canScrollPrev: boolean
28 | canScrollNext: boolean
29 | } & CarouselProps
30 |
31 | const CarouselContext = React.createContext(null)
32 |
33 | function useCarousel() {
34 | const context = React.useContext(CarouselContext)
35 |
36 | if (!context) {
37 | throw new Error("useCarousel must be used within a ")
38 | }
39 |
40 | return context
41 | }
42 |
43 | const Carousel = React.forwardRef<
44 | HTMLDivElement,
45 | React.HTMLAttributes & CarouselProps
46 | >(
47 | (
48 | {
49 | orientation = "horizontal",
50 | opts,
51 | setApi,
52 | plugins,
53 | className,
54 | children,
55 | ...props
56 | },
57 | ref
58 | ) => {
59 | const [carouselRef, api] = useEmblaCarousel(
60 | {
61 | ...opts,
62 | axis: orientation === "horizontal" ? "x" : "y",
63 | },
64 | plugins
65 | )
66 | const [canScrollPrev, setCanScrollPrev] = React.useState(false)
67 | const [canScrollNext, setCanScrollNext] = React.useState(false)
68 |
69 | const onSelect = React.useCallback((api: CarouselApi) => {
70 | if (!api) {
71 | return
72 | }
73 |
74 | setCanScrollPrev(api.canScrollPrev())
75 | setCanScrollNext(api.canScrollNext())
76 | }, [])
77 |
78 | const scrollPrev = React.useCallback(() => {
79 | api?.scrollPrev()
80 | }, [api])
81 |
82 | const scrollNext = React.useCallback(() => {
83 | api?.scrollNext()
84 | }, [api])
85 |
86 | const handleKeyDown = React.useCallback(
87 | (event: React.KeyboardEvent) => {
88 | if (event.key === "ArrowLeft") {
89 | event.preventDefault()
90 | scrollPrev()
91 | } else if (event.key === "ArrowRight") {
92 | event.preventDefault()
93 | scrollNext()
94 | }
95 | },
96 | [scrollPrev, scrollNext]
97 | )
98 |
99 | React.useEffect(() => {
100 | if (!api || !setApi) {
101 | return
102 | }
103 |
104 | setApi(api)
105 | }, [api, setApi])
106 |
107 | React.useEffect(() => {
108 | if (!api) {
109 | return
110 | }
111 |
112 | onSelect(api)
113 | api.on("reInit", onSelect)
114 | api.on("select", onSelect)
115 |
116 | return () => {
117 | api?.off("select", onSelect)
118 | }
119 | }, [api, onSelect])
120 |
121 | return (
122 |
135 |
143 | {children}
144 |
145 |
146 | )
147 | }
148 | )
149 | Carousel.displayName = "Carousel"
150 |
151 | const CarouselContent = React.forwardRef<
152 | HTMLDivElement,
153 | React.HTMLAttributes
154 | >(({ className, ...props }, ref) => {
155 | const { carouselRef, orientation } = useCarousel()
156 |
157 | return (
158 |
169 | )
170 | })
171 | CarouselContent.displayName = "CarouselContent"
172 |
173 | const CarouselItem = React.forwardRef<
174 | HTMLDivElement,
175 | React.HTMLAttributes
176 | >(({ className, ...props }, ref) => {
177 | const { orientation } = useCarousel()
178 |
179 | return (
180 |
191 | )
192 | })
193 | CarouselItem.displayName = "CarouselItem"
194 |
195 | const CarouselPrevious = React.forwardRef<
196 | HTMLButtonElement,
197 | React.ComponentProps
198 | >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
199 | const { orientation, scrollPrev, canScrollPrev } = useCarousel()
200 |
201 | return (
202 |
220 | )
221 | })
222 | CarouselPrevious.displayName = "CarouselPrevious"
223 |
224 | const CarouselNext = React.forwardRef<
225 | HTMLButtonElement,
226 | React.ComponentProps
227 | >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
228 | const { orientation, scrollNext, canScrollNext } = useCarousel()
229 |
230 | return (
231 |
249 | )
250 | })
251 | CarouselNext.displayName = "CarouselNext"
252 |
253 | export {
254 | type CarouselApi,
255 | Carousel,
256 | CarouselContent,
257 | CarouselItem,
258 | CarouselPrevious,
259 | CarouselNext,
260 | }
261 |
--------------------------------------------------------------------------------
/client/src/components/ui/chart.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as RechartsPrimitive from "recharts"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | // Format: { THEME_NAME: CSS_SELECTOR }
7 | const THEMES = { light: "", dark: ".dark" } as const
8 |
9 | export type ChartConfig = {
10 | [k in string]: {
11 | label?: React.ReactNode
12 | icon?: React.ComponentType
13 | } & (
14 | | { color?: string; theme?: never }
15 | | { color?: never; theme: Record }
16 | )
17 | }
18 |
19 | type ChartContextProps = {
20 | config: ChartConfig
21 | }
22 |
23 | const ChartContext = React.createContext(null)
24 |
25 | function useChart() {
26 | const context = React.useContext(ChartContext)
27 |
28 | if (!context) {
29 | throw new Error("useChart must be used within a ")
30 | }
31 |
32 | return context
33 | }
34 |
35 | const ChartContainer = React.forwardRef<
36 | HTMLDivElement,
37 | React.ComponentProps<"div"> & {
38 | config: ChartConfig
39 | children: React.ComponentProps<
40 | typeof RechartsPrimitive.ResponsiveContainer
41 | >["children"]
42 | }
43 | >(({ id, className, children, config, ...props }, ref) => {
44 | const uniqueId = React.useId()
45 | const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
46 |
47 | return (
48 |
49 |
58 |
59 |
60 | {children}
61 |
62 |
63 |
64 | )
65 | })
66 | ChartContainer.displayName = "Chart"
67 |
68 | const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
69 | const colorConfig = Object.entries(config).filter(
70 | ([_, config]) => config.theme || config.color
71 | )
72 |
73 | if (!colorConfig.length) {
74 | return null
75 | }
76 |
77 | return (
78 |