feat: Add translation support

This commit is contained in:
Levente Orban
2025-09-16 11:05:59 +02:00
parent 8d01000ed4
commit f66fd03d70
12 changed files with 517 additions and 187 deletions

View File

@@ -6,6 +6,7 @@
addToOutlookCalendar,
downloadICalFile
} from '../calendarHelpers.js';
import { t } from '$lib/i18n/i18n.js';
export let isOpen: boolean = false;
export let event: CalendarEvent;
@@ -66,11 +67,13 @@
>
<div class="mx-4 w-full max-w-md rounded-sm border border-white/20 bg-slate-900 p-6 shadow-2xl">
<div class="mb-6 flex items-center justify-between">
<h3 id="calendar-modal-title" class="text-xl font-bold text-white">Add to Calendar</h3>
<h3 id="calendar-modal-title" class="text-xl font-bold text-white">
{t('calendar.addToCalendarTitle')}
</h3>
<button
on:click={closeModal}
class="text-slate-400 transition-colors duration-200 hover:text-white"
aria-label="Close modal"
aria-label={t('common.closeModal')}
>
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@@ -107,8 +110,8 @@
</svg>
</div>
<div>
<p class="font-semibold text-white">Google Calendar</p>
<p class="text-sm text-slate-400">Add to Google Calendar</p>
<p class="font-semibold text-white">{t('calendar.googleCalendarTitle')}</p>
<p class="text-sm text-slate-400">{t('calendar.googleCalendarDescription')}</p>
</div>
</div>
</button>
@@ -129,8 +132,8 @@
</svg>
</div>
<div>
<p class="font-semibold text-white">Microsoft Outlook</p>
<p class="text-sm text-slate-400">Add to Outlook Calendar</p>
<p class="font-semibold text-white">{t('calendar.microsoftOutlookTitle')}</p>
<p class="text-sm text-slate-400">{t('calendar.microsoftOutlookDescription')}</p>
</div>
</div>
</button>
@@ -156,8 +159,8 @@
</svg>
</div>
<div>
<p class="font-semibold text-white">Download iCal File</p>
<p class="text-sm text-slate-400">Download .ics file for any calendar app</p>
<p class="font-semibold text-white">{t('calendar.downloadICalTitle')}</p>
<p class="text-sm text-slate-400">{t('calendar.downloadICalDescription')}</p>
</div>
</div>
</button>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { t } from '$lib/i18n/i18n.js';
// Check if current page is active
const isActive = (path: string): boolean => {
@@ -27,28 +28,28 @@
on:click={() => goto('/')}
class={isActive('/') ? 'text-violet-400' : 'cursor-pointer'}
>
Home
{t('navigation.home')}
</button>
<button
on:click={() => goto('/discover')}
class={isActive('/discover') ? 'text-violet-400' : 'cursor-pointer'}
>
Discover
{t('navigation.discover')}
</button>
<button
on:click={() => goto('/create')}
class={isActive('/create') ? 'text-violet-400' : 'cursor-pointer'}
>
Create
{t('navigation.create')}
</button>
<button
on:click={() => goto('/event')}
class={isActive('/event') ? 'text-violet-400' : 'cursor-pointer'}
>
My Events
{t('navigation.myEvents')}
</button>
</div>
</div>

55
src/lib/i18n/i18n.ts Normal file
View File

@@ -0,0 +1,55 @@
import messages from './messages.json';
// Simple i18n utility for English-only text management
// Get message by key with optional interpolation
export function t(key: string, params?: Record<string, string | number>): string {
// Navigate through nested keys (e.g., 'common.cancel')
const keys = key.split('.');
let value: unknown = messages;
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = (value as Record<string, unknown>)[k];
} else {
console.warn(`Translation key not found: ${key}`);
return key;
}
}
if (typeof value !== 'string') {
console.warn(`Translation value is not a string: ${key}`);
return key;
}
// Interpolate parameters
if (params) {
return value.replace(/\{(\w+)\}/g, (match, paramKey) => {
return params[paramKey]?.toString() || match;
});
}
return value;
}
// Format plural forms (basic implementation)
export function tp(key: string, count: number, params?: Record<string, string | number>): string {
const baseKey = key;
const pluralKey = `${key}_plural`;
// Try to get plural form first
let message = t(pluralKey, { ...params, count });
// If plural form doesn't exist, use singular
if (message === pluralKey) {
message = t(baseKey, { ...params, count });
}
// Replace {plural} with 's' if count > 1
if (count !== 1) {
message = message.replace('{plural}', 's');
} else {
message = message.replace('{plural}', '');
}
return message;
}

246
src/lib/i18n/messages.json Normal file
View File

@@ -0,0 +1,246 @@
{
"common": {
"required": "*",
"cancel": "Cancel",
"create": "Create",
"edit": "Edit",
"delete": "Delete",
"view": "View",
"home": "Home",
"loading": "Loading...",
"error": "Error",
"success": "Success",
"name": "Name",
"date": "Date",
"time": "Time",
"location": "Location",
"type": "Type",
"visibility": "Visibility",
"public": "Public",
"private": "Private",
"limited": "Limited",
"unlimited": "Unlimited",
"capacity": "Capacity",
"attendees": "Attendees",
"attendeeLimit": "Attendee Limit",
"enterLimit": "Enter limit",
"enterEventName": "Enter event name",
"enterLocation": "Enter location",
"enterYourName": "Enter your name",
"enterNumberOfGuests": "Enter number of guests",
"yourName": "Your Name",
"numberOfGuests": "Number of Guests",
"addGuests": "Add guest users",
"joinEvent": "Join Event",
"copyLink": "Copy Link",
"addToCalendar": "Add to Calendar",
"close": "Close",
"closeModal": "Close modal",
"removeRSVP": "Remove RSVP",
"updating": "Updating...",
"creating": "Creating...",
"adding": "Adding...",
"updateEvent": "Update Event",
"createEvent": "Create Event",
"createNewEvent": "Create New Event",
"createYourFirstEvent": "Create Your First Event",
"editEvent": "Edit Event",
"deleteEvent": "Delete Event",
"myEvents": "My Events",
"discover": "Discover",
"noEventsYet": "No Events Yet",
"noPublicEventsYet": "No Public Events Yet",
"noAttendeesYet": "No attendees yet",
"beFirstToJoin": "Be the first to join!",
"eventNotFound": "Event Not Found",
"eventIsFull": "Event is Full!",
"maximumCapacityReached": "Maximum capacity reached",
"eventLinkCopied": "Event link copied to clipboard!",
"rsvpAddedSuccessfully": "RSVP added successfully!",
"removedRsvpSuccessfully": "Removed RSVP successfully.",
"anUnexpectedErrorOccurred": "An unexpected error occurred.",
"somethingWentWrong": "Something went wrong. Please try again.",
"failedToAddRsvp": "Failed to add RSVP",
"failedToRemoveRsvp": "Failed to remove RSVP",
"failedToDeleteEvent": "Failed to delete event",
"youMayNotHavePermission": "You may not have permission to delete this event.",
"anErrorOccurredWhileDeleting": "An error occurred while deleting the event:",
"databaseUnreachable": "Database unreachable.",
"eventIdNotFound": "EventId not found",
"eventNotExists": "Event not found",
"failedToLoadEvent": "Failed to load event",
"nameAndUserIdRequired": "Name and user ID are required",
"eventCapacityExceeded": "Event capacity exceeded. You're trying to add {guests} attendees (including yourself), but only {remaining} spots remain.",
"nameAlreadyExists": "Name already exists for this event",
"missingOrEmptyFields": "Missing or empty fields: {fields}",
"dateCannotBeInPast": "Date cannot be in the past.",
"limitMustBeAtLeast2": "Limit must be at least 2 for limited events.",
"unauthorized": "Unauthorized",
"youCanOnlyEditYourOwnEvents": "You can only edit your own events",
"youDoNotHavePermissionToDelete": "You do not have permission to delete this event",
"eventIdAndUserIdRequired": "Event ID and User ID are required",
"guestsWillBeAddedAs": "Guests will be added as \"{name}'s Guest #1\", \"{name}'s Guest #2\", etc.",
"yourNamePlaceholder": "Your Name",
"atTime": "at"
},
"navigation": {
"home": "Home",
"discover": "Discover",
"create": "Create",
"myEvents": "My Events"
},
"home": {
"title": "Cactoide - The RSVP site",
"description": "Create and manage event RSVPs. No registration required, instant sharing.",
"mainTitle": "Cactoide(ea)",
"subtitle": "The Ultimate RSVP Platform",
"tagline": "Create, share, and manage events with zero friction.",
"whyCactoideTitle": "Why Cactoide(ae)?🌵",
"whyCactoideDescription": "Like the cactus, great events bloom under any condition when managed with care. Cactoide(ae) helps you streamline RSVPs, simplify coordination, and keep every detail efficient—so your gatherings are resilient, vibrant, and unforgettable.",
"createEventNow": "Create Event Now",
"discoverPublicEventsTitle": "Discover Public Events",
"discoverPublicEventsDescription": "See what others are planning and get inspired",
"browseAllPublicEvents": "Browse All Public Events",
"whyCactoideFeatureTitle": "Why Cactoide?",
"instantEventCreationTitle": "Instant Event Creation",
"instantEventCreationDescription": "Create events in seconds with our streamlined form. No accounts, no waiting, just pure efficiency.",
"oneClickSharingTitle": "One-Click Sharing",
"oneClickSharingDescription": "Each event gets a unique, memorable URL. Share instantly via any platform or messaging app.",
"allInOneClarityTitle": "All-in-One Clarity",
"allInOneClarityDescription": "No more scrolling through endless chats and reactions. See everyone's availability and responses neatly in one place.",
"noHassleNoSignUpsTitle": "No Hassle, No Sign-Ups",
"noHassleNoSignUpsDescription": "Skip registrations and endless forms. Unlike other event platforms, you create and share instantly — no accounts, no barriers.",
"smartLimitsTitle": "Smart Limits",
"smartLimitsDescription": "Choose between unlimited RSVPs or set a limited capacity. Perfect for any event size.",
"effortlessSimplicityTitle": "Effortless Simplicity",
"effortlessSimplicityDescription": "Designed to be instantly clear and easy. No learning curve — just open, create, and go.",
"howItWorksTitle": "How It Works",
"step1Title": "Create Event",
"step1Description": "Fill out a simple form with event details. Choose between limited or unlimited capacity.",
"step2Title": "Get Unique URL",
"step2Description": "Receive a random, memorable URL for your event. Perfect for sharing anywhere.",
"step3Title": "Collect RSVPs",
"step3Description": "People visit your link and join with just their name. No accounts needed.",
"ctaTitle": "Ready to Create Your First Event?",
"ctaDescription": "Join thousands of event organizers who trust Cactoide",
"ctaButton": "Create"
},
"create": {
"title": "Create Event - Cactoide",
"formTitle": "Create New Event",
"eventNameLabel": "Name",
"eventNamePlaceholder": "Enter event name",
"dateLabel": "Date",
"timeLabel": "Time",
"locationLabel": "Location",
"locationPlaceholder": "Enter location",
"typeLabel": "Type",
"unlimitedOption": "Unlimited",
"limitedOption": "Limited",
"attendeeLimitLabel": "Attendee Limit",
"attendeeLimitPlaceholder": "Enter limit",
"visibilityLabel": "Visibility",
"publicOption": "🌍 Public",
"privateOption": "🔒 Private",
"publicDescription": "Public events are visible to everyone and can be discovered by others",
"privateDescription": "Private events are only visible to you and people you share the link with",
"creatingEvent": "Creating Event...",
"createEventButton": "Create Event"
},
"event": {
"title": "{eventName} - Cactoide",
"eventTitle": "Event - Cactoide",
"editTitle": "Edit Event - {eventName} - Cactoide",
"myEventsTitle": "My Events - Cactoide",
"eventNotFoundTitle": "Event Not Found",
"eventNotFoundDescription": "The event you're looking for doesn't exist or has been removed.",
"joinThisEvent": "Join This Event",
"eventIsFull": "Event is Full!",
"maximumCapacityReached": "Maximum capacity reached",
"yourNameLabel": "Your Name",
"yourNamePlaceholder": "Enter your name",
"addGuestsLabel": "Add guest users",
"numberOfGuestsLabel": "Number of Guests",
"numberOfGuestsPlaceholder": "Enter number of guests",
"guestsWillBeAddedAs": "Guests will be added as \"{name}'s Guest #1\", \"{name}'s Guest #2\", etc.",
"joinEventButton": "Join Event",
"joinEventWithGuests": "Join Event + {count} Guest{plural}",
"adding": "Adding...",
"attendeesTitle": "Attendees",
"noAttendeesYet": "No attendees yet",
"beFirstToJoin": "Be the first to join!",
"copyLinkButton": "Copy Link",
"addToCalendarButton": "Add to Calendar",
"eventLinkCopied": "Event link copied to clipboard!",
"rsvpAddedSuccessfully": "RSVP added successfully!",
"removedRsvpSuccessfully": "Removed RSVP successfully.",
"failedToAddRsvp": "Failed to add RSVP",
"failedToRemoveRsvp": "Failed to remove RSVP",
"editEventTitle": "Edit Event",
"editEventDescription": "Update your event details",
"updatingEvent": "Updating...",
"updateEventButton": "Update Event",
"myEventsDescription": "Manage your created events",
"noEventsYetTitle": "No Events Yet",
"noEventsYetDescription": "You haven't created any events yet. Start by creating your first event!",
"createYourFirstEventButton": "Create Your First Event",
"deleteEventTitle": "Delete Event",
"deleteEventDescription": "Are you sure you want to delete \"{eventName}\"? This action cannot be undone and will remove all RSVPs.",
"deleteButton": "Delete",
"viewEventAriaLabel": "View event",
"editEventAriaLabel": "Edit event",
"deleteEventAriaLabel": "Delete event",
"removeRsvpAriaLabel": "Remove RSVP"
},
"discover": {
"title": "Discover Events - Cactoide",
"noPublicEventsTitle": "No Public Events Yet",
"noPublicEventsDescription": "There are no public events available at the moment. Be the first to create one!",
"createButton": "Create",
"publicEventsTitle": "Public Events ({count})",
"publicEventsDescription": "Discover events created by the community",
"searchPlaceholder": "Search events by name, location...",
"searchInputAriaLabel": "Search input",
"toggleFiltersAriaLabel": "Toggle filters",
"typeFilterLabel": "Type:",
"typeFilterAll": "All",
"typeFilterLimited": "Limited",
"typeFilterUnlimited": "Unlimited",
"statusFilterLabel": "Status:",
"statusFilterAll": "All events",
"statusFilterUpcoming": "Upcoming events",
"statusFilterPast": "Past events",
"timeFilterLabel": "Time:",
"timeFilterAny": "Any time",
"timeFilterNextWeek": "Next week",
"timeFilterNextMonth": "Next month",
"sortOrderLabel": "Sort:",
"sortOrderEarliest": "Earliest first",
"sortOrderLatest": "Latest first",
"viewButton": "View",
"noEventsFoundTitle": "No events found",
"noEventsFoundDescription": "Try adjusting your search terms or browse all events"
},
"calendar": {
"addToCalendarTitle": "Add to Calendar",
"googleCalendarTitle": "Google Calendar",
"googleCalendarDescription": "Add to Google Calendar",
"microsoftOutlookTitle": "Microsoft Outlook",
"microsoftOutlookDescription": "Add to Outlook Calendar",
"downloadICalTitle": "Download iCal File",
"downloadICalDescription": "Download .ics file for any calendar app"
},
"errors": {
"title": "Error - Cactoide",
"errorTitle": "Error",
"anUnexpectedErrorOccurred": "An unexpected error occurred.",
"homeButton": "Home"
},
"layout": {
"defaultTitle": "Cactoide -",
"defaultDescription": "Create and manage event RSVPs",
"userIdCookieText": "Your UserID storated as a cookie:",
"firstTimeVisiting": "First time visiting. Generating new UserID...",
"copyright": "© 2025 Cactoide"
}
}

View File

@@ -1,12 +1,13 @@
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { t } from '$lib/i18n/i18n.js';
$: error = $page.error;
</script>
<svelte:head>
<title>Error - Cactoide</title>
<title>{t('errors.title')}</title>
</svelte:head>
<div class="flex min-h-screen flex-col">
@@ -15,10 +16,10 @@
<div class="mx-auto max-w-md text-center">
<div class="rounded-sm border border-red-500/30 bg-red-900/20 p-8">
<div class="mb-4 text-6xl text-red-400">🚨</div>
<h2 class="mb-4 text-2xl font-bold text-red-400">Error</h2>
<h2 class="mb-4 text-2xl font-bold text-red-400">{t('errors.errorTitle')}</h2>
<p class=" mb-6">
{error?.message || 'An unexpected error occurred.'}
{error?.message || t('errors.anUnexpectedErrorOccurred')}
</p>
<div class="space-y-3">
@@ -26,7 +27,7 @@
on:click={() => goto('/')}
class="border-white-500 bg-white-400/20 mt-2 w-48 rounded-sm border px-6 py-3 font-semibold text-white duration-400 hover:scale-110 hover:bg-white/10"
>
Home
{t('errors.homeButton')}
</button>
</div>
</div>

View File

@@ -1,13 +1,14 @@
<script>
import '../app.css';
import Navbar from '$lib/components/Navbar.svelte';
import { t } from '$lib/i18n/i18n.js';
let { data } = $props();
</script>
<svelte:head>
<title>Cactoide -</title>
<meta name="description" content="Create and manage event RSVPs" />
<title>{t('layout.defaultTitle')}</title>
<meta name="description" content={t('layout.defaultDescription')} />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
@@ -28,13 +29,12 @@
<div class="container mx-auto px-4 text-center">
<div class="text-sm">
<p class="mb-4 text-gray-100/80">
Your UserID storated as a cookie: <span class="font-bold text-violet-400"
>{data.cactoideUserId
? data.cactoideUserId
: 'First time visiting. Generating new UserID...'}</span
{t('layout.userIdCookieText')}
<span class="font-bold text-violet-400"
>{data.cactoideUserId ? data.cactoideUserId : t('layout.firstTimeVisiting')}</span
>
</p>
<p>&copy; 2025 Cactoide</p>
<p>{t('layout.copyright')}</p>
</div>
</div>
</footer>

View File

@@ -1,45 +1,41 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { t } from '$lib/i18n/i18n.js';
</script>
<svelte:head>
<title>Cactoide - The RSVP site</title>
<meta
name="description"
content="Create and manage event RSVPs. No registration required, instant sharing."
/>
<title>{t('home.title')}</title>
<meta name="description" content={t('home.description')} />
</svelte:head>
<div class="flex min-h-screen flex-col">
<section class="mx-auto w-full pt-20 pb-20 md:w-3/4">
<div class="container mx-auto px-4 text-center">
<h1 class="text-5xl font-bold md:text-7xl lg:text-8xl">
Cactoide(ea)<span class="text-violet-400"
{t('home.mainTitle')}<span class="text-violet-400"
><a href="https://en.wikipedia.org/wiki/Cactoideae" target="_blank">*</a></span
> 🌵
</h1>
<h2 class="mt-6 text-xl md:text-2xl">The Ultimate RSVP Platform</h2>
<h2 class="mt-6 text-xl md:text-2xl">{t('home.subtitle')}</h2>
<p class="mt-4 text-lg italic md:text-xl">
Create, share, and manage events with zero friction.
{t('home.tagline')}
</p>
<h2 class="mt-6 pt-8 text-xl md:text-2xl">
Why Cactoide(ae)<span class="text-violet-400"
{t('home.whyCactoideTitle')}<span class="text-violet-400"
><a href="https://en.wikipedia.org/wiki/Cactoideae" target="_blank">*</a></span
>?🌵
</h2>
<p class="mt-4 text-lg md:text-xl">
Like the cactus, great events bloom under any condition when managed with care. Cactoide(ae)
helps you streamline RSVPs, simplify coordination, and keep every detail efficient—so your
gatherings are resilient, vibrant, and unforgettable.
{t('home.whyCactoideDescription')}
</p>
<button
on:click={() => goto('/create')}
class="mt-8 rounded-sm border-2 border-violet-500 px-8 py-4 font-bold duration-400 hover:scale-110 hover:bg-violet-500/10"
>
Create Event Now
{t('home.createEventNow')}
</button>
</div>
</section>
@@ -48,8 +44,8 @@
<section class="py-8">
<div class="container mx-auto px-4">
<div class="mb-16 text-center">
<h2 class="text-4xl font-bold text-white">Discover Public Events</h2>
<p class="mt-4 text-xl text-slate-300">See what others are planning and get inspired</p>
<h2 class="text-4xl font-bold text-white">{t('home.discoverPublicEventsTitle')}</h2>
<p class="mt-4 text-xl text-slate-300">{t('home.discoverPublicEventsDescription')}</p>
</div>
<div class="text-center">
@@ -57,7 +53,7 @@
on:click={() => goto('/discover')}
class="rounded-sm border-2 border-violet-500 px-8 py-4 font-bold duration-400 hover:scale-110 hover:bg-violet-500/10"
>
Browse All Public Events
{t('home.browseAllPublicEvents')}
</button>
</div>
</div>
@@ -67,7 +63,7 @@
<section class="py-20">
<div class="container mx-auto px-4">
<h2 class=" mb-16 text-center text-4xl font-bold">
Why <span class="text-violet-400">Cactoide?</span>
{t('home.whyCactoideFeatureTitle')}
</h2>
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
<!-- Feature 1 -->
@@ -75,10 +71,9 @@
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
<span class="text-4xl">🎯</span>
</div>
<h3 class="mb-4 text-xl font-bold text-white">Instant Event Creation</h3>
<h3 class="mb-4 text-xl font-bold text-white">{t('home.instantEventCreationTitle')}</h3>
<p class="">
Create events in seconds with our streamlined form. No accounts, no waiting, just pure
efficiency.
{t('home.instantEventCreationDescription')}
</p>
</div>
@@ -87,10 +82,9 @@
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
<span class="text-4xl">🔗</span>
</div>
<h3 class="mb-4 text-xl font-bold text-white">One-Click Sharing</h3>
<h3 class="mb-4 text-xl font-bold text-white">{t('home.oneClickSharingTitle')}</h3>
<p class="">
Each event gets a unique, memorable URL. Share instantly via any platform or messaging
app.
{t('home.oneClickSharingDescription')}
</p>
</div>
@@ -99,10 +93,9 @@
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
<span class="text-4xl">🔍</span>
</div>
<h3 class="mb-4 text-xl font-bold text-white">All-in-One Clarity</h3>
<h3 class="mb-4 text-xl font-bold text-white">{t('home.allInOneClarityTitle')}</h3>
<p class="">
No more scrolling through endless chats and reactions. See everyones availability and
responses neatly in one place.
{t('home.allInOneClarityDescription')}
</p>
</div>
@@ -111,10 +104,9 @@
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
<span class="text-4xl">👤</span>
</div>
<h3 class="mb-4 text-xl font-bold text-white">No Hassle, No Sign-Ups</h3>
<h3 class="mb-4 text-xl font-bold text-white">{t('home.noHassleNoSignUpsTitle')}</h3>
<p class="">
Skip registrations and endless forms. Unlike other event platforms, you create and share
instantly — no accounts, no barriers.
{t('home.noHassleNoSignUpsDescription')}
</p>
</div>
@@ -123,9 +115,9 @@
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
<span class="text-4xl">🛡️</span>
</div>
<h3 class="mb-4 text-xl font-bold text-white">Smart Limits</h3>
<h3 class="mb-4 text-xl font-bold text-white">{t('home.smartLimitsTitle')}</h3>
<p class="">
Choose between unlimited RSVPs or set a limited capacity. Perfect for any event size.
{t('home.smartLimitsDescription')}
</p>
</div>
@@ -134,9 +126,9 @@
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
<span class="text-4xl"></span>
</div>
<h3 class="mb-4 text-xl font-bold text-white">Effortless Simplicity</h3>
<h3 class="mb-4 text-xl font-bold text-white">{t('home.effortlessSimplicityTitle')}</h3>
<p class="">
Designed to be instantly clear and easy. No learning curve — just open, create, and go.
{t('home.effortlessSimplicityDescription')}
</p>
</div>
</div>
@@ -146,35 +138,38 @@
<!-- How It Works Section -->
<section class="py-20">
<div class="container mx-auto px-4">
<h2 class=" mb-16 text-center text-4xl font-bold">How It Works</h2>
<h2 class=" mb-16 text-center text-4xl font-bold">{t('home.howItWorksTitle')}</h2>
<div class="grid gap-8 md:grid-cols-3">
<!-- Step 1 -->
<div class="text-center">
<h3 class="mb-4 text-xl font-bold text-white">
<span class="text-violet-400">1.</span> Create Event
<span class="text-violet-400">1.</span>
{t('home.step1Title')}
</h3>
<p class="">
Fill out a simple form with event details. Choose between limited or unlimited capacity.
{t('home.step1Description')}
</p>
</div>
<!-- Step 2 -->
<div class="text-center">
<h3 class="mb-4 text-xl font-bold text-white">
<span class="text-violet-400">2.</span> Get Unique URL
<span class="text-violet-400">2.</span>
{t('home.step2Title')}
</h3>
<p class="">
Receive a random, memorable URL for your event. Perfect for sharing anywhere.
{t('home.step2Description')}
</p>
</div>
<!-- Step 3 -->
<div class="text-center">
<h3 class="mb-4 text-xl font-bold text-white">
<span class="text-violet-400">3.</span> Collect RSVPs
<span class="text-violet-400">3.</span>
{t('home.step3Title')}
</h3>
<p class="">People visit your link and join with just their name. No accounts needed.</p>
<p class="">{t('home.step3Description')}</p>
</div>
</div>
</div>
@@ -184,14 +179,14 @@
<section class="py-20">
<div class="container mx-auto px-4 text-center">
<h2 class="mb-6 text-4xl font-bold text-white">
Ready to Create Your <span class="text-violet-400">First Event</span>?
{t('home.ctaTitle')}
</h2>
<p class="mb-10 text-xl">Join thousands of event organizers who trust Cactoide</p>
<p class="mb-10 text-xl">{t('home.ctaDescription')}</p>
<button
on:click={() => goto('/create')}
class="rounded-sm border-2 border-violet-500 px-8 py-4 font-bold duration-400 hover:scale-110 hover:bg-violet-500/10"
>
Create
{t('home.ctaButton')}
</button>
</div>
</section>

View File

@@ -2,6 +2,7 @@
import type { CreateEventData, EventType } from '$lib/types';
import { enhance } from '$app/forms';
import { goto } from '$app/navigation';
import { t } from '$lib/i18n/i18n.js';
export let form;
@@ -51,7 +52,7 @@
</script>
<svelte:head>
<title>Create Event - Cactoide</title>
<title>{t('create.title')}</title>
</svelte:head>
<div class="flex min-h-screen flex-col">
@@ -60,7 +61,7 @@
<div class="mx-auto max-w-md">
<!-- Event Creation Form -->
<div class="rounded-sm border p-8">
<h2 class="mb-8 text-center text-3xl font-bold text-violet-400">Create New Event</h2>
<h2 class="mb-8 text-center text-3xl font-bold text-violet-400">{t('create.formTitle')}</h2>
<form
method="POST"
@@ -71,7 +72,7 @@
if (result.type === 'failure') {
// Handle validation errors
if (result.data?.error) {
errors.server = result.data.error;
errors.server = String(result.data.error);
}
}
update();
@@ -92,7 +93,7 @@
<!-- Event Name -->
<div>
<label for="name" class="text-dark-800 mb-3 block text-sm font-semibold">
Name <span class="text-red-400">*</span>
{t('create.eventNameLabel')} <span class="text-red-400">{t('common.required')}</span>
</label>
<input
id="name"
@@ -100,7 +101,7 @@
type="text"
bind:value={eventData.name}
class="border-dark-300 w-full rounded-sm border-2 px-4 py-3 text-slate-900 shadow-sm"
placeholder="Enter event name"
placeholder={t('create.eventNamePlaceholder')}
maxlength="100"
required
/>
@@ -113,7 +114,7 @@
<div class="grid grid-cols-2 gap-4">
<div>
<label for="date" class="text-dark-800 mb-3 block text-sm font-semibold">
Date <span class="text-red-400">*</span>
{t('create.dateLabel')} <span class="text-red-400">{t('common.required')}</span>
</label>
<input
id="date"
@@ -131,7 +132,7 @@
<div>
<label for="time" class="text-dark-800 mb-3 block text-sm font-semibold">
Time <span class="text-red-400">*</span>
{t('create.timeLabel')} <span class="text-red-400">{t('common.required')}</span>
</label>
<input
id="time"
@@ -150,7 +151,7 @@
<!-- Location -->
<div>
<label for="location" class="text-dark-800 mb-3 block text-sm font-semibold">
Location <span class="text-red-400">*</span>
{t('create.locationLabel')} <span class="text-red-400">{t('common.required')}</span>
</label>
<input
id="location"
@@ -158,7 +159,7 @@
type="text"
bind:value={eventData.location}
class="border-dark-300 placeholder-dark-500 w-full rounded-sm border-2 px-4 py-3 text-slate-900 shadow-sm transition-all"
placeholder="Enter location"
placeholder={t('create.locationPlaceholder')}
maxlength="200"
required
/>
@@ -170,7 +171,8 @@
<!-- Event Type -->
<div>
<label class="text-dark-800 mb-3 block text-sm font-semibold">
Type <span class="text-red-400">*</span></label
{t('create.typeLabel')}
<span class="text-red-400">{t('common.required')}</span></label
>
<div class="grid grid-cols-2 gap-3">
<button
@@ -181,7 +183,7 @@
: 'border-dark-300 text-dark-700'}"
on:click={() => handleTypeChange('unlimited')}
>
Unlimited
{t('create.unlimitedOption')}
</button>
<button
type="button"
@@ -191,7 +193,7 @@
: 'border-dark-300 text-dark-700 bg-gray-600/20 hover:bg-gray-600/70'}"
on:click={() => handleTypeChange('limited')}
>
Limited
{t('create.limitedOption')}
</button>
</div>
</div>
@@ -200,7 +202,8 @@
{#if eventData.type === 'limited'}
<div>
<label for="limit" class="text-dark-800 mb-3 block text-sm font-semibold">
Attendee Limit *
{t('create.attendeeLimitLabel')}
{t('common.required')}
</label>
<input
id="attendee_limit"
@@ -210,7 +213,7 @@
min="1"
max="1000"
class="border-dark-300 w-full rounded-sm border-2 bg-white px-4 py-3 text-slate-900 shadow-sm transition-all duration-200"
placeholder="Enter limit"
placeholder={t('create.attendeeLimitPlaceholder')}
required
/>
{#if errors.attendee_limit}
@@ -222,7 +225,8 @@
<!-- Event Visibility -->
<div>
<label class="text-dark-800 mb-3 block text-sm font-semibold">
Visibility <span class="text-red-400">*</span></label
{t('create.visibilityLabel')}
<span class="text-red-400">{t('common.required')}</span></label
>
<div class="grid grid-cols-2 gap-3">
<button
@@ -233,7 +237,7 @@
: 'border-dark-300 text-dark-700'}"
on:click={() => (eventData.visibility = 'public')}
>
🌍 Public
{t('create.publicOption')}
</button>
<button
type="button"
@@ -243,13 +247,13 @@
: 'border-dark-300 text-dark-700 bg-gray-600/20 hover:bg-gray-600/70'}"
on:click={() => (eventData.visibility = 'private')}
>
🔒 Private
{t('create.privateOption')}
</button>
</div>
<p class="mt-2 text-xs text-slate-400">
{eventData.visibility === 'public'
? 'Public events are visible to everyone and can be discovered by others'
: 'Private events are only visible to you and people you share the link with'}
? t('create.publicDescription')
: t('create.privateDescription')}
</p>
</div>
@@ -259,7 +263,7 @@
on:click={handleCancel}
class="flex-1 rounded-sm border-2 border-slate-300 bg-slate-200 px-4 py-3 font-semibold text-slate-700 transition-all duration-200 hover:bg-slate-400 hover:text-slate-200"
>
Cancel
{t('common.cancel')}
</button>
<!-- Submit Button -->
<button
@@ -270,10 +274,10 @@
{#if isSubmitting}
<div class="flex items-center justify-center">
<div class="mr-2 h-5 w-5 animate-spin rounded-full border-b-2 border-white"></div>
Creating Event...
{t('create.creatingEvent')}
</div>
{:else}
Create Event
{t('create.createEventButton')}
{/if}
</button>
</div>

View File

@@ -3,6 +3,7 @@
import { goto } from '$app/navigation';
import type { PageData } from '../$types';
import { formatTime, formatDate, isEventInTimeRange } from '$lib/dateHelpers';
import { t } from '$lib/i18n/i18n.js';
import Fuse from 'fuse.js';
let publicEvents: Event[] = [];
@@ -17,7 +18,7 @@
export let data: PageData;
// Use the server-side data
$: publicEvents = data.events;
$: publicEvents = data?.events || [];
// Initialize Fuse.js with search options
$: fuse = new Fuse(publicEvents, {
@@ -81,7 +82,7 @@
</script>
<svelte:head>
<title>Discover Events - Cactoide</title>
<title>{t('discover.title')}</title>
</svelte:head>
<div class="flex min-h-screen flex-col">
@@ -90,34 +91,36 @@
{#if error}
<div class="mx-auto max-w-2xl text-center">
<div class="mb-4 text-4xl">⚠️</div>
<p class="py-4">Something went wrong. Please try again.</p>
<p class="py-4">{t('common.somethingWentWrong')}</p>
<p class="text-red-600">{error}</p>
<button
on:click={() => goto('/')}
class="rounded-sm border-2 border-violet-500 px-8 py-4 font-bold duration-400 hover:scale-110 hover:bg-violet-500/10"
>
Home
{t('common.home')}
</button>
</div>
{:else if publicEvents.length === 0}
<div class="mx-auto max-w-2xl text-center">
<div class="mb-4 animate-pulse text-6xl">🔍</div>
<h2 class="mb-4 text-2xl font-bold">No Public Events Yet</h2>
<h2 class="mb-4 text-2xl font-bold">{t('discover.noPublicEventsTitle')}</h2>
<p class="text-white-600 mb-8">
There are no public events available at the moment. Be the first to create one!
{t('discover.noPublicEventsDescription')}
</p>
<button
on:click={() => goto('/create')}
class="rounded-sm border-2 border-violet-500 px-8 py-4 font-bold duration-400 hover:scale-110 hover:bg-violet-500/10"
>
Create
{t('discover.createButton')}
</button>
</div>
{:else}
<div class="mx-auto max-w-4xl">
<div class="mb-6">
<h2 class="text-2xl font-bold text-slate-300">Public Events ({filteredEvents.length})</h2>
<p class="text-slate-500">Discover events created by the community</p>
<h2 class="text-2xl font-bold text-slate-300">
{t('discover.publicEventsTitle', { count: filteredEvents.length })}
</h2>
<p class="text-slate-500">{t('discover.publicEventsDescription')}</p>
</div>
<!-- Search and Filter Section -->
@@ -143,14 +146,14 @@
<input
type="text"
bind:value={searchQuery}
placeholder="Search events by name, location..."
placeholder={t('discover.searchPlaceholder')}
class="w-full rounded-sm border border-slate-600 bg-slate-800 pl-10 text-white placeholder-slate-400 focus:border-violet-500 focus:ring-violet-500/20"
/>
{#if searchQuery}
<button
on:click={() => (searchQuery = '')}
class="absolute inset-y-0 right-0 flex items-center pr-3 text-slate-400 hover:text-slate-300"
aria-label="Search input"
aria-label={t('discover.searchInputAriaLabel')}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@@ -170,7 +173,7 @@
class="flex items-center rounded-sm border p-3 font-semibold {showFilters
? 'border-violet-500 bg-violet-400/20'
: 'border-slate-600 bg-slate-800'}"
aria-label="Toggle filters"
aria-label={t('discover.toggleFiltersAriaLabel')}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@@ -191,57 +194,61 @@
<!-- Event Type Filter -->
<div class="flex items-center gap-2">
<label for="event-type-filter" class="text-sm font-medium text-slate-400"
>Type:</label
>{t('discover.typeFilterLabel')}</label
>
<select
id="event-type-filter"
bind:value={selectedEventType}
class="rounded-sm border border-slate-600 bg-slate-800 px-3 py-2 text-sm text-white focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20"
>
<option value="all">All</option>
<option value="limited">Limited</option>
<option value="unlimited">Unlimited</option>
<option value="all">{t('discover.typeFilterAll')}</option>
<option value="limited">{t('discover.typeFilterLimited')}</option>
<option value="unlimited">{t('discover.typeFilterUnlimited')}</option>
</select>
</div>
<!-- Temporal Status Filter -->
<div class="flex items-center gap-2">
<label for="temporal-status-filter" class="text-sm font-medium text-slate-400"
>Status:</label
>{t('discover.statusFilterLabel')}</label
>
<select
id="temporal-status-filter"
bind:value={selectedTemporalStatus}
class="rounded-sm border border-slate-600 bg-slate-800 px-3 py-2 text-sm text-white focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20"
>
<option value="all">All events</option>
<option value="upcoming">Upcoming events</option>
<option value="past">Past events</option>
<option value="all">{t('discover.statusFilterAll')}</option>
<option value="upcoming">{t('discover.statusFilterUpcoming')}</option>
<option value="past">{t('discover.statusFilterPast')}</option>
</select>
</div>
<!-- Time Filter Dropdown -->
<div class="flex items-center gap-2">
<label for="time-filter" class="text-sm font-medium text-slate-400">Time:</label>
<label for="time-filter" class="text-sm font-medium text-slate-400"
>{t('discover.timeFilterLabel')}</label
>
<select
id="time-filter"
bind:value={selectedTimeFilter}
class="rounded-sm border border-slate-600 bg-slate-800 px-3 py-2 text-sm text-white focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20"
>
<option value="any">Any time</option>
<option value="next-week">Next week</option>
<option value="next-month">Next month</option>
<option value="any">{t('discover.timeFilterAny')}</option>
<option value="next-week">{t('discover.timeFilterNextWeek')}</option>
<option value="next-month">{t('discover.timeFilterNextMonth')}</option>
</select>
</div>
<!-- Sort Order Dropdown -->
<div class="flex items-center gap-2">
<label for="sort-order" class="text-sm font-medium text-slate-400">Sort:</label>
<label for="sort-order" class="text-sm font-medium text-slate-400"
>{t('discover.sortOrderLabel')}</label
>
<select
id="sort-order"
bind:value={selectedSortOrder}
class="rounded-sm border border-slate-600 bg-slate-800 px-3 py-2 text-sm text-white focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20"
>
<option value="asc">Earliest first</option>
<option value="desc">Latest first</option>
<option value="asc">{t('discover.sortOrderEarliest')}</option>
<option value="desc">{t('discover.sortOrderLatest')}</option>
</select>
</div>
</div>
@@ -263,7 +270,9 @@
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
></path>
</svg>
<span>{formatDate(event.date)} at {formatTime(event.time)}</span>
<span
>{formatDate(event.date)} {t('common.atTime')} {formatTime(event.time)}</span
>
</div>
<div class="flex items-center space-x-2">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -289,7 +298,7 @@
? 'border-amber-600 text-amber-600'
: 'border-teal-500 text-teal-500'}"
>
{event.type === 'limited' ? 'Limited' : 'Unlimited'}
{event.type === 'limited' ? t('common.limited') : t('common.unlimited')}
</span>
</div>
</div>
@@ -300,7 +309,7 @@
on:click={() => goto(`/event/${event.id}`)}
class="flex-1 rounded-sm border-2 border-violet-500 bg-violet-400/20 px-4 py-2 font-semibold duration-200 hover:bg-violet-400/70"
>
View
{t('discover.viewButton')}
</button>
</div>
</div>
@@ -310,8 +319,10 @@
{#if searchQuery && filteredEvents.length === 0}
<div class="mt-8 text-center">
<div class="mb-4 text-4xl">🔍</div>
<h3 class="mb-2 text-xl font-bold text-slate-300">No events found</h3>
<p class="text-slate-500">Try adjusting your search terms or browse all events</p>
<h3 class="mb-2 text-xl font-bold text-slate-300">
{t('discover.noEventsFoundTitle')}
</h3>
<p class="text-slate-500">{t('discover.noEventsFoundDescription')}</p>
</div>
{/if}
</div>

View File

@@ -2,6 +2,7 @@
import type { Event } from '$lib/types';
import { goto } from '$app/navigation';
import { formatTime, formatDate } from '$lib/dateHelpers';
import { t } from '$lib/i18n/i18n.js';
export let data: { events: Event[] };
@@ -60,7 +61,7 @@
</script>
<svelte:head>
<title>My Events - Cactoide</title>
<title>{t('event.myEventsTitle')}</title>
</svelte:head>
<div class="flex min-h-screen flex-col">
@@ -69,22 +70,24 @@
{#if userEvents.length === 0}
<div class="mx-auto max-w-2xl text-center">
<div class="mb-4 animate-pulse text-6xl">🎉</div>
<h2 class="mb-4 text-2xl font-bold">No Events Yet</h2>
<h2 class="mb-4 text-2xl font-bold">{t('event.noEventsYetTitle')}</h2>
<p class="text-white-600 mb-8">
You haven't created any events yet. Start by creating your first event!
{t('event.noEventsYetDescription')}
</p>
<button
on:click={() => goto('/create')}
class="rounded-sm border-2 border-violet-500 px-8 py-4 font-bold duration-400 hover:scale-110 hover:bg-violet-500/10"
>
Create Your First Event
{t('event.createYourFirstEventButton')}
</button>
</div>
{:else}
<div class="mx-auto max-w-4xl">
<div class="mb-6">
<h2 class="text-2xl font-bold text-slate-400">My Events ({userEvents.length})</h2>
<p class="text-slate-500">Manage your created events</p>
<h2 class="text-2xl font-bold text-slate-400">
{t('event.myEventsTitle')} ({userEvents.length})
</h2>
<p class="text-slate-500">{t('event.myEventsDescription')}</p>
</div>
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
@@ -104,7 +107,9 @@
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
></path>
</svg>
<span>{formatDate(event.date)} at {formatTime(event.time)}</span>
<span
>{formatDate(event.date)} {t('common.atTime')} {formatTime(event.time)}</span
>
</div>
<div class="flex items-center space-x-2">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -130,7 +135,7 @@
? 'border-amber-600 text-amber-600'
: 'border-teal-500 text-teal-500'}"
>
{event.type === 'limited' ? 'Limited' : 'Unlimited'}
{event.type === 'limited' ? t('common.limited') : t('common.unlimited')}
</span>
<span
class="rounded-sm border px-2 py-1 text-xs font-medium {event.visibility ===
@@ -138,7 +143,7 @@
? 'border-green-300 text-green-400'
: 'border-orange-300 text-orange-400'}"
>
{event.visibility === 'public' ? 'Public' : 'Private'}
{event.visibility === 'public' ? t('common.public') : t('common.private')}
</span>
</div>
<div class="flex items-center space-x-2"></div>
@@ -149,7 +154,7 @@
<button
on:click={() => goto(`/event/${event.id}`)}
class="flex-1 rounded-sm border-2 border-violet-500 bg-violet-400/20 px-4 py-2 font-semibold duration-200 hover:bg-violet-400/70"
aria-label="View event"
aria-label={t('event.viewEventAriaLabel')}
>
<svg
class="mx-auto h-4 w-4"
@@ -174,7 +179,7 @@
<button
on:click={() => goto(`/event/${event.id}/edit`)}
class="flex-1 rounded-sm border-2 border-blue-400 bg-blue-400/20 px-4 py-2 font-semibold text-white duration-200 hover:bg-blue-400/70"
aria-label="Edit event"
aria-label={t('event.editEventAriaLabel')}
>
<svg
class="mx-auto h-4 w-4"
@@ -193,7 +198,7 @@
<button
on:click={() => openDeleteModal(event)}
class="flex-1 rounded-sm border-2 border-red-400 bg-red-400/20 px-4 py-2 font-semibold text-white duration-200 hover:bg-red-400/70"
aria-label="Delete event"
aria-label={t('event.deleteEventAriaLabel')}
>
<svg
class="mx-auto h-4 w-4"
@@ -228,10 +233,9 @@
>
<span class="text-2xl text-red-600">🗑️</span>
</div>
<h3 class="mb-2 text-xl font-bold text-white">Delete Event</h3>
<h3 class="mb-2 text-xl font-bold text-white">{t('event.deleteEventTitle')}</h3>
<p class="text-slate-400">
Are you sure you want to delete "<span class="font-semibold">{eventToDelete.name}</span>"?
This action cannot be undone and will remove all RSVPs.
{t('event.deleteEventDescription', { eventName: eventToDelete.name })}
</p>
</div>
@@ -240,13 +244,13 @@
on:click={closeDeleteModal}
class="flex-1 rounded-sm border-2 border-slate-300 bg-slate-200 px-4 py-2 font-semibold text-slate-700 transition-all duration-200 hover:bg-slate-300"
>
Cancel
{t('common.cancel')}
</button>
<button
on:click={confirmDelete}
class="flex-1 rounded-sm border-2 border-red-500 bg-red-500 px-4 py-2 font-semibold text-white transition-all duration-200 hover:bg-red-600"
>
Delete
{t('common.delete')}
</button>
</div>
</div>

View File

@@ -6,6 +6,7 @@
import { formatTime, formatDate } from '$lib/dateHelpers.js';
import CalendarModal from '$lib/components/CalendarModal.svelte';
import type { CalendarEvent } from '$lib/calendarHelpers.js';
import { t } from '$lib/i18n/i18n.js';
export let data: { event: Event; rsvps: RSVP[]; userId: string };
export let form;
@@ -80,7 +81,7 @@
</script>
<svelte:head>
<title>{event?.name || 'Event'} - Cactoide</title>
<title>{event?.name || t('event.eventTitle')}</title>
</svelte:head>
<div class="flex min-h-screen flex-col">
@@ -91,13 +92,13 @@
<div class="mx-auto max-w-md text-center">
<div class="rounded-sm border border-red-500/30 bg-red-900/20 p-8">
<div class="mb-4 text-6xl text-red-400">⚠️</div>
<h2 class="mb-4 text-2xl font-bold text-red-400">Event Not Found</h2>
<p class="my-8">The event you're looking for doesn't exist or has been removed.</p>
<h2 class="mb-4 text-2xl font-bold text-red-400">{t('event.eventNotFoundTitle')}</h2>
<p class="my-8">{t('event.eventNotFoundDescription')}</p>
<button
on:click={() => goto('/create')}
class="border-white-500 bg-white-400/20 mt-2 rounded-sm border px-6 py-3 font-semibold text-white duration-400 hover:scale-110 hover:bg-white/10"
>
Create New Event
{t('common.createNewEvent')}
</button>
</div>
</div>
@@ -163,7 +164,7 @@
? 'border-amber-600 text-amber-600'
: 'border-teal-500 text-teal-500'}"
>
{event.type === 'limited' ? 'Limited' : 'Unlimited'}
{event.type === 'limited' ? t('common.limited') : t('common.unlimited')}
</span>
<span
class="rounded-sm border px-2 py-1 text-xs font-medium {event.visibility ===
@@ -171,13 +172,13 @@
? 'border-green-300 text-green-400'
: 'border-orange-300 text-orange-400'}"
>
{event.visibility === 'public' ? 'Public' : 'Private'}
{event.visibility === 'public' ? t('common.public') : t('common.private')}
</span>
</div>
{#if event.type === 'limited' && event.attendee_limit}
<div class="text-right">
<p class="text-sm">Capacity</p>
<p class="text-sm">{t('common.capacity')}</p>
<p class=" text-lg font-bold">
{rsvps.length}/{event.attendee_limit}
</p>
@@ -189,13 +190,13 @@
<!-- RSVP Form -->
<div class=" rounded-sm border p-6 shadow-2xl backdrop-blur-sm">
<h3 class=" mb-4 text-xl font-bold">Join This Event</h3>
<h3 class=" mb-4 text-xl font-bold">{t('event.joinThisEvent')}</h3>
{#if event.type === 'limited' && event.attendee_limit && rsvps.length >= event.attendee_limit}
<div class="py-6 text-center">
<div class="mb-3 text-4xl text-red-400">🚫</div>
<p class="font-semibold text-red-400">Event is Full!</p>
<p class="mt-1 text-sm">Maximum capacity reached</p>
<p class="font-semibold text-red-400">{t('event.eventIsFull')}</p>
<p class="mt-1 text-sm">{t('event.maximumCapacityReached')}</p>
</div>
{:else}
<form
@@ -217,7 +218,8 @@
<input type="hidden" name="userId" value={currentUserId} />
<div>
<label for="attendeeName" class=" mb-2 block text-sm font-semibold">
Your Name <span class="text-red-400">*</span>
{t('event.yourNameLabel')}
<span class="text-red-400">{t('common.required')}</span>
</label>
<input
id="attendeeName"
@@ -225,7 +227,7 @@
type="text"
bind:value={newAttendeeName}
class="border-dark-300 w-full rounded-sm border-2 px-4 py-3 text-slate-900 shadow-sm"
placeholder="Enter your name"
placeholder={t('event.yourNamePlaceholder')}
maxlength="50"
required
/>
@@ -240,7 +242,7 @@
class="h-4 w-4 rounded border-gray-300 text-violet-600 focus:ring-violet-500"
/>
<label for="addGuests" class="text-sm font-medium text-white">
Add guest users
{t('event.addGuestsLabel')}
</label>
</div>
@@ -248,7 +250,8 @@
{#if addGuests}
<div>
<label for="numberOfGuests" class="mb-2 block text-sm font-semibold">
Number of Guests <span class="text-red-400">*</span>
{t('event.numberOfGuestsLabel')}
<span class="text-red-400">{t('common.required')}</span>
</label>
<input
id="numberOfGuests"
@@ -258,12 +261,13 @@
min="1"
max="10"
class="border-dark-300 w-full rounded-sm border-2 px-4 py-3 text-slate-900 shadow-sm"
placeholder="Enter number of guests"
placeholder={t('event.numberOfGuestsPlaceholder')}
required
/>
<p class="mt-1 text-xs text-slate-400">
Guests will be added as "{newAttendeeName || 'Your Name'}'s Guest #1", "{newAttendeeName ||
'Your Name'}'s Guest #2", etc.
{t('event.guestsWillBeAddedAs', {
name: newAttendeeName || t('common.yourNamePlaceholder')
})}
</p>
</div>
{/if}
@@ -280,12 +284,15 @@
<div
class="mr-2 h-5 w-5 animate-spin rounded-full border-b-2 border-white"
></div>
Adding...
{t('event.adding')}
</div>
{:else if addGuests && numberOfGuests > 0}
Join Event + {numberOfGuests} Guest{numberOfGuests > 1 ? 's' : ''}
{t('event.joinEventWithGuests', {
count: numberOfGuests,
plural: numberOfGuests > 1 ? 's' : ''
})}
{:else}
Join Event
{t('event.joinEventButton')}
{/if}
</button>
</form>
@@ -295,14 +302,14 @@
<!-- Attendees List -->
<div class="rounded-sm border p-6 shadow-2xl backdrop-blur-sm">
<div class="mb-4 flex items-center justify-between">
<h3 class=" text-xl font-bold">Attendees</h3>
<h3 class=" text-xl font-bold">{t('event.attendeesTitle')}</h3>
<span class="text-2xl font-bold">{rsvps.length}</span>
</div>
{#if rsvps.length === 0}
<div class="text-dark-400 py-8 text-center">
<p>No attendees yet</p>
<p class="mt-1 text-sm">Be the first to join!</p>
<p>{t('event.noAttendeesYet')}</p>
<p class="mt-1 text-sm">{t('event.beFirstToJoin')}</p>
</div>
{:else}
<div class="space-y-3">
@@ -361,7 +368,7 @@
<button
type="submit"
class="text-dark-400 p-1 transition-colors duration-200 hover:text-red-400"
aria-label="Remove RSVP"
aria-label={t('event.removeRsvpAriaLabel')}
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@@ -386,13 +393,13 @@
on:click={copyEventLink}
class="hover:bg-violet-400/70' w-full rounded-sm border-2 border-violet-500 bg-violet-400/20 px-4 py-3 py-4 font-bold font-medium font-semibold text-white shadow-lg transition-all duration-200 hover:scale-105"
>
Copy Link
{t('event.copyLinkButton')}
</button>
<button
on:click={openCalendarModal}
class="hover:bg-violet-400/70' w-full rounded-sm border-2 border-violet-500 bg-violet-400/20 px-4 py-3 py-4 font-bold font-medium font-semibold text-white shadow-lg transition-all duration-200 hover:scale-105"
>
Add to Calendar
{t('event.addToCalendarButton')}
</button>
</div>
</div>
@@ -423,7 +430,7 @@
<div
class="fixed right-4 bottom-4 z-40 w-128 rounded-sm border border-yellow-500/30 bg-yellow-900/20 p-4 text-yellow-400"
>
Removed RSVP successfully.
{t('event.removedRsvpSuccessfully')}
</div>
{/if}
{/if}

View File

@@ -2,6 +2,7 @@
import type { EventType } from '$lib/types';
import { enhance } from '$app/forms';
import { goto } from '$app/navigation';
import { t } from '$lib/i18n/i18n.js';
export let data;
export let form;
@@ -33,7 +34,9 @@
eventData = {
...eventData,
...values,
attendee_limit: values.attendee_limit ? parseInt(String(values.attendee_limit)) : null
attendee_limit: (values as any).attendee_limit
? parseInt(String((values as any).attendee_limit))
: null
};
}
@@ -50,7 +53,7 @@
</script>
<svelte:head>
<title>Edit Event - {data.event.name} - Cactoide</title>
<title>{t('event.editTitle', { eventName: data.event.name })}</title>
</svelte:head>
<div class="flex min-h-screen flex-col">
@@ -60,8 +63,8 @@
<!-- Event Edit Form -->
<div class="rounded-sm border p-8">
<div class="mb-8 text-center">
<h2 class="text-3xl font-bold text-violet-400">Edit Event</h2>
<p class="mt-2 text-sm text-slate-400">Update your event details</p>
<h2 class="text-3xl font-bold text-violet-400">{t('event.editEventTitle')}</h2>
<p class="mt-2 text-sm text-slate-400">{t('event.editEventDescription')}</p>
</div>
<form
@@ -93,7 +96,7 @@
<!-- Event Name -->
<div>
<label for="name" class="text-dark-800 mb-3 block text-sm font-semibold">
Name <span class="text-red-400">*</span>
{t('common.name')} <span class="text-red-400">{t('common.required')}</span>
</label>
<input
id="name"
@@ -101,7 +104,7 @@
type="text"
bind:value={eventData.name}
class="border-dark-300 w-full rounded-sm border-2 px-4 py-3 text-slate-900 shadow-sm"
placeholder="Enter event name"
placeholder={t('common.enterEventName')}
maxlength="100"
required
/>
@@ -114,7 +117,7 @@
<div class="grid grid-cols-2 gap-4">
<div>
<label for="date" class="text-dark-800 mb-3 block text-sm font-semibold">
Date <span class="text-red-400">*</span>
{t('common.date')} <span class="text-red-400">{t('common.required')}</span>
</label>
<input
id="date"
@@ -132,7 +135,7 @@
<div>
<label for="time" class="text-dark-800 mb-3 block text-sm font-semibold">
Time <span class="text-red-400">*</span>
{t('common.time')} <span class="text-red-400">{t('common.required')}</span>
</label>
<input
id="time"
@@ -151,7 +154,7 @@
<!-- Location -->
<div>
<label for="location" class="text-dark-800 mb-3 block text-sm font-semibold">
Location <span class="text-red-400">*</span>
{t('common.location')} <span class="text-red-400">{t('common.required')}</span>
</label>
<input
id="location"
@@ -159,7 +162,7 @@
type="text"
bind:value={eventData.location}
class="border-dark-300 placeholder-dark-500 w-full rounded-sm border-2 px-4 py-3 text-slate-900 shadow-sm transition-all"
placeholder="Enter location"
placeholder={t('common.enterLocation')}
maxlength="200"
required
/>
@@ -172,7 +175,7 @@
<div>
<fieldset>
<legend class="text-dark-800 mb-3 block text-sm font-semibold">
Type <span class="text-red-400">*</span>
{t('common.type')} <span class="text-red-400">{t('common.required')}</span>
</legend>
<div class="grid grid-cols-2 gap-3">
<button
@@ -183,7 +186,7 @@
: 'border-dark-300 text-dark-700'}"
on:click={() => handleTypeChange('unlimited')}
>
Unlimited
{t('common.unlimited')}
</button>
<button
type="button"
@@ -193,7 +196,7 @@
: 'border-dark-300 text-dark-700 bg-gray-600/20 hover:bg-gray-600/70'}"
on:click={() => handleTypeChange('limited')}
>
Limited
{t('common.limited')}
</button>
</div>
</fieldset>
@@ -203,7 +206,7 @@
{#if eventData.type === 'limited'}
<div>
<label for="limit" class="text-dark-800 mb-3 block text-sm font-semibold">
Attendee Limit *
{t('common.attendeeLimit')} <span class="text-red-400">{t('common.required')}</span>
</label>
<input
id="attendee_limit"
@@ -213,7 +216,7 @@
min="1"
max="1000"
class="border-dark-300 w-full rounded-sm border-2 bg-white px-4 py-3 text-slate-900 shadow-sm transition-all duration-200"
placeholder="Enter limit"
placeholder={t('common.enterLimit')}
required
/>
{#if errors.attendee_limit}
@@ -226,7 +229,7 @@
<div>
<fieldset>
<legend class="text-dark-800 mb-3 block text-sm font-semibold">
Visibility <span class="text-red-400">*</span>
{t('common.visibility')} <span class="text-red-400">{t('common.required')}</span>
</legend>
<div class="grid grid-cols-2 gap-3">
<button
@@ -237,7 +240,7 @@
: 'border-dark-300 text-dark-700'}"
on:click={() => (eventData.visibility = 'public')}
>
🌍 Public
{t('create.publicOption')}
</button>
<button
type="button"
@@ -247,13 +250,13 @@
: 'border-dark-300 text-dark-700 bg-gray-600/20 hover:bg-gray-600/70'}"
on:click={() => (eventData.visibility = 'private')}
>
🔒 Private
{t('create.privateOption')}
</button>
</div>
<p class="mt-2 text-xs text-slate-400">
{eventData.visibility === 'public'
? 'Public events are visible to everyone and can be discovered by others'
: 'Private events are only visible to you and people you share the link with'}
? t('create.publicDescription')
: t('create.privateDescription')}
</p>
</fieldset>
</div>
@@ -265,7 +268,7 @@
on:click={handleCancel}
class="flex-1 rounded-sm border-2 border-slate-300 bg-slate-200 px-4 py-3 font-semibold text-slate-700 transition-all duration-200 hover:bg-slate-400 hover:text-slate-200"
>
Cancel
{t('common.cancel')}
</button>
<button
type="submit"
@@ -275,10 +278,10 @@
{#if isSubmitting}
<div class="flex items-center justify-center">
<div class="mr-2 h-5 w-5 animate-spin rounded-full border-b-2 border-white"></div>
Updating...
{t('event.updatingEvent')}
</div>
{:else}
Update Event
{t('event.updateEventButton')}
{/if}
</button>
</div>