bare repo
This commit is contained in:
parent
d73f9bc01e
commit
3d356225cd
31 changed files with 134 additions and 3005 deletions
|
|
@ -1,397 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick, onMounted, watch, onUnmounted } from 'vue'
|
||||
import { useNostrStore } from '@/stores/nostr'
|
||||
import { npubToHex } from '@/lib/nostr'
|
||||
import type { DirectMessage } from '@/types/nostr'
|
||||
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Send } from 'lucide-vue-next'
|
||||
import MessageBubble from '@/components/ui/message-bubble/MessageBubble.vue'
|
||||
import ConnectionStatus from '@/components/ConnectionStatus.vue'
|
||||
|
||||
const nostrStore = useNostrStore()
|
||||
const input = ref('')
|
||||
const isSending = ref(false)
|
||||
const error = ref('')
|
||||
const messagesEndRef = ref<HTMLDivElement | null>(null)
|
||||
const isSubscribing = ref(false)
|
||||
const SUPPORT_NPUB = import.meta.env.VITE_SUPPORT_NPUB
|
||||
|
||||
if (!SUPPORT_NPUB) {
|
||||
error.value = 'Support public key not configured'
|
||||
}
|
||||
|
||||
const inputLength = computed(() => input.value.trim().length)
|
||||
|
||||
// Group messages by sender and time
|
||||
interface MessageGroup {
|
||||
sent: boolean
|
||||
messages: DirectMessage[]
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
const groupedMessages = computed<MessageGroup[]>(() => {
|
||||
const groups: MessageGroup[] = []
|
||||
let currentGroup: MessageGroup | null = null
|
||||
|
||||
// Sort messages by timestamp first
|
||||
const sortedMessages = [...nostrStore.currentMessages].sort((a, b) => a.created_at - b.created_at)
|
||||
|
||||
for (const message of sortedMessages) {
|
||||
// Start a new group if:
|
||||
// 1. No current group
|
||||
// 2. Different sender than last message
|
||||
// 3. More than 2 minutes since last message
|
||||
if (!currentGroup ||
|
||||
currentGroup.sent !== message.sent ||
|
||||
message.created_at - currentGroup.messages[currentGroup.messages.length - 1].created_at > 120) {
|
||||
currentGroup = {
|
||||
sent: message.sent,
|
||||
messages: [],
|
||||
timestamp: message.created_at
|
||||
}
|
||||
groups.push(currentGroup)
|
||||
}
|
||||
currentGroup.messages.push(message)
|
||||
}
|
||||
return groups
|
||||
})
|
||||
|
||||
// Scroll to bottom when new messages arrive
|
||||
watch(() => nostrStore.currentMessages.length, () => {
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
if (!SUPPORT_NPUB) return
|
||||
|
||||
const supportPubkeyHex = npubToHex(SUPPORT_NPUB)
|
||||
nostrStore.activeChat = supportPubkeyHex
|
||||
|
||||
// Only subscribe if not already subscribed
|
||||
if (!nostrStore.hasActiveSubscription) {
|
||||
isSubscribing.value = true
|
||||
nostrStore.subscribeToMessages()
|
||||
.catch(err => {
|
||||
console.debug('Support chat subscription error:', err)
|
||||
})
|
||||
.finally(() => {
|
||||
isSubscribing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
scrollToBottom()
|
||||
} catch (err) {
|
||||
console.debug('Support chat setup error:', err)
|
||||
}
|
||||
})
|
||||
|
||||
// Remove the unsubscribe on unmount since we want to keep the connection
|
||||
onUnmounted(() => {
|
||||
// Only clear active chat
|
||||
nostrStore.activeChat = null
|
||||
})
|
||||
|
||||
// Watch for changes in activeChat
|
||||
watch(() => nostrStore.activeChat, async (newChat) => {
|
||||
if (newChat) {
|
||||
try {
|
||||
isSubscribing.value = true
|
||||
await nostrStore.subscribeToMessages()
|
||||
} catch (err) {
|
||||
console.debug('Chat subscription error:', err)
|
||||
// Continue anyway
|
||||
} finally {
|
||||
isSubscribing.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function scrollToBottom() {
|
||||
if (messagesEndRef.value) {
|
||||
// Get the scroll area element
|
||||
const scrollArea = messagesEndRef.value.closest('.scrollarea-viewport')
|
||||
if (scrollArea) {
|
||||
scrollArea.scrollTop = scrollArea.scrollHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessage = async (event: Event) => {
|
||||
event.preventDefault()
|
||||
if (inputLength.value === 0 || !nostrStore.activeChat || isSending.value) return
|
||||
|
||||
try {
|
||||
isSending.value = true
|
||||
await nostrStore.sendMessage(nostrStore.activeChat, input.value)
|
||||
input.value = ''
|
||||
error.value = ''
|
||||
} catch (err) {
|
||||
console.error('Failed to send message:', err)
|
||||
error.value = 'Failed to send message. Please try again.'
|
||||
} finally {
|
||||
isSending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp * 1000)
|
||||
const today = new Date()
|
||||
const yesterday = new Date(today)
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return 'Today'
|
||||
} else if (date.toDateString() === yesterday.toDateString()) {
|
||||
return 'Yesterday'
|
||||
}
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
|
||||
const getMessageGroupClasses = (sent: boolean) => {
|
||||
return [
|
||||
'group flex flex-col gap-0.5 animate-in slide-in-from-bottom-2',
|
||||
sent ? 'items-end' : 'items-start'
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card
|
||||
class="flex flex-col h-full bg-gradient-to-b from-card to-background border-border shadow-2xl overflow-hidden relative z-0">
|
||||
<CardHeader
|
||||
class="flex-shrink-0 flex flex-row items-center justify-between px-6 py-4 border-b border-border/50 bg-background/95 backdrop-blur-md relative z-50">
|
||||
<!-- Left side with avatar and name -->
|
||||
<div class="flex items-center gap-5 flex-shrink-0">
|
||||
<div class="relative group">
|
||||
<div
|
||||
class="absolute -inset-0.5 bg-gradient-to-r from-primary to-primary/50 rounded-full opacity-75 group-hover:opacity-100 blur transition duration-200">
|
||||
</div>
|
||||
<Avatar
|
||||
class="relative h-11 w-11 bg-muted ring-2 ring-ring ring-offset-2 ring-offset-background shadow-md transition-all duration-200 hover:shadow-lg group-hover:scale-105">
|
||||
<AvatarFallback class="text-base font-semibold text-foreground">SA</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div class="hidden sm:block">
|
||||
<p class="font-semibold leading-none text-foreground tracking-tight">Support Agent</p>
|
||||
<div class="flex items-center gap-1.5 mt-1.5">
|
||||
<ConnectionStatus />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center badge -->
|
||||
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<div
|
||||
class="text-xs font-medium text-foreground bg-gradient-to-r from-muted/80 to-muted/50 backdrop-blur-sm px-4 py-2 rounded-full whitespace-nowrap shadow-lg border border-border/50">
|
||||
Customer Support
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right side spacer to maintain layout balance -->
|
||||
<div class="flex items-center gap-5 flex-shrink-0 invisible">
|
||||
<div class="h-11 w-11"></div>
|
||||
<div class="hidden sm:block">
|
||||
<div class="h-5"></div>
|
||||
<div class="h-5 mt-1.5"></div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="flex-1 min-h-0 p-0 bg-gradient-to-b from-card to-background overflow-hidden">
|
||||
<ScrollArea class="h-full" type="hover">
|
||||
<div class="flex flex-col gap-4 px-6 py-4">
|
||||
<template v-for="(group, groupIndex) in groupedMessages" :key="groupIndex">
|
||||
<!-- Date separator -->
|
||||
<div v-if="groupIndex === 0 ||
|
||||
formatDate(group.timestamp) !== formatDate(groupedMessages[groupIndex - 1].timestamp)"
|
||||
class="flex justify-center my-8">
|
||||
<div
|
||||
class="px-4 py-1.5 rounded-full bg-gradient-to-r from-muted/30 to-muted/30 text-xs font-medium text-muted-foreground shadow-lg backdrop-blur-sm border border-border/20">
|
||||
{{ formatDate(group.timestamp) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message group -->
|
||||
<div :class="getMessageGroupClasses(group.sent)" class="w-full">
|
||||
<div class="flex flex-col gap-[3px] w-full" :class="{ 'items-end': group.sent }">
|
||||
<MessageBubble v-for="(message, messageIndex) in group.messages" :key="message.id" :sent="group.sent"
|
||||
:is-first="messageIndex === 0" :is-last="messageIndex === group.messages.length - 1"
|
||||
:content="message.content" :timestamp="message.created_at"
|
||||
:show-timestamp="messageIndex === group.messages.length - 1" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="messagesEndRef" class="h-px" />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter class="flex-shrink-0 border-t border-border/50 bg-background/95 backdrop-blur-md p-4 shadow-xl">
|
||||
<form @submit="sendMessage" class="flex w-full items-center gap-4">
|
||||
<Input id="message" v-model="input" placeholder="Type your message..."
|
||||
class="flex-1 bg-card/90 border-border text-foreground placeholder:text-muted-foreground focus:ring-2 focus:ring-ring focus:border-ring transition-all duration-300 shadow-lg hover:border-border/60 rounded-xl h-11"
|
||||
autocomplete="off" />
|
||||
<Button type="submit" size="icon" :disabled="inputLength === 0 || isSending"
|
||||
class="bg-gradient-to-r from-primary to-primary/80 text-primary-foreground hover:brightness-110 active:brightness-90 transition-all duration-300 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:hover:shadow-lg h-11 w-11 rounded-xl flex-shrink-0">
|
||||
<Send v-if="!isSending" class="h-4 w-4" />
|
||||
<div v-else
|
||||
class="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-r-transparent" />
|
||||
<span class="sr-only">{{ isSending ? 'Sending...' : 'Send' }}</span>
|
||||
</Button>
|
||||
</form>
|
||||
</CardFooter>
|
||||
|
||||
<!-- Add loading indicator if needed -->
|
||||
<div v-if="isSubscribing" class="absolute top-4 right-4 flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<div class="h-2 w-2 rounded-full bg-primary animate-pulse"></div>
|
||||
Connecting...
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.animate-in {
|
||||
animation: animate-in 0.3s cubic-bezier(0.21, 1.02, 0.73, 1);
|
||||
}
|
||||
|
||||
@keyframes animate-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(4px) scale(0.98);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.break-words {
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.select-none {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.select-text {
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
||||
/* Enhanced scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #45475a;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #585b70;
|
||||
}
|
||||
|
||||
:deep(.scrollarea-viewport) {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* Remove any scroll-behavior from parent elements */
|
||||
.scrollarea-viewport {
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
|
||||
/* Ensure proper containment */
|
||||
.card {
|
||||
position: relative;
|
||||
contain: content;
|
||||
}
|
||||
|
||||
/* Improved focus styles */
|
||||
:focus-visible {
|
||||
outline: 2px solid #cba6f7;
|
||||
outline-offset: 2px;
|
||||
transition: outline-offset 0.2s ease;
|
||||
}
|
||||
|
||||
/* Enhanced button hover states */
|
||||
button:not(:disabled):hover,
|
||||
a:hover {
|
||||
transform: translateY(-1px) scale(1.02);
|
||||
}
|
||||
|
||||
button:not(:disabled):active,
|
||||
a:active {
|
||||
transform: translateY(0) scale(0.98);
|
||||
}
|
||||
|
||||
/* Smooth transitions */
|
||||
* {
|
||||
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
/* Glass morphism effects */
|
||||
.backdrop-blur-md {
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
/* Gradient animations */
|
||||
@keyframes gradient-shift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-gradient-animate {
|
||||
background-size: 200% 200%;
|
||||
animation: gradient-shift 15s ease infinite;
|
||||
}
|
||||
|
||||
/* Add these new styles */
|
||||
.translate-center {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* Update flex layout styles */
|
||||
.chat-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 2rem);
|
||||
max-height: calc(100vh - 2rem);
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { type DirectoryItem } from '@/types/directory'
|
||||
import {
|
||||
MapPin,
|
||||
Phone,
|
||||
ExternalLink,
|
||||
Zap,
|
||||
MessageCircle,
|
||||
Facebook,
|
||||
Instagram,
|
||||
Twitter,
|
||||
Youtube,
|
||||
UtensilsCrossed,
|
||||
Bed,
|
||||
ShoppingBag,
|
||||
Car,
|
||||
Ship,
|
||||
HelpCircle,
|
||||
Sparkles,
|
||||
} from 'lucide-vue-next'
|
||||
import TukTuk from '@/components/icons/TukTuk.vue'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from '@/components/ui/card'
|
||||
|
||||
defineProps<{
|
||||
item: DirectoryItem
|
||||
}>()
|
||||
|
||||
|
||||
const categoryIcons = {
|
||||
restaurant: UtensilsCrossed,
|
||||
lodging: Bed,
|
||||
goods: ShoppingBag,
|
||||
services: Sparkles,
|
||||
taxi: Car,
|
||||
tuktuk: TukTuk,
|
||||
lancha: Ship,
|
||||
other: HelpCircle,
|
||||
} as const
|
||||
|
||||
const categoryColors = {
|
||||
restaurant: 'text-orange-500',
|
||||
lodging: 'text-purple-500',
|
||||
goods: 'text-green-500',
|
||||
services: 'text-pink-500',
|
||||
taxi: 'text-yellow-500',
|
||||
tuktuk: 'text-amber-500',
|
||||
lancha: 'text-blue-500',
|
||||
other: 'text-gray-500',
|
||||
} as const
|
||||
|
||||
const socials = {
|
||||
facebook: Facebook,
|
||||
instagram: Instagram,
|
||||
twitter: Twitter,
|
||||
youtube: Youtube,
|
||||
}
|
||||
|
||||
const socialColors = {
|
||||
facebook: 'text-blue-600 hover:text-blue-700',
|
||||
instagram: 'text-pink-600 hover:text-pink-700',
|
||||
twitter: 'text-blue-400 hover:text-blue-500',
|
||||
youtube: 'text-red-600 hover:text-red-700',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card class="hover:shadow-md transition-shadow relative overflow-hidden group cursor-pointer"
|
||||
@click="$router.push(`/directory/${item.id}`)">
|
||||
<!-- Local watermark -->
|
||||
<!-- <div v-if="item.local" class="absolute right-3 bottom-2 text-2xl tracking-widest font-bold text-primary opacity-30"> -->
|
||||
<!-- LOCAL -->
|
||||
<!-- </div> -->
|
||||
|
||||
<CardContent class="p-6 space-y-4 relative z-20">
|
||||
<!-- Image -->
|
||||
<div v-if="item.imageUrl" class="aspect-video w-full overflow-hidden rounded-md">
|
||||
<img :src="item.imageUrl" :alt="item.name" class="h-full w-full object-cover" />
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<component :is="categoryIcons[item.category]" class="h-6 w-6" :class="categoryColors[item.category]"
|
||||
:title="item.category" />
|
||||
<CardTitle>{{ item.name }}</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardDescription>
|
||||
{{ item.description }}
|
||||
</CardDescription>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div class="space-y-2">
|
||||
<div v-if="item.town || item.address" class="flex items-center text-sm group">
|
||||
<MapPin class="mr-2 h-4 w-4 text-red-400" />
|
||||
<a v-if="item.mapsUrl" :href="item.mapsUrl" target="_blank" rel="noopener noreferrer"
|
||||
class="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors relative"
|
||||
@click.stop>
|
||||
<span>{{ [item.address, item.town].filter(Boolean).join(', ') }}</span>
|
||||
<ExternalLink class="h-3 w-3 ml-1" />
|
||||
</a>
|
||||
<span v-else>{{ [item.address, item.town].filter(Boolean).join(', ') }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="item.contact" class="flex items-center text-sm gap-2">
|
||||
<Phone class="h-4 w-4" />
|
||||
<span>{{ item.contact }}</span>
|
||||
<div v-if="item.contactType" class="flex gap-1">
|
||||
<a v-if="item.contactType.includes('whatsapp')" :href="`https://wa.me/${item.contact.replace(/\D/g, '')}`"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
class="bg-green-100 text-green-800 px-2 py-0.5 rounded-full text-xs font-medium flex items-center gap-1 hover:bg-green-200 transition-colors relative"
|
||||
@click.stop>
|
||||
<MessageCircle class="h-3 w-3" />
|
||||
WhatsApp
|
||||
</a>
|
||||
<a v-if="item.contactType.includes('telegram')" :href="`https://t.me/${item.contact.replace(/\D/g, '')}`"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
class="bg-blue-100 text-blue-800 px-2 py-0.5 rounded-full text-xs font-medium flex items-center gap-1 hover:bg-blue-200 transition-colors relative"
|
||||
@click.stop>
|
||||
<MessageCircle class="h-3 w-3" />
|
||||
Telegram
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.lightning" class="flex items-center text-sm">
|
||||
<Zap class="mr-2 h-4 w-4 text-amber-500" />
|
||||
<a :href="`lightning:${item.lightning}`"
|
||||
class="text-muted-foreground hover:text-foreground transition-colors" @click.stop>
|
||||
{{ item.lightning }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Website -->
|
||||
<div v-if="item.url" class="flex items-center text-sm">
|
||||
<ExternalLink class="mr-2 h-4 w-4" />
|
||||
<a :href="item.url" target="_blank" rel="noopener noreferrer"
|
||||
class="text-muted-foreground hover:text-foreground transition-colors" @click.stop>
|
||||
{{ item.url.replace(/^https?:\/\//, '').split('/')[0] }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Social Media Links -->
|
||||
<div v-if="item.social" class="flex items-center gap-2 text-sm">
|
||||
<template v-for="(url, platform) in item.social" :key="platform">
|
||||
<a v-if="url" :href="url" target="_blank" rel="noopener noreferrer" :class="[
|
||||
'transition-colors relative',
|
||||
socialColors[platform as keyof typeof socialColors]
|
||||
]" :title="platform.charAt(0).toUpperCase() + platform.slice(1)" @click.stop>
|
||||
<component :is="socials[platform as keyof typeof socials]" class="h-4 w-4" />
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Search } from 'lucide-vue-next'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
UtensilsCrossed,
|
||||
Bed,
|
||||
ShoppingBag,
|
||||
Sparkles,
|
||||
Car,
|
||||
Ship,
|
||||
HelpCircle,
|
||||
} from 'lucide-vue-next'
|
||||
import TukTuk from '@/components/icons/TukTuk.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
category: string
|
||||
search: string
|
||||
town: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:category': [value: string]
|
||||
'update:search': [value: string]
|
||||
'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 = {
|
||||
tuktuk: TukTuk,
|
||||
restaurant: UtensilsCrossed,
|
||||
lodging: Bed,
|
||||
goods: ShoppingBag,
|
||||
services: Sparkles,
|
||||
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',
|
||||
lancha: 'text-blue-500',
|
||||
taxi: 'text-yellow-500',
|
||||
other: 'text-gray-500',
|
||||
} as const
|
||||
|
||||
const categories = computed(() => [
|
||||
{ id: 'all', label: t('directory.categories.all') },
|
||||
{ id: 'restaurant', label: t('directory.categories.restaurant'), icon: categoryIcons.restaurant },
|
||||
{ id: 'tuktuk', label: t('directory.categories.tuktuk'), icon: categoryIcons.tuktuk },
|
||||
{ id: 'lodging', label: t('directory.categories.lodging'), icon: categoryIcons.lodging },
|
||||
{ id: 'goods', label: t('directory.categories.goods'), icon: categoryIcons.goods },
|
||||
{ id: 'services', label: t('directory.categories.services'), icon: categoryIcons.services },
|
||||
{ id: 'lancha', label: t('directory.categories.lancha'), icon: categoryIcons.lancha },
|
||||
{ id: 'taxi', label: t('directory.categories.taxi'), icon: categoryIcons.taxi },
|
||||
{ id: 'other', label: t('directory.categories.other'), icon: categoryIcons.other },
|
||||
])
|
||||
|
||||
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="w-full space-y-3">
|
||||
<!-- Search Input -->
|
||||
<div class="relative w-full max-w-xl mx-auto">
|
||||
<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 v-model="searchValue" type="text" class="pl-10 w-full" :placeholder="t('directory.search')"
|
||||
inputmode="text" enterkeyhint="search" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Town Filter -->
|
||||
<div class="flex justify-start md:justify-center gap-1 overflow-x-auto pb-2 px-2 md:px-0
|
||||
[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||
<Button v-for="to in towns" :key="to.id" @click="emit('update:town', to.id)"
|
||||
:variant="props.town === to.id ? 'default' : 'secondary'" size="sm"
|
||||
class="rounded-full whitespace-nowrap">
|
||||
{{ to.label }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Category Filter -->
|
||||
<div class="flex justify-start md:justify-center gap-1 overflow-x-auto pb-2 px-2 md:px-0
|
||||
[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||
<Button v-for="cat in categories" :key="cat.id" @click="emit('update:category', cat.id)"
|
||||
: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="[
|
||||
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="props.category === cat.id ? '' : categoryColors[cat.id as keyof typeof categoryColors]" />
|
||||
<span class="hidden md:inline">{{ cat.label }}</span>
|
||||
</template>
|
||||
<!-- Always show text for 'all' category -->
|
||||
<template v-else>
|
||||
{{ cat.label }}
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import Fuse from 'fuse.js'
|
||||
import DirectoryCard from './DirectoryCard.vue'
|
||||
import { type DirectoryItem } from '@/types/directory'
|
||||
|
||||
const selectedCategory = ref<string>('all')
|
||||
const selectedTown = ref<string>('all')
|
||||
const searchQuery = ref('')
|
||||
|
||||
const props = defineProps<{
|
||||
items: DirectoryItem[]
|
||||
}>()
|
||||
|
||||
// Configure Fuse.js options
|
||||
const fuseOptions = {
|
||||
keys: [
|
||||
'name',
|
||||
'description',
|
||||
'town',
|
||||
'address',
|
||||
'contact',
|
||||
'lightning'
|
||||
],
|
||||
threshold: 0.3, // Lower threshold means more strict matching
|
||||
ignoreLocation: true,
|
||||
shouldSort: true
|
||||
}
|
||||
|
||||
const fuse = new Fuse(props.items, fuseOptions)
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
let results = props.items
|
||||
|
||||
// Apply search if query exists
|
||||
if (searchQuery.value) {
|
||||
results = fuse.search(searchQuery.value).map((result: { item: any }) => result.item)
|
||||
}
|
||||
|
||||
// Apply category filter
|
||||
if (selectedCategory.value !== 'all') {
|
||||
results = results.filter(item => item.category === selectedCategory.value)
|
||||
}
|
||||
|
||||
// Apply town filter
|
||||
if (selectedTown.value !== 'all') {
|
||||
results = results.filter(item => item.town === selectedTown.value)
|
||||
}
|
||||
|
||||
return results
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Grid -->
|
||||
<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>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="filteredItems.length === 0" class="text-center py-12">
|
||||
<p class="text-lg text-muted-foreground">
|
||||
No results found. Try adjusting your filters.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { type DirectoryItem } from '@/types/directory'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
MapPin,
|
||||
Phone,
|
||||
ExternalLink,
|
||||
Zap,
|
||||
MessageCircle,
|
||||
Facebook,
|
||||
Instagram,
|
||||
Twitter,
|
||||
Youtube,
|
||||
UtensilsCrossed,
|
||||
Bed,
|
||||
ShoppingBag,
|
||||
Car,
|
||||
Ship,
|
||||
HelpCircle,
|
||||
Sparkles,
|
||||
} from 'lucide-vue-next'
|
||||
import TukTuk from '@/components/icons/TukTuk.vue'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from '@/components/ui/card'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
item: DirectoryItem
|
||||
}>()
|
||||
|
||||
const socials = {
|
||||
facebook: Facebook,
|
||||
instagram: Instagram,
|
||||
twitter: Twitter,
|
||||
youtube: Youtube,
|
||||
}
|
||||
|
||||
const categoryIcons = {
|
||||
restaurant: UtensilsCrossed,
|
||||
lodging: Bed,
|
||||
goods: ShoppingBag,
|
||||
services: Sparkles,
|
||||
taxi: Car,
|
||||
tuktuk: TukTuk,
|
||||
lancha: Ship,
|
||||
other: HelpCircle,
|
||||
} as const
|
||||
|
||||
const categoryColors = {
|
||||
restaurant: 'text-orange-500',
|
||||
lodging: 'text-purple-500',
|
||||
goods: 'text-green-500',
|
||||
services: 'text-pink-500',
|
||||
taxi: 'text-yellow-500',
|
||||
tuktuk: 'text-amber-500',
|
||||
lancha: 'text-blue-500',
|
||||
other: 'text-gray-500',
|
||||
} as const
|
||||
|
||||
const socialColors = {
|
||||
facebook: 'text-blue-600 hover:text-blue-700',
|
||||
instagram: 'text-pink-600 hover:text-pink-700',
|
||||
twitter: 'text-blue-400 hover:text-blue-500',
|
||||
youtube: 'text-red-600 hover:text-red-700',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card class="relative overflow-hidden">
|
||||
<!-- Local badge -->
|
||||
<!-- <div v-if="item.local" -->
|
||||
<!-- class="absolute right-6 top-6 rounded-full bg-primary/10 px-3 py-1 text-sm font-medium text-primary"> -->
|
||||
<!-- LOCAL -->
|
||||
<!-- </div> -->
|
||||
|
||||
<!-- Image Banner -->
|
||||
<div v-if="item.imageUrl" class="w-full h-64 overflow-hidden">
|
||||
<img :src="item.imageUrl" :alt="item.name" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
|
||||
<CardHeader>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<component :is="categoryIcons[item.category]" class="h-8 w-8" :class="categoryColors[item.category]"
|
||||
:title="t(`directory.categories.${item.category}`)" />
|
||||
<CardTitle class="text-3xl">{{ item.name }}</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
<CardDescription v-if="item.description" class="text-base mt-4">
|
||||
{{ item.description }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<!-- Left Column -->
|
||||
<div class="space-y-4">
|
||||
<!-- Location -->
|
||||
<div v-if="item.town || item.address" class="flex items-start gap-3 text-base">
|
||||
<MapPin class="h-5 w-5 text-red-500 mt-0.5" />
|
||||
<div class="space-y-1">
|
||||
<div>{{ [item.address, item.town].filter(Boolean).join(', ') }}</div>
|
||||
<a v-if="item.mapsUrl" :href="item.mapsUrl" target="_blank" rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors">
|
||||
{{ t('directory.viewMap') }}
|
||||
<ExternalLink class="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phone -->
|
||||
<div v-if="item.contact" class="flex items-start gap-3 text-base">
|
||||
<Phone class="h-5 w-5 mt-0.5" />
|
||||
<div class="space-y-2">
|
||||
<div>{{ item.contact }}</div>
|
||||
<div v-if="item.contactType" class="flex gap-2">
|
||||
<a v-if="item.contactType.includes('whatsapp')"
|
||||
:href="`https://wa.me/${item.contact.replace(/\D/g, '')}`" target="_blank" rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1.5 rounded-full bg-green-100 px-3 py-1 text-sm font-medium text-green-800 hover:bg-green-200 transition-colors">
|
||||
<MessageCircle class="h-4 w-4" />
|
||||
WhatsApp
|
||||
</a>
|
||||
<a v-if="item.contactType.includes('telegram')"
|
||||
:href="`https://t.me/${item.contact.replace(/\D/g, '')}`" target="_blank" rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1.5 rounded-full bg-blue-100 px-3 py-1 text-sm font-medium text-blue-800 hover:bg-blue-200 transition-colors">
|
||||
<MessageCircle class="h-4 w-4" />
|
||||
Telegram
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lightning Address -->
|
||||
<div v-if="item.lightning" class="flex items-center gap-3 text-base">
|
||||
<Zap class="h-5 w-5 text-amber-500" />
|
||||
<a :href="`lightning:${item.lightning}`"
|
||||
class="text-muted-foreground hover:text-foreground transition-colors">
|
||||
{{ item.lightning }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="space-y-4">
|
||||
<!-- Website -->
|
||||
<div v-if="item.url" class="flex items-center gap-3 text-base">
|
||||
<ExternalLink class="h-5 w-5" />
|
||||
<a :href="item.url" target="_blank" rel="noopener noreferrer"
|
||||
class="text-muted-foreground hover:text-foreground transition-colors">
|
||||
{{ item.url.replace(/^https?:\/\//, '') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Social Media -->
|
||||
<div v-if="item.social" class="space-y-2">
|
||||
<div v-for="(url, platform) in item.social" :key="platform" class="flex items-center gap-3">
|
||||
<component :is="socials[platform as keyof typeof socials]" class="h-5 w-5"
|
||||
:class="socialColors[platform as keyof typeof socialColors]" />
|
||||
<a :href="url" target="_blank" rel="noopener noreferrer"
|
||||
class="text-muted-foreground hover:text-foreground transition-colors">
|
||||
{{ platform.charAt(0).toUpperCase() + platform.slice(1) }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<template>
|
||||
<svg
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<path fill="currentColor" d="M55.467,315.733C24.892,315.733,0,340.617,0,371.2c0,30.583,24.892,55.467,55.467,55.467s55.467-24.883,55.467-55.467
|
||||
C110.933,340.617,86.05,315.733,55.467,315.733z M55.467,409.6c-21.171,0-38.4-17.229-38.4-38.4s17.229-38.4,38.4-38.4
|
||||
s38.4,17.229,38.4,38.4S76.646,409.6,55.467,409.6z"/>
|
||||
<path fill="currentColor" d="M503.467,256h-8.533v-93.867c0-38.05-27.819-69.734-64.188-75.768c-1.212-0.657-2.603-1.033-4.079-1.033H153.6
|
||||
c-0.29,0-0.572,0.009-0.862,0.043c-54.443,5.513-88.055,98.987-99.388,136.491H42.667c-4.71,0-8.533,3.823-8.533,8.533v34.133
|
||||
c0,4.71,3.823,8.533,8.533,8.533h25.6v8.96c-2.807-0.282-5.649-0.427-8.533-0.427c-21.035,0-41.242,7.714-56.892,21.734
|
||||
c-3.516,3.14-3.806,8.533-0.666,12.049c3.149,3.516,8.533,3.814,12.049,0.666c12.518-11.204,28.689-17.382,45.508-17.382
|
||||
c37.641,0,68.267,30.626,68.267,68.267c0,4.71,3.823,8.533,8.533,8.533h222.549c4.164,28.902,29.022,51.2,59.051,51.2
|
||||
c30.029,0,54.886-22.298,59.051-51.2h9.216c4.71,0,8.533-3.823,8.533-8.533V307.2h8.533c4.71,0,8.533-3.823,8.533-8.533v-34.133
|
||||
C512,259.823,508.186,256,503.467,256z M154.061,102.4h264.073c32.939,0,59.733,26.795,59.733,59.733v85.333H358.4V128
|
||||
c0-4.71-3.823-8.533-8.533-8.533H147.652l-13.406-8.943C140.629,106.044,147.243,103.168,154.061,102.4z M341.333,136.533v115.934
|
||||
L309.7,284.1c-1.596,1.596-2.5,3.772-2.5,6.033v34.133h-59.733V136.533H341.333z M230.4,136.533v85.333H128.316l23.27-85.333
|
||||
H230.4z M121.122,122.291l14.097,9.404l-23.287,85.393l-33.434-16.717C89.762,169.694,104.346,140.706,121.122,122.291z
|
||||
M418.133,409.6c-20.599,0-37.837-14.686-41.805-34.133h83.61C455.979,394.914,438.741,409.6,418.133,409.6z M494.933,290.133
|
||||
H486.4c-4.71,0-8.533,3.823-8.533,8.533V358.4H144.64c-3.43-34.372-27.332-62.805-59.307-72.883v-20.983
|
||||
c0-4.71-3.823-8.533-8.533-8.533H51.2v-17.067h8.533c3.806,0,7.159-2.526,8.201-6.187c1.527-5.342,3.2-10.718,4.949-16.094
|
||||
l42.769,21.385c1.186,0.589,2.492,0.896,3.814,0.896H230.4V332.8c0,4.71,3.823,8.533,8.533,8.533h76.8
|
||||
c4.71,0,8.533-3.823,8.533-8.533v-39.134l29.141-29.133h124.459c0,4.71,3.823,8.533,8.533,8.533h8.533V290.133z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n'
|
|||
import { Menu, X, Sun, Moon, Zap, MessageSquareText, LogIn } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useTheme } from '@/components/theme-provider'
|
||||
import { useNostrStore } from '@/stores/nostr'
|
||||
import { useRouter } from 'vue-router'
|
||||
import LogoutDialog from '@/components/ui/logout-dialog/LogoutDialog.vue'
|
||||
import Login from '@/components/Login.vue'
|
||||
|
|
@ -13,15 +12,13 @@ import ConnectionStatus from '@/components/ConnectionStatus.vue'
|
|||
|
||||
const { t, locale } = useI18n()
|
||||
const { theme, setTheme } = useTheme()
|
||||
const nostrStore = useNostrStore()
|
||||
// const nostrStore = useNostrStore()
|
||||
const router = useRouter()
|
||||
const isOpen = ref(false)
|
||||
const showLoginDialog = ref(false)
|
||||
|
||||
const navigation = computed(() => [
|
||||
{ name: t('nav.home'), href: '/' },
|
||||
{ name: t('nav.directory'), href: '/directory' },
|
||||
{ name: t('nav.faq'), href: '/faq' },
|
||||
{ name: t('nav.support'), href: '/support', icon: MessageSquareText },
|
||||
])
|
||||
|
||||
|
|
@ -42,7 +39,7 @@ const toggleLocale = () => {
|
|||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
await nostrStore.logout()
|
||||
// await nostrStore.logout()
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
|
|
@ -82,18 +79,20 @@ const openLogin = () => {
|
|||
</Button>
|
||||
|
||||
<!-- Hide language toggle on mobile -->
|
||||
<Button variant="ghost"
|
||||
<Button variant="ghost"
|
||||
class="text-muted-foreground hover:text-foreground transition-colors hidden sm:inline-flex"
|
||||
@click="toggleLocale">
|
||||
{{ locale === 'en' ? '🇪🇸 ES' : '🇺🇸 EN' }}
|
||||
</Button>
|
||||
|
||||
<ConnectionStatus v-if="nostrStore.isLoggedIn" />
|
||||
<!-- <ConnectionStatus v-if="nostrStore.isLoggedIn" /> -->
|
||||
<ConnectionStatus v-if="true" />
|
||||
|
||||
<template v-if="nostrStore.isLoggedIn">
|
||||
<!-- <template v-if="nostrStore.isLoggedIn"> -->
|
||||
<template v-if="true">
|
||||
<LogoutDialog :onLogout="handleLogout" />
|
||||
</template>
|
||||
<Button v-else variant="ghost" size="icon" @click="openLogin"
|
||||
<Button v-else variant="ghost" size="icon" @click="openLogin"
|
||||
class="text-muted-foreground hover:text-foreground">
|
||||
<LogIn class="h-5 w-5" />
|
||||
</Button>
|
||||
|
|
@ -118,8 +117,7 @@ const openLogin = () => {
|
|||
<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"
|
||||
<Button variant="ghost" class="text-muted-foreground hover:text-foreground transition-colors"
|
||||
@click="toggleLocale">
|
||||
{{ locale === 'en' ? '🇪🇸 ES' : '🇺🇸 EN' }}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ type Theme = 'dark' | 'light' | 'system'
|
|||
const useTheme = () => {
|
||||
const theme = ref<Theme>('dark')
|
||||
const systemTheme = ref<'dark' | 'light'>('light')
|
||||
const currentTown = ref(localStorage.getItem('current-town') || 'all')
|
||||
|
||||
const updateSystemTheme = () => {
|
||||
systemTheme.value = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
|
|
@ -28,7 +27,7 @@ const useTheme = () => {
|
|||
if (stored && (stored === 'dark' || stored === 'light' || stored === 'system')) {
|
||||
theme.value = stored as Theme
|
||||
}
|
||||
|
||||
|
||||
updateSystemTheme()
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateSystemTheme)
|
||||
applyTheme()
|
||||
|
|
@ -43,19 +42,12 @@ const useTheme = () => {
|
|||
localStorage.setItem('ui-theme', newTheme)
|
||||
}
|
||||
|
||||
const setCurrentTown = (town: string) => {
|
||||
currentTown.value = town
|
||||
localStorage.setItem('current-town', town)
|
||||
}
|
||||
|
||||
return {
|
||||
theme,
|
||||
setTheme,
|
||||
systemTheme,
|
||||
currentTheme,
|
||||
currentTown,
|
||||
setCurrentTown
|
||||
}
|
||||
}
|
||||
|
||||
export { useTheme }
|
||||
export { useTheme }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue