mirror of
https://github.com/polaroi8d/cactoide.git
synced 2026-03-22 22:25:29 +00:00
Compare commits
28 Commits
0.1.0
...
fix/userId
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
984c296725 | ||
|
|
9acfa08ea8 | ||
|
|
45cb95f6a8 | ||
|
|
8426bd5704 | ||
|
|
b9833db3bb | ||
|
|
b3572293ba | ||
|
|
c1752efe4b | ||
|
|
491d0020bd | ||
|
|
5c1182dc66 | ||
|
|
638b5ff1ca | ||
|
|
069ca11917 | ||
|
|
4675fa4623 | ||
|
|
af88d6462b | ||
|
|
ef6005e648 | ||
|
|
11875b4a1e | ||
|
|
22038f7779 | ||
|
|
de2cb07a15 | ||
|
|
ffc29b9c24 | ||
|
|
4860b9439c | ||
|
|
d10af13134 | ||
|
|
c98260efec | ||
|
|
a40b83c2b3 | ||
|
|
bfb76aa268 | ||
|
|
f8758d7b47 | ||
|
|
f51f89e35f | ||
|
|
26824eb3a8 | ||
|
|
d2024d31ba | ||
|
|
cc3c868f7d |
@@ -13,3 +13,5 @@ DATABASE_URL="postgres://cactoide:cactoide_password@localhost:5432/cactoide_data
|
|||||||
APP_VERSION=latest
|
APP_VERSION=latest
|
||||||
PORT=3000
|
PORT=3000
|
||||||
HOSTNAME=0.0.0.0
|
HOSTNAME=0.0.0.0
|
||||||
|
|
||||||
|
PUBLIC_LANDING_INFO=true
|
||||||
|
|||||||
2
.github/workflows/build-and-push.yml
vendored
2
.github/workflows/build-and-push.yml
vendored
@@ -60,6 +60,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
build-args: |
|
||||||
|
PUBLIC_LANDING_INFO=${{ vars.PUBLIC_LANDING_INFO }}
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
|
|||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -28,6 +28,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Build application
|
- name: Build application
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
env:
|
||||||
|
PUBLIC_LANDING_INFO: ${{ vars.PUBLIC_LANDING_INFO }}
|
||||||
|
|
||||||
- name: Test build output
|
- name: Test build output
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -3,25 +3,27 @@ WORKDIR /app
|
|||||||
COPY package*.json .
|
COPY package*.json .
|
||||||
|
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
|
ARG PUBLIC_LANDING_INFO
|
||||||
|
ENV PUBLIC_LANDING_INFO=$PUBLIC_LANDING_INFO
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
RUN npm prune --production
|
RUN npm prune --production
|
||||||
|
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /app/build build/
|
COPY --from=builder /app/build build/
|
||||||
COPY --from=builder /app/node_modules node_modules/
|
COPY --from=builder /app/node_modules node_modules/
|
||||||
COPY package.json .
|
COPY package.json .
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
ENV PORT 3000
|
ENV PORT 3000
|
||||||
ENV HOSTNAME "0.0.0.0"
|
ENV HOSTNAME "0.0.0.0"
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||||
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/healthz || exit 1
|
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/healthz || exit 1
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
CMD [ "node", "build" ]
|
CMD [ "node", "build" ]
|
||||||
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
42
README.md
42
README.md
@@ -55,8 +55,48 @@ 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.
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
#### 1. Landing page option
|
||||||
|
|
||||||
|
Supports a conditional landing page display based on the `PUBLIC_LANDING_INFO` environment variable. If you don't want to show your users the cactoide landing page, just use the `PUBLIC_LANDING_INFO=false` variable. This will automatically remove the landing home page and redirect users to the `/discover` page.
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
|
||||||
|
- Creating a minimal discovery-focused experience
|
||||||
|
- Customizing the user journey based on deployment environment
|
||||||
|
|
||||||
|
#### 2. 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Support
|
||||||
|
|
||||||
|
Cactoide is an open-source project licensed under `AGPL-3.0`. Its growth and development are possible thanks to the amazing support of the community. This project is the result of many late nights, weekends, and after-hours work.
|
||||||
|
|
||||||
|
It isn’t backed by a big company. Development depends on the support and generosity of people like you. With your help, I can focus more on making Cactoide even better and building tools that make coding more enjoyable.
|
||||||
|
|
||||||
|
You can support in a few ways:
|
||||||
|
|
||||||
|
- Send a one-time donation via [paypal.me/zenoazurben](paypal.me/zenoazurben)
|
||||||
|
- Reach me directly: leventeorb[@]gmail.com
|
||||||
|
|
||||||
|
If you enjoy using Cactoide, or if your business depends on it, please consider sponsoring its development. Your support keeps the project alive, improves it for everyone, and helps create educational content like blog posts and videos for the whole Cactoide community.
|
||||||
|
|
||||||
### 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);
|
||||||
|
|
||||||
|
|||||||
92
package-lock.json
generated
92
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "event-cactus",
|
"name": "event-cactus",
|
||||||
"version": "0.0.1",
|
"version": "0.1.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "event-cactus",
|
"name": "event-cactus",
|
||||||
"version": "0.0.1",
|
"version": "0.1.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/adapter-node": "^5.3.1",
|
"@sveltejs/adapter-node": "^5.3.1",
|
||||||
"drizzle-orm": "^0.44.5",
|
"drizzle-orm": "^0.44.5",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.20.0",
|
"typescript-eslint": "^8.20.0",
|
||||||
"vite": "^7.0.4"
|
"vite": "^7.1.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@drizzle-team/brocli": {
|
"node_modules/@drizzle-team/brocli": {
|
||||||
@@ -1949,6 +1949,66 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||||
|
"version": "1.4.5",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.0.4",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.4.5",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||||
|
"version": "0.2.12",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/core": "^1.4.3",
|
||||||
|
"@emnapi/runtime": "^1.4.3",
|
||||||
|
"@tybys/wasm-util": "^0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||||
|
"version": "0.10.0",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||||
|
"version": "2.8.0",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz",
|
||||||
@@ -2602,9 +2662,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/devalue": {
|
"node_modules/devalue": {
|
||||||
"version": "5.1.1",
|
"version": "5.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.4.1.tgz",
|
||||||
"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
|
"integrity": "sha512-YtoaOfsqjbZQKGIMRYDWKjUmSB4VJ/RElB+bXZawQAQYAo4xu08GKTMVlsZDTF6R2MbAgjcAQRPI5eIyRAT2OQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/drizzle-kit": {
|
"node_modules/drizzle-kit": {
|
||||||
@@ -4737,13 +4797,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.14",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.5.0",
|
||||||
"picomatch": "^4.0.2"
|
"picomatch": "^4.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -4864,17 +4924,17 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.10.tgz",
|
||||||
"integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==",
|
"integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.6",
|
"fdir": "^6.5.0",
|
||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"rollup": "^4.43.0",
|
"rollup": "^4.43.0",
|
||||||
"tinyglobby": "^0.2.14"
|
"tinyglobby": "^0.2.15"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "cactoide",
|
"name": "cactoide",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.3",
|
"version": "0.1.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.20.0",
|
"typescript-eslint": "^8.20.0",
|
||||||
"vite": "^7.0.4"
|
"vite": "^7.1.10"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/adapter-node": "^5.3.1",
|
"@sveltejs/adapter-node": "^5.3.1",
|
||||||
|
|||||||
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"
|
||||||
22
src/hooks.server.ts
Normal file
22
src/hooks.server.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// src/hooks.server.ts
|
||||||
|
import type { Handle } from '@sveltejs/kit';
|
||||||
|
import { generateUserId } from '$lib/generateUserId.js';
|
||||||
|
|
||||||
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
|
const cactoideUserId = event.cookies.get('cactoideUserId');
|
||||||
|
const userId = generateUserId();
|
||||||
|
|
||||||
|
const DAYS = 400; // practical upper bound in many browsers for cookies
|
||||||
|
const MAX_AGE = 60 * 60 * 24 * DAYS;
|
||||||
|
const PATH = '/';
|
||||||
|
|
||||||
|
if (!cactoideUserId) {
|
||||||
|
console.debug(`There is no cactoideUserId cookie, generating new one...`);
|
||||||
|
event.cookies.set('cactoideUserId', userId, { path: PATH, maxAge: MAX_AGE });
|
||||||
|
} else {
|
||||||
|
console.debug(`cactoideUserId: ${cactoideUserId}`);
|
||||||
|
console.debug(`cactoideUserId cookie found, using existing one...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(event);
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { t } from '$lib/i18n/i18n.js';
|
import { t } from '$lib/i18n/i18n.js';
|
||||||
|
import { PUBLIC_LANDING_INFO } from '$env/static/public';
|
||||||
|
|
||||||
// Check if current page is active
|
// Check if current page is active
|
||||||
const isActive = (path: string): boolean => {
|
const isActive = (path: string): boolean => {
|
||||||
@@ -24,12 +25,14 @@
|
|||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<div class="md:flex md:items-center md:space-x-8">
|
<div class="md:flex md:items-center md:space-x-8">
|
||||||
<button
|
{#if PUBLIC_LANDING_INFO !== 'false'}
|
||||||
on:click={() => goto('/')}
|
<button
|
||||||
class={isActive('/') ? 'text-violet-400' : 'cursor-pointer'}
|
on:click={() => goto('/')}
|
||||||
>
|
class={isActive('/') ? 'text-violet-400' : 'cursor-pointer'}
|
||||||
{t('navigation.home')}
|
>
|
||||||
</button>
|
{t('navigation.home')}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
on:click={() => goto('/discover')}
|
on:click={() => goto('/discover')}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
import { generateUserId } from '$lib/generateUserId.js';
|
|
||||||
|
|
||||||
export function load({ cookies }) {
|
export function load({ cookies }) {
|
||||||
const cactoideUserId = cookies.get('cactoideUserId');
|
const cactoideUserId = cookies.get('cactoideUserId');
|
||||||
const userId = generateUserId();
|
|
||||||
|
|
||||||
const DAYS = 400; // practical upper bound in many browsers for cookies
|
|
||||||
const MAX_AGE = 60 * 60 * 24 * DAYS;
|
|
||||||
const PATH = '/';
|
|
||||||
|
|
||||||
if (!cactoideUserId) {
|
|
||||||
console.debug(`There is no cactoideUserId cookie, generating new one...`);
|
|
||||||
cookies.set('cactoideUserId', userId, { path: PATH, maxAge: MAX_AGE });
|
|
||||||
} else {
|
|
||||||
console.debug(`cactoideUserId: ${cactoideUserId}`);
|
|
||||||
console.debug(`cactoideUserId cookie found, using existing one...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cactoideUserId
|
cactoideUserId
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import Navbar from '$lib/components/Navbar.svelte';
|
import Navbar from '$lib/components/Navbar.svelte';
|
||||||
import { t } from '$lib/i18n/i18n.js';
|
import { t } from '$lib/i18n/i18n.js';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data, children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<main class="relative z-10">
|
<main class="relative z-10">
|
||||||
<slot />
|
{@render children?.()}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
|
|||||||
11
src/routes/+page.server.ts
Normal file
11
src/routes/+page.server.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { PUBLIC_LANDING_INFO } from '$env/static/public';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async () => {
|
||||||
|
if (PUBLIC_LANDING_INFO === 'false') {
|
||||||
|
throw redirect(302, '/discover');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -106,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">
|
||||||
@@ -136,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">
|
||||||
@@ -155,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>
|
||||||
|
|
||||||
|
|||||||
@@ -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