forked from jmug/cactoide
feat: Add translation support
This commit is contained in:
@@ -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