Internationalize all UI strings and localize date formatting
All hardcoded English strings now use t() with i18n keys: bottom nav tabs, search placeholder, empty states, favorites page, settings page. Added useDateLocale composable that maps the current i18n language to the corresponding date-fns locale (fr/es/enUS). All date formatting across ActivityCard, ActivityDetailPage, DatePickerStrip, calendar view, and search overlay now passes the locale to date-fns format(). Month names, day names, and date labels change with language switch. Added nav, search, favorites, and settings i18n sections to all three locales (EN/FR/ES). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f782cd1a7a
commit
93c2a73e21
14 changed files with 209 additions and 42 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Toaster } from '@/components/ui/sonner'
|
import { Toaster } from '@/components/ui/sonner'
|
||||||
import LoginDialog from '@/components/auth/LoginDialog.vue'
|
import LoginDialog from '@/components/auth/LoginDialog.vue'
|
||||||
import { useTheme } from '@/components/theme-provider'
|
import { useTheme } from '@/components/theme-provider'
|
||||||
|
|
@ -11,19 +12,20 @@ import {
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
useTheme()
|
useTheme()
|
||||||
|
|
||||||
const showLoginDialog = ref(false)
|
const showLoginDialog = ref(false)
|
||||||
|
|
||||||
// Bottom navigation tabs (p'a semana style)
|
// Bottom navigation tabs (p'a semana style)
|
||||||
const bottomTabs = [
|
const bottomTabs = computed(() => [
|
||||||
{ name: 'Feed', icon: Search, path: '/activities' },
|
{ name: t('activities.nav.feed'), icon: Search, path: '/activities' },
|
||||||
{ name: 'Calendar', icon: CalendarDays, path: '/activities/calendar' },
|
{ name: t('activities.nav.calendar'), icon: CalendarDays, path: '/activities/calendar' },
|
||||||
{ name: 'Map', icon: Map, path: '/activities/map' },
|
{ name: t('activities.nav.map'), icon: Map, path: '/activities/map' },
|
||||||
{ name: 'Favorites', icon: Heart, path: '/activities/favorites' },
|
{ name: t('activities.nav.favorites'), icon: Heart, path: '/activities/favorites' },
|
||||||
{ name: 'Settings', icon: Settings, path: '/settings' },
|
{ name: t('activities.nav.settings'), icon: Settings, path: '/settings' },
|
||||||
]
|
])
|
||||||
|
|
||||||
const isLoginPage = computed(() => route.path === '/login')
|
const isLoginPage = computed(() => route.path === '/login')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { useI18n } from 'vue-i18n'
|
||||||
import { Sun, Moon, LogIn, LogOut } from 'lucide-vue-next'
|
import { Sun, Moon, LogIn, LogOut } from 'lucide-vue-next'
|
||||||
|
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
const { locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
|
|
||||||
const languages: { code: AvailableLocale; label: string }[] = [
|
const languages: { code: AvailableLocale; label: string }[] = [
|
||||||
{ code: 'fr', label: 'Français' },
|
{ code: 'fr', label: 'Français' },
|
||||||
|
|
@ -35,27 +35,27 @@ async function handleLogout() {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto px-4 py-6 max-w-lg">
|
<div class="container mx-auto px-4 py-6 max-w-lg">
|
||||||
<h1 class="text-2xl font-bold text-foreground mb-6">Settings</h1>
|
<h1 class="text-2xl font-bold text-foreground mb-6">{{ t('activities.settings.title') }}</h1>
|
||||||
|
|
||||||
<!-- Account -->
|
<!-- Account -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide">Account</h2>
|
<h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide">{{ t('activities.settings.account') }}</h2>
|
||||||
<div v-if="isAuthenticated" class="bg-muted/50 rounded-lg p-4 space-y-3">
|
<div v-if="isAuthenticated" class="bg-muted/50 rounded-lg p-4 space-y-3">
|
||||||
<p class="text-sm text-foreground font-mono truncate">
|
<p class="text-sm text-foreground font-mono truncate">
|
||||||
{{ userPubkey }}
|
{{ userPubkey }}
|
||||||
</p>
|
</p>
|
||||||
<Button variant="outline" size="sm" class="w-full gap-2" @click="handleLogout">
|
<Button variant="outline" size="sm" class="w-full gap-2" @click="handleLogout">
|
||||||
<LogOut class="w-4 h-4" />
|
<LogOut class="w-4 h-4" />
|
||||||
Log out
|
{{ t('activities.settings.logOut') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="bg-muted/50 rounded-lg p-4">
|
<div v-else class="bg-muted/50 rounded-lg p-4">
|
||||||
<p class="text-sm text-muted-foreground mb-3">
|
<p class="text-sm text-muted-foreground mb-3">
|
||||||
Log in to bookmark activities, RSVP, and purchase tickets.
|
{{ t('activities.settings.loginPrompt') }}
|
||||||
</p>
|
</p>
|
||||||
<Button size="sm" class="w-full gap-2" @click="$router.push('/login')">
|
<Button size="sm" class="w-full gap-2" @click="$router.push('/login')">
|
||||||
<LogIn class="w-4 h-4" />
|
<LogIn class="w-4 h-4" />
|
||||||
Log in
|
{{ t('activities.settings.logIn') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -64,9 +64,9 @@ async function handleLogout() {
|
||||||
|
|
||||||
<!-- Appearance -->
|
<!-- Appearance -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide">Appearance</h2>
|
<h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide">{{ t('activities.settings.appearance') }}</h2>
|
||||||
<div class="flex items-center justify-between bg-muted/50 rounded-lg p-4">
|
<div class="flex items-center justify-between bg-muted/50 rounded-lg p-4">
|
||||||
<span class="text-sm text-foreground">Theme</span>
|
<span class="text-sm text-foreground">{{ t('activities.settings.theme') }}</span>
|
||||||
<Button variant="outline" size="icon" class="h-8 w-8" @click="toggleTheme">
|
<Button variant="outline" size="icon" class="h-8 w-8" @click="toggleTheme">
|
||||||
<Sun v-if="theme === 'dark'" class="w-4 h-4" />
|
<Sun v-if="theme === 'dark'" class="w-4 h-4" />
|
||||||
<Moon v-else class="w-4 h-4" />
|
<Moon v-else class="w-4 h-4" />
|
||||||
|
|
@ -78,7 +78,7 @@ async function handleLogout() {
|
||||||
|
|
||||||
<!-- Language -->
|
<!-- Language -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide">Language</h2>
|
<h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide">{{ t('activities.settings.language') }}</h2>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
v-for="lang in languages"
|
v-for="lang in languages"
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,34 @@ const messages: LocaleMessages = {
|
||||||
pending: 'Pending',
|
pending: 'Pending',
|
||||||
registered: 'Registered',
|
registered: 'Registered',
|
||||||
},
|
},
|
||||||
|
nav: {
|
||||||
|
feed: 'Feed',
|
||||||
|
calendar: 'Calendar',
|
||||||
|
map: 'Map',
|
||||||
|
favorites: 'Favorites',
|
||||||
|
settings: 'Settings',
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
placeholder: 'Search activities...',
|
||||||
|
noResults: 'No activities found',
|
||||||
|
},
|
||||||
|
favorites: {
|
||||||
|
title: 'Favorites',
|
||||||
|
loginPrompt: 'Log in to save your favorite activities',
|
||||||
|
empty: 'No favorites yet',
|
||||||
|
emptyHint: 'Tap the heart icon on any activity to save it here',
|
||||||
|
logIn: 'Log in',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: 'Settings',
|
||||||
|
account: 'Account',
|
||||||
|
loginPrompt: 'Log in to bookmark activities, RSVP, and purchase tickets.',
|
||||||
|
logIn: 'Log in',
|
||||||
|
logOut: 'Log out',
|
||||||
|
appearance: 'Appearance',
|
||||||
|
theme: 'Theme',
|
||||||
|
language: 'Language',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
dateTimeFormats: {
|
dateTimeFormats: {
|
||||||
short: {
|
short: {
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,34 @@ const messages: LocaleMessages = {
|
||||||
pending: 'Pendiente',
|
pending: 'Pendiente',
|
||||||
registered: 'Registrado',
|
registered: 'Registrado',
|
||||||
},
|
},
|
||||||
|
nav: {
|
||||||
|
feed: 'Inicio',
|
||||||
|
calendar: 'Calendario',
|
||||||
|
map: 'Mapa',
|
||||||
|
favorites: 'Favoritos',
|
||||||
|
settings: 'Ajustes',
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
placeholder: 'Buscar actividades...',
|
||||||
|
noResults: 'No se encontraron actividades',
|
||||||
|
},
|
||||||
|
favorites: {
|
||||||
|
title: 'Favoritos',
|
||||||
|
loginPrompt: 'Inicia sesión para guardar tus actividades favoritas',
|
||||||
|
empty: 'Aún no tienes favoritos',
|
||||||
|
emptyHint: 'Toca el corazón en cualquier actividad para guardarla aquí',
|
||||||
|
logIn: 'Iniciar sesión',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: 'Ajustes',
|
||||||
|
account: 'Cuenta',
|
||||||
|
loginPrompt: 'Inicia sesión para guardar actividades, confirmar asistencia y comprar boletos.',
|
||||||
|
logIn: 'Iniciar sesión',
|
||||||
|
logOut: 'Cerrar sesión',
|
||||||
|
appearance: 'Apariencia',
|
||||||
|
theme: 'Tema',
|
||||||
|
language: 'Idioma',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
dateTimeFormats: {
|
dateTimeFormats: {
|
||||||
short: {
|
short: {
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,34 @@ const messages: LocaleMessages = {
|
||||||
pending: 'En attente',
|
pending: 'En attente',
|
||||||
registered: 'Enregistré',
|
registered: 'Enregistré',
|
||||||
},
|
},
|
||||||
|
nav: {
|
||||||
|
feed: 'Fil',
|
||||||
|
calendar: 'Calendrier',
|
||||||
|
map: 'Carte',
|
||||||
|
favorites: 'Favoris',
|
||||||
|
settings: 'Réglages',
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
placeholder: 'Rechercher des activités...',
|
||||||
|
noResults: 'Aucune activité trouvée',
|
||||||
|
},
|
||||||
|
favorites: {
|
||||||
|
title: 'Favoris',
|
||||||
|
loginPrompt: 'Connectez-vous pour sauvegarder vos activités préférées',
|
||||||
|
empty: 'Pas encore de favoris',
|
||||||
|
emptyHint: "Appuyez sur le cœur d'une activité pour la sauvegarder ici",
|
||||||
|
logIn: 'Se connecter',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: 'Réglages',
|
||||||
|
account: 'Compte',
|
||||||
|
loginPrompt: 'Connectez-vous pour sauvegarder des activités, confirmer votre présence et acheter des billets.',
|
||||||
|
logIn: 'Se connecter',
|
||||||
|
logOut: 'Se déconnecter',
|
||||||
|
appearance: 'Apparence',
|
||||||
|
theme: 'Thème',
|
||||||
|
language: 'Langue',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
dateTimeFormats: {
|
dateTimeFormats: {
|
||||||
short: {
|
short: {
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,34 @@ export interface LocaleMessages {
|
||||||
pending: string
|
pending: string
|
||||||
registered: string
|
registered: string
|
||||||
}
|
}
|
||||||
|
nav: {
|
||||||
|
feed: string
|
||||||
|
calendar: string
|
||||||
|
map: string
|
||||||
|
favorites: string
|
||||||
|
settings: string
|
||||||
|
}
|
||||||
|
search: {
|
||||||
|
placeholder: string
|
||||||
|
noResults: string
|
||||||
|
}
|
||||||
|
favorites: {
|
||||||
|
title: string
|
||||||
|
loginPrompt: string
|
||||||
|
empty: string
|
||||||
|
emptyHint: string
|
||||||
|
logIn: string
|
||||||
|
}
|
||||||
|
settings: {
|
||||||
|
title: string
|
||||||
|
account: string
|
||||||
|
loginPrompt: string
|
||||||
|
logIn: string
|
||||||
|
logOut: string
|
||||||
|
appearance: string
|
||||||
|
theme: string
|
||||||
|
language: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Add date/time formats
|
// Add date/time formats
|
||||||
dateTimeFormats: {
|
dateTimeFormats: {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
} from 'date-fns'
|
} from 'date-fns'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
||||||
|
import { useDateLocale } from '../composables/useDateLocale'
|
||||||
import type { Activity } from '../types/activity'
|
import type { Activity } from '../types/activity'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -18,11 +19,24 @@ const emit = defineEmits<{
|
||||||
selectActivity: [activity: Activity]
|
selectActivity: [activity: Activity]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const { dateLocale } = useDateLocale()
|
||||||
|
|
||||||
const currentMonth = ref(new Date())
|
const currentMonth = ref(new Date())
|
||||||
|
|
||||||
const monthLabel = computed(() => format(currentMonth.value, 'MMMM yyyy'))
|
const monthLabel = computed(() => format(currentMonth.value, 'MMMM yyyy', { locale: dateLocale.value }))
|
||||||
|
|
||||||
const weekDays = ['M', 'T', 'W', 'T', 'F', 'S', 'S']
|
const weekDays = computed(() => {
|
||||||
|
// Generate localized single-letter day names starting from Monday
|
||||||
|
const days: string[] = []
|
||||||
|
// Start from a known Monday (2024-01-01 is a Monday)
|
||||||
|
const monday = new Date(2024, 0, 1)
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const d = new Date(monday)
|
||||||
|
d.setDate(d.getDate() + i)
|
||||||
|
days.push(format(d, 'EEEEE', { locale: dateLocale.value }))
|
||||||
|
}
|
||||||
|
return days
|
||||||
|
})
|
||||||
|
|
||||||
const calendarDays = computed(() => {
|
const calendarDays = computed(() => {
|
||||||
const monthStart = startOfMonth(currentMonth.value)
|
const monthStart = startOfMonth(currentMonth.value)
|
||||||
|
|
@ -134,7 +148,7 @@ function nextMonth() {
|
||||||
<!-- Selected day activities -->
|
<!-- Selected day activities -->
|
||||||
<div v-if="selectedDay" class="border-t pt-4 space-y-2">
|
<div v-if="selectedDay" class="border-t pt-4 space-y-2">
|
||||||
<h3 class="text-sm font-medium text-muted-foreground">
|
<h3 class="text-sm font-medium text-muted-foreground">
|
||||||
{{ format(selectedDay, 'EEEE, MMMM d') }}
|
{{ format(selectedDay, 'EEEE, MMMM d', { locale: dateLocale }) }}
|
||||||
<span v-if="selectedDayActivities.length > 0" class="ml-1">
|
<span v-if="selectedDayActivities.length > 0" class="ml-1">
|
||||||
({{ selectedDayActivities.length }})
|
({{ selectedDayActivities.length }})
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { Card, CardContent } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { MapPin, Calendar, Ticket } from 'lucide-vue-next'
|
import { MapPin, Calendar, Ticket } from 'lucide-vue-next'
|
||||||
import BookmarkButton from './BookmarkButton.vue'
|
import BookmarkButton from './BookmarkButton.vue'
|
||||||
|
import { useDateLocale } from '../composables/useDateLocale'
|
||||||
import type { Activity } from '../types/activity'
|
import type { Activity } from '../types/activity'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -17,16 +18,18 @@ const emit = defineEmits<{
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { dateLocale } = useDateLocale()
|
||||||
|
|
||||||
const dateDisplay = computed(() => {
|
const dateDisplay = computed(() => {
|
||||||
const a = props.activity
|
const a = props.activity
|
||||||
if (!a.startDate || isNaN(a.startDate.getTime())) return ''
|
if (!a.startDate || isNaN(a.startDate.getTime())) return ''
|
||||||
try {
|
try {
|
||||||
|
const opts = { locale: dateLocale.value }
|
||||||
if (a.type === 'date') {
|
if (a.type === 'date') {
|
||||||
return format(a.startDate, 'EEE, MMM d')
|
return format(a.startDate, 'EEE, MMM d', opts)
|
||||||
}
|
}
|
||||||
const date = format(a.startDate, 'EEE, MMM d')
|
const date = format(a.startDate, 'EEE, MMM d', opts)
|
||||||
const time = format(a.startDate, 'HH:mm')
|
const time = format(a.startDate, 'HH:mm', opts)
|
||||||
return `${date} \u2022 ${time}`
|
return `${date} \u2022 ${time}`
|
||||||
} catch {
|
} catch {
|
||||||
return ''
|
return ''
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ const { t } = useI18n()
|
||||||
{{ t('activities.noActivities') }}
|
{{ t('activities.noActivities') }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
Try adjusting your filters or check back later
|
{{ t('activities.search.noResults') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
|
import { useDateLocale } from '../composables/useDateLocale'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Search, X, MapPin, Calendar } from 'lucide-vue-next'
|
import { Search, X, MapPin, Calendar } from 'lucide-vue-next'
|
||||||
|
|
@ -15,6 +17,8 @@ const emit = defineEmits<{
|
||||||
select: [activity: Activity]
|
select: [activity: Activity]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { dateLocale } = useDateLocale()
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
const inputRef = ref<HTMLInputElement | null>(null)
|
const inputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
|
|
@ -51,8 +55,9 @@ const showNoResults = computed(() => isOpen.value && isSearching.value && filter
|
||||||
function formatDate(activity: Activity): string {
|
function formatDate(activity: Activity): string {
|
||||||
if (!activity.startDate || isNaN(activity.startDate.getTime())) return ''
|
if (!activity.startDate || isNaN(activity.startDate.getTime())) return ''
|
||||||
try {
|
try {
|
||||||
if (activity.type === 'date') return format(activity.startDate, 'MMM d')
|
const opts = { locale: dateLocale.value }
|
||||||
return format(activity.startDate, 'MMM d · HH:mm')
|
if (activity.type === 'date') return format(activity.startDate, 'MMM d', opts)
|
||||||
|
return format(activity.startDate, 'MMM d · HH:mm', opts)
|
||||||
} catch {
|
} catch {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +110,7 @@ watch(isOpen, (open) => {
|
||||||
:model-value="searchQuery"
|
:model-value="searchQuery"
|
||||||
@update:model-value="handleInput"
|
@update:model-value="handleInput"
|
||||||
@focus="handleFocus"
|
@focus="handleFocus"
|
||||||
placeholder="Search activities..."
|
:placeholder="t('activities.search.placeholder')"
|
||||||
class="pl-9 pr-9"
|
class="pl-9 pr-9"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -126,7 +131,7 @@ watch(isOpen, (open) => {
|
||||||
>
|
>
|
||||||
<!-- No results -->
|
<!-- No results -->
|
||||||
<div v-if="showNoResults" class="p-4 text-sm text-muted-foreground text-center">
|
<div v-if="showNoResults" class="p-4 text-sm text-muted-foreground text-center">
|
||||||
No activities found
|
{{ t('activities.search.noResults') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Result items -->
|
<!-- Result items -->
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { ref, computed } from 'vue'
|
||||||
import { addDays, format, isSameDay, startOfWeek } from 'date-fns'
|
import { addDays, format, isSameDay, startOfWeek } from 'date-fns'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
||||||
|
import { useDateLocale } from '../composables/useDateLocale'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** Currently selected date (if any) */
|
/** Currently selected date (if any) */
|
||||||
|
|
@ -13,6 +14,8 @@ const emit = defineEmits<{
|
||||||
select: [date: Date]
|
select: [date: Date]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const { dateLocale } = useDateLocale()
|
||||||
|
|
||||||
/** Start of the visible week */
|
/** Start of the visible week */
|
||||||
const weekStart = ref(startOfWeek(new Date(), { weekStartsOn: 1 }))
|
const weekStart = ref(startOfWeek(new Date(), { weekStartsOn: 1 }))
|
||||||
|
|
||||||
|
|
@ -55,7 +58,7 @@ function nextWeek() {
|
||||||
<span class="text-[10px] font-medium uppercase leading-none"
|
<span class="text-[10px] font-medium uppercase leading-none"
|
||||||
:class="isSelected(day) ? 'text-primary-foreground/70' : 'text-muted-foreground'"
|
:class="isSelected(day) ? 'text-primary-foreground/70' : 'text-muted-foreground'"
|
||||||
>
|
>
|
||||||
{{ format(day, 'EEEEE') }}
|
{{ format(day, 'EEEEE', { locale: dateLocale }) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm font-semibold leading-tight mt-0.5">
|
<span class="text-sm font-semibold leading-tight mt-0.5">
|
||||||
{{ format(day, 'd') }}
|
{{ format(day, 'd') }}
|
||||||
|
|
|
||||||
23
src/modules/activities/composables/useDateLocale.ts
Normal file
23
src/modules/activities/composables/useDateLocale.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { fr, es, enUS } from 'date-fns/locale'
|
||||||
|
import type { Locale } from 'date-fns'
|
||||||
|
|
||||||
|
const localeMap: Record<string, Locale> = {
|
||||||
|
fr,
|
||||||
|
es,
|
||||||
|
en: enUS,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a reactive date-fns Locale based on the current i18n language.
|
||||||
|
*/
|
||||||
|
export function useDateLocale() {
|
||||||
|
const { locale } = useI18n()
|
||||||
|
|
||||||
|
const dateLocale = computed<Locale>(() => {
|
||||||
|
return localeMap[locale.value] ?? enUS
|
||||||
|
})
|
||||||
|
|
||||||
|
return { dateLocale }
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Heart } from 'lucide-vue-next'
|
import { Heart } from 'lucide-vue-next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
|
|
@ -11,6 +12,7 @@ import ActivityList from '../components/ActivityList.vue'
|
||||||
import type { Activity } from '../types/activity'
|
import type { Activity } from '../types/activity'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { t } = useI18n()
|
||||||
const { isAuthenticated } = useAuth()
|
const { isAuthenticated } = useAuth()
|
||||||
const { isBookmarkedByDTag, isLoaded } = useBookmarks()
|
const { isBookmarkedByDTag, isLoaded } = useBookmarks()
|
||||||
const store = useActivitiesStore()
|
const store = useActivitiesStore()
|
||||||
|
|
@ -25,9 +27,9 @@ function handleSelect(activity: Activity) {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!isAuthenticated.value) {
|
if (!isAuthenticated.value) {
|
||||||
toast.info('Log in to save favorites', {
|
toast.info(t('activities.favorites.loginPrompt'), {
|
||||||
action: {
|
action: {
|
||||||
label: 'Log in',
|
label: t('activities.favorites.logIn'),
|
||||||
onClick: () => router.push('/login'),
|
onClick: () => router.push('/login'),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -37,14 +39,14 @@ onMounted(() => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto px-4 py-6">
|
<div class="container mx-auto px-4 py-6">
|
||||||
<h1 class="text-2xl font-bold text-foreground mb-4">Favorites</h1>
|
<h1 class="text-2xl font-bold text-foreground mb-4">{{ t('activities.favorites.title') }}</h1>
|
||||||
|
|
||||||
<!-- Not authenticated -->
|
<!-- Not authenticated -->
|
||||||
<div v-if="!isAuthenticated" class="flex flex-col items-center justify-center py-16 text-center">
|
<div v-if="!isAuthenticated" class="flex flex-col items-center justify-center py-16 text-center">
|
||||||
<Heart class="w-16 h-16 text-muted-foreground/30 mb-4" />
|
<Heart class="w-16 h-16 text-muted-foreground opacity-30 mb-4" />
|
||||||
<p class="text-muted-foreground mb-3">Log in to save your favorite activities</p>
|
<p class="text-muted-foreground mb-3">{{ t('activities.favorites.loginPrompt') }}</p>
|
||||||
<Button variant="outline" size="sm" @click="router.push('/login')">
|
<Button variant="outline" size="sm" @click="router.push('/login')">
|
||||||
Log in
|
{{ t('activities.favorites.logIn') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -55,9 +57,9 @@ onMounted(() => {
|
||||||
|
|
||||||
<!-- Empty -->
|
<!-- Empty -->
|
||||||
<div v-else-if="favoriteActivities.length === 0" class="flex flex-col items-center justify-center py-16 text-center">
|
<div v-else-if="favoriteActivities.length === 0" class="flex flex-col items-center justify-center py-16 text-center">
|
||||||
<Heart class="w-16 h-16 text-muted-foreground/30 mb-4" />
|
<Heart class="w-16 h-16 text-muted-foreground opacity-30 mb-4" />
|
||||||
<p class="text-muted-foreground">No favorites yet</p>
|
<p class="text-muted-foreground">{{ t('activities.favorites.empty') }}</p>
|
||||||
<p class="text-sm text-muted-foreground/70 mt-1">Tap the heart icon on any activity to save it here</p>
|
<p class="text-sm text-muted-foreground/70 mt-1">{{ t('activities.favorites.emptyHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Favorites list -->
|
<!-- Favorites list -->
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
|
import { useDateLocale } from '../composables/useDateLocale'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
|
@ -20,20 +21,22 @@ const { t } = useI18n()
|
||||||
|
|
||||||
const activityId = route.params.id as string
|
const activityId = route.params.id as string
|
||||||
const { activity, isLoading, error, reload } = useActivityDetail(activityId)
|
const { activity, isLoading, error, reload } = useActivityDetail(activityId)
|
||||||
|
const { dateLocale } = useDateLocale()
|
||||||
|
|
||||||
const dateDisplay = computed(() => {
|
const dateDisplay = computed(() => {
|
||||||
if (!activity.value) return ''
|
if (!activity.value) return ''
|
||||||
const a = activity.value
|
const a = activity.value
|
||||||
|
const opts = { locale: dateLocale.value }
|
||||||
if (a.type === 'date') {
|
if (a.type === 'date') {
|
||||||
const start = format(a.startDate, 'EEEE, MMMM d, yyyy')
|
const start = format(a.startDate, 'EEEE, MMMM d, yyyy', opts)
|
||||||
if (a.endDate) {
|
if (a.endDate) {
|
||||||
return `${start} — ${format(a.endDate, 'EEEE, MMMM d, yyyy')}`
|
return `${start} — ${format(a.endDate, 'EEEE, MMMM d, yyyy', opts)}`
|
||||||
}
|
}
|
||||||
return start
|
return start
|
||||||
}
|
}
|
||||||
const start = format(a.startDate, 'EEEE, MMMM d, yyyy \u2022 HH:mm')
|
const start = format(a.startDate, 'EEEE, MMMM d, yyyy \u2022 HH:mm', opts)
|
||||||
if (a.endDate) {
|
if (a.endDate) {
|
||||||
return `${start} — ${format(a.endDate, 'HH:mm')}`
|
return `${start} — ${format(a.endDate, 'HH:mm', opts)}`
|
||||||
}
|
}
|
||||||
return start
|
return start
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue