Refactor streak modal into a single card with top border-b iframe animation and bottom greetings, daily check-in Snapchat-like streak update/reset logic
This commit is contained in:
@@ -480,6 +480,34 @@ def get_user_profile(user_id: UUID, session: Session = Depends(get_session)):
|
||||
user = session.get(models.User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Snapchat-style daily active streak check
|
||||
now = datetime.utcnow()
|
||||
today = now.date()
|
||||
last_active_date = user.last_active.date()
|
||||
delta = (today - last_active_date).days
|
||||
|
||||
changed = False
|
||||
if user.streak_count == 0:
|
||||
user.streak_count = 1
|
||||
user.longest_streak = max(user.longest_streak, 1)
|
||||
user.last_active = now
|
||||
changed = True
|
||||
elif delta == 1:
|
||||
user.streak_count += 1
|
||||
user.longest_streak = max(user.longest_streak, user.streak_count)
|
||||
user.last_active = now
|
||||
changed = True
|
||||
elif delta > 1:
|
||||
user.streak_count = 1
|
||||
user.last_active = now
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
|
||||
return user
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
--navy-900:#081226; --navy-800:#0c1c39; --navy-700:#13294d;
|
||||
--gold-200:#fdeeb8; --gold-300:#f6dd93; --gold-400:#e8c463; --gold-500:#d4af37;
|
||||
--gold-600:#b1892a; --gold-700:#7d5f18;
|
||||
--ink:#e9eefb; --badge:230px; --grow:1.18; --halo-w:170%;
|
||||
--ink:#e9eefb; --badge:190px; --grow:1.18; --halo-w:170%;
|
||||
--font:"Space Grotesk", ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
@@ -164,9 +164,13 @@
|
||||
body::after {
|
||||
display: none !important;
|
||||
}
|
||||
#stage, .hint {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
.stage {
|
||||
height: 280px !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
gap: 0 !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
.count, .panel, .hint {
|
||||
display: none !important;
|
||||
|
||||
@@ -19,9 +19,13 @@ def port_html():
|
||||
body::after {
|
||||
display: none !important;
|
||||
}
|
||||
#stage, .hint {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
.stage {
|
||||
height: 280px !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
gap: 0 !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
.count, .panel, .hint {
|
||||
display: none !important;
|
||||
@@ -54,6 +58,9 @@ def port_html():
|
||||
html = html.replace("const sad = target<5;", "const sad = target<=2;")
|
||||
html = html.replace("Math.max(target,5)/5", "Math.max(target,3)/3")
|
||||
html = html.replace("(4 - target) / 3", "(3 - target) / 3")
|
||||
|
||||
# 5. Adjust badge size from 230px to 190px
|
||||
html = html.replace("--badge:230px;", "--badge:190px;")
|
||||
|
||||
with open(dest_path, 'w', encoding='utf-8') as f:
|
||||
f.write(html)
|
||||
|
||||
@@ -44,52 +44,76 @@ export function StreakOnboardingModal() {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex flex-col items-center justify-end pb-12 bg-black/90 backdrop-blur-lg animate-in fade-in duration-300">
|
||||
|
||||
{/* Celebration Animation Canvas & Logo Iframe */}
|
||||
<iframe
|
||||
src={`/streak-animation.html?streak=${streak}`}
|
||||
className="absolute inset-0 w-full h-full border-none pointer-events-none z-10"
|
||||
title="Streak Celebration"
|
||||
allowtransparency="true"
|
||||
/>
|
||||
// Calculate trial days left
|
||||
let trialDaysLeft = 0;
|
||||
if (user.trial_end) {
|
||||
const end = new Date(user.trial_end);
|
||||
const now = new Date();
|
||||
const diffTime = end.getTime() - now.getTime();
|
||||
trialDaysLeft = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
{/* Adaptive Modal Content Card (positioned at the bottom to let the animated logo shine in the center) */}
|
||||
<div className="bg-background/85 backdrop-blur-md border border-border/60 shadow-[0_0_50px_-15px_rgba(0,240,255,0.1)] rounded-3xl w-full max-w-lg p-6 text-center space-y-4 relative overflow-hidden animate-in zoom-in-95 slide-in-from-bottom-4 duration-300 z-20 pointer-events-auto mx-4">
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/85 backdrop-blur-md animate-in fade-in duration-300">
|
||||
|
||||
{/* Centered Modal Card Container with Border (containing both the animation and text) */}
|
||||
<div className="bg-background/95 border border-border shadow-[0_0_60px_-10px_rgba(0,240,255,0.2)] rounded-3xl w-full max-w-lg 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">
|
||||
|
||||
{/* 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 -left-20 -bottom-20 size-48 bg-primary/5 rounded-full blur-3xl pointer-events-none" />
|
||||
|
||||
{/* Text Greetings */}
|
||||
<div className="space-y-2">
|
||||
{streak > 0 ? (
|
||||
<div className="inline-flex items-center gap-1.5 px-3 py-0.5 bg-orange-500/10 border border-orange-500/20 text-orange-500 text-xs font-extrabold rounded-full tracking-wider uppercase">
|
||||
<Flame size={12} className="fill-current animate-bounce" /> {streak} Day Streak Active
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex items-center gap-1.5 px-3 py-0.5 bg-blue-500/10 border border-blue-500/20 text-blue-400 text-xs font-extrabold rounded-full tracking-wider uppercase">
|
||||
<Sparkles size={12} className="animate-spin" style={{ animationDuration: '4s' }} /> Day 1 Awaits
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h3 className="text-xl md:text-2xl font-black tracking-tight bg-gradient-to-r from-white via-white to-gray-400 bg-clip-text text-transparent">
|
||||
{wishTitle}
|
||||
</h3>
|
||||
<p className="text-gray-400 text-xs md:text-sm leading-relaxed max-w-sm mx-auto">
|
||||
{wishMessage}
|
||||
</p>
|
||||
{/* Top Celebration Animation Panel (Iframe with height 280px, border-b) */}
|
||||
<div className="relative w-full h-[280px] border-b border-border/40 z-10 pointer-events-none overflow-hidden bg-[#0c1c39]/30">
|
||||
<iframe
|
||||
src={`/streak-animation.html?streak=${streak}`}
|
||||
className="w-full h-full border-none"
|
||||
title="Streak Celebration"
|
||||
allowtransparency="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<div className="pt-1">
|
||||
<Button
|
||||
onClick={handleEnter}
|
||||
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
|
||||
</Button>
|
||||
{/* Bottom Greetings & Action Panel */}
|
||||
<div className="p-6 text-center space-y-4">
|
||||
|
||||
{/* Badge Indicators */}
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{streak > 0 ? (
|
||||
<div className="inline-flex items-center gap-1.5 px-3 py-0.5 bg-orange-500/10 border border-orange-500/20 text-orange-500 text-xs font-extrabold rounded-full tracking-wider uppercase">
|
||||
<Flame size={12} className="fill-current animate-bounce" /> {streak} Day Streak Active
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex items-center gap-1.5 px-3 py-0.5 bg-blue-500/10 border border-blue-500/20 text-blue-400 text-xs font-extrabold rounded-full tracking-wider uppercase">
|
||||
<Sparkles size={12} className="animate-spin" style={{ animationDuration: '4s' }} /> Day 1 Awaits
|
||||
</div>
|
||||
)}
|
||||
|
||||
{trialDaysLeft > 0 && (
|
||||
<div className="inline-flex items-center gap-1.5 px-3 py-0.5 bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs font-semibold rounded-full tracking-wider">
|
||||
🎁 Free Trial: {trialDaysLeft} {trialDaysLeft === 1 ? "day" : "days"} remaining
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Greetings Text */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl md:text-2xl font-black tracking-tight bg-gradient-to-r from-white via-white to-gray-400 bg-clip-text text-transparent">
|
||||
{wishTitle}
|
||||
</h3>
|
||||
<p className="text-gray-400 text-xs md:text-sm leading-relaxed max-w-sm mx-auto">
|
||||
{wishMessage}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<div className="pt-1">
|
||||
<Button
|
||||
onClick={handleEnter}
|
||||
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
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface UserProfile {
|
||||
longest_streak: number;
|
||||
active_session_token?: string;
|
||||
created_at?: string;
|
||||
trial_end?: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_USER_ID = "b0e17378-864d-439e-8806-708dc48a891b"; // AI Coach
|
||||
|
||||
Reference in New Issue
Block a user