kiwi/frontend/src/composables/useEasterEggs.ts
pyr0ball a22a249280 feat(frontend): recipe UI — filters, dismissal, load more, prep notes, nutrition chips
- Style/category filter panel with active chip display
- Dismiss (excluded_ids) support — recipes don't reappear until next fresh search
- Load more appends next batch without full re-fetch
- Prep notes 'Before you start:' section above directions
- Nutrition macro chips (kcal, fat, protein, carbs, fiber, sugar, sodium)
- Composables extracted for reuse
2026-04-02 22:12:45 -07:00

132 lines
4.1 KiB
TypeScript

import { ref, onMounted, onUnmounted } from 'vue'
const KONAMI = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a']
const KIWI_WORD = ['k','i','w','i']
// Module-level shared state — single instance across all component uses
const neonMode = ref(false)
const kiwiVisible = ref(false)
const kiwiDirection = ref<'ltr' | 'rtl'>('rtl') // bird enters from right by default
const NEON_VARS: Record<string, string> = {
'--color-bg-primary': '#070011',
'--color-bg-secondary': '#0f001f',
'--color-bg-elevated': '#160028',
'--color-bg-card': '#160028',
'--color-bg-input': '#0f001f',
'--color-primary': '#ff006e',
'--color-text-primary': '#f0e6ff',
'--color-text-secondary': '#c090ff',
'--color-text-muted': '#7040a0',
'--color-border': 'rgba(255, 0, 110, 0.22)',
'--color-border-focus': '#ff006e',
'--color-info': '#00f5ff',
'--color-info-bg': 'rgba(0, 245, 255, 0.10)',
'--color-info-border': 'rgba(0, 245, 255, 0.30)',
'--color-info-light': '#00f5ff',
'--color-success': '#39ff14',
'--color-success-bg': 'rgba(57, 255, 20, 0.10)',
'--color-success-border': 'rgba(57, 255, 20, 0.30)',
'--color-success-light': '#39ff14',
'--color-warning': '#ffbe0b',
'--color-warning-bg': 'rgba(255, 190, 11, 0.10)',
'--color-warning-border': 'rgba(255, 190, 11, 0.30)',
'--color-warning-light': '#ffbe0b',
'--shadow-amber': '0 0 18px rgba(255, 0, 110, 0.55)',
'--shadow-md': '0 2px 16px rgba(255, 0, 110, 0.18)',
'--shadow-lg': '0 4px 28px rgba(255, 0, 110, 0.25)',
'--gradient-primary': 'linear-gradient(135deg, #ff006e 0%, #8338ec 100%)',
'--gradient-header': 'linear-gradient(135deg, #070011 0%, #160028 100%)',
'--color-loc-fridge': '#00f5ff',
'--color-loc-freezer': '#8338ec',
'--color-loc-pantry': '#ff006e',
'--color-loc-cabinet': '#ffbe0b',
'--color-loc-garage-freezer': '#39ff14',
}
function applyNeon() {
const root = document.documentElement
for (const [prop, val] of Object.entries(NEON_VARS)) {
root.style.setProperty(prop, val)
}
document.body.classList.add('neon-mode')
}
function removeNeon() {
const root = document.documentElement
for (const prop of Object.keys(NEON_VARS)) {
root.style.removeProperty(prop)
}
document.body.classList.remove('neon-mode')
}
function toggleNeon() {
neonMode.value = !neonMode.value
if (neonMode.value) {
applyNeon()
localStorage.setItem('kiwi-neon-mode', '1')
} else {
removeNeon()
localStorage.removeItem('kiwi-neon-mode')
}
}
function spawnKiwi() {
kiwiDirection.value = Math.random() > 0.5 ? 'ltr' : 'rtl'
kiwiVisible.value = true
setTimeout(() => { kiwiVisible.value = false }, 5500)
}
export function useEasterEggs() {
const konamiBuffer: string[] = []
const kiwiBuffer: string[] = []
function onKeyDown(e: KeyboardEvent) {
// Skip when user is typing in a form input
const tag = (e.target as HTMLElement)?.tagName
const isInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'
// Konami code — works even in inputs
konamiBuffer.push(e.key)
if (konamiBuffer.length > KONAMI.length) konamiBuffer.shift()
if (konamiBuffer.join(',') === KONAMI.join(',')) {
toggleNeon()
konamiBuffer.length = 0
}
// KIWI word — only when not in a form input
if (!isInput) {
const key = e.key.toLowerCase()
if ('kiwi'.includes(key) && key.length === 1) {
kiwiBuffer.push(key)
if (kiwiBuffer.length > KIWI_WORD.length) kiwiBuffer.shift()
if (kiwiBuffer.join('') === 'kiwi') {
spawnKiwi()
kiwiBuffer.length = 0
}
} else {
kiwiBuffer.length = 0
}
}
}
onMounted(() => {
if (localStorage.getItem('kiwi-neon-mode')) {
neonMode.value = true
applyNeon()
}
window.addEventListener('keydown', onKeyDown)
})
onUnmounted(() => {
window.removeEventListener('keydown', onKeyDown)
})
return {
neonMode,
kiwiVisible,
kiwiDirection,
toggleNeon,
spawnKiwi,
}
}