From aca76666d90bafdd59147f45329639f828239a57 Mon Sep 17 00:00:00 2001 From: SylvainP1 <5533467-SylvainP1@users.noreply.replit.com> Date: Tue, 14 Apr 2026 07:58:20 +0000 Subject: [PATCH] Add core features for timesheet and project management Implement API endpoints and frontend components for creating and managing timesheets, projects, and dashboard functionalities. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 55837015-10e9-4be9-b857-7f5e6be73772 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: e5763354-5d83-482b-a89e-394e3eb5a41e Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1cc377db-7ea0-49f2-97ce-c3e87e0228cc/55837015-10e9-4be9-b857-7f5e6be73772/JpyvMwJ Replit-Helium-Checkpoint-Created: true --- .replit | 17 +- artifacts/api-server/src/routes/dashboard.ts | 120 ++ artifacts/api-server/src/routes/index.ts | 6 + artifacts/api-server/src/routes/projects.ts | 103 ++ artifacts/api-server/src/routes/timesheets.ts | 327 ++++ .../cra-app/.replit-artifact/artifact.toml | 31 + artifacts/cra-app/components.json | 20 + artifacts/cra-app/index.html | 16 + artifacts/cra-app/package.json | 77 + artifacts/cra-app/public/favicon.svg | 3 + artifacts/cra-app/public/opengraph.jpg | Bin 0 -> 66034 bytes artifacts/cra-app/src/App.tsx | 21 + .../cra-app/src/components/layout/layout.tsx | 14 + .../cra-app/src/components/layout/sidebar.tsx | 69 + .../cra-app/src/components/ui/accordion.tsx | 55 + .../src/components/ui/alert-dialog.tsx | 139 ++ artifacts/cra-app/src/components/ui/alert.tsx | 59 + .../src/components/ui/aspect-ratio.tsx | 5 + .../cra-app/src/components/ui/avatar.tsx | 50 + artifacts/cra-app/src/components/ui/badge.tsx | 43 + .../cra-app/src/components/ui/breadcrumb.tsx | 115 ++ .../src/components/ui/button-group.tsx | 83 + .../cra-app/src/components/ui/button.tsx | 65 + .../cra-app/src/components/ui/calendar.tsx | 213 +++ artifacts/cra-app/src/components/ui/card.tsx | 76 + .../cra-app/src/components/ui/carousel.tsx | 260 +++ artifacts/cra-app/src/components/ui/chart.tsx | 367 ++++ .../cra-app/src/components/ui/checkbox.tsx | 28 + .../cra-app/src/components/ui/collapsible.tsx | 11 + .../cra-app/src/components/ui/command.tsx | 153 ++ .../src/components/ui/context-menu.tsx | 198 +++ .../cra-app/src/components/ui/dialog.tsx | 120 ++ .../cra-app/src/components/ui/drawer.tsx | 116 ++ .../src/components/ui/dropdown-menu.tsx | 201 +++ artifacts/cra-app/src/components/ui/empty.tsx | 104 ++ artifacts/cra-app/src/components/ui/field.tsx | 244 +++ artifacts/cra-app/src/components/ui/form.tsx | 176 ++ .../cra-app/src/components/ui/hover-card.tsx | 27 + .../cra-app/src/components/ui/input-group.tsx | 168 ++ .../cra-app/src/components/ui/input-otp.tsx | 69 + artifacts/cra-app/src/components/ui/input.tsx | 22 + artifacts/cra-app/src/components/ui/item.tsx | 193 +++ artifacts/cra-app/src/components/ui/kbd.tsx | 28 + artifacts/cra-app/src/components/ui/label.tsx | 26 + .../cra-app/src/components/ui/menubar.tsx | 254 +++ .../src/components/ui/navigation-menu.tsx | 128 ++ .../cra-app/src/components/ui/pagination.tsx | 117 ++ .../cra-app/src/components/ui/popover.tsx | 31 + .../cra-app/src/components/ui/progress.tsx | 28 + .../cra-app/src/components/ui/radio-group.tsx | 42 + .../cra-app/src/components/ui/resizable.tsx | 45 + .../cra-app/src/components/ui/scroll-area.tsx | 46 + .../cra-app/src/components/ui/select.tsx | 159 ++ .../cra-app/src/components/ui/separator.tsx | 29 + artifacts/cra-app/src/components/ui/sheet.tsx | 140 ++ .../cra-app/src/components/ui/sidebar.tsx | 727 ++++++++ .../cra-app/src/components/ui/skeleton.tsx | 15 + .../cra-app/src/components/ui/slider.tsx | 26 + .../cra-app/src/components/ui/sonner.tsx | 31 + .../cra-app/src/components/ui/spinner.tsx | 16 + .../cra-app/src/components/ui/switch.tsx | 27 + artifacts/cra-app/src/components/ui/table.tsx | 120 ++ artifacts/cra-app/src/components/ui/tabs.tsx | 53 + .../cra-app/src/components/ui/textarea.tsx | 22 + artifacts/cra-app/src/components/ui/toast.tsx | 127 ++ .../cra-app/src/components/ui/toaster.tsx | 33 + .../src/components/ui/toggle-group.tsx | 61 + .../cra-app/src/components/ui/toggle.tsx | 43 + .../cra-app/src/components/ui/tooltip.tsx | 32 + artifacts/cra-app/src/hooks/use-mobile.tsx | 19 + artifacts/cra-app/src/hooks/use-toast.ts | 191 +++ artifacts/cra-app/src/index.css | 176 ++ artifacts/cra-app/src/lib/utils.ts | 44 + artifacts/cra-app/src/main.tsx | 20 + artifacts/cra-app/src/pages/dashboard.tsx | 196 +++ artifacts/cra-app/src/pages/not-found.tsx | 21 + artifacts/cra-app/src/pages/projects.tsx | 309 ++++ .../cra-app/src/pages/timesheet-detail.tsx | 440 +++++ artifacts/cra-app/src/pages/timesheets.tsx | 242 +++ artifacts/cra-app/tsconfig.json | 22 + artifacts/cra-app/vite.config.ts | 75 + attached_assets/image_1776152995497.png | Bin 0 -> 154260 bytes .../src/generated/api.schemas.ts | 187 +- lib/api-client-react/src/generated/api.ts | 1527 ++++++++++++++++- lib/api-spec/openapi.yaml | 663 ++++++- lib/api-zod/src/generated/api.ts | 289 +++- .../src/generated/types/createProjectBody.ts | 18 + .../generated/types/createTimesheetBody.ts | 13 + .../types/createTimesheetLineBody.ts | 11 + .../src/generated/types/dashboardSummary.ts | 15 + .../types/getDashboardSummaryParams.ts | 11 + .../generated/types/getMonthlyHoursParams.ts | 11 + .../generated/types/getProjectHoursParams.ts | 12 + .../src/generated/types/healthStatus.ts | 2 +- lib/api-zod/src/generated/types/index.ts | 25 +- .../generated/types/listTimesheetsParams.ts | 12 + .../src/generated/types/monthlyHours.ts | 13 + lib/api-zod/src/generated/types/project.ts | 21 + .../src/generated/types/projectHours.ts | 14 + lib/api-zod/src/generated/types/timeEntry.ts | 14 + lib/api-zod/src/generated/types/timesheet.ts | 19 + .../src/generated/types/timesheetDetail.ts | 21 + .../generated/types/timesheetDetailStatus.ts | 16 + .../src/generated/types/timesheetLine.ts | 20 + .../types/timesheetLineWithEntries.ts | 22 + .../src/generated/types/timesheetStatus.ts | 16 + .../src/generated/types/updateProjectBody.ts | 19 + .../generated/types/updateTimesheetBody.ts | 13 + .../types/updateTimesheetBodyStatus.ts | 16 + .../generated/types/upsertTimeEntriesBody.ts | 12 + .../types/upsertTimeEntriesBodyEntriesItem.ts | 13 + lib/api-zod/src/index.ts | 1 - lib/db/src/schema/index.ts | 24 +- lib/db/src/schema/projects.ts | 18 + lib/db/src/schema/timeEntries.ts | 15 + lib/db/src/schema/timesheetLines.ts | 16 + lib/db/src/schema/timesheets.ts | 18 + pnpm-lock.yaml | 266 +++ replit.md | 35 +- 119 files changed, 11750 insertions(+), 33 deletions(-) create mode 100644 artifacts/api-server/src/routes/dashboard.ts create mode 100644 artifacts/api-server/src/routes/projects.ts create mode 100644 artifacts/api-server/src/routes/timesheets.ts create mode 100644 artifacts/cra-app/.replit-artifact/artifact.toml create mode 100644 artifacts/cra-app/components.json create mode 100644 artifacts/cra-app/index.html create mode 100644 artifacts/cra-app/package.json create mode 100644 artifacts/cra-app/public/favicon.svg create mode 100644 artifacts/cra-app/public/opengraph.jpg create mode 100644 artifacts/cra-app/src/App.tsx create mode 100644 artifacts/cra-app/src/components/layout/layout.tsx create mode 100644 artifacts/cra-app/src/components/layout/sidebar.tsx create mode 100644 artifacts/cra-app/src/components/ui/accordion.tsx create mode 100644 artifacts/cra-app/src/components/ui/alert-dialog.tsx create mode 100644 artifacts/cra-app/src/components/ui/alert.tsx create mode 100644 artifacts/cra-app/src/components/ui/aspect-ratio.tsx create mode 100644 artifacts/cra-app/src/components/ui/avatar.tsx create mode 100644 artifacts/cra-app/src/components/ui/badge.tsx create mode 100644 artifacts/cra-app/src/components/ui/breadcrumb.tsx create mode 100644 artifacts/cra-app/src/components/ui/button-group.tsx create mode 100644 artifacts/cra-app/src/components/ui/button.tsx create mode 100644 artifacts/cra-app/src/components/ui/calendar.tsx create mode 100644 artifacts/cra-app/src/components/ui/card.tsx create mode 100644 artifacts/cra-app/src/components/ui/carousel.tsx create mode 100644 artifacts/cra-app/src/components/ui/chart.tsx create mode 100644 artifacts/cra-app/src/components/ui/checkbox.tsx create mode 100644 artifacts/cra-app/src/components/ui/collapsible.tsx create mode 100644 artifacts/cra-app/src/components/ui/command.tsx create mode 100644 artifacts/cra-app/src/components/ui/context-menu.tsx create mode 100644 artifacts/cra-app/src/components/ui/dialog.tsx create mode 100644 artifacts/cra-app/src/components/ui/drawer.tsx create mode 100644 artifacts/cra-app/src/components/ui/dropdown-menu.tsx create mode 100644 artifacts/cra-app/src/components/ui/empty.tsx create mode 100644 artifacts/cra-app/src/components/ui/field.tsx create mode 100644 artifacts/cra-app/src/components/ui/form.tsx create mode 100644 artifacts/cra-app/src/components/ui/hover-card.tsx create mode 100644 artifacts/cra-app/src/components/ui/input-group.tsx create mode 100644 artifacts/cra-app/src/components/ui/input-otp.tsx create mode 100644 artifacts/cra-app/src/components/ui/input.tsx create mode 100644 artifacts/cra-app/src/components/ui/item.tsx create mode 100644 artifacts/cra-app/src/components/ui/kbd.tsx create mode 100644 artifacts/cra-app/src/components/ui/label.tsx create mode 100644 artifacts/cra-app/src/components/ui/menubar.tsx create mode 100644 artifacts/cra-app/src/components/ui/navigation-menu.tsx create mode 100644 artifacts/cra-app/src/components/ui/pagination.tsx create mode 100644 artifacts/cra-app/src/components/ui/popover.tsx create mode 100644 artifacts/cra-app/src/components/ui/progress.tsx create mode 100644 artifacts/cra-app/src/components/ui/radio-group.tsx create mode 100644 artifacts/cra-app/src/components/ui/resizable.tsx create mode 100644 artifacts/cra-app/src/components/ui/scroll-area.tsx create mode 100644 artifacts/cra-app/src/components/ui/select.tsx create mode 100644 artifacts/cra-app/src/components/ui/separator.tsx create mode 100644 artifacts/cra-app/src/components/ui/sheet.tsx create mode 100644 artifacts/cra-app/src/components/ui/sidebar.tsx create mode 100644 artifacts/cra-app/src/components/ui/skeleton.tsx create mode 100644 artifacts/cra-app/src/components/ui/slider.tsx create mode 100644 artifacts/cra-app/src/components/ui/sonner.tsx create mode 100644 artifacts/cra-app/src/components/ui/spinner.tsx create mode 100644 artifacts/cra-app/src/components/ui/switch.tsx create mode 100644 artifacts/cra-app/src/components/ui/table.tsx create mode 100644 artifacts/cra-app/src/components/ui/tabs.tsx create mode 100644 artifacts/cra-app/src/components/ui/textarea.tsx create mode 100644 artifacts/cra-app/src/components/ui/toast.tsx create mode 100644 artifacts/cra-app/src/components/ui/toaster.tsx create mode 100644 artifacts/cra-app/src/components/ui/toggle-group.tsx create mode 100644 artifacts/cra-app/src/components/ui/toggle.tsx create mode 100644 artifacts/cra-app/src/components/ui/tooltip.tsx create mode 100644 artifacts/cra-app/src/hooks/use-mobile.tsx create mode 100644 artifacts/cra-app/src/hooks/use-toast.ts create mode 100644 artifacts/cra-app/src/index.css create mode 100644 artifacts/cra-app/src/lib/utils.ts create mode 100644 artifacts/cra-app/src/main.tsx create mode 100644 artifacts/cra-app/src/pages/dashboard.tsx create mode 100644 artifacts/cra-app/src/pages/not-found.tsx create mode 100644 artifacts/cra-app/src/pages/projects.tsx create mode 100644 artifacts/cra-app/src/pages/timesheet-detail.tsx create mode 100644 artifacts/cra-app/src/pages/timesheets.tsx create mode 100644 artifacts/cra-app/tsconfig.json create mode 100644 artifacts/cra-app/vite.config.ts create mode 100644 attached_assets/image_1776152995497.png create mode 100644 lib/api-zod/src/generated/types/createProjectBody.ts create mode 100644 lib/api-zod/src/generated/types/createTimesheetBody.ts create mode 100644 lib/api-zod/src/generated/types/createTimesheetLineBody.ts create mode 100644 lib/api-zod/src/generated/types/dashboardSummary.ts create mode 100644 lib/api-zod/src/generated/types/getDashboardSummaryParams.ts create mode 100644 lib/api-zod/src/generated/types/getMonthlyHoursParams.ts create mode 100644 lib/api-zod/src/generated/types/getProjectHoursParams.ts create mode 100644 lib/api-zod/src/generated/types/listTimesheetsParams.ts create mode 100644 lib/api-zod/src/generated/types/monthlyHours.ts create mode 100644 lib/api-zod/src/generated/types/project.ts create mode 100644 lib/api-zod/src/generated/types/projectHours.ts create mode 100644 lib/api-zod/src/generated/types/timeEntry.ts create mode 100644 lib/api-zod/src/generated/types/timesheet.ts create mode 100644 lib/api-zod/src/generated/types/timesheetDetail.ts create mode 100644 lib/api-zod/src/generated/types/timesheetDetailStatus.ts create mode 100644 lib/api-zod/src/generated/types/timesheetLine.ts create mode 100644 lib/api-zod/src/generated/types/timesheetLineWithEntries.ts create mode 100644 lib/api-zod/src/generated/types/timesheetStatus.ts create mode 100644 lib/api-zod/src/generated/types/updateProjectBody.ts create mode 100644 lib/api-zod/src/generated/types/updateTimesheetBody.ts create mode 100644 lib/api-zod/src/generated/types/updateTimesheetBodyStatus.ts create mode 100644 lib/api-zod/src/generated/types/upsertTimeEntriesBody.ts create mode 100644 lib/api-zod/src/generated/types/upsertTimeEntriesBodyEntriesItem.ts create mode 100644 lib/db/src/schema/projects.ts create mode 100644 lib/db/src/schema/timeEntries.ts create mode 100644 lib/db/src/schema/timesheetLines.ts create mode 100644 lib/db/src/schema/timesheets.ts diff --git a/.replit b/.replit index 82bac8f..8bec5c0 100644 --- a/.replit +++ b/.replit @@ -1,4 +1,4 @@ -modules = ["nodejs-24"] +modules = ["nodejs-24", "postgresql-16"] [deployment] router = "application" @@ -18,3 +18,18 @@ expertMode = true [postMerge] path = "scripts/post-merge.sh" timeoutMs = 20000 + +[[ports]] +localPort = 8080 +externalPort = 8080 + +[[ports]] +localPort = 8081 +externalPort = 80 + +[[ports]] +localPort = 24212 +externalPort = 3000 + +[nix] +channel = "stable-25_05" diff --git a/artifacts/api-server/src/routes/dashboard.ts b/artifacts/api-server/src/routes/dashboard.ts new file mode 100644 index 0000000..5f3d165 --- /dev/null +++ b/artifacts/api-server/src/routes/dashboard.ts @@ -0,0 +1,120 @@ +import { Router, type IRouter } from "express"; +import { eq, sql, and } from "drizzle-orm"; +import { db, timesheetsTable, timesheetLinesTable, timeEntriesTable, projectsTable } from "@workspace/db"; +import { + GetDashboardSummaryQueryParams, + GetDashboardSummaryResponse, + GetMonthlyHoursQueryParams, + GetMonthlyHoursResponse, + GetProjectHoursQueryParams, + GetProjectHoursResponse, +} from "@workspace/api-zod"; + +const router: IRouter = Router(); + +const MONTH_LABELS = [ + "", "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", + "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre", +]; + +router.get("/dashboard/summary", async (req, res): Promise => { + const query = GetDashboardSummaryQueryParams.safeParse(req.query); + if (!query.success) { + res.status(400).json({ error: query.error.message }); + return; + } + + const year = query.data.year ?? new Date().getFullYear(); + const currentMonth = new Date().getMonth() + 1; + + const allTimesheets = await db + .select() + .from(timesheetsTable) + .where(eq(timesheetsTable.year, year)); + + const currentMonthSheet = allTimesheets.find((t) => t.month === currentMonth); + const totalHoursThisMonth = currentMonthSheet?.totalHours ?? 0; + const totalHoursThisYear = allTimesheets.reduce((sum, t) => sum + t.totalHours, 0); + + const activeProjects = await db + .select({ count: sql`count(*)` }) + .from(projectsTable) + .where(eq(projectsTable.isActive, true)); + + const pendingTimesheets = allTimesheets.filter( + (t) => t.status === "draft" || t.status === "submitted" + ).length; + const validatedTimesheets = allTimesheets.filter( + (t) => t.status === "validated" + ).length; + + res.json( + GetDashboardSummaryResponse.parse({ + totalHoursThisMonth, + totalHoursThisYear, + activeProjects: Number(activeProjects[0]?.count ?? 0), + pendingTimesheets, + validatedTimesheets, + }) + ); +}); + +router.get("/dashboard/monthly-hours", async (req, res): Promise => { + const query = GetMonthlyHoursQueryParams.safeParse(req.query); + if (!query.success) { + res.status(400).json({ error: query.error.message }); + return; + } + + const year = query.data.year ?? new Date().getFullYear(); + + const timesheets = await db + .select() + .from(timesheetsTable) + .where(eq(timesheetsTable.year, year)) + .orderBy(timesheetsTable.month); + + const monthlyData = Array.from({ length: 12 }, (_, i) => { + const month = i + 1; + const ts = timesheets.find((t) => t.month === month); + return { + month, + totalHours: ts?.totalHours ?? 0, + label: MONTH_LABELS[month], + }; + }); + + res.json(GetMonthlyHoursResponse.parse(monthlyData)); +}); + +router.get("/dashboard/project-hours", async (req, res): Promise => { + const query = GetProjectHoursQueryParams.safeParse(req.query); + if (!query.success) { + res.status(400).json({ error: query.error.message }); + return; + } + + const year = query.data.year ?? new Date().getFullYear(); + + const conditions = [eq(timesheetsTable.year, year)]; + if (query.data.month) { + conditions.push(eq(timesheetsTable.month, query.data.month)); + } + + const result = await db + .select({ + projectId: projectsTable.id, + projectCode: projectsTable.code, + projectName: projectsTable.name, + totalHours: sql`COALESCE(SUM(${timesheetLinesTable.totalHours}), 0)`, + }) + .from(timesheetLinesTable) + .innerJoin(projectsTable, eq(timesheetLinesTable.projectId, projectsTable.id)) + .innerJoin(timesheetsTable, eq(timesheetLinesTable.timesheetId, timesheetsTable.id)) + .where(and(...conditions)) + .groupBy(projectsTable.id, projectsTable.code, projectsTable.name); + + res.json(GetProjectHoursResponse.parse(result)); +}); + +export default router; diff --git a/artifacts/api-server/src/routes/index.ts b/artifacts/api-server/src/routes/index.ts index 5a1f77a..3dd2068 100644 --- a/artifacts/api-server/src/routes/index.ts +++ b/artifacts/api-server/src/routes/index.ts @@ -1,8 +1,14 @@ import { Router, type IRouter } from "express"; import healthRouter from "./health"; +import projectsRouter from "./projects"; +import timesheetsRouter from "./timesheets"; +import dashboardRouter from "./dashboard"; const router: IRouter = Router(); router.use(healthRouter); +router.use(projectsRouter); +router.use(timesheetsRouter); +router.use(dashboardRouter); export default router; diff --git a/artifacts/api-server/src/routes/projects.ts b/artifacts/api-server/src/routes/projects.ts new file mode 100644 index 0000000..8640cb3 --- /dev/null +++ b/artifacts/api-server/src/routes/projects.ts @@ -0,0 +1,103 @@ +import { Router, type IRouter } from "express"; +import { eq } from "drizzle-orm"; +import { db, projectsTable } from "@workspace/db"; +import { + CreateProjectBody, + GetProjectParams, + GetProjectResponse, + UpdateProjectParams, + UpdateProjectBody, + UpdateProjectResponse, + DeleteProjectParams, + ListProjectsResponse, +} from "@workspace/api-zod"; + +const router: IRouter = Router(); + +router.get("/projects", async (_req, res): Promise => { + const projects = await db + .select() + .from(projectsTable) + .orderBy(projectsTable.code); + res.json(ListProjectsResponse.parse(projects)); +}); + +router.post("/projects", async (req, res): Promise => { + const parsed = CreateProjectBody.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + const [project] = await db.insert(projectsTable).values(parsed.data).returning(); + res.status(201).json(GetProjectResponse.parse(project)); +}); + +router.get("/projects/:id", async (req, res): Promise => { + const params = GetProjectParams.safeParse(req.params); + if (!params.success) { + res.status(400).json({ error: params.error.message }); + return; + } + + const [project] = await db + .select() + .from(projectsTable) + .where(eq(projectsTable.id, params.data.id)); + + if (!project) { + res.status(404).json({ error: "Project not found" }); + return; + } + + res.json(GetProjectResponse.parse(project)); +}); + +router.patch("/projects/:id", async (req, res): Promise => { + const params = UpdateProjectParams.safeParse(req.params); + if (!params.success) { + res.status(400).json({ error: params.error.message }); + return; + } + + const parsed = UpdateProjectBody.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + const [project] = await db + .update(projectsTable) + .set(parsed.data) + .where(eq(projectsTable.id, params.data.id)) + .returning(); + + if (!project) { + res.status(404).json({ error: "Project not found" }); + return; + } + + res.json(UpdateProjectResponse.parse(project)); +}); + +router.delete("/projects/:id", async (req, res): Promise => { + const params = DeleteProjectParams.safeParse(req.params); + if (!params.success) { + res.status(400).json({ error: params.error.message }); + return; + } + + const [project] = await db + .delete(projectsTable) + .where(eq(projectsTable.id, params.data.id)) + .returning(); + + if (!project) { + res.status(404).json({ error: "Project not found" }); + return; + } + + res.sendStatus(204); +}); + +export default router; diff --git a/artifacts/api-server/src/routes/timesheets.ts b/artifacts/api-server/src/routes/timesheets.ts new file mode 100644 index 0000000..2275159 --- /dev/null +++ b/artifacts/api-server/src/routes/timesheets.ts @@ -0,0 +1,327 @@ +import { Router, type IRouter } from "express"; +import { eq, and, sql } from "drizzle-orm"; +import { db, timesheetsTable, timesheetLinesTable, timeEntriesTable, projectsTable } from "@workspace/db"; +import { + CreateTimesheetBody, + GetTimesheetParams, + GetTimesheetResponse, + UpdateTimesheetParams, + UpdateTimesheetBody, + UpdateTimesheetResponse, + DeleteTimesheetParams, + ListTimesheetsQueryParams, + ListTimesheetsResponse, + ListTimesheetLinesParams, + ListTimesheetLinesResponse, + CreateTimesheetLineParams, + CreateTimesheetLineBody, + DeleteTimesheetLineParams, + UpsertTimeEntriesParams, + UpsertTimeEntriesBody, + UpsertTimeEntriesResponse, +} from "@workspace/api-zod"; + +const router: IRouter = Router(); + +router.get("/timesheets", async (req, res): Promise => { + const query = ListTimesheetsQueryParams.safeParse(req.query); + if (!query.success) { + res.status(400).json({ error: query.error.message }); + return; + } + + const conditions = []; + if (query.data.year) { + conditions.push(eq(timesheetsTable.year, query.data.year)); + } + if (query.data.month) { + conditions.push(eq(timesheetsTable.month, query.data.month)); + } + + const timesheets = await db + .select() + .from(timesheetsTable) + .where(conditions.length > 0 ? and(...conditions) : undefined) + .orderBy(timesheetsTable.year, timesheetsTable.month); + + res.json(ListTimesheetsResponse.parse(timesheets)); +}); + +router.post("/timesheets", async (req, res): Promise => { + const parsed = CreateTimesheetBody.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + const [timesheet] = await db.insert(timesheetsTable).values(parsed.data).returning(); + res.status(201).json(GetTimesheetResponse.parse({ ...timesheet, lines: [] })); +}); + +router.get("/timesheets/:id", async (req, res): Promise => { + const params = GetTimesheetParams.safeParse(req.params); + if (!params.success) { + res.status(400).json({ error: params.error.message }); + return; + } + + const [timesheet] = await db + .select() + .from(timesheetsTable) + .where(eq(timesheetsTable.id, params.data.id)); + + if (!timesheet) { + res.status(404).json({ error: "Timesheet not found" }); + return; + } + + const lines = await db + .select({ + id: timesheetLinesTable.id, + timesheetId: timesheetLinesTable.timesheetId, + projectId: timesheetLinesTable.projectId, + totalHours: timesheetLinesTable.totalHours, + projectCode: projectsTable.code, + projectName: projectsTable.name, + client: projectsTable.client, + category: projectsTable.category, + }) + .from(timesheetLinesTable) + .innerJoin(projectsTable, eq(timesheetLinesTable.projectId, projectsTable.id)) + .where(eq(timesheetLinesTable.timesheetId, params.data.id)); + + const linesWithEntries = await Promise.all( + lines.map(async (line) => { + const entries = await db + .select() + .from(timeEntriesTable) + .where(eq(timeEntriesTable.timesheetLineId, line.id)); + return { ...line, entries }; + }) + ); + + res.json(GetTimesheetResponse.parse({ ...timesheet, lines: linesWithEntries })); +}); + +router.patch("/timesheets/:id", async (req, res): Promise => { + const params = UpdateTimesheetParams.safeParse(req.params); + if (!params.success) { + res.status(400).json({ error: params.error.message }); + return; + } + + const parsed = UpdateTimesheetBody.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + const [timesheet] = await db + .update(timesheetsTable) + .set({ ...parsed.data, updatedAt: new Date() }) + .where(eq(timesheetsTable.id, params.data.id)) + .returning(); + + if (!timesheet) { + res.status(404).json({ error: "Timesheet not found" }); + return; + } + + res.json(UpdateTimesheetResponse.parse(timesheet)); +}); + +router.delete("/timesheets/:id", async (req, res): Promise => { + const params = DeleteTimesheetParams.safeParse(req.params); + if (!params.success) { + res.status(400).json({ error: params.error.message }); + return; + } + + const [timesheet] = await db + .delete(timesheetsTable) + .where(eq(timesheetsTable.id, params.data.id)) + .returning(); + + if (!timesheet) { + res.status(404).json({ error: "Timesheet not found" }); + return; + } + + res.sendStatus(204); +}); + +router.get("/timesheets/:timesheetId/lines", async (req, res): Promise => { + const params = ListTimesheetLinesParams.safeParse(req.params); + if (!params.success) { + res.status(400).json({ error: params.error.message }); + return; + } + + const lines = await db + .select({ + id: timesheetLinesTable.id, + timesheetId: timesheetLinesTable.timesheetId, + projectId: timesheetLinesTable.projectId, + totalHours: timesheetLinesTable.totalHours, + projectCode: projectsTable.code, + projectName: projectsTable.name, + client: projectsTable.client, + category: projectsTable.category, + }) + .from(timesheetLinesTable) + .innerJoin(projectsTable, eq(timesheetLinesTable.projectId, projectsTable.id)) + .where(eq(timesheetLinesTable.timesheetId, params.data.timesheetId)); + + res.json(ListTimesheetLinesResponse.parse(lines)); +}); + +router.post("/timesheets/:timesheetId/lines", async (req, res): Promise => { + const params = CreateTimesheetLineParams.safeParse(req.params); + if (!params.success) { + res.status(400).json({ error: params.error.message }); + return; + } + + const parsed = CreateTimesheetLineBody.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + const [project] = await db + .select() + .from(projectsTable) + .where(eq(projectsTable.id, parsed.data.projectId)); + + if (!project) { + res.status(404).json({ error: "Project not found" }); + return; + } + + const [line] = await db + .insert(timesheetLinesTable) + .values({ + timesheetId: params.data.timesheetId, + projectId: parsed.data.projectId, + }) + .returning(); + + res.status(201).json({ + ...line, + projectCode: project.code, + projectName: project.name, + client: project.client, + category: project.category, + }); +}); + +router.delete("/timesheets/:timesheetId/lines/:lineId", async (req, res): Promise => { + const params = DeleteTimesheetLineParams.safeParse(req.params); + if (!params.success) { + res.status(400).json({ error: params.error.message }); + return; + } + + const [line] = await db + .delete(timesheetLinesTable) + .where( + and( + eq(timesheetLinesTable.id, params.data.lineId), + eq(timesheetLinesTable.timesheetId, params.data.timesheetId) + ) + ) + .returning(); + + if (!line) { + res.status(404).json({ error: "Line not found" }); + return; + } + + await recalculateTimesheetTotal(params.data.timesheetId); + res.sendStatus(204); +}); + +router.put("/timesheets/:timesheetId/entries", async (req, res): Promise => { + const params = UpsertTimeEntriesParams.safeParse(req.params); + if (!params.success) { + res.status(400).json({ error: params.error.message }); + return; + } + + const parsed = UpsertTimeEntriesBody.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + const results = []; + for (const entry of parsed.data.entries) { + const dateStr = typeof entry.date === "string" ? entry.date : (entry.date as Date).toISOString().split("T")[0]; + const existing = await db + .select() + .from(timeEntriesTable) + .where( + and( + eq(timeEntriesTable.timesheetLineId, entry.timesheetLineId), + eq(timeEntriesTable.date, dateStr) + ) + ); + + if (existing.length > 0) { + if (entry.hours === 0) { + await db + .delete(timeEntriesTable) + .where(eq(timeEntriesTable.id, existing[0].id)); + results.push({ ...existing[0], hours: 0 }); + } else { + const [updated] = await db + .update(timeEntriesTable) + .set({ hours: entry.hours }) + .where(eq(timeEntriesTable.id, existing[0].id)) + .returning(); + results.push(updated); + } + } else if (entry.hours > 0) { + const [created] = await db + .insert(timeEntriesTable) + .values({ + timesheetLineId: entry.timesheetLineId, + date: dateStr, + hours: entry.hours, + }) + .returning(); + results.push(created); + } + } + + const affectedLineIds = [...new Set(parsed.data.entries.map((e) => e.timesheetLineId))]; + for (const lineId of affectedLineIds) { + const entries = await db + .select() + .from(timeEntriesTable) + .where(eq(timeEntriesTable.timesheetLineId, lineId)); + const total = entries.reduce((sum, e) => sum + e.hours, 0); + await db + .update(timesheetLinesTable) + .set({ totalHours: total }) + .where(eq(timesheetLinesTable.id, lineId)); + } + + await recalculateTimesheetTotal(params.data.timesheetId); + + res.json(UpsertTimeEntriesResponse.parse(results.filter((r) => r.hours > 0))); +}); + +async function recalculateTimesheetTotal(timesheetId: number) { + const lines = await db + .select() + .from(timesheetLinesTable) + .where(eq(timesheetLinesTable.timesheetId, timesheetId)); + const total = lines.reduce((sum, l) => sum + l.totalHours, 0); + await db + .update(timesheetsTable) + .set({ totalHours: total, updatedAt: new Date() }) + .where(eq(timesheetsTable.id, timesheetId)); +} + +export default router; diff --git a/artifacts/cra-app/.replit-artifact/artifact.toml b/artifacts/cra-app/.replit-artifact/artifact.toml new file mode 100644 index 0000000..96d60bd --- /dev/null +++ b/artifacts/cra-app/.replit-artifact/artifact.toml @@ -0,0 +1,31 @@ +kind = "web" +previewPath = "/" +title = "CRA - Compte Rendu d'Activité" +version = "1.0.0" +id = "artifacts/cra-app" +router = "path" + +[[integratedSkills]] +name = "react-vite" +version = "1.0.0" + +[[services]] +name = "web" +paths = [ "/" ] +localPort = 24212 + +[services.development] +run = "pnpm --filter @workspace/cra-app run dev" + +[services.production] +build = [ "pnpm", "--filter", "@workspace/cra-app", "run", "build" ] +publicDir = "artifacts/cra-app/dist/public" +serve = "static" + +[[services.production.rewrites]] +from = "/*" +to = "/index.html" + +[services.env] +PORT = "24212" +BASE_PATH = "/" diff --git a/artifacts/cra-app/components.json b/artifacts/cra-app/components.json new file mode 100644 index 0000000..3ff62cf --- /dev/null +++ b/artifacts/cra-app/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} \ No newline at end of file diff --git a/artifacts/cra-app/index.html b/artifacts/cra-app/index.html new file mode 100644 index 0000000..1575459 --- /dev/null +++ b/artifacts/cra-app/index.html @@ -0,0 +1,16 @@ + + + + + + CRA - Compte Rendu d'Activité + + + + + + +
+ + + diff --git a/artifacts/cra-app/package.json b/artifacts/cra-app/package.json new file mode 100644 index 0000000..8a62114 --- /dev/null +++ b/artifacts/cra-app/package.json @@ -0,0 +1,77 @@ +{ + "name": "@workspace/cra-app", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --config vite.config.ts --host 0.0.0.0", + "build": "vite build --config vite.config.ts", + "serve": "vite preview --config vite.config.ts --host 0.0.0.0", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "devDependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "^1.2.4", + "@radix-ui/react-alert-dialog": "^1.1.7", + "@radix-ui/react-aspect-ratio": "^1.1.3", + "@radix-ui/react-avatar": "^1.1.4", + "@radix-ui/react-checkbox": "^1.1.5", + "@radix-ui/react-collapsible": "^1.1.4", + "@radix-ui/react-context-menu": "^2.2.7", + "@radix-ui/react-dialog": "^1.1.7", + "@radix-ui/react-dropdown-menu": "^2.1.7", + "@radix-ui/react-hover-card": "^1.1.7", + "@radix-ui/react-label": "^2.1.3", + "@radix-ui/react-menubar": "^1.1.7", + "@radix-ui/react-navigation-menu": "^1.2.6", + "@radix-ui/react-popover": "^1.1.7", + "@radix-ui/react-progress": "^1.1.3", + "@radix-ui/react-radio-group": "^1.2.4", + "@radix-ui/react-scroll-area": "^1.2.4", + "@radix-ui/react-select": "^2.1.7", + "@radix-ui/react-separator": "^1.1.3", + "@radix-ui/react-slider": "^1.2.4", + "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-switch": "^1.1.4", + "@radix-ui/react-tabs": "^1.1.4", + "@radix-ui/react-toast": "^1.2.7", + "@radix-ui/react-toggle": "^1.1.3", + "@radix-ui/react-toggle-group": "^1.1.3", + "@radix-ui/react-tooltip": "^1.2.0", + "@replit/vite-plugin-cartographer": "catalog:", + "@replit/vite-plugin-dev-banner": "catalog:", + "@replit/vite-plugin-runtime-error-modal": "catalog:", + "@tailwindcss/typography": "^0.5.15", + "@tailwindcss/vite": "catalog:", + "@tanstack/react-query": "catalog:", + "@types/node": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitejs/plugin-react": "catalog:", + "@workspace/api-client-react": "workspace:*", + "class-variance-authority": "catalog:", + "clsx": "catalog:", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.6.0", + "framer-motion": "catalog:", + "input-otp": "^1.4.2", + "lucide-react": "catalog:", + "next-themes": "^0.4.6", + "react": "catalog:", + "react-day-picker": "^9.11.1", + "react-dom": "catalog:", + "react-hook-form": "^7.55.0", + "react-icons": "^5.4.0", + "react-resizable-panels": "^2.1.7", + "recharts": "^2.15.2", + "sonner": "^2.0.7", + "tailwind-merge": "catalog:", + "tailwindcss": "catalog:", + "tw-animate-css": "^1.4.0", + "vaul": "^1.1.2", + "vite": "catalog:", + "wouter": "^3.3.5", + "zod": "catalog:" + } +} diff --git a/artifacts/cra-app/public/favicon.svg b/artifacts/cra-app/public/favicon.svg new file mode 100644 index 0000000..4373d3c --- /dev/null +++ b/artifacts/cra-app/public/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/artifacts/cra-app/public/opengraph.jpg b/artifacts/cra-app/public/opengraph.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b60f32a62aa20edef15946505c9dd9bcc954d4a GIT binary patch literal 66034 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 fFF9E6ufS)(B^8UmvsK + + + + + + + + + ); +} diff --git a/artifacts/cra-app/src/components/layout/layout.tsx b/artifacts/cra-app/src/components/layout/layout.tsx new file mode 100644 index 0000000..31f2c2e --- /dev/null +++ b/artifacts/cra-app/src/components/layout/layout.tsx @@ -0,0 +1,14 @@ +import { AppSidebar } from "./sidebar"; + +export function AppLayout({ children }: { children: React.ReactNode }) { + return ( +

+ +
+
+ {children} +
+
+
+ ); +} diff --git a/artifacts/cra-app/src/components/layout/sidebar.tsx b/artifacts/cra-app/src/components/layout/sidebar.tsx new file mode 100644 index 0000000..21c4484 --- /dev/null +++ b/artifacts/cra-app/src/components/layout/sidebar.tsx @@ -0,0 +1,69 @@ +import { Link, useLocation } from "wouter"; +import { LayoutDashboard, Clock, FolderKanban, Settings } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const navItems = [ + { + title: "Tableau de bord", + href: "/", + icon: LayoutDashboard, + }, + { + title: "Feuilles de temps", + href: "/timesheets", + icon: Clock, + }, + { + title: "Projets", + href: "/projects", + icon: FolderKanban, + }, +]; + +export function AppSidebar() { + const [location] = useLocation(); + + return ( +
+
+
+ +
+

CRA Manager

+
+ + + +
+
+
+ JD +
+
+ Jean Dupont + Consultant +
+
+
+
+ ); +} diff --git a/artifacts/cra-app/src/components/ui/accordion.tsx b/artifacts/cra-app/src/components/ui/accordion.tsx new file mode 100644 index 0000000..e1797c9 --- /dev/null +++ b/artifacts/cra-app/src/components/ui/accordion.tsx @@ -0,0 +1,55 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/artifacts/cra-app/src/components/ui/alert-dialog.tsx b/artifacts/cra-app/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..fa2b442 --- /dev/null +++ b/artifacts/cra-app/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/artifacts/cra-app/src/components/ui/alert.tsx b/artifacts/cra-app/src/components/ui/alert.tsx new file mode 100644 index 0000000..5afd41d --- /dev/null +++ b/artifacts/cra-app/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/artifacts/cra-app/src/components/ui/aspect-ratio.tsx b/artifacts/cra-app/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..c4abbf3 --- /dev/null +++ b/artifacts/cra-app/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/artifacts/cra-app/src/components/ui/avatar.tsx b/artifacts/cra-app/src/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/artifacts/cra-app/src/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/artifacts/cra-app/src/components/ui/badge.tsx b/artifacts/cra-app/src/components/ui/badge.tsx new file mode 100644 index 0000000..3f03665 --- /dev/null +++ b/artifacts/cra-app/src/components/ui/badge.tsx @@ -0,0 +1,43 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + // @replit + // Whitespace-nowrap: Badges should never wrap. + "whitespace-nowrap 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" + + " hover-elevate ", + { + variants: { + variant: { + default: + // @replit shadow-xs instead of shadow, no hover because we use hover-elevate + "border-transparent bg-primary text-primary-foreground shadow-xs", + secondary: + // @replit no hover because we use hover-elevate + "border-transparent bg-secondary text-secondary-foreground", + destructive: + // @replit shadow-xs instead of shadow, no hover because we use hover-elevate + "border-transparent bg-destructive text-destructive-foreground shadow-xs", + // @replit shadow-xs" - use badge outline variable + outline: "text-foreground border [border-color:var(--badge-outline)]", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/artifacts/cra-app/src/components/ui/breadcrumb.tsx b/artifacts/cra-app/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..60e6c96 --- /dev/null +++ b/artifacts/cra-app/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>