Compare commits

...

13 Commits

Author SHA1 Message Date
Levente Orban
d998aff383 fix: remove cactus & netlify adapter and unused fonts 2025-09-01 11:43:47 +02:00
Levente Orban
4a2defe1f8 Merge pull request #8 from nandor-magyar/nandor-magyar/main
fix: small adjusments, renames for the /healthz and readme
2025-09-01 11:03:33 +02:00
Levente Orban
5c178d8a79 fix: small adjusments, renames for the /healthz and readme 2025-09-01 11:01:38 +02:00
Levente Orban
8a76421571 fix: small adjusments, renames for the /healthz and readme 2025-09-01 10:43:40 +02:00
Nandor Magyar
94fffc5695 add healthz, docker fixes, minor readme tweaks 2025-09-01 10:43:39 +02:00
Levente Orban
cc8266ae6e Merge pull request #7 from polaroi8d/feat/fuse-search
feat: add fuse.js search module
2025-08-31 18:58:05 +02:00
Levente Orban
16ad15071c feat: add fuse.js search module 2025-08-31 18:50:47 +02:00
Levente Orban
834b9e0715 feat: add search bar for /discovery and seed.sql for populating random data 2025-08-31 17:58:59 +02:00
Levente Orban
8435289e1e fix: typo and add wikipedia referral for the naming 2025-08-29 09:10:01 +02:00
Levente Orban
fefca207c5 feat: remove or refactor unused console.logs 2025-08-27 22:39:42 +02:00
Levente Orban
e89c9b1843 feat: add docker DATABASE_URL 2025-08-27 22:35:21 +02:00
Levente Orban
4b14f649d6 fix: docker-compose renames and fixes 2025-08-27 22:34:06 +02:00
Levente Orban
3cbdd93386 ci: testing the tags pipeline 2025-08-27 17:13:47 +02:00
22 changed files with 335 additions and 109 deletions

View File

@@ -1,10 +1,13 @@
# Postgres configuration # Postgres configuration
POSTGRES_DB=cactoied_database POSTGRES_DB=cactoide_database
POSTGRES_USER=cactoide POSTGRES_USER=cactoide
POSTGRES_PASSWORD=cactoide_password POSTGRES_PASSWORD=cactoide_password
POSTGRES_PORT=5432 POSTGRES_PORT=5432
DATABASE_URL="postgres://cactoide:cactoide_password@localhost:5432/cactoied_database" # localhost
DATABASE_URL="postgres://cactoide:cactoide_password@localhost:5432/cactoide_database"
# docker
# DATABASE_URL="postgres://cactoide:cactoide_password@postgres:5432/cactoide_database"
# Application configuration # Application configuration
APP_VERSION=latest APP_VERSION=latest

View File

@@ -1,3 +1,4 @@
# .github/workflows/docker-build-and-push.yml
name: build & push the images name: build & push the images
on: on:
@@ -38,8 +39,24 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
# Generate tags & labels:
# - short SHA on all pushes
# - git tag on tag pushes
# - latest only on default branch (e.g., main)
- name: Extract Docker metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}
tags: |
type=sha,format=short
type=ref,event=tag
type=raw,value=latest,enable={{is_default_branch}}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
@@ -47,13 +64,5 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
tags: | tags: ${{ steps.meta.outputs.tags }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}:latest labels: ${{ steps.meta.outputs.labels }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}:${{ github.ref_type == 'tag' && github.ref_name || '' }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
- name: Output image info
run: |
echo "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}:latest"

View File

@@ -20,4 +20,8 @@ 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 \
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/healthz || exit 1
CMD [ "node", "build" ] CMD [ "node", "build" ]

View File

@@ -1,4 +1,4 @@
.PHONY: help build up db-only logs clean .PHONY: help build up db-only logs db-clean prune
# Default target # Default target
help: help:
@@ -13,32 +13,37 @@ help:
@echo "" @echo ""
@echo "Utility commands:" @echo "Utility commands:"
@echo " make logs - Show logs from all services" @echo " make logs - Show logs from all services"
@echo " make clean - Remove all containers, images, and volumes" @echo " make db-clean - Stop & remove database container"
@echo " make prune - Remove all containers, images, and volumes"
@echo " make help - Show this help message" @echo " make help - Show this help message"
# Build the Docker images # Build the Docker images
build: build:
@echo "Building Docker images..." @echo "Building Docker images..."
docker-compose build docker compose build
# Start all services # Start all services
up: up:
@echo "Starting all services..." @echo "Starting all services..."
docker-compose up -d docker compose up -d
# Start only the database # Start only the database
db-only: db-only:
@echo "Starting only the database..." @echo "Starting only the database..."
docker-compose up -d postgres docker compose up -d postgres
# Show logs from all services # Show logs from all services
logs: logs:
@echo "Showing logs from all services..." @echo "Showing logs from all services..."
docker-compose logs -f docker compose logs -f
db-clean:
@echo "Cleaning up all Docker resources..."
docker stop cactoide-db && docker rm cactoide-db && docker volume prune -f && docker network prune -f
# Clean up everything (containers, images, volumes) # Clean up everything (containers, images, volumes)
clean: 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

View File

@@ -27,10 +27,24 @@ A mobile-first event RSVP platform that lets you create events, share unique URL
### Quick Start ### Quick Start
#### Requirements
`git, docker, docker-compose, node at least suggested 20.19.0`
Uses the [`docker-compose.yml`](docker-compose.yml) file to setup the application with the database. You can define all ENV variables in the [`.env`](.env.example) file from the `.env.example`.
```bash
git clone https://github.com/polaroi8d/cactoide/
cd cactoide
cp env.example .env
docker compose up -d
```
### Development
```bash ```bash
git clone https://github.com/polaroi8d/cactoide/ git clone https://github.com/polaroi8d/cactoide/
cd cactoide cd cactoide
npm install
cp env.example .env cp env.example .env
make db-only make db-only
npm run dev -- --open npm run dev -- --open
@@ -38,9 +52,7 @@ npm run dev -- --open
Your app will be available at `http://localhost:5173`. You can use the Makefile commands to run the application or the database, eg.: `make db-only`. Your app will be available at `http://localhost:5173`. You can use the Makefile commands to run the application or the database, eg.: `make db-only`.
### Self-Host Use the `database/seed.sql` if you want to populate your database with dummy data.
Use the [`docker-compose.yml`](docker-compose.yml) file to setup the application with the database. You can define all ENV variables in the [`.env`](.env.example) file from the `.env.example`.
### License ### License

77
database/seed.sql Normal file
View File

@@ -0,0 +1,77 @@
BEGIN;
-- Optional: start clean (will also remove RSVPs via CASCADE)
TRUNCATE TABLE events CASCADE;
-- -----------------------------
-- Seed 100 events
-- -----------------------------
WITH params AS (
SELECT
ARRAY[
'Budapest', 'Berlin', 'Paris', 'Madrid', 'Rome', 'Vienna', 'Prague',
'Warsaw', 'Amsterdam', 'Lisbon', 'Copenhagen', 'Dublin', 'Athens',
'Zurich', 'Helsinki', 'Oslo', 'Stockholm', 'Brussels', 'Munich', 'Milan'
]::text[] AS cities,
ARRAY['Hall','Park','Rooftop','Auditorium','Conference Center','Café','Online']::text[] AS venues,
ARRAY['Tech Talk','Meetup','Workshop','Concert','Yoga','Brunch','Game Night','Hackathon','Book Club','Networking']::text[] AS themes
),
to_insert AS (
SELECT
-- 8-char ID (hex)
LEFT(ENCODE(gen_random_bytes(4), 'hex'), 8) AS id,
-- Make varied names by mixing a theme and city
(SELECT themes[(gs % array_length(themes,1)) + 1] FROM params) || ' @ ' ||
(SELECT cities[(gs % array_length(cities,1)) + 1] FROM params) AS name,
-- Spread dates across past and future (centered around today)
(CURRENT_DATE + (gs - 50))::date AS date,
-- Varied times: between 08:00 and 19:xx
make_time( (8 + (gs % 12))::int, (gs*7 % 60)::int, 0)::time AS time,
-- City + venue
(SELECT cities[(gs % array_length(cities,1)) + 1] FROM params) || ' ' ||
(SELECT venues[(gs % array_length(venues,1)) + 1] FROM params) AS location,
-- Alternate types
CASE WHEN gs % 2 = 0 THEN 'limited' ELSE 'unlimited' END AS type,
-- Only set attendee_limit for limited events
CASE WHEN gs % 2 = 0 THEN 10 + (gs % 40) ELSE NULL END AS attendee_limit,
-- Rotate through 20 user_ids
'user_' || ((gs % 20) + 1)::text AS user_id,
-- Mix public/private
CASE WHEN gs % 3 = 0 THEN 'private' ELSE 'public' END AS visibility
FROM generate_series(1, 100) AS gs
)
INSERT INTO events (id, name, date, time, location, type, attendee_limit, user_id, visibility, created_at, updated_at)
SELECT id, name, date, time, location, type, attendee_limit, user_id, visibility, NOW(), NOW()
FROM to_insert;
-- -----------------------------
-- Seed RSVPs
-- - For limited events: 0..attendee_limit attendees
-- - For unlimited events: 0..75 attendees
-- -----------------------------
WITH ev AS (
SELECT e.id, e.type, e.attendee_limit
FROM events e
),
counts AS (
SELECT
id,
type,
attendee_limit,
CASE
WHEN type = 'limited' THEN GREATEST(0, LEAST(attendee_limit, FLOOR(random() * (COALESCE(attendee_limit,0) + 1))::int))
ELSE FLOOR(random() * 76)::int
END AS rsvp_count
FROM ev
)
INSERT INTO rsvps (event_id, name, user_id, created_at, updated_at)
SELECT
c.id AS event_id,
'Attendee ' || c.id || '-' || g AS name,
-- distribute user_ids across 200 synthetic users, deterministically mixed per event
'user_' || ((ABS(HASHTEXT(c.id)) + g) % 200 + 1)::text AS user_id,
NOW(), NOW()
FROM counts c
JOIN LATERAL generate_series(1, c.rsvp_count) AS g ON TRUE;
COMMIT;

View File

@@ -1,14 +1,14 @@
version: '3.8'
services: services:
# Database # Database
postgres: postgres:
image: postgres:15-alpine image: postgres:15-alpine
container_name: cactoide-db container_name: cactoide-db
environment: environment:
POSTGRES_DB: ${POSTGRES_DB:-cactoied_database} POSTGRES_DB: ${POSTGRES_DB:-cactoide_database}
POSTGRES_USER: ${POSTGRES_USER:-cactoide} POSTGRES_USER: ${POSTGRES_USER:-cactoide}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-cactoide_password} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-cactoide_password}
expose:
- '${POSTGRES_PORT:-5432}'
ports: ports:
- '${POSTGRES_PORT:-5432}:5432' - '${POSTGRES_PORT:-5432}:5432'
volumes: volumes:
@@ -18,7 +18,7 @@ services:
test: test:
[ [
'CMD-SHELL', 'CMD-SHELL',
'pg_isready -U ${POSTGRES_USER:-cactoide} -d ${POSTGRES_DB:-cactoied_database}' 'pg_isready -U ${POSTGRES_USER:-cactoide} -d ${POSTGRES_DB:-cactoide_database}'
] ]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
@@ -29,11 +29,12 @@ services:
# Application # Application
app: app:
image: ghcr.io/polaroi8d/cactoide/cactoide:${APP_VERSION:-latest} image: ghcr.io/polaroi8d/cactoide/cactoide:${APP_VERSION:-latest}
build: .
container_name: cactoide-app container_name: cactoide-app
ports: ports:
- '${PORT:-3000}:3000' - '${PORT:-5111}:3000'
environment: environment:
DATABASE_URL: postgres://${POSTGRES_USER:-cactoide}:${POSTGRES_PASSWORD:-cactoide_password}@postgres:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-cactoied_database} DATABASE_URL: ${DATABASE_URL}
PORT: 3000 PORT: 3000
HOSTNAME: ${HOSTNAME:-0.0.0.0} HOSTNAME: ${HOSTNAME:-0.0.0.0}
depends_on: depends_on:

View File

@@ -1,6 +0,0 @@
[build]
# Build command for SvelteKit
command = "npm run build"
# Publish directory (where the built files are located)
publish = "build"

33
package-lock.json generated
View File

@@ -10,13 +10,13 @@
"dependencies": { "dependencies": {
"@sveltejs/adapter-node": "^5.3.1", "@sveltejs/adapter-node": "^5.3.1",
"drizzle-orm": "^0.44.5", "drizzle-orm": "^0.44.5",
"fuse.js": "^7.1.0",
"postgres": "^3.4.7" "postgres": "^3.4.7"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.2.5", "@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"@sveltejs/adapter-auto": "^6.0.0", "@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/adapter-netlify": "^5.2.2",
"@sveltejs/kit": "^2.22.0", "@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0", "@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
@@ -1135,13 +1135,6 @@
"url": "https://github.com/sponsors/nzakas" "url": "https://github.com/sponsors/nzakas"
} }
}, },
"node_modules/@iarna/toml": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
"integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==",
"dev": true,
"license": "ISC"
},
"node_modules/@isaacs/fs-minipass": { "node_modules/@isaacs/fs-minipass": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
@@ -1630,21 +1623,6 @@
"@sveltejs/kit": "^2.0.0" "@sveltejs/kit": "^2.0.0"
} }
}, },
"node_modules/@sveltejs/adapter-netlify": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-netlify/-/adapter-netlify-5.2.2.tgz",
"integrity": "sha512-pFWB/lxG8NuJLEYOcPxD059v5QQDFX1vxpBbVobHjgJDCpSDLySGMi4ipDKNPfysqIA9TEG+rwdexz0iaIAocg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@iarna/toml": "^2.2.5",
"esbuild": "^0.25.4",
"set-cookie-parser": "^2.6.0"
},
"peerDependencies": {
"@sveltejs/kit": "^2.4.0"
}
},
"node_modules/@sveltejs/adapter-node": { "node_modules/@sveltejs/adapter-node": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.3.1.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.3.1.tgz",
@@ -3242,6 +3220,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/fuse.js": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=10"
}
},
"node_modules/get-tsconfig": { "node_modules/get-tsconfig": {
"version": "4.10.1", "version": "4.10.1",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",

View File

@@ -1,7 +1,7 @@
{ {
"name": "event-cactus", "name": "cactoide",
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@@ -40,6 +40,7 @@
"dependencies": { "dependencies": {
"@sveltejs/adapter-node": "^5.3.1", "@sveltejs/adapter-node": "^5.3.1",
"drizzle-orm": "^0.44.5", "drizzle-orm": "^0.44.5",
"fuse.js": "^7.1.0",
"postgres": "^3.4.7" "postgres": "^3.4.7"
} }
} }

View File

@@ -3,6 +3,13 @@ import { env } from '$env/dynamic/private';
import * as schema from './schema'; import * as schema from './schema';
import postgres from 'postgres'; import postgres from 'postgres';
const client = postgres(env.DATABASE_URL, {}); // Database connection configuration
const connectionConfig = {
max: 10, // Maximum number of connections
idle_timeout: 20, // Close idle connections after 20 seconds
connect_timeout: 10 // Connection timeout in seconds
};
export const drizzleQuery = drizzle(client, { schema }); const client = postgres(env.DATABASE_URL, connectionConfig);
export const database = drizzle(client, { schema });

View File

@@ -9,11 +9,11 @@ export function load({ cookies }) {
const PATH = '/'; const PATH = '/';
if (!cactoideUserId) { if (!cactoideUserId) {
console.log(`There is no cactoideUserId cookie, generating new one...`); console.debug(`There is no cactoideUserId cookie, generating new one...`);
cookies.set('cactoideUserId', userId, { path: PATH, maxAge: MAX_AGE }); cookies.set('cactoideUserId', userId, { path: PATH, maxAge: MAX_AGE });
} else { } else {
console.log(`cactoideUserId: ${cactoideUserId}`); console.debug(`cactoideUserId: ${cactoideUserId}`);
console.log(`cactoideUserId cookie found, using existing one...`); console.debug(`cactoideUserId cookie found, using existing one...`);
} }
return { return {

View File

@@ -13,18 +13,26 @@
<div class="flex min-h-screen flex-col"> <div class="flex min-h-screen flex-col">
<section class="mx-auto w-full pt-20 pb-20 md:w-3/4"> <section class="mx-auto w-full pt-20 pb-20 md:w-3/4">
<div class="container mx-auto px-4 text-center"> <div class="container mx-auto px-4 text-center">
<h1 class="text-5xl font-bold md:text-7xl lg:text-8xl">Cactoide(ea) 🌵</h1> <h1 class="text-5xl font-bold md:text-7xl lg:text-8xl">
Cactoide(ea)<span class="text-violet-400"
><a href="https://en.wikipedia.org/wiki/Cactoideae" target="_blank">*</a></span
> 🌵
</h1>
<h2 class="mt-6 text-xl md:text-2xl">The Ultimate RSVP Platform</h2> <h2 class="mt-6 text-xl md:text-2xl">The Ultimate RSVP Platform</h2>
<p class="mt-4 text-lg italic md:text-xl"> <p class="mt-4 text-lg italic md:text-xl">
Create, share, and manage events with zero friction. Create, share, and manage events with zero friction.
</p> </p>
<h2 class="mt-6 pt-8 text-xl md:text-2xl">Why Cactoide(ae)?🌵</h2> <h2 class="mt-6 pt-8 text-xl md:text-2xl">
Why Cactoide(ae)<span class="text-violet-400"
><a href="https://en.wikipedia.org/wiki/Cactoideae" target="_blank">*</a></span
>?🌵
</h2>
<p class="mt-4 text-lg md:text-xl"> <p class="mt-4 text-lg md:text-xl">
Like the cactus, great events bloom under any condition when managed with care. Cactoide(ae) Like the cactus, great events bloom under any condition when managed with care. Cactoide(ae)
helps you streamline RSVPs, simplify coordination, and keep every detail efficient—so your helps you streamline RSVPs, simplify coordination, and keep every detail efficient—so your
gatherings are resilient, vibrant, and unforgettabl e. gatherings are resilient, vibrant, and unforgettable.
</p> </p>
<button <button

View File

@@ -1,4 +1,4 @@
import { drizzleQuery } from '$lib/database/db'; import { database } from '$lib/database/db';
import { events } from '$lib/database/schema'; import { events } from '$lib/database/schema';
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types'; import type { Actions } from './$types';
@@ -66,7 +66,7 @@ export const actions: Actions = {
const eventId = generateEventId(); const eventId = generateEventId();
await drizzleQuery await database
.insert(events) .insert(events)
.values({ .values({
id: eventId, id: eventId,

View File

@@ -1,4 +1,4 @@
import { drizzleQuery } from '$lib/database/db'; import { database } from '$lib/database/db';
import { eq, desc } from 'drizzle-orm'; import { eq, desc } from 'drizzle-orm';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { events } from '$lib/database/schema'; import { events } from '$lib/database/schema';
@@ -6,7 +6,7 @@ import { events } from '$lib/database/schema';
export const load: PageServerLoad = async () => { export const load: PageServerLoad = async () => {
try { try {
// Fetch all public events ordered by creation date (newest first) // Fetch all public events ordered by creation date (newest first)
const publicEvents = await drizzleQuery const publicEvents = await database
.select() .select()
.from(events) .from(events)
.where(eq(events.visibility, 'public')) .where(eq(events.visibility, 'public'))

View File

@@ -1,15 +1,51 @@
<script lang="ts"> <script lang="ts">
import type { Event } from '$lib/types'; import type { Event, EventType } from '$lib/types';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import type { PageData } from '../$types'; import type { PageData } from '../$types';
import { formatTime, formatDate } from '$lib/dateFormatter'; import { formatTime, formatDate } from '$lib/dateFormatter';
import Fuse from 'fuse.js';
let publicEvents: Event[] = []; let publicEvents: Event[] = [];
let error = ''; let error = '';
let searchQuery = '';
let selectedEventType: EventType | 'all' = 'all';
let fuse: Fuse<Event>;
export let data: PageData; export let data: PageData;
// Use the server-side data // Use the server-side data
$: publicEvents = data.events; $: publicEvents = data.events;
// Initialize Fuse.js with search options
$: fuse = new Fuse(publicEvents, {
keys: [
{ name: 'name', weight: 0.7 },
{ name: 'location', weight: 0.3 }
],
threshold: 0.3, // Lower threshold = more strict matching
includeScore: true,
includeMatches: true
});
// Filter events based on search query and event type using Fuse.js
$: filteredEvents = (() => {
let events = publicEvents;
// First filter by event type
if (selectedEventType !== 'all') {
events = events.filter((event) => event.type === selectedEventType);
}
// Then apply search query
if (searchQuery.trim() !== '') {
events = fuse.search(searchQuery).map((result) => result.item);
// Re-apply type filter after search
if (selectedEventType !== 'all') {
events = events.filter((event) => event.type === selectedEventType);
}
}
return events;
})();
</script> </script>
<svelte:head> <svelte:head>
@@ -48,12 +84,88 @@
{:else} {:else}
<div class="mx-auto max-w-4xl"> <div class="mx-auto max-w-4xl">
<div class="mb-6"> <div class="mb-6">
<h2 class="text-2xl font-bold text-slate-300">Public Events ({publicEvents.length})</h2> <h2 class="text-2xl font-bold text-slate-300">Public Events ({filteredEvents.length})</h2>
<p class="text-slate-500">Discover events created by the community</p> <p class="text-slate-500">Discover events created by the community</p>
</div> </div>
<!-- Search and Filter Section -->
<div class="mb-8">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-center">
<!-- Search Bar -->
<div class="relative mx-auto max-w-md sm:mx-0">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<svg
class="h-5 w-5 text-slate-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
></path>
</svg>
</div>
<input
type="text"
bind:value={searchQuery}
placeholder="Search events by name, location..."
class="w-full rounded-lg border border-slate-600 bg-slate-800 px-4 py-3 pl-10 text-white placeholder-slate-400 focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 focus:outline-none"
/>
{#if searchQuery}
<button
on:click={() => (searchQuery = '')}
class="absolute inset-y-0 right-0 flex items-center pr-3 text-slate-400 hover:text-slate-300"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</button>
{/if}
</div>
<!-- Event Type Filter -->
<div class="flex items-center justify-center gap-2">
<button
on:click={() => (selectedEventType = 'all')}
class="rounded-sm border px-3 py-2 text-sm font-medium transition-colors {selectedEventType ===
'all'
? 'border-violet-500 bg-violet-500/20 text-violet-400'
: 'border-slate-600 text-slate-400 hover:border-slate-500 hover:text-slate-300'}"
>
All
</button>
<button
on:click={() => (selectedEventType = 'limited')}
class="rounded-sm border px-3 py-2 text-sm font-medium transition-colors {selectedEventType ===
'limited'
? 'border-amber-600 bg-amber-600/20 text-amber-600'
: 'border-slate-600 text-slate-400 hover:border-slate-500 hover:text-slate-300'}"
>
Limited
</button>
<button
on:click={() => (selectedEventType = 'unlimited')}
class="rounded-sm border px-3 py-2 text-sm font-medium transition-colors {selectedEventType ===
'unlimited'
? 'border-teal-500 bg-teal-500/20 text-teal-500'
: 'border-slate-600 text-slate-400 hover:border-slate-500 hover:text-slate-300'}"
>
Unlimited
</button>
</div>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{#each publicEvents as event, i (i)} {#each filteredEvents as event, i (i)}
<div class="rounded-sm border border-slate-200 p-6 shadow-sm"> <div class="rounded-sm border border-slate-200 p-6 shadow-sm">
<div class="mb-4"> <div class="mb-4">
<h3 class="mb-2 text-xl font-bold text-slate-300">{event.name}</h3> <h3 class="mb-2 text-xl font-bold text-slate-300">{event.name}</h3>
@@ -110,6 +222,14 @@
</div> </div>
{/each} {/each}
</div> </div>
{#if searchQuery && filteredEvents.length === 0}
<div class="mt-8 text-center">
<div class="mb-4 text-4xl">🔍</div>
<h3 class="mb-2 text-xl font-bold text-slate-300">No events found</h3>
<p class="text-slate-500">Try adjusting your search terms or browse all events</p>
</div>
{/if}
</div> </div>
{/if} {/if}
</div> </div>

View File

@@ -1,4 +1,4 @@
import { drizzleQuery } from '$lib/database/db'; import { database } from '$lib/database/db';
import { events } from '$lib/database/schema'; import { events } from '$lib/database/schema';
import { fail } from '@sveltejs/kit'; import { fail } from '@sveltejs/kit';
import { eq, desc } from 'drizzle-orm'; import { eq, desc } from 'drizzle-orm';
@@ -11,14 +11,12 @@ export const load = async ({ cookies }) => {
} }
try { try {
const userEvents = await drizzleQuery const userEvents = await database
.select() .select()
.from(events) .from(events)
.where(eq(events.userId, userId)) .where(eq(events.userId, userId))
.orderBy(desc(events.createdAt)); .orderBy(desc(events.createdAt));
console.log(userEvents);
const transformedEvents = userEvents.map((event) => ({ const transformedEvents = userEvents.map((event) => ({
id: event.id, id: event.id,
name: event.name, name: event.name,
@@ -52,7 +50,7 @@ export const actions: Actions = {
try { try {
// First verify the user owns this event // First verify the user owns this event
const [eventData] = await drizzleQuery.select().from(events).where(eq(events.id, eventId)); const [eventData] = await database.select().from(events).where(eq(events.id, eventId));
if (!eventData) { if (!eventData) {
return fail(404, { error: 'Event not found' }); return fail(404, { error: 'Event not found' });
@@ -63,7 +61,7 @@ export const actions: Actions = {
} }
// Delete the event (RSVPs will be deleted automatically due to CASCADE) // Delete the event (RSVPs will be deleted automatically due to CASCADE)
await drizzleQuery.delete(events).where(eq(events.id, eventId)); await database.delete(events).where(eq(events.id, eventId));
return { success: true }; return { success: true };
} catch (error) { } catch (error) {

View File

@@ -60,7 +60,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title>My Events - Event Cactus</title> <title>My Events - Cactoide</title>
</svelte:head> </svelte:head>
<div class="flex min-h-screen flex-col"> <div class="flex min-h-screen flex-col">

View File

@@ -1,4 +1,4 @@
import { drizzleQuery } from '$lib/database/db'; import { database } from '$lib/database/db';
import { events, rsvps } from '$lib/database/schema'; import { events, rsvps } from '$lib/database/schema';
import { eq, asc } from 'drizzle-orm'; import { eq, asc } from 'drizzle-orm';
import { error, fail } from '@sveltejs/kit'; import { error, fail } from '@sveltejs/kit';
@@ -14,12 +14,8 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
try { try {
// Fetch event and RSVPs in parallel // Fetch event and RSVPs in parallel
const [eventData, rsvpData] = await Promise.all([ const [eventData, rsvpData] = await Promise.all([
drizzleQuery.select().from(events).where(eq(events.id, eventId)).limit(1), database.select().from(events).where(eq(events.id, eventId)).limit(1),
drizzleQuery database.select().from(rsvps).where(eq(rsvps.eventId, eventId)).orderBy(asc(rsvps.createdAt))
.select()
.from(rsvps)
.where(eq(rsvps.eventId, eventId))
.orderBy(asc(rsvps.createdAt))
]); ]);
if (!eventData[0]) { if (!eventData[0]) {
@@ -75,42 +71,33 @@ export const actions: Actions = {
const name = formData.get('newAttendeeName') as string; const name = formData.get('newAttendeeName') as string;
const userId = cookies.get('cactoideUserId'); const userId = cookies.get('cactoideUserId');
console.log(`name: ${name}`);
console.log(`userId: ${userId}`);
if (!name?.trim() || !userId) { if (!name?.trim() || !userId) {
return fail(400, { error: 'Name and user ID are required' }); return fail(400, { error: 'Name and user ID are required' });
} }
try { try {
// Check if event exists and get its details // Check if event exists and get its details
const [eventData] = await drizzleQuery.select().from(events).where(eq(events.id, eventId)); const [eventData] = await database.select().from(events).where(eq(events.id, eventId));
if (!eventData) { if (!eventData) {
return fail(404, { error: 'Event not found' }); return fail(404, { error: 'Event not found' });
} }
// Check if event is full (for limited type events) // Check if event is full (for limited type events)
if (eventData.type === 'limited' && eventData.attendeeLimit) { if (eventData.type === 'limited' && eventData.attendeeLimit) {
const currentRSVPs = await drizzleQuery const currentRSVPs = await database.select().from(rsvps).where(eq(rsvps.eventId, eventId));
.select()
.from(rsvps)
.where(eq(rsvps.eventId, eventId));
if (currentRSVPs.length >= eventData.attendeeLimit) { if (currentRSVPs.length >= eventData.attendeeLimit) {
return fail(400, { error: 'Event is full' }); return fail(400, { error: 'Event is full' });
} }
} }
// Check if name is already in the list // Check if name is already in the list
const existingRSVPs = await drizzleQuery const existingRSVPs = await database.select().from(rsvps).where(eq(rsvps.eventId, eventId));
.select()
.from(rsvps)
.where(eq(rsvps.eventId, eventId));
if (existingRSVPs.some((rsvp) => rsvp.name.toLowerCase() === name.toLowerCase())) { if (existingRSVPs.some((rsvp) => rsvp.name.toLowerCase() === name.toLowerCase())) {
return fail(400, { error: 'Name already exists for this event' }); return fail(400, { error: 'Name already exists for this event' });
} }
// Add RSVP to database // Add RSVP to database
await drizzleQuery.insert(rsvps).values({ await database.insert(rsvps).values({
eventId: eventId, eventId: eventId,
name: name.trim(), name: name.trim(),
userId: userId, userId: userId,
@@ -134,7 +121,7 @@ export const actions: Actions = {
} }
try { try {
await drizzleQuery.delete(rsvps).where(eq(rsvps.id, rsvpId)); await database.delete(rsvps).where(eq(rsvps.id, rsvpId));
return { success: true, type: 'remove' }; return { success: true, type: 'remove' };
} catch (err) { } catch (err) {
console.error('Error removing RSVP:', err); console.error('Error removing RSVP:', err);

View File

@@ -0,0 +1,16 @@
// src/routes/healthz/+server.ts
import { json } from '@sveltejs/kit';
import { database } from '$lib/database/db';
import { sql } from 'drizzle-orm';
export async function GET() {
try {
await database.execute(sql`select 1`);
return json({ ok: true }, { headers: { 'cache-control': 'no-store' } });
} catch (err) {
return json(
{ ok: false, error: (err as Error)?.message, message: 'Database unreachable.' },
{ status: 503, headers: { 'cache-control': 'no-store' } }
);
}
}

View File

@@ -8,7 +8,6 @@ const config = {
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { kit: {
// Using Netlify adapter for deployment
adapter: adapter({ adapter: adapter({
// if you want to use 'split' mode, set this to 'split' // if you want to use 'split' mode, set this to 'split'
// and create a _redirects file with the redirects you want // and create a _redirects file with the redirects you want

View File

@@ -4,9 +4,7 @@ export default {
theme: { theme: {
extend: { extend: {
fontFamily: { fontFamily: {
mono: ['JetBrains Mono', 'Fira Code', 'monospace'], mono: ['JetBrains Mono', 'Fira Code', 'monospace']
display: ['Inter', 'system-ui', 'sans-serif'],
sans: ['Inter', 'system-ui', 'sans-serif']
} }
} }
} }