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
This commit is contained in:
cheffe01
2026-05-25 13:04:53 +00:00
parent 1de22a6979
commit 036973ea32
111 changed files with 11006 additions and 96 deletions
+273
View File
@@ -0,0 +1,273 @@
import { useState } from "react";
import { useLocation } from "wouter";
import { useForm, useFieldArray } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { useCreateTool, getListToolsQueryKey } from "@workspace/api-client-react";
import { useQueryClient } from "@tanstack/react-query";
import { Layout } from "@/components/layout";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription } from "@/components/ui/form";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { useToast } from "@/hooks/use-toast";
import { Wrench, Plus, X, ArrowLeft } from "lucide-react";
import { Link } from "wouter";
const toolSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
description: z.string().min(10, "Description must be at least 10 characters"),
category: z.string().min(2, "Category is required"),
websiteUrl: z.string().url("Must be a valid URL").optional().or(z.literal("")),
features: z.array(z.object({ value: z.string() })).optional(),
tags: z.array(z.object({ value: z.string() })).optional(),
});
type ToolFormValues = z.infer<typeof toolSchema>;
export default function ToolNew() {
const [, setLocation] = useLocation();
const { toast } = useToast();
const queryClient = useQueryClient();
const createTool = useCreateTool();
const form = useForm<ToolFormValues>({
resolver: zodResolver(toolSchema),
defaultValues: {
name: "",
description: "",
category: "",
websiteUrl: "",
features: [{ value: "" }],
tags: [{ value: "" }]
},
});
const { fields: featureFields, append: appendFeature, remove: removeFeature } = useFieldArray({
control: form.control,
name: "features",
});
const { fields: tagFields, append: appendTag, remove: removeTag } = useFieldArray({
control: form.control,
name: "tags",
});
const onSubmit = (data: ToolFormValues) => {
// Transform arrays back to strings
const payload = {
...data,
websiteUrl: data.websiteUrl || undefined,
features: data.features?.map(f => f.value).filter(v => v.trim() !== ""),
tags: data.tags?.map(t => t.value).filter(v => v.trim() !== "")
};
createTool.mutate({ data: payload }, {
onSuccess: (newTool) => {
toast({
title: "Tool added successfully",
description: "Your tool is now available for review.",
});
queryClient.invalidateQueries({ queryKey: getListToolsQueryKey() });
setLocation(`/tools/${newTool.id}`);
},
onError: (error) => {
toast({
title: "Failed to add tool",
description: error.error || "An unexpected error occurred",
variant: "destructive"
});
}
});
};
return (
<Layout>
<div className="max-w-3xl mx-auto space-y-6 pb-10">
<Button variant="ghost" asChild className="mb-2 -ml-4 text-muted-foreground">
<Link href="/tools"><ArrowLeft className="w-4 h-4 mr-2" /> Back to browse</Link>
</Button>
<div>
<h1 className="text-3xl font-bold tracking-tight mb-2">Add a New Tool</h1>
<p className="text-muted-foreground">Submit a tool you use to let the community rate and review it.</p>
</div>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Wrench className="w-5 h-5 text-primary" />
Tool Details
</CardTitle>
<CardDescription>Provide the basic information about the tool.</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="e.g. React, Next.js, Postgres" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="category"
render={({ field }) => (
<FormItem>
<FormLabel>Category</FormLabel>
<FormControl>
<Input placeholder="e.g. Framework, Database, CI/CD" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="websiteUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Website URL (Optional)</FormLabel>
<FormControl>
<Input placeholder="https://..." type="url" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="What does this tool do? Why do people use it?"
className="min-h-[120px] resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-4 pt-4 border-t">
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-medium">Features</h3>
<p className="text-sm text-muted-foreground">List key capabilities of the tool.</p>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => appendFeature({ value: "" })}
>
<Plus className="w-4 h-4 mr-2" /> Add Feature
</Button>
</div>
<div className="space-y-3">
{featureFields.map((field, index) => (
<FormField
key={field.id}
control={form.control}
name={`features.${index}.value`}
render={({ field }) => (
<FormItem className="flex items-start gap-2 space-y-0">
<FormControl>
<Input placeholder="e.g. Real-time collaboration" {...field} />
</FormControl>
<Button
type="button"
variant="ghost"
size="icon"
className="shrink-0 text-muted-foreground hover:text-destructive"
onClick={() => removeFeature(index)}
>
<X className="w-4 h-4" />
</Button>
</FormItem>
)}
/>
))}
{featureFields.length === 0 && (
<p className="text-sm text-muted-foreground italic">No features added.</p>
)}
</div>
</div>
<div className="space-y-4 pt-4 border-t">
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-medium">Tags</h3>
<p className="text-sm text-muted-foreground">Keywords to help find this tool.</p>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => appendTag({ value: "" })}
>
<Plus className="w-4 h-4 mr-2" /> Add Tag
</Button>
</div>
<div className="flex flex-wrap gap-2">
{tagFields.map((field, index) => (
<FormField
key={field.id}
control={form.control}
name={`tags.${index}.value`}
render={({ field }) => (
<FormItem className="flex items-center space-y-0 relative w-[150px]">
<FormControl>
<Input placeholder="Tag" className="pr-8 h-9 text-sm" {...field} />
</FormControl>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-9 w-8 text-muted-foreground hover:text-destructive"
onClick={() => removeTag(index)}
>
<X className="w-3 h-3" />
</Button>
</FormItem>
)}
/>
))}
</div>
</div>
<div className="pt-6 border-t flex justify-end">
<Button type="submit" disabled={createTool.isPending} className="w-full sm:w-auto">
{createTool.isPending ? "Adding Tool..." : "Submit Tool"}
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</Layout>
);
}