diff --git a/client/src/App.tsx b/client/src/App.tsx index 753cf33..530b22a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -22,9 +22,11 @@ import AuthPage from "@/pages/auth-page"; import DemoAiFixPage from "@/pages/demo-ai-fix"; import AuditPage from "@/pages/audit"; import PricingPage from "@/pages/pricing"; +import FreeAuditRequest from "@/pages/free-audit-request"; import AdminOverview from "@/pages/admin/overview"; import AdminUsers from "@/pages/admin/users"; import AdminOrders from "@/pages/admin/orders"; +import AdminFreeAuditQueue from "@/pages/admin/free-audit-queue"; import AdminRequests from "@/pages/admin/requests"; import AdminSystem from "@/pages/admin/system"; import AdminAuditLog from "@/pages/admin/audit-log"; @@ -63,11 +65,13 @@ function App() { + {/* Admin Routes with Sidebar */} + diff --git a/client/src/pages/admin/free-audit-queue.tsx b/client/src/pages/admin/free-audit-queue.tsx new file mode 100644 index 0000000..e3456a8 --- /dev/null +++ b/client/src/pages/admin/free-audit-queue.tsx @@ -0,0 +1,204 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { Loader2, CheckCircle2, XCircle, Search } from "lucide-react"; +import { format } from "date-fns"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { useToast } from "@/hooks/use-toast"; + +type FreeAuditRequest = { + id: string; + repoUrl: string; + contactName: string; + contactEmail: string; + motivationText: string; + status: string; + submittedAt: string; +}; + +type FreeAuditsResponse = { + requests: FreeAuditRequest[]; + todayCost: number; +}; + +export default function AdminFreeAuditQueue() { + const { toast } = useToast(); + const queryClient = useQueryClient(); + + const { data, isLoading } = useQuery({ + queryKey: ["/api/admin/free-audits"], + }); + + const approveMutation = useMutation({ + mutationFn: async (id: string) => { + const res = await fetch(`/api/admin/free-audits/${id}/approve`, { + method: "POST", + }); + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.error || "Failed to approve request"); + } + return res.json(); + }, + onSuccess: () => { + toast({ title: "Audit Approved", description: "The audit has been started and marked as comped." }); + queryClient.invalidateQueries({ queryKey: ["/api/admin/free-audits"] }); + }, + onError: (error) => { + toast({ title: "Error", description: error.message, variant: "destructive" }); + }, + }); + + const rejectMutation = useMutation({ + mutationFn: async (id: string) => { + const res = await fetch(`/api/admin/free-audits/${id}/reject`, { + method: "POST", + }); + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.error || "Failed to reject request"); + } + return res.json(); + }, + onSuccess: () => { + toast({ title: "Request Rejected" }); + queryClient.invalidateQueries({ queryKey: ["/api/admin/free-audits"] }); + }, + onError: (error) => { + toast({ title: "Error", description: error.message, variant: "destructive" }); + }, + }); + + if (isLoading) { + return ( +
+ +
+ ); + } + + const requests = data?.requests || []; + const todayCost = data?.todayCost || 0; + const isCeilingReached = todayCost >= 100; + + return ( +
+
+
+

Free Audit Queue

+

+ Review and approve requests submitted via the public free audit offer. +

+
+
+ + Today's API Cost: ${todayCost.toFixed(2)} / $100.00 + +
+
+ + {isCeilingReached && ( +
+ +

+ Daily cost ceiling reached. You cannot approve new free audits until tomorrow or unless the limit is increased. +

+
+ )} + + + + Pending Requests + + {requests.length === 0 ? "No pending requests at the moment." : `Found ${requests.length} pending request(s).`} + + + +
+ + + + Submitted + Repository + Contact + Motivation + Actions + + + + {requests.map((req) => ( + + + {format(new Date(req.submittedAt), "MMM d, yyyy")} + + + + {req.repoUrl.replace(/^https?:\/\/(www\.)?github\.com\//, "")} + + + +
{req.contactName}
+
{req.contactEmail}
+
+ +

+ {req.motivationText} +

+
+ +
+ + +
+
+
+ ))} + {requests.length === 0 && ( + + + No pending requests found. + + + )} +
+
+
+
+
+
+ ); +} diff --git a/client/src/pages/free-audit-request.tsx b/client/src/pages/free-audit-request.tsx new file mode 100644 index 0000000..8b8cfb4 --- /dev/null +++ b/client/src/pages/free-audit-request.tsx @@ -0,0 +1,251 @@ +import { useState } from "react"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import { useLocation } from "wouter"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Loader2, CheckCircle2, ShieldAlert } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Checkbox } from "@/components/ui/checkbox"; +import { useToast } from "@/hooks/use-toast"; + +const requestSchema = z.object({ + repoUrl: z.string().min(1, "Repository URL is required").url("Must be a valid URL"), + contactName: z.string().min(1, "Name is required"), + contactEmail: z.string().email("Invalid email address"), + motivationText: z.string().min(10, "Please provide a bit more detail (min 10 chars)"), + confirmAccess: z.literal(true, { + errorMap: () => ({ message: "You must confirm you have the right to grant access" }), + }), +}); + +type PromoOfferData = { + active: boolean; + pendingCount?: number; + offer?: { + id: string; + name: string; + description: string; + startsAt: string; + endsAt: string; + }; +}; + +export default function FreeAuditRequest() { + const [, setLocation] = useLocation(); + const { toast } = useToast(); + const [submitted, setSubmitted] = useState(false); + + const { data: offerData, isLoading } = useQuery({ + queryKey: ["/api/public/promo-offer"], + }); + + const form = useForm>({ + resolver: zodResolver(requestSchema), + defaultValues: { + repoUrl: "", + contactName: "", + contactEmail: "", + motivationText: "", + }, + }); + + const mutation = useMutation({ + mutationFn: async (values: z.infer) => { + const res = await fetch("/api/public/free-audit-request", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(values), + }); + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || "Failed to submit request"); + } + return res.json(); + }, + onSuccess: () => { + setSubmitted(true); + }, + onError: (error) => { + toast({ + title: "Submission failed", + description: error.message, + variant: "destructive", + }); + }, + }); + + function onSubmit(values: z.infer) { + mutation.mutate(values); + } + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!offerData?.active) { + return ( +
+ +

+ This week's free audit offer has ended +

+

+ Thank you for your interest! We're currently processing the batch of requests we received. +

+ +
+ ); + } + + if (submitted) { + return ( +
+ +

+ Request Received +

+

+ Thanks — I'm reviewing these personally this week and will email you directly. +

+
+ ); + } + + return ( +
+
+

+ {offerData?.offer?.name === "linkedin-launch-week" + ? "Free CodeGuard Audit" + : "Free CodeGuard Audit"} +

+

+ {offerData?.offer?.description || "Drop your repo, and I'll run an Audit Mode pass on it personally."} +

+
+ + {(offerData?.pendingCount ?? 0) > 50 && ( +
+ +

+ High volume: Due to the number of requests, new submissions may not be reviewed before the offer ends. +

+
+ )} + + + + Submit your repository + + No credit card required. I review every submission personally to ensure we can provide value. + + + +
+ + ( + + Repository URL + + + + + Must be a public repository. If private, please include instructions to grant read access in the motivation field below. + + + + )} + /> + +
+ ( + + Your Name + + + + + + )} + /> + ( + + Email Address + + + + + + )} + /> +
+ + ( + + What are you hoping to learn from the report? + +