510 lines
14 KiB
PHP
510 lines
14 KiB
PHP
<?php
|
|
require_once __DIR__ . '/db.php';
|
|
|
|
$items = $pdo->query("SELECT * FROM training_items ORDER BY sort_order ASC")->fetchAll();
|
|
$today = date('Y-m-d');
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Pawgress 🐾 Daily Training</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&display=swap" rel="stylesheet" />
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--orange: #FF7B2C;
|
|
--yellow: #FFD93D;
|
|
--green: #4CAF82;
|
|
--red: #FF5C5C;
|
|
--bg: #FFF8F0;
|
|
--card: #FFFFFF;
|
|
--text: #2D2D2D;
|
|
--muted: #8A8A8A;
|
|
--border: #F0E6D8;
|
|
--shadow: 0 4px 24px rgba(0,0,0,0.07);
|
|
--radius: 16px;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Nunito', sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* — HEADER — */
|
|
.header {
|
|
background: linear-gradient(135deg, #FF7B2C 0%, #FF9A5C 50%, #FFD93D 100%);
|
|
padding: 28px 24px 80px;
|
|
text-align: center;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.header::before {
|
|
content: '';
|
|
position: absolute; inset: 0;
|
|
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.06'%3E%3Ccircle cx='30' cy='30' r='20'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
|
pointer-events: none;
|
|
}
|
|
.header-logo {
|
|
font-size: 52px;
|
|
display: block;
|
|
margin-bottom: 6px;
|
|
animation: bounce 2s infinite;
|
|
}
|
|
@keyframes bounce {
|
|
0%,100% { transform: translateY(0); }
|
|
50% { transform: translateY(-8px); }
|
|
}
|
|
.header h1 {
|
|
font-size: 2.4rem;
|
|
font-weight: 900;
|
|
color: #fff;
|
|
letter-spacing: -0.5px;
|
|
text-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
}
|
|
.header p {
|
|
color: rgba(255,255,255,0.88);
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* — PROGRESS CARD — */
|
|
.progress-card {
|
|
background: var(--card);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
padding: 24px;
|
|
margin: -52px 20px 24px;
|
|
position: relative;
|
|
z-index: 10;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20px;
|
|
}
|
|
.progress-ring-wrap {
|
|
position: relative;
|
|
flex-shrink: 0;
|
|
width: 90px;
|
|
height: 90px;
|
|
}
|
|
.progress-ring {
|
|
transform: rotate(-90deg);
|
|
width: 90px;
|
|
height: 90px;
|
|
}
|
|
.progress-ring circle {
|
|
fill: none;
|
|
stroke-width: 8;
|
|
stroke-linecap: round;
|
|
transition: stroke-dashoffset 0.5s ease, stroke 0.5s ease;
|
|
}
|
|
.ring-bg { stroke: #F0E6D8; }
|
|
.ring-fill { stroke: var(--orange); stroke-dasharray: 226; stroke-dashoffset: 226; }
|
|
.ring-emoji {
|
|
position: absolute;
|
|
top: 50%; left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
font-size: 28px;
|
|
line-height: 1;
|
|
}
|
|
.progress-info { flex: 1; }
|
|
.progress-label {
|
|
font-size: 0.8rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
color: var(--muted);
|
|
margin-bottom: 4px;
|
|
}
|
|
.progress-count {
|
|
font-size: 2rem;
|
|
font-weight: 900;
|
|
color: var(--text);
|
|
line-height: 1;
|
|
}
|
|
.progress-count span { font-size: 1rem; color: var(--muted); font-weight: 600; }
|
|
.progress-bar-wrap {
|
|
background: var(--border);
|
|
border-radius: 99px;
|
|
height: 8px;
|
|
margin-top: 10px;
|
|
overflow: hidden;
|
|
}
|
|
.progress-bar {
|
|
height: 100%;
|
|
border-radius: 99px;
|
|
background: linear-gradient(90deg, var(--orange), var(--yellow));
|
|
transition: width 0.5s ease;
|
|
width: 0%;
|
|
}
|
|
.progress-msg {
|
|
font-size: 0.82rem;
|
|
color: var(--muted);
|
|
font-weight: 600;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
/* — MAIN CONTENT — */
|
|
.container {
|
|
max-width: 640px;
|
|
margin: 0 auto;
|
|
padding: 0 20px 100px;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 0.78rem;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1.2px;
|
|
color: var(--muted);
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
/* — TASK CARDS — */
|
|
.task-list { display: flex; flex-direction: column; gap: 10px; }
|
|
|
|
.task-card {
|
|
background: var(--card);
|
|
border-radius: var(--radius);
|
|
box-shadow: 0 2px 12px rgba(0,0,0,0.05);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
padding: 16px 18px;
|
|
cursor: pointer;
|
|
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.2s ease;
|
|
border: 2px solid transparent;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.task-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0; top: 0; bottom: 0;
|
|
width: 5px;
|
|
background: var(--border);
|
|
transition: background 0.3s ease;
|
|
border-radius: 4px 0 0 4px;
|
|
}
|
|
.task-card:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,0.09); }
|
|
.task-card.done { background: #F6FFF9; border-color: #D4EFDF; }
|
|
.task-card.done::before { background: var(--green); }
|
|
|
|
.task-checkbox {
|
|
width: 26px; height: 26px;
|
|
border-radius: 50%;
|
|
border: 2.5px solid var(--border);
|
|
display: flex; align-items: center; justify-content: center;
|
|
flex-shrink: 0;
|
|
transition: all 0.2s ease;
|
|
background: #fff;
|
|
}
|
|
.task-card.done .task-checkbox {
|
|
background: var(--green);
|
|
border-color: var(--green);
|
|
}
|
|
.task-checkbox svg { display: none; }
|
|
.task-card.done .task-checkbox svg { display: block; }
|
|
|
|
.task-body { flex: 1; }
|
|
.task-title {
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
color: var(--text);
|
|
transition: color 0.2s, text-decoration 0.2s;
|
|
}
|
|
.task-card.done .task-title {
|
|
color: var(--muted);
|
|
text-decoration: line-through;
|
|
}
|
|
.task-desc {
|
|
font-size: 0.82rem;
|
|
color: var(--muted);
|
|
font-weight: 600;
|
|
margin-top: 2px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* — COMPLETE BANNER — */
|
|
.complete-banner {
|
|
background: linear-gradient(135deg, #4CAF82, #38D996);
|
|
border-radius: var(--radius);
|
|
padding: 20px 24px;
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
display: none;
|
|
animation: popIn 0.4s cubic-bezier(0.34,1.56,0.64,1);
|
|
}
|
|
.complete-banner.show { display: block; }
|
|
@keyframes popIn {
|
|
from { transform: scale(0.8); opacity: 0; }
|
|
to { transform: scale(1); opacity: 1; }
|
|
}
|
|
.complete-banner h2 {
|
|
font-size: 1.4rem;
|
|
font-weight: 900;
|
|
color: #fff;
|
|
margin-bottom: 4px;
|
|
}
|
|
.complete-banner p { color: rgba(255,255,255,0.88); font-weight: 600; font-size: 0.9rem; }
|
|
|
|
/* — RESET BUTTON — */
|
|
.reset-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
background: #fff;
|
|
border: 2px solid var(--border);
|
|
color: var(--muted);
|
|
font-family: 'Nunito', sans-serif;
|
|
font-size: 0.9rem;
|
|
font-weight: 700;
|
|
padding: 12px 24px;
|
|
border-radius: 99px;
|
|
cursor: pointer;
|
|
margin: 20px auto 0;
|
|
transition: all 0.2s ease;
|
|
width: 100%;
|
|
max-width: 280px;
|
|
}
|
|
.reset-btn:hover { border-color: var(--red); color: var(--red); background: #FFF5F5; }
|
|
|
|
/* — FOOTER NAV — */
|
|
.footer-nav {
|
|
position: fixed;
|
|
bottom: 0; left: 0; right: 0;
|
|
background: #fff;
|
|
border-top: 1px solid var(--border);
|
|
padding: 12px 24px 20px;
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 16px;
|
|
z-index: 100;
|
|
}
|
|
.footer-nav a {
|
|
font-size: 0.8rem;
|
|
font-weight: 700;
|
|
color: var(--muted);
|
|
text-decoration: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 8px 18px;
|
|
border-radius: 99px;
|
|
transition: all 0.2s;
|
|
}
|
|
.footer-nav a:hover { background: var(--bg); color: var(--orange); }
|
|
.footer-nav a.active { background: #FFF0E6; color: var(--orange); }
|
|
|
|
/* — EMPTY STATE — */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 48px 24px;
|
|
color: var(--muted);
|
|
}
|
|
.empty-state .icon { font-size: 56px; margin-bottom: 12px; }
|
|
.empty-state h3 { font-size: 1.2rem; font-weight: 800; margin-bottom: 6px; color: var(--text); }
|
|
.empty-state p { font-size: 0.9rem; font-weight: 600; }
|
|
.empty-state a {
|
|
display: inline-block;
|
|
margin-top: 16px;
|
|
background: var(--orange);
|
|
color: #fff;
|
|
text-decoration: none;
|
|
padding: 10px 24px;
|
|
border-radius: 99px;
|
|
font-weight: 800;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.header h1 { font-size: 1.9rem; }
|
|
.progress-card { flex-direction: column; text-align: center; }
|
|
.progress-info { width: 100%; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="header">
|
|
<span class="header-logo">🐾</span>
|
|
<h1>Pawgress</h1>
|
|
<p><?= date('l, F j') ?> • Daily Training</p>
|
|
</div>
|
|
|
|
<div class="container">
|
|
|
|
<!-- Progress Card -->
|
|
<div class="progress-card">
|
|
<div class="progress-ring-wrap">
|
|
<svg class="progress-ring" viewBox="0 0 90 90">
|
|
<circle class="ring-bg" cx="45" cy="45" r="36" />
|
|
<circle class="ring-fill" cx="45" cy="45" r="36" id="ringFill" />
|
|
</svg>
|
|
<span class="ring-emoji" id="ringEmoji">🐶</span>
|
|
</div>
|
|
<div class="progress-info">
|
|
<div class="progress-label">Today's Progress</div>
|
|
<div class="progress-count" id="progressCount">0 <span>/ <?= count($items) ?></span></div>
|
|
<div class="progress-bar-wrap">
|
|
<div class="progress-bar" id="progressBar"></div>
|
|
</div>
|
|
<div class="progress-msg" id="progressMsg">Let's get training! 💪</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Complete Banner -->
|
|
<div class="complete-banner" id="completeBanner">
|
|
<h2>🎉 Amazing Work!</h2>
|
|
<p>You crushed every training task today. Your pup is so proud!</p>
|
|
</div>
|
|
|
|
<!-- Task List -->
|
|
<?php if (empty($items)): ?>
|
|
<div class="empty-state">
|
|
<div class="icon">🐕</div>
|
|
<h3>No Training Items Yet</h3>
|
|
<p>Head to the admin panel to add your first training tasks.</p>
|
|
<a href="/admin.php">Go to Admin →</a>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="section-title">Training Checklist</div>
|
|
<div class="task-list" id="taskList">
|
|
<?php foreach ($items as $item): ?>
|
|
<div class="task-card" data-id="<?= $item['id'] ?>" onclick="toggleTask(this)">
|
|
<div class="task-checkbox">
|
|
<svg width="14" height="11" viewBox="0 0 14 11" fill="none">
|
|
<path d="M1.5 5.5L5.5 9.5L12.5 1.5" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
</div>
|
|
<div class="task-body">
|
|
<div class="task-title"><?= htmlspecialchars($item['title']) ?></div>
|
|
<?php if (!empty($item['description'])): ?>
|
|
<div class="task-desc"><?= nl2br(str_replace('&bull;', '•', htmlspecialchars($item['description']))) ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<button class="reset-btn" onclick="resetProgress()">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-3.29"/>
|
|
</svg>
|
|
Reset Today's Progress
|
|
</button>
|
|
<?php endif; ?>
|
|
|
|
</div>
|
|
|
|
<nav class="footer-nav">
|
|
<a href="/" class="active">🏠 Today</a>
|
|
<a href="/admin.php">⚙️ Admin</a>
|
|
</nav>
|
|
|
|
<script>
|
|
const TODAY = '<?= $today ?>';
|
|
const TOTAL = <?= count($items) ?>;
|
|
const COOKIE_KEY = 'pawgress_' + TODAY;
|
|
|
|
const messages = [
|
|
"Let's get training! 💪",
|
|
"Great start! Keep going! 🐶",
|
|
"You're on a roll! 🔥",
|
|
"Almost there! So close! ⭐",
|
|
"You're crushing it! 🏆",
|
|
];
|
|
|
|
const emojis = ['🐶','🏃','⭐','🏆'];
|
|
|
|
function getCookie(name) {
|
|
const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
|
|
return match ? JSON.parse(decodeURIComponent(match[1])) : [];
|
|
}
|
|
|
|
function setCookie(name, value) {
|
|
const midnight = new Date();
|
|
midnight.setHours(24, 0, 0, 0);
|
|
document.cookie = name + '=' + encodeURIComponent(JSON.stringify(value))
|
|
+ '; expires=' + midnight.toUTCString() + '; path=/';
|
|
}
|
|
|
|
function updateUI() {
|
|
const checked = getCookie(COOKIE_KEY);
|
|
let count = 0;
|
|
|
|
document.querySelectorAll('.task-card').forEach(card => {
|
|
const id = card.dataset.id;
|
|
if (checked.includes(id)) {
|
|
card.classList.add('done');
|
|
count++;
|
|
} else {
|
|
card.classList.remove('done');
|
|
}
|
|
});
|
|
|
|
const pct = TOTAL > 0 ? count / TOTAL : 0;
|
|
const circumference = 2 * Math.PI * 36;
|
|
const offset = circumference - (pct * circumference);
|
|
|
|
const ring = document.getElementById('ringFill');
|
|
ring.style.strokeDasharray = circumference;
|
|
ring.style.strokeDashoffset = offset;
|
|
|
|
// Color the ring
|
|
if (pct >= 1) ring.style.stroke = '#4CAF82';
|
|
else if (pct >= 0.5) ring.style.stroke = '#FFD93D';
|
|
else ring.style.stroke = '#FF7B2C';
|
|
|
|
// Emoji
|
|
const emojiIdx = pct >= 1 ? 3 : pct >= 0.66 ? 2 : pct >= 0.33 ? 1 : 0;
|
|
document.getElementById('ringEmoji').textContent = emojis[emojiIdx];
|
|
|
|
// Count
|
|
document.getElementById('progressCount').innerHTML = count + ' <span>/ ' + TOTAL + '</span>';
|
|
|
|
// Bar
|
|
document.getElementById('progressBar').style.width = (pct * 100) + '%';
|
|
|
|
// Message
|
|
const msgIdx = Math.min(Math.floor(pct * (messages.length - 1) + 0.01), messages.length - 1);
|
|
document.getElementById('progressMsg').textContent = messages[msgIdx];
|
|
|
|
// Complete banner
|
|
const banner = document.getElementById('completeBanner');
|
|
if (count === TOTAL && TOTAL > 0) {
|
|
banner.classList.add('show');
|
|
} else {
|
|
banner.classList.remove('show');
|
|
}
|
|
}
|
|
|
|
function toggleTask(card) {
|
|
const id = card.dataset.id;
|
|
let checked = getCookie(COOKIE_KEY);
|
|
if (checked.includes(id)) {
|
|
checked = checked.filter(x => x !== id);
|
|
} else {
|
|
checked.push(id);
|
|
}
|
|
setCookie(COOKIE_KEY, checked);
|
|
updateUI();
|
|
}
|
|
|
|
function resetProgress() {
|
|
setCookie(COOKIE_KEY, []);
|
|
updateUI();
|
|
}
|
|
|
|
// Init
|
|
updateUI();
|
|
</script>
|
|
</body>
|
|
</html>
|