Compare commits

...

9 Commits

Author SHA1 Message Date
jmug
9cc10e47e6 Handmade content changes 2026-03-07 06:17:39 +00:00
Levente Orban
d18afc43da fix: add permissions to github workflow 2025-12-08 09:41:43 +01:00
Levente Orban
662476f820 fix: add permissions to github workflow 2025-12-08 08:43:00 +01:00
Levente Orban
7ebf95bb16 feat: add github to landing pagE 2025-11-11 17:43:41 +01:00
Levente Orban
0491ec4c4b fix: change the readme.md gif 2025-11-11 16:24:03 +01:00
Levente Orban
fcdef065d7 fix: missing federation.config error 2025-11-10 11:46:50 +01:00
Levente Orban
a1fa879f36 fix: improve the federation.config 2025-11-10 11:46:02 +01:00
Levente Orban
b723aac180 fix: federation config refactor 2025-11-10 10:14:44 +01:00
Levente Orban
277ad3ff14 feat: add db-seed to makefile and merge migrations 2025-11-10 10:01:23 +01:00
16 changed files with 185 additions and 178 deletions

View File

@@ -10,4 +10,14 @@ APP_VERSION=latest
PORT=5173 PORT=5173
HOSTNAME=0.0.0.0 HOSTNAME=0.0.0.0
# Logger configuration
LOG_PRETTY=true
LOG_LEVEL="trace"
# If you don't want to use the default home page you can turn off
# in this case the /discovery page remain the home of your site
PUBLIC_LANDING_INFO=true PUBLIC_LANDING_INFO=true
# Federation config
FEDERATION_INSTANCE=false

View File

@@ -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:

View File

@@ -1,4 +1,7 @@
name: test & build name: test & build
permissions:
contents: read
pull-requests: write
on: on:
push: push:

View File

@@ -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>
@@ -59,6 +59,17 @@ make db-only
npm run dev -- --open npm run dev -- --open
``` ```
#### Build the image in local
```
docker build \
--build-arg LOG_PRETTY=${LOG_PRETTY:-true} \
--build-arg LOG_LEVEL=${LOG_LEVEL:-trace} \
--build-arg PUBLIC_LANDING_INFO=${PUBLIC_LANDING_INFO:-true} \
--build-arg FEDERATION_INSTANCE=${FEDERATION_INSTANCE:-true} \
-t cactoide-example .
```
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`.
Use the `database/seed.sql` if you want to populate your database with dummy data. Use the `database/seed.sql` if you want to populate your database with dummy data.
@@ -103,12 +114,12 @@ Your instance will automatically expose:
To add your instance to the global federation list (so other instances can discover your events): To add your instance to the global federation list (so other instances can discover your events):
1. Fork the [Cactoide repository](https://github.com/polaroi8d/cactoide) 1. Fork the [Cactoide repository](https://github.com/polaroi8d/cactoide)
2. Add your instance URL to the `instances` array in [`federation.config.js`](https://github.com/polaroi8d/cactoide/blob/main/federation.config.js): 2. Add your instance URL to the `instances` array in `federation.config.js`:
3. Open a pull request to the main repository 3. Open a pull request to the main repository
Once merged, your instance will appear in the federation network, and other instances will be able to discover and display your public events. Once merged, your instance will appear in the federation network, and other instances will be able to discover and display your public events.
You can view all registered federated instances in the main repository: [`federation.config.js`](https://github.com/polaroi8d/cactoide/blob/main/federation.config.js) file. You can view all registered federated instances in the main repository: `federation.config.js` file.
### Options ### Options

View File

@@ -2,6 +2,7 @@ services:
# Database # Database
postgres: postgres:
image: postgres:15-alpine image: postgres:15-alpine
restart: unless-stopped
container_name: cactoide-db container_name: cactoide-db
environment: environment:
POSTGRES_DB: ${POSTGRES_DB:-cactoide_database} POSTGRES_DB: ${POSTGRES_DB:-cactoide_database}
@@ -28,7 +29,8 @@ services:
# Application # Application
app: app:
image: ghcr.io/polaroi8d/cactoide/cactoide:${APP_VERSION:-latest} image: cactoide:handmade
restart: unless-stopped
build: . build: .
container_name: cactoide-app container_name: cactoide-app
ports: ports:
@@ -37,12 +39,15 @@ services:
DATABASE_URL: ${DATABASE_URL:-postgres://cactoide:cactoide_password@postgres:5432/cactoide_database} DATABASE_URL: ${DATABASE_URL:-postgres://cactoide:cactoide_password@postgres:5432/cactoide_database}
PORT: 3000 PORT: 3000
HOSTNAME: ${HOSTNAME:-0.0.0.0} HOSTNAME: ${HOSTNAME:-0.0.0.0}
LOG_PRETTY: ${LOG_PRETTY:-true}
LOG_LEVEL: ${LOG_LEVEL:-trace}
PUBLIC_LANDING_INFO: ${PUBLIC_LANDING_INFO:-true}
FEDERATION_INSTANCE: ${FEDERATION_INSTANCE:-true}
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
networks: networks:
- cactoide-network - cactoide-network
restart: unless-stopped
volumes: volumes:
postgres_data: postgres_data:

View File

@@ -6,12 +6,6 @@
<link rel="icon" type="image/x-icon" href="/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/favicon.ico" />
%sveltekit.head% %sveltekit.head%
<!-- Remove if you don't want to use analytics -->
<script
defer
src="https://analytics.dalev.hu/script.js"
data-website-id="7425d098-e340-4464-bd03-c2e47b004cd9"
></script>
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>

View 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>

View File

@@ -1,9 +1,9 @@
const config = { const config = {
name: 'Cactoide Genesis', name: 'Cactoide Genesis',
instances: [ instances: [
{ // {
url: 'cactoide.org' // url: 'cactoide.org'
} // }
// { // {
// url: 'YOUR_INSTANCE_URL' // url: 'YOUR_INSTANCE_URL'
// } // }

View File

@@ -1,76 +1,18 @@
import { readFileSync } from 'fs';
import { join } from 'path';
import { logger } from '$lib/logger'; import { logger } from '$lib/logger';
import type { Event } from '$lib/types'; import type { Event } from '$lib/types';
import config from '../../federation.config.js'; import config from '$lib/config/federation.config.js';
console.log(config.instances);
interface FederationConfig {
name: string;
instances: Array<{ url: string }>;
}
interface FederationEventsResponse { interface FederationEventsResponse {
events: Array<Event & { federation?: boolean }>; events: Array<Event & { federation?: boolean }>;
count?: number; count?: number;
} }
/**
* Reads the federation config file
*/
async function readFederationConfig(): Promise<FederationConfig | null> {
try {
const configPath = join(process.cwd(), 'federation.config.js');
// Use dynamic import to load the config file as a module
// This is safer than eval and works with ES modules
const configModule = await import(configPath + '?t=' + Date.now());
const config = (configModule.default || configModule.config) as FederationConfig;
if (config && config.instances && Array.isArray(config.instances)) {
return config;
}
logger.warn('Invalid federation config structure');
return null;
} catch (error) {
// If dynamic import fails, try reading as text and parsing
try {
const configPath = join(process.cwd(), 'federation.config.js');
const configContent = readFileSync(configPath, 'utf-8');
// Try to extract JSON-like structure
const configMatch = configContent.match(/instances:\s*\[([\s\S]*?)\]/);
if (configMatch) {
// Simple parsing - extract URLs
const urlMatches = configContent.matchAll(/url:\s*['"]([^'"]+)['"]/g);
const instances = Array.from(urlMatches, (match) => ({ url: match[1] }));
if (instances.length > 0) {
return {
name: 'Federated Instances',
instances
};
}
}
} catch (fallbackError) {
logger.error({ error: fallbackError }, 'Error parsing federation.config.js as fallback');
}
logger.error({ error }, 'Error reading federation.config.js');
return null;
}
}
/** /**
* Fetches events from a single federated instance * Fetches events from a single federated instance
*/ */
async function fetchEventsFromInstance(instanceUrl: string): Promise<Event[]> { async function fetchEventsFromInstance(instanceUrl: string): Promise<Event[]> {
try { try {
// Ensure URL has protocol and append /api/federation/events
const apiUrl = `http://${instanceUrl}/api/federation/events`; const apiUrl = `http://${instanceUrl}/api/federation/events`;
logger.debug({ apiUrl }, 'Fetching events from federated instance'); logger.debug({ apiUrl }, 'Fetching events from federated instance');
@@ -120,18 +62,11 @@ async function fetchEventsFromInstance(instanceUrl: string): Promise<Event[]> {
* Fetches events from all configured federated instances * Fetches events from all configured federated instances
*/ */
export async function fetchAllFederatedEvents(): Promise<Event[]> { export async function fetchAllFederatedEvents(): Promise<Event[]> {
const config = await readFederationConfig();
if (!config || !config.instances || config.instances.length === 0) { if (!config || !config.instances || config.instances.length === 0) {
logger.debug('No federation config or instances found'); logger.debug('No federation config or instances found');
return []; return [];
} }
logger.info(
{ instanceCount: config.instances.length },
'Fetching events from federated instances'
);
// Fetch from all instances in parallel // Fetch from all instances in parallel
const fetchPromises = config.instances.map((instance) => fetchEventsFromInstance(instance.url)); const fetchPromises = config.instances.map((instance) => fetchEventsFromInstance(instance.url));

View File

@@ -103,13 +103,16 @@
"instance": "Instance" "instance": "Instance"
}, },
"home": { "home": {
"title": "Cactoide - The RSVP site", "title": "RSVP | Handmade Cities",
"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",
"subtitle": "The Ultimate RSVP Platform", "subtitle": "Handmade's Preferred RSVP System",
"tagline": "Create, share, and manage events with zero friction.", "tagline": "Create, share, and manage events with zero friction.",
"whyCactoideTitle": "Why Cactoide(ae)?🌵", "openSourceTitle": "Open Source & Self-Hostable",
"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.", "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?",
"whyCactoideDescription": "Cactoide is lightweight and open source. Meetup Hosts should ALWAYS create private events. We currently don't prevent strangers from spamming public ones:",
"createEventNow": "Create Event Now", "createEventNow": "Create Event Now",
"discoverPublicEventsTitle": "Discover Public Events", "discoverPublicEventsTitle": "Discover Public Events",
"discoverPublicEventsDescription": "See what others are planning and get inspired", "discoverPublicEventsDescription": "See what others are planning and get inspired",
@@ -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.",
@@ -139,7 +146,7 @@
"ctaButton": "Create" "ctaButton": "Create"
}, },
"create": { "create": {
"title": "Create Event - Cactoide", "title": "Create Event - Handmade Cities",
"formTitle": "Create New Event", "formTitle": "Create New Event",
"eventNameLabel": "Name", "eventNameLabel": "Name",
"eventNamePlaceholder": "Enter event name", "eventNamePlaceholder": "Enter event name",
@@ -172,10 +179,10 @@
"createEventButton": "Create Event" "createEventButton": "Create Event"
}, },
"event": { "event": {
"title": "{eventName} - Cactoide", "title": "{eventName} - Handmade Cities",
"eventTitle": "Event - Cactoide", "eventTitle": "Event - Handmade Cities",
"editTitle": "Edit Event - {eventName} - Cactoide", "editTitle": "Edit Event - {eventName} - Handmade Cities",
"myEventsTitle": "My Events - Cactoide", "myEventsTitle": "My Events - Handmade Cities",
"eventNotFoundTitle": "Event Not Found", "eventNotFoundTitle": "Event Not Found",
"eventNotFoundDescription": "The event you're looking for doesn't exist or has been removed.", "eventNotFoundDescription": "The event you're looking for doesn't exist or has been removed.",
"joinThisEvent": "Join This Event", "joinThisEvent": "Join This Event",
@@ -223,7 +230,7 @@
"inviteLinkExpiresAt": "This link expires when the event starts: {time}" "inviteLinkExpiresAt": "This link expires when the event starts: {time}"
}, },
"discover": { "discover": {
"title": "Discover Events - Cactoide", "title": "Discover Events - Handmade Cities",
"noPublicEventsTitle": "No Public Events Yet", "noPublicEventsTitle": "No Public Events Yet",
"noPublicEventsDescription": "There are no public events available at the moment. Be the first to create one!", "noPublicEventsDescription": "There are no public events available at the moment. Be the first to create one!",
"createButton": "Create", "createButton": "Create",
@@ -276,13 +283,13 @@
"downloadICalDescription": "Download .ics file for any calendar app" "downloadICalDescription": "Download .ics file for any calendar app"
}, },
"errors": { "errors": {
"title": "Error - Cactoide", "title": "Error - Handmade Cities",
"errorTitle": "Error", "errorTitle": "Error",
"anUnexpectedErrorOccurred": "An unexpected error occurred.", "anUnexpectedErrorOccurred": "An unexpected error occurred.",
"homeButton": "Home" "homeButton": "Home"
}, },
"layout": { "layout": {
"defaultTitle": "Cactoide -", "defaultTitle": "Handmade Cities -",
"defaultDescription": "Create and manage event RSVPs", "defaultDescription": "Create and manage event RSVPs",
"userIdCookieText": "Your UserID stored as a cookie:", "userIdCookieText": "Your UserID stored as a cookie:",
"firstTimeVisiting": "First time visiting. Generating new UserID...", "firstTimeVisiting": "First time visiting. Generating new UserID...",

View File

@@ -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>

View File

@@ -4,7 +4,7 @@ import { database } from '$lib/database/db';
import { events } from '$lib/database/schema'; import { events } from '$lib/database/schema';
import { eq, count } from 'drizzle-orm'; import { eq, count } from 'drizzle-orm';
import { logger } from '$lib/logger'; import { logger } from '$lib/logger';
import federationConfig from '../../../../../federation.config.js'; import federationConfig from '$lib/config/federation.config.js';
import { FEDERATION_INSTANCE } from '$env/static/private'; import { FEDERATION_INSTANCE } from '$env/static/private';

View File

@@ -8,6 +8,7 @@
import CalendarModal from '$lib/components/CalendarModal.svelte'; import CalendarModal from '$lib/components/CalendarModal.svelte';
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';
import { onMount } from 'svelte';
export let data: { event: Event; rsvps: RSVP[]; userId: string }; export let data: { event: Event; rsvps: RSVP[]; userId: string };
type FormDataLocal = { success?: boolean; error?: string; type?: 'add' | 'remove' | 'copy' }; type FormDataLocal = { success?: boolean; error?: string; type?: 'add' | 'remove' | 'copy' };
@@ -27,6 +28,17 @@
let typeToShow: 'add' | 'remove' | 'copy' | undefined; let typeToShow: 'add' | 'remove' | 'copy' | undefined;
let successHideTimer: number | null = null; let successHideTimer: number | null = null;
// Compute eventId early so reactive blocks can use it.
const eventId = $page.params.id || '';
// client-only origin (empty during SSR).
let origin = '';
// Safe: Only runs in browser.
onMount(() => {
origin = window.location.origin;
});
// Use server-side data // Use server-side data
$: event = data.event; $: event = data.event;
$: rsvps = data.rsvps; $: rsvps = data.rsvps;
@@ -40,10 +52,26 @@
date: event.date, date: event.date,
time: event.time, time: event.time,
location: event.location, location: event.location,
url: `${$page.url.origin}/event/${eventId}` // Fallback to relative path on server render.
url: origin ? `${origin}/event/${eventId}` : `/event/${eventId}`
}; };
} }
const copyEventLink = () => {
if (browser && isEventCreator) {
const url = origin ? `${origin}/event/${eventId}` : `${location.origin}/event/${eventId}`;
navigator.clipboard.writeText(url).then(() => {
toastType = 'copy';
success = t('event.eventLinkCopied');
setTimeout(() => {
success = '';
toastType = null;
}, 3000);
});
}
};
// Handle form errors from server // Handle form errors from server
$: if (form?.error) { $: if (form?.error) {
error = String(form.error); error = String(form.error);
@@ -79,22 +107,7 @@
// Derive toast type from local or server form // Derive toast type from local or server form
$: typeToShow = toastType ?? form?.type; $: typeToShow = toastType ?? form?.type;
const eventId = $page.params.id || '';
const copyEventLink = () => {
if (browser && isEventCreator) {
const url = `${$page.url.origin}/event/${eventId}`;
navigator.clipboard.writeText(url).then(() => {
toastType = 'copy';
success = t('event.eventLinkCopied');
setTimeout(() => {
success = '';
toastType = null;
}, 3000);
});
}
};
const clearMessages = () => { const clearMessages = () => {
error = ''; error = '';
@@ -476,7 +489,7 @@
bind:isOpen={showCalendarModal} bind:isOpen={showCalendarModal}
event={calendarEvent} event={calendarEvent}
{eventId} {eventId}
baseUrl={$page.url.origin} baseUrl={origin}
on:close={closeCalendarModal} on:close={closeCalendarModal}
/> />
{/if} {/if}

View File

@@ -1,6 +1,6 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { logger } from '$lib/logger'; import { logger } from '$lib/logger';
import federationConfig from '../../../federation.config.js'; import federationConfig from '$lib/config/federation.config.js';
interface InstanceInfo { interface InstanceInfo {
name: string; name: string;

View File

@@ -130,10 +130,7 @@
<p class="py-8 text-center text-slate-400"> <p class="py-8 text-center text-slate-400">
{t('instance.description')} {t('instance.description')}
<a {t('instance.configFile')}
href="https://github.com/cactoide/cactoide/blob/main/federation.config.js"
class="text-violet-300/80">{t('instance.configFile')}</a
>
{t('instance.file')} {t('instance.file')}
</p> </p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 162 KiB