Implement active user tiers, class-based Tailwind theme switcher, 3-tiled static dashboard layout, Minio object storage integration, school & state settings database sync, and streak wishes onboarding modal

This commit is contained in:
2026-07-07 13:43:30 +05:30
parent 426b98ddab
commit 04db17ea0e
24 changed files with 913 additions and 236 deletions

View File

@@ -4,16 +4,19 @@ import { useEffect, useState } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { GraduationCap, MapPin, RefreshCw } from "lucide-react"
const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b"
import { useActiveUser } from "@/hooks/useActiveUser"
export function CollegePredictorCard() {
const [data, setData] = useState<any>(null)
const [loading, setLoading] = useState(true)
const { userId, loading: userLoading } = useActiveUser();
useEffect(() => {
if (userLoading || !userId) return;
async function fetchReadiness() {
try {
const res = await fetch(`http://localhost:8000/api/ai-coach/readiness/${USER_ID}`)
const res = await fetch(`/api/ai-coach/readiness/${userId}`)
if (!res.ok) throw new Error("Failed to fetch")
const json = await res.json()
setData(json)
@@ -28,7 +31,7 @@ export function CollegePredictorCard() {
}
}
fetchReadiness()
}, [])
}, [userId, userLoading])
if (loading) {
return (

View File

@@ -4,16 +4,19 @@ import { useEffect, useState } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Activity, RefreshCw } from "lucide-react"
const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b"
import { useActiveUser } from "@/hooks/useActiveUser"
export function ReadinessGauge() {
const [data, setData] = useState<any>(null)
const [loading, setLoading] = useState(true)
const { userId, loading: userLoading } = useActiveUser();
useEffect(() => {
if (userLoading || !userId) return;
async function fetchReadiness() {
try {
const res = await fetch(`http://localhost:8000/api/ai-coach/readiness/${USER_ID}`)
const res = await fetch(`/api/ai-coach/readiness/${userId}`)
if (!res.ok) throw new Error("Failed to fetch")
const json = await res.json()
setData(json)
@@ -34,7 +37,7 @@ export function ReadinessGauge() {
}
}
fetchReadiness()
}, [])
}, [userId, userLoading])
if (loading) {
return (

View File

@@ -5,16 +5,18 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/com
import { Button } from "@/components/ui/button"
import { Link as LinkIcon, Share2, Check, Gift } from "lucide-react"
const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b"
import { useActiveUser } from "@/hooks/useActiveUser"
export function ReferralCard() {
const { userId, loading: userLoading } = useActiveUser();
const [data, setData] = useState<any>(null)
const [copied, setCopied] = useState(false)
useEffect(() => {
if (userLoading || !userId) return;
async function fetchReferral() {
try {
const res = await fetch(`http://localhost:8000/api/growth/referral/${USER_ID}`)
const res = await fetch(`/api/growth/referral/${userId}`)
if (res.ok) {
const json = await res.json()
setData(json)
@@ -24,9 +26,9 @@ export function ReferralCard() {
}
}
fetchReferral()
}, [])
}, [userId, userLoading])
const link = data?.referral_link || "https://pragativeer.com/join/demo"
const link = data?.referral_link || `https://pragativeer.com/join/${userId || "demo"}`
const message = data?.share_message || "Join me on PragatiVeer! Use my link to get 7 days of the AI Coach for free."
const copyToClipboard = () => {

View File

@@ -5,16 +5,19 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/com
import { Badge } from "@/components/ui/badge"
import { CalendarClock, CheckCircle2, Circle, Clock, Loader2, PlayCircle, RefreshCw, XCircle } from "lucide-react"
const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b"
import { useActiveUser } from "@/hooks/useActiveUser"
export function StudyPlanCard() {
const [tasks, setTasks] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const { userId, loading: userLoading } = useActiveUser();
useEffect(() => {
if (userLoading || !userId) return;
async function fetchPlan() {
try {
const res = await fetch(`http://localhost:8000/api/ai-coach/study-plan/today?user_id=${USER_ID}`)
const res = await fetch(`/api/ai-coach/study-plan/today?user_id=${userId}`)
if (!res.ok) throw new Error("No plan")
const data = await res.json()
setTasks(data.tasks || [])
@@ -31,7 +34,7 @@ export function StudyPlanCard() {
}
}
fetchPlan()
}, [])
}, [userId, userLoading])
const toggleTask = (idx: number, newStatus: string) => {
setTasks(prev => {

View File

@@ -10,7 +10,7 @@ import remarkMath from "remark-math"
import rehypeKatex from "rehype-katex"
import "katex/dist/katex.min.css"
const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b"
import { useActiveUser } from "@/hooks/useActiveUser"
interface Message {
role: "user" | "assistant"
@@ -24,6 +24,8 @@ export function TutorChat() {
const [input, setInput] = useState("")
const [streaming, setStreaming] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const { userId, loading: userLoading } = useActiveUser();
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
@@ -35,7 +37,7 @@ export function TutorChat() {
const handleSend = async (e?: React.FormEvent) => {
e?.preventDefault()
if (!input.trim() || streaming) return
if (!input.trim() || streaming || userLoading || !userId) return
const userMsg = input.trim()
setInput("")
@@ -46,10 +48,10 @@ export function TutorChat() {
setStreaming(true)
try {
const res = await fetch("http://localhost:8000/api/ai-coach/chat/stream", {
const res = await fetch("/api/ai-coach/chat/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: USER_ID, message: userMsg })
body: JSON.stringify({ user_id: userId, message: userMsg })
})
if (!res.ok) {

View File

@@ -0,0 +1,92 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Flame, Trophy, Award, Sparkles } from "lucide-react";
import { useActiveUser } from "@/hooks/useActiveUser";
export function StreakOnboardingModal() {
const { user, loading } = useActiveUser();
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
if (loading || !user) return;
// Check sessionStorage
const hasSeen = sessionStorage.getItem(`hasSeenStreakPopup_${user.id}`);
if (!hasSeen) {
setIsOpen(true);
}
}, [user, loading]);
if (!isOpen || !user) return null;
const streak = user.streak_count;
// Streak-based wishes and titles
let wishTitle = "Welcome to PragatiVeer! 🌟";
let wishMessage = "Prepare yourself to conquer JEE/NEET. Your daily practice starts here!";
let iconTheme = "text-[#00F0FF] bg-[#00F0FF]/10";
let iconComponent = <Sparkles className="size-10" />;
if (streak === 0) {
wishTitle = "Start Your Streak Today! 🚀";
wishMessage = "Today is Day 1 of your journey. Establish your streak now and build the consistency needed for IIT/AIIMS!";
iconTheme = "text-blue-500 bg-blue-500/10";
iconComponent = <Sparkles className="size-10" />;
} else if (streak < 4) {
wishTitle = "Keep the Fire Burning! 🔥";
wishMessage = `Great job! You have maintained a ${streak}-day study streak. Keep practicing daily to build solid muscle memory!`;
iconTheme = "text-orange-500 bg-orange-500/10 animate-bounce";
iconComponent = <Flame className="size-10 fill-current" />;
} else {
wishTitle = "Unstoppable Dedication! ⚡";
wishMessage = `Incredible! A ${streak}-day streak shows true championship discipline. You are outperforming the competition. Let's make today count!`;
iconTheme = "text-[#D1FF4D] bg-[#D1FF4D]/10 animate-pulse";
iconComponent = <Trophy className="size-10" />;
}
const handleEnter = () => {
sessionStorage.setItem(`hasSeenStreakPopup_${user.id}`, "true");
setIsOpen(false);
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/85 backdrop-blur-md animate-in fade-in duration-300">
<div className="bg-background border border-border shadow-[0_0_50px_-12px_rgba(0,240,255,0.2)] rounded-3xl w-full max-w-lg p-8 text-center space-y-6 relative overflow-hidden animate-in zoom-in-95 slide-in-from-bottom-4 duration-300">
{/* Animated Background Gradients */}
<div className="absolute -right-20 -top-20 size-48 bg-[#00F0FF]/10 rounded-full blur-3xl pointer-events-none" />
<div className="absolute -left-20 -bottom-20 size-48 bg-primary/10 rounded-full blur-3xl pointer-events-none" />
{/* Dynamic Icon Badge */}
<div className={`size-20 rounded-2xl ${iconTheme} flex items-center justify-center mx-auto border border-current/20 shadow-[0_0_15px_rgba(0,0,0,0.1)]`}>
{iconComponent}
</div>
<div className="space-y-2">
{streak > 0 && (
<div className="inline-flex items-center gap-1.5 px-3 py-1 bg-orange-500/10 border border-orange-500/20 text-orange-500 text-xs font-extrabold rounded-full tracking-wider uppercase">
<Flame size={12} className="fill-current" /> {streak} Day Streak Active
</div>
)}
<h3 className="text-3xl font-black tracking-tight bg-gradient-to-r from-foreground via-foreground to-muted-foreground bg-clip-text text-transparent">
{wishTitle}
</h3>
<p className="text-muted-foreground text-sm md:text-base leading-relaxed max-w-md mx-auto pt-2">
{wishMessage}
</p>
</div>
<div className="pt-4">
<Button
onClick={handleEnter}
className="w-full size-12 rounded-xl text-base font-black bg-[#00F0FF] text-background hover:bg-[#00F0FF]/90 shadow-[0_0_20px_-3px_#00F0FF] hover:shadow-[0_0_30px_#00F0FF] transition-all duration-300"
>
Enter Workspace
</Button>
</div>
</div>
</div>
);
}