forked from jmug/cactoide
feat: Add translation support
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
55
src/lib/i18n/i18n.ts
Normal 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
246
src/lib/i18n/messages.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>© 2025 Cactoide</p>
|
||||
<p>{t('layout.copyright')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -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 everyone’s 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user