UI/UX Polishing: CBT dashboard, sidebar tiers, landing pricing, and exam disclaimer
This commit is contained in:
@@ -85,6 +85,40 @@ def get_test_by_id(test_id: UUID, user_id: UUID, session: Session = Depends(get_
|
|||||||
raise HTTPException(status_code=404, detail="Requested examination structure not found.")
|
raise HTTPException(status_code=404, detail="Requested examination structure not found.")
|
||||||
return test
|
return test
|
||||||
|
|
||||||
|
@app.get("/api/tests/{test_id}/questions", response_model=List[models.Question], tags=["CBT Engine"])
|
||||||
|
def get_questions_by_test_id(test_id: UUID, session: Session = Depends(get_session)):
|
||||||
|
test = session.get(models.Test, test_id)
|
||||||
|
if not test:
|
||||||
|
raise HTTPException(status_code=404, detail="Requested examination structure not found.")
|
||||||
|
|
||||||
|
# Extract all question IDs from sections
|
||||||
|
question_ids = []
|
||||||
|
for section_name, section_data in test.sections.items():
|
||||||
|
question_ids.extend(section_data.get("questions", []))
|
||||||
|
|
||||||
|
# Fetch all questions in one query
|
||||||
|
if not question_ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Ensure they are UUID objects
|
||||||
|
import uuid
|
||||||
|
valid_ids = []
|
||||||
|
for q_id in question_ids:
|
||||||
|
if isinstance(q_id, str):
|
||||||
|
try:
|
||||||
|
valid_ids.append(uuid.UUID(q_id))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
elif isinstance(q_id, uuid.UUID):
|
||||||
|
valid_ids.append(q_id)
|
||||||
|
|
||||||
|
query = select(models.Question).where(models.Question.id.in_(valid_ids))
|
||||||
|
questions = session.exec(query).all()
|
||||||
|
|
||||||
|
# Order them to match the section order if needed, but the frontend currently doesn't rely on it strictly
|
||||||
|
# except for section bounds which it reconstructs.
|
||||||
|
return questions
|
||||||
|
|
||||||
@app.get("/api/questions/{question_id}", response_model=models.Question, tags=["CBT Engine"])
|
@app.get("/api/questions/{question_id}", response_model=models.Question, tags=["CBT Engine"])
|
||||||
def get_question_by_id(question_id: UUID, session: Session = Depends(get_session)):
|
def get_question_by_id(question_id: UUID, session: Session = Depends(get_session)):
|
||||||
question = session.get(models.Question, question_id)
|
question = session.get(models.Question, question_id)
|
||||||
@@ -143,7 +177,7 @@ def submit_test_attempt(payload: Dict[str, Any], session: Session = Depends(get_
|
|||||||
if not question:
|
if not question:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
correct_ans = question.correct_answer
|
correct_ans = question.correct_answer or {}
|
||||||
# Support both {"value": "A"} and {"correct": "A"} key formats
|
# Support both {"value": "A"} and {"correct": "A"} key formats
|
||||||
correct_value = correct_ans.get("value") or correct_ans.get("correct")
|
correct_value = correct_ans.get("value") or correct_ans.get("correct")
|
||||||
|
|
||||||
@@ -151,8 +185,13 @@ def submit_test_attempt(payload: Dict[str, Any], session: Session = Depends(get_
|
|||||||
# Subjective: award credit for any non-trivial written answer
|
# Subjective: award credit for any non-trivial written answer
|
||||||
student_text = str(selected or "").strip()
|
student_text = str(selected or "").strip()
|
||||||
is_correct = len(student_text) > 5
|
is_correct = len(student_text) > 5
|
||||||
|
elif isinstance(correct_value, list):
|
||||||
|
# Multiple Choice (Multi-Correct)
|
||||||
|
selected_list = sorted([s.strip() for s in str(selected or "").split(",") if s.strip()])
|
||||||
|
correct_list = sorted([str(c).strip() for c in correct_value])
|
||||||
|
is_correct = (selected_list == correct_list)
|
||||||
else:
|
else:
|
||||||
is_correct = bool(correct_value) and correct_value == selected
|
is_correct = bool(correct_value) and str(correct_value).strip() == str(selected or "").strip()
|
||||||
|
|
||||||
if is_correct:
|
if is_correct:
|
||||||
correct_count += 1
|
correct_count += 1
|
||||||
@@ -241,8 +280,16 @@ def submit_mistake_review(payload: Dict[str, Any], session: Session = Depends(ge
|
|||||||
raise HTTPException(status_code=404, detail="Mistake not found.")
|
raise HTTPException(status_code=404, detail="Mistake not found.")
|
||||||
|
|
||||||
question = session.get(models.Question, mistake.question_id)
|
question = session.get(models.Question, mistake.question_id)
|
||||||
correct_value = question.correct_answer.get("value") or question.correct_answer.get("correct")
|
correct_ans = question.correct_answer or {}
|
||||||
is_correct = correct_value == selected_answer
|
correct_value = correct_ans.get("value") or correct_ans.get("correct")
|
||||||
|
|
||||||
|
if isinstance(correct_value, list):
|
||||||
|
# Multiple Choice (Multi-Correct)
|
||||||
|
selected_list = sorted([s.strip() for s in str(selected_answer or "").split(",") if s.strip()])
|
||||||
|
correct_list = sorted([str(c).strip() for c in correct_value])
|
||||||
|
is_correct = (selected_list == correct_list)
|
||||||
|
else:
|
||||||
|
is_correct = bool(correct_value) and str(correct_value).strip() == str(selected_answer or "").strip()
|
||||||
|
|
||||||
q_value = 4 if is_correct else 1
|
q_value = 4 if is_correct else 1
|
||||||
days_since_creation = (datetime.utcnow() - mistake.created_at).days
|
days_since_creation = (datetime.utcnow() - mistake.created_at).days
|
||||||
@@ -450,6 +497,21 @@ def update_user_profile(user_id: UUID, payload: UserUpdate, session: Session = D
|
|||||||
session.refresh(user)
|
session.refresh(user)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
class UserSessionUpdate(BaseModel):
|
||||||
|
session_token: str
|
||||||
|
|
||||||
|
@app.post("/api/users/{user_id}/session", response_model=models.User, tags=["Users"])
|
||||||
|
def update_user_session(user_id: UUID, payload: UserSessionUpdate, session: Session = Depends(get_session)):
|
||||||
|
user = session.get(models.User, user_id)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
user.active_session_token = payload.session_token
|
||||||
|
session.add(user)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(user)
|
||||||
|
return user
|
||||||
|
|
||||||
from storage import upload_file
|
from storage import upload_file
|
||||||
|
|
||||||
@app.post("/api/users/{user_id}/upload-avatar", tags=["Users"])
|
@app.post("/api/users/{user_id}/upload-avatar", tags=["Users"])
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class User(SQLModel, table=True):
|
|||||||
longest_streak: int = Field(default=0)
|
longest_streak: int = Field(default=0)
|
||||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
last_active: datetime = Field(default_factory=datetime.utcnow)
|
last_active: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
active_session_token: Optional[str] = None
|
||||||
|
|
||||||
class Parent(SQLModel, table=True):
|
class Parent(SQLModel, table=True):
|
||||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
|||||||
const resolvedParams = use(params);
|
const resolvedParams = use(params);
|
||||||
const examId = resolvedParams.examId;
|
const examId = resolvedParams.examId;
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [examType, setExamType] = useState<string>('Test');
|
||||||
const [showCalc, setShowCalc] = useState(false);
|
const [showCalc, setShowCalc] = useState(false);
|
||||||
const [showScratchpad, setShowScratchpad] = useState(false);
|
const [showScratchpad, setShowScratchpad] = useState(false);
|
||||||
const [showPeriodicTable, setShowPeriodicTable] = useState(false);
|
const [showPeriodicTable, setShowPeriodicTable] = useState(false);
|
||||||
@@ -61,7 +62,10 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
|||||||
submitAttemptToServer,
|
submitAttemptToServer,
|
||||||
resetExamState,
|
resetExamState,
|
||||||
status,
|
status,
|
||||||
attemptId
|
attemptId,
|
||||||
|
currentQuestionId,
|
||||||
|
getQuestion,
|
||||||
|
examState
|
||||||
} = useCBTStore();
|
} = useCBTStore();
|
||||||
|
|
||||||
const { formattedTime, isTimedOut } = useExamTimer();
|
const { formattedTime, isTimedOut } = useExamTimer();
|
||||||
@@ -75,6 +79,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
|||||||
const testRes = await fetch(`${apiUrl}/api/tests/${examId}?user_id=b0e17378-864d-439e-8806-708dc48a891b`);
|
const testRes = await fetch(`${apiUrl}/api/tests/${examId}?user_id=b0e17378-864d-439e-8806-708dc48a891b`);
|
||||||
if (!testRes.ok) throw new Error("Failed to fetch test structure");
|
if (!testRes.ok) throw new Error("Failed to fetch test structure");
|
||||||
const testData = await testRes.json();
|
const testData = await testRes.json();
|
||||||
|
setExamType(testData.type);
|
||||||
|
|
||||||
// Extract all question IDs from sections
|
// Extract all question IDs from sections
|
||||||
const questionIds: string[] = [];
|
const questionIds: string[] = [];
|
||||||
@@ -82,23 +87,31 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
|||||||
questionIds.push(...section.questions);
|
questionIds.push(...section.questions);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch full question objects
|
const qRes = await fetch(`${apiUrl}/api/tests/${examId}/questions`);
|
||||||
const fetchedQuestions = await Promise.all(
|
if (!qRes.ok) throw new Error("Failed to fetch questions in bulk");
|
||||||
questionIds.map(async (id, idx) => {
|
const allQuestionsData = await qRes.json();
|
||||||
const qRes = await fetch(`${apiUrl}/api/questions/${id}`);
|
|
||||||
const qData = await qRes.json();
|
// Map questions by ID for fast lookup
|
||||||
return {
|
const questionsMap = new Map();
|
||||||
id: qData.id,
|
allQuestionsData.forEach((q: any) => {
|
||||||
number: idx + 1,
|
questionsMap.set(q.id, q);
|
||||||
subject: qData.subject,
|
});
|
||||||
type: qData.type,
|
|
||||||
text_en: qData.question_text,
|
// Construct fetchedQuestions retaining the exact order of questionIds
|
||||||
text_hi: qData.question_text_hi || qData.question_text,
|
const fetchedQuestions = questionIds.map((id, idx) => {
|
||||||
options: Object.values(qData.options || {}),
|
const qData = questionsMap.get(id);
|
||||||
options_hi: Object.values(qData.options_hi || qData.options || {}),
|
if (!qData) return null; // Fallback in case of mismatch
|
||||||
};
|
return {
|
||||||
})
|
id: qData.id,
|
||||||
);
|
number: idx + 1,
|
||||||
|
subject: qData.subject,
|
||||||
|
type: qData.type,
|
||||||
|
text_en: qData.question_text,
|
||||||
|
text_hi: qData.question_text_hi || qData.question_text,
|
||||||
|
options: Object.values(qData.options || {}),
|
||||||
|
options_hi: Object.values(qData.options_hi || qData.options || {}),
|
||||||
|
};
|
||||||
|
}).filter(Boolean) as any[];
|
||||||
|
|
||||||
initExam(examId, fetchedQuestions, testData.total_time * 60);
|
initExam(examId, fetchedQuestions, testData.total_time * 60);
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
@@ -221,6 +234,10 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-8 p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-xl text-yellow-600 dark:text-yellow-500 text-sm text-left">
|
||||||
|
<strong>Disclaimer:</strong> Only the score obtained in your <em>first attempt</em> will be considered for growth tracking and performance evaluation. Any subsequent attempts are intended solely for practice and will not be included in the official assessment.
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mb-8 p-4 bg-muted rounded-xl flex items-center justify-between">
|
<div className="mb-8 p-4 bg-muted rounded-xl flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold">Preferred Theme</p>
|
<p className="font-semibold">Preferred Theme</p>
|
||||||
@@ -247,19 +264,34 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
|||||||
<ExamResult
|
<ExamResult
|
||||||
payload={payload}
|
payload={payload}
|
||||||
attemptId={attemptId}
|
attemptId={attemptId}
|
||||||
onRetake={() => {
|
onRetake={examType === 'DPS' ? () => {
|
||||||
resetExamState();
|
resetExamState();
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}}
|
} : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentQuestion = questions[currentQuestionIndex];
|
const currentQuestion = getQuestion(currentQuestionId)
|
||||||
const currentState = answers[currentQuestion.id];
|
const currentState = currentQuestion ? examState?.answers[currentQuestion.id] : null
|
||||||
|
|
||||||
// Derive palette stats
|
const handleOptionSelect = (qId: string, letter: string, type: string) => {
|
||||||
const paletteStats = Object.values(answers).reduce(
|
if (type === 'Multiple_Choice') {
|
||||||
|
const currentSelected = currentState?.selected_answer || "";
|
||||||
|
const selectedArray = currentSelected.split(',').filter(Boolean);
|
||||||
|
if (selectedArray.includes(letter)) {
|
||||||
|
const newArray = selectedArray.filter((l: string) => l !== letter).sort();
|
||||||
|
selectAnswer(qId, newArray.length > 0 ? newArray.join(',') : null);
|
||||||
|
} else {
|
||||||
|
selectedArray.push(letter);
|
||||||
|
selectAnswer(qId, selectedArray.sort().join(','));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectAnswer(qId, letter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Effect to close overlay tools on EscapealetteStats = Object.values(answers).reduce(
|
||||||
(acc, val) => {
|
(acc, val) => {
|
||||||
acc[val.status]++;
|
acc[val.status]++;
|
||||||
return acc;
|
return acc;
|
||||||
@@ -322,9 +354,9 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
|||||||
|
|
||||||
<div className="flex items-center gap-3 md:gap-6">
|
<div className="flex items-center gap-3 md:gap-6">
|
||||||
<div className="hidden lg:flex items-center gap-2 mr-4">
|
<div className="hidden lg:flex items-center gap-2 mr-4">
|
||||||
<Button variant="ghost" size="sm" onClick={() => setShowCalc(true)} className="gap-2 text-xs font-semibold text-muted-foreground hover:text-foreground"><CalculatorIcon size={14}/> Calc</Button>
|
<Button variant="ghost" size="sm" onClick={() => setShowCalc(true)} className="gap-2 text-xs font-semibold text-muted-foreground hover:bg-[#00F0FF] hover:text-black transition-colors"><CalculatorIcon size={14}/> Calculator</Button>
|
||||||
<Button variant="ghost" size="sm" onClick={() => setShowPeriodicTable(true)} className="gap-2 text-xs font-semibold text-muted-foreground hover:text-foreground"><FlaskConical size={14}/> Periodic Table</Button>
|
<Button variant="ghost" size="sm" onClick={() => setShowPeriodicTable(true)} className="gap-2 text-xs font-semibold text-muted-foreground hover:bg-[#00F0FF] hover:text-black transition-colors"><FlaskConical size={14}/> Periodic Table</Button>
|
||||||
<Button variant="ghost" size="sm" onClick={() => setShowScratchpad(true)} className="gap-2 text-xs font-semibold text-muted-foreground hover:text-foreground"><Edit3 size={14}/> Scratchpad</Button>
|
<Button variant="ghost" size="sm" onClick={() => setShowScratchpad(true)} className="gap-2 text-xs font-semibold text-muted-foreground hover:bg-[#00F0FF] hover:text-black transition-colors"><Edit3 size={14}/> Scratchpad</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`flex items-center gap-2 px-3 py-1.5 md:px-4 md:py-2 rounded-xl font-mono text-base md:text-lg tracking-wider font-medium border border-border ${isTimedOut ? 'bg-red-500/10 text-red-500' : 'bg-secondary/50 text-foreground'}`}>
|
<div className={`flex items-center gap-2 px-3 py-1.5 md:px-4 md:py-2 rounded-xl font-mono text-base md:text-lg tracking-wider font-medium border border-border ${isTimedOut ? 'bg-red-500/10 text-red-500' : 'bg-secondary/50 text-foreground'}`}>
|
||||||
@@ -368,7 +400,15 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3 mt-8">
|
<div className="space-y-3 mt-8">
|
||||||
{(currentQuestion.type === 'Subjective' || currentQuestion.type === 'Numerical') ? (
|
{currentQuestion.type === 'Numerical' ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full p-4 rounded-xl border border-border bg-background focus:ring-2 focus:ring-primary focus:border-transparent text-base"
|
||||||
|
placeholder="Type your numerical answer here..."
|
||||||
|
value={currentState?.selected_answer || ""}
|
||||||
|
onChange={(e) => selectAnswer(currentQuestion.id, e.target.value)}
|
||||||
|
/>
|
||||||
|
) : currentQuestion.type === 'Subjective' ? (
|
||||||
<textarea
|
<textarea
|
||||||
className="w-full min-h-[150px] p-4 rounded-xl border border-border bg-background focus:ring-2 focus:ring-primary focus:border-transparent resize-y text-base"
|
className="w-full min-h-[150px] p-4 rounded-xl border border-border bg-background focus:ring-2 focus:ring-primary focus:border-transparent resize-y text-base"
|
||||||
placeholder="Type your answer or explanation here..."
|
placeholder="Type your answer or explanation here..."
|
||||||
@@ -378,16 +418,19 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
|||||||
) : (
|
) : (
|
||||||
currentQuestion.options?.map((option: string, idx: number) => {
|
currentQuestion.options?.map((option: string, idx: number) => {
|
||||||
const letter = String.fromCharCode(65 + idx);
|
const letter = String.fromCharCode(65 + idx);
|
||||||
const isSelected = currentState?.selected_answer === letter;
|
const isSelected = currentQuestion.type === 'Multiple_Choice'
|
||||||
|
? (currentState?.selected_answer || "").split(',').includes(letter)
|
||||||
|
: currentState?.selected_answer === letter;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={() => selectAnswer(currentQuestion.id, letter)}
|
onClick={() => handleOptionSelect(currentQuestion.id, letter, currentQuestion.type)}
|
||||||
className={`w-full text-left p-4 rounded-xl border transition-all duration-200 flex items-center gap-4 group focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent
|
className={`w-full text-left p-4 rounded-xl border transition-all duration-200 flex items-center gap-4 group focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent
|
||||||
${isSelected ? 'border-primary bg-primary/5 shadow-[0_0_15px_-5px_var(--color-primary)]' : 'border-border bg-background hover:border-primary/50 hover:bg-secondary/20'}
|
${isSelected ? 'border-primary bg-primary/5 shadow-[0_0_15px_-5px_var(--color-primary)]' : 'border-border bg-background hover:border-primary/50 hover:bg-secondary/20'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div className={`flex-shrink-0 size-8 rounded-lg border flex items-center justify-center font-medium transition-colors
|
<div className={`flex-shrink-0 size-8 ${currentQuestion.type === 'Multiple_Choice' ? 'rounded-md' : 'rounded-full'} border flex items-center justify-center font-medium transition-colors
|
||||||
${isSelected ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground group-hover:border-primary/50 group-hover:text-primary group-hover:bg-primary/5'}
|
${isSelected ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground group-hover:border-primary/50 group-hover:text-primary group-hover:bg-primary/5'}
|
||||||
`}>
|
`}>
|
||||||
{letter}
|
{letter}
|
||||||
@@ -424,7 +467,15 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3 mt-8">
|
<div className="space-y-3 mt-8">
|
||||||
{(currentQuestion.type === 'Subjective' || currentQuestion.type === 'Numerical') ? (
|
{currentQuestion.type === 'Numerical' ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full p-4 rounded-xl border border-border bg-background focus:ring-2 focus:ring-primary focus:border-transparent text-base"
|
||||||
|
placeholder="अपना संख्यात्मक उत्तर यहाँ टाइप करें..."
|
||||||
|
value={currentState?.selected_answer || ""}
|
||||||
|
onChange={(e) => selectAnswer(currentQuestion.id, e.target.value)}
|
||||||
|
/>
|
||||||
|
) : currentQuestion.type === 'Subjective' ? (
|
||||||
<textarea
|
<textarea
|
||||||
className="w-full min-h-[150px] p-4 rounded-xl border border-border bg-background focus:ring-2 focus:ring-primary focus:border-transparent resize-y text-base"
|
className="w-full min-h-[150px] p-4 rounded-xl border border-border bg-background focus:ring-2 focus:ring-primary focus:border-transparent resize-y text-base"
|
||||||
placeholder="अपना उत्तर या स्पष्टीकरण यहाँ टाइप करें..."
|
placeholder="अपना उत्तर या स्पष्टीकरण यहाँ टाइप करें..."
|
||||||
@@ -434,16 +485,19 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
|||||||
) : (
|
) : (
|
||||||
currentQuestion.options_hi?.map((option: string, idx: number) => {
|
currentQuestion.options_hi?.map((option: string, idx: number) => {
|
||||||
const letter = String.fromCharCode(65 + idx);
|
const letter = String.fromCharCode(65 + idx);
|
||||||
const isSelected = currentState?.selected_answer === letter;
|
const isSelected = currentQuestion.type === 'Multiple_Choice'
|
||||||
|
? (currentState?.selected_answer || "").split(',').includes(letter)
|
||||||
|
: currentState?.selected_answer === letter;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={() => selectAnswer(currentQuestion.id, letter)}
|
onClick={() => handleOptionSelect(currentQuestion.id, letter, currentQuestion.type)}
|
||||||
className={`w-full text-left p-4 rounded-xl border transition-all duration-200 flex items-center gap-4 group focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent
|
className={`w-full text-left p-4 rounded-xl border transition-all duration-200 flex items-center gap-4 group focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent
|
||||||
${isSelected ? 'border-primary bg-primary/5 shadow-[0_0_15px_-5px_var(--color-primary)]' : 'border-border bg-background hover:border-primary/50 hover:bg-secondary/20'}
|
${isSelected ? 'border-primary bg-primary/5 shadow-[0_0_15px_-5px_var(--color-primary)]' : 'border-border bg-background hover:border-primary/50 hover:bg-secondary/20'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div className={`flex-shrink-0 size-8 rounded-lg border flex items-center justify-center font-medium transition-colors
|
<div className={`flex-shrink-0 size-8 ${currentQuestion.type === 'Multiple_Choice' ? 'rounded-md' : 'rounded-full'} border flex items-center justify-center font-medium transition-colors
|
||||||
${isSelected ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground group-hover:border-primary/50 group-hover:text-primary group-hover:bg-primary/5'}
|
${isSelected ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground group-hover:border-primary/50 group-hover:text-primary group-hover:bg-primary/5'}
|
||||||
`}>
|
`}>
|
||||||
{letter}
|
{letter}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export default function CBTPage() {
|
|||||||
const [targetExam, setTargetExam] = useState("");
|
const [targetExam, setTargetExam] = useState("");
|
||||||
const [attempts, setAttempts] = useState<any[]>([]);
|
const [attempts, setAttempts] = useState<any[]>([]);
|
||||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
||||||
|
const [showAllExams, setShowAllExams] = useState(false);
|
||||||
|
|
||||||
const { userId, user, hasAccessTo, loading: userLoading } = useActiveUser();
|
const { userId, user, hasAccessTo, loading: userLoading } = useActiveUser();
|
||||||
|
|
||||||
@@ -111,76 +112,165 @@ export default function CBTPage() {
|
|||||||
No active exams found for {targetExam || "your target exam"}. Try switching it in settings!
|
No active exams found for {targetExam || "your target exam"}. Try switching it in settings!
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="space-y-8">
|
||||||
{filteredExams.map((exam) => {
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
const attempt = attemptsByTest[exam.id];
|
{filteredExams.slice(0, 3).map((exam) => {
|
||||||
const isAttempted = !!attempt;
|
const attempt = attemptsByTest[exam.id];
|
||||||
const isLocked = !hasAccessTo("weekly-test-series");
|
const isAttempted = !!attempt;
|
||||||
|
const isLocked = !hasAccessTo("weekly-test-series");
|
||||||
|
|
||||||
// Date logic
|
// Date logic
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const scheduledTime = exam.scheduled_date ? new Date(exam.scheduled_date) : now;
|
const scheduledTime = exam.scheduled_date ? new Date(exam.scheduled_date) : now;
|
||||||
const isUpcoming = scheduledTime > now;
|
const isUpcoming = scheduledTime > now;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isAttempted ? 'border-primary/20' : ''} ${isLocked ? 'opacity-80' : ''}`}>
|
<Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isAttempted ? 'border-primary/20' : ''} ${isLocked ? 'opacity-80' : ''}`}>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="flex justify-between items-start mb-4">
|
<div className="flex justify-between items-start mb-4">
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
<Badge variant="outline" className="bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20">
|
<Badge variant="outline" className="bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20">
|
||||||
{exam.type} Test
|
{exam.type} Test
|
||||||
</Badge>
|
|
||||||
{exam.exam_pattern && (
|
|
||||||
<Badge variant="outline" className="bg-primary/20 text-primary border-primary/30 uppercase tracking-wider text-[10px]">
|
|
||||||
{exam.exam_pattern === 'JEE_Main' ? 'JEE Main' : exam.exam_pattern === 'JEE_Advanced' ? 'JEE Advanced' : exam.exam_pattern}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
{exam.exam_pattern && (
|
||||||
|
<Badge variant="outline" className="bg-primary/20 text-primary border-primary/30 uppercase tracking-wider text-[10px]">
|
||||||
|
{exam.exam_pattern === 'JEE_Main' ? 'JEE Main' : exam.exam_pattern === 'JEE_Advanced' ? 'JEE Advanced' : exam.exam_pattern}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isUpcoming ? (
|
||||||
|
<Badge variant="secondary" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">
|
||||||
|
Upcoming
|
||||||
|
</Badge>
|
||||||
|
) : isLocked ? (
|
||||||
|
<Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20 flex items-center gap-1">
|
||||||
|
<Lock size={12} /> Premium
|
||||||
|
</Badge>
|
||||||
|
) : isAttempted ? (
|
||||||
|
<Badge variant="secondary" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||||
|
Attempted
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{isUpcoming ? (
|
<h3 className="font-bold text-lg leading-tight mb-2 group-hover:text-[#00F0FF] transition-colors">
|
||||||
<Badge variant="secondary" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">
|
{exam.title}
|
||||||
Upcoming
|
</h3>
|
||||||
</Badge>
|
<div className="text-sm text-muted-foreground space-y-1 mb-6">
|
||||||
) : isLocked ? (
|
<div className="font-medium text-foreground">
|
||||||
<Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20 flex items-center gap-1">
|
{isUpcoming ? `Available on ${formatDate(exam.scheduled_date!)}` : 'Available Now'}
|
||||||
<Lock size={12} /> Premium
|
</div>
|
||||||
</Badge>
|
<div>{exam.total_time} Min • {exam.difficulty} Difficulty</div>
|
||||||
) : isAttempted ? (
|
|
||||||
<Badge variant="secondary" className="bg-green-500/10 text-green-500 border-green-500/20">
|
|
||||||
Attempted
|
|
||||||
</Badge>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<h3 className="font-bold text-lg leading-tight mb-2 group-hover:text-[#00F0FF] transition-colors">
|
|
||||||
{exam.title}
|
|
||||||
</h3>
|
|
||||||
<div className="text-sm text-muted-foreground space-y-1 mb-6">
|
|
||||||
<div className="font-medium text-foreground">
|
|
||||||
{isUpcoming ? `Available on ${formatDate(exam.scheduled_date!)}` : 'Available Now'}
|
|
||||||
</div>
|
</div>
|
||||||
<div>{exam.total_time} Min • {exam.difficulty} Difficulty</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isUpcoming ? (
|
{isUpcoming ? (
|
||||||
<Button disabled className="w-full gap-2 rounded-xl bg-secondary text-muted-foreground font-bold">
|
<Button disabled className="w-full gap-2 rounded-xl bg-secondary text-muted-foreground font-bold">
|
||||||
<Clock size={16} /> Starts Soon
|
<Clock size={16} /> Starts Soon
|
||||||
</Button>
|
|
||||||
) : isLocked ? (
|
|
||||||
<Button
|
|
||||||
onClick={() => setShowUpgradeModal(true)}
|
|
||||||
className="w-full gap-2 rounded-xl bg-secondary text-muted-foreground border border-dashed border-border hover:border-[#00F0FF]/50 hover:text-foreground font-bold"
|
|
||||||
>
|
|
||||||
<Lock size={16} /> Unlock Test
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Link href={`/cbt/${exam.id}`}>
|
|
||||||
<Button className={`w-full gap-2 rounded-xl transition-colors font-bold ${isAttempted ? 'bg-secondary border border-border text-foreground' : 'bg-secondary text-foreground hover:bg-[#00F0FF] hover:text-background'}`}>
|
|
||||||
<PlayCircle size={16} /> {isAttempted ? 'Retake Exam' : 'Start Exam'}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
) : isLocked ? (
|
||||||
)}
|
<Button
|
||||||
</CardContent>
|
onClick={() => setShowUpgradeModal(true)}
|
||||||
</Card>
|
className="w-full gap-2 rounded-xl bg-secondary text-muted-foreground border border-dashed border-border hover:border-[#00F0FF]/50 hover:text-foreground font-bold"
|
||||||
)})}
|
>
|
||||||
|
<Lock size={16} /> Unlock Test
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Link href={isAttempted ? `/cbt/${exam.id}/result` : `/cbt/${exam.id}`}>
|
||||||
|
<Button className={`w-full gap-2 rounded-xl transition-colors font-bold ${isAttempted ? 'bg-secondary border border-border text-foreground hover:bg-secondary/80' : 'bg-secondary text-foreground hover:bg-[#00F0FF] hover:text-background'}`}>
|
||||||
|
<PlayCircle size={16} /> {isAttempted ? 'View Result' : 'Start Exam'}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredExams.length > 3 && !showAllExams && (
|
||||||
|
<div className="flex justify-center mt-6">
|
||||||
|
<Button variant="outline" onClick={() => setShowAllExams(true)} className="rounded-xl border-dashed">
|
||||||
|
Show More Earlier Exams ({filteredExams.length - 3})
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showAllExams && filteredExams.length > 3 && (
|
||||||
|
<div className="space-y-6 pt-4 animate-in fade-in duration-500">
|
||||||
|
<h3 className="text-lg font-bold border-b border-border pb-2 flex items-center gap-2">
|
||||||
|
<Clock size={18} className="text-muted-foreground" /> Earlier Exams
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{filteredExams.slice(3).map((exam) => {
|
||||||
|
const attempt = attemptsByTest[exam.id];
|
||||||
|
const isAttempted = !!attempt;
|
||||||
|
const isLocked = !hasAccessTo("weekly-test-series");
|
||||||
|
|
||||||
|
// Date logic
|
||||||
|
const now = new Date();
|
||||||
|
const scheduledTime = exam.scheduled_date ? new Date(exam.scheduled_date) : now;
|
||||||
|
const isUpcoming = scheduledTime > now;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isAttempted ? 'border-primary/20' : ''} ${isLocked ? 'opacity-80' : ''}`}>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex justify-between items-start mb-4">
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<Badge variant="outline" className="bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20">
|
||||||
|
{exam.type} Test
|
||||||
|
</Badge>
|
||||||
|
{exam.exam_pattern && (
|
||||||
|
<Badge variant="outline" className="bg-primary/20 text-primary border-primary/30 uppercase tracking-wider text-[10px]">
|
||||||
|
{exam.exam_pattern === 'JEE_Main' ? 'JEE Main' : exam.exam_pattern === 'JEE_Advanced' ? 'JEE Advanced' : exam.exam_pattern}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isUpcoming ? (
|
||||||
|
<Badge variant="secondary" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">
|
||||||
|
Upcoming
|
||||||
|
</Badge>
|
||||||
|
) : isLocked ? (
|
||||||
|
<Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20 flex items-center gap-1">
|
||||||
|
<Lock size={12} /> Premium
|
||||||
|
</Badge>
|
||||||
|
) : isAttempted ? (
|
||||||
|
<Badge variant="secondary" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||||
|
Attempted
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold text-lg leading-tight mb-2 group-hover:text-[#00F0FF] transition-colors">
|
||||||
|
{exam.title}
|
||||||
|
</h3>
|
||||||
|
<div className="text-sm text-muted-foreground space-y-1 mb-6">
|
||||||
|
<div className="font-medium text-foreground">
|
||||||
|
{isUpcoming ? `Available on ${formatDate(exam.scheduled_date!)}` : 'Available Now'}
|
||||||
|
</div>
|
||||||
|
<div>{exam.total_time} Min • {exam.difficulty} Difficulty</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isUpcoming ? (
|
||||||
|
<Button disabled className="w-full gap-2 rounded-xl bg-secondary text-muted-foreground font-bold">
|
||||||
|
<Clock size={16} /> Starts Soon
|
||||||
|
</Button>
|
||||||
|
) : isLocked ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowUpgradeModal(true)}
|
||||||
|
className="w-full gap-2 rounded-xl bg-secondary text-muted-foreground border border-dashed border-border hover:border-[#00F0FF]/50 hover:text-foreground font-bold"
|
||||||
|
>
|
||||||
|
<Lock size={16} /> Unlock Test
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Link href={isAttempted ? `/cbt/${exam.id}/result` : `/cbt/${exam.id}`}>
|
||||||
|
<Button className={`w-full gap-2 rounded-xl transition-colors font-bold ${isAttempted ? 'bg-secondary border border-border text-foreground hover:bg-secondary/80' : 'bg-secondary text-foreground hover:bg-[#00F0FF] hover:text-background'}`}>
|
||||||
|
<PlayCircle size={16} /> {isAttempted ? 'View Result' : 'Start Exam'}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export default function CommunityPage() {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!hasAccessTo("ask-doubts-from-tutors")) {
|
if (!hasAccessTo("doubt-resolution-community")) {
|
||||||
return (
|
return (
|
||||||
<div className="p-8 h-full flex flex-col items-center justify-center text-center animate-in fade-in zoom-in duration-500 max-w-2xl mx-auto">
|
<div className="p-8 h-full flex flex-col items-center justify-center text-center animate-in fade-in zoom-in duration-500 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">
|
<div className="size-20 rounded-full bg-red-500/10 flex items-center justify-center mx-auto text-red-500 mb-6">
|
||||||
|
|||||||
@@ -52,9 +52,6 @@ export default function DPPPage() {
|
|||||||
Hyper-personalized problem sets generated daily by our AI Evaluator to target your specific weak areas based on past exam performance.
|
Hyper-personalized problem sets generated daily by our AI Evaluator to target your specific weak areas based on past exam performance.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button size="lg" className="h-14 px-8 rounded-xl bg-[#00F0FF] text-background hover:bg-[#00F0FF]/90 font-bold shadow-[0_0_20px_-5px_#00F0FF] shrink-0">
|
|
||||||
<BrainCircuit className="mr-2" /> Generate Custom DPP
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row justify-between items-center gap-4 border-b border-border pb-6">
|
<div className="flex flex-col sm:flex-row justify-between items-center gap-4 border-b border-border pb-6">
|
||||||
|
|||||||
@@ -35,6 +35,17 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
const userInitials = user ? user.name.split(" ").filter(n => /^[a-zA-Z]/.test(n)).slice(0, 2).map(n => n[0]).join("").toUpperCase() : "DS"
|
const userInitials = user ? user.name.split(" ").filter(n => /^[a-zA-Z]/.test(n)).slice(0, 2).map(n => n[0]).join("").toUpperCase() : "DS"
|
||||||
const targetExamLabel = user?.target_exam === "JEE_Main" ? "JEE Main" : user?.target_exam === "JEE_Advanced" ? "JEE Advanced" : user?.target_exam === "NEET" ? "NEET" : "NEET/JEE"
|
const targetExamLabel = user?.target_exam === "JEE_Main" ? "JEE Main" : user?.target_exam === "JEE_Advanced" ? "JEE Advanced" : user?.target_exam === "NEET" ? "NEET" : "NEET/JEE"
|
||||||
|
|
||||||
|
let daysLeft = null;
|
||||||
|
if (user && user.created_at) {
|
||||||
|
const createdDate = new Date(user.created_at);
|
||||||
|
const msPerDay = 1000 * 60 * 60 * 24;
|
||||||
|
const daysElapsed = Math.floor((new Date().getTime() - createdDate.getTime()) / msPerDay);
|
||||||
|
daysLeft = Math.max(0, 15 - daysElapsed);
|
||||||
|
} else if (user && user.tier === 'Free') {
|
||||||
|
// Mock value for demo Free tier if no date
|
||||||
|
daysLeft = 14;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-background overflow-hidden text-foreground selection:bg-primary/20">
|
<div className="flex h-screen bg-background overflow-hidden text-foreground selection:bg-primary/20">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
@@ -43,7 +54,22 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
<Logo href="/dashboard" className="w-full" imageClassName="object-contain object-left scale-110 origin-left" height={56} />
|
<Logo href="/dashboard" className="w-full" imageClassName="object-contain object-left scale-110 origin-left" height={56} />
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex-1 px-4 space-y-1.5 mt-4 overflow-y-auto">
|
<nav className="flex-1 px-4 space-y-1.5 mt-4 overflow-y-auto">
|
||||||
|
<div className="pt-2 pb-2">
|
||||||
|
<p className="px-3 text-xs font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-2">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-white"></span> Free Tier
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<NavItem href="/dashboard" icon={<LayoutDashboard size={18} />} label="Overview" active={pathname === "/dashboard"} />
|
<NavItem href="/dashboard" icon={<LayoutDashboard size={18} />} label="Overview" active={pathname === "/dashboard"} />
|
||||||
|
<NavItem href="/dashboard/dpp" icon={<FileText size={18} />} label="Daily Practice" active={pathname.startsWith("/dashboard/dpp")} />
|
||||||
|
<NavItem href="/dashboard/materials" icon={<BookOpen size={18} />} label="Study Materials" active={pathname.startsWith("/dashboard/materials")} />
|
||||||
|
<NavItem href="/dashboard/progress" icon={<Activity size={18} />} label="Syllabus Tracker" active={pathname.startsWith("/dashboard/progress")} />
|
||||||
|
<NavItem href="/dashboard/leaderboard" icon={<Trophy size={18} />} label="Leaderboard" active={pathname.startsWith("/dashboard/leaderboard")} />
|
||||||
|
|
||||||
|
<div className="pt-4 pb-2">
|
||||||
|
<p className="px-3 text-xs font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-2">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-yellow-500 shadow-[0_0_8px_-2px_var(--color-yellow-500)]"></span> Pro Tier
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<NavItem
|
<NavItem
|
||||||
href="/dashboard/cbt"
|
href="/dashboard/cbt"
|
||||||
icon={<Activity size={18} />}
|
icon={<Activity size={18} />}
|
||||||
@@ -51,25 +77,6 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
active={pathname.startsWith("/dashboard/cbt")}
|
active={pathname.startsWith("/dashboard/cbt")}
|
||||||
isLocked={!hasAccessTo("weekly-test-series")}
|
isLocked={!hasAccessTo("weekly-test-series")}
|
||||||
/>
|
/>
|
||||||
<NavItem href="/dashboard/dpp" icon={<FileText size={18} />} label="Daily Practice" active={pathname.startsWith("/dashboard/dpp")} />
|
|
||||||
<NavItem href="/dashboard/materials" icon={<BookOpen size={18} />} label="Study Materials" active={pathname.startsWith("/dashboard/materials")} />
|
|
||||||
|
|
||||||
<div className="pt-4 pb-2">
|
|
||||||
<p className="px-3 text-xs font-bold uppercase tracking-widest text-muted-foreground">Community & Tracking</p>
|
|
||||||
</div>
|
|
||||||
<NavItem href="/dashboard/leaderboard" icon={<Trophy size={18} />} label="Leaderboard" active={pathname.startsWith("/dashboard/leaderboard")} />
|
|
||||||
<NavItem href="/dashboard/progress" icon={<Activity size={18} />} label="Syllabus Tracker" active={pathname.startsWith("/dashboard/progress")} />
|
|
||||||
<NavItem
|
|
||||||
href="/dashboard/community"
|
|
||||||
icon={<BookOpen size={18} />}
|
|
||||||
label="Doubt Forum"
|
|
||||||
active={pathname.startsWith("/dashboard/community")}
|
|
||||||
isLocked={!hasAccessTo("ask-doubts-from-tutors")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="pt-4 pb-2">
|
|
||||||
<p className="px-3 text-xs font-bold uppercase tracking-widest text-muted-foreground">AI Intelligence</p>
|
|
||||||
</div>
|
|
||||||
<NavItem
|
<NavItem
|
||||||
href="/dashboard/analytics"
|
href="/dashboard/analytics"
|
||||||
icon={<LineChart size={18} />}
|
icon={<LineChart size={18} />}
|
||||||
@@ -84,6 +91,19 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
active={pathname.startsWith("/dashboard/rank-predictor")}
|
active={pathname.startsWith("/dashboard/rank-predictor")}
|
||||||
isLocked={!hasAccessTo("college-predictor")}
|
isLocked={!hasAccessTo("college-predictor")}
|
||||||
/>
|
/>
|
||||||
|
<NavItem
|
||||||
|
href="/dashboard/community"
|
||||||
|
icon={<BookOpen size={18} />}
|
||||||
|
label="Doubt Forum"
|
||||||
|
active={pathname.startsWith("/dashboard/community")}
|
||||||
|
isLocked={!hasAccessTo("ask-doubts-from-tutors")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="pt-4 pb-2">
|
||||||
|
<p className="px-3 text-xs font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-2">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-[#00F0FF] shadow-[0_0_8px_-2px_#00F0FF]"></span> AI Coach
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<NavItem
|
<NavItem
|
||||||
href="/dashboard/ai-coach"
|
href="/dashboard/ai-coach"
|
||||||
icon={<BrainCircuitIcon />}
|
icon={<BrainCircuitIcon />}
|
||||||
@@ -160,6 +180,11 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
{targetExamLabel} Target
|
{targetExamLabel} Target
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
{daysLeft !== null && user?.tier !== 'Al_Coach' && user?.tier !== 'Pro' && (
|
||||||
|
<Badge variant="outline" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20 font-bold px-2.5 py-0.5">
|
||||||
|
{daysLeft} Days Left in AI Coach Trial
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -185,7 +210,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 border-l border-border pl-4">
|
<div className="flex items-center gap-2 border-l border-border pl-4">
|
||||||
<LanguageToggle />
|
{/* <LanguageToggle /> */}
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,10 @@ export default function DashboardPage() {
|
|||||||
{/* Basic Performance Dashboard (F-05) */}
|
{/* Basic Performance Dashboard (F-05) */}
|
||||||
<section>
|
<section>
|
||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
<h2 className="text-xl font-semibold tracking-tight">Performance Overview</h2>
|
<h2 className="text-xl font-semibold tracking-tight flex items-center gap-2">
|
||||||
|
Performance Overview
|
||||||
|
<Badge variant="outline" className="border-white/30 text-muted-foreground bg-white/5 rounded-md text-[10px] uppercase font-bold">Free</Badge>
|
||||||
|
</h2>
|
||||||
<Button onClick={handleExport} variant="outline" size="sm" className="gap-2 font-medium">
|
<Button onClick={handleExport} variant="outline" size="sm" className="gap-2 font-medium">
|
||||||
<Share2 size={16} /> Share Card
|
<Share2 size={16} /> Share Card
|
||||||
</Button>
|
</Button>
|
||||||
@@ -109,7 +112,11 @@ export default function DashboardPage() {
|
|||||||
{/* Dynamic DPP Modules */}
|
{/* Dynamic DPP Modules */}
|
||||||
<section>
|
<section>
|
||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
<h2 className="text-xl font-semibold tracking-tight">Today's DPPs</h2>
|
<h2 className="text-xl font-semibold tracking-tight flex items-center gap-2">
|
||||||
|
Today's DPPs
|
||||||
|
<Badge variant="outline" className="border-white/30 text-muted-foreground bg-white/5 rounded-md text-[10px] uppercase font-bold">Free (1/Day)</Badge>
|
||||||
|
<Badge variant="outline" className="border-yellow-500/30 text-yellow-500 bg-yellow-500/5 rounded-md text-[10px] uppercase font-bold">Pro (Unlimited)</Badge>
|
||||||
|
</h2>
|
||||||
<Button variant="ghost" size="sm" className="text-muted-foreground font-medium">View All</Button>
|
<Button variant="ghost" size="sm" className="text-muted-foreground font-medium">View All</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -171,7 +178,10 @@ export default function DashboardPage() {
|
|||||||
{/* Upcoming CBTs */}
|
{/* Upcoming CBTs */}
|
||||||
<section>
|
<section>
|
||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
<h2 className="text-xl font-semibold tracking-tight">Upcoming CBTs</h2>
|
<h2 className="text-xl font-semibold tracking-tight flex items-center gap-2">
|
||||||
|
Upcoming CBTs
|
||||||
|
<Badge variant="outline" className="border-yellow-500/30 text-yellow-500 bg-yellow-500/5 rounded-md text-[10px] uppercase font-bold">Pro</Badge>
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<Card className="overflow-hidden">
|
<Card className="overflow-hidden">
|
||||||
<div className="p-6 border-b border-border flex items-center justify-between bg-background">
|
<div className="p-6 border-b border-border flex items-center justify-between bg-background">
|
||||||
|
|||||||
@@ -422,12 +422,12 @@ export default function MarketingPage() {
|
|||||||
<p className="text-muted-foreground mb-8 text-lg">Perfect to get started.</p>
|
<p className="text-muted-foreground mb-8 text-lg">Perfect to get started.</p>
|
||||||
<div className="text-6xl font-bold tracking-tighter mb-10">₹0<span className="text-xl text-muted-foreground font-medium">/yr</span></div>
|
<div className="text-6xl font-bold tracking-tighter mb-10">₹0<span className="text-xl text-muted-foreground font-medium">/yr</span></div>
|
||||||
<ul className="space-y-5 mb-10 flex-1 font-medium text-lg">
|
<ul className="space-y-5 mb-10 flex-1 font-medium text-lg">
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-muted-foreground" /> 1 Daily Practice Sheet</li>
|
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-white" /> 1 Daily Practice Sheet</li>
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-muted-foreground" /> Basic Performance Analytics</li>
|
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-white" /> Basic Performance Analytics</li>
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-muted-foreground" /> Community Access</li>
|
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-white" /> Community Access</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Link href="/signup">
|
<Link href="/signup">
|
||||||
<Button variant="outline" size="lg" className="w-full rounded-2xl h-16 border-border font-bold text-lg hover:bg-secondary hover:text-foreground">Get Started Free</Button>
|
<Button variant="outline" size="lg" className="w-full rounded-2xl h-16 border-white text-white font-bold text-lg hover:bg-secondary hover:text-white">Get Started Free</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -442,47 +442,44 @@ export default function MarketingPage() {
|
|||||||
<Card className="bg-background/80 backdrop-blur-md border-border p-8 rounded-[2.5rem] flex flex-col hover:border-border/80 transition-colors h-full z-10 relative shadow-xl">
|
<Card className="bg-background/80 backdrop-blur-md border-border p-8 rounded-[2.5rem] flex flex-col hover:border-border/80 transition-colors h-full z-10 relative shadow-xl">
|
||||||
<h3 className="text-3xl font-bold mb-2">Pro</h3>
|
<h3 className="text-3xl font-bold mb-2">Pro</h3>
|
||||||
<p className="text-muted-foreground mb-8 text-lg">Perfect for self-paced practice.</p>
|
<p className="text-muted-foreground mb-8 text-lg">Perfect for self-paced practice.</p>
|
||||||
<div className="text-6xl font-bold tracking-tighter mb-10">₹2,499<span className="text-xl text-muted-foreground font-medium">/yr</span></div>
|
<div className="text-6xl font-bold tracking-tighter mb-10">₹12,499<span className="text-xl text-muted-foreground font-medium">/yr</span></div>
|
||||||
<ul className="space-y-5 mb-10 flex-1 font-medium text-lg">
|
<ul className="space-y-5 mb-10 flex-1 font-medium text-lg">
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-primary" /> Unlimited Daily Practice Sheets</li>
|
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-yellow-500" /> Unlimited Daily Practice Sheets</li>
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-primary" /> Weekly Test Series & Mock Tests</li>
|
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-yellow-500" /> Weekly Test Series & Mock Tests</li>
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-primary" /> Detailed Performance Analytics</li>
|
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-yellow-500" /> Detailed Performance Analytics</li>
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-primary" /> Parent Dashboard Access</li>
|
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-yellow-500" /> Parent Dashboard Access</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Link href="/signup">
|
<Link href="/signup">
|
||||||
<Button variant="outline" size="lg" className="w-full rounded-2xl h-16 border-border font-bold text-lg hover:bg-secondary hover:text-foreground">Choose Pro</Button>
|
<Button size="lg" className="w-full rounded-2xl h-16 bg-yellow-500 text-black font-bold text-lg hover:bg-yellow-400">Choose Pro</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* AI Coach Plan */}
|
{/* AI Coach Plan */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: 50 }}
|
initial={{ opacity: 0, x: 50 }}
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.6, type: "spring", delay: 0.2 }}
|
transition={{ duration: 0.6, type: "spring", delay: 0.2 }}
|
||||||
className="relative mt-8 lg:mt-0"
|
className="relative mt-8 lg:mt-0"
|
||||||
>
|
>
|
||||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2 z-20">
|
<Card className="bg-secondary/30 backdrop-blur-xl border-[#00F0FF] p-8 rounded-[2.5rem] flex flex-col transition-all h-full relative">
|
||||||
<Badge className="bg-primary text-primary-foreground font-extrabold px-6 py-2 text-sm rounded-full shadow-[0_0_20px_-2px_var(--color-primary)]">MOST POPULAR</Badge>
|
<div className="absolute inset-0 rounded-[2.5rem] overflow-hidden">
|
||||||
</div>
|
|
||||||
<Card className="bg-secondary/30 backdrop-blur-xl border-primary p-8 rounded-[2.5rem] flex flex-col shadow-[0_0_60px_-15px_var(--color-primary)] hover:shadow-[0_0_80px_-15px_var(--color-primary)] transition-all h-full relative">
|
|
||||||
<div className="absolute inset-0 rounded-[2.5rem] overflow-hidden">
|
|
||||||
<div className="absolute inset-0 -translate-x-full animate-shine bg-gradient-to-r from-transparent via-white/10 to-transparent z-0 pointer-events-none"></div>
|
<div className="absolute inset-0 -translate-x-full animate-shine bg-gradient-to-r from-transparent via-white/10 to-transparent z-0 pointer-events-none"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative z-10 flex flex-col h-full mt-4">
|
<div className="relative z-10 flex flex-col h-full mt-4">
|
||||||
<h3 className="text-3xl font-bold mb-2">AI Coach</h3>
|
<h3 className="text-3xl font-bold mb-2">AI Coach</h3>
|
||||||
<p className="text-muted-foreground mb-8 text-lg">The complete AI-driven system.</p>
|
<p className="text-muted-foreground mb-8 text-lg">The complete AI-driven system.</p>
|
||||||
<div className="text-6xl font-bold tracking-tighter mb-10 text-primary drop-shadow-[0_0_10px_var(--color-primary)]">₹4,999<span className="text-xl text-foreground font-medium">/yr</span></div>
|
<div className="text-6xl font-bold tracking-tighter mb-10 text-[#00F0FF] drop-shadow-[0_0_10px_rgba(0,240,255,0.5)]">₹4,999<span className="text-xl text-foreground font-medium">/yr</span></div>
|
||||||
<ul className="space-y-5 mb-10 flex-1 font-medium text-lg">
|
<ul className="space-y-5 mb-10 flex-1 font-medium text-lg">
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> Everything in Pro</li>
|
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> Everything in Pro</li>
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> AI Spaced Repetition (Mistakes)</li>
|
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> AI Spaced Repetition (Mistakes)</li>
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> 24/7 Streaming AI Tutor Chat</li>
|
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> 24/7 Streaming AI Tutor Chat</li>
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> Predictive Rank & College Predictor</li>
|
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> Predictive Rank & College Predictor</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Link href="/signup">
|
<Link href="/signup">
|
||||||
<Button size="lg" className="w-full rounded-2xl h-16 font-bold text-lg bg-primary text-primary-foreground hover:bg-primary/90 shadow-[0_0_20px_-5px_var(--color-primary)] transition-all">Get AI Coach</Button>
|
<Button size="lg" className="w-full rounded-2xl h-16 font-bold text-lg bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 transition-all">Get AI Coach</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -496,14 +493,14 @@ export default function MarketingPage() {
|
|||||||
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">See exactly what you get with each PragatiVeer plan.</p>
|
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">See exactly what you get with each PragatiVeer plan.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto rounded-2xl border border-border/50 bg-secondary/10 backdrop-blur-sm shadow-xl">
|
<div className="overflow-x-auto rounded-2xl border border-border/50 bg-secondary/10 backdrop-blur-sm">
|
||||||
<table className="w-full text-left border-collapse min-w-[700px]">
|
<table className="w-full text-left border-collapse min-w-[700px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-border/50 bg-background/50">
|
<tr className="border-b border-border/50 bg-background/50">
|
||||||
<th className="p-4 md:p-6 font-bold text-lg w-2/5">Feature</th>
|
<th className="p-4 md:p-6 font-bold text-lg w-2/5">Feature</th>
|
||||||
<th className="p-4 md:p-6 font-bold text-lg text-center w-1/5">Free</th>
|
<th className="p-4 md:p-6 font-bold text-lg text-center w-1/5 border-x border-white">Free</th>
|
||||||
<th className="p-4 md:p-6 font-bold text-lg text-center w-1/5 text-primary drop-shadow-[0_0_5px_var(--color-primary)]">Pro</th>
|
<th className="p-4 md:p-6 font-bold text-lg text-center w-1/5 text-yellow-500 border-x border-yellow-500">Pro</th>
|
||||||
<th className="p-4 md:p-6 font-bold text-lg text-center w-1/5 text-[#00F0FF] drop-shadow-[0_0_5px_rgba(0,240,255,0.5)]">AI Coach</th>
|
<th className="p-4 md:p-6 font-bold text-lg text-center w-1/5 text-[#00F0FF] border-x border-[#00F0FF]">AI Coach</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-border/30 text-sm md:text-base">
|
<tbody className="divide-y divide-border/30 text-sm md:text-base">
|
||||||
@@ -532,15 +529,15 @@ export default function MarketingPage() {
|
|||||||
{ name: "Study Time Optimization", free: false, pro: false, ai: true },
|
{ name: "Study Time Optimization", free: false, pro: false, ai: true },
|
||||||
].map((feature, idx) => (
|
].map((feature, idx) => (
|
||||||
<tr key={idx} className="hover:bg-background/40 transition-colors">
|
<tr key={idx} className="hover:bg-background/40 transition-colors">
|
||||||
<td className="p-4 md:p-6 font-medium">{feature.name}</td>
|
<td className="p-4 md:p-6 font-medium border-y border-border/30">{feature.name}</td>
|
||||||
<td className="p-4 md:p-6 text-center text-muted-foreground">
|
<td className="p-4 md:p-6 text-center text-muted-foreground border border-white">
|
||||||
{typeof feature.free === 'boolean' ? (feature.free ? <CheckCircle2 className="mx-auto text-muted-foreground" size={20} /> : <Minus className="mx-auto opacity-30" size={20} />) : <span className="font-semibold">{feature.free}</span>}
|
{typeof feature.free === 'boolean' ? (feature.free ? <CheckCircle2 className="mx-auto text-white" size={20} /> : <Minus className="mx-auto text-white/30" size={20} />) : <span className="font-semibold text-white">{feature.free}</span>}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4 md:p-6 text-center">
|
<td className="p-4 md:p-6 text-center border border-yellow-500">
|
||||||
{typeof feature.pro === 'boolean' ? (feature.pro ? <CheckCircle2 className="mx-auto text-primary" size={20} /> : <Minus className="mx-auto opacity-30" size={20} />) : <span className="font-semibold text-primary">{feature.pro}</span>}
|
{typeof feature.pro === 'boolean' ? (feature.pro ? <CheckCircle2 className="mx-auto text-yellow-500" size={20} /> : <Minus className="mx-auto text-yellow-500/30" size={20} />) : <span className="font-semibold text-yellow-500">{feature.pro}</span>}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4 md:p-6 text-center bg-[#00F0FF]/5">
|
<td className="p-4 md:p-6 text-center bg-[#00F0FF]/5 border border-[#00F0FF]">
|
||||||
{typeof feature.ai === 'boolean' ? (feature.ai ? <CheckCircle2 className="mx-auto text-[#00F0FF]" size={20} /> : <Minus className="mx-auto opacity-30" size={20} />) : <span className="font-semibold text-[#00F0FF]">{feature.ai}</span>}
|
{typeof feature.ai === 'boolean' ? (feature.ai ? <CheckCircle2 className="mx-auto text-[#00F0FF]" size={20} /> : <Minus className="mx-auto text-[#00F0FF]/30" size={20} />) : <span className="font-semibold text-[#00F0FF]">{feature.ai}</span>}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export default function ParentDashboard() {
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground mt-1 font-medium">{t.subtitle} — {data?.student_name}</p>
|
<p className="text-muted-foreground mt-1 font-medium">{t.subtitle} — {data?.student_name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{/*
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Label htmlFor="lang-switch" className="font-bold text-muted-foreground">{lang === 'hi' ? 'English' : 'हिंदी'}</Label>
|
<Label htmlFor="lang-switch" className="font-bold text-muted-foreground">{lang === 'hi' ? 'English' : 'हिंदी'}</Label>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -121,6 +122,7 @@ export default function ParentDashboard() {
|
|||||||
onCheckedChange={(c) => setLang(c ? 'hi' : 'en')}
|
onCheckedChange={(c) => setLang(c ? 'hi' : 'en')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
*/}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Metric Blocks */}
|
{/* Metric Blocks */}
|
||||||
|
|||||||
@@ -105,43 +105,42 @@ export default function UpgradePage() {
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 items-start">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 items-start">
|
||||||
{/* Free Tier */}
|
{/* Free Tier */}
|
||||||
<Card className="border-border">
|
<Card className="bg-background/80 backdrop-blur-md border-white p-8 rounded-[2.5rem] flex flex-col h-full z-10 relative">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl">Free</CardTitle>
|
<CardTitle className="text-2xl">Free</CardTitle>
|
||||||
<CardDescription>Basic access for everyone.</CardDescription>
|
<CardDescription>Basic access for everyone.</CardDescription>
|
||||||
<div className="mt-4 font-black text-4xl">₹0<span className="text-xl text-muted-foreground font-normal">/mo</span></div>
|
<div className="mt-4 font-black text-4xl">₹10<span className="text-xl text-muted-foreground font-normal">/mo</span></div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4 text-sm font-medium">
|
<CardContent className="space-y-4 text-sm font-medium">
|
||||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Subject-wise diagnostics</div>
|
<div className="flex items-center gap-3"><Check className="text-white" size={18}/> Subject-wise diagnostics</div>
|
||||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Basic progress tracking</div>
|
<div className="flex items-center gap-3"><Check className="text-white" size={18}/> Basic progress tracking</div>
|
||||||
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><Minus size={18}/> Tests</div>
|
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><Minus size={18}/> Tests</div>
|
||||||
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><Minus size={18}/> AI Spaced Repetition</div>
|
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><Minus size={18}/> AI Spaced Repetition</div>
|
||||||
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><Minus size={18}/> Parent Analytics</div>
|
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><Minus size={18}/> Parent Analytics</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
<Button variant="outline" className="w-full" disabled>Current Plan</Button>
|
<Button variant="outline" className="w-full rounded-2xl h-16 border-white text-white font-bold text-lg hover:bg-secondary hover:text-white" disabled>Current Plan</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Pro Tier */}
|
{/* Pro Tier */}
|
||||||
<Card className="border-border border-primary shadow-lg shadow-primary/10 relative">
|
<Card className="bg-background/80 backdrop-blur-md border-yellow-500 p-8 rounded-[2.5rem] flex flex-col h-full z-10 relative">
|
||||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-primary text-primary-foreground px-3 py-1 rounded-full text-xs font-bold tracking-wider uppercase">Most Popular</div>
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl text-primary">Pro</CardTitle>
|
<CardTitle className="text-2xl text-yellow-500">Pro</CardTitle>
|
||||||
<CardDescription>For serious aspirants.</CardDescription>
|
<CardDescription>For serious aspirants.</CardDescription>
|
||||||
<div className="mt-4 font-black text-4xl">₹299<span className="text-xl text-muted-foreground font-normal">/mo</span></div>
|
<div className="mt-4 font-black text-4xl">₹1299<span className="text-xl text-muted-foreground font-normal">/mo</span></div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4 text-sm font-medium">
|
<CardContent className="space-y-4 text-sm font-medium">
|
||||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Everything in Free</div>
|
<div className="flex items-center gap-3"><Check className="text-yellow-500" size={18}/> Everything in Free</div>
|
||||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Unlimited Sectional Tests</div>
|
<div className="flex items-center gap-3"><Check className="text-yellow-500" size={18}/> Unlimited Sectional Tests</div>
|
||||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Performance Analytics</div>
|
<div className="flex items-center gap-3"><Check className="text-yellow-500" size={18}/> Performance Analytics</div>
|
||||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Parent Dashboard</div>
|
<div className="flex items-center gap-3"><Check className="text-yellow-500" size={18}/> Parent Dashboard</div>
|
||||||
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><Minus size={18}/> AI Mistake Heatmap</div>
|
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><Minus size={18}/> AI Mistake Heatmap</div>
|
||||||
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><Minus size={18}/> Spaced Repetition (SM-2)</div>
|
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><Minus size={18}/> Spaced Repetition (SM-2)</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full bg-yellow-500 text-black hover:bg-yellow-400 font-bold text-lg rounded-2xl h-16"
|
||||||
onClick={() => handleUpgrade("Pro")}
|
onClick={() => handleUpgrade("Pro")}
|
||||||
disabled={!!loadingTier}
|
disabled={!!loadingTier}
|
||||||
>
|
>
|
||||||
@@ -151,23 +150,22 @@ export default function UpgradePage() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* AI Coach Tier */}
|
{/* AI Coach Tier */}
|
||||||
<Card className="border-border bg-gradient-to-b from-primary/10 to-background border-primary">
|
<Card className="bg-secondary/30 backdrop-blur-xl border-[#00F0FF] p-8 rounded-[2.5rem] flex flex-col transition-all h-full relative">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl flex items-center gap-2">AI Coach <ShieldCheck size={24} className="text-primary"/></CardTitle>
|
<CardTitle className="text-2xl flex items-center gap-2 text-[#00F0FF]">AI Coach <ShieldCheck size={24} className="text-[#00F0FF]"/></CardTitle>
|
||||||
<CardDescription>Personalized mastery loop.</CardDescription>
|
<CardDescription>Personalized mastery loop.</CardDescription>
|
||||||
<div className="mt-4 font-black text-4xl">₹599<span className="text-xl text-muted-foreground font-normal">/mo</span></div>
|
<div className="mt-4 font-black text-4xl">₹1599<span className="text-xl text-muted-foreground font-normal">/mo</span></div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4 text-sm font-medium">
|
<CardContent className="space-y-4 text-sm font-medium">
|
||||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Everything in Pro</div>
|
<div className="flex items-center gap-3"><Check className="text-[#00F0FF]" size={18}/> Everything in Pro</div>
|
||||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Daily AI Error Review Queue</div>
|
<div className="flex items-center gap-3"><Check className="text-[#00F0FF]" size={18}/> Daily AI Error Review Queue</div>
|
||||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> SM-2 Spaced Repetition</div>
|
<div className="flex items-center gap-3"><Check className="text-[#00F0FF]" size={18}/> SM-2 Spaced Repetition</div>
|
||||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Mistake Heatmap Tracking</div>
|
<div className="flex items-center gap-3"><Check className="text-[#00F0FF]" size={18}/> Mistake Heatmap Tracking</div>
|
||||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> AI Chat Tutor Support</div>
|
<div className="flex items-center gap-3"><Check className="text-[#00F0FF]" size={18}/> AI Chat Tutor Support</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
<Button
|
<Button
|
||||||
className="w-full font-bold"
|
className="w-full font-bold text-lg rounded-2xl h-16 bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 transition-all"
|
||||||
size="lg"
|
|
||||||
onClick={() => handleUpgrade("Al_Coach")}
|
onClick={() => handleUpgrade("Al_Coach")}
|
||||||
disabled={!!loadingTier}
|
disabled={!!loadingTier}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { CheckCircle2, Clock, SkipForward, BrainCircuit } from "lucide-react"
|
import { CheckCircle2, Clock, SkipForward, BrainCircuit } from "lucide-react"
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ export function AdaptiveStudyPlan() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-xl flex items-center gap-2">
|
<CardTitle className="text-xl flex items-center gap-2">
|
||||||
<BrainCircuit className="text-primary" /> Adaptive Study Plan
|
<BrainCircuit className="text-primary" /> Adaptive Study Plan
|
||||||
|
<Badge variant="outline" className="border-[#00F0FF]/30 text-[#00F0FF] bg-[#00F0FF]/5 rounded-md text-[10px] uppercase font-bold ml-2 hidden sm:flex">AI Coach</Badge>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="flex items-center gap-2 bg-secondary/50 px-3 py-1 rounded-full text-xs font-bold text-foreground">
|
<div className="flex items-center gap-2 bg-secondary/50 px-3 py-1 rounded-full text-xs font-bold text-foreground">
|
||||||
<Clock size={14} className="text-primary" /> Today: {totalTime} min
|
<Clock size={14} className="text-primary" /> Today: {totalTime} min
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Flame, Info } from "lucide-react"
|
import { Flame, Info } from "lucide-react"
|
||||||
|
|
||||||
const HEATMAP_DATA = [
|
const HEATMAP_DATA = [
|
||||||
@@ -23,6 +24,7 @@ export function MistakeHeatmap() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-xl flex items-center gap-2">
|
<CardTitle className="text-xl flex items-center gap-2">
|
||||||
<Flame className="text-orange-500" /> Mistake Heatmap
|
<Flame className="text-orange-500" /> Mistake Heatmap
|
||||||
|
<Badge variant="outline" className="border-[#00F0FF]/30 text-[#00F0FF] bg-[#00F0FF]/5 rounded-md text-[10px] uppercase font-bold ml-2 hidden sm:flex">AI Coach</Badge>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Info size={16} className="text-muted-foreground" />
|
<Info size={16} className="text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -161,12 +161,13 @@ export function Scratchpad({ isOpen, onClose }: ScratchpadProps) {
|
|||||||
<div className="absolute top-4 right-4 z-10">
|
<div className="absolute top-4 right-4 z-10">
|
||||||
<Button variant="outline" size="sm" onClick={clearNotes} className="bg-zinc-900 border-zinc-700 text-zinc-300">Clear Notes</Button>
|
<Button variant="outline" size="sm" onClick={clearNotes} className="bg-zinc-900 border-zinc-700 text-zinc-300">Clear Notes</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 p-4">
|
<div className="flex-1 overflow-auto p-4 flex items-center justify-center">
|
||||||
<textarea
|
<textarea
|
||||||
value={notes}
|
value={notes}
|
||||||
onChange={handleNotesChange}
|
onChange={handleNotesChange}
|
||||||
placeholder="Type your rough notes, formulas, and calculations here..."
|
placeholder="Type your rough notes, formulas, and calculations here..."
|
||||||
className="w-full h-full bg-zinc-900 border border-zinc-800 rounded-lg shadow-inner text-zinc-100 p-6 resize-none focus:outline-none focus:ring-1 focus:ring-primary text-lg"
|
className="bg-zinc-900 border border-zinc-800 rounded-lg shadow-inner text-zinc-100 p-6 resize-none focus:outline-none focus:ring-1 focus:ring-primary text-lg"
|
||||||
|
style={{ width: '500px', height: '500px' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ export interface UserProfile {
|
|||||||
state: string;
|
state: string;
|
||||||
tier: "Free" | "Pro" | "Al_Coach"; // Al_Coach in database schema
|
tier: "Free" | "Pro" | "Al_Coach"; // Al_Coach in database schema
|
||||||
streak_count: number;
|
streak_count: number;
|
||||||
|
streak_count: number;
|
||||||
longest_streak: number;
|
longest_streak: number;
|
||||||
|
active_session_token?: string;
|
||||||
|
created_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_USER_ID = "b0e17378-864d-439e-8806-708dc48a891b"; // AI Coach
|
export const DEFAULT_USER_ID = "b0e17378-864d-439e-8806-708dc48a891b"; // AI Coach
|
||||||
@@ -50,7 +53,20 @@ export function useActiveUser() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setUser(data);
|
|
||||||
|
// Single Active Session Check
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const localToken = localStorage.getItem("active_session_token");
|
||||||
|
if (data.active_session_token && localToken && data.active_session_token !== localToken) {
|
||||||
|
localStorage.removeItem("activeUserId");
|
||||||
|
localStorage.removeItem("active_session_token");
|
||||||
|
alert("Your account was logged in from another device. You have been logged out.");
|
||||||
|
window.location.href = "/";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setUser(data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Error fetching active user profile:", err);
|
console.error("Error fetching active user profile:", err);
|
||||||
setError(err.message || "Failed to load user");
|
setError(err.message || "Failed to load user");
|
||||||
@@ -71,11 +87,33 @@ export function useActiveUser() {
|
|||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
const handleUpdate = () => fetchUserProfile(userId);
|
const handleUpdate = () => fetchUserProfile(userId);
|
||||||
window.addEventListener('pragativeer-user-updated', handleUpdate);
|
window.addEventListener('pragativeer-user-updated', handleUpdate);
|
||||||
return () => window.removeEventListener('pragativeer-user-updated', handleUpdate);
|
|
||||||
|
// Poll every 10 seconds for session validity
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchUserProfile(userId);
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('pragativeer-user-updated', handleUpdate);
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
}, [userId, fetchUserProfile]);
|
}, [userId, fetchUserProfile]);
|
||||||
|
|
||||||
const switchUser = useCallback((newId: string) => {
|
const switchUser = useCallback(async (newId: string) => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
|
const sessionToken = crypto.randomUUID();
|
||||||
|
localStorage.setItem("active_session_token", sessionToken);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(`/api/users/${newId}/session`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ session_token: sessionToken })
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to set session", err);
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.setItem("activeUserId", newId);
|
localStorage.setItem("activeUserId", newId);
|
||||||
setUserIdState(newId);
|
setUserIdState(newId);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
|||||||
Reference in New Issue
Block a user