- Replace custom CSS with Tailwind utility classes - Convert all inline SVGs to Heroicons sprite system - Add consistent Tailwind config with design tokens - Improve responsive layout for onboarding screen
564 lines
22 KiB
HTML
564 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>내 진행 상황 - Chakmate</title>
|
|
<!-- Heroicons SVG Sprite -->
|
|
<svg xmlns="http://www.w3.org/2000/svg" style="display:none;">
|
|
<symbol id="icon-arrow-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
|
</symbol>
|
|
</svg>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: { DEFAULT: '#3b82f6', light: '#60a5fa', dark: '#1d4ed8' },
|
|
secondary: { DEFAULT: '#06b6d4', light: '#22d3ee' },
|
|
accent: { DEFAULT: '#0ea5e9', warn: '#38bdf8', danger: '#f472b6' },
|
|
surface: { primary: '#f8fafc', secondary: '#e2e8f0', card: '#ffffff' },
|
|
text: { primary: '#0f172a', secondary: '#475569', muted: '#94a3b8' },
|
|
},
|
|
fontFamily: {
|
|
sans: ['Noto Sans KR', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'],
|
|
display: ['Outfit', 'sans-serif'],
|
|
},
|
|
boxShadow: {
|
|
'sm': '0 2px 8px rgba(14, 165, 233, 0.06)',
|
|
'md': '0 4px 20px rgba(14, 165, 233, 0.08)',
|
|
'lg': '0 8px 40px rgba(14, 165, 233, 0.12)',
|
|
'glow': '0 0 30px rgba(14, 165, 233, 0.25)',
|
|
'blue': '0 4px 20px rgba(59, 130, 246, 0.3)',
|
|
},
|
|
animation: {
|
|
'shimmer': 'shimmer 3s ease-in-out infinite',
|
|
'pulse-slow': 'pulse 1.5s ease-in-out infinite',
|
|
'unlock-pulse': 'unlockPulse 0.6s ease-out',
|
|
'sparkle': 'sparkle 0.6s ease-out forwards',
|
|
'confetti-fall': 'confettiFall 2s ease-out forwards',
|
|
'fire-flicker': 'fireFlicker 0.3s ease-in-out infinite',
|
|
},
|
|
keyframes: {
|
|
shimmer: {
|
|
'0%, 100%': { transform: 'translate(-10%, -10%) rotate(0deg)' },
|
|
'50%': { transform: 'translate(10%, 10%) rotate(180deg)' },
|
|
},
|
|
unlockPulse: {
|
|
'0%': { transform: 'scale(1)', boxShadow: '0 0 0 0 rgba(251, 191, 36, 0.4)' },
|
|
'50%': { transform: 'scale(1.1)', boxShadow: '0 0 0 15px rgba(251, 191, 36, 0)' },
|
|
'100%': { transform: 'scale(1)', boxShadow: '0 0 0 0 rgba(251, 191, 36, 0)' },
|
|
},
|
|
sparkle: {
|
|
'0%, 100%': { opacity: '0', transform: 'scale(0) rotate(0deg)' },
|
|
'50%': { opacity: '1', transform: 'scale(1) rotate(180deg)' },
|
|
},
|
|
confettiFall: {
|
|
'0%': { opacity: '1', transform: 'translateY(-100px) rotate(0deg)' },
|
|
'100%': { opacity: '0', transform: 'translateY(100vh) rotate(720deg)' },
|
|
},
|
|
fireFlicker: {
|
|
'0%, 100%': { transform: 'scaleY(1) scaleX(1)' },
|
|
'25%': { transform: 'scaleY(1.1) scaleX(0.95)' },
|
|
'50%': { transform: 'scaleY(0.95) scaleX(1.05)' },
|
|
'75%': { transform: 'scaleY(1.05) scaleX(0.98)' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
<style>
|
|
.confetti {
|
|
position: absolute;
|
|
width: 10px;
|
|
height: 10px;
|
|
opacity: 0;
|
|
}
|
|
.achievement.just-unlocked {
|
|
animation: unlockPulse 0.6s ease-out;
|
|
}
|
|
.sparkle {
|
|
position: absolute;
|
|
font-size: 1rem;
|
|
animation: sparkle 0.6s ease-out forwards;
|
|
}
|
|
.toast.show {
|
|
opacity: 1 !important;
|
|
transform: translateX(-50%) translateY(0) !important;
|
|
}
|
|
.toggle-switch {
|
|
position: relative;
|
|
width: 56px;
|
|
height: 32px;
|
|
}
|
|
.toggle-switch input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
.toggle-slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
inset: 0;
|
|
background: #e2e8f0;
|
|
border-radius: 9999px;
|
|
transition: background 250ms ease;
|
|
}
|
|
.toggle-slider::before {
|
|
content: '';
|
|
position: absolute;
|
|
height: 24px;
|
|
width: 24px;
|
|
left: 4px;
|
|
bottom: 4px;
|
|
background: white;
|
|
border-radius: 50%;
|
|
transition: transform 500ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.06);
|
|
}
|
|
.toggle-switch input:checked + .toggle-slider {
|
|
background: linear-gradient(90deg, #3b82f6 0%, #0ea5e9 100%);
|
|
}
|
|
.toggle-switch input:checked + .toggle-slider::before {
|
|
transform: translateX(24px);
|
|
}
|
|
.achievement-tooltip {
|
|
position: absolute;
|
|
bottom: calc(100% + 8px);
|
|
left: 50%;
|
|
transform: translateX(-50%) translateY(0);
|
|
background: #0f172a;
|
|
color: #ffffff;
|
|
padding: 8px 12px;
|
|
border-radius: 8px;
|
|
font-size: 0.7rem;
|
|
white-space: nowrap;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: all 150ms ease;
|
|
z-index: 10;
|
|
}
|
|
.achievement-tooltip::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
border: 6px solid transparent;
|
|
border-top-color: #0f172a;
|
|
}
|
|
.achievement.locked:hover .achievement-tooltip {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
transform: translateX(-50%) translateY(-10px);
|
|
}
|
|
.achievement.unlocked .achievement-tooltip {
|
|
display: none;
|
|
}
|
|
.week-day-indicator {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 12px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.1rem;
|
|
background: #e2e8f0;
|
|
transition: all 250ms ease;
|
|
}
|
|
.week-day-indicator.completed {
|
|
background: linear-gradient(135deg, #0ea5e9 0%, #10b981 100%);
|
|
color: white;
|
|
box-shadow: 0 4px 12px rgba(52, 211, 153, 0.3);
|
|
}
|
|
.week-day-indicator.today {
|
|
border: 2px solid #3b82f6;
|
|
box-shadow: 0 0 0 4px rgba(14, 165, 233, 0.08);
|
|
}
|
|
.achievement {
|
|
position: relative;
|
|
aspect-ratio: 1;
|
|
border-radius: 12px;
|
|
background: #e2e8f0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 4px;
|
|
padding: 8px;
|
|
transition: transform 500ms cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 250ms ease;
|
|
cursor: pointer;
|
|
}
|
|
.achievement.unlocked {
|
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
|
box-shadow: 0 4px 20px rgba(251, 191, 36, 0.3);
|
|
}
|
|
.achievement.unlocked.gold {
|
|
background: linear-gradient(135deg, #fef3c7 0%, #fcd34d 100%);
|
|
}
|
|
.achievement.unlocked.silver {
|
|
background: linear-gradient(135deg, #f3f4f6 0%, #d1d5db 100%);
|
|
box-shadow: 0 4px 20px rgba(156, 163, 175, 0.3);
|
|
}
|
|
.achievement.unlocked.bronze {
|
|
background: linear-gradient(135deg, #fed7aa 0%, #fdba74 100%);
|
|
box-shadow: 0 4px 20px rgba(251, 146, 60, 0.3);
|
|
}
|
|
.achievement.locked {
|
|
opacity: 0.6;
|
|
}
|
|
.achievement:hover:not(.locked) {
|
|
transform: scale(1.05) translateY(-4px);
|
|
}
|
|
.achievement.locked:hover {
|
|
transform: scale(1.02);
|
|
}
|
|
.streak-icon.animate {
|
|
animation: fireFlicker 0.3s ease-in-out infinite;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="font-sans bg-surface-primary text-text-primary min-h-screen overflow-x-hidden antialiased">
|
|
<div class="max-w-[480px] mx-auto p-4 pb-[100px]">
|
|
<header class="flex items-center justify-between p-4 bg-surface-card shadow-sm sticky top-0 z-[100] mb-6 rounded-xl">
|
|
<div class="flex items-center gap-3">
|
|
<div class="relative w-10 h-10 bg-gradient-to-br from-primary to-secondary rounded-xl flex items-center justify-center shadow-blue">
|
|
<svg viewBox="0 0 24 24" fill="none" class="w-6 h-6 fill-white drop-shadow-md">
|
|
<defs>
|
|
<linearGradient id="logoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" stop-color="#06b6d4"/>
|
|
<stop offset="50%" stop-color="#3b82f6"/>
|
|
<stop offset="100%" stop-color="#0ea5e9"/>
|
|
</linearGradient>
|
|
</defs>
|
|
<path d="M6 3a3 3 0 013 3v2a3 3 0 01-3 3H4a2 2 0 00-2 2v6a2 2 0 002 2h2a3 3 0 013 3v2a3 3 0 01-3 3H6a3 3 0 01-3-3v-2a3 3 0 013-3h2a2 2 0 002-2V8a2 2 0 00-2-2H6a3 3 0 01-3-3V6a3 3 0 013-3z" fill="url(#logoGrad)"/>
|
|
<circle cx="12" cy="12" r="3" fill="white" opacity="0.3"/>
|
|
</svg>
|
|
</div>
|
|
<h1 class="font-display text-xl font-semibold">내 진행 상황</h1>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<button class="w-11 h-11 rounded-xl bg-surface-secondary flex items-center justify-center cursor-pointer shadow-sm hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 transition-all duration-150" onclick="window.location.href='index.html'">
|
|
<svg class="w-5 h-5 stroke-text-primary"><use href="#icon-arrow-left"></use></svg>
|
|
뒤로
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="bg-gradient-to-br from-primary to-secondary rounded-[28px] p-8 text-center relative overflow-hidden shadow-glow mb-6">
|
|
<div class="streak-icon text-5xl mb-2 animate-pulse-slow" id="streakIcon">🔥</div>
|
|
<div class="font-display text-7xl font-extrabold text-white leading-none drop-shadow-lg" id="streakCount">0</div>
|
|
<div class="text-xl font-bold text-white/95 uppercase tracking-[2px] mt-1">STREAK!</div>
|
|
<div class="text-white/90 text-sm mt-4" id="streakMessage">오늘 시작하세요!</div>
|
|
</div>
|
|
|
|
<div class="bg-surface-card rounded-[20px] p-5 mb-5 shadow-sm hover:-translate-y-0.5 hover:shadow-md transition-all duration-250">
|
|
<div class="text-xs font-semibold text-text-secondary uppercase tracking-wider mb-4 flex items-center gap-2">
|
|
<span class="w-1 h-4 bg-gradient-to-b from-primary to-secondary rounded-sm"></span>
|
|
주간 목표
|
|
</div>
|
|
<div class="mb-4">
|
|
<div class="flex justify-between items-center mb-3">
|
|
<span class="text-sm text-text-primary font-medium">이번 주 완료된 일수</span>
|
|
<span class="text-sm text-primary font-semibold"><span id="daysCompleted">0</span>/7</span>
|
|
</div>
|
|
<div class="h-3 bg-surface-secondary rounded-full overflow-hidden relative">
|
|
<div class="h-full bg-gradient-to-r from-primary to-accent rounded-full w-0 transition-all duration-1000 relative" id="progressFill"></div>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-7 gap-2 mt-4" id="weekGrid">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-surface-card rounded-[20px] p-5 mb-5 shadow-sm hover:-translate-y-0.5 hover:shadow-md transition-all duration-250">
|
|
<div class="text-xs font-semibold text-text-secondary uppercase tracking-wider mb-4 flex items-center gap-2">
|
|
<span class="w-1 h-4 bg-gradient-to-b from-primary to-secondary rounded-sm"></span>
|
|
업적
|
|
</div>
|
|
<div class="grid grid-cols-4 gap-3" id="achievementsGrid">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-surface-card rounded-[20px] p-5 mb-5 shadow-sm hover:-translate-y-0.5 hover:shadow-md transition-all duration-250">
|
|
<div class="text-xs font-semibold text-text-secondary uppercase tracking-wider mb-4 flex items-center gap-2">
|
|
<span class="w-1 h-4 bg-gradient-to-b from-primary to-secondary rounded-sm"></span>
|
|
오늘의 팁
|
|
</div>
|
|
<div class="bg-gradient-to-br from-surface-secondary to-surface-card rounded-xl p-4 flex gap-3 items-start">
|
|
<span class="text-2xl shrink-0">💡</span>
|
|
<div class="flex-1">
|
|
<p class="text-sm text-text-primary leading-relaxed" id="tipText">작은 걸음이 큰 변화를 만듭니다! 오늘 5분만 투자하세요.</p>
|
|
<p class="text-xs text-text-muted mt-2">— 오늘의 동기</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-4 bg-surface-card rounded-[20px] shadow-sm">
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-2xl">🔔</span>
|
|
<div>
|
|
<div class="text-sm font-medium text-text-primary">습관 알림</div>
|
|
<div class="text-xs text-text-muted mt-0.5">매일 알림 받기</div>
|
|
</div>
|
|
</div>
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="habitToggle">
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="fixed bottom-[100px] left-1/2 -translate-x-1/2 translate-y-[100px] bg-text-primary text-surface-card px-6 py-4 rounded-full text-sm font-medium shadow-lg opacity-0 transition-all duration-500 z-[100] flex items-center gap-2" id="toast">
|
|
<span id="toastIcon">🎉</span>
|
|
<span id="toastText">업적 달성!</span>
|
|
</div>
|
|
|
|
<div class="fixed top-0 left-0 w-full h-full pointer-events-none z-[1000] overflow-hidden" id="confettiContainer"></div>
|
|
|
|
<script>
|
|
// Data Management
|
|
const STORAGE_KEY = 'chackly_gamification';
|
|
|
|
const defaultData = {
|
|
streak: 12,
|
|
weeklyProgress: [true, true, true, true, false, false, false],
|
|
achievements: [
|
|
{ id: 'first_sort', name: '첫 정리', icon: '🏆', unlocked: true, tier: 'gold', requirement: '첫 정리 세션 완료' },
|
|
{ id: 'week_warrior', name: '주간 챌린저', icon: '🌟', unlocked: true, tier: 'gold', requirement: '7일 연속 완료' },
|
|
{ id: 'streak_7', name: '7일 스트릭', icon: '🔥', unlocked: true, tier: 'silver', requirement: '7일 스트릭 유지' },
|
|
{ id: 'diamond', name: '다이아몬드', icon: '💎', unlocked: true, tier: 'gold', requirement: '30일 스트릭 유지' },
|
|
{ id: 'organizer', name: '정리 달인', icon: '📦', unlocked: false, tier: 'silver', requirement: '100개 파일 정리' },
|
|
{ id: 'minimalist', name: '미니멀리스트', icon: '✨', unlocked: false, tier: 'bronze', requirement: '50개 파일 삭제' },
|
|
{ id: 'speed_demon', name: '스피드 데몬', icon: '⚡', unlocked: false, tier: 'gold', requirement: '하루에 50개 파일 정리' },
|
|
{ id: 'collector', name: '수집가', icon: '🗂️', unlocked: false, tier: 'silver', requirement: '5개 커스텀 폴더 생성' }
|
|
],
|
|
habitReminderEnabled: true,
|
|
lastUpdated: new Date().toISOString()
|
|
};
|
|
|
|
function loadData() {
|
|
const stored = localStorage.getItem(STORAGE_KEY);
|
|
if (stored) {
|
|
return { ...defaultData, ...JSON.parse(stored) };
|
|
}
|
|
return defaultData;
|
|
}
|
|
|
|
function saveData(data) {
|
|
data.lastUpdated = new Date().toISOString();
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
}
|
|
|
|
// UI Elements
|
|
const streakCount = document.getElementById('streakCount');
|
|
const streakIcon = document.getElementById('streakIcon');
|
|
const streakMessage = document.getElementById('streakMessage');
|
|
const progressFill = document.getElementById('progressFill');
|
|
const daysCompleted = document.getElementById('daysCompleted');
|
|
const weekGrid = document.getElementById('weekGrid');
|
|
const achievementsGrid = document.getElementById('achievementsGrid');
|
|
const habitToggle = document.getElementById('habitToggle');
|
|
const toast = document.getElementById('toast');
|
|
const toastIcon = document.getElementById('toastIcon');
|
|
const toastText = document.getElementById('toastText');
|
|
const confettiContainer = document.getElementById('confettiContainer');
|
|
|
|
const motivationalMessages = [
|
|
"불을 이어가세요!",
|
|
"오늘 불이 나고 있어요!",
|
|
"놀라운 일관성!",
|
|
"아무도 당신을 막을 수 없어요!",
|
|
"챔피언의 행동!"
|
|
];
|
|
|
|
const dailyTips = [
|
|
{ text: "작은 걸음이 큰 변화를 만듭니다! 오늘 5분만 투자하세요.", author: "일일 동기" },
|
|
{ text: "정돈된 공간은 정돈된 마음을 만듭니다. 작게 시작하세요!", author: "정리의 지혜" },
|
|
{ text: "정리된 모든 파일이 진보입니다.庆祝하세요!", author: "업적 달성" },
|
|
{ text: "미래의 당신이 오늘 정리한 것에 감사할 것입니다.", author: "타임 트래블러" },
|
|
{ text: "일관성이 완벽함을 이깁니다. 매일来吧!", author: "습관 마스터" }
|
|
];
|
|
|
|
// Animate streak count
|
|
function animateCount(element, target, duration = 1500) {
|
|
const start = 0;
|
|
const startTime = performance.now();
|
|
|
|
function update(currentTime) {
|
|
const elapsed = currentTime - startTime;
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
const easeOut = 1 - Math.pow(1 - progress, 3);
|
|
const current = Math.floor(start + (target - start) * easeOut);
|
|
element.textContent = current;
|
|
|
|
if (progress < 1) {
|
|
requestAnimationFrame(update);
|
|
}
|
|
}
|
|
|
|
requestAnimationFrame(update);
|
|
}
|
|
|
|
// Render week grid
|
|
function renderWeekGrid(progress) {
|
|
const days = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
|
|
const today = new Date().getDay();
|
|
const mondayIndex = today === 0 ? 6 : today - 1;
|
|
|
|
weekGrid.innerHTML = days.map((day, i) => {
|
|
const completed = progress[i];
|
|
const isToday = i === mondayIndex;
|
|
return `
|
|
<div class="week-day">
|
|
<div class="week-day-label">${day}</div>
|
|
<div class="week-day-indicator ${completed ? 'completed' : ''} ${isToday ? 'today' : ''}">
|
|
${completed ? '✅' : (isToday ? '◉' : '')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// Render achievements
|
|
function renderAchievements(achievements) {
|
|
achievementsGrid.innerHTML = achievements.map(ach => `
|
|
<div class="achievement ${ach.unlocked ? `unlocked ${ach.tier}` : 'locked'}" data-id="${ach.id}">
|
|
<span class="achievement-icon">${ach.icon}</span>
|
|
<span class="achievement-name">${ach.name}</span>
|
|
<div class="achievement-tooltip">${ach.requirement}</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Show toast notification
|
|
function showToast(icon, message) {
|
|
toastIcon.textContent = icon;
|
|
toastText.textContent = message;
|
|
toast.classList.add('show');
|
|
|
|
setTimeout(() => {
|
|
toast.classList.remove('show');
|
|
}, 3000);
|
|
}
|
|
|
|
// Confetti celebration
|
|
function triggerConfetti() {
|
|
const colors = ['#6366f1', '#f472b6', '#34d399', '#fbbf24', '#818cf8', '#34d399'];
|
|
const confettiCount = 50;
|
|
|
|
for (let i = 0; i < confettiCount; i++) {
|
|
const confetti = document.createElement('div');
|
|
confetti.className = 'confetti';
|
|
confetti.style.left = Math.random() * 100 + 'vw';
|
|
confetti.style.background = colors[Math.floor(Math.random() * colors.length)];
|
|
confetti.style.borderRadius = Math.random() > 0.5 ? '50%' : '0';
|
|
confetti.style.width = Math.random() * 10 + 5 + 'px';
|
|
confetti.style.height = confetti.style.width;
|
|
confetti.style.animation = `confettiFall ${Math.random() * 2 + 2}s ease-out forwards`;
|
|
confetti.style.animationDelay = Math.random() * 0.5 + 's';
|
|
|
|
confettiContainer.appendChild(confetti);
|
|
|
|
setTimeout(() => {
|
|
confetti.remove();
|
|
}, 4000);
|
|
}
|
|
}
|
|
|
|
// Trigger badge unlock animation
|
|
function animateBadgeUnlock(achievementId) {
|
|
const badge = document.querySelector(`.achievement[data-id="${achievementId}"]`);
|
|
if (badge) {
|
|
badge.classList.add('just-unlocked');
|
|
|
|
// Add sparkles
|
|
for (let i = 0; i < 6; i++) {
|
|
const sparkle = document.createElement('span');
|
|
sparkle.className = 'sparkle';
|
|
sparkle.textContent = '✨';
|
|
sparkle.style.left = Math.random() * 100 + '%';
|
|
sparkle.style.top = Math.random() * 100 + '%';
|
|
badge.appendChild(sparkle);
|
|
}
|
|
|
|
setTimeout(() => {
|
|
badge.classList.remove('just-unlocked');
|
|
badge.querySelectorAll('.sparkle').forEach(s => s.remove());
|
|
}, 600);
|
|
}
|
|
}
|
|
|
|
// Initialize page
|
|
function init() {
|
|
const data = loadData();
|
|
|
|
// Animate streak count
|
|
animateCount(streakCount, data.streak);
|
|
|
|
// Streak icon animation
|
|
if (data.streak > 0) {
|
|
streakIcon.classList.add('animate');
|
|
setTimeout(() => streakIcon.classList.remove('animate'), 1000);
|
|
}
|
|
|
|
// Update streak message
|
|
if (data.streak > 0) {
|
|
streakMessage.textContent = motivationalMessages[Math.floor(Math.random() * motivationalMessages.length)];
|
|
}
|
|
|
|
// Animate progress bar
|
|
const completedDays = data.weeklyProgress.filter(Boolean).length;
|
|
setTimeout(() => {
|
|
progressFill.style.width = (completedDays / 7 * 100) + '%';
|
|
}, 300);
|
|
daysCompleted.textContent = completedDays;
|
|
|
|
// Render week grid
|
|
renderWeekGrid(data.weeklyProgress);
|
|
|
|
// Render achievements
|
|
renderAchievements(data.achievements);
|
|
|
|
// Set habit toggle state
|
|
habitToggle.checked = data.habitReminderEnabled;
|
|
|
|
// Random daily tip
|
|
const randomTip = dailyTips[Math.floor(Math.random() * dailyTips.length)];
|
|
document.getElementById('tipText').textContent = `"${randomTip.text}"`;
|
|
document.querySelector('.tip-author').textContent = `— ${randomTip.author}`;
|
|
|
|
// Habit toggle handler
|
|
habitToggle.addEventListener('change', (e) => {
|
|
data.habitReminderEnabled = e.target.checked;
|
|
saveData(data);
|
|
showToast(e.target.checked ? '🔔' : '🔕', e.target.checked ? 'Reminders enabled!' : 'Reminders disabled');
|
|
});
|
|
|
|
// Achievement click handler (for demo unlock)
|
|
achievementsGrid.addEventListener('click', (e) => {
|
|
const achievement = e.target.closest('.achievement');
|
|
if (achievement && !achievement.classList.contains('unlocked')) {
|
|
// Simulate unlock for demo
|
|
const achId = achievement.dataset.id;
|
|
const achData = data.achievements.find(a => a.id === achId);
|
|
if (achData) {
|
|
achData.unlocked = true;
|
|
saveData(data);
|
|
animateBadgeUnlock(achId);
|
|
triggerConfetti();
|
|
showToast(achData.icon, `${achData.name} unlocked!`);
|
|
renderAchievements(data.achievements);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Run on load
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
</script>
|
|
</body>
|
|
</html> |