bare repo

This commit is contained in:
padreug 2025-03-09 12:28:49 +01:00
parent d73f9bc01e
commit 3d356225cd
31 changed files with 134 additions and 3005 deletions

View file

@ -1,111 +0,0 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import DirectoryGrid from '@/components/directory/DirectoryGrid.vue'
import DirectoryFilter from '@/components/directory/DirectoryFilter.vue'
import { mockDirectoryItems } from '@/data/directory'
import { ref, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTheme } from '@/components/theme-provider'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const { currentTown } = useTheme()
// Use the imported mock data
const items = mockDirectoryItems
const category = ref('all')
const search = ref('')
const town = ref(currentTown.value)
// Watch for route changes to update filters
watch(() => route.query, (newQuery) => {
category.value = newQuery.category?.toString() || 'all'
search.value = newQuery.search?.toString() || ''
town.value = newQuery.town === undefined ? currentTown.value : (newQuery.town?.toString() || 'all')
}, { immediate: true })
// Watch for filter changes and update URL
watch([category, search, town], ([newCategory, newSearch, newTown]) => {
// Don't update URL if it matches current query params
if (
newCategory === route.query.category &&
newSearch === route.query.search &&
newTown === route.query.town
) {
return
}
const query = {
...(newCategory !== 'all' && { category: newCategory }),
...(newSearch && { search: newSearch }),
...(newTown !== currentTown.value && { town: newTown })
}
router.replace({ query })
})
// Filter items based on category, search, and town
const filteredItems = computed(() => {
return items.filter(item => {
const matchesCategory = category.value === 'all' || item.category === category.value
const matchesSearch = !search.value ||
item.name.toLowerCase().includes(search.value.toLowerCase()) ||
item.description?.toLowerCase().includes(search.value.toLowerCase())
const matchesTown = town.value === 'all' || item.town === town.value
return matchesCategory && matchesSearch && matchesTown
})
})
</script>
<template>
<div class="px-0 md:container md:px-4 py-2 md:py-8">
<div class="space-y-4 md:space-y-6">
<!-- Directory Header -->
<div class="text-center space-y-2 mb-2 md:mb-6">
<h1 class="md:hidden text-lg font-semibold tracking-tight px-4">
{{ t('directory.title') }}
</h1>
<div class="hidden md:block space-y-2">
<h1 class="text-2xl font-bold tracking-tight sm:text-3xl">
{{ t('directory.title') }}
</h1>
<p class="text-sm sm:text-base text-muted-foreground">
{{ t('directory.subtitle') }}
</p>
</div>
</div>
<!-- Sticky Container for Filter -->
<div class="sticky top-14 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b md:border-b-0 w-full shadow-sm transition-all duration-200">
<div class="w-full">
<DirectoryFilter v-model:category="category" v-model:search="search" v-model:town="town" class="py-3 px-4" />
</div>
</div>
<!-- Directory Grid -->
<div class="pt-4 px-4 md:px-0">
<DirectoryGrid :items="filteredItems" class="transition-all duration-300" />
</div>
</div>
</div>
</template>
<style scoped>
.fade-move,
.fade-enter-active,
.fade-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(10px);
}
.fade-leave-active {
position: absolute;
}
</style>

View file

@ -1,124 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { Share2, Copy, Check } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import DirectoryItemDetail from '@/components/directory/DirectoryItemDetail.vue'
import { type DirectoryItem } from '@/types/directory'
import { mockDirectoryItems } from '@/data/directory'
const { t } = useI18n()
const props = defineProps<{
id: string
}>()
const item = ref<DirectoryItem | null>(null)
const loading = ref(true)
const error = ref(false)
const justCopied = ref(false)
// Check if Web Share API is available
const canShare = computed(() => typeof navigator !== 'undefined' && !!navigator.share)
// Share functionality
const shareItem = async () => {
if (!item.value) return
const shareData = {
title: item.value.name,
text: item.value.description || `Check out ${item.value.name} on Atitlan Directory`,
url: window.location.href
}
if (canShare.value) {
try {
await navigator.share(shareData)
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
console.error('Error sharing:', err)
}
}
} else {
// Fallback to copying the URL
await copyToClipboard()
}
}
// Copy to clipboard functionality
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(window.location.href)
justCopied.value = true
setTimeout(() => {
justCopied.value = false
}, 2000)
} catch (err) {
console.error('Failed to copy:', err)
}
}
onMounted(async () => {
try {
const found = mockDirectoryItems.find(i => i.id === props.id)
if (!found) {
error.value = true
return
}
item.value = found
} catch (e) {
error.value = true
} finally {
loading.value = false
}
})
</script>
<template>
<div class="container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto">
<!-- Loading State -->
<div v-if="loading" class="text-center py-12">
<div class="animate-pulse">
<div class="h-8 bg-muted rounded w-3/4 mx-auto mb-4"></div>
<div class="h-4 bg-muted rounded w-1/2 mx-auto"></div>
</div>
</div>
<!-- Error State -->
<div v-else-if="error" class="text-center py-12">
<h2 class="text-xl font-semibold mb-2">{{ t('directory.itemNotFound') }}</h2>
<p class="text-muted-foreground mb-4">{{ t('directory.itemNotFoundDesc') }}</p>
<router-link to="/directory"
class="inline-flex items-center justify-center rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90">
{{ t('directory.backToDirectory') }}
</router-link>
</div>
<!-- Directory Item -->
<template v-else-if="item">
<div class="mb-6 flex justify-between items-center">
<router-link to="/directory"
class="text-sm text-muted-foreground hover:text-foreground transition-colors">
{{ t('directory.backToDirectory') }}
</router-link>
<!-- Share Button -->
<Button variant="outline" size="sm" @click="shareItem">
<template v-if="canShare">
<Share2 class="h-4 w-4 mr-2" />
{{ t('directory.share') }}
</template>
<template v-else>
<Copy v-if="!justCopied" class="h-4 w-4 mr-2" />
<Check v-else class="h-4 w-4 mr-2" />
{{ justCopied ? t('directory.linkCopied') : t('directory.copyLink') }}
</template>
</Button>
</div>
<DirectoryItemDetail :item="item" />
</template>
</div>
</div>
</template>

View file

@ -1,48 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion'
const { t } = useI18n()
const faqs = computed(() => [
{
question: t('faq.items.0.question'),
answer: t('faq.items.0.answer')
},
{
question: t('faq.items.1.question'),
answer: t('faq.items.1.answer')
},
{
question: t('faq.items.2.question'),
answer: t('faq.items.2.answer')
},
{
question: t('faq.items.3.question'),
answer: t('faq.items.3.answer')
}
])
</script>
<template>
<div class="container mx-auto px-4 py-8">
<div class="max-w-3xl mx-auto">
<h1 class="text-3xl font-bold text-center mb-8">
{{ t('faq.title') }}
</h1>
<Accordion type="single" collapsible>
<AccordionItem v-for="(faq, index) in faqs" :key="index" :value="'item-' + index">
<AccordionTrigger>{{ faq.question }}</AccordionTrigger>
<AccordionContent>{{ faq.answer }}</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
</template>

View file

@ -1,137 +1,10 @@
<template>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useTheme } from '@/components/theme-provider'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Button } from '@/components/ui/button'
import { computed } from 'vue'
import {
UtensilsCrossed,
Bed,
ShoppingBag,
Sparkles,
Car,
Ship,
HelpCircle,
} from 'lucide-vue-next'
import TukTuk from '@/components/icons/TukTuk.vue'
import { type DirectoryItem } from '@/types/directory'
const { t } = useI18n()
const { currentTown, setCurrentTown } = useTheme()
type CategoryType = DirectoryItem['category']
const categories: CategoryType[] = ['tuktuk', 'restaurant', 'services', 'goods', 'lodging', 'taxi', 'lancha']
const categoryIcons = {
restaurant: UtensilsCrossed,
lodging: Bed,
goods: ShoppingBag,
services: Sparkles,
tuktuk: TukTuk,
taxi: Car,
lancha: Ship,
other: HelpCircle,
} as const
const categoryColors = {
tuktuk: 'text-amber-500',
restaurant: 'text-orange-500',
lodging: 'text-purple-500',
goods: 'text-green-500',
services: 'text-pink-500',
taxi: 'text-yellow-500',
lancha: 'text-blue-500',
other: 'text-gray-500',
} as const
const towns = computed(() => [
{ id: 'all', label: t('directory.towns.all') },
{ id: 'San Marcos', label: 'San Marcos' },
{ id: 'San Pedro', label: 'San Pedro' },
{ id: 'Tzununa', label: 'Tzununa' },
{ id: 'Jaibalito', label: 'Jaibalito' },
{ id: 'San Pablo', label: 'San Pablo' },
{ id: 'Panajachel', label: 'Panajachel' },
])
</script>
<template>
<div class="container mx-auto px-4 py-8 sm:py-12">
<div class="max-w-3xl mx-auto text-center space-y-8 sm:space-y-12">
<!-- Hero Section -->
<div class="space-y-4">
<h1 class="text-4xl font-bold tracking-tight sm:text-6xl text-foreground animate-in fade-in slide-in-from-bottom-4 duration-1000 fill-mode-both">
{{ t('home.title') }}
</h1>
<p class="text-base sm:text-lg text-muted-foreground max-w-2xl mx-auto animate-in fade-in slide-in-from-bottom-4 duration-1000 delay-150 fill-mode-both">
{{ t('home.subtitle') }}
</p>
</div>
<!-- Town Selector -->
<div class="flex flex-col items-center gap-3 animate-in fade-in slide-in-from-bottom-4 duration-1000 delay-300 fill-mode-both">
<h2 class="text-base font-medium text-muted-foreground">{{ t('home.selectTown') }}</h2>
<Select :model-value="currentTown" @update:model-value="setCurrentTown">
<SelectTrigger class="w-[240px] h-11">
<SelectValue :placeholder="t('home.selectTownPlaceholder')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="town in towns" :key="town.id" :value="town.id">
{{ town.label }}
</SelectItem>
</SelectContent>
</Select>
</div>
<!-- Category Buttons -->
<div class="relative mx-auto max-w-2xl pt-4 animate-in fade-in slide-in-from-bottom-4 duration-1000 delay-500 fill-mode-both">
<!-- Gradient Fade Edges -->
<div class="absolute left-0 top-0 bottom-0 w-4 bg-gradient-to-r from-background to-transparent z-10 sm:hidden"></div>
<div class="absolute right-0 top-0 bottom-0 w-4 bg-gradient-to-l from-background to-transparent z-10 sm:hidden"></div>
<!-- Scrollable Container -->
<div class="flex overflow-x-auto gap-2 px-4 pb-4
[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]
sm:flex-wrap sm:justify-center sm:gap-4 sm:px-0">
<router-link v-for="category in categories" :key="category"
:to="`/directory?category=${category}&town=${currentTown}`"
class="flex-shrink-0 w-[100px] sm:w-[110px]">
<Button variant="ghost" size="default"
class="w-full h-20 sm:h-24 flex flex-col items-center justify-center gap-1.5 sm:gap-2.5
hover:bg-accent hover:scale-105 active:scale-100 transition-all duration-200 group">
<component :is="categoryIcons[category]"
class="h-6 w-6 sm:h-7 sm:w-7 transition-transform duration-200 group-hover:scale-110"
:class="categoryColors[category]" />
<span class="text-xs sm:text-sm font-medium text-muted-foreground group-hover:text-foreground transition-colors">
{{ t(`directory.categories.${category}`) }}
</span>
</Button>
</router-link>
</div>
</div>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center pt-4 animate-in fade-in slide-in-from-bottom-4 duration-1000 delay-700 fill-mode-both">
<router-link to="/directory" class="sm:w-[160px]">
<Button variant="default" size="lg" class="w-full font-medium">
{{ t('home.browse') }}
</Button>
</router-link>
<router-link to="/faq" class="sm:w-[160px]">
<Button variant="outline" size="lg" class="w-full font-medium">
{{ t('home.learnMore') }}
</Button>
</router-link>
</div>
</div>
</div>
</template>

View file

@ -1,67 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useNostrStore } from '@/stores/nostr'
import SupportChat from '@/components/SupportChat.vue'
import { Dialog, DialogContent } from '@/components/ui/dialog'
import Login from '@/components/Login.vue'
const nostrStore = useNostrStore()
const showLoginDialog = ref(!nostrStore.isLoggedIn)
const handleLoginSuccess = () => {
showLoginDialog.value = false
}
</script>
<template>
<div class="container max-w-4xl mx-auto h-[calc(100vh-4rem)] py-4 px-4 sm:px-6 lg:px-8">
<div class="h-full">
<div class="h-full animate-in fade-in-50 slide-in-from-bottom-3">
<SupportChat v-if="nostrStore.isLoggedIn" />
</div>
</div>
</div>
<!-- Login Dialog -->
<Dialog v-model:open="showLoginDialog">
<DialogContent class="sm:max-w-md">
<Login @success="handleLoginSuccess" />
</DialogContent>
</Dialog>
</template>
<style scoped>
.animate-in {
animation-duration: 0.3s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
animation-fill-mode: forwards;
}
.fade-in-50 {
animation-name: fade-in-50;
}
.slide-in-from-bottom-3 {
animation-name: slide-in-from-bottom-3;
}
@keyframes fade-in-50 {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-in-from-bottom-3 {
from {
transform: translateY(3px);
}
to {
transform: translateY(0);
}
}
</style>