mirror of
https://github.com/polaroi8d/cactoide.git
synced 2026-08-12 03:09:11 +00:00
Compare commits
6 Commits
fix/missin
...
feat/user-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdc04502f9 | ||
|
|
d18afc43da | ||
|
|
662476f820 | ||
|
|
7ebf95bb16 | ||
|
|
0491ec4c4b | ||
|
|
fcdef065d7 |
4
.github/workflows/build-and-push.yml
vendored
4
.github/workflows/build-and-push.yml
vendored
@@ -1,5 +1,7 @@
|
|||||||
# .github/workflows/docker-build-and-push.yml
|
|
||||||
name: build & push the images
|
name: build & push the images
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|||||||
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
@@ -1,4 +1,7 @@
|
|||||||
name: test & build
|
name: test & build
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Like the cactus, great events bloom under any condition when managed with care.
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://cactoide.org/" target="blank">
|
<a href="https://cactoide.org/" target="blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="actoide" src="https://github.com/user-attachments/assets/30b87181-1e3b-49d0-869e-bef6dcf7f777" width="840">
|
<img alt="actoide" src="https://github.com/user-attachments/assets/a7f7a732-1279-486e-808c-1d2348c68780" width="840">
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -27,17 +27,16 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cactoideUserId = event.cookies.get('cactoideUserId');
|
const cactoideUserId = event.cookies.get('cactoideUserId');
|
||||||
const userId = generateUserId();
|
|
||||||
|
|
||||||
const DAYS = 400; // practical upper bound in many browsers for cookies
|
const DAYS = 400; // practical upper bound in many browsers for cookies
|
||||||
const MAX_AGE = 60 * 60 * 24 * DAYS;
|
const MAX_AGE = 60 * 60 * 24 * DAYS;
|
||||||
const PATH = '/';
|
const PATH = '/';
|
||||||
|
|
||||||
if (!cactoideUserId) {
|
if (!cactoideUserId) {
|
||||||
logger.debug({ userId }, 'No cactoideUserId cookie found, generating new one');
|
logger.debug('No cactoideUserId cookie found, generating new one');
|
||||||
event.cookies.set('cactoideUserId', userId, { path: PATH, maxAge: MAX_AGE });
|
event.cookies.set('cactoideUserId', generateUserId(), { path: PATH, maxAge: MAX_AGE });
|
||||||
} else {
|
} else {
|
||||||
logger.debug({ cactoideUserId }, 'cactoideUserId cookie found, using existing one');
|
logger.debug('cactoideUserId cookie found, using existing one');
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolve(event);
|
return resolve(event);
|
||||||
|
|||||||
19
src/lib/components/FeatureCard.svelte
Normal file
19
src/lib/components/FeatureCard.svelte
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { t } from '$lib/i18n/i18n.js';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
emoji: string;
|
||||||
|
titleKey: string;
|
||||||
|
descriptionKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { emoji, titleKey, descriptionKey }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="rounded-sm border p-4 text-center">
|
||||||
|
<div class="mx-auto mb-2 flex h-20 w-20 items-center justify-center rounded-full">
|
||||||
|
<span class="text-4xl">{emoji}</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-white">{t(titleKey)}</h3>
|
||||||
|
<p class="">{t(descriptionKey)}</p>
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
export const generateUserId = () => {
|
import { randomUUID } from 'crypto';
|
||||||
const userId = 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
||||||
|
|
||||||
return userId;
|
// This id is the only credential the app has — it must not be guessable,
|
||||||
};
|
// and it must never be serialized to the client.
|
||||||
|
export const generateUserId = () => 'user_' + randomUUID();
|
||||||
|
|||||||
@@ -259,8 +259,6 @@
|
|||||||
"layout": {
|
"layout": {
|
||||||
"defaultTitle": "Cactoide -",
|
"defaultTitle": "Cactoide -",
|
||||||
"defaultDescription": "Crea e gestisci gli RSVP degli eventi",
|
"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"
|
"copyright": "© 2025 Cactoide"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,10 @@
|
|||||||
"description": "Create and manage event RSVPs. No registration required, instant sharing.",
|
"description": "Create and manage event RSVPs. No registration required, instant sharing.",
|
||||||
"mainTitle": "Cactoide(ea)",
|
"mainTitle": "Cactoide(ea)",
|
||||||
"subtitle": "The Ultimate RSVP Platform",
|
"subtitle": "The Ultimate RSVP Platform",
|
||||||
"tagline": "Create, share, and manage events with zero friction.",
|
"tagline": "A federated mobile-first event RSVP platform that lets you create events, share unique URLs, and collect RSVPs without any registration required. With built-in federation, discover and share events across a decentralized network of instances.",
|
||||||
|
"openSourceTitle": "Open Source & Self-Hostable",
|
||||||
|
"openSourceDescription": "Cactoide is open source and easily self-hostable. View the source code, contribute, or host your own instance.",
|
||||||
|
"viewOnGitHub": "View on GitHub",
|
||||||
"whyCactoideTitle": "Why Cactoide(ae)?🌵",
|
"whyCactoideTitle": "Why Cactoide(ae)?🌵",
|
||||||
"whyCactoideDescription": "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 gatherings are resilient, vibrant, and unforgettable.",
|
"whyCactoideDescription": "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 gatherings are resilient, vibrant, and unforgettable.",
|
||||||
"createEventNow": "Create Event Now",
|
"createEventNow": "Create Event Now",
|
||||||
@@ -127,6 +130,10 @@
|
|||||||
"smartLimitsDescription": "Choose between unlimited RSVPs or set a limited capacity. Perfect for any event size.",
|
"smartLimitsDescription": "Choose between unlimited RSVPs or set a limited capacity. Perfect for any event size.",
|
||||||
"effortlessSimplicityTitle": "Effortless Simplicity",
|
"effortlessSimplicityTitle": "Effortless Simplicity",
|
||||||
"effortlessSimplicityDescription": "Designed to be instantly clear and easy. No learning curve — just open, create, and go.",
|
"effortlessSimplicityDescription": "Designed to be instantly clear and easy. No learning curve — just open, create, and go.",
|
||||||
|
"inviteLinksTitle": "Invite Links",
|
||||||
|
"inviteLinksDescription": "Create invite-only events with special links. Only people with the specific invite link can RSVP, giving you full control over who can attend.",
|
||||||
|
"federationTitle": "Federation",
|
||||||
|
"federationDescription": "Connect with other Cactoide instances to discover events across the network. Share your public events and create a decentralized event discovery network.",
|
||||||
"howItWorksTitle": "How It Works",
|
"howItWorksTitle": "How It Works",
|
||||||
"step1Title": "Create Event",
|
"step1Title": "Create Event",
|
||||||
"step1Description": "Fill out a simple form with event details. Choose between limited or unlimited capacity.",
|
"step1Description": "Fill out a simple form with event details. Choose between limited or unlimited capacity.",
|
||||||
@@ -284,8 +291,6 @@
|
|||||||
"layout": {
|
"layout": {
|
||||||
"defaultTitle": "Cactoide -",
|
"defaultTitle": "Cactoide -",
|
||||||
"defaultDescription": "Create and manage event RSVPs",
|
"defaultDescription": "Create and manage event RSVPs",
|
||||||
"userIdCookieText": "Your UserID stored as a cookie:",
|
|
||||||
"firstTimeVisiting": "First time visiting. Generating new UserID...",
|
|
||||||
"copyright": "© 2025 Cactoide"
|
"copyright": "© 2025 Cactoide"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface Event {
|
|||||||
type: EventType;
|
type: EventType;
|
||||||
attendee_limit?: number;
|
attendee_limit?: number;
|
||||||
visibility: EventVisibility;
|
visibility: EventVisibility;
|
||||||
user_id: string;
|
is_creator?: boolean; // Optional: absent on events fetched from federated instances
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
federation?: boolean; // Optional: true if event is from a federated instance
|
federation?: boolean; // Optional: true if event is from a federated instance
|
||||||
@@ -25,7 +25,7 @@ export interface RSVP {
|
|||||||
id: string;
|
id: string;
|
||||||
event_id: string;
|
event_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
user_id: string;
|
is_mine: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export function load({ cookies }) {
|
|
||||||
const cactoideUserId = cookies.get('cactoideUserId');
|
|
||||||
|
|
||||||
return {
|
|
||||||
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, children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -28,12 +28,6 @@
|
|||||||
<footer class="py-12">
|
<footer class="py-12">
|
||||||
<div class="container mx-auto px-4 text-center">
|
<div class="container mx-auto px-4 text-center">
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<p class="mb-4 text-gray-100/80">
|
|
||||||
{t('layout.userIdCookieText')}
|
|
||||||
<span class="font-bold text-violet-400"
|
|
||||||
>{data.cactoideUserId ? data.cactoideUserId : t('layout.firstTimeVisiting')}</span
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<p>{t('layout.copyright')}</p>
|
<p>{t('layout.copyright')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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 FeatureCard from '$lib/components/FeatureCard.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -22,6 +23,34 @@
|
|||||||
{t('home.tagline')}
|
{t('home.tagline')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- Open Source Section -->
|
||||||
|
<div class="mt-8 flex items-center justify-center gap-3">
|
||||||
|
<a
|
||||||
|
href="https://github.com/polaroi8d/cactoide"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="group flex items-center gap-2 rounded-sm border-2 border-violet-500/50 px-6 py-3 text-sm font-medium transition-all duration-300 hover:scale-105 hover:border-violet-500 hover:bg-violet-500/10 md:text-base"
|
||||||
|
aria-label={t('home.viewOnGitHub')}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 transition-transform group-hover:scale-110"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>{t('home.viewOnGitHub')}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-sm text-slate-400 md:text-base">
|
||||||
|
{t('home.openSourceDescription')}
|
||||||
|
</p>
|
||||||
|
|
||||||
<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
|
||||||
@@ -65,72 +94,54 @@
|
|||||||
<h2 class=" mb-16 text-center text-4xl font-bold">
|
<h2 class=" mb-16 text-center text-4xl font-bold">
|
||||||
{t('home.whyCactoideFeatureTitle')}
|
{t('home.whyCactoideFeatureTitle')}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<!-- Feature 1 -->
|
<FeatureCard
|
||||||
<div class="rounded-sm border p-8 text-center">
|
emoji="🎯"
|
||||||
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
|
titleKey="home.instantEventCreationTitle"
|
||||||
<span class="text-4xl">🎯</span>
|
descriptionKey="home.instantEventCreationDescription"
|
||||||
</div>
|
/>
|
||||||
<h3 class="mb-4 text-xl font-bold text-white">{t('home.instantEventCreationTitle')}</h3>
|
|
||||||
<p class="">
|
|
||||||
{t('home.instantEventCreationDescription')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Feature 2 -->
|
<FeatureCard
|
||||||
<div class="rounded-sm border p-8 text-center">
|
emoji="🔗"
|
||||||
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
|
titleKey="home.oneClickSharingTitle"
|
||||||
<span class="text-4xl">🔗</span>
|
descriptionKey="home.oneClickSharingDescription"
|
||||||
</div>
|
/>
|
||||||
<h3 class="mb-4 text-xl font-bold text-white">{t('home.oneClickSharingTitle')}</h3>
|
|
||||||
<p class="">
|
|
||||||
{t('home.oneClickSharingDescription')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Feature 2 -->
|
<FeatureCard
|
||||||
<div class="rounded-sm border p-8 text-center">
|
emoji="🔍"
|
||||||
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
|
titleKey="home.allInOneClarityTitle"
|
||||||
<span class="text-4xl">🔍</span>
|
descriptionKey="home.allInOneClarityDescription"
|
||||||
</div>
|
/>
|
||||||
<h3 class="mb-4 text-xl font-bold text-white">{t('home.allInOneClarityTitle')}</h3>
|
|
||||||
<p class="">
|
|
||||||
{t('home.allInOneClarityDescription')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Feature 4 -->
|
<FeatureCard
|
||||||
<div class="rounded-sm border p-8 text-center">
|
emoji="👤"
|
||||||
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
|
titleKey="home.noHassleNoSignUpsTitle"
|
||||||
<span class="text-4xl">👤</span>
|
descriptionKey="home.noHassleNoSignUpsDescription"
|
||||||
</div>
|
/>
|
||||||
<h3 class="mb-4 text-xl font-bold text-white">{t('home.noHassleNoSignUpsTitle')}</h3>
|
|
||||||
<p class="">
|
|
||||||
{t('home.noHassleNoSignUpsDescription')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Feature 5 -->
|
<FeatureCard
|
||||||
<div class="rounded-sm border p-8 text-center">
|
emoji="🛡️"
|
||||||
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
|
titleKey="home.smartLimitsTitle"
|
||||||
<span class="text-4xl">🛡️</span>
|
descriptionKey="home.smartLimitsDescription"
|
||||||
</div>
|
/>
|
||||||
<h3 class="mb-4 text-xl font-bold text-white">{t('home.smartLimitsTitle')}</h3>
|
|
||||||
<p class="">
|
|
||||||
{t('home.smartLimitsDescription')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Feature 5 -->
|
<FeatureCard
|
||||||
<div class="rounded-sm border p-8 text-center">
|
emoji="✨"
|
||||||
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
|
titleKey="home.effortlessSimplicityTitle"
|
||||||
<span class="text-4xl">✨</span>
|
descriptionKey="home.effortlessSimplicityDescription"
|
||||||
</div>
|
/>
|
||||||
<h3 class="mb-4 text-xl font-bold text-white">{t('home.effortlessSimplicityTitle')}</h3>
|
|
||||||
<p class="">
|
<FeatureCard
|
||||||
{t('home.effortlessSimplicityDescription')}
|
emoji="🎫"
|
||||||
</p>
|
titleKey="home.inviteLinksTitle"
|
||||||
</div>
|
descriptionKey="home.inviteLinksDescription"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FeatureCard
|
||||||
|
emoji="🌐"
|
||||||
|
titleKey="home.federationTitle"
|
||||||
|
descriptionKey="home.federationDescription"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export const GET: RequestHandler = async () => {
|
|||||||
federation: true,
|
federation: true,
|
||||||
attendee_limit: event.attendeeLimit,
|
attendee_limit: event.attendeeLimit,
|
||||||
visibility: event.visibility,
|
visibility: event.visibility,
|
||||||
user_id: event.userId,
|
|
||||||
created_at: event.createdAt?.toISOString() || '',
|
created_at: event.createdAt?.toISOString() || '',
|
||||||
updated_at: event.updatedAt?.toISOString() || ''
|
updated_at: event.updatedAt?.toISOString() || ''
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
|
|
||||||
let errors: Record<string, string> = {};
|
let errors: Record<string, string> = {};
|
||||||
let isSubmitting = false;
|
let isSubmitting = false;
|
||||||
let currentUserId = '';
|
|
||||||
|
|
||||||
// Get today's date in YYYY-MM-DD format for min attribute
|
// Get today's date in YYYY-MM-DD format for min attribute
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
@@ -95,7 +94,6 @@
|
|||||||
}}
|
}}
|
||||||
class="space-y-6"
|
class="space-y-6"
|
||||||
>
|
>
|
||||||
<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} />
|
<input type="hidden" name="location_type" value={eventData.location_type} />
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const load: PageServerLoad = async () => {
|
|||||||
type: event.type,
|
type: event.type,
|
||||||
attendee_limit: event.attendeeLimit,
|
attendee_limit: event.attendeeLimit,
|
||||||
visibility: event.visibility,
|
visibility: event.visibility,
|
||||||
user_id: event.userId,
|
|
||||||
created_at: event.createdAt?.toISOString(),
|
created_at: event.createdAt?.toISOString(),
|
||||||
updated_at: event.updatedAt?.toISOString(),
|
updated_at: event.updatedAt?.toISOString(),
|
||||||
federation: false // Add false for local events
|
federation: false // Add false for local events
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export const load = async ({ cookies }) => {
|
|||||||
type: event.type,
|
type: event.type,
|
||||||
attendee_limit: event.attendeeLimit,
|
attendee_limit: event.attendeeLimit,
|
||||||
visibility: event.visibility,
|
visibility: event.visibility,
|
||||||
user_id: event.userId,
|
|
||||||
created_at: event.createdAt?.toISOString() || new Date().toISOString(),
|
created_at: event.createdAt?.toISOString() || new Date().toISOString(),
|
||||||
updated_at: event.updatedAt?.toISOString() || new Date().toISOString()
|
updated_at: event.updatedAt?.toISOString() || new Date().toISOString()
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
export let data: { events: Event[] };
|
export let data: { events: Event[] };
|
||||||
|
|
||||||
let userEvents: Event[] = [];
|
let userEvents: Event[] = [];
|
||||||
let currentUserId = '';
|
|
||||||
let showDeleteModal = false;
|
let showDeleteModal = false;
|
||||||
let eventToDelete: Event | null = null;
|
let eventToDelete: Event | null = null;
|
||||||
|
|
||||||
@@ -28,7 +27,6 @@
|
|||||||
// Use server-side action for deletion
|
// Use server-side action for deletion
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('eventId', eventId);
|
formData.append('eventId', eventId);
|
||||||
formData.append('userId', currentUserId);
|
|
||||||
|
|
||||||
const response = await fetch('?/deleteEvent', {
|
const response = await fetch('?/deleteEvent', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { logger } from '$lib/logger';
|
|||||||
|
|
||||||
export const load: PageServerLoad = async ({ params, cookies }) => {
|
export const load: PageServerLoad = async ({ params, cookies }) => {
|
||||||
const eventId = params.id;
|
const eventId = params.id;
|
||||||
|
const userId = cookies.get('cactoideUserId');
|
||||||
|
|
||||||
if (!eventId) {
|
if (!eventId) {
|
||||||
throw error(404, 'EventId not found');
|
throw error(404, 'EventId not found');
|
||||||
@@ -29,7 +30,6 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
|
|||||||
// Check if this is an invite-only event
|
// Check if this is an invite-only event
|
||||||
if (event.visibility === 'invite-only') {
|
if (event.visibility === 'invite-only') {
|
||||||
// For invite-only events, check if user is the event creator
|
// For invite-only events, check if user is the event creator
|
||||||
const userId = cookies.get('cactoideUserId');
|
|
||||||
if (event.userId !== userId) {
|
if (event.userId !== userId) {
|
||||||
// User is not the creator, redirect to a message about needing invite
|
// User is not the creator, redirect to a message about needing invite
|
||||||
throw error(403, 'This event requires an invite link to view');
|
throw error(403, 'This event requires an invite link to view');
|
||||||
@@ -48,7 +48,8 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
|
|||||||
type: event.type,
|
type: event.type,
|
||||||
attendee_limit: event.attendeeLimit,
|
attendee_limit: event.attendeeLimit,
|
||||||
visibility: event.visibility,
|
visibility: event.visibility,
|
||||||
user_id: event.userId,
|
// Never send raw user ids to the client — they are the credential
|
||||||
|
is_creator: !!userId && event.userId === userId,
|
||||||
created_at: event.createdAt?.toISOString() || new Date().toISOString(),
|
created_at: event.createdAt?.toISOString() || new Date().toISOString(),
|
||||||
updated_at: event.updatedAt?.toISOString() || new Date().toISOString()
|
updated_at: event.updatedAt?.toISOString() || new Date().toISOString()
|
||||||
};
|
};
|
||||||
@@ -57,16 +58,13 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
|
|||||||
id: rsvp.id,
|
id: rsvp.id,
|
||||||
event_id: rsvp.eventId,
|
event_id: rsvp.eventId,
|
||||||
name: rsvp.name,
|
name: rsvp.name,
|
||||||
user_id: rsvp.userId,
|
is_mine: !!userId && rsvp.userId === userId,
|
||||||
created_at: rsvp.createdAt?.toISOString() || new Date().toISOString()
|
created_at: rsvp.createdAt?.toISOString() || new Date().toISOString()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const userId = cookies.get('cactoideUserId');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
event: transformedEvent,
|
event: transformedEvent,
|
||||||
rsvps: transformedRsvps,
|
rsvps: transformedRsvps
|
||||||
userId: userId
|
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Response) throw err; // This is the 404 error
|
if (err instanceof Response) throw err; // This is the 404 error
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import type { CalendarEvent } from '$lib/calendarHelpers.js';
|
import type { CalendarEvent } from '$lib/calendarHelpers.js';
|
||||||
import { t } from '$lib/i18n/i18n.js';
|
import { t } from '$lib/i18n/i18n.js';
|
||||||
|
|
||||||
export let data: { event: Event; rsvps: RSVP[]; userId: string };
|
export let data: { event: Event; rsvps: RSVP[] };
|
||||||
type FormDataLocal = { success?: boolean; error?: string; type?: 'add' | 'remove' | 'copy' };
|
type FormDataLocal = { success?: boolean; error?: string; type?: 'add' | 'remove' | 'copy' };
|
||||||
export let form: FormDataLocal | undefined;
|
export let form: FormDataLocal | undefined;
|
||||||
|
|
||||||
@@ -30,8 +30,7 @@
|
|||||||
// Use server-side data
|
// Use server-side data
|
||||||
$: event = data.event;
|
$: event = data.event;
|
||||||
$: rsvps = data.rsvps;
|
$: rsvps = data.rsvps;
|
||||||
$: currentUserId = data.userId;
|
$: isEventCreator = event.is_creator ?? false;
|
||||||
$: isEventCreator = event.user_id === currentUserId;
|
|
||||||
|
|
||||||
// Create calendar event object when event data changes
|
// Create calendar event object when event data changes
|
||||||
$: if (event && browser) {
|
$: if (event && browser) {
|
||||||
@@ -266,7 +265,6 @@
|
|||||||
}}
|
}}
|
||||||
class="space-y-4"
|
class="space-y-4"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="userId" value={currentUserId} />
|
|
||||||
<div>
|
<div>
|
||||||
<label for="attendeeName" class=" mb-2 block text-sm font-semibold">
|
<label for="attendeeName" class=" mb-2 block text-sm font-semibold">
|
||||||
{t('event.yourNameLabel')}
|
{t('event.yourNameLabel')}
|
||||||
@@ -401,7 +399,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if attendee.user_id === currentUserId}
|
{#if attendee.is_mine}
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/removeRSVP"
|
action="?/removeRSVP"
|
||||||
|
|||||||
@@ -44,10 +44,13 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strip the owner id — it is the credential, and the load above already proved ownership
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { userId: _owner, ...eventRow } = event[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
event: event[0],
|
event: eventRow,
|
||||||
inviteToken,
|
inviteToken
|
||||||
userId
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -394,7 +394,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Invite Link Section (only for invite-only events and event creator) -->
|
<!-- Invite Link Section (only for invite-only events and event creator) -->
|
||||||
{#if eventData.visibility === 'invite-only' && inviteToken && data.event.userId === data.userId}
|
{#if eventData.visibility === 'invite-only' && inviteToken}
|
||||||
<div class="rounded-sm border border-amber-500/30 bg-amber-900/20 p-4">
|
<div class="rounded-sm border border-amber-500/30 bg-amber-900/20 p-4">
|
||||||
<div class="mb-3 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
<h3 class="text-lg font-semibold text-amber-400">Invite Link</h3>
|
<h3 class="text-lg font-semibold text-amber-400">Invite Link</h3>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { isTokenValid } from '$lib/inviteTokenHelpers.js';
|
|||||||
export const load: PageServerLoad = async ({ params, cookies }) => {
|
export const load: PageServerLoad = async ({ params, cookies }) => {
|
||||||
const eventId = params.id;
|
const eventId = params.id;
|
||||||
const token = params.token;
|
const token = params.token;
|
||||||
|
const userId = cookies.get('cactoideUserId');
|
||||||
|
|
||||||
if (!eventId || !token) {
|
if (!eventId || !token) {
|
||||||
throw error(404, 'Event or token not found');
|
throw error(404, 'Event or token not found');
|
||||||
@@ -59,7 +60,8 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
|
|||||||
type: event.type,
|
type: event.type,
|
||||||
attendee_limit: event.attendeeLimit,
|
attendee_limit: event.attendeeLimit,
|
||||||
visibility: event.visibility,
|
visibility: event.visibility,
|
||||||
user_id: event.userId,
|
// Never send raw user ids to the client — they are the credential
|
||||||
|
is_creator: !!userId && event.userId === userId,
|
||||||
created_at: event.createdAt?.toISOString() || new Date().toISOString(),
|
created_at: event.createdAt?.toISOString() || new Date().toISOString(),
|
||||||
updated_at: event.updatedAt?.toISOString() || new Date().toISOString()
|
updated_at: event.updatedAt?.toISOString() || new Date().toISOString()
|
||||||
};
|
};
|
||||||
@@ -68,16 +70,13 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
|
|||||||
id: rsvp.id,
|
id: rsvp.id,
|
||||||
event_id: rsvp.eventId,
|
event_id: rsvp.eventId,
|
||||||
name: rsvp.name,
|
name: rsvp.name,
|
||||||
user_id: rsvp.userId,
|
is_mine: !!userId && rsvp.userId === userId,
|
||||||
created_at: rsvp.createdAt?.toISOString() || new Date().toISOString()
|
created_at: rsvp.createdAt?.toISOString() || new Date().toISOString()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const userId = cookies.get('cactoideUserId');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
event: transformedEvent,
|
event: transformedEvent,
|
||||||
rsvps: transformedRsvps,
|
rsvps: transformedRsvps,
|
||||||
userId: userId,
|
|
||||||
inviteToken: {
|
inviteToken: {
|
||||||
id: inviteToken.id,
|
id: inviteToken.id,
|
||||||
event_id: inviteToken.eventId,
|
event_id: inviteToken.eventId,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import type { CalendarEvent } from '$lib/calendarHelpers.js';
|
import type { CalendarEvent } from '$lib/calendarHelpers.js';
|
||||||
import { t } from '$lib/i18n/i18n.js';
|
import { t } from '$lib/i18n/i18n.js';
|
||||||
|
|
||||||
export let data: { event: Event; rsvps: RSVP[]; userId: string; inviteToken: InviteToken };
|
export let data: { event: Event; rsvps: RSVP[]; inviteToken: InviteToken };
|
||||||
export let form;
|
export let form;
|
||||||
|
|
||||||
let event: Event;
|
let event: Event;
|
||||||
@@ -26,8 +26,7 @@
|
|||||||
// Use server-side data
|
// Use server-side data
|
||||||
$: event = data.event;
|
$: event = data.event;
|
||||||
$: rsvps = data.rsvps;
|
$: rsvps = data.rsvps;
|
||||||
$: currentUserId = data.userId;
|
$: isEventCreator = event.is_creator ?? false;
|
||||||
$: isEventCreator = event.user_id === currentUserId;
|
|
||||||
|
|
||||||
// Create calendar event object when event data changes
|
// Create calendar event object when event data changes
|
||||||
$: if (event && browser) {
|
$: if (event && browser) {
|
||||||
@@ -240,7 +239,6 @@
|
|||||||
}}
|
}}
|
||||||
class="space-y-4"
|
class="space-y-4"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="userId" value={currentUserId} />
|
|
||||||
<div>
|
<div>
|
||||||
<label for="attendeeName" class=" mb-2 block text-sm font-semibold">
|
<label for="attendeeName" class=" mb-2 block text-sm font-semibold">
|
||||||
{t('event.yourNameLabel')}
|
{t('event.yourNameLabel')}
|
||||||
@@ -374,7 +372,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if attendee.user_id === currentUserId}
|
{#if attendee.is_mine}
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/removeRSVP"
|
action="?/removeRSVP"
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ const config = {
|
|||||||
// see "split" mode in https://github.com/sveltejs/kit/tree/main/packages/adapter-netlify
|
// see "split" mode in https://github.com/sveltejs/kit/tree/main/packages/adapter-netlify
|
||||||
edge: false,
|
edge: false,
|
||||||
split: false
|
split: false
|
||||||
}),
|
})
|
||||||
csrf: {
|
|
||||||
checkOrigin: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user