From 3d133a6539cb429d40f9dd7f254f1e136302ae4b Mon Sep 17 00:00:00 2001 From: Levente Orban Date: Wed, 27 Aug 2025 08:47:12 +0200 Subject: [PATCH] feat: implement psql and improvements --- README.md | 2 +- package-lock.json | 666 +++++++++++++++++++++++++- package.json | 5 +- src/lib/components/Navbar.svelte | 2 +- src/lib/database/db.ts | 8 + src/lib/database/schema.ts | 97 ++++ src/lib/generateUserId.ts | 5 + src/routes/+error.svelte | 2 +- src/routes/+layout.server.ts | 22 + src/routes/+layout.svelte | 13 +- src/routes/+page.svelte | 9 +- src/routes/create/+page.server.ts | 89 ++++ src/routes/create/+page.svelte | 118 ++--- src/routes/discover/+page.server.ts | 41 ++ src/routes/discover/+page.svelte | 54 +-- src/routes/event/+page.server.ts | 74 +++ src/routes/event/+page.svelte | 94 ++-- src/routes/event/[id]/+error.svelte | 43 +- src/routes/event/[id]/+page.server.ts | 145 ++++++ src/routes/event/[id]/+page.svelte | 173 ++++--- 20 files changed, 1379 insertions(+), 283 deletions(-) create mode 100644 src/lib/database/db.ts create mode 100644 src/lib/database/schema.ts create mode 100644 src/lib/generateUserId.ts create mode 100644 src/routes/+layout.server.ts create mode 100644 src/routes/create/+page.server.ts create mode 100644 src/routes/discover/+page.server.ts create mode 100644 src/routes/event/+page.server.ts create mode 100644 src/routes/event/[id]/+page.server.ts diff --git a/README.md b/README.md index 151d5c2..4f3151d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Event Cactus 🌵 +# Cactoide 🌵 A mobile-first event RSVP SaaS application. Create events, share unique URLs, and collect RSVPs without any registration required. diff --git a/package-lock.json b/package-lock.json index ef305d8..c38ac09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "event-cactus", "version": "0.0.1", "dependencies": { - "@supabase/supabase-js": "^2.55.0" + "@supabase/supabase-js": "^2.55.0", + "drizzle-orm": "^0.44.5", + "postgres": "^3.4.7" }, "devDependencies": { "@eslint/compat": "^1.2.5", @@ -20,6 +22,7 @@ "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.0.0", + "drizzle-kit": "^0.31.4", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-svelte": "^3.0.0", @@ -35,6 +38,449 @@ "vite": "^7.0.4" } }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", @@ -2022,6 +2468,13 @@ "node": ">=8" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2202,6 +2655,147 @@ "dev": true, "license": "MIT" }, + "node_modules/drizzle-kit": { + "version": "0.31.4", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.4.tgz", + "integrity": "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "esbuild-register": "^3.5.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.44.5", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.5.tgz", + "integrity": "sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -2258,6 +2852,19 @@ "@esbuild/win32-x64": "0.25.9" } }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2651,6 +3258,19 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3568,6 +4188,19 @@ "node": ">=4" } }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3747,6 +4380,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -3893,6 +4536,16 @@ "node": ">=18" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3903,6 +4556,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/package.json b/package.json index 77d3a75..b163c58 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.0.0", + "drizzle-kit": "^0.31.4", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-svelte": "^3.0.0", @@ -39,6 +40,8 @@ "vite": "^7.0.4" }, "dependencies": { - "@supabase/supabase-js": "^2.55.0" + "@supabase/supabase-js": "^2.55.0", + "drizzle-orm": "^0.44.5", + "postgres": "^3.4.7" } } diff --git a/src/lib/components/Navbar.svelte b/src/lib/components/Navbar.svelte index bb32267..af65a16 100644 --- a/src/lib/components/Navbar.svelte +++ b/src/lib/components/Navbar.svelte @@ -21,7 +21,7 @@ on:click={() => navigateTo('/')} class="cursor-pointer text-2xl font-medium text-violet-400" > - Event Cactus + Cactoide diff --git a/src/lib/database/db.ts b/src/lib/database/db.ts new file mode 100644 index 0000000..d594bb9 --- /dev/null +++ b/src/lib/database/db.ts @@ -0,0 +1,8 @@ +import { drizzle } from 'drizzle-orm/postgres-js'; +import { env } from '$env/dynamic/private'; +import * as schema from './schema'; +import postgres from 'postgres'; + +const client = postgres(env.DATABASE_URL, {}); + +export const drizzleQuery = drizzle(client, { schema }); diff --git a/src/lib/database/schema.ts b/src/lib/database/schema.ts new file mode 100644 index 0000000..fdc786a --- /dev/null +++ b/src/lib/database/schema.ts @@ -0,0 +1,97 @@ +import { + pgTable, + varchar, + integer, + date, + time, + timestamp, + uuid, + pgEnum, + index, + uniqueIndex, + check +} from 'drizzle-orm/pg-core'; +import { sql } from 'drizzle-orm'; +import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'; + +// --- Enums (matching the SQL CHECK constraints) +export const eventTypeEnum = pgEnum('event_type', ['limited', 'unlimited']); +export const visibilityEnum = pgEnum('visibility', ['public', 'private']); + +// --- Events table +export const events = pgTable( + 'events', + { + id: varchar('id', { length: 8 }).primaryKey(), + name: varchar('name', { length: 100 }).notNull(), + date: date('date', { mode: 'string' }).notNull(), // ISO 'YYYY-MM-DD' + time: time('time', { withTimezone: false }).notNull(), // 'HH:MM:SS' + location: varchar('location', { length: 200 }).notNull(), + type: eventTypeEnum('type').notNull(), + attendeeLimit: integer('attendee_limit'), // nullable in SQL + userId: varchar('user_id', { length: 100 }).notNull(), + visibility: visibilityEnum('visibility').notNull().default('public'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow() + }, + (t) => ({ + // Primary key already indexes id + idxEventsId: index('idx_events_id').on(t.id), + idxEventsUserId: index('idx_events_user_id').on(t.userId), + idxEventsVisibility: index('idx_events_visibility').on(t.visibility), + // CHECK (attendee_limit > 0) like in SQL (still allows NULL) + attendeeLimitPositive: check( + 'events_attendee_limit_positive', + sql`${t.attendeeLimit} IS NULL OR ${t.attendeeLimit} > 0` + ) + }) +); + +// --- RSVPs table +export const rsvps = pgTable( + 'rsvps', + { + id: uuid('id').defaultRandom().primaryKey(), // gen_random_uuid() + eventId: varchar('event_id', { length: 8 }) + .notNull() + .references(() => events.id, { onDelete: 'cascade' }), + name: varchar('name', { length: 50 }).notNull(), + userId: varchar('user_id', { length: 100 }).notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow() + }, + (t) => ({ + idxRsvpsEventId: index('idx_rsvps_event_id').on(t.eventId), + idxRsvpsUserId: index('idx_rsvps_user_id').on(t.userId), + idxRsvpsCreatedAt: index('idx_rsvps_created_at').on(t.createdAt), + // UNIQUE(event_id, name) constraint + uqEventName: uniqueIndex('rsvps_event_id_name_unique').on(t.eventId, t.name) + }) +); + +// --- Relations (optional but handy for type safety) +import { relations } from 'drizzle-orm'; + +export const eventsRelations = relations(events, ({ many }) => ({ + rsvps: many(rsvps) +})); + +export const rsvpsRelations = relations(rsvps, ({ one }) => ({ + event: one(events, { + fields: [rsvps.eventId], + references: [events.id] + }) +})); + +// --- Inferred types for use in the application +export type Event = InferSelectModel; +export type NewEvent = InferInsertModel; +export type Rsvp = InferSelectModel; +export type NewRsvp = InferInsertModel; + +// --- Additional utility types +export type EventWithRsvps = Event & { + rsvps: Rsvp[]; +}; + +export type CreateEventData = Omit; +export type CreateRsvpData = Omit; diff --git a/src/lib/generateUserId.ts b/src/lib/generateUserId.ts new file mode 100644 index 0000000..fd8ecee --- /dev/null +++ b/src/lib/generateUserId.ts @@ -0,0 +1,5 @@ +export const generateUserId = () => { + const userId = 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + + return userId; +}; diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte index f7548ff..e14f840 100644 --- a/src/routes/+error.svelte +++ b/src/routes/+error.svelte @@ -6,7 +6,7 @@ - Error - Event Cactus + Error - Cactoide
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..0b0c075 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,22 @@ +import { generateUserId } from '$lib/generateUserId.js'; + +export function load({ cookies }) { + 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.log(`There is no cactoideUserId cookie, generating new one...`); + cookies.set('cactoideUserId', userId, { path: PATH, maxAge: MAX_AGE }); + } else { + console.log(`cactoideUserId: ${cactoideUserId}`); + console.log(`cactoideUserId cookie found, using existing one...`); + } + + return { + cactoideUserId + }; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 2550cf0..3f4c1a3 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,10 +1,12 @@ - Event Cactus - + Cactoide - @@ -25,7 +27,14 @@
-

© 2025 Event Cactus

+

+ Your UserID storated as a cookie: {data.cactoideUserId + ? data.cactoideUserId + : 'First time visiting. Generating new UserID...'} +

+

© 2025 Cactoide

diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index b314344..57deb78 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -3,7 +3,7 @@ - Event Cactus - The RSVP site + Cactoide - The RSVP site

- Event Cactus 🌵 + Cactoide 🌵

+

The Ultimate RSVP Platform

Create, share, and manage events with zero friction.

@@ -71,7 +72,7 @@

- Why Event Cactus? + Why Cactoide?

@@ -190,7 +191,7 @@

Ready to Create Your First Event?

-

Join thousands of event organizers who trust Event Cactus

+

Join thousands of event organizers who trust Cactoide

{:else if publicEvents.length === 0} @@ -73,17 +55,17 @@ on:click={() => goto('/create')} class="rounded-sm border-2 border-violet-500 px-8 py-4 font-bold duration-400 hover:scale-110 hover:bg-violet-500/10" > - Create Your First Event + Create
{:else}
-

Public Events ({publicEvents.length})

+

Public Events ({publicEvents.length})

Discover events created by the community

-
+
{#each publicEvents as event}
@@ -118,12 +100,14 @@ {event.location}
- + {event.type === 'limited' ? 'Limited' : 'Unlimited'} - {#if event.type === 'limited' && event.attendee_limit} - • {event.attendee_limit} max - {/if}
diff --git a/src/routes/event/+page.server.ts b/src/routes/event/+page.server.ts new file mode 100644 index 0000000..c88186e --- /dev/null +++ b/src/routes/event/+page.server.ts @@ -0,0 +1,74 @@ +import { drizzleQuery } from '$lib/database/db'; +import { events } from '$lib/database/schema'; +import { fail } from '@sveltejs/kit'; +import { eq, desc } from 'drizzle-orm'; + +export const load = async ({ cookies }) => { + const userId = cookies.get('cactoideUserId'); + + if (!userId) { + return { events: [] }; + } + + try { + const userEvents = await drizzleQuery + .select() + .from(events) + .where(eq(events.userId, userId)) + .orderBy(desc(events.createdAt)); + + console.log(userEvents); + + const transformedEvents = userEvents.map((event) => ({ + id: event.id, + name: event.name, + date: event.date, + time: event.time, + location: event.location, + type: event.type, + attendee_limit: event.attendeeLimit, + visibility: event.visibility, + user_id: event.userId, + created_at: event.createdAt?.toISOString() || new Date().toISOString(), + updated_at: event.updatedAt?.toISOString() || new Date().toISOString() + })); + + return { events: transformedEvents }; + } catch (error) { + console.error('Error loading user events:', error); + return { events: [] }; + } +}; + +export const actions: Actions = { + deleteEvent: async ({ request, cookies }) => { + const formData = await request.formData(); + const eventId = formData.get('eventId') as string; + const userId = cookies.get('cactoideUserId'); + + if (!eventId || !userId) { + return fail(400, { error: 'Event ID and User ID are required' }); + } + + try { + // First verify the user owns this event + const [eventData] = await drizzleQuery.select().from(events).where(eq(events.id, eventId)); + + if (!eventData) { + return fail(404, { error: 'Event not found' }); + } + + if (eventData.userId !== userId) { + return fail(403, { error: 'You do not have permission to delete this event' }); + } + + // Delete the event (RSVPs will be deleted automatically due to CASCADE) + await drizzleQuery.delete(events).where(eq(events.id, eventId)); + + return { success: true }; + } catch (error) { + console.error('Error deleting event:', error); + return fail(500, { error: 'Failed to delete event' }); + } + } +}; diff --git a/src/routes/event/+page.svelte b/src/routes/event/+page.svelte index a1de180..587a70d 100644 --- a/src/routes/event/+page.svelte +++ b/src/routes/event/+page.svelte @@ -1,43 +1,16 @@ - Error - Event Cactus + Error - Cactoide
- -
-
-

Error

-

Something went wrong

-
-
-
@@ -25,26 +17,25 @@
🚨

Something Went Wrong

-

+

{error?.message || 'An unexpected error occurred.'}

- -
- - - -
+
+ + + +
diff --git a/src/routes/event/[id]/+page.server.ts b/src/routes/event/[id]/+page.server.ts new file mode 100644 index 0000000..d54a96d --- /dev/null +++ b/src/routes/event/[id]/+page.server.ts @@ -0,0 +1,145 @@ +import { drizzleQuery } from '$lib/database/db'; +import { events, rsvps } from '$lib/database/schema'; +import { eq, asc } from 'drizzle-orm'; +import { error, fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; + +export const load: PageServerLoad = async ({ params, cookies }) => { + const eventId = params.id; + + if (!eventId) { + throw error(404, 'EventId not found'); + } + + try { + // Fetch event and RSVPs in parallel + const [eventData, rsvpData] = await Promise.all([ + drizzleQuery.select().from(events).where(eq(events.id, eventId)).limit(1), + drizzleQuery + .select() + .from(rsvps) + .where(eq(rsvps.eventId, eventId)) + .orderBy(asc(rsvps.createdAt)) + ]); + + if (!eventData[0]) { + throw error(404, 'Event not found'); + } + + const event = eventData[0]; + const eventRsvps = rsvpData; + + // Transform the data to match the expected interface + const transformedEvent = { + id: event.id, + name: event.name, + date: event.date, + time: event.time, + location: event.location, + type: event.type, + attendee_limit: event.attendeeLimit, + visibility: event.visibility, + user_id: event.userId, + created_at: event.createdAt?.toISOString() || new Date().toISOString(), + updated_at: event.updatedAt?.toISOString() || new Date().toISOString() + }; + + const transformedRsvps = eventRsvps.map((rsvp) => ({ + id: rsvp.id, + event_id: rsvp.eventId, + name: rsvp.name, + user_id: rsvp.userId, + created_at: rsvp.createdAt?.toISOString() || new Date().toISOString() + })); + + const userId = cookies.get('cactoideUserId'); + + return { + event: transformedEvent, + rsvps: transformedRsvps, + userId: userId + }; + } catch (err) { + if (err instanceof Response) throw err; // This is the 404 error + + console.error('Error loading event:', err); + throw error(500, 'Failed to load event'); + } +}; + +export const actions: Actions = { + addRSVP: async ({ request, params, cookies }) => { + const eventId = params.id; + const formData = await request.formData(); + + const name = formData.get('newAttendeeName') as string; + const userId = cookies.get('cactoideUserId'); + + console.log(`name: ${name}`); + console.log(`userId: ${userId}`); + + if (!name?.trim() || !userId) { + return fail(400, { error: 'Name and user ID are required' }); + } + + try { + // Check if event exists and get its details + const [eventData] = await drizzleQuery.select().from(events).where(eq(events.id, eventId)); + if (!eventData) { + return fail(404, { error: 'Event not found' }); + } + + // Check if event is full (for limited type events) + if (eventData.type === 'limited' && eventData.attendeeLimit) { + const currentRSVPs = await drizzleQuery + .select() + .from(rsvps) + .where(eq(rsvps.eventId, eventId)); + if (currentRSVPs.length >= eventData.attendeeLimit) { + return fail(400, { error: 'Event is full' }); + } + } + + // Check if name is already in the list + const existingRSVPs = await drizzleQuery + .select() + .from(rsvps) + .where(eq(rsvps.eventId, eventId)); + if (existingRSVPs.some((rsvp) => rsvp.name.toLowerCase() === name.toLowerCase())) { + return fail(400, { error: 'Name already exists for this event' }); + } + + // Add RSVP to database + await drizzleQuery.insert(rsvps).values({ + eventId: eventId, + name: name.trim(), + userId: userId, + createdAt: new Date() + }); + + return { success: true }; + } catch (err) { + console.error('Error adding RSVP:', err); + return fail(500, { error: 'Failed to add RSVP' }); + } + }, + + removeRSVP: async ({ request, params }) => { + const eventId = params.id; + const formData = await request.formData(); + + const rsvpId = formData.get('rsvpId') as string; + + if (!rsvpId) { + return fail(400, { error: 'RSVP ID is required' }); + } + + try { + await drizzleQuery.delete(rsvps).where(eq(rsvps.id, rsvpId)); + return { success: true }; + } catch (err) { + console.error('Error removing RSVP:', err); + return fail(500, { error: 'Failed to remove RSVP' }); + } + } +}; diff --git a/src/routes/event/[id]/+page.svelte b/src/routes/event/[id]/+page.svelte index 4b2cc8d..0b5f98b 100644 --- a/src/routes/event/[id]/+page.svelte +++ b/src/routes/event/[id]/+page.svelte @@ -1,51 +1,39 @@ - {event?.name || 'Event'} - Event Cactus + {event?.name || 'Event'} - Cactoide
@@ -180,16 +142,20 @@
- + {event.type === 'limited' ? 'Limited' : 'Unlimited'} - {event.visibility === 'public' ? '🌍 Public' : '🔒 Private'} + {event.visibility === 'public' ? 'Public' : 'Private'}
@@ -216,13 +182,30 @@

Maximum capacity reached

{:else} - + { + isAddingRSVP = true; + clearMessages(); + return async ({ result, update }) => { + isAddingRSVP = false; + if (result.type === 'failure') { + error = result.data?.error || 'Failed to add RSVP'; + } + update(); + }; + }} + class="space-y-4" + > +
{#if attendee.user_id === currentUserId} - + + + {/if}
{/each}