mirror of
https://github.com/polaroi8d/cactoide.git
synced 2026-03-22 14:15:28 +00:00
Compare commits
8 Commits
feat/trans
...
fix/locati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26824eb3a8 | ||
|
|
d2024d31ba | ||
|
|
cc3c868f7d | ||
|
|
69a760d3f1 | ||
|
|
a59bc3601c | ||
|
|
b64d48a933 | ||
|
|
1a5eff4dbd | ||
|
|
1bbc4b590f |
@@ -21,6 +21,7 @@ A mobile-first event RSVP platform that lets you create events, share unique URL
|
|||||||
- **🎯 Instant Event Creation** - Create events in seconds with our streamlined form. No accounts, no waiting, just pure efficiency.
|
- **🎯 Instant Event Creation** - Create events in seconds with our streamlined form. No accounts, no waiting, just pure efficiency.
|
||||||
- **🔗 One-Click Sharing** - Each event gets a unique, memorable URL. Share instantly via any platform or messaging app.
|
- **🔗 One-Click Sharing** - Each event gets a unique, memorable URL. Share instantly via any platform or messaging app.
|
||||||
- **🔍 All-in-One Clarity** - No more scrolling through endless chats and reactions. See everyone's availability and responses neatly in one place.
|
- **🔍 All-in-One Clarity** - No more scrolling through endless chats and reactions. See everyone's availability and responses neatly in one place.
|
||||||
|
- **📅 iCal Integration** - One-tap add-to-calendar via ICS/webcal links. Works with Apple Calendar, Google Calendar, and Outlook, with automatic time zone handling.
|
||||||
- **👤 No Hassle, No Sign-Ups** - Skip registrations and endless forms. Unlike other event platforms, you create and share instantly — no accounts, no barriers.
|
- **👤 No Hassle, No Sign-Ups** - Skip registrations and endless forms. Unlike other event platforms, you create and share instantly — no accounts, no barriers.
|
||||||
- **🛡️ Smart Limits** - Choose between unlimited RSVPs or set a limited capacity. Perfect for any event size.
|
- **🛡️ Smart Limits** - Choose between unlimited RSVPs or set a limited capacity. Perfect for any event size.
|
||||||
- **✨ Effortless Simplicity** - Designed to be instantly clear and easy. No learning curve — just open, create, and go.
|
- **✨ Effortless Simplicity** - Designed to be instantly clear and easy. No learning curve — just open, create, and go.
|
||||||
@@ -29,7 +30,7 @@ A mobile-first event RSVP platform that lets you create events, share unique URL
|
|||||||
|
|
||||||
#### Requirements
|
#### Requirements
|
||||||
|
|
||||||
`git, docker, docker-compose, node at least suggested 20.19.0`
|
`git`, `docker`, `docker-compose`, `node` at least suggested 20.19.0
|
||||||
|
|
||||||
Uses the [`docker-compose.yml`](docker-compose.yml) file to setup the application with the database. You can define all ENV variables in the [`.env`](.env.example) file from the `.env.example`.
|
Uses the [`docker-compose.yml`](docker-compose.yml) file to setup the application with the database. You can define all ENV variables in the [`.env`](.env.example) file from the `.env.example`.
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ CREATE TABLE IF NOT EXISTS events (
|
|||||||
date DATE NOT NULL,
|
date DATE NOT NULL,
|
||||||
time TIME NOT NULL,
|
time TIME NOT NULL,
|
||||||
location VARCHAR(200) NOT NULL,
|
location VARCHAR(200) NOT NULL,
|
||||||
|
location_type VARCHAR(20) NOT NULL DEFAULT 'none' CHECK (location_type IN ('none','text','maps')),
|
||||||
|
location_url VARCHAR(500),
|
||||||
type VARCHAR(20) NOT NULL CHECK (type IN ('limited','unlimited')),
|
type VARCHAR(20) NOT NULL CHECK (type IN ('limited','unlimited')),
|
||||||
attendee_limit INTEGER CHECK (attendee_limit > 0),
|
attendee_limit INTEGER CHECK (attendee_limit > 0),
|
||||||
user_id VARCHAR(100) NOT NULL,
|
user_id VARCHAR(100) NOT NULL,
|
||||||
@@ -37,6 +39,7 @@ CREATE TABLE IF NOT EXISTS rsvps (
|
|||||||
-- =======================================
|
-- =======================================
|
||||||
CREATE INDEX IF NOT EXISTS idx_events_user_id ON events(user_id);
|
CREATE INDEX IF NOT EXISTS idx_events_user_id ON events(user_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_events_date ON events(date);
|
CREATE INDEX IF NOT EXISTS idx_events_date ON events(date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_events_location_type ON events(location_type);
|
||||||
CREATE INDEX IF NOT EXISTS idx_rsvps_event_id ON rsvps(event_id);
|
CREATE INDEX IF NOT EXISTS idx_rsvps_event_id ON rsvps(event_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_rsvps_user_id ON rsvps(user_id);
|
CREATE INDEX IF NOT EXISTS idx_rsvps_user_id ON rsvps(user_id);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type { InferInsertModel, InferSelectModel } from 'drizzle-orm';
|
|||||||
// --- Enums (matching the SQL CHECK constraints)
|
// --- Enums (matching the SQL CHECK constraints)
|
||||||
export const eventTypeEnum = pgEnum('event_type', ['limited', 'unlimited']);
|
export const eventTypeEnum = pgEnum('event_type', ['limited', 'unlimited']);
|
||||||
export const visibilityEnum = pgEnum('visibility', ['public', 'private']);
|
export const visibilityEnum = pgEnum('visibility', ['public', 'private']);
|
||||||
|
export const locationTypeEnum = pgEnum('location_type', ['none', 'text', 'maps']);
|
||||||
|
|
||||||
// --- Events table
|
// --- Events table
|
||||||
export const events = pgTable(
|
export const events = pgTable(
|
||||||
@@ -27,6 +28,8 @@ export const events = pgTable(
|
|||||||
date: date('date', { mode: 'string' }).notNull(), // ISO 'YYYY-MM-DD'
|
date: date('date', { mode: 'string' }).notNull(), // ISO 'YYYY-MM-DD'
|
||||||
time: time('time', { withTimezone: false }).notNull(), // 'HH:MM:SS'
|
time: time('time', { withTimezone: false }).notNull(), // 'HH:MM:SS'
|
||||||
location: varchar('location', { length: 200 }).notNull(),
|
location: varchar('location', { length: 200 }).notNull(),
|
||||||
|
locationType: locationTypeEnum('location_type').notNull().default('none'),
|
||||||
|
locationUrl: varchar('location_url', { length: 500 }),
|
||||||
type: eventTypeEnum('type').notNull(),
|
type: eventTypeEnum('type').notNull(),
|
||||||
attendeeLimit: integer('attendee_limit'), // nullable in SQL
|
attendeeLimit: integer('attendee_limit'), // nullable in SQL
|
||||||
userId: varchar('user_id', { length: 100 }).notNull(),
|
userId: varchar('user_id', { length: 100 }).notNull(),
|
||||||
|
|||||||
@@ -14,6 +14,15 @@
|
|||||||
"date": "Date",
|
"date": "Date",
|
||||||
"time": "Time",
|
"time": "Time",
|
||||||
"location": "Location",
|
"location": "Location",
|
||||||
|
"locationType": "Location Type",
|
||||||
|
"locationNone": "None",
|
||||||
|
"locationText": "Text",
|
||||||
|
"locationMaps": "Google Maps",
|
||||||
|
"locationNoneDescription": "No location specified",
|
||||||
|
"locationTextDescription": "Enter location as plain text.",
|
||||||
|
"locationMapsDescription": "Enter Google Maps link.",
|
||||||
|
"googleMapsUrl": "Google Maps URL",
|
||||||
|
"googleMapsUrlPlaceholder": "https://maps.google.com/...",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"visibility": "Visibility",
|
"visibility": "Visibility",
|
||||||
"public": "Public",
|
"public": "Public",
|
||||||
@@ -134,6 +143,15 @@
|
|||||||
"timeLabel": "Time",
|
"timeLabel": "Time",
|
||||||
"locationLabel": "Location",
|
"locationLabel": "Location",
|
||||||
"locationPlaceholder": "Enter location",
|
"locationPlaceholder": "Enter location",
|
||||||
|
"locationTypeLabel": "Location Type",
|
||||||
|
"locationNoneOption": "None",
|
||||||
|
"locationTextOption": "Plain Text",
|
||||||
|
"locationMapsOption": "Google Maps",
|
||||||
|
"locationNoneDescription": "No location specified.",
|
||||||
|
"locationTextDescription": "Enter location as plain text.",
|
||||||
|
"locationMapsDescription": "Enter Google Maps link.",
|
||||||
|
"googleMapsUrlLabel": "Google Maps URL",
|
||||||
|
"googleMapsUrlPlaceholder": "https://maps.google.com/...",
|
||||||
"typeLabel": "Type",
|
"typeLabel": "Type",
|
||||||
"unlimitedOption": "Unlimited",
|
"unlimitedOption": "Unlimited",
|
||||||
"limitedOption": "Limited",
|
"limitedOption": "Limited",
|
||||||
@@ -142,8 +160,8 @@
|
|||||||
"visibilityLabel": "Visibility",
|
"visibilityLabel": "Visibility",
|
||||||
"publicOption": "🌍 Public",
|
"publicOption": "🌍 Public",
|
||||||
"privateOption": "🔒 Private",
|
"privateOption": "🔒 Private",
|
||||||
"publicDescription": "Public events are visible to everyone and can be discovered by others",
|
"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",
|
"privateDescription": "Private events are only visible to you and people you share the link with.",
|
||||||
"creatingEvent": "Creating Event...",
|
"creatingEvent": "Creating Event...",
|
||||||
"createEventButton": "Create Event"
|
"createEventButton": "Create Event"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export type EventType = 'limited' | 'unlimited';
|
export type EventType = 'limited' | 'unlimited';
|
||||||
export type EventVisibility = 'public' | 'private';
|
export type EventVisibility = 'public' | 'private';
|
||||||
export type ActionType = 'add' | 'remove';
|
export type ActionType = 'add' | 'remove';
|
||||||
|
export type LocationType = 'none' | 'text' | 'maps';
|
||||||
|
|
||||||
export interface Event {
|
export interface Event {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -8,6 +9,8 @@ export interface Event {
|
|||||||
date: string;
|
date: string;
|
||||||
time: string;
|
time: string;
|
||||||
location: string;
|
location: string;
|
||||||
|
location_type: LocationType;
|
||||||
|
location_url?: string;
|
||||||
type: EventType;
|
type: EventType;
|
||||||
attendee_limit?: number;
|
attendee_limit?: number;
|
||||||
visibility: EventVisibility;
|
visibility: EventVisibility;
|
||||||
@@ -29,6 +32,8 @@ export interface CreateEventData {
|
|||||||
date: string;
|
date: string;
|
||||||
time: string;
|
time: string;
|
||||||
location: string;
|
location: string;
|
||||||
|
location_type: LocationType;
|
||||||
|
location_url?: string;
|
||||||
type: EventType;
|
type: EventType;
|
||||||
attendee_limit?: number;
|
attendee_limit?: number;
|
||||||
visibility: EventVisibility;
|
visibility: EventVisibility;
|
||||||
@@ -40,6 +45,8 @@ export interface DatabaseEvent {
|
|||||||
date: string;
|
date: string;
|
||||||
time: string;
|
time: string;
|
||||||
location: string;
|
location: string;
|
||||||
|
location_type: LocationType;
|
||||||
|
location_url?: string;
|
||||||
type: EventType;
|
type: EventType;
|
||||||
attendee_limit?: number;
|
attendee_limit?: number;
|
||||||
visibility: EventVisibility;
|
visibility: EventVisibility;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<h2 class="mt-6 pt-8 text-xl md:text-2xl">
|
<h2 class="mt-6 pt-8 text-xl md:text-2xl">
|
||||||
{t('home.whyCactoideTitle')}<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
|
><a href="https://en.wikipedia.org/wiki/Cactoideae" target="_blank">*</a></span
|
||||||
>?🌵
|
>
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-4 text-lg md:text-xl">
|
<p class="mt-4 text-lg md:text-xl">
|
||||||
{t('home.whyCactoideDescription')}
|
{t('home.whyCactoideDescription')}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export const actions: Actions = {
|
|||||||
const date = formData.get('date') as string;
|
const date = formData.get('date') as string;
|
||||||
const time = formData.get('time') as string;
|
const time = formData.get('time') as string;
|
||||||
const location = formData.get('location') as string;
|
const location = formData.get('location') as string;
|
||||||
|
const locationType = formData.get('location_type') as 'none' | 'text' | 'maps';
|
||||||
|
const locationUrl = formData.get('location_url') as string;
|
||||||
const type = formData.get('type') as 'limited' | 'unlimited';
|
const type = formData.get('type') as 'limited' | 'unlimited';
|
||||||
const attendeeLimit = formData.get('attendee_limit') as string;
|
const attendeeLimit = formData.get('attendee_limit') as string;
|
||||||
const visibility = formData.get('visibility') as 'public' | 'private';
|
const visibility = formData.get('visibility') as 'public' | 'private';
|
||||||
@@ -32,7 +34,9 @@ export const actions: Actions = {
|
|||||||
if (!name?.trim()) missingFields.push('name');
|
if (!name?.trim()) missingFields.push('name');
|
||||||
if (!date) missingFields.push('date');
|
if (!date) missingFields.push('date');
|
||||||
if (!time) missingFields.push('time');
|
if (!time) missingFields.push('time');
|
||||||
if (!location?.trim()) missingFields.push('location');
|
if (!locationType) missingFields.push('location_type');
|
||||||
|
if (locationType === 'text' && !location?.trim()) missingFields.push('location');
|
||||||
|
if (locationType === 'maps' && !locationUrl?.trim()) missingFields.push('location_url');
|
||||||
if (!userId) missingFields.push('userId');
|
if (!userId) missingFields.push('userId');
|
||||||
|
|
||||||
if (missingFields.length > 0) {
|
if (missingFields.length > 0) {
|
||||||
@@ -43,6 +47,8 @@ export const actions: Actions = {
|
|||||||
date,
|
date,
|
||||||
time,
|
time,
|
||||||
location,
|
location,
|
||||||
|
location_type: locationType,
|
||||||
|
location_url: locationUrl,
|
||||||
type,
|
type,
|
||||||
attendee_limit: attendeeLimit,
|
attendee_limit: attendeeLimit,
|
||||||
visibility
|
visibility
|
||||||
@@ -53,14 +59,34 @@ export const actions: Actions = {
|
|||||||
if (new Date(date) < new Date()) {
|
if (new Date(date) < new Date()) {
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
error: 'Date cannot be in the past.',
|
error: 'Date cannot be in the past.',
|
||||||
values: { name, date, time, location, type, attendee_limit: attendeeLimit, visibility }
|
values: {
|
||||||
|
name,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
location,
|
||||||
|
location_type: locationType,
|
||||||
|
location_url: locationUrl,
|
||||||
|
type,
|
||||||
|
attendee_limit: attendeeLimit,
|
||||||
|
visibility
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'limited' && (!attendeeLimit || parseInt(attendeeLimit) < 2)) {
|
if (type === 'limited' && (!attendeeLimit || parseInt(attendeeLimit) < 2)) {
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
error: 'Limit must be at least 2 for limited events.',
|
error: 'Limit must be at least 2 for limited events.',
|
||||||
values: { name, date, time, location, type, attendee_limit: attendeeLimit, visibility }
|
values: {
|
||||||
|
name,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
location,
|
||||||
|
location_type: locationType,
|
||||||
|
location_url: locationUrl,
|
||||||
|
type,
|
||||||
|
attendee_limit: attendeeLimit,
|
||||||
|
visibility
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +99,9 @@ export const actions: Actions = {
|
|||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
date: date,
|
date: date,
|
||||||
time: time,
|
time: time,
|
||||||
location: location.trim(),
|
location: location?.trim() || '',
|
||||||
|
locationType: locationType,
|
||||||
|
locationUrl: locationType === 'maps' ? locationUrl?.trim() : null,
|
||||||
type: type,
|
type: type,
|
||||||
attendeeLimit: type === 'limited' ? parseInt(attendeeLimit) : null,
|
attendeeLimit: type === 'limited' ? parseInt(attendeeLimit) : null,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { CreateEventData, EventType } from '$lib/types';
|
import type { CreateEventData, EventType, LocationType } from '$lib/types';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { t } from '$lib/i18n/i18n.js';
|
import { t } from '$lib/i18n/i18n.js';
|
||||||
@@ -11,6 +11,8 @@
|
|||||||
date: '',
|
date: '',
|
||||||
time: '',
|
time: '',
|
||||||
location: '',
|
location: '',
|
||||||
|
location_type: 'none',
|
||||||
|
location_url: '',
|
||||||
type: 'unlimited',
|
type: 'unlimited',
|
||||||
attendee_limit: undefined,
|
attendee_limit: undefined,
|
||||||
visibility: 'public'
|
visibility: 'public'
|
||||||
@@ -46,6 +48,19 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLocationTypeChange = (locationType: LocationType) => {
|
||||||
|
eventData.location_type = locationType;
|
||||||
|
if (locationType === 'none') {
|
||||||
|
eventData.location = '';
|
||||||
|
eventData.location_url = '';
|
||||||
|
} else if (locationType === 'text') {
|
||||||
|
eventData.location_url = '';
|
||||||
|
eventData.location = '';
|
||||||
|
} else {
|
||||||
|
eventData.location = 'Google Maps';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
goto(`/discover`);
|
goto(`/discover`);
|
||||||
};
|
};
|
||||||
@@ -58,7 +73,7 @@
|
|||||||
<div class="flex min-h-screen flex-col">
|
<div class="flex min-h-screen flex-col">
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="container mx-auto flex-1 px-4 py-8">
|
<div class="container mx-auto flex-1 px-4 py-8">
|
||||||
<div class="mx-auto max-w-md">
|
<div class="mx-auto max-w-2xl">
|
||||||
<!-- Event Creation Form -->
|
<!-- Event Creation Form -->
|
||||||
<div class="rounded-sm border p-8">
|
<div class="rounded-sm border p-8">
|
||||||
<h2 class="mb-8 text-center text-3xl font-bold text-violet-400">{t('create.formTitle')}</h2>
|
<h2 class="mb-8 text-center text-3xl font-bold text-violet-400">{t('create.formTitle')}</h2>
|
||||||
@@ -83,6 +98,7 @@
|
|||||||
<input type="hidden" name="userId" value={currentUserId} />
|
<input type="hidden" name="userId" value={currentUserId} />
|
||||||
<input type="hidden" name="type" value={eventData.type} />
|
<input type="hidden" name="type" value={eventData.type} />
|
||||||
<input type="hidden" name="visibility" value={eventData.visibility} />
|
<input type="hidden" name="visibility" value={eventData.visibility} />
|
||||||
|
<input type="hidden" name="location_type" value={eventData.location_type} />
|
||||||
|
|
||||||
{#if errors.server}
|
{#if errors.server}
|
||||||
<div class="mb-6 rounded-sm border border-red-200 bg-red-50 p-4 text-red-700">
|
<div class="mb-6 rounded-sm border border-red-200 bg-red-50 p-4 text-red-700">
|
||||||
@@ -148,11 +164,65 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Location -->
|
<!-- Location Type -->
|
||||||
|
<div>
|
||||||
|
<fieldset>
|
||||||
|
<legend class="text-dark-800 mb-3 block text-sm font-semibold">
|
||||||
|
{t('create.locationTypeLabel')}
|
||||||
|
<span class="text-red-400">{t('common.required')}</span>
|
||||||
|
</legend>
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.location_type ===
|
||||||
|
'none'
|
||||||
|
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
||||||
|
: 'border-dark-300 text-dark-700'}"
|
||||||
|
on:click={() => handleLocationTypeChange('none')}
|
||||||
|
>
|
||||||
|
{t('create.locationNoneOption')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.location_type ===
|
||||||
|
'text'
|
||||||
|
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
||||||
|
: 'border-dark-300 text-dark-700'}"
|
||||||
|
on:click={() => handleLocationTypeChange('text')}
|
||||||
|
>
|
||||||
|
{t('create.locationTextOption')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.location_type ===
|
||||||
|
'maps'
|
||||||
|
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
||||||
|
: 'border-dark-300 text-dark-700 bg-gray-600/20 hover:bg-gray-600/70'}"
|
||||||
|
on:click={() => handleLocationTypeChange('maps')}
|
||||||
|
>
|
||||||
|
{t('create.locationMapsOption')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-slate-400 italic">
|
||||||
|
{eventData.location_type === 'none'
|
||||||
|
? t('create.locationNoneDescription')
|
||||||
|
: eventData.location_type === 'text'
|
||||||
|
? t('create.locationTextDescription')
|
||||||
|
: t('create.locationMapsDescription')}
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Location Input (only show when not 'none') -->
|
||||||
|
{#if eventData.location_type !== 'none'}
|
||||||
<div>
|
<div>
|
||||||
<label for="location" class="text-dark-800 mb-3 block text-sm font-semibold">
|
<label for="location" class="text-dark-800 mb-3 block text-sm font-semibold">
|
||||||
{t('create.locationLabel')} <span class="text-red-400">{t('common.required')}</span>
|
{eventData.location_type === 'text'
|
||||||
|
? t('create.locationLabel')
|
||||||
|
: t('create.googleMapsUrlLabel')}
|
||||||
|
<span class="text-red-400">{t('common.required')}</span>
|
||||||
</label>
|
</label>
|
||||||
|
{#if eventData.location_type === 'text'}
|
||||||
<input
|
<input
|
||||||
id="location"
|
id="location"
|
||||||
name="location"
|
name="location"
|
||||||
@@ -163,17 +233,34 @@
|
|||||||
maxlength="200"
|
maxlength="200"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
id="location_url"
|
||||||
|
name="location_url"
|
||||||
|
type="url"
|
||||||
|
bind:value={eventData.location_url}
|
||||||
|
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={t('create.googleMapsUrlPlaceholder')}
|
||||||
|
maxlength="500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{#if errors.location}
|
{#if errors.location}
|
||||||
<p class="mt-2 text-sm font-medium text-red-600">{errors.location}</p>
|
<p class="mt-2 text-sm font-medium text-red-600">{errors.location}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if errors.location_url}
|
||||||
|
<p class="mt-2 text-sm font-medium text-red-600">{errors.location_url}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Event Type -->
|
<!-- Event Type -->
|
||||||
<div>
|
<div>
|
||||||
<label class="text-dark-800 mb-3 block text-sm font-semibold">
|
<fieldset>
|
||||||
|
<legend class="text-dark-800 mb-3 block text-sm font-semibold">
|
||||||
{t('create.typeLabel')}
|
{t('create.typeLabel')}
|
||||||
<span class="text-red-400">{t('common.required')}</span></label
|
<span class="text-red-400">{t('common.required')}</span>
|
||||||
>
|
</legend>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -196,6 +283,7 @@
|
|||||||
{t('create.limitedOption')}
|
{t('create.limitedOption')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Limit (only for limited events) -->
|
<!-- Limit (only for limited events) -->
|
||||||
@@ -203,7 +291,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<label for="limit" class="text-dark-800 mb-3 block text-sm font-semibold">
|
<label for="limit" class="text-dark-800 mb-3 block text-sm font-semibold">
|
||||||
{t('create.attendeeLimitLabel')}
|
{t('create.attendeeLimitLabel')}
|
||||||
{t('common.required')}
|
<span class="text-red-400">{t('common.required')}</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="attendee_limit"
|
id="attendee_limit"
|
||||||
@@ -224,10 +312,11 @@
|
|||||||
|
|
||||||
<!-- Event Visibility -->
|
<!-- Event Visibility -->
|
||||||
<div>
|
<div>
|
||||||
<label class="text-dark-800 mb-3 block text-sm font-semibold">
|
<fieldset>
|
||||||
|
<legend class="text-dark-800 mb-3 block text-sm font-semibold">
|
||||||
{t('create.visibilityLabel')}
|
{t('create.visibilityLabel')}
|
||||||
<span class="text-red-400">{t('common.required')}</span></label
|
<span class="text-red-400">{t('common.required')}</span>
|
||||||
>
|
</legend>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -250,11 +339,12 @@
|
|||||||
{t('create.privateOption')}
|
{t('create.privateOption')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-xs text-slate-400">
|
<p class="mt-2 text-xs text-slate-400 italic">
|
||||||
{eventData.visibility === 'public'
|
{eventData.visibility === 'public'
|
||||||
? t('create.publicDescription')
|
? t('create.publicDescription')
|
||||||
: t('create.privateDescription')}
|
: t('create.privateDescription')}
|
||||||
</p>
|
</p>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex space-x-3">
|
<div class="flex space-x-3">
|
||||||
@@ -269,7 +359,7 @@
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
class="hover:bg-violet-400/70'l 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"
|
class="hover:bg-violet-400/70'l flex-2 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"
|
||||||
>
|
>
|
||||||
{#if isSubmitting}
|
{#if isSubmitting}
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export const load: PageServerLoad = async () => {
|
|||||||
date: event.date, // Already in 'YYYY-MM-DD' format
|
date: event.date, // Already in 'YYYY-MM-DD' format
|
||||||
time: event.time, // Already in 'HH:MM:SS' format
|
time: event.time, // Already in 'HH:MM:SS' format
|
||||||
location: event.location,
|
location: event.location,
|
||||||
|
location_type: event.locationType,
|
||||||
|
location_url: event.locationUrl,
|
||||||
type: event.type,
|
type: event.type,
|
||||||
attendee_limit: event.attendeeLimit, // Note: schema uses camelCase
|
attendee_limit: event.attendeeLimit, // Note: schema uses camelCase
|
||||||
visibility: event.visibility,
|
visibility: event.visibility,
|
||||||
|
|||||||
@@ -289,7 +289,20 @@
|
|||||||
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
{#if event.location_type === 'none'}
|
||||||
|
<span>N/A</span>
|
||||||
|
{:else if event.location_type === 'maps' && event.location_url}
|
||||||
|
<a
|
||||||
|
href={event.location_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-slate-500 transition-colors duration-200 hover:text-slate-300"
|
||||||
|
>
|
||||||
|
{t('create.locationMapsOption')}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
<span>{event.location}</span>
|
<span>{event.location}</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export const load = async ({ cookies }) => {
|
|||||||
date: event.date,
|
date: event.date,
|
||||||
time: event.time,
|
time: event.time,
|
||||||
location: event.location,
|
location: event.location,
|
||||||
|
location_type: event.locationType,
|
||||||
|
location_url: event.locationUrl,
|
||||||
type: event.type,
|
type: event.type,
|
||||||
attendee_limit: event.attendeeLimit,
|
attendee_limit: event.attendeeLimit,
|
||||||
visibility: event.visibility,
|
visibility: event.visibility,
|
||||||
|
|||||||
@@ -126,7 +126,20 @@
|
|||||||
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
{#if event.location_type === 'none'}
|
||||||
|
<span>N/A</span>
|
||||||
|
{:else if event.location_type === 'maps' && event.location_url}
|
||||||
|
<a
|
||||||
|
href={event.location_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-slate-500 transition-colors duration-200 hover:text-slate-300"
|
||||||
|
>
|
||||||
|
Google Maps
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
<span>{event.location}</span>
|
<span>{event.location}</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
|
|||||||
date: event.date,
|
date: event.date,
|
||||||
time: event.time,
|
time: event.time,
|
||||||
location: event.location,
|
location: event.location,
|
||||||
|
location_type: event.locationType,
|
||||||
|
location_url: event.locationUrl,
|
||||||
type: event.type,
|
type: event.type,
|
||||||
attendee_limit: event.attendeeLimit,
|
attendee_limit: event.attendeeLimit,
|
||||||
visibility: event.visibility,
|
visibility: event.visibility,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
import type { Event, RSVP } from '$lib/types';
|
import type { Event, RSVP } from '$lib/types';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
@@ -28,13 +29,13 @@
|
|||||||
$: currentUserId = data.userId;
|
$: currentUserId = data.userId;
|
||||||
|
|
||||||
// Create calendar event object when event data changes
|
// Create calendar event object when event data changes
|
||||||
$: if (event) {
|
$: if (event && browser) {
|
||||||
calendarEvent = {
|
calendarEvent = {
|
||||||
name: event.name,
|
name: event.name,
|
||||||
date: event.date,
|
date: event.date,
|
||||||
time: event.time,
|
time: event.time,
|
||||||
location: event.location,
|
location: event.location,
|
||||||
url: `${window.location.origin}/event/${eventId}`
|
url: `${$page.url.origin}/event/${eventId}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,13 +57,15 @@
|
|||||||
const eventId = $page.params.id || '';
|
const eventId = $page.params.id || '';
|
||||||
|
|
||||||
const copyEventLink = () => {
|
const copyEventLink = () => {
|
||||||
const url = `${window.location.origin}/event/${eventId}`;
|
if (browser) {
|
||||||
|
const url = `${$page.url.origin}/event/${eventId}`;
|
||||||
navigator.clipboard.writeText(url).then(() => {
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
success = 'Event link copied to clipboard!';
|
success = 'Event link copied to clipboard!';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
success = '';
|
success = '';
|
||||||
}, 3000);
|
}, 3000);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearMessages = () => {
|
const clearMessages = () => {
|
||||||
@@ -103,7 +106,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if event}
|
{:else if event}
|
||||||
<div class="mx-auto max-w-md space-y-6">
|
<div class="mx-auto max-w-2xl space-y-6">
|
||||||
<!-- Event Details Card -->
|
<!-- Event Details Card -->
|
||||||
|
|
||||||
<div class="rounded-sm border p-6 shadow-2xl">
|
<div class="rounded-sm border p-6 shadow-2xl">
|
||||||
@@ -133,7 +136,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Location -->
|
<!-- Location (only show when not 'none') -->
|
||||||
<div class="flex items-center space-x-3 text-violet-400">
|
<div class="flex items-center space-x-3 text-violet-400">
|
||||||
<div class="flex h-8 w-8 items-center justify-center rounded-sm">
|
<div class="flex h-8 w-8 items-center justify-center rounded-sm">
|
||||||
<svg class=" h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class=" h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -152,7 +155,20 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
{#if event.location_type === 'none'}
|
||||||
|
<p class="font-semibold text-white">N/A</p>
|
||||||
|
{:else if event.location_type === 'maps' && event.location_url}
|
||||||
|
<a
|
||||||
|
href={event.location_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="font-semibold text-white transition-colors duration-200 hover:text-violet-300"
|
||||||
|
>
|
||||||
|
{t('create.locationMapsOption')}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
<p class="font-semibold text-white">{event.location}</p>
|
<p class="font-semibold text-white">{event.location}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -408,12 +424,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Calendar Modal -->
|
<!-- Calendar Modal -->
|
||||||
{#if calendarEvent}
|
{#if calendarEvent && browser}
|
||||||
<CalendarModal
|
<CalendarModal
|
||||||
bind:isOpen={showCalendarModal}
|
bind:isOpen={showCalendarModal}
|
||||||
event={calendarEvent}
|
event={calendarEvent}
|
||||||
{eventId}
|
{eventId}
|
||||||
baseUrl={window.location.origin}
|
baseUrl={$page.url.origin}
|
||||||
on:close={closeCalendarModal}
|
on:close={closeCalendarModal}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ export const actions: Actions = {
|
|||||||
const date = formData.get('date') as string;
|
const date = formData.get('date') as string;
|
||||||
const time = formData.get('time') as string;
|
const time = formData.get('time') as string;
|
||||||
const location = formData.get('location') as string;
|
const location = formData.get('location') as string;
|
||||||
|
const locationType = formData.get('location_type') as 'none' | 'text' | 'maps';
|
||||||
|
const locationUrl = formData.get('location_url') as string;
|
||||||
const type = formData.get('type') as 'limited' | 'unlimited';
|
const type = formData.get('type') as 'limited' | 'unlimited';
|
||||||
const attendeeLimit = formData.get('attendee_limit') as string;
|
const attendeeLimit = formData.get('attendee_limit') as string;
|
||||||
const visibility = formData.get('visibility') as 'public' | 'private';
|
const visibility = formData.get('visibility') as 'public' | 'private';
|
||||||
@@ -63,7 +65,9 @@ export const actions: Actions = {
|
|||||||
if (!name?.trim()) missingFields.push('name');
|
if (!name?.trim()) missingFields.push('name');
|
||||||
if (!date) missingFields.push('date');
|
if (!date) missingFields.push('date');
|
||||||
if (!time) missingFields.push('time');
|
if (!time) missingFields.push('time');
|
||||||
if (!location?.trim()) missingFields.push('location');
|
if (!locationType) missingFields.push('location_type');
|
||||||
|
if (locationType === 'text' && !location?.trim()) missingFields.push('location');
|
||||||
|
if (locationType === 'maps' && !locationUrl?.trim()) missingFields.push('location_url');
|
||||||
|
|
||||||
if (missingFields.length > 0) {
|
if (missingFields.length > 0) {
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
@@ -73,6 +77,8 @@ export const actions: Actions = {
|
|||||||
date,
|
date,
|
||||||
time,
|
time,
|
||||||
location,
|
location,
|
||||||
|
location_type: locationType,
|
||||||
|
location_url: locationUrl,
|
||||||
type,
|
type,
|
||||||
attendee_limit: attendeeLimit,
|
attendee_limit: attendeeLimit,
|
||||||
visibility
|
visibility
|
||||||
@@ -88,14 +94,34 @@ export const actions: Actions = {
|
|||||||
if (eventDate < today) {
|
if (eventDate < today) {
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
error: 'Date cannot be in the past.',
|
error: 'Date cannot be in the past.',
|
||||||
values: { name, date, time, location, type, attendee_limit: attendeeLimit, visibility }
|
values: {
|
||||||
|
name,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
location,
|
||||||
|
location_type: locationType,
|
||||||
|
location_url: locationUrl,
|
||||||
|
type,
|
||||||
|
attendee_limit: attendeeLimit,
|
||||||
|
visibility
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'limited' && (!attendeeLimit || parseInt(attendeeLimit) < 2)) {
|
if (type === 'limited' && (!attendeeLimit || parseInt(attendeeLimit) < 2)) {
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
error: 'Limit must be at least 2 for limited events.',
|
error: 'Limit must be at least 2 for limited events.',
|
||||||
values: { name, date, time, location, type, attendee_limit: attendeeLimit, visibility }
|
values: {
|
||||||
|
name,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
location,
|
||||||
|
location_type: locationType,
|
||||||
|
location_url: locationUrl,
|
||||||
|
type,
|
||||||
|
attendee_limit: attendeeLimit,
|
||||||
|
visibility
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +132,9 @@ export const actions: Actions = {
|
|||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
date: date,
|
date: date,
|
||||||
time: time,
|
time: time,
|
||||||
location: location.trim(),
|
location: location?.trim() || '',
|
||||||
|
locationType: locationType,
|
||||||
|
locationUrl: locationType === 'maps' ? locationUrl?.trim() : null,
|
||||||
type: type,
|
type: type,
|
||||||
attendeeLimit: type === 'limited' ? parseInt(attendeeLimit) : null,
|
attendeeLimit: type === 'limited' ? parseInt(attendeeLimit) : null,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { EventType } from '$lib/types';
|
import type { EventType, LocationType } from '$lib/types';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { t } from '$lib/i18n/i18n.js';
|
import { t } from '$lib/i18n/i18n.js';
|
||||||
@@ -12,6 +12,8 @@
|
|||||||
date: data.event.date,
|
date: data.event.date,
|
||||||
time: data.event.time,
|
time: data.event.time,
|
||||||
location: data.event.location,
|
location: data.event.location,
|
||||||
|
location_type: data.event.locationType || 'none',
|
||||||
|
location_url: data.event.locationUrl || '',
|
||||||
type: data.event.type,
|
type: data.event.type,
|
||||||
attendee_limit: data.event.attendeeLimit,
|
attendee_limit: data.event.attendeeLimit,
|
||||||
visibility: data.event.visibility
|
visibility: data.event.visibility
|
||||||
@@ -49,6 +51,19 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLocationTypeChange = (locationType: LocationType) => {
|
||||||
|
eventData.location_type = locationType;
|
||||||
|
if (locationType === 'none') {
|
||||||
|
eventData.location = '';
|
||||||
|
eventData.location_url = '';
|
||||||
|
} else if (locationType === 'text') {
|
||||||
|
eventData.location_url = '';
|
||||||
|
eventData.location = '';
|
||||||
|
} else {
|
||||||
|
eventData.location = 'Google Maps';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
goto(`/event/${data.event.id}`);
|
goto(`/event/${data.event.id}`);
|
||||||
};
|
};
|
||||||
@@ -61,7 +76,7 @@
|
|||||||
<div class="flex min-h-screen flex-col">
|
<div class="flex min-h-screen flex-col">
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="container mx-auto flex-1 px-4 py-8">
|
<div class="container mx-auto flex-1 px-4 py-8">
|
||||||
<div class="mx-auto max-w-md">
|
<div class="mx-auto max-w-2xl">
|
||||||
<!-- Event Edit Form -->
|
<!-- Event Edit Form -->
|
||||||
<div class="rounded-sm border p-8">
|
<div class="rounded-sm border p-8">
|
||||||
<div class="mb-8 text-center">
|
<div class="mb-8 text-center">
|
||||||
@@ -86,9 +101,6 @@
|
|||||||
}}
|
}}
|
||||||
class="space-y-6"
|
class="space-y-6"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="type" value={eventData.type} />
|
|
||||||
<input type="hidden" name="visibility" value={eventData.visibility} />
|
|
||||||
|
|
||||||
{#if errors.server}
|
{#if errors.server}
|
||||||
<div class="mb-6 rounded-sm border border-red-200 bg-red-50 p-4 text-red-700">
|
<div class="mb-6 rounded-sm border border-red-200 bg-red-50 p-4 text-red-700">
|
||||||
{errors.server}
|
{errors.server}
|
||||||
@@ -153,25 +165,95 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Location -->
|
<!-- Location Type -->
|
||||||
|
<div>
|
||||||
|
<fieldset>
|
||||||
|
<legend class="text-dark-800 mb-3 block text-sm font-semibold">
|
||||||
|
{t('create.locationTypeLabel')}
|
||||||
|
<span class="text-red-400">{t('common.required')}</span>
|
||||||
|
</legend>
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.location_type ===
|
||||||
|
'none'
|
||||||
|
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
||||||
|
: 'border-dark-300 text-dark-700'}"
|
||||||
|
on:click={() => handleLocationTypeChange('none')}
|
||||||
|
>
|
||||||
|
{t('create.locationNoneOption')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.location_type ===
|
||||||
|
'text'
|
||||||
|
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
||||||
|
: 'border-dark-300 text-dark-700'}"
|
||||||
|
on:click={() => handleLocationTypeChange('text')}
|
||||||
|
>
|
||||||
|
{t('create.locationTextOption')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.location_type ===
|
||||||
|
'maps'
|
||||||
|
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
||||||
|
: 'border-dark-300 text-dark-700 bg-gray-600/20 hover:bg-gray-600/70'}"
|
||||||
|
on:click={() => handleLocationTypeChange('maps')}
|
||||||
|
>
|
||||||
|
{t('create.locationMapsOption')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-slate-400">
|
||||||
|
{eventData.location_type === 'none'
|
||||||
|
? t('create.locationNoneDescription')
|
||||||
|
: eventData.location_type === 'text'
|
||||||
|
? t('create.locationTextDescription')
|
||||||
|
: t('create.locationMapsDescription')}
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Location Input (only show when not 'none') -->
|
||||||
|
{#if eventData.location_type !== 'none'}
|
||||||
<div>
|
<div>
|
||||||
<label for="location" class="text-dark-800 mb-3 block text-sm font-semibold">
|
<label for="location" class="text-dark-800 mb-3 block text-sm font-semibold">
|
||||||
{t('common.location')} <span class="text-red-400">{t('common.required')}</span>
|
{eventData.location_type === 'text'
|
||||||
|
? t('create.locationLabel')
|
||||||
|
: t('create.googleMapsUrlLabel')}
|
||||||
|
<span class="text-red-400">{t('common.required')}</span>
|
||||||
</label>
|
</label>
|
||||||
|
{#if eventData.location_type === 'text'}
|
||||||
<input
|
<input
|
||||||
id="location"
|
id="location"
|
||||||
name="location"
|
name="location"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={eventData.location}
|
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"
|
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={t('common.enterLocation')}
|
placeholder={t('create.locationPlaceholder')}
|
||||||
maxlength="200"
|
maxlength="200"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
id="location_url"
|
||||||
|
name="location_url"
|
||||||
|
type="url"
|
||||||
|
bind:value={eventData.location_url}
|
||||||
|
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={t('create.googleMapsUrlPlaceholder')}
|
||||||
|
maxlength="500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{#if errors.location}
|
{#if errors.location}
|
||||||
<p class="mt-2 text-sm font-medium text-red-600">{errors.location}</p>
|
<p class="mt-2 text-sm font-medium text-red-600">{errors.location}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if errors.location_url}
|
||||||
|
<p class="mt-2 text-sm font-medium text-red-600">{errors.location_url}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Event Type -->
|
<!-- Event Type -->
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user