├── .gitignore ├── generated-icon.png ├── postcss.config.js ├── theme.json ├── client ├── src │ ├── components │ │ ├── ui │ │ │ ├── aspect-ratio.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── label.tsx │ │ │ ├── textarea.tsx │ │ │ ├── separator.tsx │ │ │ ├── progress.tsx │ │ │ ├── toaster.tsx │ │ │ ├── input.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── slider.tsx │ │ │ ├── switch.tsx │ │ │ ├── tooltip.tsx │ │ │ ├── badge.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── popover.tsx │ │ │ ├── avatar.tsx │ │ │ ├── toggle.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── alert.tsx │ │ │ ├── resizable.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── tabs.tsx │ │ │ ├── button.tsx │ │ │ ├── accordion.tsx │ │ │ ├── card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── calendar.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── pagination.tsx │ │ │ ├── table.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dialog.tsx │ │ │ ├── sheet.tsx │ │ │ ├── form.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── toast.tsx │ │ │ ├── command.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── select.tsx │ │ │ ├── carousel.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── menubar.tsx │ │ │ └── chart.tsx │ │ ├── tasks │ │ │ ├── status-chart.tsx │ │ │ ├── task-list.tsx │ │ │ └── task-form.tsx │ │ └── api-test │ │ │ ├── response-viewer.tsx │ │ │ └── request-builder.tsx │ ├── lib │ │ ├── utils.ts │ │ ├── queryClient.ts │ │ ├── websocket.ts │ │ └── api.ts │ ├── index.css │ ├── main.tsx │ ├── hooks │ │ ├── use-mobile.tsx │ │ └── use-toast.ts │ ├── pages │ │ ├── not-found.tsx │ │ ├── test-dashboard.tsx │ │ ├── dashboard.tsx │ │ └── metrics-dashboard.tsx │ └── App.tsx └── index.html ├── drizzle.config.ts ├── db ├── index.ts └── schema.ts ├── tsconfig.json ├── vite.config.ts ├── .replit ├── LICENSE ├── server ├── index.ts ├── vite.ts └── routes.ts ├── tailwind.config.ts ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | server/public 5 | vite.config.ts.* 6 | *.tar.gz -------------------------------------------------------------------------------- /generated-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoheinakajima/pippin-tasks/HEAD/generated-icon.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "variant": "professional", 3 | "primary": "hsl(222.2 47.4% 11.2%)", 4 | "appearance": "light", 5 | "radius": 0.5 6 | } 7 | -------------------------------------------------------------------------------- /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/lib/utils.ts: -------------------------------------------------------------------------------- 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 | } 7 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | * { 7 | @apply border-border; 8 | } 9 | 10 | body { 11 | @apply font-sans antialiased bg-background text-foreground; 12 | } 13 | } -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /client/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /client/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 2 | 3 | const Collapsible = CollapsiblePrimitive.Root 4 | 5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 6 | 7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 8 | 9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 10 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | 3 | if (!process.env.DATABASE_URL) { 4 | throw new Error("DATABASE_URL, ensure the database is provisioned"); 5 | } 6 | 7 | export default defineConfig({ 8 | out: "./migrations", 9 | schema: "./db/schema.ts", 10 | dialect: "postgresql", 11 | dbCredentials: { 12 | url: process.env.DATABASE_URL, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/neon-serverless"; 2 | import ws from "ws"; 3 | import * as schema from "@db/schema"; 4 | 5 | if (!process.env.DATABASE_URL) { 6 | throw new Error( 7 | "DATABASE_URL must be set. Did you forget to provision a database?", 8 | ); 9 | } 10 | 11 | export const db = drizzle({ 12 | connection: process.env.DATABASE_URL, 13 | schema, 14 | ws: ws, 15 | }); 16 | -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { QueryClientProvider } from "@tanstack/react-query"; 4 | import { queryClient } from "./lib/queryClient"; 5 | import App from './App'; 6 | import "./index.css"; 7 | 8 | const root = document.getElementById("root"); 9 | if (!root) { 10 | throw new Error("Root element not found"); 11 | } 12 | 13 | createRoot(root).render( 14 | 15 | 16 | 17 | 18 | 19 | ); -------------------------------------------------------------------------------- /client/src/hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["client/src/**/*", "db/**/*", "server/**/*"], 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 | "moduleResolution": "bundler", 16 | "baseUrl": ".", 17 | "types": ["node", "vite/client"], 18 | "paths": { 19 | "@db": ["./db/index.ts"], 20 | "@db/*": ["./db/*"], 21 | "@/*": ["./client/src/*"] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/src/pages/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent } from "@/components/ui/card"; 2 | import { AlertCircle } from "lucide-react"; 3 | 4 | export default function NotFound() { 5 | return ( 6 |
7 | 8 | 9 |
10 | 11 |

404 Page Not Found

12 |
13 | 14 |

15 | Did you forget to add the page to the router? 16 |

17 |
18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /client/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /client/src/lib/queryClient.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from "@tanstack/react-query"; 2 | 3 | export const queryClient = new QueryClient({ 4 | defaultOptions: { 5 | queries: { 6 | queryFn: async ({ queryKey }) => { 7 | const res = await fetch(queryKey[0] as string, { 8 | credentials: "include", 9 | }); 10 | 11 | if (!res.ok) { 12 | if (res.status >= 500) { 13 | throw new Error(`${res.status}: ${res.statusText}`); 14 | } 15 | 16 | throw new Error(`${res.status}: ${await res.text()}`); 17 | } 18 | 19 | return res.json(); 20 | }, 21 | refetchInterval: false, 22 | refetchOnWindowFocus: false, 23 | staleTime: Infinity, 24 | retry: false, 25 | }, 26 | mutations: { 27 | retry: false, 28 | } 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import themePlugin from "@replit/vite-plugin-shadcn-theme-json"; 4 | import path, { dirname } from "path"; 5 | import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal"; 6 | import { fileURLToPath } from "url"; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = dirname(__filename); 10 | export default defineConfig({ 11 | plugins: [react(), runtimeErrorOverlay(), themePlugin()], 12 | resolve: { 13 | alias: { 14 | "@db": path.resolve(__dirname, "db"), 15 | "@": path.resolve(__dirname, "client", "src"), 16 | }, 17 | }, 18 | root: path.resolve(__dirname, "client"), 19 | build: { 20 | outDir: path.resolve(__dirname, "dist/public"), 21 | emptyOutDir: true, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |