Add flair badge gamification system with custom SVG badges
- Create 12 Office Space themed SVG badges (extraction, basement, printer, coffee, tps, bobs, memo, oface, stapler, conclusions, spreadsheet, flair-badge) - Implement FlairBadge.astro component with localStorage persistence - Add 15-second timer per page to earn flair - Create floating counter, modal flair board, toast notifications - Override Starlight Footer to inject flair system globally - Add name capture dialog on first flair earned Features: - Badges glow golden when earned, grayscale when locked - Progress persists across browser sessions - Stan quote: "We need to talk about your document processing..."
@ -11,6 +11,9 @@ export default defineConfig({
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'mcwaddams',
|
||||
components: {
|
||||
Footer: './src/components/Footer.astro',
|
||||
},
|
||||
tagline: 'I was told there would be document extraction.',
|
||||
logo: {
|
||||
src: './src/assets/stapler.svg',
|
||||
|
||||
12
public/flair/basement.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "Basement Dweller" - Basement window with stapler silhouette -->
|
||||
<rect x="8" y="8" width="32" height="32" rx="2" fill="#1e293b"/>
|
||||
<!-- Basement window bars -->
|
||||
<rect x="12" y="12" width="24" height="18" rx="1" fill="#334155"/>
|
||||
<rect x="12" y="12" width="24" height="18" rx="1" stroke="#64748b" stroke-width="1"/>
|
||||
<rect x="23" y="12" width="2" height="18" fill="#64748b"/>
|
||||
<rect x="12" y="20" width="24" height="2" fill="#64748b"/>
|
||||
<!-- Red stapler silhouette in darkness -->
|
||||
<rect x="16" y="34" width="16" height="6" rx="1" fill="#dc2626"/>
|
||||
<rect x="14" y="36" width="4" height="4" rx="1" fill="#b91c1c"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 711 B |
17
public/flair/bobs.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "The Bobs Approved" - Business tie with checkmark -->
|
||||
<!-- Shirt collar -->
|
||||
<path d="M8 6 L24 16 L40 6" stroke="#e2e8f0" stroke-width="4" fill="none"/>
|
||||
<!-- Tie knot -->
|
||||
<polygon points="20,14 28,14 26,20 22,20" fill="#1e40af"/>
|
||||
<!-- Tie body -->
|
||||
<polygon points="22,20 26,20 28,42 24,46 20,42" fill="#1e40af"/>
|
||||
<!-- Diagonal stripes on tie -->
|
||||
<path d="M22 24 L26 22" stroke="#3b82f6" stroke-width="1.5"/>
|
||||
<path d="M21 28 L27 25" stroke="#3b82f6" stroke-width="1.5"/>
|
||||
<path d="M21 32 L27 29" stroke="#3b82f6" stroke-width="1.5"/>
|
||||
<path d="M21 36 L27 33" stroke="#3b82f6" stroke-width="1.5"/>
|
||||
<!-- Checkmark badge -->
|
||||
<circle cx="36" cy="12" r="8" fill="#16a34a"/>
|
||||
<path d="M32 12 L35 15 L40 9" stroke="white" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 905 B |
14
public/flair/coffee.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "Case of the Mondays" - Coffee mug with steam -->
|
||||
<!-- Mug body -->
|
||||
<rect x="10" y="18" width="22" height="22" rx="3" fill="#78350f"/>
|
||||
<rect x="12" y="20" width="18" height="18" rx="2" fill="#92400e"/>
|
||||
<!-- Coffee surface -->
|
||||
<ellipse cx="21" cy="22" rx="8" ry="2" fill="#451a03"/>
|
||||
<!-- Mug handle -->
|
||||
<path d="M32 22 Q40 22 40 30 Q40 38 32 38" stroke="#78350f" stroke-width="4" fill="none"/>
|
||||
<!-- Steam wisps -->
|
||||
<path d="M14 14 Q16 10 14 6" stroke="#94a3b8" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
<path d="M21 12 Q23 8 21 4" stroke="#94a3b8" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
<path d="M28 14 Q30 10 28 6" stroke="#94a3b8" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 822 B |
17
public/flair/conclusions.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "Jump to Conclusions" - The Jump to Conclusions mat -->
|
||||
<!-- Mat base -->
|
||||
<rect x="4" y="28" width="40" height="16" rx="2" fill="#16a34a" transform="skewX(-5)"/>
|
||||
<rect x="6" y="30" width="36" height="12" rx="1" fill="#22c55e" transform="skewX(-5)"/>
|
||||
<!-- Conclusion squares -->
|
||||
<rect x="8" y="32" width="8" height="8" rx="1" fill="#f8fafc" transform="skewX(-5)"/>
|
||||
<rect x="18" y="32" width="8" height="8" rx="1" fill="#fef08a" transform="skewX(-5)"/>
|
||||
<rect x="28" y="32" width="8" height="8" rx="1" fill="#fca5a5" transform="skewX(-5)"/>
|
||||
<!-- Jumping figure -->
|
||||
<circle cx="24" cy="12" r="5" fill="#1e293b"/>
|
||||
<path d="M24 17 L24 24" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M24 19 L18 22" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M24 19 L30 22" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M24 24 L20 30" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M24 24 L28 30" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
11
public/flair/extraction.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "I Was Told There Would Be Extraction" - Document with extraction arrow -->
|
||||
<rect x="10" y="6" width="22" height="28" rx="2" fill="#f8fafc" stroke="#475569" stroke-width="2"/>
|
||||
<rect x="14" y="12" width="14" height="2" rx="1" fill="#94a3b8"/>
|
||||
<rect x="14" y="17" width="14" height="2" rx="1" fill="#94a3b8"/>
|
||||
<rect x="14" y="22" width="10" height="2" rx="1" fill="#94a3b8"/>
|
||||
<!-- Extraction arrow coming out -->
|
||||
<path d="M28 26 L38 26 L38 22 L46 28 L38 34 L38 30 L28 30 Z" fill="#dc2626"/>
|
||||
<!-- Small glow effect -->
|
||||
<ellipse cx="46" cy="28" rx="4" ry="6" fill="#dc2626" opacity="0.2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 689 B |
16
public/flair/flair-badge.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "37 Pieces of Flair" - Championship medal -->
|
||||
<!-- Ribbon -->
|
||||
<polygon points="16,4 20,4 24,16 28,4 32,4 26,20 22,20" fill="#dc2626"/>
|
||||
<polygon points="16,4 20,4 22,12" fill="#b91c1c"/>
|
||||
<polygon points="32,4 28,4 26,12" fill="#b91c1c"/>
|
||||
<!-- Medal circle -->
|
||||
<circle cx="24" cy="30" r="14" fill="#f59e0b"/>
|
||||
<circle cx="24" cy="30" r="14" stroke="#b45309" stroke-width="2"/>
|
||||
<circle cx="24" cy="30" r="11" fill="#fbbf24"/>
|
||||
<circle cx="24" cy="30" r="11" stroke="#f59e0b" stroke-width="1"/>
|
||||
<!-- "37" on medal -->
|
||||
<text x="24" y="34" font-size="12" fill="#78350f" font-family="sans-serif" font-weight="bold" text-anchor="middle">37</text>
|
||||
<!-- Star accent -->
|
||||
<polygon points="24,18 25,21 28,21 25.5,23 26.5,26 24,24 21.5,26 22.5,23 20,21 23,21" fill="#fef3c7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 874 B |
18
public/flair/memo.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "Did You Get The Memo?" - Memo/sticky note with pushpin -->
|
||||
<!-- Shadow -->
|
||||
<rect x="10" y="12" width="30" height="30" rx="1" fill="#1e293b" opacity="0.2" transform="translate(2,2)"/>
|
||||
<!-- Yellow memo paper -->
|
||||
<rect x="8" y="10" width="30" height="30" rx="1" fill="#fef08a"/>
|
||||
<!-- Corner fold -->
|
||||
<path d="M38 10 L38 18 L30 10 Z" fill="#fde047"/>
|
||||
<path d="M30 10 L38 18" stroke="#eab308" stroke-width="1"/>
|
||||
<!-- Pushpin -->
|
||||
<circle cx="24" cy="6" r="4" fill="#dc2626"/>
|
||||
<rect x="23" y="8" width="2" height="6" fill="#94a3b8"/>
|
||||
<!-- Text lines -->
|
||||
<text x="12" y="22" font-size="6" fill="#713f12" font-family="sans-serif" font-weight="bold">MEMO</text>
|
||||
<rect x="12" y="26" width="22" height="2" rx="1" fill="#ca8a04" opacity="0.5"/>
|
||||
<rect x="12" y="30" width="18" height="2" rx="1" fill="#ca8a04" opacity="0.5"/>
|
||||
<rect x="12" y="34" width="20" height="2" rx="1" fill="#ca8a04" opacity="0.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1010 B |
20
public/flair/oface.svg
Normal file
@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "O Face" - Surprised/amazed face -->
|
||||
<!-- Face circle -->
|
||||
<circle cx="24" cy="24" r="20" fill="#fef3c7"/>
|
||||
<circle cx="24" cy="24" r="20" stroke="#f59e0b" stroke-width="2"/>
|
||||
<!-- Eyebrows raised -->
|
||||
<path d="M12 14 Q16 10 20 14" stroke="#78350f" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
<path d="M28 14 Q32 10 36 14" stroke="#78350f" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
<!-- Wide eyes -->
|
||||
<ellipse cx="16" cy="20" rx="4" ry="5" fill="white"/>
|
||||
<circle cx="16" cy="20" r="2.5" fill="#1e293b"/>
|
||||
<ellipse cx="32" cy="20" rx="4" ry="5" fill="white"/>
|
||||
<circle cx="32" cy="20" r="2.5" fill="#1e293b"/>
|
||||
<!-- "O" mouth -->
|
||||
<ellipse cx="24" cy="34" rx="6" ry="8" fill="#dc2626"/>
|
||||
<ellipse cx="24" cy="33" rx="4" ry="5" fill="#991b1b"/>
|
||||
<!-- Cheek blush -->
|
||||
<ellipse cx="10" cy="26" rx="3" ry="2" fill="#fca5a5" opacity="0.6"/>
|
||||
<ellipse cx="38" cy="26" rx="3" ry="2" fill="#fca5a5" opacity="0.6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
15
public/flair/printer.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "PC Load Letter" - Printer with error -->
|
||||
<rect x="6" y="18" width="36" height="18" rx="3" fill="#64748b"/>
|
||||
<rect x="8" y="20" width="32" height="14" rx="2" fill="#475569"/>
|
||||
<!-- Paper tray top -->
|
||||
<rect x="12" y="8" width="24" height="12" rx="1" fill="#e2e8f0"/>
|
||||
<rect x="14" y="10" width="20" height="8" rx="1" fill="#f8fafc"/>
|
||||
<!-- Paper output slot -->
|
||||
<rect x="14" y="34" width="20" height="8" rx="1" fill="#f8fafc"/>
|
||||
<!-- Error light -->
|
||||
<circle cx="36" cy="24" r="3" fill="#dc2626"/>
|
||||
<!-- Error LCD display -->
|
||||
<rect x="12" y="22" width="18" height="6" rx="1" fill="#1e293b"/>
|
||||
<text x="14" y="27" font-size="5" fill="#dc2626" font-family="monospace">PC LOAD</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 783 B |
23
public/flair/spreadsheet.svg
Normal file
@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "Spreadsheet Survivor" - Excel grid with survivor badge -->
|
||||
<!-- Sheet background -->
|
||||
<rect x="6" y="6" width="36" height="36" rx="2" fill="#16a34a"/>
|
||||
<rect x="8" y="8" width="32" height="32" rx="1" fill="#f8fafc"/>
|
||||
<!-- Grid lines -->
|
||||
<line x1="8" y1="16" x2="40" y2="16" stroke="#94a3b8" stroke-width="1"/>
|
||||
<line x1="8" y1="24" x2="40" y2="24" stroke="#94a3b8" stroke-width="1"/>
|
||||
<line x1="8" y1="32" x2="40" y2="32" stroke="#94a3b8" stroke-width="1"/>
|
||||
<line x1="18" y1="8" x2="18" y2="40" stroke="#94a3b8" stroke-width="1"/>
|
||||
<line x1="28" y1="8" x2="28" y2="40" stroke="#94a3b8" stroke-width="1"/>
|
||||
<!-- Header row (green) -->
|
||||
<rect x="8" y="8" width="32" height="8" fill="#16a34a"/>
|
||||
<!-- Header column (green) -->
|
||||
<rect x="8" y="8" width="10" height="32" fill="#16a34a" opacity="0.7"/>
|
||||
<!-- Cell labels -->
|
||||
<text x="13" y="14" font-size="5" fill="white" font-family="sans-serif" text-anchor="middle">A</text>
|
||||
<text x="23" y="14" font-size="5" fill="white" font-family="sans-serif" text-anchor="middle">B</text>
|
||||
<text x="34" y="14" font-size="5" fill="white" font-family="sans-serif" text-anchor="middle">C</text>
|
||||
<!-- Star survivor badge -->
|
||||
<circle cx="38" cy="38" r="8" fill="#f59e0b"/>
|
||||
<polygon points="38,32 39.5,36 44,36.5 40.5,39 42,44 38,41 34,44 35.5,39 32,36.5 36.5,36" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
17
public/flair/stapler.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "I Have Your Stapler" - Milton's famous red Swingline stapler -->
|
||||
<!-- Stapler base -->
|
||||
<rect x="4" y="32" width="40" height="8" rx="2" fill="#1e293b"/>
|
||||
<rect x="6" y="34" width="36" height="4" rx="1" fill="#334155"/>
|
||||
<!-- Stapler top (red!) -->
|
||||
<path d="M6 32 L6 24 Q6 20 10 20 L38 20 Q42 20 42 24 L42 32 Z" fill="#dc2626"/>
|
||||
<path d="M8 30 L8 25 Q8 22 11 22 L37 22 Q40 22 40 25 L40 30 Z" fill="#ef4444"/>
|
||||
<!-- Swingline branding area -->
|
||||
<rect x="14" y="24" width="20" height="4" rx="1" fill="#b91c1c"/>
|
||||
<!-- Chrome accent -->
|
||||
<rect x="6" y="30" width="36" height="2" fill="#94a3b8"/>
|
||||
<!-- Staple loading slot -->
|
||||
<rect x="16" y="20" width="16" height="2" fill="#991b1b"/>
|
||||
<!-- Shadow/depth -->
|
||||
<rect x="4" y="38" width="40" height="2" rx="1" fill="#0f172a" opacity="0.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 890 B |
15
public/flair/tps.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "TPS Report Expert" - Clipboard with TPS -->
|
||||
<!-- Clipboard board -->
|
||||
<rect x="8" y="8" width="32" height="36" rx="2" fill="#78350f"/>
|
||||
<!-- Clip -->
|
||||
<rect x="16" y="4" width="16" height="8" rx="2" fill="#94a3b8"/>
|
||||
<rect x="18" y="6" width="12" height="4" rx="1" fill="#cbd5e1"/>
|
||||
<!-- Paper -->
|
||||
<rect x="12" y="12" width="24" height="28" rx="1" fill="#f8fafc"/>
|
||||
<!-- TPS text -->
|
||||
<text x="24" y="24" font-size="10" fill="#1e293b" font-family="sans-serif" font-weight="bold" text-anchor="middle">TPS</text>
|
||||
<rect x="14" y="28" width="20" height="2" rx="1" fill="#94a3b8"/>
|
||||
<rect x="14" y="32" width="16" height="2" rx="1" fill="#94a3b8"/>
|
||||
<rect x="14" y="36" width="18" height="2" rx="1" fill="#94a3b8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 814 B |
12
src/assets/flair/basement.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "Basement Dweller" - Basement window with stapler silhouette -->
|
||||
<rect x="8" y="8" width="32" height="32" rx="2" fill="#1e293b"/>
|
||||
<!-- Basement window bars -->
|
||||
<rect x="12" y="12" width="24" height="18" rx="1" fill="#334155"/>
|
||||
<rect x="12" y="12" width="24" height="18" rx="1" stroke="#64748b" stroke-width="1"/>
|
||||
<rect x="23" y="12" width="2" height="18" fill="#64748b"/>
|
||||
<rect x="12" y="20" width="24" height="2" fill="#64748b"/>
|
||||
<!-- Red stapler silhouette in darkness -->
|
||||
<rect x="16" y="34" width="16" height="6" rx="1" fill="#dc2626"/>
|
||||
<rect x="14" y="36" width="4" height="4" rx="1" fill="#b91c1c"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 711 B |
17
src/assets/flair/bobs.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "The Bobs Approved" - Business tie with checkmark -->
|
||||
<!-- Shirt collar -->
|
||||
<path d="M8 6 L24 16 L40 6" stroke="#e2e8f0" stroke-width="4" fill="none"/>
|
||||
<!-- Tie knot -->
|
||||
<polygon points="20,14 28,14 26,20 22,20" fill="#1e40af"/>
|
||||
<!-- Tie body -->
|
||||
<polygon points="22,20 26,20 28,42 24,46 20,42" fill="#1e40af"/>
|
||||
<!-- Diagonal stripes on tie -->
|
||||
<path d="M22 24 L26 22" stroke="#3b82f6" stroke-width="1.5"/>
|
||||
<path d="M21 28 L27 25" stroke="#3b82f6" stroke-width="1.5"/>
|
||||
<path d="M21 32 L27 29" stroke="#3b82f6" stroke-width="1.5"/>
|
||||
<path d="M21 36 L27 33" stroke="#3b82f6" stroke-width="1.5"/>
|
||||
<!-- Checkmark badge -->
|
||||
<circle cx="36" cy="12" r="8" fill="#16a34a"/>
|
||||
<path d="M32 12 L35 15 L40 9" stroke="white" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 905 B |
14
src/assets/flair/coffee.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "Case of the Mondays" - Coffee mug with steam -->
|
||||
<!-- Mug body -->
|
||||
<rect x="10" y="18" width="22" height="22" rx="3" fill="#78350f"/>
|
||||
<rect x="12" y="20" width="18" height="18" rx="2" fill="#92400e"/>
|
||||
<!-- Coffee surface -->
|
||||
<ellipse cx="21" cy="22" rx="8" ry="2" fill="#451a03"/>
|
||||
<!-- Mug handle -->
|
||||
<path d="M32 22 Q40 22 40 30 Q40 38 32 38" stroke="#78350f" stroke-width="4" fill="none"/>
|
||||
<!-- Steam wisps -->
|
||||
<path d="M14 14 Q16 10 14 6" stroke="#94a3b8" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
<path d="M21 12 Q23 8 21 4" stroke="#94a3b8" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
<path d="M28 14 Q30 10 28 6" stroke="#94a3b8" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 822 B |
17
src/assets/flair/conclusions.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "Jump to Conclusions" - The Jump to Conclusions mat -->
|
||||
<!-- Mat base -->
|
||||
<rect x="4" y="28" width="40" height="16" rx="2" fill="#16a34a" transform="skewX(-5)"/>
|
||||
<rect x="6" y="30" width="36" height="12" rx="1" fill="#22c55e" transform="skewX(-5)"/>
|
||||
<!-- Conclusion squares -->
|
||||
<rect x="8" y="32" width="8" height="8" rx="1" fill="#f8fafc" transform="skewX(-5)"/>
|
||||
<rect x="18" y="32" width="8" height="8" rx="1" fill="#fef08a" transform="skewX(-5)"/>
|
||||
<rect x="28" y="32" width="8" height="8" rx="1" fill="#fca5a5" transform="skewX(-5)"/>
|
||||
<!-- Jumping figure -->
|
||||
<circle cx="24" cy="12" r="5" fill="#1e293b"/>
|
||||
<path d="M24 17 L24 24" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M24 19 L18 22" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M24 19 L30 22" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M24 24 L20 30" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M24 24 L28 30" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
11
src/assets/flair/extraction.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "I Was Told There Would Be Extraction" - Document with extraction arrow -->
|
||||
<rect x="10" y="6" width="22" height="28" rx="2" fill="#f8fafc" stroke="#475569" stroke-width="2"/>
|
||||
<rect x="14" y="12" width="14" height="2" rx="1" fill="#94a3b8"/>
|
||||
<rect x="14" y="17" width="14" height="2" rx="1" fill="#94a3b8"/>
|
||||
<rect x="14" y="22" width="10" height="2" rx="1" fill="#94a3b8"/>
|
||||
<!-- Extraction arrow coming out -->
|
||||
<path d="M28 26 L38 26 L38 22 L46 28 L38 34 L38 30 L28 30 Z" fill="#dc2626"/>
|
||||
<!-- Small glow effect -->
|
||||
<ellipse cx="46" cy="28" rx="4" ry="6" fill="#dc2626" opacity="0.2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 689 B |
16
src/assets/flair/flair-badge.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "37 Pieces of Flair" - Championship medal -->
|
||||
<!-- Ribbon -->
|
||||
<polygon points="16,4 20,4 24,16 28,4 32,4 26,20 22,20" fill="#dc2626"/>
|
||||
<polygon points="16,4 20,4 22,12" fill="#b91c1c"/>
|
||||
<polygon points="32,4 28,4 26,12" fill="#b91c1c"/>
|
||||
<!-- Medal circle -->
|
||||
<circle cx="24" cy="30" r="14" fill="#f59e0b"/>
|
||||
<circle cx="24" cy="30" r="14" stroke="#b45309" stroke-width="2"/>
|
||||
<circle cx="24" cy="30" r="11" fill="#fbbf24"/>
|
||||
<circle cx="24" cy="30" r="11" stroke="#f59e0b" stroke-width="1"/>
|
||||
<!-- "37" on medal -->
|
||||
<text x="24" y="34" font-size="12" fill="#78350f" font-family="sans-serif" font-weight="bold" text-anchor="middle">37</text>
|
||||
<!-- Star accent -->
|
||||
<polygon points="24,18 25,21 28,21 25.5,23 26.5,26 24,24 21.5,26 22.5,23 20,21 23,21" fill="#fef3c7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 874 B |
18
src/assets/flair/memo.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "Did You Get The Memo?" - Memo/sticky note with pushpin -->
|
||||
<!-- Shadow -->
|
||||
<rect x="10" y="12" width="30" height="30" rx="1" fill="#1e293b" opacity="0.2" transform="translate(2,2)"/>
|
||||
<!-- Yellow memo paper -->
|
||||
<rect x="8" y="10" width="30" height="30" rx="1" fill="#fef08a"/>
|
||||
<!-- Corner fold -->
|
||||
<path d="M38 10 L38 18 L30 10 Z" fill="#fde047"/>
|
||||
<path d="M30 10 L38 18" stroke="#eab308" stroke-width="1"/>
|
||||
<!-- Pushpin -->
|
||||
<circle cx="24" cy="6" r="4" fill="#dc2626"/>
|
||||
<rect x="23" y="8" width="2" height="6" fill="#94a3b8"/>
|
||||
<!-- Text lines -->
|
||||
<text x="12" y="22" font-size="6" fill="#713f12" font-family="sans-serif" font-weight="bold">MEMO</text>
|
||||
<rect x="12" y="26" width="22" height="2" rx="1" fill="#ca8a04" opacity="0.5"/>
|
||||
<rect x="12" y="30" width="18" height="2" rx="1" fill="#ca8a04" opacity="0.5"/>
|
||||
<rect x="12" y="34" width="20" height="2" rx="1" fill="#ca8a04" opacity="0.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1010 B |
20
src/assets/flair/oface.svg
Normal file
@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "O Face" - Surprised/amazed face -->
|
||||
<!-- Face circle -->
|
||||
<circle cx="24" cy="24" r="20" fill="#fef3c7"/>
|
||||
<circle cx="24" cy="24" r="20" stroke="#f59e0b" stroke-width="2"/>
|
||||
<!-- Eyebrows raised -->
|
||||
<path d="M12 14 Q16 10 20 14" stroke="#78350f" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
<path d="M28 14 Q32 10 36 14" stroke="#78350f" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
<!-- Wide eyes -->
|
||||
<ellipse cx="16" cy="20" rx="4" ry="5" fill="white"/>
|
||||
<circle cx="16" cy="20" r="2.5" fill="#1e293b"/>
|
||||
<ellipse cx="32" cy="20" rx="4" ry="5" fill="white"/>
|
||||
<circle cx="32" cy="20" r="2.5" fill="#1e293b"/>
|
||||
<!-- "O" mouth -->
|
||||
<ellipse cx="24" cy="34" rx="6" ry="8" fill="#dc2626"/>
|
||||
<ellipse cx="24" cy="33" rx="4" ry="5" fill="#991b1b"/>
|
||||
<!-- Cheek blush -->
|
||||
<ellipse cx="10" cy="26" rx="3" ry="2" fill="#fca5a5" opacity="0.6"/>
|
||||
<ellipse cx="38" cy="26" rx="3" ry="2" fill="#fca5a5" opacity="0.6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
15
src/assets/flair/printer.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "PC Load Letter" - Printer with error -->
|
||||
<rect x="6" y="18" width="36" height="18" rx="3" fill="#64748b"/>
|
||||
<rect x="8" y="20" width="32" height="14" rx="2" fill="#475569"/>
|
||||
<!-- Paper tray top -->
|
||||
<rect x="12" y="8" width="24" height="12" rx="1" fill="#e2e8f0"/>
|
||||
<rect x="14" y="10" width="20" height="8" rx="1" fill="#f8fafc"/>
|
||||
<!-- Paper output slot -->
|
||||
<rect x="14" y="34" width="20" height="8" rx="1" fill="#f8fafc"/>
|
||||
<!-- Error light -->
|
||||
<circle cx="36" cy="24" r="3" fill="#dc2626"/>
|
||||
<!-- Error LCD display -->
|
||||
<rect x="12" y="22" width="18" height="6" rx="1" fill="#1e293b"/>
|
||||
<text x="14" y="27" font-size="5" fill="#dc2626" font-family="monospace">PC LOAD</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 783 B |
23
src/assets/flair/spreadsheet.svg
Normal file
@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "Spreadsheet Survivor" - Excel grid with survivor badge -->
|
||||
<!-- Sheet background -->
|
||||
<rect x="6" y="6" width="36" height="36" rx="2" fill="#16a34a"/>
|
||||
<rect x="8" y="8" width="32" height="32" rx="1" fill="#f8fafc"/>
|
||||
<!-- Grid lines -->
|
||||
<line x1="8" y1="16" x2="40" y2="16" stroke="#94a3b8" stroke-width="1"/>
|
||||
<line x1="8" y1="24" x2="40" y2="24" stroke="#94a3b8" stroke-width="1"/>
|
||||
<line x1="8" y1="32" x2="40" y2="32" stroke="#94a3b8" stroke-width="1"/>
|
||||
<line x1="18" y1="8" x2="18" y2="40" stroke="#94a3b8" stroke-width="1"/>
|
||||
<line x1="28" y1="8" x2="28" y2="40" stroke="#94a3b8" stroke-width="1"/>
|
||||
<!-- Header row (green) -->
|
||||
<rect x="8" y="8" width="32" height="8" fill="#16a34a"/>
|
||||
<!-- Header column (green) -->
|
||||
<rect x="8" y="8" width="10" height="32" fill="#16a34a" opacity="0.7"/>
|
||||
<!-- Cell labels -->
|
||||
<text x="13" y="14" font-size="5" fill="white" font-family="sans-serif" text-anchor="middle">A</text>
|
||||
<text x="23" y="14" font-size="5" fill="white" font-family="sans-serif" text-anchor="middle">B</text>
|
||||
<text x="34" y="14" font-size="5" fill="white" font-family="sans-serif" text-anchor="middle">C</text>
|
||||
<!-- Star survivor badge -->
|
||||
<circle cx="38" cy="38" r="8" fill="#f59e0b"/>
|
||||
<polygon points="38,32 39.5,36 44,36.5 40.5,39 42,44 38,41 34,44 35.5,39 32,36.5 36.5,36" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
17
src/assets/flair/stapler.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "I Have Your Stapler" - Milton's famous red Swingline stapler -->
|
||||
<!-- Stapler base -->
|
||||
<rect x="4" y="32" width="40" height="8" rx="2" fill="#1e293b"/>
|
||||
<rect x="6" y="34" width="36" height="4" rx="1" fill="#334155"/>
|
||||
<!-- Stapler top (red!) -->
|
||||
<path d="M6 32 L6 24 Q6 20 10 20 L38 20 Q42 20 42 24 L42 32 Z" fill="#dc2626"/>
|
||||
<path d="M8 30 L8 25 Q8 22 11 22 L37 22 Q40 22 40 25 L40 30 Z" fill="#ef4444"/>
|
||||
<!-- Swingline branding area -->
|
||||
<rect x="14" y="24" width="20" height="4" rx="1" fill="#b91c1c"/>
|
||||
<!-- Chrome accent -->
|
||||
<rect x="6" y="30" width="36" height="2" fill="#94a3b8"/>
|
||||
<!-- Staple loading slot -->
|
||||
<rect x="16" y="20" width="16" height="2" fill="#991b1b"/>
|
||||
<!-- Shadow/depth -->
|
||||
<rect x="4" y="38" width="40" height="2" rx="1" fill="#0f172a" opacity="0.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 890 B |
15
src/assets/flair/tps.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- "TPS Report Expert" - Clipboard with TPS -->
|
||||
<!-- Clipboard board -->
|
||||
<rect x="8" y="8" width="32" height="36" rx="2" fill="#78350f"/>
|
||||
<!-- Clip -->
|
||||
<rect x="16" y="4" width="16" height="8" rx="2" fill="#94a3b8"/>
|
||||
<rect x="18" y="6" width="12" height="4" rx="1" fill="#cbd5e1"/>
|
||||
<!-- Paper -->
|
||||
<rect x="12" y="12" width="24" height="28" rx="1" fill="#f8fafc"/>
|
||||
<!-- TPS text -->
|
||||
<text x="24" y="24" font-size="10" fill="#1e293b" font-family="sans-serif" font-weight="bold" text-anchor="middle">TPS</text>
|
||||
<rect x="14" y="28" width="20" height="2" rx="1" fill="#94a3b8"/>
|
||||
<rect x="14" y="32" width="16" height="2" rx="1" fill="#94a3b8"/>
|
||||
<rect x="14" y="36" width="18" height="2" rx="1" fill="#94a3b8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 814 B |
979
src/components/FlairBadge.astro
Normal file
@ -0,0 +1,979 @@
|
||||
---
|
||||
/**
|
||||
* FlairBadge - Collect pieces of flair by spending time on key pages
|
||||
* Adapted for Starlight documentation site
|
||||
*
|
||||
* "We need to talk about your flair..." - Stan, Manager
|
||||
*
|
||||
* Features:
|
||||
* - Floating counter in bottom-right corner
|
||||
* - Opens modal "flair board" showing collection
|
||||
* - 15-second timer per page to earn flair
|
||||
* - localStorage persistence across sessions
|
||||
* - Astro view transition support
|
||||
*/
|
||||
|
||||
import flairConfig from '../data/flair-config.json';
|
||||
|
||||
interface Props {
|
||||
currentPath: string;
|
||||
}
|
||||
|
||||
const { currentPath } = Astro.props;
|
||||
|
||||
// Normalize path for matching (handle trailing slashes)
|
||||
const normalizedPath = currentPath === '/' ? '/' : currentPath.replace(/\/$/, '') + '/';
|
||||
const altPath = currentPath === '/' ? '/' : currentPath.replace(/\/$/, '');
|
||||
|
||||
// Check if current page has flair (try both with and without trailing slash)
|
||||
const currentFlair = flairConfig.flairs.find(f =>
|
||||
f.path === normalizedPath || f.path === altPath || f.path === currentPath
|
||||
);
|
||||
---
|
||||
|
||||
<!-- Floating Flair Counter -->
|
||||
<button
|
||||
id="flair-counter"
|
||||
type="button"
|
||||
class="flair-counter-btn"
|
||||
aria-label="Open flair collection"
|
||||
>
|
||||
<span class="flair-icon">🎖️</span>
|
||||
<span class="flair-count">0</span>
|
||||
<span class="flair-divider">/</span>
|
||||
<span class="flair-total">{flairConfig.flairs.length}</span>
|
||||
|
||||
<!-- Progress ring indicator -->
|
||||
<span id="flair-progress-indicator" class="progress-indicator hidden"></span>
|
||||
</button>
|
||||
|
||||
<!-- Flair Earned Toast -->
|
||||
<div id="flair-toast" class="flair-toast hidden-right">
|
||||
<div class="toast-icon-container">
|
||||
<img id="toast-image" src="" alt="" class="toast-image hidden" />
|
||||
<span class="toast-emoji" id="toast-icon">🎉</span>
|
||||
</div>
|
||||
<div class="toast-content">
|
||||
<p class="toast-title">Flair Earned!</p>
|
||||
<p class="toast-name" id="toast-name">Badge Name</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Name Capture Dialog -->
|
||||
<dialog id="name-dialog" class="name-dialog">
|
||||
<div class="dialog-content">
|
||||
<div class="dialog-header">
|
||||
<span id="name-dialog-icon" class="dialog-icon">🎖️</span>
|
||||
<h2 id="name-dialog-title" class="dialog-title">You earned your first flair!</h2>
|
||||
<p id="name-dialog-subtitle" class="dialog-subtitle">What should we call you?</p>
|
||||
</div>
|
||||
|
||||
<form id="name-form" class="name-form">
|
||||
<input
|
||||
type="text"
|
||||
id="visitor-name-input"
|
||||
placeholder="Your name"
|
||||
maxlength="30"
|
||||
class="name-input"
|
||||
/>
|
||||
<div class="dialog-buttons">
|
||||
<button type="button" id="skip-name-btn" class="btn-secondary">Skip</button>
|
||||
<button type="submit" class="btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Flair Board Modal - Stan's Chotchkie's Vest -->
|
||||
<dialog id="flair-modal" class="flair-modal">
|
||||
<div class="modal-container">
|
||||
<!-- Close button -->
|
||||
<button
|
||||
type="button"
|
||||
id="flair-modal-close"
|
||||
class="modal-close"
|
||||
aria-label="Close flair board"
|
||||
>
|
||||
<svg class="close-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Vest Container -->
|
||||
<div class="vest-container">
|
||||
<!-- Header -->
|
||||
<div class="vest-header">
|
||||
<h2 class="vest-title" id="modal-title">Your Flair Collection</h2>
|
||||
<div class="name-tag-wrapper">
|
||||
<div class="name-tag">
|
||||
<span id="vest-name-tag">VISITOR</span>
|
||||
<button type="button" id="edit-name-btn" class="edit-name-btn" aria-label="Edit your name" title="Edit your name">
|
||||
<svg class="edit-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flair Grid -->
|
||||
<div class="flair-grid">
|
||||
{flairConfig.flairs.map((flair, index) => {
|
||||
const rotations = [-5, 3, -2, 6, -4, 2, 5, -3, 4, -6, 3, -2];
|
||||
const rotation = rotations[index % rotations.length];
|
||||
return (
|
||||
<div
|
||||
class="flair-item"
|
||||
data-flair-id={flair.id}
|
||||
data-flair-image={flair.image || ''}
|
||||
data-flair-placeholder={flair.placeholder}
|
||||
data-earned="false"
|
||||
style={`--rotation: ${rotation}deg`}
|
||||
>
|
||||
<div class="flair-badge">
|
||||
{flair.image ? (
|
||||
<img src={flair.image} alt={flair.name} class="badge-image" />
|
||||
) : (
|
||||
<span class="badge-emoji">{flair.placeholder}</span>
|
||||
)}
|
||||
</div>
|
||||
<div class="flair-tooltip">
|
||||
<span class="tooltip-name">{flair.name}</span>
|
||||
<span class="tooltip-desc">{flair.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<!-- Progress Footer -->
|
||||
<div class="vest-footer">
|
||||
<div class="progress-counter">
|
||||
<span class="earned-count" id="modal-earned-count">0</span>
|
||||
<span class="count-divider">/</span>
|
||||
<span class="total-count">{flairConfig.flairs.length}</span>
|
||||
<span class="count-label">pieces of flair</span>
|
||||
</div>
|
||||
|
||||
<div class="stan-quote">
|
||||
<p class="quote-text">"{flairConfig.stanQuote}"</p>
|
||||
<p class="quote-attribution">— Stan, Manager</p>
|
||||
</div>
|
||||
|
||||
<!-- Completion message -->
|
||||
<div id="completion-message" class="completion-message hidden">
|
||||
<p>{flairConfig.completionMessage}</p>
|
||||
</div>
|
||||
|
||||
<!-- Current page hint -->
|
||||
{currentFlair && (
|
||||
<div class="current-page-hint">
|
||||
<span class="hint-label">Current page:</span>
|
||||
<span class="hint-name">{currentFlair.name}</span>
|
||||
<span id="timer-display" class="timer-display"></span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script define:vars={{ flairConfig, currentFlair }}>
|
||||
// Flair Collection System
|
||||
const STORAGE_KEY = flairConfig.storageKey;
|
||||
const REQUIRED_TIME = flairConfig.requiredTime;
|
||||
|
||||
let pageTimer = null;
|
||||
let timeSpent = 0;
|
||||
let isTimerRunning = false;
|
||||
|
||||
// Get/set flair data from localStorage
|
||||
function getFlairData() {
|
||||
try {
|
||||
const data = localStorage.getItem(STORAGE_KEY);
|
||||
return data ? JSON.parse(data) : { earned: [], timestamps: {}, visitorName: null, namePrompted: false };
|
||||
} catch {
|
||||
return { earned: [], timestamps: {}, visitorName: null, namePrompted: false };
|
||||
}
|
||||
}
|
||||
|
||||
function saveFlairData(data) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||
} catch (e) {
|
||||
console.warn('Could not save flair data:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if flair is earned
|
||||
function hasEarnedFlair(flairId) {
|
||||
const data = getFlairData();
|
||||
return data.earned.includes(flairId);
|
||||
}
|
||||
|
||||
// Award flair
|
||||
function awardFlair(flairId) {
|
||||
const data = getFlairData();
|
||||
if (!data.earned.includes(flairId)) {
|
||||
const isFirstFlair = data.earned.length === 0;
|
||||
data.earned.push(flairId);
|
||||
data.timestamps[flairId] = Date.now();
|
||||
saveFlairData(data);
|
||||
updateUI();
|
||||
showToast(flairId);
|
||||
|
||||
// Prompt for name on first flair
|
||||
if (isFirstFlair && !data.namePrompted) {
|
||||
setTimeout(() => showNamePrompt(), 3500);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show name prompt dialog
|
||||
function showNamePrompt(mode = 'first-flair') {
|
||||
const nameDialog = document.getElementById('name-dialog');
|
||||
const iconEl = document.getElementById('name-dialog-icon');
|
||||
const titleEl = document.getElementById('name-dialog-title');
|
||||
const subtitleEl = document.getElementById('name-dialog-subtitle');
|
||||
const input = document.getElementById('visitor-name-input');
|
||||
const skipBtn = document.getElementById('skip-name-btn');
|
||||
|
||||
if (!nameDialog) return;
|
||||
|
||||
if (mode === 'edit') {
|
||||
if (iconEl) iconEl.textContent = '✏️';
|
||||
if (titleEl) titleEl.textContent = 'Update Your Name';
|
||||
if (subtitleEl) subtitleEl.textContent = 'Change how your flair collection is personalized';
|
||||
if (skipBtn) skipBtn.textContent = 'Cancel';
|
||||
const data = getFlairData();
|
||||
if (input && data.visitorName) {
|
||||
input.value = data.visitorName;
|
||||
}
|
||||
} else {
|
||||
if (iconEl) iconEl.textContent = '🎖️';
|
||||
if (titleEl) titleEl.textContent = 'You earned your first flair!';
|
||||
if (subtitleEl) subtitleEl.textContent = 'What should we call you?';
|
||||
if (skipBtn) skipBtn.textContent = 'Skip';
|
||||
if (input) input.value = '';
|
||||
}
|
||||
|
||||
nameDialog.showModal();
|
||||
if (input) {
|
||||
input.focus();
|
||||
if (mode === 'edit') input.select();
|
||||
}
|
||||
}
|
||||
|
||||
// Save visitor name
|
||||
function saveVisitorName(name) {
|
||||
const data = getFlairData();
|
||||
data.visitorName = name || null;
|
||||
data.namePrompted = true;
|
||||
saveFlairData(data);
|
||||
updateModalTitle();
|
||||
}
|
||||
|
||||
// Update modal title and vest name tag
|
||||
function updateModalTitle() {
|
||||
const data = getFlairData();
|
||||
const titleEl = document.getElementById('modal-title');
|
||||
const vestNameTag = document.getElementById('vest-name-tag');
|
||||
|
||||
if (titleEl && data.visitorName) {
|
||||
titleEl.textContent = `${data.visitorName}'s Flair Collection`;
|
||||
} else if (titleEl) {
|
||||
titleEl.textContent = 'Your Flair Collection';
|
||||
}
|
||||
|
||||
if (vestNameTag) {
|
||||
vestNameTag.textContent = data.visitorName || 'VISITOR';
|
||||
}
|
||||
}
|
||||
|
||||
// Show toast notification
|
||||
function showToast(flairId) {
|
||||
const flair = flairConfig.flairs.find(f => f.id === flairId);
|
||||
if (!flair) return;
|
||||
|
||||
const toast = document.getElementById('flair-toast');
|
||||
const toastIcon = document.getElementById('toast-icon');
|
||||
const toastImage = document.getElementById('toast-image');
|
||||
const toastName = document.getElementById('toast-name');
|
||||
|
||||
if (toast && toastIcon && toastImage && toastName) {
|
||||
if (flair.image) {
|
||||
toastImage.src = flair.image;
|
||||
toastImage.alt = flair.name;
|
||||
toastImage.classList.remove('hidden');
|
||||
toastIcon.classList.add('hidden');
|
||||
} else {
|
||||
toastIcon.textContent = flair.placeholder;
|
||||
toastIcon.classList.remove('hidden');
|
||||
toastImage.classList.add('hidden');
|
||||
}
|
||||
|
||||
toastName.textContent = flair.name;
|
||||
toast.classList.remove('hidden-right');
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('hidden-right');
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Update all UI elements
|
||||
function updateUI() {
|
||||
const data = getFlairData();
|
||||
const earnedCount = data.earned.length;
|
||||
const totalCount = flairConfig.flairs.length;
|
||||
|
||||
// Update counter
|
||||
const countEl = document.querySelector('.flair-count');
|
||||
if (countEl) countEl.textContent = earnedCount.toString();
|
||||
|
||||
// Update modal
|
||||
const modalCount = document.getElementById('modal-earned-count');
|
||||
if (modalCount) modalCount.textContent = earnedCount.toString();
|
||||
|
||||
// Update flair grid
|
||||
document.querySelectorAll('.flair-item').forEach((item) => {
|
||||
const flairId = item.getAttribute('data-flair-id');
|
||||
const isEarned = data.earned.includes(flairId);
|
||||
item.setAttribute('data-earned', isEarned.toString());
|
||||
});
|
||||
|
||||
// Show completion message if all earned
|
||||
const completionMsg = document.getElementById('completion-message');
|
||||
if (completionMsg) {
|
||||
completionMsg.classList.toggle('hidden', earnedCount < totalCount);
|
||||
}
|
||||
}
|
||||
|
||||
// Timer display update
|
||||
function updateTimerDisplay() {
|
||||
const timerEl = document.getElementById('timer-display');
|
||||
if (!timerEl || !currentFlair) return;
|
||||
|
||||
if (hasEarnedFlair(currentFlair.id)) {
|
||||
timerEl.textContent = '✓ Earned!';
|
||||
timerEl.classList.add('earned');
|
||||
} else if (isTimerRunning) {
|
||||
const remaining = Math.max(0, REQUIRED_TIME - timeSpent);
|
||||
timerEl.textContent = `(${remaining}s remaining)`;
|
||||
timerEl.classList.remove('earned');
|
||||
}
|
||||
}
|
||||
|
||||
// Start page timer
|
||||
function startTimer() {
|
||||
if (!currentFlair || hasEarnedFlair(currentFlair.id)) {
|
||||
updateTimerDisplay();
|
||||
return;
|
||||
}
|
||||
|
||||
isTimerRunning = true;
|
||||
timeSpent = 0;
|
||||
|
||||
const indicator = document.getElementById('flair-progress-indicator');
|
||||
if (indicator) indicator.classList.remove('hidden');
|
||||
|
||||
pageTimer = setInterval(() => {
|
||||
timeSpent++;
|
||||
updateTimerDisplay();
|
||||
|
||||
if (timeSpent >= REQUIRED_TIME) {
|
||||
stopTimer();
|
||||
awardFlair(currentFlair.id);
|
||||
if (indicator) indicator.classList.add('hidden');
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
updateTimerDisplay();
|
||||
}
|
||||
|
||||
// Stop timer
|
||||
function stopTimer() {
|
||||
isTimerRunning = false;
|
||||
if (pageTimer) {
|
||||
clearInterval(pageTimer);
|
||||
pageTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
function init() {
|
||||
updateUI();
|
||||
updateModalTitle();
|
||||
|
||||
// Counter click opens modal
|
||||
const counter = document.getElementById('flair-counter');
|
||||
const modal = document.getElementById('flair-modal');
|
||||
const closeBtn = document.getElementById('flair-modal-close');
|
||||
|
||||
if (counter && modal) {
|
||||
counter.addEventListener('click', () => modal.showModal());
|
||||
}
|
||||
|
||||
if (closeBtn && modal) {
|
||||
closeBtn.addEventListener('click', () => modal.close());
|
||||
}
|
||||
|
||||
// Edit name button
|
||||
const editNameBtn = document.getElementById('edit-name-btn');
|
||||
if (editNameBtn && modal) {
|
||||
editNameBtn.addEventListener('click', () => {
|
||||
modal.close();
|
||||
showNamePrompt('edit');
|
||||
});
|
||||
}
|
||||
|
||||
// Close modal on backdrop click
|
||||
if (modal) {
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) modal.close();
|
||||
});
|
||||
}
|
||||
|
||||
// Name form handlers
|
||||
const nameDialog = document.getElementById('name-dialog');
|
||||
const nameForm = document.getElementById('name-form');
|
||||
const skipBtn = document.getElementById('skip-name-btn');
|
||||
const nameInput = document.getElementById('visitor-name-input');
|
||||
|
||||
if (nameForm && nameDialog) {
|
||||
nameForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const name = nameInput?.value.trim();
|
||||
saveVisitorName(name);
|
||||
nameDialog.close();
|
||||
});
|
||||
}
|
||||
|
||||
if (skipBtn && nameDialog) {
|
||||
skipBtn.addEventListener('click', () => {
|
||||
saveVisitorName(null);
|
||||
nameDialog.close();
|
||||
});
|
||||
}
|
||||
|
||||
if (nameDialog) {
|
||||
nameDialog.addEventListener('click', (e) => {
|
||||
if (e.target === nameDialog) {
|
||||
saveVisitorName(null);
|
||||
nameDialog.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start timer for current page
|
||||
startTimer();
|
||||
}
|
||||
|
||||
// Cleanup on page transition
|
||||
function cleanup() {
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
// Handle Astro view transitions
|
||||
document.addEventListener('astro:before-swap', cleanup);
|
||||
document.addEventListener('astro:page-load', init);
|
||||
|
||||
// Initial load
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Reset and base styles */
|
||||
.hidden { display: none !important; }
|
||||
.hidden-right { transform: translateX(150%); }
|
||||
|
||||
/* Floating Counter Button */
|
||||
.flair-counter-btn {
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
right: 1.5rem;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 1rem;
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
border-radius: 9999px;
|
||||
color: white;
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.flair-counter-btn:hover {
|
||||
background: rgba(30, 41, 59, 0.95);
|
||||
border-color: rgba(245, 158, 11, 0.5);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.flair-icon { font-size: 1.125rem; }
|
||||
.flair-count { font-weight: 600; color: #fbbf24; }
|
||||
.flair-divider { color: #64748b; }
|
||||
.flair-total { color: #cbd5e1; }
|
||||
|
||||
.progress-indicator {
|
||||
position: absolute;
|
||||
top: -0.25rem;
|
||||
right: -0.25rem;
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
background: #f59e0b;
|
||||
border-radius: 9999px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Toast Notification */
|
||||
.flair-toast {
|
||||
position: fixed;
|
||||
bottom: 5rem;
|
||||
right: 1.5rem;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(6, 78, 59, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(16, 185, 129, 0.5);
|
||||
border-radius: 0.75rem;
|
||||
color: white;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.toast-icon-container {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toast-image {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.toast-emoji { font-size: 1.5rem; }
|
||||
.toast-title { font-weight: 600; color: #6ee7b7; margin: 0; }
|
||||
.toast-name { font-size: 0.875rem; color: #d1fae5; margin: 0; }
|
||||
|
||||
/* Dialogs */
|
||||
.name-dialog,
|
||||
.flair-modal {
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
background: transparent;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.name-dialog::backdrop,
|
||||
.flair-modal::backdrop {
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.name-dialog[open],
|
||||
.flair-modal[open] {
|
||||
animation: modal-appear 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modal-appear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Name Dialog */
|
||||
.name-dialog {
|
||||
max-width: 24rem;
|
||||
width: 100%;
|
||||
background: #0f172a;
|
||||
border: 1px solid rgba(245, 158, 11, 0.5);
|
||||
}
|
||||
|
||||
.dialog-content { padding: 1.5rem; }
|
||||
|
||||
.dialog-header {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog-icon {
|
||||
font-size: 2.5rem;
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.dialog-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.name-form { display: flex; flex-direction: column; gap: 1rem; }
|
||||
|
||||
.name-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #1e293b;
|
||||
border: 1px solid #475569;
|
||||
border-radius: 0.75rem;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.name-input:focus {
|
||||
outline: none;
|
||||
border-color: #f59e0b;
|
||||
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2);
|
||||
}
|
||||
|
||||
.name-input::placeholder { color: #64748b; }
|
||||
|
||||
.dialog-buttons {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-secondary,
|
||||
.btn-primary {
|
||||
flex: 1;
|
||||
padding: 0.625rem 1rem;
|
||||
border-radius: 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #334155;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.btn-secondary:hover { background: #475569; }
|
||||
|
||||
.btn-primary {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover { background: #d97706; }
|
||||
|
||||
/* Flair Modal */
|
||||
.flair-modal {
|
||||
max-width: 32rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-container { position: relative; }
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: -0.5rem;
|
||||
right: -0.5rem;
|
||||
z-index: 10;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #1e293b;
|
||||
border: 1px solid #475569;
|
||||
border-radius: 9999px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.close-icon { width: 1rem; height: 1rem; }
|
||||
|
||||
/* Vest Container */
|
||||
.vest-container {
|
||||
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.vest-header {
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vest-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.name-tag-wrapper { display: flex; align-items: center; }
|
||||
|
||||
.name-tag {
|
||||
background: #16a34a;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
letter-spacing: 0.05em;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.edit-name-btn {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.edit-name-btn:hover { background: #f59e0b; }
|
||||
.edit-icon { width: 0.625rem; height: 0.625rem; color: white; }
|
||||
|
||||
/* Flair Grid */
|
||||
.flair-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(180deg, #334155 0%, #1e293b 100%);
|
||||
}
|
||||
|
||||
.flair-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
transform: rotate(var(--rotation, 0deg));
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.flair-item:hover {
|
||||
transform: rotate(var(--rotation, 0deg)) scale(1.1);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.flair-badge {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #475569;
|
||||
border: 2px solid #64748b;
|
||||
border-radius: 9999px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.flair-item[data-earned="false"] .flair-badge {
|
||||
filter: grayscale(100%);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.flair-item[data-earned="true"] .flair-badge {
|
||||
filter: none;
|
||||
opacity: 1;
|
||||
background: linear-gradient(145deg, #fcd34d 0%, #f59e0b 50%, #d97706 100%);
|
||||
border-color: #b45309;
|
||||
box-shadow:
|
||||
0 2px 4px rgba(0, 0, 0, 0.3),
|
||||
0 4px 8px rgba(251, 191, 36, 0.3),
|
||||
inset 0 1px 2px rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.badge-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.badge-emoji { font-size: 1.25rem; }
|
||||
|
||||
/* Flair Tooltip */
|
||||
.flair-tooltip {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
border-radius: 0.375rem;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.2s;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
z-index: 10;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flair-item:hover .flair-tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.tooltip-name {
|
||||
display: block;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tooltip-desc {
|
||||
display: block;
|
||||
font-size: 0.625rem;
|
||||
color: #94a3b8;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
/* Vest Footer */
|
||||
.vest-footer {
|
||||
padding: 1.25rem 1.5rem;
|
||||
text-align: center;
|
||||
background: #0f172a;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.progress-counter {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 0.25rem;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 9999px;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.earned-count {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.count-divider { color: #64748b; }
|
||||
.total-count { font-size: 1.125rem; color: #cbd5e1; }
|
||||
.count-label { font-size: 0.75rem; color: #64748b; margin-left: 0.25rem; }
|
||||
|
||||
.stan-quote {
|
||||
max-width: 16rem;
|
||||
margin: 0 auto 0.75rem;
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
font-size: 0.6875rem;
|
||||
font-style: italic;
|
||||
color: rgba(251, 191, 36, 0.9);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.quote-attribution {
|
||||
font-size: 0.625rem;
|
||||
color: #64748b;
|
||||
margin: 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.completion-message {
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(16, 185, 129, 0.9);
|
||||
border: 1px solid #10b981;
|
||||
border-radius: 0.5rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.completion-message p {
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.current-page-hint {
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-size: 0.8125rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.hint-label { color: #fbbf24; }
|
||||
.hint-name { color: white; margin-left: 0.25rem; }
|
||||
.timer-display { margin-left: 0.5rem; color: #64748b; }
|
||||
.timer-display.earned { color: #10b981; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 480px) {
|
||||
.flair-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.flair-badge {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.vest-header { padding: 1rem; }
|
||||
.vest-footer { padding: 1rem; }
|
||||
}
|
||||
</style>
|
||||
15
src/components/Footer.astro
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
/**
|
||||
* Custom Footer component that includes the FlairBadge system
|
||||
* Wraps Starlight's default Footer
|
||||
*/
|
||||
import type { Props } from '@astrojs/starlight/props';
|
||||
import Default from '@astrojs/starlight/components/Footer.astro';
|
||||
import FlairBadge from './FlairBadge.astro';
|
||||
|
||||
// Get current path for flair matching
|
||||
const currentPath = Astro.url.pathname;
|
||||
---
|
||||
|
||||
<Default {...Astro.props}><slot /></Default>
|
||||
<FlairBadge currentPath={currentPath} />
|
||||
@ -7,83 +7,95 @@
|
||||
"path": "/",
|
||||
"name": "I Was Told There Would Be Extraction",
|
||||
"placeholder": "📄",
|
||||
"image": "/flair/extraction.svg",
|
||||
"description": "Started your mcwaddams journey"
|
||||
},
|
||||
{
|
||||
"id": "backstory",
|
||||
"path": "/backstory",
|
||||
"path": "/backstory/",
|
||||
"name": "Basement Dweller",
|
||||
"placeholder": "🔴",
|
||||
"image": "/flair/basement.svg",
|
||||
"description": "Learned about Milton and the legacy documents"
|
||||
},
|
||||
{
|
||||
"id": "installation",
|
||||
"path": "/installation",
|
||||
"path": "/installation/",
|
||||
"name": "PC Load Letter",
|
||||
"placeholder": "🖨️",
|
||||
"image": "/flair/printer.svg",
|
||||
"description": "Successfully installed mcwaddams"
|
||||
},
|
||||
{
|
||||
"id": "quickstart",
|
||||
"path": "/quickstart",
|
||||
"path": "/quickstart/",
|
||||
"name": "Case of the Mondays",
|
||||
"placeholder": "☕",
|
||||
"image": "/flair/coffee.svg",
|
||||
"description": "Completed the quick start guide"
|
||||
},
|
||||
{
|
||||
"id": "reference",
|
||||
"path": "/reference/tools",
|
||||
"path": "/reference/tools/",
|
||||
"name": "TPS Report Expert",
|
||||
"placeholder": "📋",
|
||||
"image": "/flair/tps.svg",
|
||||
"description": "Read the complete tools reference"
|
||||
},
|
||||
{
|
||||
"id": "architecture",
|
||||
"path": "/explanation/architecture",
|
||||
"path": "/explanation/architecture/",
|
||||
"name": "The Bobs Approved",
|
||||
"placeholder": "👔",
|
||||
"image": "/flair/bobs.svg",
|
||||
"description": "Understood the architecture"
|
||||
},
|
||||
{
|
||||
"id": "dashboard",
|
||||
"path": "/tps/dashboard",
|
||||
"path": "/tps/dashboard/",
|
||||
"name": "Did You Get The Memo?",
|
||||
"placeholder": "📝",
|
||||
"image": "/flair/memo.svg",
|
||||
"description": "Checked the test dashboard"
|
||||
},
|
||||
{
|
||||
"id": "torture",
|
||||
"path": "/tps/torture",
|
||||
"path": "/tps/torture/",
|
||||
"name": "O Face",
|
||||
"placeholder": "😮",
|
||||
"image": "/flair/oface.svg",
|
||||
"description": "Witnessed the torture test results"
|
||||
},
|
||||
{
|
||||
"id": "credits",
|
||||
"path": "/community/credits",
|
||||
"path": "/community/credits/",
|
||||
"name": "I Have Your Stapler",
|
||||
"placeholder": "🔴",
|
||||
"image": "/flair/stapler.svg",
|
||||
"description": "Found the credits and attributions"
|
||||
},
|
||||
{
|
||||
"id": "tutorial",
|
||||
"path": "/tutorials/first-extraction",
|
||||
"path": "/tutorials/first-extraction/",
|
||||
"name": "Jump to Conclusions",
|
||||
"placeholder": "🎲",
|
||||
"image": "/flair/conclusions.svg",
|
||||
"description": "Completed your first extraction tutorial"
|
||||
},
|
||||
{
|
||||
"id": "tables",
|
||||
"path": "/how-to/extract-tables",
|
||||
"path": "/how-to/extract-tables/",
|
||||
"name": "Spreadsheet Survivor",
|
||||
"placeholder": "📊",
|
||||
"image": "/flair/spreadsheet.svg",
|
||||
"description": "Mastered table extraction"
|
||||
},
|
||||
{
|
||||
"id": "collector",
|
||||
"path": "/community/leaderboard",
|
||||
"path": "/community/leaderboard/",
|
||||
"name": "37 Pieces of Flair",
|
||||
"placeholder": "🎖️",
|
||||
"image": "/flair/flair-badge.svg",
|
||||
"description": "Discovered the flair leaderboard"
|
||||
}
|
||||
],
|
||||
|
||||