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)
|
user = session.get(models.User, user_id)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
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
|
return user
|
||||||
|
|
||||||
class UserUpdate(BaseModel):
|
class UserUpdate(BaseModel):
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
--navy-900:#081226; --navy-800:#0c1c39; --navy-700:#13294d;
|
--navy-900:#081226; --navy-800:#0c1c39; --navy-700:#13294d;
|
||||||
--gold-200:#fdeeb8; --gold-300:#f6dd93; --gold-400:#e8c463; --gold-500:#d4af37;
|
--gold-200:#fdeeb8; --gold-300:#f6dd93; --gold-400:#e8c463; --gold-500:#d4af37;
|
||||||
--gold-600:#b1892a; --gold-700:#7d5f18;
|
--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;
|
--font:"Space Grotesk", ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||||
}
|
}
|
||||||
*{box-sizing:border-box}
|
*{box-sizing:border-box}
|
||||||
@@ -164,9 +164,13 @@
|
|||||||
body::after {
|
body::after {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
#stage, .hint {
|
.stage {
|
||||||
background: transparent !important;
|
height: 280px !important;
|
||||||
box-shadow: none !important;
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
gap: 0 !important;
|
||||||
|
justify-content: center !important;
|
||||||
}
|
}
|
||||||
.count, .panel, .hint {
|
.count, .panel, .hint {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
|
|||||||
@@ -19,9 +19,13 @@ def port_html():
|
|||||||
body::after {
|
body::after {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
#stage, .hint {
|
.stage {
|
||||||
background: transparent !important;
|
height: 280px !important;
|
||||||
box-shadow: none !important;
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
gap: 0 !important;
|
||||||
|
justify-content: center !important;
|
||||||
}
|
}
|
||||||
.count, .panel, .hint {
|
.count, .panel, .hint {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@@ -54,6 +58,9 @@ def port_html():
|
|||||||
html = html.replace("const sad = target<5;", "const sad = target<=2;")
|
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("Math.max(target,5)/5", "Math.max(target,3)/3")
|
||||||
html = html.replace("(4 - target) / 3", "(3 - target) / 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:
|
with open(dest_path, 'w', encoding='utf-8') as f:
|
||||||
f.write(html)
|
f.write(html)
|
||||||
|
|||||||
@@ -44,52 +44,76 @@ export function StreakOnboardingModal() {
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// Calculate trial days left
|
||||||
<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">
|
let trialDaysLeft = 0;
|
||||||
|
if (user.trial_end) {
|
||||||
{/* Celebration Animation Canvas & Logo Iframe */}
|
const end = new Date(user.trial_end);
|
||||||
<iframe
|
const now = new Date();
|
||||||
src={`/streak-animation.html?streak=${streak}`}
|
const diffTime = end.getTime() - now.getTime();
|
||||||
className="absolute inset-0 w-full h-full border-none pointer-events-none z-10"
|
trialDaysLeft = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
title="Streak Celebration"
|
}
|
||||||
allowtransparency="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Adaptive Modal Content Card (positioned at the bottom to let the animated logo shine in the center) */}
|
return (
|
||||||
<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">
|
<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 */}
|
{/* 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" />
|
||||||
<div className="absolute -left-20 -bottom-20 size-48 bg-primary/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 */}
|
{/* Top Celebration Animation Panel (Iframe with height 280px, border-b) */}
|
||||||
<div className="space-y-2">
|
<div className="relative w-full h-[280px] border-b border-border/40 z-10 pointer-events-none overflow-hidden bg-[#0c1c39]/30">
|
||||||
{streak > 0 ? (
|
<iframe
|
||||||
<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">
|
src={`/streak-animation.html?streak=${streak}`}
|
||||||
<Flame size={12} className="fill-current animate-bounce" /> {streak} Day Streak Active
|
className="w-full h-full border-none"
|
||||||
</div>
|
title="Streak Celebration"
|
||||||
) : (
|
allowtransparency="true"
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Button */}
|
{/* Bottom Greetings & Action Panel */}
|
||||||
<div className="pt-1">
|
<div className="p-6 text-center space-y-4">
|
||||||
<Button
|
|
||||||
onClick={handleEnter}
|
{/* Badge Indicators */}
|
||||||
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"
|
<div className="flex flex-col items-center gap-2">
|
||||||
>
|
{streak > 0 ? (
|
||||||
Enter Workspace
|
<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">
|
||||||
</Button>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface UserProfile {
|
|||||||
longest_streak: number;
|
longest_streak: number;
|
||||||
active_session_token?: string;
|
active_session_token?: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
|
trial_end?: 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
|
||||||
|
|||||||
Reference in New Issue
Block a user