fix: QA report bugs BUG-01 through BUG-11 resolved - streak modal close btn, calculator crash guard, AI chat auto-scroll, leaderboard zero-streak fix, name annotation cleanup everywhere, CBT score display colors, PrepPilot difficulty badge layout
This commit is contained in:
BIN
PragatiVeer_v2_Production_QA_Report.pdf
Normal file
BIN
PragatiVeer_v2_Production_QA_Report.pdf
Normal file
Binary file not shown.
@@ -8,6 +8,7 @@ from uuid import UUID
|
|||||||
from datetime import datetime, timedelta, date
|
from datetime import datetime, timedelta, date
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from database import init_db, get_session
|
from database import init_db, get_session
|
||||||
import models
|
import models
|
||||||
@@ -838,14 +839,14 @@ def get_leaderboard(
|
|||||||
# Calculate real user's stats
|
# Calculate real user's stats
|
||||||
user_score = 0
|
user_score = 0
|
||||||
user_accuracy = "0%"
|
user_accuracy = "0%"
|
||||||
user_streak = 1
|
user_streak = 0 # BUG-04 fix: real zero for new users (no artificial min floor)
|
||||||
if requesting_user:
|
if requesting_user:
|
||||||
if str(requesting_user.id) in DEMO_USER_UUIDS:
|
if str(requesting_user.id) in DEMO_USER_UUIDS:
|
||||||
user_score = 9250
|
user_score = 9250
|
||||||
user_accuracy = "88.5%"
|
user_accuracy = "88.5%"
|
||||||
user_streak = requesting_user.streak_count or 25
|
user_streak = requesting_user.streak_count or 25
|
||||||
else:
|
else:
|
||||||
user_streak = max(1, requesting_user.streak_count)
|
user_streak = requesting_user.streak_count or 0 # BUG-04 fix: 0 is valid
|
||||||
user_attempts = session.exec(
|
user_attempts = session.exec(
|
||||||
select(models.Attempt).where(
|
select(models.Attempt).where(
|
||||||
models.Attempt.user_id == requesting_user.id,
|
models.Attempt.user_id == requesting_user.id,
|
||||||
@@ -871,10 +872,11 @@ def get_leaderboard(
|
|||||||
"isCurrentUser": False,
|
"isCurrentUser": False,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Add the requesting real user
|
# Add the requesting real user — strip role annotations from display name
|
||||||
if requesting_user:
|
if requesting_user:
|
||||||
|
clean_name = re.sub(r'\s*\([^)]*\)\s*$', '', requesting_user.name).strip()
|
||||||
entries.append({
|
entries.append({
|
||||||
"name": requesting_user.name,
|
"name": clean_name,
|
||||||
"score": user_score,
|
"score": user_score,
|
||||||
"accuracy": user_accuracy,
|
"accuracy": user_accuracy,
|
||||||
"streak": user_streak,
|
"streak": user_streak,
|
||||||
|
|||||||
@@ -185,10 +185,12 @@ export default function AIDailyPracticePage() {
|
|||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-lg leading-tight group-hover:text-[#00F0FF] transition-colors">
|
<h3 className="font-bold text-lg leading-tight group-hover:text-[#00F0FF] transition-colors">
|
||||||
{cleanTitle(dpp.title)}
|
{cleanTitle(dpp.title)}
|
||||||
<span className={`ml-2 text-[10px] uppercase px-2 py-0.5 rounded-md border font-bold tracking-wider ${getDifficultyColor(dpp.difficulty)}`}>
|
</h3>
|
||||||
|
<div className="mt-1">
|
||||||
|
<span className={`text-[10px] uppercase px-2 py-0.5 rounded-md border font-bold tracking-wider ${getDifficultyColor(dpp.difficulty)}`}>
|
||||||
{dpp.difficulty}
|
{dpp.difficulty}
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground mt-2 font-medium">
|
<div className="text-sm text-muted-foreground mt-2 font-medium">
|
||||||
{count} Questions • {dpp.total_time} mins
|
{count} Questions • {dpp.total_time} mins
|
||||||
{dpp.scheduled_date && (
|
{dpp.scheduled_date && (
|
||||||
|
|||||||
@@ -411,7 +411,14 @@ export default function CBTPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : attempts.filter(a => a.source_type === "Test" && exams.some(e => e.id === a.source_id)).map((attempt, i) => {
|
) : attempts.filter(a => a.source_type === "Test" && exams.some(e => e.id === a.source_id)).map((attempt, i) => {
|
||||||
const exam = exams.find(e => e.id === attempt.source_id);
|
const exam = exams.find(e => e.id === attempt.source_id);
|
||||||
const scoreCalc = (attempt.score * 4) - ((attempt.total_questions - attempt.score) * 1); // rough calc for display
|
// BUG-07 fix: use actual final_score from backend if available, else derive correctly
|
||||||
|
// backend stores final_score = (correct*4) - (incorrect*1)
|
||||||
|
const correctCount = attempt.answers ? attempt.answers.filter((a: any) => a.is_correct).length : attempt.score;
|
||||||
|
const incorrectCount = attempt.answers ? (attempt.answers.filter((a: any) => a.selected_answer !== null).length - correctCount) : 0;
|
||||||
|
const scoreCalc = attempt.final_score ?? ((correctCount * 4) - (incorrectCount * 1));
|
||||||
|
const maxScore = attempt.total_questions * 4;
|
||||||
|
const scorePercent = maxScore > 0 ? (scoreCalc / maxScore) * 100 : 0;
|
||||||
|
const scoreColor = scorePercent >= 60 ? 'text-green-500' : scorePercent >= 35 ? 'text-amber-500' : 'text-red-500';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={i} className="bg-secondary/20">
|
<Card key={i} className="bg-secondary/20">
|
||||||
@@ -424,8 +431,8 @@ export default function CBTPage() {
|
|||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-xs md:text-sm text-muted-foreground font-bold uppercase tracking-wider">Score</div>
|
<div className="text-xs md:text-sm text-muted-foreground font-bold uppercase tracking-wider">Score</div>
|
||||||
<div className="font-bold text-base md:text-lg">
|
<div className="font-bold text-base md:text-lg">
|
||||||
<span className="text-foreground">{scoreCalc}</span>
|
<span className={scoreColor}>{scoreCalc}</span>
|
||||||
<span className="text-muted-foreground">/{attempt.total_questions * 4}</span>
|
<span className="text-muted-foreground">/{maxScore}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
|
|||||||
@@ -49,7 +49,11 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
return () => document.removeEventListener("mousedown", handleClickOutside)
|
return () => document.removeEventListener("mousedown", handleClickOutside)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const userInitials = user ? user.name.split(" ").filter(n => /^[a-zA-Z]/.test(n)).slice(0, 2).map(n => n[0]).join("").toUpperCase() : "DS"
|
// BUG-05 fix: strip any parenthetical role annotations from display names
|
||||||
|
// e.g. "Rahul (AI Coach)" → "Rahul", "Priya (NEET Aspirant)" → "Priya"
|
||||||
|
const cleanDisplayName = (name: string) => name.replace(/\s*\([^)]*\)\s*$/, '').trim();
|
||||||
|
|
||||||
|
const userInitials = user ? cleanDisplayName(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" : user?.target_exam === "JEE_Advanced" ? "JEE Advanced" : user?.target_exam === "NEET" ? "NEET" : "NEET/JEE"
|
const targetExamLabel = user?.target_exam === "JEE_Main" ? "JEE" : user?.target_exam === "JEE_Advanced" ? "JEE Advanced" : user?.target_exam === "NEET" ? "NEET" : "NEET/JEE"
|
||||||
|
|
||||||
// Trial logic removed as requested
|
// Trial logic removed as requested
|
||||||
@@ -251,7 +255,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
{dropdownOpen && user && (
|
{dropdownOpen && user && (
|
||||||
<div className="absolute bottom-full left-0 w-full mb-2 bg-background border border-border rounded-xl shadow-xl shadow-black/20 overflow-hidden animate-in fade-in slide-in-from-bottom-2 duration-200 z-50">
|
<div className="absolute bottom-full left-0 w-full mb-2 bg-background border border-border rounded-xl shadow-xl shadow-black/20 overflow-hidden animate-in fade-in slide-in-from-bottom-2 duration-200 z-50">
|
||||||
<div className="px-4 py-3 border-b border-border bg-secondary/30">
|
<div className="px-4 py-3 border-b border-border bg-secondary/30">
|
||||||
<p className="text-sm font-bold text-foreground truncate">{user.name}</p>
|
<p className="text-sm font-bold text-foreground truncate">{cleanDisplayName(user.name)}</p>
|
||||||
<p className="text-xs text-muted-foreground truncate">{user.email || user.phone}</p>
|
<p className="text-xs text-muted-foreground truncate">{user.email || user.phone}</p>
|
||||||
{/* Trial info in dropdown */}
|
{/* Trial info in dropdown */}
|
||||||
{trialInfo.isTrialActive && (
|
{trialInfo.isTrialActive && (
|
||||||
@@ -295,7 +299,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
{userInitials}
|
{userInitials}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-bold leading-none truncate">{user ? user.name.split(" ")[0] : "Loading..."}</p>
|
<p className="text-sm font-bold leading-none truncate">{user ? cleanDisplayName(user.name).split(" ")[0] : "Loading..."}</p>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
{trialInfo.isTrialActive ? (
|
{trialInfo.isTrialActive ? (
|
||||||
<span className="inline-flex items-center text-[10px] font-bold text-primary">
|
<span className="inline-flex items-center text-[10px] font-bold text-primary">
|
||||||
@@ -319,7 +323,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
<header className="h-16 border-b border-border bg-background/95 backdrop-blur-md flex items-center justify-between px-8 shrink-0 z-40">
|
<header className="h-16 border-b border-border bg-background/95 backdrop-blur-md flex items-center justify-between px-8 shrink-0 z-40">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<h2 className="font-bold text-lg hidden sm:block">
|
<h2 className="font-bold text-lg hidden sm:block">
|
||||||
{user ? `Welcome back, ${user.name.split(" ")[0]}!` : "Welcome back!"}
|
{user ? `Welcome back, ${cleanDisplayName(user.name).split(" ")[0]}!` : "Welcome back!"}
|
||||||
</h2>
|
</h2>
|
||||||
{user && (
|
{user && (
|
||||||
<Badge variant="outline" className="bg-cyan-500/10 text-cyan-700 dark:text-cyan-400 border-cyan-500/20 font-bold tracking-wide uppercase px-2.5 py-0.5 animate-pulse shadow-none dark:shadow-[0_0_10px_-2px_#00F0FF]">
|
<Badge variant="outline" className="bg-cyan-500/10 text-cyan-700 dark:text-cyan-400 border-cyan-500/20 font-bold tracking-wide uppercase px-2.5 py-0.5 animate-pulse shadow-none dark:shadow-[0_0_10px_-2px_#00F0FF]">
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ export default function LeaderboardPage() {
|
|||||||
const [rankings, setRankings] = useState<any[]>([])
|
const [rankings, setRankings] = useState<any[]>([])
|
||||||
const [loadingRankings, setLoadingRankings] = useState(true)
|
const [loadingRankings, setLoadingRankings] = useState(true)
|
||||||
|
|
||||||
|
// Strip parenthetical role annotations from user names (e.g. "Rahul (AI Coach)" → "Rahul")
|
||||||
|
const cleanDisplayName = (name: string) => name.replace(/\s*\([^)]*\)\s*$/, '').trim();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchLeaderboard() {
|
async function fetchLeaderboard() {
|
||||||
setLoadingRankings(true)
|
setLoadingRankings(true)
|
||||||
@@ -39,7 +42,7 @@ export default function LeaderboardPage() {
|
|||||||
rank: rankings.length + 1,
|
rank: rankings.length + 1,
|
||||||
score: 0,
|
score: 0,
|
||||||
streak: user?.streak_count || 1,
|
streak: user?.streak_count || 1,
|
||||||
name: user?.name || "You"
|
name: user?.name ? cleanDisplayName(user.name) : "You"
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -121,7 +124,7 @@ export default function LeaderboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className={`font-bold ${student.isCurrentUser ? 'text-[#00F0FF]' : 'text-foreground'}`}>
|
<p className={`font-bold ${student.isCurrentUser ? 'text-[#00F0FF]' : 'text-foreground'}`}>
|
||||||
{student.isCurrentUser ? (isAnonymous ? "Anonymous User" : `${student.name} (You)`) : student.name}
|
{student.isCurrentUser ? (isAnonymous ? "Anonymous User" : `${cleanDisplayName(student.name)} (You)`) : cleanDisplayName(student.name)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
||||||
<Shield size={12} /> Accuracy: {student.accuracy}
|
<Shield size={12} /> Accuracy: {student.accuracy}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export default function RankPredictorPage() {
|
|||||||
</p>
|
</p>
|
||||||
{user && (
|
{user && (
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1 bg-emerald-500/10 text-emerald-400 border border-emerald-500/30 rounded-xl text-xs font-bold mt-1">
|
<div className="inline-flex items-center gap-2 px-3 py-1 bg-emerald-500/10 text-emerald-400 border border-emerald-500/30 rounded-xl text-xs font-bold mt-1">
|
||||||
<UserCheck size={14} /> Profile Connected: {user.name} ({user.target_exam?.replace("_", " ")})
|
<UserCheck size={14} /> Profile Connected: {user.name.replace(/\s*\([^)]*\)\s*$/, '').trim()} ({user.target_exam?.replace("_", " ")})
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ export default function SettingsPage() {
|
|||||||
setAvatarUrl(localStorage.getItem(`user_avatar_${user.id}`) || null);
|
setAvatarUrl(localStorage.getItem(`user_avatar_${user.id}`) || null);
|
||||||
|
|
||||||
if (user.name) {
|
if (user.name) {
|
||||||
const parts = user.name.split(" ");
|
// BUG-06 fix: strip parenthetical role annotations before splitting into first/last
|
||||||
|
const cleanName = user.name.replace(/\s*\([^)]*\)\s*$/, '').trim();
|
||||||
|
const parts = cleanName.split(" ");
|
||||||
setFirstName(parts[0] || "");
|
setFirstName(parts[0] || "");
|
||||||
setLastName(parts.slice(1).join(" ") || "");
|
setLastName(parts.slice(1).join(" ") || "");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState, useRef, useEffect } from "react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
@@ -39,6 +39,15 @@ export function AITutorChat() {
|
|||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
// BUG-03 fix: ref to anchor bottom of chat for auto-scroll
|
||||||
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Auto-scroll to latest message whenever messages change
|
||||||
|
useEffect(() => {
|
||||||
|
if (bottomRef.current) {
|
||||||
|
bottomRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
if (!input.trim()) return;
|
if (!input.trim()) return;
|
||||||
@@ -118,6 +127,8 @@ export function AITutorChat() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{/* BUG-03 fix: invisible scroll anchor */}
|
||||||
|
<div ref={bottomRef} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter className="p-4 border-t border-border bg-background shrink-0">
|
<CardFooter className="p-4 border-t border-border bg-background shrink-0">
|
||||||
|
|||||||
@@ -20,8 +20,17 @@ export function Calculator({ isOpen, onClose }: CalculatorProps) {
|
|||||||
|
|
||||||
const handleCalculate = () => {
|
const handleCalculate = () => {
|
||||||
try {
|
try {
|
||||||
|
if (!display.trim()) return;
|
||||||
|
|
||||||
|
// BUG-02 fix: strip trailing operator(s) so "5*", "9+", "/2" (leading slash) don't crash
|
||||||
|
const sanitized = display
|
||||||
|
.replace(/[+\-*/^]+$/, "") // remove trailing operators
|
||||||
|
.replace(/^[*/^]+/, ""); // remove leading * / ^ (but allow leading minus for negatives)
|
||||||
|
|
||||||
|
if (!sanitized.trim()) return;
|
||||||
|
|
||||||
// Map functions to JS Math
|
// Map functions to JS Math
|
||||||
let mappedExp = display
|
let mappedExp = sanitized
|
||||||
.replace(/sin\(/g, 'Math.sin(')
|
.replace(/sin\(/g, 'Math.sin(')
|
||||||
.replace(/cos\(/g, 'Math.cos(')
|
.replace(/cos\(/g, 'Math.cos(')
|
||||||
.replace(/tan\(/g, 'Math.tan(')
|
.replace(/tan\(/g, 'Math.tan(')
|
||||||
@@ -30,8 +39,15 @@ export function Calculator({ isOpen, onClose }: CalculatorProps) {
|
|||||||
.replace(/sqrt\(/g, 'Math.sqrt(')
|
.replace(/sqrt\(/g, 'Math.sqrt(')
|
||||||
.replace(/\^/g, '**');
|
.replace(/\^/g, '**');
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-new-func
|
||||||
const result = new Function('return ' + mappedExp)();
|
const result = new Function('return ' + mappedExp)();
|
||||||
|
|
||||||
|
// Guard against NaN / Infinity
|
||||||
|
if (result === null || result === undefined || !isFinite(result) || isNaN(result)) {
|
||||||
|
setDisplay("Error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle floating point precision issues simply
|
// Handle floating point precision issues simply
|
||||||
if (typeof result === 'number' && !Number.isInteger(result)) {
|
if (typeof result === 'number' && !Number.isInteger(result)) {
|
||||||
setDisplay(String(parseFloat(result.toFixed(6))));
|
setDisplay(String(parseFloat(result.toFixed(6))));
|
||||||
@@ -43,6 +59,7 @@ export function Calculator({ isOpen, onClose }: CalculatorProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleClear = () => setDisplay("");
|
const handleClear = () => setDisplay("");
|
||||||
const handleBackspace = () => setDisplay((prev) => prev.slice(0, -1));
|
const handleBackspace = () => setDisplay((prev) => prev.slice(0, -1));
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Flame, Sparkles } from "lucide-react";
|
import { Flame, Sparkles, X } from "lucide-react";
|
||||||
import { useActiveUser } from "@/hooks/useActiveUser";
|
import { useActiveUser } from "@/hooks/useActiveUser";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export function StreakOnboardingModal() {
|
|||||||
wishMessage = `Incredible! A ${streak}-day streak shows true championship discipline. You are outperforming the competition. Let's make today count!`;
|
wishMessage = `Incredible! A ${streak}-day streak shows true championship discipline. You are outperforming the competition. Let's make today count!`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEnter = () => {
|
const handleDismiss = () => {
|
||||||
sessionStorage.setItem(`hasSeenStreakPopup_${user.id}`, "true");
|
sessionStorage.setItem(`hasSeenStreakPopup_${user.id}`, "true");
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
@@ -57,10 +57,24 @@ export function StreakOnboardingModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/85 backdrop-blur-md animate-in fade-in duration-300">
|
// BUG-01 fix: backdrop click dismisses modal
|
||||||
|
<div
|
||||||
{/* Centered Modal Card Container covering 80% screen size (w-[80vw] h-[80vh]) */}
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md animate-in fade-in duration-300"
|
||||||
<div className="bg-background border-none shadow-[0_0_60px_-10px_rgba(0,240,255,0.25)] rounded-3xl w-[80vw] max-w-5xl h-[80vh] max-h-[800px] relative overflow-hidden animate-in zoom-in-95 slide-in-from-bottom-4 duration-300 z-20 pointer-events-auto mx-4 flex flex-col justify-start">
|
onClick={handleDismiss}
|
||||||
|
>
|
||||||
|
{/* Centered Modal Card — stop propagation so inner clicks don't close */}
|
||||||
|
<div
|
||||||
|
className="bg-background border border-border shadow-[0_0_60px_-10px_rgba(0,240,255,0.25)] rounded-3xl w-[90vw] max-w-5xl h-[85vh] max-h-[800px] relative overflow-hidden animate-in zoom-in-95 slide-in-from-bottom-4 duration-300 z-20 pointer-events-auto mx-4 flex flex-col justify-start"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* BUG-01 fix: X close button */}
|
||||||
|
<button
|
||||||
|
onClick={handleDismiss}
|
||||||
|
className="absolute top-4 right-4 z-30 size-9 flex items-center justify-center rounded-full bg-background/80 border border-border text-muted-foreground hover:text-foreground hover:bg-secondary transition-all duration-200 shadow-sm"
|
||||||
|
aria-label="Close streak modal"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Subtle background glow */}
|
{/* Subtle background glow */}
|
||||||
<div className="absolute -right-20 -top-20 size-48 bg-[#00F0FF]/5 rounded-full blur-3xl pointer-events-none" />
|
<div className="absolute -right-20 -top-20 size-48 bg-[#00F0FF]/5 rounded-full blur-3xl pointer-events-none" />
|
||||||
@@ -112,7 +126,7 @@ export function StreakOnboardingModal() {
|
|||||||
{/* Action Button */}
|
{/* Action Button */}
|
||||||
<div className="pt-2 max-w-sm w-full mx-auto">
|
<div className="pt-2 max-w-sm w-full mx-auto">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleEnter}
|
onClick={handleDismiss}
|
||||||
className="w-full h-12 rounded-xl text-base font-black bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 shadow-[0_0_15px_-3px_#00F0FF] hover:shadow-[0_0_25px_#00F0FF] hover:-translate-y-0.5 transition-all duration-300"
|
className="w-full h-12 rounded-xl text-base font-black bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 shadow-[0_0_15px_-3px_#00F0FF] hover:shadow-[0_0_25px_#00F0FF] hover:-translate-y-0.5 transition-all duration-300"
|
||||||
>
|
>
|
||||||
Enter Workspace
|
Enter Workspace
|
||||||
|
|||||||
Reference in New Issue
Block a user