feat: add option to link Google Maps to events

This commit is contained in:
Levente Orban
2025-09-24 21:15:31 +02:00
parent 69a760d3f1
commit cc3c868f7d
14 changed files with 353 additions and 93 deletions

View File

@@ -24,6 +24,8 @@ export const load = async ({ cookies }) => {
date: event.date,
time: event.time,
location: event.location,
location_type: event.locationType,
location_url: event.locationUrl,
type: event.type,
attendee_limit: event.attendeeLimit,
visibility: event.visibility,

View File

@@ -126,7 +126,18 @@
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
></path>
</svg>
<span>{event.location}</span>
{#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"
>
{event.location}
</a>
{:else}
<span>{event.location}</span>
{/if}
</div>
<div class="flex items-center space-x-2">
<span

View File

@@ -32,6 +32,8 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
date: event.date,
time: event.time,
location: event.location,
location_type: event.locationType,
location_url: event.locationUrl,
type: event.type,
attendee_limit: event.attendeeLimit,
visibility: event.visibility,

View File

@@ -155,7 +155,18 @@
</svg>
</div>
<div>
<p class="font-semibold text-white">{event.location}</p>
{#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"
>
{event.location}
</a>
{:else}
<p class="font-semibold text-white">{event.location}</p>
{/if}
</div>
</div>

View File

@@ -53,6 +53,8 @@ export const actions: Actions = {
const date = formData.get('date') as string;
const time = formData.get('time') as string;
const location = formData.get('location') as string;
const locationType = formData.get('location_type') as 'text' | 'maps';
const locationUrl = formData.get('location_url') as string;
const type = formData.get('type') as 'limited' | 'unlimited';
const attendeeLimit = formData.get('attendee_limit') as string;
const visibility = formData.get('visibility') as 'public' | 'private';
@@ -64,6 +66,8 @@ export const actions: Actions = {
if (!date) missingFields.push('date');
if (!time) missingFields.push('time');
if (!location?.trim()) missingFields.push('location');
if (!locationType) missingFields.push('location_type');
if (locationType === 'maps' && !locationUrl?.trim()) missingFields.push('location_url');
if (missingFields.length > 0) {
return fail(400, {
@@ -73,6 +77,8 @@ export const actions: Actions = {
date,
time,
location,
location_type: locationType,
location_url: locationUrl,
type,
attendee_limit: attendeeLimit,
visibility
@@ -88,14 +94,34 @@ export const actions: Actions = {
if (eventDate < today) {
return fail(400, {
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)) {
return fail(400, {
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
}
});
}
@@ -107,6 +133,8 @@ export const actions: Actions = {
date: date,
time: time,
location: location.trim(),
locationType: locationType,
locationUrl: locationType === 'maps' ? locationUrl?.trim() : null,
type: type,
attendeeLimit: type === 'limited' ? parseInt(attendeeLimit) : null,
visibility: visibility,

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import type { EventType } from '$lib/types';
import type { EventType, LocationType } from '$lib/types';
import { enhance } from '$app/forms';
import { goto } from '$app/navigation';
import { t } from '$lib/i18n/i18n.js';
@@ -12,6 +12,8 @@
date: data.event.date,
time: data.event.time,
location: data.event.location,
location_type: data.event.locationType || 'text',
location_url: data.event.locationUrl || '',
type: data.event.type,
attendee_limit: data.event.attendeeLimit,
visibility: data.event.visibility
@@ -49,6 +51,16 @@
}
};
const handleLocationTypeChange = (locationType: LocationType) => {
eventData.location_type = locationType;
if (locationType === 'text') {
eventData.location_url = '';
eventData.location = '';
} else {
eventData.location = 'Google Maps';
}
};
const handleCancel = () => {
goto(`/event/${data.event.id}`);
};
@@ -86,9 +98,6 @@
}}
class="space-y-6"
>
<input type="hidden" name="type" value={eventData.type} />
<input type="hidden" name="visibility" value={eventData.visibility} />
{#if errors.server}
<div class="mb-6 rounded-sm border border-red-200 bg-red-50 p-4 text-red-700">
{errors.server}
@@ -153,24 +162,80 @@
</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-2 gap-3">
<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 === 'text'
? t('create.locationTextDescription')
: t('create.locationMapsDescription')}
</p>
</fieldset>
</div>
<!-- Location Input -->
<div>
<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.locationTypeLabel')
: t('create.googleMapsUrlLabel')}
<span class="text-red-400">{t('common.required')}</span>
</label>
<input
id="location"
name="location"
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={t('common.enterLocation')}
maxlength="200"
required
/>
{#if eventData.location_type === 'text'}
<input
id="location"
name="location"
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={t('create.locationPlaceholder')}
maxlength="200"
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}
<p class="mt-2 text-sm font-medium text-red-600">{errors.location}</p>
{/if}
{#if errors.location_url}
<p class="mt-2 text-sm font-medium text-red-600">{errors.location_url}</p>
{/if}
</div>
<!-- Event Type -->