352 lines
17 KiB
TypeScript
352 lines
17 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
|
|
import { MessageSquare, ThumbsUp, ChevronUp, Search, Image as ImageIcon, CheckCircle, Lock, X, Send } from "lucide-react"
|
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
|
import { PremiumGate } from "@/components/PremiumGate"
|
|
|
|
interface Doubt {
|
|
id: string
|
|
author: string
|
|
avatar: string
|
|
timeAgo: string
|
|
subject: string
|
|
topic: string
|
|
title: string
|
|
content: string
|
|
upvotes: number
|
|
replies: number
|
|
isSolved: boolean
|
|
hasModeratorReply: boolean
|
|
}
|
|
|
|
export default function CommunityPage() {
|
|
const { user, hasAccessTo, loading } = useActiveUser()
|
|
const isDemo = isDemoUser(user?.id)
|
|
const currentUserName = user?.name || (isDemo ? "Neha Verma" : "Student")
|
|
const currentUserInitials = currentUserName.substring(0, 2).toUpperCase()
|
|
const [activeFilter, setActiveFilter] = useState<'all' | 'my-doubts' | 'solved' | 'unsolved'>('all')
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
const [isAskModalOpen, setIsAskModalOpen] = useState(false)
|
|
const [expandedDoubtId, setExpandedDoubtId] = useState<string | null>(null)
|
|
|
|
const mockDoubts: Doubt[] = [
|
|
{
|
|
id: "1",
|
|
author: "Rahul Verma",
|
|
avatar: "RV",
|
|
timeAgo: "2 hours ago",
|
|
subject: "Physics",
|
|
topic: "Rotational Mechanics",
|
|
title: "Why does friction act forwards in a rolling wheel?",
|
|
content: "I'm confused about the direction of friction when a car accelerates forward. My book says friction pushes the wheel forward, but shouldn't it oppose motion?",
|
|
upvotes: 42,
|
|
replies: 5,
|
|
isSolved: true,
|
|
hasModeratorReply: true
|
|
},
|
|
{
|
|
id: "2",
|
|
author: "Priya Sharma",
|
|
avatar: "PS",
|
|
timeAgo: "5 hours ago",
|
|
subject: "Chemistry",
|
|
topic: "Chemical Kinetics",
|
|
title: "How to quickly identify a pseudo first-order reaction?",
|
|
content: "Are there any tricks to spot these in MCQs? I always end up wasting 2 minutes trying to derive the rate law.",
|
|
upvotes: 18,
|
|
replies: 2,
|
|
isSolved: false,
|
|
hasModeratorReply: false
|
|
},
|
|
...(isDemo ? [{
|
|
id: "3",
|
|
author: currentUserName,
|
|
avatar: currentUserInitials,
|
|
timeAgo: "1 day ago",
|
|
subject: "Mathematics",
|
|
topic: "Definite Integration",
|
|
title: "Applying King's Rule when limits are -a to a",
|
|
content: "I keep messing up the signs when applying the property f(x) + f(-x). Can someone show a step-by-step for the modulus function?",
|
|
upvotes: 12,
|
|
replies: 8,
|
|
isSolved: true,
|
|
hasModeratorReply: true
|
|
}] : [])
|
|
]
|
|
|
|
const filteredDoubts = mockDoubts.filter(d => {
|
|
if (activeFilter === 'my-doubts' && d.author !== currentUserName) return false
|
|
if (activeFilter === 'solved' && !d.isSolved) return false
|
|
if (activeFilter === 'unsolved' && d.isSolved) return false
|
|
if (searchQuery && !d.title.toLowerCase().includes(searchQuery.toLowerCase())) return false
|
|
return true
|
|
})
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="h-[70vh] flex flex-col items-center justify-center p-8">
|
|
<div className="h-8 w-8 border-4 border-[var(--cyan-ai)] border-t-transparent rounded-full animate-spin mb-4"></div>
|
|
<p className="text-muted-foreground font-medium animate-pulse">Loading Doubt Forum...</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!hasAccessTo("doubt-resolution-community")) {
|
|
return (
|
|
<div className="p-8 h-full flex flex-col items-center justify-center text-center max-w-2xl mx-auto">
|
|
<div className="size-20 rounded-full bg-red-500/10 flex items-center justify-center mx-auto text-red-500 mb-6">
|
|
<Lock size={40} />
|
|
</div>
|
|
<h2 className="text-3xl font-bold mb-4">Premium Feature</h2>
|
|
<p className="text-muted-foreground text-lg mb-8">
|
|
The Doubt Forum is a premium feature. Upgrade your plan to ask doubts and interact with tutors and the community.
|
|
</p>
|
|
<button
|
|
className="px-8 py-3 bg-primary text-primary-foreground font-bold rounded-xl shadow-[0_0_20px_-5px_var(--color-primary)] text-lg"
|
|
onClick={() => window.location.href = '/upgrade'}
|
|
>
|
|
Upgrade Plan
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<PremiumGate featureKey="doubt-resolution-community">
|
|
<div className="p-8 space-y-6 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto">
|
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-4">
|
|
<div>
|
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
|
<span className="p-2.5 rounded-xl bg-yellow-50 dark:bg-[var(--cyan-ai-10)] border border-yellow-300/60 dark:border-[var(--cyan-ai-20)]">
|
|
<MessageSquare className="text-yellow-600 dark:text-[var(--cyan-ai)]" size={28} />
|
|
</span>
|
|
Doubt Forum
|
|
</h1>
|
|
<p className="text-muted-foreground text-base font-medium max-w-2xl pl-1">
|
|
Ask questions, share knowledge, and learn together with the community.
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsAskModalOpen(true)}
|
|
className="px-5 py-2.5 bg-[var(--cyan-ai)] text-black font-bold rounded-xl shadow-[0_0_15px_-3px_rgba(12,96,96,0.4)] hover:bg-[#00d0dd] transition-colors"
|
|
>
|
|
Ask a Doubt
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex flex-col md:flex-row gap-4">
|
|
<div className="relative flex-1">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<Search size={18} className="text-muted-foreground" />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
placeholder="Search doubts by keyword..."
|
|
className="w-full pl-10 pr-4 py-2.5 bg-card border border-border rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai-10)]"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 bg-secondary/50 p-1 rounded-xl border border-border shrink-0 overflow-x-auto">
|
|
<FilterButton active={activeFilter === 'all'} onClick={() => setActiveFilter('all')}>All Doubts</FilterButton>
|
|
<FilterButton active={activeFilter === 'my-doubts'} onClick={() => setActiveFilter('my-doubts')}>My Doubts</FilterButton>
|
|
<FilterButton active={activeFilter === 'solved'} onClick={() => setActiveFilter('solved')}>Solved</FilterButton>
|
|
<FilterButton active={activeFilter === 'unsolved'} onClick={() => setActiveFilter('unsolved')}>Unsolved</FilterButton>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{filteredDoubts.map((doubt) => (
|
|
<Card key={doubt.id} className="border-border shadow-sm hover:border-[var(--cyan-ai-10)] transition-colors cursor-pointer">
|
|
<CardContent className="p-0 flex">
|
|
{/* Upvote Sidebar */}
|
|
<div className="w-16 bg-secondary/20 border-r border-border flex flex-col items-center py-4 gap-1">
|
|
<button className="p-1.5 text-muted-foreground hover:text-[var(--cyan-ai)] hover:bg-[var(--cyan-ai-10)] rounded-lg transition-colors">
|
|
<ChevronUp size={24} />
|
|
</button>
|
|
<span className="font-bold text-foreground">{doubt.upvotes}</span>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<div className="flex-1 p-5">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs font-bold px-2.5 py-1 bg-secondary text-foreground rounded-md">{doubt.subject}</span>
|
|
<span className="text-xs font-medium text-muted-foreground">• {doubt.topic}</span>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">{doubt.timeAgo}</div>
|
|
</div>
|
|
|
|
<h3 className="text-lg font-bold text-foreground mb-1">{doubt.title}</h3>
|
|
<p className="text-sm text-muted-foreground line-clamp-2 mb-4">{doubt.content}</p>
|
|
|
|
<div className="flex items-center justify-between mt-auto">
|
|
<div className="flex items-center gap-2">
|
|
<div className="h-6 w-6 rounded-full bg-secondary flex items-center justify-center text-[10px] font-bold">
|
|
{doubt.avatar}
|
|
</div>
|
|
<span className="text-xs font-medium text-foreground">{doubt.author}</span>
|
|
{doubt.hasModeratorReply && (
|
|
<span className="ml-2 flex items-center gap-1 text-[10px] font-bold text-emerald-500 bg-emerald-500/10 px-2 py-0.5 rounded-sm border border-emerald-500/20">
|
|
<CheckCircle size={10} /> Expert Replied
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
{doubt.isSolved && (
|
|
<span className="text-xs font-bold text-emerald-500 flex items-center gap-1">
|
|
<CheckCircle size={14} /> Solved
|
|
</span>
|
|
)}
|
|
<button
|
|
onClick={() => setExpandedDoubtId(expandedDoubtId === doubt.id ? null : doubt.id)}
|
|
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-colors text-sm font-medium"
|
|
>
|
|
<MessageSquare size={16} />
|
|
{doubt.replies} Replies
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Expanded Replies Section */}
|
|
{expandedDoubtId === doubt.id && (
|
|
<div className="mt-6 pt-4 border-t border-border space-y-4">
|
|
<div className="flex gap-3">
|
|
<div className="h-8 w-8 shrink-0 rounded-full bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] flex items-center justify-center text-xs font-bold border border-[var(--cyan-ai-30)]">
|
|
AI
|
|
</div>
|
|
<div className="bg-secondary/40 rounded-2xl rounded-tl-none p-4 text-sm flex-1">
|
|
<div className="font-bold mb-1 flex items-center gap-2">
|
|
PragatiVeer AI
|
|
<span className="text-[10px] text-emerald-500 bg-emerald-500/10 px-1.5 py-0.5 rounded uppercase tracking-wider">Expert</span>
|
|
</div>
|
|
<p className="text-muted-foreground">
|
|
This is a great question! When the car accelerates, the engine applies a torque to the wheel, making it want to spin faster. If there were no friction, the bottom of the tire would slip backward relative to the road. Therefore, static friction acts in the forward direction on the tire to oppose this backward slipping motion.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-3 mt-4 items-center">
|
|
<input
|
|
type="text"
|
|
placeholder="Add a reply..."
|
|
className="flex-1 bg-background border border-border rounded-xl px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai-10)]"
|
|
/>
|
|
<button className="p-2 bg-[var(--cyan-ai)] text-black rounded-xl hover:bg-[#00d0dd] transition-colors">
|
|
<Send size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
{filteredDoubts.length === 0 && (
|
|
<div className="text-center py-12 text-muted-foreground bg-secondary/10 border border-dashed border-border rounded-2xl p-6">
|
|
<MessageSquare size={48} className="mx-auto mb-3 opacity-30 text-[var(--cyan-ai)]" />
|
|
<p className="font-bold text-foreground mb-1">
|
|
{activeFilter === 'my-doubts' ? "No Doubts Asked Yet" : "No doubts found matching your criteria."}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground max-w-sm mx-auto">
|
|
{activeFilter === 'my-doubts'
|
|
? "You haven't asked any doubts yet. Click 'Ask a Doubt' above to get answers from mentors and peers."
|
|
: "Try selecting another filter or clearing your search query."}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{/* Ask Doubt Modal */}
|
|
{isAskModalOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-background/80 backdrop-blur-sm">
|
|
<div className="bg-background border border-border w-full max-w-2xl rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
|
|
<div className="flex justify-between items-center p-6 border-b border-border bg-secondary/30">
|
|
<h2 className="text-2xl font-bold flex items-center gap-2">
|
|
<MessageSquare className="text-[var(--cyan-ai)]" /> Ask a Doubt
|
|
</h2>
|
|
<button
|
|
onClick={() => setIsAskModalOpen(false)}
|
|
className="text-muted-foreground hover:text-foreground hover:bg-secondary p-2 rounded-lg transition-colors"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-6 space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-1.5">
|
|
<label className="text-sm font-medium text-muted-foreground">Subject</label>
|
|
<select className="w-full bg-background border border-border rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai-10)]">
|
|
<option>Physics</option>
|
|
<option>Chemistry</option>
|
|
<option>Mathematics</option>
|
|
<option>Biology</option>
|
|
</select>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-sm font-medium text-muted-foreground">Topic</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Rotational Mechanics"
|
|
className="w-full bg-background border border-border rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai-10)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="text-sm font-medium text-muted-foreground">Question Title</label>
|
|
<input
|
|
type="text"
|
|
placeholder="What exactly are you stuck on?"
|
|
className="w-full bg-background border border-border rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai-10)]"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="text-sm font-medium text-muted-foreground">Detailed Description</label>
|
|
<textarea
|
|
rows={4}
|
|
placeholder="Explain your approach, what you've tried, and where you're getting stuck..."
|
|
className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai-10)] resize-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="pt-2 border-t border-border flex justify-between items-center">
|
|
<button className="flex items-center gap-2 text-muted-foreground hover:text-[var(--cyan-ai)] transition-colors text-sm font-medium px-3 py-2 rounded-lg hover:bg-secondary">
|
|
<ImageIcon size={18} /> Attach Image
|
|
</button>
|
|
<button
|
|
onClick={() => setIsAskModalOpen(false)}
|
|
className="flex items-center gap-2 px-6 py-2.5 bg-[var(--cyan-ai)] text-black font-bold rounded-xl shadow-[0_0_15px_-3px_rgba(12,96,96,0.4)] hover:bg-[#00d0dd] transition-colors"
|
|
>
|
|
Post Doubt <Send size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</PremiumGate>
|
|
)
|
|
}
|
|
|
|
function FilterButton({ children, active, onClick }: { children: React.ReactNode, active: boolean, onClick: () => void }) {
|
|
return (
|
|
<button
|
|
onClick={onClick}
|
|
className={`px-4 py-1.5 rounded-lg text-sm font-medium transition-colors whitespace-nowrap ${
|
|
active
|
|
? 'bg-background shadow-sm border border-border text-foreground'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
{children}
|
|
</button>
|
|
)
|
|
}
|