From 95ee5a5dec797df6fc1a159e9bd445832516b744 Mon Sep 17 00:00:00 2001 From: Michilis Date: Thu, 12 Feb 2026 07:12:51 +0000 Subject: [PATCH 1/2] Improve llms.txt for AI: metadata, ISO dates, explicit status, structured events, update policy, AI summary; fix social links Co-authored-by: Cursor --- backend/src/db/schema.ts | 2 +- frontend/src/app/llms.txt/route.ts | 104 ++++++++++++++++++++++++----- 2 files changed, 90 insertions(+), 16 deletions(-) diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index f11f645..2443433 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -657,4 +657,4 @@ export type NewSiteSettings = typeof sqliteSiteSettings.$inferInsert; export type LegalPage = typeof sqliteLegalPages.$inferSelect; export type NewLegalPage = typeof sqliteLegalPages.$inferInsert; export type FaqQuestion = typeof sqliteFaqQuestions.$inferSelect; -export type NewFaqQuestion = typeof sqliteFaqQuestions.$inferInsert; +export type NewFaqQuestion = typeof sqliteFaqQuestions.$inferInsert; \ No newline at end of file diff --git a/frontend/src/app/llms.txt/route.ts b/frontend/src/app/llms.txt/route.ts index 4411cf7..35d43ff 100644 --- a/frontend/src/app/llms.txt/route.ts +++ b/frontend/src/app/llms.txt/route.ts @@ -79,6 +79,39 @@ function formatPrice(price: number, currency: string): string { return `${price.toLocaleString()} ${currency}`; } +function formatISODate(dateStr: string): string { + return new Date(dateStr).toLocaleDateString('en-CA', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + timeZone: EVENT_TIMEZONE, + }); +} + +function formatISOTime(dateStr: string): string { + return new Date(dateStr).toLocaleTimeString('en-GB', { + hour: '2-digit', + minute: '2-digit', + hour12: false, + timeZone: EVENT_TIMEZONE, + }); +} + +function getTodayISO(): string { + return new Date().toLocaleDateString('en-CA', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + timeZone: EVENT_TIMEZONE, + }); +} + +function getEventStatus(event: LlmsEvent): string { + if (event.availableSeats !== undefined && event.availableSeats === 0) return 'Sold Out'; + if (event.status === 'published') return 'Available'; + return event.status; +} + async function getHomepageFaqs(): Promise { try { const response = await fetch(`${apiUrl}/api/faq?homepage=true`, { @@ -107,6 +140,15 @@ export async function GET() { // Header lines.push('# Spanglish Community'); lines.push(''); + lines.push('## Metadata'); + lines.push(''); + lines.push('- Type: Event Community'); + lines.push('- Primary Language: English, Spanish'); + lines.push('- Location: Asunción, Paraguay'); + lines.push(`- Time Zone: ${EVENT_TIMEZONE}`); + lines.push(`- Last Updated: ${getTodayISO()}`); + lines.push(`- Canonical URL: ${siteUrl}`); + lines.push(''); lines.push('> English-Spanish language exchange community organizing social events and meetups in Asunción, Paraguay.'); lines.push(''); lines.push(`- Website: ${siteUrl}`); @@ -118,8 +160,8 @@ export async function GET() { const telegram = process.env.NEXT_PUBLIC_TELEGRAM; const email = process.env.NEXT_PUBLIC_EMAIL; - if (instagram) lines.push(`- Instagram: https://instagram.com/${instagram}`); - if (telegram) lines.push(`- Telegram: https://t.me/${telegram}`); + if (instagram) lines.push(`- Instagram: ${instagram}`); + if (telegram) lines.push(`- Telegram: ${telegram}`); if (email) lines.push(`- Email: ${email}`); if (whatsapp) lines.push(`- WhatsApp: ${whatsapp}`); @@ -130,18 +172,25 @@ export async function GET() { lines.push(''); if (nextEvent) { - lines.push(`- Event: ${nextEvent.title}`); - lines.push(`- Date: ${formatEventDate(nextEvent.startDatetime)}`); - lines.push(`- Time: ${formatEventTime(nextEvent.startDatetime)}`); + const status = getEventStatus(nextEvent); + lines.push(`- Event Name: ${nextEvent.title}`); + lines.push(`- Event ID: ${nextEvent.id}`); + lines.push(`- Status: ${status}`); + lines.push(`- Date: ${formatISODate(nextEvent.startDatetime)}`); + lines.push(`- Start Time: ${formatISOTime(nextEvent.startDatetime)}`); if (nextEvent.endDatetime) { - lines.push(`- End time: ${formatEventTime(nextEvent.endDatetime)}`); + lines.push(`- End Time: ${formatISOTime(nextEvent.endDatetime)}`); } - lines.push(`- Location: ${nextEvent.location}, Asunción, Paraguay`); - lines.push(`- Price: ${formatPrice(nextEvent.price, nextEvent.currency)}`); + lines.push(`- Time Zone: ${EVENT_TIMEZONE}`); + lines.push(`- Venue: ${nextEvent.location}`); + lines.push('- City: Asunción'); + lines.push('- Country: Paraguay'); + lines.push(`- Price: ${nextEvent.price === 0 ? 'Free' : nextEvent.price}`); + lines.push(`- Currency: ${nextEvent.currency}`); if (nextEvent.availableSeats !== undefined) { - lines.push(`- Available spots: ${nextEvent.availableSeats}`); + lines.push(`- Capacity Remaining: ${nextEvent.availableSeats}`); } - lines.push(`- Details and tickets: ${siteUrl}/events/${nextEvent.id}`); + lines.push(`- Tickets URL: ${siteUrl}/events/${nextEvent.id}`); if (nextEvent.shortDescription) { lines.push(`- Description: ${nextEvent.shortDescription}`); } @@ -156,12 +205,25 @@ export async function GET() { lines.push('## All Upcoming Events'); lines.push(''); for (const event of upcomingEvents) { + const status = getEventStatus(event); lines.push(`### ${event.title}`); - lines.push(`- Date: ${formatEventDate(event.startDatetime)}`); - lines.push(`- Time: ${formatEventTime(event.startDatetime)}`); - lines.push(`- Location: ${event.location}, Asunción, Paraguay`); - lines.push(`- Price: ${formatPrice(event.price, event.currency)}`); - lines.push(`- Details: ${siteUrl}/events/${event.id}`); + lines.push(`- Event ID: ${event.id}`); + lines.push(`- Status: ${status}`); + lines.push(`- Date: ${formatISODate(event.startDatetime)}`); + lines.push(`- Start Time: ${formatISOTime(event.startDatetime)}`); + if (event.endDatetime) { + lines.push(`- End Time: ${formatISOTime(event.endDatetime)}`); + } + lines.push(`- Time Zone: ${EVENT_TIMEZONE}`); + lines.push(`- Venue: ${event.location}`); + lines.push('- City: Asunción'); + lines.push('- Country: Paraguay'); + lines.push(`- Price: ${event.price === 0 ? 'Free' : event.price}`); + lines.push(`- Currency: ${event.currency}`); + if (event.availableSeats !== undefined) { + lines.push(`- Capacity Remaining: ${event.availableSeats}`); + } + lines.push(`- Tickets URL: ${siteUrl}/events/${event.id}`); lines.push(''); } } @@ -185,6 +247,18 @@ export async function GET() { lines.push(`- More FAQ: ${siteUrl}/faq`); lines.push(''); + // Update Policy + lines.push('## Update Policy'); + lines.push(''); + lines.push('Event information is updated whenever new events are published or ticket availability changes.'); + lines.push(''); + + // AI Summary + lines.push('## AI Summary'); + lines.push(''); + lines.push('Spanglish Community organizes English-Spanish language exchange events in Asunción, Paraguay. Events require registration via the website.'); + lines.push(''); + const content = lines.join('\n'); return new NextResponse(content, { -- 2.49.1 From 18254c566e7e8a3e6efc7f1531a838712a25992f Mon Sep 17 00:00:00 2001 From: Michilis Date: Thu, 12 Feb 2026 07:55:43 +0000 Subject: [PATCH 2/2] SEO: robots.txt, sitemap, Organization & Event schema; dashboard fmtTime fix; frontend updates Co-authored-by: Cursor --- .../src/app/(public)/book/[eventId]/page.tsx | 25 +--- .../app/(public)/booking/[ticketId]/page.tsx | 25 +--- .../booking/success/[ticketId]/page.tsx | 20 +--- .../(public)/components/NextEventSection.tsx | 21 +--- .../dashboard/components/PaymentsTab.tsx | 1 + .../dashboard/components/ProfileTab.tsx | 2 +- .../dashboard/components/SecurityTab.tsx | 1 + .../dashboard/components/TicketsTab.tsx | 1 + frontend/src/app/(public)/dashboard/page.tsx | 18 +-- .../events/[id]/EventDetailClient.tsx | 23 +--- .../src/app/(public)/events/[id]/page.tsx | 8 +- frontend/src/app/(public)/events/page.tsx | 20 +--- frontend/src/app/(public)/layout.tsx | 4 +- frontend/src/app/(public)/page.tsx | 1 + frontend/src/app/admin/bookings/page.tsx | 1 + frontend/src/app/admin/contacts/page.tsx | 1 + frontend/src/app/admin/emails/page.tsx | 5 +- frontend/src/app/admin/events/[id]/page.tsx | 7 +- frontend/src/app/admin/events/page.tsx | 1 + frontend/src/app/admin/gallery/page.tsx | 1 + frontend/src/app/admin/legal-pages/page.tsx | 1 + frontend/src/app/admin/page.tsx | 1 + frontend/src/app/admin/payments/page.tsx | 1 + frontend/src/app/admin/scanner/page.tsx | 5 +- frontend/src/app/admin/settings/page.tsx | 1 + frontend/src/app/admin/tickets/page.tsx | 1 + frontend/src/app/admin/users/page.tsx | 1 + frontend/src/app/linktree/page.tsx | 20 +--- frontend/src/app/robots.ts | 28 ++--- frontend/src/app/sitemap.ts | 70 ++++++++---- frontend/src/lib/utils.ts | 108 ++++++++++++++++++ 31 files changed, 227 insertions(+), 196 deletions(-) diff --git a/frontend/src/app/(public)/book/[eventId]/page.tsx b/frontend/src/app/(public)/book/[eventId]/page.tsx index 106bd7a..8675a3b 100644 --- a/frontend/src/app/(public)/book/[eventId]/page.tsx +++ b/frontend/src/app/(public)/book/[eventId]/page.tsx @@ -6,7 +6,7 @@ import Link from 'next/link'; import { useLanguage } from '@/context/LanguageContext'; import { useAuth } from '@/context/AuthContext'; import { eventsApi, ticketsApi, paymentOptionsApi, Event, PaymentOptionsConfig } from '@/lib/api'; -import { formatPrice } from '@/lib/utils'; +import { formatPrice, formatDateLong, formatTime } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import Input from '@/components/ui/Input'; @@ -217,21 +217,8 @@ export default function BookingPage() { } }, [user]); - const formatDate = (dateStr: string) => { - return new Date(dateStr).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - }); - }; - - const formatTime = (dateStr: string) => { - return new Date(dateStr).toLocaleTimeString(locale === 'es' ? 'es-ES' : 'en-US', { - hour: '2-digit', - minute: '2-digit', - }); - }; + const formatDate = (dateStr: string) => formatDateLong(dateStr, locale as 'en' | 'es'); + const fmtTime = (dateStr: string) => formatTime(dateStr, locale as 'en' | 'es'); const validateForm = (): boolean => { const newErrors: Partial> = {}; @@ -879,7 +866,7 @@ export default function BookingPage() {

{t('booking.success.event')}: {event?.title}

{t('booking.success.date')}: {event && formatDate(event.startDatetime)}

-

{t('booking.success.time')}: {event && formatTime(event.startDatetime)}

+

{t('booking.success.time')}: {event && fmtTime(event.startDatetime)}

{t('booking.success.location')}: {event?.location}

@@ -955,7 +942,7 @@ export default function BookingPage() {

{t('booking.success.event')}: {event.title}

{t('booking.success.date')}: {formatDate(event.startDatetime)}

-

{t('booking.success.time')}: {formatTime(event.startDatetime)}

+

{t('booking.success.time')}: {fmtTime(event.startDatetime)}

{t('booking.success.location')}: {event.location}

@@ -1045,7 +1032,7 @@ export default function BookingPage() {
- {formatDate(event.startDatetime)} • {formatTime(event.startDatetime)} + {formatDate(event.startDatetime)} • {fmtTime(event.startDatetime)}
diff --git a/frontend/src/app/(public)/booking/[ticketId]/page.tsx b/frontend/src/app/(public)/booking/[ticketId]/page.tsx index 3e5b291..2efe005 100644 --- a/frontend/src/app/(public)/booking/[ticketId]/page.tsx +++ b/frontend/src/app/(public)/booking/[ticketId]/page.tsx @@ -5,7 +5,7 @@ import { useParams, useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { useLanguage } from '@/context/LanguageContext'; import { ticketsApi, paymentOptionsApi, Ticket, PaymentOptionsConfig } from '@/lib/api'; -import { formatPrice } from '@/lib/utils'; +import { formatPrice, formatDateLong, formatTime } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import { @@ -152,21 +152,8 @@ export default function BookingPaymentPage() { } }; - const formatDate = (dateStr: string) => { - return new Date(dateStr).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - }); - }; - - const formatTime = (dateStr: string) => { - return new Date(dateStr).toLocaleTimeString(locale === 'es' ? 'es-ES' : 'en-US', { - hour: '2-digit', - minute: '2-digit', - }); - }; + const formatDate = (dateStr: string) => formatDateLong(dateStr, locale as 'en' | 'es'); + const fmtTime = (dateStr: string) => formatTime(dateStr, locale as 'en' | 'es'); // Loading state if (step === 'loading') { @@ -237,7 +224,7 @@ export default function BookingPaymentPage() {

{locale === 'es' ? 'Evento' : 'Event'}: {ticket.event.title}

{locale === 'es' ? 'Fecha' : 'Date'}: {formatDate(ticket.event.startDatetime)}

-

{locale === 'es' ? 'Hora' : 'Time'}: {formatTime(ticket.event.startDatetime)}

+

{locale === 'es' ? 'Hora' : 'Time'}: {fmtTime(ticket.event.startDatetime)}

{locale === 'es' ? 'Ubicación' : 'Location'}: {ticket.event.location}

)} @@ -286,7 +273,7 @@ export default function BookingPaymentPage() {

{locale === 'es' ? 'Evento' : 'Event'}: {ticket.event.title}

{locale === 'es' ? 'Fecha' : 'Date'}: {formatDate(ticket.event.startDatetime)}

-

{locale === 'es' ? 'Hora' : 'Time'}: {formatTime(ticket.event.startDatetime)}

+

{locale === 'es' ? 'Hora' : 'Time'}: {fmtTime(ticket.event.startDatetime)}

{locale === 'es' ? 'Ubicación' : 'Location'}: {ticket.event.location}

)} @@ -333,7 +320,7 @@ export default function BookingPaymentPage() {
- {formatDate(ticket.event.startDatetime)} - {formatTime(ticket.event.startDatetime)} + {formatDate(ticket.event.startDatetime)} - {fmtTime(ticket.event.startDatetime)}
diff --git a/frontend/src/app/(public)/booking/success/[ticketId]/page.tsx b/frontend/src/app/(public)/booking/success/[ticketId]/page.tsx index 43fd1ef..27ad450 100644 --- a/frontend/src/app/(public)/booking/success/[ticketId]/page.tsx +++ b/frontend/src/app/(public)/booking/success/[ticketId]/page.tsx @@ -5,6 +5,7 @@ import { useParams } from 'next/navigation'; import Link from 'next/link'; import { useLanguage } from '@/context/LanguageContext'; import { ticketsApi, Ticket } from '@/lib/api'; +import { formatDateLong, formatTime } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import { @@ -69,21 +70,8 @@ export default function BookingSuccessPage() { }; }, [ticketId]); - const formatDate = (dateStr: string) => { - return new Date(dateStr).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - }); - }; - - const formatTime = (dateStr: string) => { - return new Date(dateStr).toLocaleTimeString(locale === 'es' ? 'es-ES' : 'en-US', { - hour: '2-digit', - minute: '2-digit', - }); - }; + const formatDate = (dateStr: string) => formatDateLong(dateStr, locale as 'en' | 'es'); + const fmtTime = (dateStr: string) => formatTime(dateStr, locale as 'en' | 'es'); if (loading) { return ( @@ -191,7 +179,7 @@ export default function BookingSuccessPage() { <>

{locale === 'es' ? 'Evento' : 'Event'}: {ticket.event.title}

{locale === 'es' ? 'Fecha' : 'Date'}: {formatDate(ticket.event.startDatetime)}

-

{locale === 'es' ? 'Hora' : 'Time'}: {formatTime(ticket.event.startDatetime)}

+

{locale === 'es' ? 'Hora' : 'Time'}: {fmtTime(ticket.event.startDatetime)}

{locale === 'es' ? 'Ubicación' : 'Location'}: {ticket.event.location}

)} diff --git a/frontend/src/app/(public)/components/NextEventSection.tsx b/frontend/src/app/(public)/components/NextEventSection.tsx index 2fac674..2353af0 100644 --- a/frontend/src/app/(public)/components/NextEventSection.tsx +++ b/frontend/src/app/(public)/components/NextEventSection.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; import { useLanguage } from '@/context/LanguageContext'; import { eventsApi, Event } from '@/lib/api'; -import { formatPrice } from '@/lib/utils'; +import { formatPrice, formatDateLong, formatTime } from '@/lib/utils'; import Button from '@/components/ui/Button'; import Card from '@/components/ui/Card'; import { CalendarIcon, MapPinIcon } from '@heroicons/react/24/outline'; @@ -27,21 +27,8 @@ export default function NextEventSection({ initialEvent }: NextEventSectionProps .finally(() => setLoading(false)); }, [initialEvent]); - const formatDate = (dateStr: string) => { - return new Date(dateStr).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - }); - }; - - const formatTime = (dateStr: string) => { - return new Date(dateStr).toLocaleTimeString(locale === 'es' ? 'es-ES' : 'en-US', { - hour: '2-digit', - minute: '2-digit', - }); - }; + const formatDate = (dateStr: string) => formatDateLong(dateStr, locale as 'en' | 'es'); + const fmtTime = (dateStr: string) => formatTime(dateStr, locale as 'en' | 'es'); if (loading) { return ( @@ -84,7 +71,7 @@ export default function NextEventSection({ initialEvent }: NextEventSectionProps - {formatTime(nextEvent.startDatetime)} + {fmtTime(nextEvent.startDatetime)}
diff --git a/frontend/src/app/(public)/dashboard/components/PaymentsTab.tsx b/frontend/src/app/(public)/dashboard/components/PaymentsTab.tsx index 39d73fb..ea9fd21 100644 --- a/frontend/src/app/(public)/dashboard/components/PaymentsTab.tsx +++ b/frontend/src/app/(public)/dashboard/components/PaymentsTab.tsx @@ -25,6 +25,7 @@ export default function PaymentsTab({ payments, language }: PaymentsTabProps) { year: 'numeric', month: 'short', day: 'numeric', + timeZone: 'America/Asuncion', }); }; diff --git a/frontend/src/app/(public)/dashboard/components/ProfileTab.tsx b/frontend/src/app/(public)/dashboard/components/ProfileTab.tsx index c2828c9..bff2f5f 100644 --- a/frontend/src/app/(public)/dashboard/components/ProfileTab.tsx +++ b/frontend/src/app/(public)/dashboard/components/ProfileTab.tsx @@ -118,7 +118,7 @@ export default function ProfileTab({ onUpdate }: ProfileTabProps) { {profile?.memberSince ? new Date(profile.memberSince).toLocaleDateString( language === 'es' ? 'es-ES' : 'en-US', - { year: 'numeric', month: 'long', day: 'numeric' } + { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'America/Asuncion' } ) : '-'} diff --git a/frontend/src/app/(public)/dashboard/components/SecurityTab.tsx b/frontend/src/app/(public)/dashboard/components/SecurityTab.tsx index 13e0a32..fd43bbe 100644 --- a/frontend/src/app/(public)/dashboard/components/SecurityTab.tsx +++ b/frontend/src/app/(public)/dashboard/components/SecurityTab.tsx @@ -153,6 +153,7 @@ export default function SecurityTab() { day: 'numeric', hour: '2-digit', minute: '2-digit', + timeZone: 'America/Asuncion', }); }; diff --git a/frontend/src/app/(public)/dashboard/components/TicketsTab.tsx b/frontend/src/app/(public)/dashboard/components/TicketsTab.tsx index 647e505..69335f7 100644 --- a/frontend/src/app/(public)/dashboard/components/TicketsTab.tsx +++ b/frontend/src/app/(public)/dashboard/components/TicketsTab.tsx @@ -30,6 +30,7 @@ export default function TicketsTab({ tickets, language }: TicketsTabProps) { year: 'numeric', month: 'short', day: 'numeric', + timeZone: 'America/Asuncion', }); }; diff --git a/frontend/src/app/(public)/dashboard/page.tsx b/frontend/src/app/(public)/dashboard/page.tsx index 6eefb25..3a16e1f 100644 --- a/frontend/src/app/(public)/dashboard/page.tsx +++ b/frontend/src/app/(public)/dashboard/page.tsx @@ -7,6 +7,7 @@ import { useAuth } from '@/context/AuthContext'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import { dashboardApi, DashboardSummary, NextEventInfo, UserTicket, UserPayment } from '@/lib/api'; +import { formatDateLong, formatTime } from '@/lib/utils'; import toast from 'react-hot-toast'; import Link from 'next/link'; import { @@ -85,21 +86,8 @@ export default function DashboardPage() { ); } - const formatDate = (dateStr: string) => { - return new Date(dateStr).toLocaleDateString(language === 'es' ? 'es-ES' : 'en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - }); - }; - - const formatTime = (dateStr: string) => { - return new Date(dateStr).toLocaleTimeString(language === 'es' ? 'es-ES' : 'en-US', { - hour: '2-digit', - minute: '2-digit', - }); - }; + const formatDate = (dateStr: string) => formatDateLong(dateStr, language as 'en' | 'es'); + const fmtTime = (dateStr: string) => formatTime(dateStr, language as 'en' | 'es'); return (
diff --git a/frontend/src/app/(public)/events/[id]/EventDetailClient.tsx b/frontend/src/app/(public)/events/[id]/EventDetailClient.tsx index 94731bd..0b8a1c6 100644 --- a/frontend/src/app/(public)/events/[id]/EventDetailClient.tsx +++ b/frontend/src/app/(public)/events/[id]/EventDetailClient.tsx @@ -5,7 +5,7 @@ import Link from 'next/link'; import Image from 'next/image'; import { useLanguage } from '@/context/LanguageContext'; import { eventsApi, Event } from '@/lib/api'; -import { formatPrice } from '@/lib/utils'; +import { formatPrice, formatDateLong, formatTime } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import ShareButtons from '@/components/ShareButtons'; @@ -54,21 +54,8 @@ export default function EventDetailClient({ eventId, initialEvent }: EventDetail setTicketQuantity(prev => Math.min(maxTickets, prev + 1)); }; - const formatDate = (dateStr: string) => { - return new Date(dateStr).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - }); - }; - - const formatTime = (dateStr: string) => { - return new Date(dateStr).toLocaleTimeString(locale === 'es' ? 'es-ES' : 'en-US', { - hour: '2-digit', - minute: '2-digit', - }); - }; + const formatDate = (dateStr: string) => formatDateLong(dateStr, locale as 'en' | 'es'); + const fmtTime = (dateStr: string) => formatTime(dateStr, locale as 'en' | 'es'); const isCancelled = event.status === 'cancelled'; // Only calculate isPastEvent after mount to avoid hydration mismatch @@ -228,8 +215,8 @@ export default function EventDetailClient({ eventId, initialEvent }: EventDetail

{t('events.details.time')}

- {formatTime(event.startDatetime)} - {event.endDatetime && ` - ${formatTime(event.endDatetime)}`} + {fmtTime(event.startDatetime)} + {event.endDatetime && ` - ${fmtTime(event.endDatetime)}`}

diff --git a/frontend/src/app/(public)/events/[id]/page.tsx b/frontend/src/app/(public)/events/[id]/page.tsx index 05fb4df..6b6bc9b 100644 --- a/frontend/src/app/(public)/events/[id]/page.tsx +++ b/frontend/src/app/(public)/events/[id]/page.tsx @@ -95,11 +95,9 @@ function generateEventJsonLd(event: Event) { startDate: event.startDatetime, endDate: event.endDatetime || event.startDatetime, eventAttendanceMode: 'https://schema.org/OfflineEventAttendanceMode', - eventStatus: isCancelled - ? 'https://schema.org/EventCancelled' - : isPastEvent - ? 'https://schema.org/EventPostponed' - : 'https://schema.org/EventScheduled', + eventStatus: isCancelled + ? 'https://schema.org/EventCancelled' + : 'https://schema.org/EventScheduled', location: { '@type': 'Place', name: event.location, diff --git a/frontend/src/app/(public)/events/page.tsx b/frontend/src/app/(public)/events/page.tsx index 8985852..9ecbc7b 100644 --- a/frontend/src/app/(public)/events/page.tsx +++ b/frontend/src/app/(public)/events/page.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; import { useLanguage } from '@/context/LanguageContext'; import { eventsApi, Event } from '@/lib/api'; -import { formatPrice } from '@/lib/utils'; +import { formatPrice, formatDateShort, formatTime } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import { CalendarIcon, MapPinIcon, UserGroupIcon } from '@heroicons/react/24/outline'; @@ -33,20 +33,8 @@ export default function EventsPage() { const displayedEvents = filter === 'upcoming' ? upcomingEvents : pastEvents; - const formatDate = (dateStr: string) => { - return new Date(dateStr).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', { - weekday: 'short', - month: 'short', - day: 'numeric', - }); - }; - - const formatTime = (dateStr: string) => { - return new Date(dateStr).toLocaleTimeString(locale === 'es' ? 'es-ES' : 'en-US', { - hour: '2-digit', - minute: '2-digit', - }); - }; + const formatDate = (dateStr: string) => formatDateShort(dateStr, locale as 'en' | 'es'); + const fmtTime = (dateStr: string) => formatTime(dateStr, locale as 'en' | 'es'); const getStatusBadge = (event: Event) => { if (event.status === 'cancelled') { @@ -130,7 +118,7 @@ export default function EventsPage() {
- {formatDate(event.startDatetime)} - {formatTime(event.startDatetime)} + {formatDate(event.startDatetime)} - {fmtTime(event.startDatetime)}
diff --git a/frontend/src/app/(public)/layout.tsx b/frontend/src/app/(public)/layout.tsx index d7474a9..a3a7b23 100644 --- a/frontend/src/app/(public)/layout.tsx +++ b/frontend/src/app/(public)/layout.tsx @@ -20,7 +20,7 @@ export const metadata: Metadata = { const organizationSchema = { '@context': 'https://schema.org', '@type': 'Organization', - name: 'Spanglish', + name: 'Spanglish Community', url: siteUrl, logo: `${siteUrl}/images/logo.png`, description: 'Language exchange community organizing English and Spanish meetups in Asunción, Paraguay.', @@ -30,7 +30,7 @@ const organizationSchema = { addressCountry: 'PY', }, sameAs: [ - process.env.NEXT_PUBLIC_INSTAGRAM_URL, + 'https://instagram.com/spanglishsocialpy', process.env.NEXT_PUBLIC_WHATSAPP_URL, process.env.NEXT_PUBLIC_TELEGRAM_URL, ].filter(Boolean), diff --git a/frontend/src/app/(public)/page.tsx b/frontend/src/app/(public)/page.tsx index b08b89f..e6a7283 100644 --- a/frontend/src/app/(public)/page.tsx +++ b/frontend/src/app/(public)/page.tsx @@ -66,6 +66,7 @@ export async function generateMetadata(): Promise { year: 'numeric', month: 'long', day: 'numeric', + timeZone: 'America/Asuncion', }); const description = `Next event: ${eventDate} – ${event.title}. Practice English and Spanish at relaxed social events in Asunción. Meet locals and internationals.`; diff --git a/frontend/src/app/admin/bookings/page.tsx b/frontend/src/app/admin/bookings/page.tsx index b346dc6..97a5b22 100644 --- a/frontend/src/app/admin/bookings/page.tsx +++ b/frontend/src/app/admin/bookings/page.tsx @@ -125,6 +125,7 @@ export default function AdminBookingsPage() { year: 'numeric', hour: '2-digit', minute: '2-digit', + timeZone: 'America/Asuncion', }); }; diff --git a/frontend/src/app/admin/contacts/page.tsx b/frontend/src/app/admin/contacts/page.tsx index 6043853..8953ed9 100644 --- a/frontend/src/app/admin/contacts/page.tsx +++ b/frontend/src/app/admin/contacts/page.tsx @@ -49,6 +49,7 @@ export default function AdminContactsPage() { day: 'numeric', hour: '2-digit', minute: '2-digit', + timeZone: 'America/Asuncion', }); }; diff --git a/frontend/src/app/admin/emails/page.tsx b/frontend/src/app/admin/emails/page.tsx index 6d57f5b..7535707 100644 --- a/frontend/src/app/admin/emails/page.tsx +++ b/frontend/src/app/admin/emails/page.tsx @@ -373,6 +373,7 @@ export default function AdminEmailsPage() { year: 'numeric', hour: '2-digit', minute: '2-digit', + timeZone: 'America/Asuncion', }); }; @@ -545,7 +546,7 @@ export default function AdminEmailsPage() {
{hasDraft && ( - Draft saved {composeForm.savedAt ? new Date(composeForm.savedAt).toLocaleString() : ''} + Draft saved {composeForm.savedAt ? new Date(composeForm.savedAt).toLocaleString(locale === 'es' ? 'es-ES' : 'en-US', { timeZone: 'America/Asuncion' }) : ''} )}