| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422 |
- <?php
- ini_set('display_errors', 1); ini_set('display_startup_errors', 1); error_reporting(E_ALL);
- // packing.php – displays the packing list for a given hash
- require_once __DIR__ . '/db.php';
- $pdo = get_pdo();
- $hash = isset($_GET['h']) ? trim($_GET['h']) : '';
- if (!preg_match('/^[a-f0-9]{64}$/', $hash)) {
- http_response_code(400);
- die('<p>Invalid or missing list hash.</p>');
- }
- // Resolve hash → activity IDs
- $stmt = $pdo->prepare("SELECT activity_ids FROM selection_hashes WHERE hash = ?");
- $stmt->execute([$hash]);
- $row = $stmt->fetch();
- if (!$row) {
- http_response_code(404);
- die('<p>List not found. It may have expired or the link is wrong.</p>');
- }
- $activityIds = array_map('intval', explode(',', $row['activity_ids']));
- if (empty($activityIds)) {
- die('<p>No activities stored for this hash.</p>');
- }
- // Fetch the selected activity names for display
- $ph = implode(',', array_fill(0, count($activityIds), '?'));
- $stmt = $pdo->prepare("
- SELECT a.activityName
- FROM activities a
- WHERE a.activityID IN ($ph)
- ORDER BY a.activityName
- ");
- $stmt->execute($activityIds);
- $activityNames = $stmt->fetchAll(PDO::FETCH_COLUMN);
- // Get all items for these activities, deduplicated, grouped by item_group
- $stmt = $pdo->prepare("
- SELECT DISTINCT
- ig.groupID,
- ig.groupName,
- ig.sortOrder AS groupSort,
- i.itemID,
- i.itemName,
- i.sortOrder AS itemSort
- FROM activity_item_map aim
- JOIN items i ON i.itemID = aim.itemID
- JOIN item_groups ig ON ig.groupID = i.groupID
- WHERE aim.activityID IN ($ph)
- ORDER BY ig.sortOrder, ig.groupID, i.sortOrder, i.itemID
- ");
- $stmt->execute($activityIds);
- $allItems = $stmt->fetchAll();
- // Group items by item_group
- $grouped = [];
- foreach ($allItems as $item) {
- $gid = $item['groupID'];
- if (!isset($grouped[$gid])) {
- $grouped[$gid] = ['name' => $item['groupName'], 'items' => []];
- }
- $grouped[$gid]['items'][] = ['id' => $item['itemID'], 'name' => $item['itemName']];
- }
- $pageUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http')
- . '://' . $_SERVER['HTTP_HOST']
- . $_SERVER['REQUEST_URI'];
- $totalItems = count($allItems);
- ?>
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>PackIt – Your Packing List</title>
- <link rel="preconnect" href="https://fonts.googleapis.com">
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
- <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
- <style>
- :root {
- --bg: #0f1117;
- --surface: #181c27;
- --border: #2a2f3f;
- --accent: #e8c547;
- --accent2: #4fd1c5;
- --text: #e8eaf0;
- --muted: #7a8099;
- --green: #5cb85c;
- --radius: 10px;
- --gap: 1.5rem;
- }
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
- body {
- background: var(--bg);
- color: var(--text);
- font-family: 'DM Sans', sans-serif;
- font-size: 15px;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- }
- header {
- padding: 2rem var(--gap) 1.5rem;
- border-bottom: 1px solid var(--border);
- display: flex;
- align-items: flex-end;
- justify-content: space-between;
- gap: 1rem;
- flex-wrap: wrap;
- position: relative;
- overflow: hidden;
- }
- header::before {
- content: '';
- position: absolute;
- inset: 0;
- background: radial-gradient(ellipse 60% 80% at 90% 50%, rgba(79,209,197,.06) 0%, transparent 70%);
- pointer-events: none;
- }
- .header-left { display: flex; align-items: flex-end; gap: 1rem; position: relative; }
- .logo {
- font-family: 'Bebas Neue', sans-serif;
- font-size: clamp(2.2rem, 5vw, 3.5rem);
- letter-spacing: .04em;
- color: var(--accent);
- line-height: 1;
- }
- .header-meta {
- font-size: .8rem;
- color: var(--muted);
- padding-bottom: .2rem;
- }
- .header-meta strong { color: var(--accent2); }
- .btn-row {
- display: flex;
- gap: .6rem;
- flex-wrap: wrap;
- position: relative;
- }
- .btn {
- display: inline-flex; align-items: center; gap: .5rem;
- padding: .55rem 1.1rem;
- border-radius: var(--radius);
- font-family: 'DM Sans', sans-serif;
- font-size: .82rem;
- font-weight: 500;
- letter-spacing: .03em;
- cursor: pointer;
- border: 1.5px solid transparent;
- transition: all .15s;
- text-decoration: none;
- }
- .btn-accent {
- background: var(--accent); color: #0f1117; border-color: var(--accent);
- }
- .btn-accent:hover { background: #f0d260; }
- .btn-outline {
- background: transparent; color: var(--text); border-color: var(--border);
- }
- .btn-outline:hover { border-color: var(--muted); }
- .btn-teal {
- background: transparent; color: var(--accent2); border-color: var(--accent2);
- }
- .btn-teal:hover { background: rgba(79,209,197,.08); }
- main {
- flex: 1;
- max-width: 760px;
- width: 100%;
- margin: 0 auto;
- padding: 2.5rem var(--gap) 3rem;
- }
- /* Activities pills */
- .activity-pills {
- display: flex;
- flex-wrap: wrap;
- gap: .4rem;
- margin-bottom: 2.5rem;
- }
- .pill {
- background: rgba(232,197,71,.1);
- border: 1px solid rgba(232,197,71,.3);
- color: var(--accent);
- font-size: .75rem;
- font-weight: 500;
- padding: .3rem .75rem;
- border-radius: 999px;
- letter-spacing: .03em;
- }
- /* Progress bar */
- .progress-bar-wrap {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- padding: 1rem 1.25rem;
- margin-bottom: 2.5rem;
- display: flex;
- align-items: center;
- gap: 1.5rem;
- }
- .progress-track {
- flex: 1;
- height: 6px;
- background: var(--border);
- border-radius: 999px;
- overflow: hidden;
- }
- .progress-fill {
- height: 100%;
- background: linear-gradient(90deg, var(--accent2), var(--accent));
- border-radius: 999px;
- width: 0%;
- transition: width .3s ease;
- }
- .progress-label {
- font-size: .82rem;
- color: var(--muted);
- white-space: nowrap;
- }
- .progress-label strong { color: var(--accent); font-variant-numeric: tabular-nums; }
- .section-title {
- font-family: 'Bebas Neue', sans-serif;
- font-size: 1.1rem;
- letter-spacing: .12em;
- color: var(--accent2);
- text-transform: uppercase;
- margin-bottom: .75rem;
- display: flex;
- align-items: center;
- gap: .6rem;
- }
- .section-title::after {
- content: '';
- flex: 1;
- height: 1px;
- background: var(--border);
- }
- .item-group {
- margin-bottom: 2rem;
- }
- .item-list {
- display: flex;
- flex-direction: column;
- gap: .4rem;
- }
- .item-row {
- display: flex;
- align-items: center;
- gap: .75rem;
- padding: .6rem 1rem;
- background: var(--surface);
- border: 1.5px solid var(--border);
- border-radius: var(--radius);
- cursor: pointer;
- transition: border-color .15s, background .15s;
- user-select: none;
- }
- .item-row:hover { background: #1e2333; }
- .item-row.checked {
- border-color: rgba(92,184,92,.5);
- background: rgba(92,184,92,.05);
- }
- .item-row.checked .item-name {
- text-decoration: line-through;
- color: var(--muted);
- }
- .item-check {
- width: 20px; height: 20px;
- border: 1.5px solid var(--border);
- border-radius: 5px;
- flex-shrink: 0;
- display: flex; align-items: center; justify-content: center;
- background: var(--bg);
- transition: all .15s;
- }
- .item-row.checked .item-check {
- background: var(--green);
- border-color: var(--green);
- }
- .item-check svg { display: none; }
- .item-row.checked .item-check svg { display: block; }
- .item-name { flex: 1; font-size: .95rem; }
- /* Share box */
- .share-box {
- background: var(--surface);
- border: 1.5px solid var(--border);
- border-radius: var(--radius);
- padding: 1.25rem 1.5rem;
- margin-bottom: 2.5rem;
- display: flex;
- gap: .75rem;
- align-items: center;
- flex-wrap: wrap;
- }
- .share-url {
- flex: 1;
- font-size: .8rem;
- color: var(--muted);
- font-family: monospace;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 0;
- }
- .copy-feedback {
- font-size: .8rem;
- color: var(--green);
- opacity: 0;
- transition: opacity .3s;
- }
- .copy-feedback.show { opacity: 1; }
- @media print {
- header .btn-row, .share-box, .progress-bar-wrap { display: none !important; }
- body { background: #fff; color: #000; }
- .item-row { border-color: #ccc; background: none; }
- .section-title { color: #333; }
- .pill { background: #f5f5f5; color: #333; border-color: #ccc; }
- }
- </style>
- </head>
- <body>
- <header>
- <div class="header-left">
- <div class="logo">PackIt</div>
- <div class="header-meta">
- <strong><?= $totalItems ?></strong> items ·
- <strong><?= count($activityNames) ?></strong> activities
- </div>
- </div>
- <div class="btn-row">
- <a href="index.php" class="btn btn-outline">← New list</a>
- <button class="btn btn-teal" onclick="window.print()">🖨 Print</button>
- <button class="btn btn-accent" onclick="copyLink()">🔗 Copy link</button>
- </div>
- </header>
- <main>
- <!-- Activity pills -->
- <div class="activity-pills">
- <?php foreach ($activityNames as $n): ?>
- <span class="pill"><?= htmlspecialchars($n) ?></span>
- <?php endforeach; ?>
- </div>
- <!-- Share box -->
- <div class="share-box">
- <span class="share-url" id="shareUrl"><?= htmlspecialchars($pageUrl) ?></span>
- <span class="copy-feedback" id="copyFb">Copied!</span>
- <button class="btn btn-teal" onclick="copyLink()">Copy link</button>
- </div>
- <!-- Progress -->
- <div class="progress-bar-wrap">
- <div class="progress-track"><div class="progress-fill" id="progressFill"></div></div>
- <div class="progress-label"><strong id="checkedCnt">0</strong> / <?= $totalItems ?> packed</div>
- </div>
- <!-- Grouped item list -->
- <?php foreach ($grouped as $gid => $group): ?>
- <div class="item-group">
- <div class="section-title"><?= htmlspecialchars($group['name']) ?></div>
- <div class="item-list">
- <?php foreach ($group['items'] as $item): ?>
- <div class="item-row" onclick="toggleItem(this)">
- <div class="item-check">
- <svg width="10" height="8" viewBox="0 0 10 8" fill="none">
- <path d="M1 4l3 3 5-6" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>
- </div>
- <span class="item-name"><?= htmlspecialchars($item['name']) ?></span>
- </div>
- <?php endforeach; ?>
- </div>
- </div>
- <?php endforeach; ?>
- </main>
- <script>
- const total = <?= $totalItems ?>;
- function toggleItem(el) {
- el.classList.toggle('checked');
- updateProgress();
- }
- function updateProgress() {
- const checked = document.querySelectorAll('.item-row.checked').length;
- document.getElementById('checkedCnt').textContent = checked;
- document.getElementById('progressFill').style.width = (checked / total * 100) + '%';
- }
- function copyLink() {
- const url = document.getElementById('shareUrl').textContent;
- navigator.clipboard.writeText(url).then(() => {
- const fb = document.getElementById('copyFb');
- fb.classList.add('show');
- setTimeout(() => fb.classList.remove('show'), 2000);
- }).catch(() => {
- // Fallback
- const ta = document.createElement('textarea');
- ta.value = url;
- document.body.appendChild(ta);
- ta.select();
- document.execCommand('copy');
- document.body.removeChild(ta);
- });
- }
- </script>
- </body>
- </html>
|