- Restructure: move backend from new_project/ to backend/ - Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks) - Add docs/ with SRS specs, user stories, and workflow documentation - Update .gitignore for new directory layout Workflows implemented: WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration, WF4 General English Exam, WF5 Course Generation, WF6 Entity Student Onboarding, AI Course Generation, Adaptive Learning Engine UI, White-Label Branding, Score Release Made-with: Cursor
97 lines
4.1 KiB
TypeScript
97 lines
4.1 KiB
TypeScript
import { useState } from "react";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
|
import { Search, HelpCircle, Video } from "lucide-react";
|
|
import { useFaqCategories, useFaqItems } from "@/hooks/queries";
|
|
|
|
export default function FaqPage() {
|
|
const [search, setSearch] = useState("");
|
|
const { data: categories = [], isLoading: lc } = useFaqCategories();
|
|
const { data: allItems = [], isLoading: li } = useFaqItems(search ? { search } : undefined);
|
|
|
|
const filteredCategories = categories.filter(cat =>
|
|
allItems.some(item => item.category_id === cat.id),
|
|
);
|
|
|
|
const getItemsForCategory = (catId: number) =>
|
|
allItems.filter(item => item.category_id === catId);
|
|
|
|
if (lc || li) return <div className="flex items-center justify-center min-h-[400px]"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" /></div>;
|
|
|
|
return (
|
|
<div className="space-y-6 max-w-3xl mx-auto">
|
|
<div className="text-center">
|
|
<h1 className="text-3xl font-bold">Frequently Asked Questions</h1>
|
|
<p className="text-muted-foreground mt-2">Find answers to common questions about EnCoach.</p>
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
className="pl-10"
|
|
placeholder="Search questions..."
|
|
value={search}
|
|
onChange={e => setSearch(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
{filteredCategories.length > 0 ? (
|
|
<div className="space-y-6">
|
|
{filteredCategories.map(cat => {
|
|
const catItems = getItemsForCategory(cat.id);
|
|
if (catItems.length === 0) return null;
|
|
return (
|
|
<Card key={cat.id}>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center gap-2">
|
|
<HelpCircle className="h-5 w-5 text-primary" />
|
|
<CardTitle className="text-lg">{cat.name}</CardTitle>
|
|
<Badge variant="secondary" className="text-xs">{cat.item_count} questions</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Accordion type="multiple">
|
|
{catItems
|
|
.sort((a, b) => a.sequence - b.sequence)
|
|
.map(item => (
|
|
<AccordionItem key={item.id} value={String(item.id)}>
|
|
<AccordionTrigger className="text-left text-sm font-medium">
|
|
{item.question}
|
|
</AccordionTrigger>
|
|
<AccordionContent>
|
|
<div className="space-y-2">
|
|
<p className="text-sm text-muted-foreground whitespace-pre-wrap">{item.answer}</p>
|
|
{item.video_url && (
|
|
<a
|
|
href={item.video_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
|
|
>
|
|
<Video className="h-4 w-4" /> Watch video
|
|
</a>
|
|
)}
|
|
</div>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
))}
|
|
</Accordion>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<Card>
|
|
<CardContent className="py-12 text-center text-muted-foreground">
|
|
<HelpCircle className="h-12 w-12 mx-auto mb-3 opacity-40" />
|
|
<p>{search ? "No questions match your search." : "No FAQ items available yet."}</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|