mirror of
https://github.com/polaroi8d/cactoide.git
synced 2026-03-22 06:05:28 +00:00
Compare commits
9 Commits
chore/read
...
feat/i18n-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfb76aa268 | ||
|
|
f8758d7b47 | ||
|
|
f51f89e35f | ||
|
|
26824eb3a8 | ||
|
|
d2024d31ba | ||
|
|
cc3c868f7d | ||
|
|
69a760d3f1 | ||
|
|
a59bc3601c | ||
|
|
b64d48a933 |
20
Makefile
20
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: help build up db-only logs db-clean prune
|
.PHONY: help build up db-only logs db-clean prune i18n lint format
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
help:
|
help:
|
||||||
@@ -15,6 +15,7 @@ help:
|
|||||||
@echo " make logs - Show logs from all services"
|
@echo " make logs - Show logs from all services"
|
||||||
@echo " make db-clean - Stop & remove database container"
|
@echo " make db-clean - Stop & remove database container"
|
||||||
@echo " make prune - Remove all containers, images, and volumes"
|
@echo " make prune - Remove all containers, images, and volumes"
|
||||||
|
@echo " make i18n - Validate translation files against messages.json"
|
||||||
@echo " make help - Show this help message"
|
@echo " make help - Show this help message"
|
||||||
|
|
||||||
# Build the Docker images
|
# Build the Docker images
|
||||||
@@ -46,4 +47,21 @@ prune:
|
|||||||
@echo "Cleaning up all Docker resources..."
|
@echo "Cleaning up all Docker resources..."
|
||||||
docker compose down -v --rmi all
|
docker compose down -v --rmi all
|
||||||
|
|
||||||
|
# Validate translation files
|
||||||
|
i18n:
|
||||||
|
@echo "Validating translation files..."
|
||||||
|
@if [ -n "$(FILE)" ]; then \
|
||||||
|
./scripts/i18n-check.sh $(FILE); \
|
||||||
|
else \
|
||||||
|
./scripts/i18n-check.sh; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
lint:
|
||||||
|
@echo "Linting the project..."
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
format:
|
||||||
|
@echo "Formatting the project..."
|
||||||
|
npm run format
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -55,8 +55,24 @@ Your app will be available at `http://localhost:5173`. You can use the Makefile
|
|||||||
|
|
||||||
Use the `database/seed.sql` if you want to populate your database with dummy data.
|
Use the `database/seed.sql` if you want to populate your database with dummy data.
|
||||||
|
|
||||||
|
### i18n
|
||||||
|
|
||||||
|
There is no proper i18n implemented, we have an `/i18n` folder with specific languages. To use an existing translation, just rename the language code JSON file to `messages.json` and you are ready to go. If you would like to add a new translation (which is really appreciated), just create a new `<language_code>.json` file and add the translations from the `messages.json`.
|
||||||
|
|
||||||
|
The project includes a translation validation script to ensure all translation files are complete and up-to-date with the source `messages.json` file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate all translation files
|
||||||
|
make i18n
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate a specific translation file
|
||||||
|
make i18n FILE=src/lib/i18n/it.json
|
||||||
|
```
|
||||||
|
|
||||||
### License
|
### License
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
|
This project is licensed under the `AGPL-3.0 License` - see the [LICENSE](./LICENSE) file for details.
|
||||||
|
|
||||||
**Made with ❤️ by @polaroi8d**
|
**Made with ❤️ by @polaroi8d**
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
191
scripts/i18n-check.sh
Executable file
191
scripts/i18n-check.sh
Executable file
@@ -0,0 +1,191 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Translation validation script
|
||||||
|
# Compares a translation file against the source messages.json to find missing keys
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Default paths
|
||||||
|
SOURCE_FILE="src/lib/i18n/messages.json"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
# Function to show usage
|
||||||
|
show_usage() {
|
||||||
|
echo "Usage: $0 [LANGUAGE_FILE]"
|
||||||
|
echo ""
|
||||||
|
echo "Validates a translation file against the source messages.json"
|
||||||
|
echo ""
|
||||||
|
echo "Arguments:"
|
||||||
|
echo " LANGUAGE_FILE Path to the translation file to validate (e.g., src/lib/i18n/it.json)"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 src/lib/i18n/it.json"
|
||||||
|
echo " $0 src/lib/i18n/fr.json"
|
||||||
|
echo ""
|
||||||
|
echo "If no file is provided, it will check all .json files in src/lib/i18n/ except messages.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to get all keys from a JSON file recursively
|
||||||
|
get_keys() {
|
||||||
|
local file="$1"
|
||||||
|
local prefix="$2"
|
||||||
|
|
||||||
|
# Use jq to extract all keys recursively
|
||||||
|
jq -r 'paths(scalars) as $p | $p | join(".")' "$file" | while read -r key; do
|
||||||
|
if [ -n "$prefix" ]; then
|
||||||
|
echo "${prefix}.${key}"
|
||||||
|
else
|
||||||
|
echo "$key"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to validate a single translation file
|
||||||
|
validate_file() {
|
||||||
|
local translation_file="$1"
|
||||||
|
local source_file="$2"
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Validating: $translation_file${NC}"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
# Check if files exist
|
||||||
|
if [ ! -f "$source_file" ]; then
|
||||||
|
echo -e "${RED}Error: Source file $source_file not found${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$translation_file" ]; then
|
||||||
|
echo -e "${RED}Error: Translation file $translation_file not found${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get all keys from source file
|
||||||
|
local source_keys
|
||||||
|
source_keys=$(get_keys "$source_file")
|
||||||
|
|
||||||
|
# Get all keys from translation file
|
||||||
|
local translation_keys
|
||||||
|
translation_keys=$(get_keys "$translation_file")
|
||||||
|
|
||||||
|
# Find missing keys
|
||||||
|
local missing_keys
|
||||||
|
missing_keys=$(comm -23 <(echo "$source_keys" | sort) <(echo "$translation_keys" | sort))
|
||||||
|
|
||||||
|
# Find extra keys (in translation but not in source)
|
||||||
|
local extra_keys
|
||||||
|
extra_keys=$(comm -13 <(echo "$source_keys" | sort) <(echo "$translation_keys" | sort))
|
||||||
|
|
||||||
|
# Count missing and extra keys
|
||||||
|
local missing_count
|
||||||
|
if [ -z "$missing_keys" ]; then
|
||||||
|
missing_count=0
|
||||||
|
else
|
||||||
|
missing_count=$(echo "$missing_keys" | wc -l | tr -d ' ')
|
||||||
|
fi
|
||||||
|
|
||||||
|
local extra_count
|
||||||
|
if [ -z "$extra_keys" ]; then
|
||||||
|
extra_count=0
|
||||||
|
else
|
||||||
|
extra_count=$(echo "$extra_keys" | wc -l | tr -d ' ')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Report results
|
||||||
|
if [ "$missing_count" -eq 0 ] && [ "$extra_count" -eq 0 ]; then
|
||||||
|
echo -e "${GREEN} Perfect! All keys match.${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$missing_count" -gt 0 ]; then
|
||||||
|
echo -e "${RED} Missing $missing_count key(s) in translation:${NC}"
|
||||||
|
echo "$missing_keys" | while read -r key; do
|
||||||
|
echo -e " ${RED}• $key${NC}"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$extra_count" -gt 0 ]; then
|
||||||
|
echo -e "${YELLOW} Extra $extra_count key(s) in translation (not in source):${NC}"
|
||||||
|
echo "$extra_keys" | while read -r key; do
|
||||||
|
echo -e " ${YELLOW}• $key${NC}"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Return error code if there are missing keys
|
||||||
|
if [ "$missing_count" -gt 0 ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main function
|
||||||
|
main() {
|
||||||
|
local translation_file="$1"
|
||||||
|
local source_file="$PROJECT_ROOT/$SOURCE_FILE"
|
||||||
|
local exit_code=0
|
||||||
|
|
||||||
|
# Change to project root directory
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
# If no file specified, check all translation files
|
||||||
|
if [ -z "$translation_file" ]; then
|
||||||
|
echo -e "${YELLOW}No file specified. Checking all translation files...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Find all .json files in i18n directory except messages.json
|
||||||
|
local files
|
||||||
|
files=$(find src/lib/i18n -name "*.json" -not -name "messages.json" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -z "$files" ]; then
|
||||||
|
echo -e "${YELLOW}No translation files found in src/lib/i18n/${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate each file
|
||||||
|
echo "$files" | while read -r file; do
|
||||||
|
if [ -n "$file" ]; then
|
||||||
|
if ! validate_file "$file" "$source_file"; then
|
||||||
|
exit_code=1
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return $exit_code
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate the specified file
|
||||||
|
if ! validate_file "$translation_file" "$source_file"; then
|
||||||
|
exit_code=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return $exit_code
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if jq is installed
|
||||||
|
if ! command -v jq &> /dev/null; then
|
||||||
|
echo -e "${RED}Error: jq is required but not installed.${NC}"
|
||||||
|
echo "Please install jq:"
|
||||||
|
echo " macOS: brew install jq"
|
||||||
|
echo " Ubuntu/Debian: sudo apt-get install jq"
|
||||||
|
echo " CentOS/RHEL: sudo yum install jq"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Handle help flag
|
||||||
|
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
|
||||||
|
show_usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run main function
|
||||||
|
main "$1"
|
||||||
@@ -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(),
|
||||||
|
|||||||
264
src/lib/i18n/it.json
Normal file
264
src/lib/i18n/it.json
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"required": "*",
|
||||||
|
"cancel": "Annulla",
|
||||||
|
"create": "Crea",
|
||||||
|
"edit": "Modifica",
|
||||||
|
"delete": "Elimina",
|
||||||
|
"view": "Visualizza",
|
||||||
|
"home": "Home",
|
||||||
|
"loading": "Caricamento...",
|
||||||
|
"error": "Errore",
|
||||||
|
"success": "Successo",
|
||||||
|
"name": "Nome",
|
||||||
|
"date": "Data",
|
||||||
|
"time": "Ora",
|
||||||
|
"location": "Luogo",
|
||||||
|
"locationType": "Tipo di Luogo",
|
||||||
|
"locationNone": "Nessuno",
|
||||||
|
"locationText": "Testo",
|
||||||
|
"locationMaps": "Google Maps",
|
||||||
|
"locationNoneDescription": "Nessun luogo specificato",
|
||||||
|
"locationTextDescription": "Inserisci il luogo come testo semplice.",
|
||||||
|
"locationMapsDescription": "Inserisci il link di Google Maps.",
|
||||||
|
"googleMapsUrl": "URL di Google Maps",
|
||||||
|
"googleMapsUrlPlaceholder": "https://maps.google.com/...",
|
||||||
|
"type": "Tipo",
|
||||||
|
"visibility": "Visibilità",
|
||||||
|
"public": "Pubblico",
|
||||||
|
"private": "Privato",
|
||||||
|
"limited": "Limitato",
|
||||||
|
"unlimited": "Illimitato",
|
||||||
|
"capacity": "Capacità",
|
||||||
|
"attendees": "Partecipanti",
|
||||||
|
"attendeeLimit": "Limite di Partecipanti",
|
||||||
|
"enterLimit": "Inserisci il limite",
|
||||||
|
"enterEventName": "Inserisci il nome dell'evento",
|
||||||
|
"enterLocation": "Inserisci il luogo",
|
||||||
|
"enterYourName": "Inserisci il tuo nome",
|
||||||
|
"enterNumberOfGuests": "Inserisci il numero di ospiti",
|
||||||
|
"yourName": "Il tuo nome",
|
||||||
|
"numberOfGuests": "Numero di Ospiti",
|
||||||
|
"addGuests": "Aggiungi ospiti",
|
||||||
|
"joinEvent": "Partecipa all'Evento",
|
||||||
|
"copyLink": "Copia Link",
|
||||||
|
"addToCalendar": "Aggiungi al Calendario",
|
||||||
|
"close": "Chiudi",
|
||||||
|
"closeModal": "Chiudi finestra",
|
||||||
|
"removeRSVP": "Rimuovi RSVP",
|
||||||
|
"updating": "Aggiornamento...",
|
||||||
|
"creating": "Creazione...",
|
||||||
|
"adding": "Aggiunta...",
|
||||||
|
"updateEvent": "Aggiorna Evento",
|
||||||
|
"createEvent": "Crea Evento",
|
||||||
|
"createNewEvent": "Crea Nuovo Evento",
|
||||||
|
"createYourFirstEvent": "Crea il Tuo Primo Evento",
|
||||||
|
"editEvent": "Modifica Evento",
|
||||||
|
"deleteEvent": "Elimina Evento",
|
||||||
|
"myEvents": "I Miei Eventi",
|
||||||
|
"discover": "Scopri",
|
||||||
|
"noEventsYet": "Ancora Nessun Evento",
|
||||||
|
"noPublicEventsYet": "Ancora Nessun Evento Pubblico",
|
||||||
|
"noAttendeesYet": "Ancora nessun partecipante",
|
||||||
|
"beFirstToJoin": "Sii il primo a partecipare!",
|
||||||
|
"eventNotFound": "Evento Non Trovato",
|
||||||
|
"eventIsFull": "L'Evento è Pieno!",
|
||||||
|
"maximumCapacityReached": "Raggiunta la capacità massima",
|
||||||
|
"eventLinkCopied": "Link dell'evento copiato negli appunti!",
|
||||||
|
"rsvpAddedSuccessfully": "RSVP aggiunto con successo!",
|
||||||
|
"removedRsvpSuccessfully": "RSVP rimosso con successo.",
|
||||||
|
"anUnexpectedErrorOccurred": "Si è verificato un errore inaspettato.",
|
||||||
|
"somethingWentWrong": "Qualcosa è andato storto. Riprova.",
|
||||||
|
"failedToAddRsvp": "Impossibile aggiungere RSVP",
|
||||||
|
"failedToRemoveRsvp": "Impossibile rimuovere RSVP",
|
||||||
|
"failedToDeleteEvent": "Impossibile eliminare l'evento",
|
||||||
|
"youMayNotHavePermission": "Potresti non avere il permesso di eliminare questo evento.",
|
||||||
|
"anErrorOccurredWhileDeleting": "Si è verificato un errore durante l'eliminazione dell'evento:",
|
||||||
|
"databaseUnreachable": "Database non raggiungibile.",
|
||||||
|
"eventIdNotFound": "EventId non trovato",
|
||||||
|
"eventNotExists": "Evento non trovato",
|
||||||
|
"failedToLoadEvent": "Impossibile caricare l'evento",
|
||||||
|
"nameAndUserIdRequired": "Nome e ID utente sono obbligatori",
|
||||||
|
"eventCapacityExceeded": "Capacità dell'evento superata. Stai cercando di aggiungere {guests} partecipanti (te compreso/a), ma rimangono solo {remaining} posti.",
|
||||||
|
"nameAlreadyExists": "Il nome esiste già per questo evento",
|
||||||
|
"missingOrEmptyFields": "Campi mancanti o vuoti: {fields}",
|
||||||
|
"dateCannotBeInPast": "La data non può essere nel passato.",
|
||||||
|
"limitMustBeAtLeast2": "Il limite deve essere almeno 2 per eventi limitati.",
|
||||||
|
"unauthorized": "Non autorizzato",
|
||||||
|
"youCanOnlyEditYourOwnEvents": "Puoi modificare solo i tuoi eventi",
|
||||||
|
"youDoNotHavePermissionToDelete": "Non hai il permesso di eliminare questo evento",
|
||||||
|
"eventIdAndUserIdRequired": "ID evento e ID utente sono obbligatori",
|
||||||
|
"guestsWillBeAddedAs": "Gli ospiti verranno aggiunti come \"Ospite #1 di {name}\", \"Ospite #2 di {name}\", ecc.",
|
||||||
|
"yourNamePlaceholder": "Il tuo nome",
|
||||||
|
"atTime": "alle"
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"home": "Home",
|
||||||
|
"discover": "Scopri",
|
||||||
|
"create": "Crea",
|
||||||
|
"myEvents": "I Miei Eventi"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"title": "Cactoide - Il sito per gli RSVP",
|
||||||
|
"description": "Crea e gestisci gli RSVP degli eventi. Nessuna registrazione richiesta, condivisione immediata.",
|
||||||
|
"mainTitle": "Cactoide(ea)",
|
||||||
|
"subtitle": "La Piattaforma Definitiva per gli RSVP",
|
||||||
|
"tagline": "Crea, condividi e gestisci eventi senza intoppi.",
|
||||||
|
"whyCactoideTitle": "Perché Cactoide(ae)? 🌵",
|
||||||
|
"whyCactoideDescription": "Come il cactus, i grandi eventi fioriscono in ogni condizione se gestiti con cura. Cactoide(ae) ti aiuta a semplificare gli RSVP, coordinare in modo semplice e mantenere ogni dettaglio efficiente: così i tuoi incontri sono resilienti, vivaci e indimenticabili.",
|
||||||
|
"createEventNow": "Crea Evento Ora",
|
||||||
|
"discoverPublicEventsTitle": "Scopri Eventi Pubblici",
|
||||||
|
"discoverPublicEventsDescription": "Guarda cosa stanno pianificando gli altri e lasciati ispirare",
|
||||||
|
"browseAllPublicEvents": "Sfoglia Tutti gli Eventi Pubblici",
|
||||||
|
"whyCactoideFeatureTitle": "Perché Cactoide?",
|
||||||
|
"instantEventCreationTitle": "Creazione Istantanea di Eventi",
|
||||||
|
"instantEventCreationDescription": "Crea eventi in pochi secondi con il nostro modulo semplificato. Nessun account, nessuna attesa, solo pura efficienza.",
|
||||||
|
"oneClickSharingTitle": "Condivisione con un Clic",
|
||||||
|
"oneClickSharingDescription": "Ogni evento ottiene un URL unico e memorabile. Condividi istantaneamente tramite qualsiasi piattaforma o app di messaggistica.",
|
||||||
|
"allInOneClarityTitle": "Chiarezza Tutto-in-Uno",
|
||||||
|
"allInOneClarityDescription": "Niente più scorrimento infinito tra chat e reazioni. Visualizza la disponibilità e le risposte di tutti in un unico posto.",
|
||||||
|
"noHassleNoSignUpsTitle": "Nessun Problema, Nessuna Registrazione",
|
||||||
|
"noHassleNoSignUpsDescription": "Salta le registrazioni e i moduli infiniti. A differenza di altre piattaforme di eventi, crei e condividi istantaneamente: nessun account, nessuna barriera.",
|
||||||
|
"smartLimitsTitle": "Limiti Intelligenti",
|
||||||
|
"smartLimitsDescription": "Scegli tra RSVP illimitati o imposta una capacità limitata. Perfetto per eventi di qualsiasi dimensione.",
|
||||||
|
"effortlessSimplicityTitle": "Semplicità Senza Sforzo",
|
||||||
|
"effortlessSimplicityDescription": "Progettato per essere immediatamente chiaro e facile. Nessuna curva di apprendimento: apri, crea e vai.",
|
||||||
|
"howItWorksTitle": "Come Funziona",
|
||||||
|
"step1Title": "Crea Evento",
|
||||||
|
"step1Description": "Compila un semplice modulo con i dettagli dell'evento. Scegli tra capacità limitata o illimitata.",
|
||||||
|
"step2Title": "Ottieni URL Unico",
|
||||||
|
"step2Description": "Ricevi un URL casuale e memorabile per il tuo evento. Perfetto per la condivisione ovunque.",
|
||||||
|
"step3Title": "Raccogli gli RSVP",
|
||||||
|
"step3Description": "Le persone visitano il tuo link e partecipano solo con il loro nome. Nessun account necessario.",
|
||||||
|
"ctaTitle": "Pronto a Creare il Tuo Primo Evento?",
|
||||||
|
"ctaDescription": "Unisciti a migliaia di organizzatori di eventi che si fidano di Cactoide",
|
||||||
|
"ctaButton": "Crea"
|
||||||
|
},
|
||||||
|
"create": {
|
||||||
|
"title": "Crea Evento - Cactoide",
|
||||||
|
"formTitle": "Crea Nuovo Evento",
|
||||||
|
"eventNameLabel": "Nome",
|
||||||
|
"eventNamePlaceholder": "Inserisci il nome dell'evento",
|
||||||
|
"dateLabel": "Data",
|
||||||
|
"timeLabel": "Ora",
|
||||||
|
"locationLabel": "Luogo",
|
||||||
|
"locationPlaceholder": "Inserisci il luogo",
|
||||||
|
"locationTypeLabel": "Tipo di Luogo",
|
||||||
|
"locationNoneOption": "Nessuno",
|
||||||
|
"locationTextOption": "Testo Semplice",
|
||||||
|
"locationMapsOption": "Google Maps",
|
||||||
|
"locationNoneDescription": "Nessun luogo specificato.",
|
||||||
|
"locationTextDescription": "Inserisci il luogo come testo semplice.",
|
||||||
|
"locationMapsDescription": "Inserisci il link di Google Maps.",
|
||||||
|
"googleMapsUrlLabel": "URL di Google Maps",
|
||||||
|
"googleMapsUrlPlaceholder": "https://maps.google.com/...",
|
||||||
|
"typeLabel": "Tipo",
|
||||||
|
"unlimitedOption": "Illimitato",
|
||||||
|
"limitedOption": "Limitato",
|
||||||
|
"attendeeLimitLabel": "Limite di Partecipanti",
|
||||||
|
"attendeeLimitPlaceholder": "Inserisci il limite",
|
||||||
|
"visibilityLabel": "Visibilità",
|
||||||
|
"publicOption": "🌍 Pubblico",
|
||||||
|
"privateOption": "🔒 Privato",
|
||||||
|
"publicDescription": "Gli eventi pubblici sono visibili a tutti e possono essere scoperti da altri.",
|
||||||
|
"privateDescription": "Gli eventi privati sono visibili solo a te e alle persone con cui condividi il link.",
|
||||||
|
"creatingEvent": "Creazione Evento...",
|
||||||
|
"createEventButton": "Crea Evento"
|
||||||
|
},
|
||||||
|
"event": {
|
||||||
|
"title": "{eventName} - Cactoide",
|
||||||
|
"eventTitle": "Evento - Cactoide",
|
||||||
|
"editTitle": "Modifica Evento - {eventName} - Cactoide",
|
||||||
|
"myEventsTitle": "I Miei Eventi - Cactoide",
|
||||||
|
"eventNotFoundTitle": "Evento Non Trovato",
|
||||||
|
"eventNotFoundDescription": "L'evento che stai cercando non esiste o è stato rimosso.",
|
||||||
|
"joinThisEvent": "Partecipa a Questo Evento",
|
||||||
|
"eventIsFull": "L'Evento è Pieno!",
|
||||||
|
"maximumCapacityReached": "Raggiunta la capacità massima",
|
||||||
|
"yourNameLabel": "Il tuo nome",
|
||||||
|
"yourNamePlaceholder": "Inserisci il tuo nome",
|
||||||
|
"addGuestsLabel": "Aggiungi ospiti",
|
||||||
|
"numberOfGuestsLabel": "Numero di Ospiti",
|
||||||
|
"numberOfGuestsPlaceholder": "Inserisci il numero di ospiti",
|
||||||
|
"guestsWillBeAddedAs": "Gli ospiti verranno aggiunti come \"Ospite #1 di {name}\", \"Ospite #2 di {name}\", ecc.",
|
||||||
|
"joinEventButton": "Partecipa all'Evento",
|
||||||
|
"joinEventWithGuests": "Partecipa all'Evento + {count} Ospite{plural}",
|
||||||
|
"adding": "Aggiunta...",
|
||||||
|
"attendeesTitle": "Partecipanti",
|
||||||
|
"noAttendeesYet": "Ancora nessun partecipante",
|
||||||
|
"beFirstToJoin": "Sii il primo a partecipare!",
|
||||||
|
"copyLinkButton": "Copia Link",
|
||||||
|
"addToCalendarButton": "Aggiungi al Calendario",
|
||||||
|
"eventLinkCopied": "Link dell'evento copiato negli appunti!",
|
||||||
|
"rsvpAddedSuccessfully": "RSVP aggiunto con successo!",
|
||||||
|
"removedRsvpSuccessfully": "RSVP rimosso con successo.",
|
||||||
|
"failedToAddRsvp": "Impossibile aggiungere RSVP",
|
||||||
|
"failedToRemoveRsvp": "Impossibile rimuovere RSVP",
|
||||||
|
"editEventTitle": "Modifica Evento",
|
||||||
|
"editEventDescription": "Aggiorna i dettagli del tuo evento",
|
||||||
|
"updatingEvent": "Aggiornamento...",
|
||||||
|
"updateEventButton": "Aggiorna Evento",
|
||||||
|
"myEventsDescription": "Gestisci i tuoi eventi creati",
|
||||||
|
"noEventsYetTitle": "Ancora Nessun Evento",
|
||||||
|
"noEventsYetDescription": "Non hai ancora creato nessun evento. Inizia creando il tuo primo evento!",
|
||||||
|
"createYourFirstEventButton": "Crea il Tuo Primo Evento",
|
||||||
|
"deleteEventTitle": "Elimina Evento",
|
||||||
|
"deleteEventDescription": "Sei sicuro di voler eliminare \"{eventName}\"? Questa azione non può essere annullata e rimuoverà tutti gli RSVP.",
|
||||||
|
"deleteButton": "Elimina",
|
||||||
|
"viewEventAriaLabel": "Visualizza evento",
|
||||||
|
"editEventAriaLabel": "Modifica evento",
|
||||||
|
"deleteEventAriaLabel": "Elimina evento",
|
||||||
|
"removeRsvpAriaLabel": "Rimuovi RSVP"
|
||||||
|
},
|
||||||
|
"discover": {
|
||||||
|
"title": "Scopri Eventi - Cactoide",
|
||||||
|
"noPublicEventsTitle": "Ancora Nessun Evento Pubblico",
|
||||||
|
"noPublicEventsDescription": "Al momento non ci sono eventi pubblici disponibili. Sii il primo a crearne uno!",
|
||||||
|
"createButton": "Crea",
|
||||||
|
"publicEventsTitle": "Eventi Pubblici ({count})",
|
||||||
|
"publicEventsDescription": "Scopri eventi creati dalla comunità",
|
||||||
|
"searchPlaceholder": "Cerca eventi per nome, luogo...",
|
||||||
|
"searchInputAriaLabel": "Input di ricerca",
|
||||||
|
"toggleFiltersAriaLabel": "Attiva/Disattiva filtri",
|
||||||
|
"typeFilterLabel": "Tipo:",
|
||||||
|
"typeFilterAll": "Tutti",
|
||||||
|
"typeFilterLimited": "Limitati",
|
||||||
|
"typeFilterUnlimited": "Illimitati",
|
||||||
|
"statusFilterLabel": "Stato:",
|
||||||
|
"statusFilterAll": "Tutti gli eventi",
|
||||||
|
"statusFilterUpcoming": "Eventi imminenti",
|
||||||
|
"statusFilterPast": "Eventi passati",
|
||||||
|
"timeFilterLabel": "Orario:",
|
||||||
|
"timeFilterAny": "Qualsiasi orario",
|
||||||
|
"timeFilterNextWeek": "Prossima settimana",
|
||||||
|
"timeFilterNextMonth": "Prossimo mese",
|
||||||
|
"sortOrderLabel": "Ordina:",
|
||||||
|
"sortOrderEarliest": "Prima i più vicini",
|
||||||
|
"sortOrderLatest": "Prima i più recenti",
|
||||||
|
"viewButton": "Visualizza",
|
||||||
|
"noEventsFoundTitle": "Nessun evento trovato",
|
||||||
|
"noEventsFoundDescription": "Prova a modificare i termini di ricerca o sfoglia tutti gli eventi"
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"addToCalendarTitle": "Aggiungi al Calendario",
|
||||||
|
"googleCalendarTitle": "Google Calendar",
|
||||||
|
"googleCalendarDescription": "Aggiungi a Google Calendar",
|
||||||
|
"microsoftOutlookTitle": "Microsoft Outlook",
|
||||||
|
"microsoftOutlookDescription": "Aggiungi a Outlook Calendar",
|
||||||
|
"downloadICalTitle": "Scarica File iCal",
|
||||||
|
"downloadICalDescription": "Scarica file .ics per qualsiasi app di calendario"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"title": "Errore - Cactoide",
|
||||||
|
"errorTitle": "Errore",
|
||||||
|
"anUnexpectedErrorOccurred": "Si è verificato un errore inaspettato.",
|
||||||
|
"homeButton": "Home"
|
||||||
|
},
|
||||||
|
"layout": {
|
||||||
|
"defaultTitle": "Cactoide -",
|
||||||
|
"defaultDescription": "Crea e gestisci gli RSVP degli eventi",
|
||||||
|
"userIdCookieText": "Il tuo UserID memorizzato come cookie:",
|
||||||
|
"firstTimeVisiting": "Prima visita. Generazione di un nuovo UserID...",
|
||||||
|
"copyright": "© 2025 Cactoide"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,54 +164,126 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Location -->
|
<!-- Location Type -->
|
||||||
<div>
|
<div>
|
||||||
<label for="location" class="text-dark-800 mb-3 block text-sm font-semibold">
|
<fieldset>
|
||||||
{t('create.locationLabel')} <span class="text-red-400">{t('common.required')}</span>
|
<legend class="text-dark-800 mb-3 block text-sm font-semibold">
|
||||||
</label>
|
{t('create.locationTypeLabel')}
|
||||||
<input
|
<span class="text-red-400">{t('common.required')}</span>
|
||||||
id="location"
|
</legend>
|
||||||
name="location"
|
<div class="grid grid-cols-3 gap-3">
|
||||||
type="text"
|
<button
|
||||||
bind:value={eventData.location}
|
type="button"
|
||||||
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="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.location_type ===
|
||||||
placeholder={t('create.locationPlaceholder')}
|
'none'
|
||||||
maxlength="200"
|
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
||||||
required
|
: 'border-dark-300 text-dark-700'}"
|
||||||
/>
|
on:click={() => handleLocationTypeChange('none')}
|
||||||
{#if errors.location}
|
>
|
||||||
<p class="mt-2 text-sm font-medium text-red-600">{errors.location}</p>
|
{t('create.locationNoneOption')}
|
||||||
{/if}
|
</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>
|
</div>
|
||||||
|
|
||||||
|
<!-- Location Input (only show when not 'none') -->
|
||||||
|
{#if eventData.location_type !== 'none'}
|
||||||
|
<div>
|
||||||
|
<label for="location" class="text-dark-800 mb-3 block text-sm font-semibold">
|
||||||
|
{eventData.location_type === 'text'
|
||||||
|
? t('create.locationLabel')
|
||||||
|
: t('create.googleMapsUrlLabel')}
|
||||||
|
<span class="text-red-400">{t('common.required')}</span>
|
||||||
|
</label>
|
||||||
|
{#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>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Event Type -->
|
<!-- Event Type -->
|
||||||
<div>
|
<div>
|
||||||
<label class="text-dark-800 mb-3 block text-sm font-semibold">
|
<fieldset>
|
||||||
{t('create.typeLabel')}
|
<legend class="text-dark-800 mb-3 block text-sm font-semibold">
|
||||||
<span class="text-red-400">{t('common.required')}</span></label
|
{t('create.typeLabel')}
|
||||||
>
|
<span class="text-red-400">{t('common.required')}</span>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
</legend>
|
||||||
<button
|
<div class="grid grid-cols-2 gap-3">
|
||||||
type="button"
|
<button
|
||||||
class="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.type ===
|
type="button"
|
||||||
'unlimited'
|
class="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.type ===
|
||||||
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
'unlimited'
|
||||||
: 'border-dark-300 text-dark-700'}"
|
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
||||||
on:click={() => handleTypeChange('unlimited')}
|
: 'border-dark-300 text-dark-700'}"
|
||||||
>
|
on:click={() => handleTypeChange('unlimited')}
|
||||||
{t('create.unlimitedOption')}
|
>
|
||||||
</button>
|
{t('create.unlimitedOption')}
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<button
|
||||||
class="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.type ===
|
type="button"
|
||||||
'limited'
|
class="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.type ===
|
||||||
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
'limited'
|
||||||
: 'border-dark-300 text-dark-700 bg-gray-600/20 hover:bg-gray-600/70'}"
|
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
||||||
on:click={() => handleTypeChange('limited')}
|
: 'border-dark-300 text-dark-700 bg-gray-600/20 hover:bg-gray-600/70'}"
|
||||||
>
|
on:click={() => handleTypeChange('limited')}
|
||||||
{t('create.limitedOption')}
|
>
|
||||||
</button>
|
{t('create.limitedOption')}
|
||||||
</div>
|
</button>
|
||||||
|
</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,37 +312,39 @@
|
|||||||
|
|
||||||
<!-- Event Visibility -->
|
<!-- Event Visibility -->
|
||||||
<div>
|
<div>
|
||||||
<label class="text-dark-800 mb-3 block text-sm font-semibold">
|
<fieldset>
|
||||||
{t('create.visibilityLabel')}
|
<legend class="text-dark-800 mb-3 block text-sm font-semibold">
|
||||||
<span class="text-red-400">{t('common.required')}</span></label
|
{t('create.visibilityLabel')}
|
||||||
>
|
<span class="text-red-400">{t('common.required')}</span>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
</legend>
|
||||||
<button
|
<div class="grid grid-cols-2 gap-3">
|
||||||
type="button"
|
<button
|
||||||
class="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.visibility ===
|
type="button"
|
||||||
'public'
|
class="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.visibility ===
|
||||||
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
'public'
|
||||||
: 'border-dark-300 text-dark-700'}"
|
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
||||||
on:click={() => (eventData.visibility = 'public')}
|
: 'border-dark-300 text-dark-700'}"
|
||||||
>
|
on:click={() => (eventData.visibility = 'public')}
|
||||||
{t('create.publicOption')}
|
>
|
||||||
</button>
|
{t('create.publicOption')}
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<button
|
||||||
class="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.visibility ===
|
type="button"
|
||||||
'private'
|
class="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.visibility ===
|
||||||
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
'private'
|
||||||
: 'border-dark-300 text-dark-700 bg-gray-600/20 hover:bg-gray-600/70'}"
|
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
||||||
on:click={() => (eventData.visibility = 'private')}
|
: 'border-dark-300 text-dark-700 bg-gray-600/20 hover:bg-gray-600/70'}"
|
||||||
>
|
on:click={() => (eventData.visibility = 'private')}
|
||||||
{t('create.privateOption')}
|
>
|
||||||
</button>
|
{t('create.privateOption')}
|
||||||
</div>
|
</button>
|
||||||
<p class="mt-2 text-xs text-slate-400">
|
</div>
|
||||||
{eventData.visibility === 'public'
|
<p class="mt-2 text-xs text-slate-400 italic">
|
||||||
? t('create.publicDescription')
|
{eventData.visibility === 'public'
|
||||||
: t('create.privateDescription')}
|
? t('create.publicDescription')
|
||||||
</p>
|
: t('create.privateDescription')}
|
||||||
|
</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>
|
||||||
<span>{event.location}</span>
|
{#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>
|
||||||
|
{/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>
|
||||||
<span>{event.location}</span>
|
{#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>
|
||||||
|
{/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) {
|
||||||
navigator.clipboard.writeText(url).then(() => {
|
const url = `${$page.url.origin}/event/${eventId}`;
|
||||||
success = 'Event link copied to clipboard!';
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
setTimeout(() => {
|
success = 'Event link copied to clipboard!';
|
||||||
success = '';
|
setTimeout(() => {
|
||||||
}, 3000);
|
success = '';
|
||||||
});
|
}, 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>
|
||||||
<p class="font-semibold text-white">{event.location}</p>
|
{#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>
|
||||||
|
{/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,26 +165,96 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Location -->
|
<!-- Location Type -->
|
||||||
<div>
|
<div>
|
||||||
<label for="location" class="text-dark-800 mb-3 block text-sm font-semibold">
|
<fieldset>
|
||||||
{t('common.location')} <span class="text-red-400">{t('common.required')}</span>
|
<legend class="text-dark-800 mb-3 block text-sm font-semibold">
|
||||||
</label>
|
{t('create.locationTypeLabel')}
|
||||||
<input
|
<span class="text-red-400">{t('common.required')}</span>
|
||||||
id="location"
|
</legend>
|
||||||
name="location"
|
<div class="grid grid-cols-3 gap-3">
|
||||||
type="text"
|
<button
|
||||||
bind:value={eventData.location}
|
type="button"
|
||||||
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="rounded-sm border-2 px-4 py-3 font-medium transition-all duration-200 {eventData.location_type ===
|
||||||
placeholder={t('common.enterLocation')}
|
'none'
|
||||||
maxlength="200"
|
? ' border-violet-500 bg-violet-400/20 font-semibold hover:bg-violet-400/70'
|
||||||
required
|
: 'border-dark-300 text-dark-700'}"
|
||||||
/>
|
on:click={() => handleLocationTypeChange('none')}
|
||||||
{#if errors.location}
|
>
|
||||||
<p class="mt-2 text-sm font-medium text-red-600">{errors.location}</p>
|
{t('create.locationNoneOption')}
|
||||||
{/if}
|
</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>
|
</div>
|
||||||
|
|
||||||
|
<!-- Location Input (only show when not 'none') -->
|
||||||
|
{#if eventData.location_type !== 'none'}
|
||||||
|
<div>
|
||||||
|
<label for="location" class="text-dark-800 mb-3 block text-sm font-semibold">
|
||||||
|
{eventData.location_type === 'text'
|
||||||
|
? t('create.locationLabel')
|
||||||
|
: t('create.googleMapsUrlLabel')}
|
||||||
|
<span class="text-red-400">{t('common.required')}</span>
|
||||||
|
</label>
|
||||||
|
{#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>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Event Type -->
|
<!-- Event Type -->
|
||||||
<div>
|
<div>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|||||||
Reference in New Issue
Block a user