This commit is contained in:
padreug 2025-02-11 00:25:47 +01:00
parent 9cde9b5cf7
commit f3927b97a4
21 changed files with 8672 additions and 202 deletions

View file

@ -1,29 +1,31 @@
<script setup lang="ts">
import Navbar from './components/layout/Navbar.vue'
import Footer from './components/layout/Footer.vue'
import { useTheme } from '@/components/theme-provider'
import Navbar from '@/components/layout/Navbar.vue'
import Footer from '@/components/layout/Footer.vue'
// Initialize theme
useTheme()
</script>
<template>
<div class="min-h-screen flex flex-col bg-background">
<Navbar />
<main class="flex-1">
<router-view></router-view>
</main>
<Footer />
<div class="min-h-screen bg-background font-sans antialiased">
<div class="relative flex min-h-screen flex-col"
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
<header class="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<nav class="container flex h-14 items-center">
<Navbar />
</nav>
</header>
<main class="flex-1">
<router-view />
</main>
<Footer />
</div>
</div>
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
<style>
/* Remove default styles */
</style>

View file

@ -1,6 +1,89 @@
@import "tailwindcss";
@layer base {
:root {
/* Catppuccin Latte */
--color-background: #eff1f5;
--color-foreground: #4c4f69;
--color-card: #e6e9ef;
--color-card-foreground: #4c4f69;
--color-popover: #e6e9ef;
--color-popover-foreground: #4c4f69;
--color-primary: #1e66f5;
--color-primary-foreground: #eff1f5;
--color-secondary: #ccd0da;
--color-secondary-foreground: #4c4f69;
--color-muted: #ccd0da;
--color-muted-foreground: #6c6f85;
--color-accent: #dc8a78;
--color-accent-foreground: #eff1f5;
--color-destructive: #d20f39;
--color-destructive-foreground: #eff1f5;
--color-border: #bcc0cc;
--color-input: #bcc0cc;
--color-ring: #1e66f5;
--radius: 0.5rem;
}
.dark {
/* Catppuccin Macchiato - we'll use the same colors for dark mode */
--color-background: #24273a;
--color-foreground: #cad3f5;
--color-card: #1e2030;
--color-card-foreground: #cad3f5;
--color-popover: #1e2030;
--color-popover-foreground: #cad3f5;
--color-primary: #8aadf4;
--color-primary-foreground: #24273a;
--color-secondary: #363a4f;
--color-secondary-foreground: #cad3f5;
--color-muted: #363a4f;
--color-muted-foreground: #a5adcb;
--color-accent: #f4dbd6;
--color-accent-foreground: #24273a;
--color-destructive: #ed8796;
--color-destructive-foreground: #24273a;
--color-border: #363a4f;
--color-input: #363a4f;
--color-ring: #8aadf4;
}
* {
@apply box-border;
}
body {
@apply bg-background text-foreground;
}
}
@utility bg-background {
background-color: var(--color-background);
}
@utility text-foreground {
color: var(--color-foreground);
}
/* Add other utility classes for colors */
@utility bg-card {
background-color: var(--color-card);
}
@utility text-card-foreground {
color: var(--color-card-foreground);
}
@utility bg-primary {
background-color: var(--color-primary);
}
@utility text-primary-foreground {
color: var(--color-primary-foreground);
}
/* ... add other utility classes as needed ... */
/* :root { */
/* /* gruvbox light theme */ */
/* --color-background: hsl(32 92% 87%); /* bg0 */ */
@ -64,85 +147,3 @@
/* --color-input: hsl(24 10% 51%); /* fg4 */ */
/* --color-ring: hsl(6 93% 59%); /* red */ */
/* } */
:root {
/* Catppuccin Macchiato */
--color-background: #24273a;
--color-foreground: #cad3f5;
--color-card: #1e2030;
--color-card-foreground: #cad3f5;
--color-popover: #1e2030;
--color-popover-foreground: #cad3f5;
--color-primary: #8aadf4;
--color-primary-foreground: #24273a;
--color-secondary: #363a4f;
--color-secondary-foreground: #cad3f5;
--color-muted: #363a4f;
--color-muted-foreground: #a5adcb;
--color-accent: #f4dbd6;
--color-accent-foreground: #24273a;
--color-destructive: #ed8796;
--color-destructive-foreground: #24273a;
--color-border: #363a4f;
--color-input: #363a4f;
--color-ring: #8aadf4;
--radius: 0.5rem;
}
.dark {
/* Catppuccin Macchiato - we'll use the same colors for dark mode */
--color-background: #24273a;
--color-foreground: #cad3f5;
--color-card: #1e2030;
--color-card-foreground: #cad3f5;
--color-popover: #1e2030;
--color-popover-foreground: #cad3f5;
--color-primary: #8aadf4;
--color-primary-foreground: #24273a;
--color-secondary: #363a4f;
--color-secondary-foreground: #cad3f5;
--color-muted: #363a4f;
--color-muted-foreground: #a5adcb;
--color-accent: #f4dbd6;
--color-accent-foreground: #24273a;
--color-destructive: #ed8796;
--color-destructive-foreground: #24273a;
--color-border: #363a4f;
--color-input: #363a4f;
--color-ring: #8aadf4;
}
* {
@apply box-border;
}
body {
@apply bg-background text-foreground;
}
}
@utility bg-background {
background-color: var(--color-background);
}
@utility text-foreground {
color: var(--color-foreground);
}
/* Add other utility classes for colors */
@utility bg-card {
background-color: var(--color-card);
}
@utility text-card-foreground {
color: var(--color-card-foreground);
}
@utility bg-primary {
background-color: var(--color-primary);
}
@utility text-primary-foreground {
color: var(--color-primary-foreground);
}
/* ... add other utility classes as needed ... */

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { Search } from 'lucide-vue-next'
import { Input } from '@/components/ui/input'
@ -16,7 +16,7 @@ import {
const { t } = useI18n()
defineProps<{
const props = defineProps<{
category: string
search: string
town: string
@ -28,6 +28,18 @@ const emit = defineEmits<{
'update:town': [value: string]
}>()
const searchValue = ref('')
// Watch for changes in the search value
watch(searchValue, (newValue) => {
emit('update:search', newValue)
})
// Watch for prop changes to update local value
watch(() => props.search, (newValue) => {
searchValue.value = newValue
}, { immediate: true })
const categoryIcons = {
restaurant: UtensilsCrossed,
lodging: Bed,
@ -77,27 +89,33 @@ const towns = computed(() => [
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search class="h-5 w-5 text-muted-foreground" />
</div>
<Input type="text" :value="search" @input="emit('update:search', ($event.target as HTMLInputElement).value)"
class="pl-10" :placeholder="t('directory.search')" />
<Input
v-model="searchValue"
type="text"
class="pl-10 w-full"
:placeholder="t('directory.search')"
inputmode="text"
enterkeyhint="search"
/>
</div>
<div class="flex flex-col md:flex-row gap-1 sm:gap-2">
<!-- Category Filter -->
<div class="flex gap-1 sm:gap-2 overflow-x-auto pb-1 sm:pb-2 md:pb-0">
<Button v-for="cat in categories" :key="cat.id" @click="emit('update:category', cat.id)"
:variant="category === cat.id ? 'default' : 'secondary'" size="sm" class="rounded-full whitespace-nowrap">
:variant="props.category === cat.id ? 'default' : 'secondary'" size="sm" class="rounded-full whitespace-nowrap">
<!-- Show only icon on mobile for non-'all' categories -->
<template v-if="cat.id !== 'all'">
<component :is="cat.icon"
class="h-4 w-4 md:mr-2"
:class="[
category === cat.id ? '' : categoryColors[cat.id as keyof typeof categoryColors],
props.category === cat.id ? '' : categoryColors[cat.id as keyof typeof categoryColors],
'md:hidden'
]"
/>
<component :is="cat.icon"
class="h-4 w-4 mr-2 hidden md:inline-block"
:class="category === cat.id ? '' : categoryColors[cat.id as keyof typeof categoryColors]"
:class="props.category === cat.id ? '' : categoryColors[cat.id as keyof typeof categoryColors]"
/>
<span class="hidden md:inline">{{ cat.label }}</span>
</template>
@ -111,7 +129,7 @@ const towns = computed(() => [
<!-- Town Filter -->
<div class="flex gap-1 sm:gap-2 overflow-x-auto pb-1 sm:pb-2 md:pb-0">
<Button v-for="to in towns" :key="to.id" @click="emit('update:town', to.id)"
:variant="town === to.id ? 'default' : 'secondary'" size="sm" class="rounded-full whitespace-nowrap">
:variant="props.town === to.id ? 'default' : 'secondary'" size="sm" class="rounded-full whitespace-nowrap">
{{ to.label }}
</Button>
</div>

View file

@ -53,14 +53,14 @@ const filteredItems = computed(() => {
</script>
<template>
<div class="container mx-auto px-4 py-2 sm:py-8">
<div>
<!-- Filters -->
<div class="mb-4 sm:mb-8 space-y-4 md:space-y-0 md:flex md:items-center md:justify-between">
<div class="space-y-2 md:space-y-0 md:flex md:items-center md:justify-between">
<DirectoryFilter v-model:category="selectedCategory" v-model:search="searchQuery" v-model:town="selectedTown" />
</div>
<!-- Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-4">
<DirectoryCard v-for="item in filteredItems" :key="item.id" :item="item" />
</div>

View file

@ -1,11 +1,12 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { Menu, X } from 'lucide-vue-next'
import ThemeToggle from '@/components/ThemeToggle.vue'
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
import { Menu, X, Sun, Moon, Zap } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { useTheme } from '@/components/theme-provider'
const { t } = useI18n()
const { t, locale } = useI18n()
const { theme, setTheme } = useTheme()
const isOpen = ref(false)
const navigation = computed(() => [
@ -17,62 +18,72 @@ const navigation = computed(() => [
const toggleMenu = () => {
isOpen.value = !isOpen.value
}
const toggleTheme = () => {
setTheme(theme.value === 'dark' ? 'light' : 'dark')
}
const toggleLocale = () => {
// Toggle between 'en' and 'es'
const newLocale = locale.value === 'en' ? 'es' : 'en'
locale.value = newLocale
// Store the preference
localStorage.setItem('locale', newLocale)
}
</script>
<template>
<nav class="bg-card border-b border-border">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 justify-between">
<!-- Logo and Desktop Navigation -->
<div class="flex">
<div class="flex flex-shrink-0 items-center">
<!-- Replace with your logo -->
<router-link to="/" class="text-xl font-bold text-card-foreground">
{{ t('nav.title') }}
</router-link>
</div>
<nav class="w-full">
<div class="flex items-center justify-between">
<!-- Logo and Desktop Navigation -->
<div class="flex items-center gap-6">
<router-link to="/" class="flex ml-4 items-center gap-2">
<Zap class="h-6 w-6 text-orange-600 dark:text-orange-400" />
<span class=" font-semibold">{{ t('nav.title') }}</span>
</router-link>
<!-- Desktop Navigation -->
<div class="hidden sm:ml-6 sm:flex sm:space-x-8">
<router-link v-for="item in navigation" :key="item.name" :to="item.href"
class="inline-flex items-center px-1 pt-1 text-sm font-medium" :class="[
$route.path === item.href
? 'border-b-2 border-primary text-card-foreground'
: 'text-muted-foreground hover:border-b-2 hover:border-muted hover:text-card-foreground'
]">
{{ item.name }}
</router-link>
</div>
<!-- Desktop Navigation -->
<div class="hidden md:flex gap-4">
<router-link v-for="item in navigation" :key="item.name" :to="item.href"
class="text-muted-foreground hover:text-foreground transition-colors" :class="{
'text-foreground': $route.path === item.href
}">
{{ item.name }}
</router-link>
</div>
</div>
<!-- Theme Toggle -->
<div class="flex items-center gap-2">
<LanguageSwitcher />
<ThemeToggle />
</div>
<!-- Theme Toggle and Language -->
<div class="flex items-center gap-2">
<Button variant="ghost" size="icon" @click="toggleTheme">
<Sun v-if="theme === 'dark'" class="h-5 w-5" />
<Moon v-else class="h-5 w-5" />
</Button>
<Button variant="ghost" class="text-muted-foreground hover:text-foreground transition-colors"
@click="toggleLocale">
{{ locale === 'en' ? '🇪🇸 ES' : '🇺🇸 EN' }}
</Button>
<!-- Mobile menu button -->
<div class="flex items-center sm:hidden">
<button type="button"
class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500"
@click="toggleMenu">
<span class="sr-only">Open main menu</span>
<Menu v-if="!isOpen" class="block h-6 w-6" aria-hidden="true" />
<X v-else class="block h-6 w-6" aria-hidden="true" />
</button>
</div>
<Button variant="ghost" size="icon" class="md:hidden" @click="toggleMenu">
<span class="sr-only">Open main menu</span>
<Menu v-if="!isOpen" class="h-5 w-5" />
<X v-else class="h-5 w-5" />
</Button>
</div>
</div>
<!-- Mobile menu -->
<div :class="[isOpen ? 'block' : 'hidden']" class="sm:hidden">
<div class="space-y-1 pb-3 pt-2">
<div v-show="isOpen"
class="absolute left-0 right-0 top-14 z-50 border-b bg-background md:hidden
[@supports(backdrop-filter:blur(24px))]:bg-background/95 [@supports(backdrop-filter:blur(24px))]:backdrop-blur-sm">
<div class="container space-y-1 py-3">
<router-link v-for="item in navigation" :key="item.name" :to="item.href"
class="block border-l-4 py-2 pl-3 pr-4 text-base font-medium" :class="[
$route.path === item.href
? 'border-primary bg-primary/10 text-primary'
: 'border-transparent text-gray-500 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-700'
]" @click="isOpen = false">
class="block px-3 py-2 text-base font-medium text-muted-foreground hover:text-foreground transition-colors"
:class="{
'text-foreground': $route.path === item.href
}" @click="isOpen = false">
{{ item.name }}
</router-link>
</div>

View file

@ -0,0 +1,53 @@
import { computed, onMounted, ref, watch } from 'vue'
type Theme = 'dark' | 'light' | 'system'
const useTheme = () => {
const theme = ref<Theme>('dark')
const systemTheme = ref<'dark' | 'light'>('light')
const updateSystemTheme = () => {
systemTheme.value = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
const currentTheme = computed(() => {
return theme.value === 'system' ? systemTheme.value : theme.value
})
const applyTheme = () => {
if (currentTheme.value === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}
onMounted(() => {
const stored = localStorage.getItem('ui-theme')
if (stored && (stored === 'dark' || stored === 'light' || stored === 'system')) {
theme.value = stored as Theme
}
updateSystemTheme()
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateSystemTheme)
applyTheme()
})
watch(currentTheme, () => {
applyTheme()
})
const setTheme = (newTheme: Theme) => {
theme.value = newTheme
localStorage.setItem('ui-theme', newTheme)
}
return {
theme,
setTheme,
systemTheme,
currentTheme
}
}
export { useTheme }

View file

@ -3,8 +3,23 @@ import App from './App.vue'
import router from './router'
import { i18n } from './i18n'
import './assets/index.css'
import { registerSW } from 'virtual:pwa-register'
const app = createApp(App)
app.use(router)
app.use(i18n)
// Simple periodic service worker updates
const intervalMS = 60 * 60 * 1000 // 1 hour
registerSW({
onRegistered(r) {
r && setInterval(() => {
r.update()
}, intervalMS)
},
onOfflineReady() {
console.log('App ready to work offline')
}
})
app.mount('#app')

View file

@ -10,7 +10,7 @@ const items = mockDirectoryItems
</script>
<template>
<div class="container mx-auto px-4 py-8">
<div class="container mx-auto px-4 py-2 md:py-8">
<div class="space-y-4 sm:space-y-8">
<!-- Directory Header - Hidden on mobile -->
<div class="hidden sm:block text-center space-y-4">

1
src/vite-env.d.ts vendored
View file

@ -1 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-pwa/client" />