From 036973ea32073d70bb7463393c1f888c13a26e89 Mon Sep 17 00:00:00 2001 From: cheffe01 <32869042-cheffe01@users.noreply.replit.com> Date: Mon, 25 May 2026 13:04:53 +0000 Subject: [PATCH] Add API endpoints and frontend components for tool management and analytics Implement CRUD operations for tools and ratings, introduce analytics endpoints, and develop frontend components for displaying tools, ratings, and analytics data. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 776963d0-f75d-42e2-a57b-cc36bdff8495 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: feaa4ce1-5aed-4cc0-bcea-47855b615b48 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0683fb79-a27c-485c-9333-5f4b288c4567/776963d0-f75d-42e2-a57b-cc36bdff8495/rx9K7bW Replit-Helium-Checkpoint-Created: true --- .replit | 17 +- artifacts/api-server/src/routes/analytics.ts | 181 +++ artifacts/api-server/src/routes/index.ts | 6 + artifacts/api-server/src/routes/ratings.ts | 64 + artifacts/api-server/src/routes/tools.ts | 173 +++ .../toolrate/.replit-artifact/artifact.toml | 31 + artifacts/toolrate/components.json | 20 + artifacts/toolrate/index.html | 24 + artifacts/toolrate/package.json | 77 ++ artifacts/toolrate/public/favicon.svg | 3 + artifacts/toolrate/public/opengraph.jpg | Bin 0 -> 79184 bytes artifacts/toolrate/public/robots.txt | 2 + artifacts/toolrate/src/App.tsx | 49 + artifacts/toolrate/src/components/layout.tsx | 49 + .../toolrate/src/components/rating-stars.tsx | 41 + .../toolrate/src/components/tool-card.tsx | 51 + .../toolrate/src/components/ui/accordion.tsx | 55 + .../src/components/ui/alert-dialog.tsx | 139 +++ .../toolrate/src/components/ui/alert.tsx | 59 + .../src/components/ui/aspect-ratio.tsx | 5 + .../toolrate/src/components/ui/avatar.tsx | 50 + .../toolrate/src/components/ui/badge.tsx | 43 + .../toolrate/src/components/ui/breadcrumb.tsx | 115 ++ .../src/components/ui/button-group.tsx | 83 ++ .../toolrate/src/components/ui/button.tsx | 65 + .../toolrate/src/components/ui/calendar.tsx | 213 ++++ artifacts/toolrate/src/components/ui/card.tsx | 76 ++ .../toolrate/src/components/ui/carousel.tsx | 260 ++++ .../toolrate/src/components/ui/chart.tsx | 367 ++++++ .../toolrate/src/components/ui/checkbox.tsx | 28 + .../src/components/ui/collapsible.tsx | 11 + .../toolrate/src/components/ui/command.tsx | 153 +++ .../src/components/ui/context-menu.tsx | 198 +++ .../toolrate/src/components/ui/dialog.tsx | 120 ++ .../toolrate/src/components/ui/drawer.tsx | 116 ++ .../src/components/ui/dropdown-menu.tsx | 201 +++ .../toolrate/src/components/ui/empty.tsx | 104 ++ .../toolrate/src/components/ui/field.tsx | 244 ++++ artifacts/toolrate/src/components/ui/form.tsx | 176 +++ .../toolrate/src/components/ui/hover-card.tsx | 27 + .../src/components/ui/input-group.tsx | 168 +++ .../toolrate/src/components/ui/input-otp.tsx | 69 ++ .../toolrate/src/components/ui/input.tsx | 22 + artifacts/toolrate/src/components/ui/item.tsx | 193 +++ artifacts/toolrate/src/components/ui/kbd.tsx | 28 + .../toolrate/src/components/ui/label.tsx | 26 + .../toolrate/src/components/ui/menubar.tsx | 254 ++++ .../src/components/ui/navigation-menu.tsx | 128 ++ .../toolrate/src/components/ui/pagination.tsx | 117 ++ .../toolrate/src/components/ui/popover.tsx | 31 + .../toolrate/src/components/ui/progress.tsx | 28 + .../src/components/ui/radio-group.tsx | 42 + .../toolrate/src/components/ui/resizable.tsx | 45 + .../src/components/ui/scroll-area.tsx | 46 + .../toolrate/src/components/ui/select.tsx | 159 +++ .../toolrate/src/components/ui/separator.tsx | 29 + .../toolrate/src/components/ui/sheet.tsx | 140 +++ .../toolrate/src/components/ui/sidebar.tsx | 727 +++++++++++ .../toolrate/src/components/ui/skeleton.tsx | 15 + .../toolrate/src/components/ui/slider.tsx | 26 + .../toolrate/src/components/ui/sonner.tsx | 31 + .../toolrate/src/components/ui/spinner.tsx | 16 + .../toolrate/src/components/ui/switch.tsx | 27 + .../toolrate/src/components/ui/table.tsx | 120 ++ artifacts/toolrate/src/components/ui/tabs.tsx | 53 + .../toolrate/src/components/ui/textarea.tsx | 22 + .../toolrate/src/components/ui/toast.tsx | 127 ++ .../toolrate/src/components/ui/toaster.tsx | 33 + .../src/components/ui/toggle-group.tsx | 61 + .../toolrate/src/components/ui/toggle.tsx | 43 + .../toolrate/src/components/ui/tooltip.tsx | 32 + artifacts/toolrate/src/hooks/use-mobile.tsx | 19 + artifacts/toolrate/src/hooks/use-toast.ts | 191 +++ artifacts/toolrate/src/index.css | 356 ++++++ artifacts/toolrate/src/lib/utils.ts | 6 + artifacts/toolrate/src/main.tsx | 5 + artifacts/toolrate/src/pages/analytics.tsx | 201 +++ artifacts/toolrate/src/pages/home.tsx | 161 +++ artifacts/toolrate/src/pages/not-found.tsx | 21 + artifacts/toolrate/src/pages/tool-detail.tsx | 411 +++++++ artifacts/toolrate/src/pages/tool-new.tsx | 273 +++++ artifacts/toolrate/src/pages/tools-browse.tsx | 148 +++ artifacts/toolrate/tsconfig.json | 22 + artifacts/toolrate/vite.config.ts | 75 ++ .../src/generated/api.schemas.ts | 171 ++- lib/api-client-react/src/generated/api.ts | 1074 ++++++++++++++++- lib/api-spec/openapi.yaml | 507 +++++++- lib/api-zod/src/generated/api.ts | 265 +++- .../src/generated/types/analyticsSummary.ts | 21 + .../src/generated/types/categoryStats.ts | 17 + .../src/generated/types/errorResponse.ts | 11 + .../types/getRatingDistributionParams.ts | 11 + .../src/generated/types/getTopToolsMetric.ts | 16 + .../src/generated/types/getTopToolsParams.ts | 13 + .../src/generated/types/healthStatus.ts | 4 +- lib/api-zod/src/generated/types/index.ts | 23 +- .../src/generated/types/listToolsParams.ts | 14 + .../src/generated/types/listToolsSort.ts | 16 + lib/api-zod/src/generated/types/rating.ts | 27 + .../src/generated/types/ratingDistribution.ts | 13 + .../src/generated/types/ratingInput.ts | 22 + .../src/generated/types/scoreBucket.ts | 12 + lib/api-zod/src/generated/types/tool.ts | 20 + lib/api-zod/src/generated/types/toolInput.ts | 19 + lib/api-zod/src/generated/types/toolUpdate.ts | 17 + .../src/generated/types/toolWithStats.ts | 27 + .../src/generated/types/topToolEntry.ts | 14 + lib/db/src/schema/index.ts | 22 +- lib/db/src/schema/ratings.ts | 18 + lib/db/src/schema/tools.ts | 19 + pnpm-lock.yaml | 266 ++++ 111 files changed, 11006 insertions(+), 96 deletions(-) create mode 100644 artifacts/api-server/src/routes/analytics.ts create mode 100644 artifacts/api-server/src/routes/ratings.ts create mode 100644 artifacts/api-server/src/routes/tools.ts create mode 100644 artifacts/toolrate/.replit-artifact/artifact.toml create mode 100644 artifacts/toolrate/components.json create mode 100644 artifacts/toolrate/index.html create mode 100644 artifacts/toolrate/package.json create mode 100644 artifacts/toolrate/public/favicon.svg create mode 100644 artifacts/toolrate/public/opengraph.jpg create mode 100644 artifacts/toolrate/public/robots.txt create mode 100644 artifacts/toolrate/src/App.tsx create mode 100644 artifacts/toolrate/src/components/layout.tsx create mode 100644 artifacts/toolrate/src/components/rating-stars.tsx create mode 100644 artifacts/toolrate/src/components/tool-card.tsx create mode 100644 artifacts/toolrate/src/components/ui/accordion.tsx create mode 100644 artifacts/toolrate/src/components/ui/alert-dialog.tsx create mode 100644 artifacts/toolrate/src/components/ui/alert.tsx create mode 100644 artifacts/toolrate/src/components/ui/aspect-ratio.tsx create mode 100644 artifacts/toolrate/src/components/ui/avatar.tsx create mode 100644 artifacts/toolrate/src/components/ui/badge.tsx create mode 100644 artifacts/toolrate/src/components/ui/breadcrumb.tsx create mode 100644 artifacts/toolrate/src/components/ui/button-group.tsx create mode 100644 artifacts/toolrate/src/components/ui/button.tsx create mode 100644 artifacts/toolrate/src/components/ui/calendar.tsx create mode 100644 artifacts/toolrate/src/components/ui/card.tsx create mode 100644 artifacts/toolrate/src/components/ui/carousel.tsx create mode 100644 artifacts/toolrate/src/components/ui/chart.tsx create mode 100644 artifacts/toolrate/src/components/ui/checkbox.tsx create mode 100644 artifacts/toolrate/src/components/ui/collapsible.tsx create mode 100644 artifacts/toolrate/src/components/ui/command.tsx create mode 100644 artifacts/toolrate/src/components/ui/context-menu.tsx create mode 100644 artifacts/toolrate/src/components/ui/dialog.tsx create mode 100644 artifacts/toolrate/src/components/ui/drawer.tsx create mode 100644 artifacts/toolrate/src/components/ui/dropdown-menu.tsx create mode 100644 artifacts/toolrate/src/components/ui/empty.tsx create mode 100644 artifacts/toolrate/src/components/ui/field.tsx create mode 100644 artifacts/toolrate/src/components/ui/form.tsx create mode 100644 artifacts/toolrate/src/components/ui/hover-card.tsx create mode 100644 artifacts/toolrate/src/components/ui/input-group.tsx create mode 100644 artifacts/toolrate/src/components/ui/input-otp.tsx create mode 100644 artifacts/toolrate/src/components/ui/input.tsx create mode 100644 artifacts/toolrate/src/components/ui/item.tsx create mode 100644 artifacts/toolrate/src/components/ui/kbd.tsx create mode 100644 artifacts/toolrate/src/components/ui/label.tsx create mode 100644 artifacts/toolrate/src/components/ui/menubar.tsx create mode 100644 artifacts/toolrate/src/components/ui/navigation-menu.tsx create mode 100644 artifacts/toolrate/src/components/ui/pagination.tsx create mode 100644 artifacts/toolrate/src/components/ui/popover.tsx create mode 100644 artifacts/toolrate/src/components/ui/progress.tsx create mode 100644 artifacts/toolrate/src/components/ui/radio-group.tsx create mode 100644 artifacts/toolrate/src/components/ui/resizable.tsx create mode 100644 artifacts/toolrate/src/components/ui/scroll-area.tsx create mode 100644 artifacts/toolrate/src/components/ui/select.tsx create mode 100644 artifacts/toolrate/src/components/ui/separator.tsx create mode 100644 artifacts/toolrate/src/components/ui/sheet.tsx create mode 100644 artifacts/toolrate/src/components/ui/sidebar.tsx create mode 100644 artifacts/toolrate/src/components/ui/skeleton.tsx create mode 100644 artifacts/toolrate/src/components/ui/slider.tsx create mode 100644 artifacts/toolrate/src/components/ui/sonner.tsx create mode 100644 artifacts/toolrate/src/components/ui/spinner.tsx create mode 100644 artifacts/toolrate/src/components/ui/switch.tsx create mode 100644 artifacts/toolrate/src/components/ui/table.tsx create mode 100644 artifacts/toolrate/src/components/ui/tabs.tsx create mode 100644 artifacts/toolrate/src/components/ui/textarea.tsx create mode 100644 artifacts/toolrate/src/components/ui/toast.tsx create mode 100644 artifacts/toolrate/src/components/ui/toaster.tsx create mode 100644 artifacts/toolrate/src/components/ui/toggle-group.tsx create mode 100644 artifacts/toolrate/src/components/ui/toggle.tsx create mode 100644 artifacts/toolrate/src/components/ui/tooltip.tsx create mode 100644 artifacts/toolrate/src/hooks/use-mobile.tsx create mode 100644 artifacts/toolrate/src/hooks/use-toast.ts create mode 100644 artifacts/toolrate/src/index.css create mode 100644 artifacts/toolrate/src/lib/utils.ts create mode 100644 artifacts/toolrate/src/main.tsx create mode 100644 artifacts/toolrate/src/pages/analytics.tsx create mode 100644 artifacts/toolrate/src/pages/home.tsx create mode 100644 artifacts/toolrate/src/pages/not-found.tsx create mode 100644 artifacts/toolrate/src/pages/tool-detail.tsx create mode 100644 artifacts/toolrate/src/pages/tool-new.tsx create mode 100644 artifacts/toolrate/src/pages/tools-browse.tsx create mode 100644 artifacts/toolrate/tsconfig.json create mode 100644 artifacts/toolrate/vite.config.ts create mode 100644 lib/api-zod/src/generated/types/analyticsSummary.ts create mode 100644 lib/api-zod/src/generated/types/categoryStats.ts create mode 100644 lib/api-zod/src/generated/types/errorResponse.ts create mode 100644 lib/api-zod/src/generated/types/getRatingDistributionParams.ts create mode 100644 lib/api-zod/src/generated/types/getTopToolsMetric.ts create mode 100644 lib/api-zod/src/generated/types/getTopToolsParams.ts create mode 100644 lib/api-zod/src/generated/types/listToolsParams.ts create mode 100644 lib/api-zod/src/generated/types/listToolsSort.ts create mode 100644 lib/api-zod/src/generated/types/rating.ts create mode 100644 lib/api-zod/src/generated/types/ratingDistribution.ts create mode 100644 lib/api-zod/src/generated/types/ratingInput.ts create mode 100644 lib/api-zod/src/generated/types/scoreBucket.ts create mode 100644 lib/api-zod/src/generated/types/tool.ts create mode 100644 lib/api-zod/src/generated/types/toolInput.ts create mode 100644 lib/api-zod/src/generated/types/toolUpdate.ts create mode 100644 lib/api-zod/src/generated/types/toolWithStats.ts create mode 100644 lib/api-zod/src/generated/types/topToolEntry.ts create mode 100644 lib/db/src/schema/ratings.ts create mode 100644 lib/db/src/schema/tools.ts diff --git a/.replit b/.replit index 82bac8f..6281b48 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 = 80 + +[[ports]] +localPort = 8081 +externalPort = 8081 + +[[ports]] +localPort = 26015 +externalPort = 3000 + +[nix] +channel = "stable-25_05" diff --git a/artifacts/api-server/src/routes/analytics.ts b/artifacts/api-server/src/routes/analytics.ts new file mode 100644 index 0000000..447c149 --- /dev/null +++ b/artifacts/api-server/src/routes/analytics.ts @@ -0,0 +1,181 @@ +import { Router, type IRouter } from "express"; +import { eq, sql, desc } from "drizzle-orm"; +import { db, toolsTable, ratingsTable } from "@workspace/db"; +import { + GetTopToolsQueryParams, + GetRatingDistributionQueryParams, +} from "@workspace/api-zod"; + +const router: IRouter = Router(); + +router.get("/analytics/summary", async (_req, res): Promise => { + const [toolStats] = await db + .select({ totalTools: sql`count(*)::int` }) + .from(toolsTable); + + const [ratingStats] = await db + .select({ + totalRatings: sql`count(*)::int`, + avgUsefulness: sql`avg(${ratingsTable.usefulness})`, + avgUsability: sql`avg(${ratingsTable.usability})`, + }) + .from(ratingsTable); + + const [catStats] = await db + .select({ categoriesCount: sql`count(distinct ${toolsTable.category})::int` }) + .from(toolsTable); + + const avgCombined = ratingStats.avgUsefulness != null && ratingStats.avgUsability != null + ? (Number(ratingStats.avgUsefulness) + Number(ratingStats.avgUsability)) / 2 + : null; + + // Most-rated tool + const [mostRatedRow] = await db + .select({ + toolId: ratingsTable.toolId, + ratingCount: sql`count(*)::int`, + }) + .from(ratingsTable) + .groupBy(ratingsTable.toolId) + .orderBy(desc(sql`count(*)`)) + .limit(1); + + let mostRatedTool = null; + if (mostRatedRow) { + const [tool] = await db.select().from(toolsTable).where(eq(toolsTable.id, mostRatedRow.toolId)); + if (tool) { + const ratingRows = await db + .select({ usefulness: ratingsTable.usefulness, usability: ratingsTable.usability }) + .from(ratingsTable) + .where(eq(ratingsTable.toolId, tool.id)); + const rc = ratingRows.length; + const au = rc > 0 ? ratingRows.reduce((s, r) => s + r.usefulness, 0) / rc : null; + const aus = rc > 0 ? ratingRows.reduce((s, r) => s + r.usability, 0) / rc : null; + const ac = au != null && aus != null ? (au + aus) / 2 : null; + mostRatedTool = { ...tool, ratingCount: rc, avgUsefulness: au, avgUsability: aus, avgCombined: ac }; + } + } + + res.json({ + totalTools: toolStats?.totalTools ?? 0, + totalRatings: ratingStats?.totalRatings ?? 0, + avgUsefulness: ratingStats?.avgUsefulness != null ? Number(ratingStats.avgUsefulness) : null, + avgUsability: ratingStats?.avgUsability != null ? Number(ratingStats.avgUsability) : null, + avgCombined, + categoriesCount: catStats?.categoriesCount ?? 0, + mostRatedTool, + }); +}); + +router.get("/analytics/top-tools", async (req, res): Promise => { + const parsed = GetTopToolsQueryParams.safeParse(req.query); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + const limit = parsed.data.limit ?? 10; + const metric = parsed.data.metric ?? "combined"; + + const tools = await db.select().from(toolsTable); + const allRatings = await db + .select({ toolId: ratingsTable.toolId, usefulness: ratingsTable.usefulness, usability: ratingsTable.usability }) + .from(ratingsTable); + + const ratingsByTool = new Map(); + for (const r of allRatings) { + const arr = ratingsByTool.get(r.toolId) ?? []; + arr.push({ usefulness: r.usefulness, usability: r.usability }); + ratingsByTool.set(r.toolId, arr); + } + + const toolsWithStats = tools.map((t) => { + const ratings = ratingsByTool.get(t.id) ?? []; + const rc = ratings.length; + const au = rc > 0 ? ratings.reduce((s, r) => s + r.usefulness, 0) / rc : null; + const aus = rc > 0 ? ratings.reduce((s, r) => s + r.usability, 0) / rc : null; + const ac = au != null && aus != null ? (au + aus) / 2 : null; + return { ...t, ratingCount: rc, avgUsefulness: au, avgUsability: aus, avgCombined: ac }; + }); + + const scored = toolsWithStats + .filter((t) => t.ratingCount > 0) + .map((t) => { + let score = 0; + if (metric === "usefulness") score = t.avgUsefulness ?? 0; + else if (metric === "usability") score = t.avgUsability ?? 0; + else score = t.avgCombined ?? 0; + return { tool: t, score, ratingCount: t.ratingCount }; + }) + .sort((a, b) => b.score - a.score) + .slice(0, limit); + + res.json(scored); +}); + +router.get("/analytics/by-category", async (_req, res): Promise => { + const categories = await db + .selectDistinct({ category: toolsTable.category }) + .from(toolsTable); + + const result = await Promise.all( + categories.map(async ({ category }) => { + const [toolCount] = await db + .select({ count: sql`count(*)::int` }) + .from(toolsTable) + .where(eq(toolsTable.category, category)); + + const [ratingStats] = await db + .select({ + totalRatings: sql`count(*)::int`, + avgUsefulness: sql`avg(${ratingsTable.usefulness})`, + avgUsability: sql`avg(${ratingsTable.usability})`, + }) + .from(ratingsTable) + .innerJoin(toolsTable, eq(ratingsTable.toolId, toolsTable.id)) + .where(eq(toolsTable.category, category)); + + return { + category, + toolCount: toolCount?.count ?? 0, + totalRatings: ratingStats?.totalRatings ?? 0, + avgUsefulness: ratingStats?.avgUsefulness != null ? Number(ratingStats.avgUsefulness) : null, + avgUsability: ratingStats?.avgUsability != null ? Number(ratingStats.avgUsability) : null, + }; + }) + ); + + res.json(result); +}); + +router.get("/analytics/rating-distribution", async (req, res): Promise => { + const parsed = GetRatingDistributionQueryParams.safeParse(req.query); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + const toolIdFilter = parsed.data.toolId; + + const baseQuery = toolIdFilter + ? db.select({ usefulness: ratingsTable.usefulness, usability: ratingsTable.usability }) + .from(ratingsTable) + .where(eq(ratingsTable.toolId, toolIdFilter)) + : db.select({ usefulness: ratingsTable.usefulness, usability: ratingsTable.usability }) + .from(ratingsTable); + + const allRatings = await baseQuery; + + const usefulnessBuckets = [1, 2, 3, 4, 5].map((score) => ({ + score, + count: allRatings.filter((r) => r.usefulness === score).length, + })); + + const usabilityBuckets = [1, 2, 3, 4, 5].map((score) => ({ + score, + count: allRatings.filter((r) => r.usability === score).length, + })); + + res.json({ usefulness: usefulnessBuckets, usability: usabilityBuckets }); +}); + +export default router; diff --git a/artifacts/api-server/src/routes/index.ts b/artifacts/api-server/src/routes/index.ts index 5a1f77a..7b6c72b 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 toolsRouter from "./tools"; +import ratingsRouter from "./ratings"; +import analyticsRouter from "./analytics"; const router: IRouter = Router(); router.use(healthRouter); +router.use(toolsRouter); +router.use(ratingsRouter); +router.use(analyticsRouter); export default router; diff --git a/artifacts/api-server/src/routes/ratings.ts b/artifacts/api-server/src/routes/ratings.ts new file mode 100644 index 0000000..66dfc55 --- /dev/null +++ b/artifacts/api-server/src/routes/ratings.ts @@ -0,0 +1,64 @@ +import { Router, type IRouter } from "express"; +import { eq } from "drizzle-orm"; +import { db, toolsTable, ratingsTable } from "@workspace/db"; +import { + ListToolRatingsParams, + CreateRatingParams, + CreateRatingBody, +} from "@workspace/api-zod"; + +const router: IRouter = Router(); + +router.get("/tools/:id/ratings", async (req, res): Promise => { + const params = ListToolRatingsParams.safeParse(req.params); + if (!params.success) { + res.status(400).json({ error: params.error.message }); + return; + } + + const [tool] = await db.select().from(toolsTable).where(eq(toolsTable.id, params.data.id)); + if (!tool) { + res.status(404).json({ error: "Tool not found" }); + return; + } + + const ratings = await db + .select() + .from(ratingsTable) + .where(eq(ratingsTable.toolId, params.data.id)) + .orderBy(ratingsTable.createdAt); + + res.json(ratings); +}); + +router.post("/tools/:id/ratings", async (req, res): Promise => { + const params = CreateRatingParams.safeParse(req.params); + if (!params.success) { + res.status(400).json({ error: params.error.message }); + return; + } + + const [tool] = await db.select().from(toolsTable).where(eq(toolsTable.id, params.data.id)); + if (!tool) { + res.status(404).json({ error: "Tool not found" }); + return; + } + + const parsed = CreateRatingBody.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + const [rating] = await db.insert(ratingsTable).values({ + toolId: params.data.id, + usefulness: parsed.data.usefulness, + usability: parsed.data.usability, + comment: parsed.data.comment ?? null, + reviewerName: parsed.data.reviewerName ?? null, + }).returning(); + + res.status(201).json(rating); +}); + +export default router; diff --git a/artifacts/api-server/src/routes/tools.ts b/artifacts/api-server/src/routes/tools.ts new file mode 100644 index 0000000..21466dd --- /dev/null +++ b/artifacts/api-server/src/routes/tools.ts @@ -0,0 +1,173 @@ +import { Router, type IRouter } from "express"; +import { eq, ilike, desc, sql, avg, count } from "drizzle-orm"; +import { db, toolsTable, ratingsTable } from "@workspace/db"; +import { + ListToolsQueryParams, + CreateToolBody, + GetToolParams, + UpdateToolParams, + UpdateToolBody, + DeleteToolParams, +} from "@workspace/api-zod"; + +const router: IRouter = Router(); + +function buildToolWithStats(tool: typeof toolsTable.$inferSelect, ratings: { usefulness: number; usability: number }[]) { + const ratingCount = ratings.length; + const avgUsefulness = ratingCount > 0 + ? ratings.reduce((s, r) => s + r.usefulness, 0) / ratingCount + : null; + const avgUsability = ratingCount > 0 + ? ratings.reduce((s, r) => s + r.usability, 0) / ratingCount + : null; + const avgCombined = avgUsefulness != null && avgUsability != null + ? (avgUsefulness + avgUsability) / 2 + : null; + return { ...tool, ratingCount, avgUsefulness, avgUsability, avgCombined }; +} + +router.get("/tools", async (req, res): Promise => { + const parsed = ListToolsQueryParams.safeParse(req.query); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + const { category, search, sort } = parsed.data; + + let query = db.select().from(toolsTable).$dynamic(); + if (category) { + query = query.where(eq(toolsTable.category, category)); + } + if (search) { + query = query.where(ilike(toolsTable.name, `%${search}%`)); + } + + const tools = await query.orderBy(desc(toolsTable.createdAt)); + + const toolIds = tools.map((t) => t.id); + let allRatings: { toolId: number; usefulness: number; usability: number }[] = []; + if (toolIds.length > 0) { + allRatings = await db + .select({ toolId: ratingsTable.toolId, usefulness: ratingsTable.usefulness, usability: ratingsTable.usability }) + .from(ratingsTable) + .where(sql`${ratingsTable.toolId} = ANY(${sql`ARRAY[${sql.join(toolIds.map(id => sql`${id}`), sql`, `)}]::int[]`})`); + } + + const ratingsByTool = new Map(); + for (const r of allRatings) { + const arr = ratingsByTool.get(r.toolId) ?? []; + arr.push({ usefulness: r.usefulness, usability: r.usability }); + ratingsByTool.set(r.toolId, arr); + } + + let result = tools.map((t) => buildToolWithStats(t, ratingsByTool.get(t.id) ?? [])); + + if (sort === "top_rated") { + result = result.sort((a, b) => (b.avgCombined ?? 0) - (a.avgCombined ?? 0)); + } else if (sort === "most_reviewed") { + result = result.sort((a, b) => b.ratingCount - a.ratingCount); + } + + res.json(result); +}); + +router.post("/tools", async (req, res): Promise => { + const parsed = CreateToolBody.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + const [tool] = await db.insert(toolsTable).values({ + name: parsed.data.name, + description: parsed.data.description, + category: parsed.data.category, + websiteUrl: parsed.data.websiteUrl ?? null, + features: parsed.data.features ?? [], + tags: parsed.data.tags ?? [], + }).returning(); + + res.status(201).json(tool); +}); + +router.get("/tools/:id", async (req, res): Promise => { + const params = GetToolParams.safeParse(req.params); + if (!params.success) { + res.status(400).json({ error: params.error.message }); + return; + } + + const [tool] = await db.select().from(toolsTable).where(eq(toolsTable.id, params.data.id)); + if (!tool) { + res.status(404).json({ error: "Tool not found" }); + return; + } + + const ratings = await db + .select({ usefulness: ratingsTable.usefulness, usability: ratingsTable.usability }) + .from(ratingsTable) + .where(eq(ratingsTable.toolId, params.data.id)); + + res.json(buildToolWithStats(tool, ratings)); +}); + +router.patch("/tools/:id", async (req, res): Promise => { + const params = UpdateToolParams.safeParse(req.params); + if (!params.success) { + res.status(400).json({ error: params.error.message }); + return; + } + + const parsed = UpdateToolBody.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + const updateData: Record = {}; + if (parsed.data.name !== undefined) updateData.name = parsed.data.name; + if (parsed.data.description !== undefined) updateData.description = parsed.data.description; + if (parsed.data.category !== undefined) updateData.category = parsed.data.category; + if (parsed.data.websiteUrl !== undefined) updateData.websiteUrl = parsed.data.websiteUrl; + if (parsed.data.features !== undefined) updateData.features = parsed.data.features; + if (parsed.data.tags !== undefined) updateData.tags = parsed.data.tags; + + const [tool] = await db + .update(toolsTable) + .set(updateData) + .where(eq(toolsTable.id, params.data.id)) + .returning(); + + if (!tool) { + res.status(404).json({ error: "Tool not found" }); + return; + } + + res.json(tool); +}); + +router.delete("/tools/:id", async (req, res): Promise => { + const params = DeleteToolParams.safeParse(req.params); + if (!params.success) { + res.status(400).json({ error: params.error.message }); + return; + } + + const [tool] = await db.delete(toolsTable).where(eq(toolsTable.id, params.data.id)).returning(); + if (!tool) { + res.status(404).json({ error: "Tool not found" }); + return; + } + + res.sendStatus(204); +}); + +router.get("/categories", async (_req, res): Promise => { + const rows = await db + .selectDistinct({ category: toolsTable.category }) + .from(toolsTable) + .orderBy(toolsTable.category); + res.json(rows.map((r) => r.category)); +}); + +export default router; diff --git a/artifacts/toolrate/.replit-artifact/artifact.toml b/artifacts/toolrate/.replit-artifact/artifact.toml new file mode 100644 index 0000000..6afe456 --- /dev/null +++ b/artifacts/toolrate/.replit-artifact/artifact.toml @@ -0,0 +1,31 @@ +kind = "web" +previewPath = "/" +title = "ToolRate — Tool-Bewertungsplattform" +version = "1.0.0" +id = "artifacts/toolrate" +router = "path" + +[[integratedSkills]] +name = "react-vite" +version = "1.0.0" + +[[services]] +name = "web" +paths = [ "/" ] +localPort = 26015 + +[services.development] +run = "pnpm --filter @workspace/toolrate run dev" + +[services.production] +build = [ "pnpm", "--filter", "@workspace/toolrate", "run", "build" ] +publicDir = "artifacts/toolrate/dist/public" +serve = "static" + +[[services.production.rewrites]] +from = "/*" +to = "/index.html" + +[services.env] +PORT = "26015" +BASE_PATH = "/" diff --git a/artifacts/toolrate/components.json b/artifacts/toolrate/components.json new file mode 100644 index 0000000..3ff62cf --- /dev/null +++ b/artifacts/toolrate/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/toolrate/index.html b/artifacts/toolrate/index.html new file mode 100644 index 0000000..987ecd7 --- /dev/null +++ b/artifacts/toolrate/index.html @@ -0,0 +1,24 @@ + + + + + + ToolRate — Tool-Bewertungsplattform + + + + + + + + + + + + + + +
+ + + diff --git a/artifacts/toolrate/package.json b/artifacts/toolrate/package.json new file mode 100644 index 0000000..fd5be53 --- /dev/null +++ b/artifacts/toolrate/package.json @@ -0,0 +1,77 @@ +{ + "name": "@workspace/toolrate", + "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/toolrate/public/favicon.svg b/artifacts/toolrate/public/favicon.svg new file mode 100644 index 0000000..4373d3c --- /dev/null +++ b/artifacts/toolrate/public/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/artifacts/toolrate/public/opengraph.jpg b/artifacts/toolrate/public/opengraph.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bbbdfe87bfdf51ef3390b001c3e1fd492fdbc250 GIT binary patch literal 79184 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 Kkyy0b$^Qev2_}>P literal 0 HcmV?d00001 diff --git a/artifacts/toolrate/public/robots.txt b/artifacts/toolrate/public/robots.txt new file mode 100644 index 0000000..c2a49f4 --- /dev/null +++ b/artifacts/toolrate/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/artifacts/toolrate/src/App.tsx b/artifacts/toolrate/src/App.tsx new file mode 100644 index 0000000..6628fc5 --- /dev/null +++ b/artifacts/toolrate/src/App.tsx @@ -0,0 +1,49 @@ +import { Switch, Route, Router as WouterRouter } from "wouter"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "@/components/ui/toaster"; +import { TooltipProvider } from "@/components/ui/tooltip"; + +// Pages +import Home from "@/pages/home"; +import ToolsBrowse from "@/pages/tools-browse"; +import ToolDetail from "@/pages/tool-detail"; +import ToolNew from "@/pages/tool-new"; +import Analytics from "@/pages/analytics"; +import NotFound from "@/pages/not-found"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + staleTime: 1000 * 60 * 5, // 5 minutes + }, + }, +}); + +function Router() { + return ( + + + + + + + + + ); +} + +function App() { + return ( + + + + + + + + + ); +} + +export default App; diff --git a/artifacts/toolrate/src/components/layout.tsx b/artifacts/toolrate/src/components/layout.tsx new file mode 100644 index 0000000..d999c5d --- /dev/null +++ b/artifacts/toolrate/src/components/layout.tsx @@ -0,0 +1,49 @@ +import { Link, useLocation } from "wouter"; +import { LayoutDashboard, Wrench, PlusCircle, BarChart3 } from "lucide-react"; + +export function Layout({ children }: { children: React.ReactNode }) { + const [location] = useLocation(); + + const links = [ + { href: "/", label: "Dashboard", icon: LayoutDashboard }, + { href: "/tools", label: "Browse Tools", icon: Wrench }, + { href: "/tools/new", label: "Add Tool", icon: PlusCircle }, + { href: "/analytics", label: "Analytics", icon: BarChart3 }, + ]; + + return ( +
+ +
+
+
+ + ToolRate +
+
+
+ {children} +
+
+
+ ); +} diff --git a/artifacts/toolrate/src/components/rating-stars.tsx b/artifacts/toolrate/src/components/rating-stars.tsx new file mode 100644 index 0000000..41cbd71 --- /dev/null +++ b/artifacts/toolrate/src/components/rating-stars.tsx @@ -0,0 +1,41 @@ +import { Star } from "lucide-react"; + +interface RatingStarsProps { + value: number; + max?: number; + size?: "sm" | "md" | "lg"; + interactive?: boolean; + onChange?: (value: number) => void; +} + +export function RatingStars({ value, max = 5, size = "md", interactive = false, onChange }: RatingStarsProps) { + const sizeClasses = { + sm: "w-3 h-3", + md: "w-5 h-5", + lg: "w-7 h-7" + }; + + return ( +
+ {Array.from({ length: max }).map((_, i) => { + const starValue = i + 1; + const isFilled = value >= starValue; + + return ( + + ); + })} +
+ ); +} diff --git a/artifacts/toolrate/src/components/tool-card.tsx b/artifacts/toolrate/src/components/tool-card.tsx new file mode 100644 index 0000000..22253fd --- /dev/null +++ b/artifacts/toolrate/src/components/tool-card.tsx @@ -0,0 +1,51 @@ +import { ToolWithStats } from "@workspace/api-client-react"; +import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Star, MessageSquare } from "lucide-react"; +import { Link } from "wouter"; + +export function ToolCard({ tool }: { tool: ToolWithStats }) { + return ( + + + +
+ {tool.name} + {tool.category} +
+ + {tool.description} + +
+ +
+ {tool.tags?.slice(0, 3).map((tag) => ( + + {tag} + + ))} + {(tool.tags?.length || 0) > 3 && ( + + +{(tool.tags?.length || 0) - 3} + + )} +
+
+ +
+
+ + + {tool.avgCombined ? tool.avgCombined.toFixed(1) : "N/A"} + +
+
+ + {tool.ratingCount} +
+
+
+ +
+ ); +} diff --git a/artifacts/toolrate/src/components/ui/accordion.tsx b/artifacts/toolrate/src/components/ui/accordion.tsx new file mode 100644 index 0000000..e1797c9 --- /dev/null +++ b/artifacts/toolrate/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/toolrate/src/components/ui/alert-dialog.tsx b/artifacts/toolrate/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..fa2b442 --- /dev/null +++ b/artifacts/toolrate/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/toolrate/src/components/ui/alert.tsx b/artifacts/toolrate/src/components/ui/alert.tsx new file mode 100644 index 0000000..5afd41d --- /dev/null +++ b/artifacts/toolrate/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/toolrate/src/components/ui/aspect-ratio.tsx b/artifacts/toolrate/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..c4abbf3 --- /dev/null +++ b/artifacts/toolrate/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/toolrate/src/components/ui/avatar.tsx b/artifacts/toolrate/src/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/artifacts/toolrate/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/toolrate/src/components/ui/badge.tsx b/artifacts/toolrate/src/components/ui/badge.tsx new file mode 100644 index 0000000..3f03665 --- /dev/null +++ b/artifacts/toolrate/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/toolrate/src/components/ui/breadcrumb.tsx b/artifacts/toolrate/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..60e6c96 --- /dev/null +++ b/artifacts/toolrate/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) =>