forked from jmug/cactoide
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cc10e47e6 | ||
|
|
d18afc43da | ||
|
|
662476f820 | ||
|
|
7ebf95bb16 | ||
|
|
0491ec4c4b |
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
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
@@ -1,4 +1,7 @@
|
||||
name: test & build
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
@@ -7,7 +7,7 @@ Like the cactus, great events bloom under any condition when managed with care.
|
||||
<p align="center">
|
||||
<a href="https://cactoide.org/" target="blank">
|
||||
<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>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -2,6 +2,7 @@ services:
|
||||
# Database
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
restart: unless-stopped
|
||||
container_name: cactoide-db
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-cactoide_database}
|
||||
@@ -28,7 +29,8 @@ services:
|
||||
|
||||
# Application
|
||||
app:
|
||||
image: ghcr.io/polaroi8d/cactoide/cactoide:${APP_VERSION:-latest}
|
||||
image: cactoide:handmade
|
||||
restart: unless-stopped
|
||||
build: .
|
||||
container_name: cactoide-app
|
||||
ports:
|
||||
@@ -46,7 +48,6 @@ services:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- cactoide-network
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
@@ -6,12 +6,6 @@
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
%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>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
|
||||
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>
|
||||
@@ -103,13 +103,16 @@
|
||||
"instance": "Instance"
|
||||
},
|
||||
"home": {
|
||||
"title": "Cactoide - The RSVP site",
|
||||
"title": "RSVP | Handmade Cities",
|
||||
"description": "Create and manage event RSVPs. No registration required, instant sharing.",
|
||||
"mainTitle": "Cactoide(ea)",
|
||||
"subtitle": "The Ultimate RSVP Platform",
|
||||
"mainTitle": "Cactoide",
|
||||
"subtitle": "Handmade's Preferred RSVP System",
|
||||
"tagline": "Create, share, and manage events with zero friction.",
|
||||
"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.",
|
||||
"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?",
|
||||
"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",
|
||||
"discoverPublicEventsTitle": "Discover Public Events",
|
||||
"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.",
|
||||
"effortlessSimplicityTitle": "Effortless Simplicity",
|
||||
"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",
|
||||
"step1Title": "Create Event",
|
||||
"step1Description": "Fill out a simple form with event details. Choose between limited or unlimited capacity.",
|
||||
@@ -139,7 +146,7 @@
|
||||
"ctaButton": "Create"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Event - Cactoide",
|
||||
"title": "Create Event - Handmade Cities",
|
||||
"formTitle": "Create New Event",
|
||||
"eventNameLabel": "Name",
|
||||
"eventNamePlaceholder": "Enter event name",
|
||||
@@ -172,10 +179,10 @@
|
||||
"createEventButton": "Create Event"
|
||||
},
|
||||
"event": {
|
||||
"title": "{eventName} - Cactoide",
|
||||
"eventTitle": "Event - Cactoide",
|
||||
"editTitle": "Edit Event - {eventName} - Cactoide",
|
||||
"myEventsTitle": "My Events - Cactoide",
|
||||
"title": "{eventName} - Handmade Cities",
|
||||
"eventTitle": "Event - Handmade Cities",
|
||||
"editTitle": "Edit Event - {eventName} - Handmade Cities",
|
||||
"myEventsTitle": "My Events - Handmade Cities",
|
||||
"eventNotFoundTitle": "Event Not Found",
|
||||
"eventNotFoundDescription": "The event you're looking for doesn't exist or has been removed.",
|
||||
"joinThisEvent": "Join This Event",
|
||||
@@ -223,7 +230,7 @@
|
||||
"inviteLinkExpiresAt": "This link expires when the event starts: {time}"
|
||||
},
|
||||
"discover": {
|
||||
"title": "Discover Events - Cactoide",
|
||||
"title": "Discover Events - Handmade Cities",
|
||||
"noPublicEventsTitle": "No Public Events Yet",
|
||||
"noPublicEventsDescription": "There are no public events available at the moment. Be the first to create one!",
|
||||
"createButton": "Create",
|
||||
@@ -276,13 +283,13 @@
|
||||
"downloadICalDescription": "Download .ics file for any calendar app"
|
||||
},
|
||||
"errors": {
|
||||
"title": "Error - Cactoide",
|
||||
"title": "Error - Handmade Cities",
|
||||
"errorTitle": "Error",
|
||||
"anUnexpectedErrorOccurred": "An unexpected error occurred.",
|
||||
"homeButton": "Home"
|
||||
},
|
||||
"layout": {
|
||||
"defaultTitle": "Cactoide -",
|
||||
"defaultTitle": "Handmade Cities -",
|
||||
"defaultDescription": "Create and manage event RSVPs",
|
||||
"userIdCookieText": "Your UserID stored as a cookie:",
|
||||
"firstTimeVisiting": "First time visiting. Generating new UserID...",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$lib/i18n/i18n.js';
|
||||
import FeatureCard from '$lib/components/FeatureCard.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -22,6 +23,34 @@
|
||||
{t('home.tagline')}
|
||||
</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">
|
||||
{t('home.whyCactoideTitle')}<span class="text-violet-400"
|
||||
><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">
|
||||
{t('home.whyCactoideFeatureTitle')}
|
||||
</h2>
|
||||
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Feature 1 -->
|
||||
<div class="rounded-sm border p-8 text-center">
|
||||
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
|
||||
<span class="text-4xl">🎯</span>
|
||||
</div>
|
||||
<h3 class="mb-4 text-xl font-bold text-white">{t('home.instantEventCreationTitle')}</h3>
|
||||
<p class="">
|
||||
{t('home.instantEventCreationDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-4">
|
||||
<FeatureCard
|
||||
emoji="🎯"
|
||||
titleKey="home.instantEventCreationTitle"
|
||||
descriptionKey="home.instantEventCreationDescription"
|
||||
/>
|
||||
|
||||
<!-- Feature 2 -->
|
||||
<div class="rounded-sm border p-8 text-center">
|
||||
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
|
||||
<span class="text-4xl">🔗</span>
|
||||
</div>
|
||||
<h3 class="mb-4 text-xl font-bold text-white">{t('home.oneClickSharingTitle')}</h3>
|
||||
<p class="">
|
||||
{t('home.oneClickSharingDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<FeatureCard
|
||||
emoji="🔗"
|
||||
titleKey="home.oneClickSharingTitle"
|
||||
descriptionKey="home.oneClickSharingDescription"
|
||||
/>
|
||||
|
||||
<!-- Feature 2 -->
|
||||
<div class="rounded-sm border p-8 text-center">
|
||||
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
|
||||
<span class="text-4xl">🔍</span>
|
||||
</div>
|
||||
<h3 class="mb-4 text-xl font-bold text-white">{t('home.allInOneClarityTitle')}</h3>
|
||||
<p class="">
|
||||
{t('home.allInOneClarityDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<FeatureCard
|
||||
emoji="🔍"
|
||||
titleKey="home.allInOneClarityTitle"
|
||||
descriptionKey="home.allInOneClarityDescription"
|
||||
/>
|
||||
|
||||
<!-- Feature 4 -->
|
||||
<div class="rounded-sm border p-8 text-center">
|
||||
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
|
||||
<span class="text-4xl">👤</span>
|
||||
</div>
|
||||
<h3 class="mb-4 text-xl font-bold text-white">{t('home.noHassleNoSignUpsTitle')}</h3>
|
||||
<p class="">
|
||||
{t('home.noHassleNoSignUpsDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<FeatureCard
|
||||
emoji="👤"
|
||||
titleKey="home.noHassleNoSignUpsTitle"
|
||||
descriptionKey="home.noHassleNoSignUpsDescription"
|
||||
/>
|
||||
|
||||
<!-- Feature 5 -->
|
||||
<div class="rounded-sm border p-8 text-center">
|
||||
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
|
||||
<span class="text-4xl">🛡️</span>
|
||||
</div>
|
||||
<h3 class="mb-4 text-xl font-bold text-white">{t('home.smartLimitsTitle')}</h3>
|
||||
<p class="">
|
||||
{t('home.smartLimitsDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<FeatureCard
|
||||
emoji="🛡️"
|
||||
titleKey="home.smartLimitsTitle"
|
||||
descriptionKey="home.smartLimitsDescription"
|
||||
/>
|
||||
|
||||
<!-- Feature 5 -->
|
||||
<div class="rounded-sm border p-8 text-center">
|
||||
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full">
|
||||
<span class="text-4xl">✨</span>
|
||||
</div>
|
||||
<h3 class="mb-4 text-xl font-bold text-white">{t('home.effortlessSimplicityTitle')}</h3>
|
||||
<p class="">
|
||||
{t('home.effortlessSimplicityDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<FeatureCard
|
||||
emoji="✨"
|
||||
titleKey="home.effortlessSimplicityTitle"
|
||||
descriptionKey="home.effortlessSimplicityDescription"
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
emoji="🎫"
|
||||
titleKey="home.inviteLinksTitle"
|
||||
descriptionKey="home.inviteLinksDescription"
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
emoji="🌐"
|
||||
titleKey="home.federationTitle"
|
||||
descriptionKey="home.federationDescription"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import CalendarModal from '$lib/components/CalendarModal.svelte';
|
||||
import type { CalendarEvent } from '$lib/calendarHelpers.js';
|
||||
import { t } from '$lib/i18n/i18n.js';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let data: { event: Event; rsvps: RSVP[]; userId: string };
|
||||
type FormDataLocal = { success?: boolean; error?: string; type?: 'add' | 'remove' | 'copy' };
|
||||
@@ -27,6 +28,17 @@
|
||||
let typeToShow: 'add' | 'remove' | 'copy' | undefined;
|
||||
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
|
||||
$: event = data.event;
|
||||
$: rsvps = data.rsvps;
|
||||
@@ -40,10 +52,26 @@
|
||||
date: event.date,
|
||||
time: event.time,
|
||||
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
|
||||
$: if (form?.error) {
|
||||
error = String(form.error);
|
||||
@@ -79,22 +107,7 @@
|
||||
// Derive toast type from local or server form
|
||||
$: 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 = () => {
|
||||
error = '';
|
||||
@@ -476,7 +489,7 @@
|
||||
bind:isOpen={showCalendarModal}
|
||||
event={calendarEvent}
|
||||
{eventId}
|
||||
baseUrl={$page.url.origin}
|
||||
baseUrl={origin}
|
||||
on:close={closeCalendarModal}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 162 KiB |
Reference in New Issue
Block a user