mirror of
https://github.com/polaroi8d/cactoide.git
synced 2026-03-22 06:05:28 +00:00
Compare commits
14 Commits
feat/add-l
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d18afc43da | ||
|
|
662476f820 | ||
|
|
7ebf95bb16 | ||
|
|
0491ec4c4b | ||
|
|
fcdef065d7 | ||
|
|
a1fa879f36 | ||
|
|
b723aac180 | ||
|
|
277ad3ff14 | ||
|
|
52d48e4839 | ||
|
|
5b7178bec1 | ||
|
|
7531af9d29 | ||
|
|
94152b6740 | ||
|
|
6155cc44da | ||
|
|
719cd23350 |
@@ -10,4 +10,14 @@ APP_VERSION=latest
|
||||
PORT=5173
|
||||
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
|
||||
|
||||
# Federation config
|
||||
FEDERATION_INSTANCE=false
|
||||
|
||||
|
||||
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:
|
||||
|
||||
26
Makefile
26
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: help build up down db-only logs db-clean prune i18n lint format migrate-up migrate-down
|
||||
.PHONY: help build up down db-only db-seed logs db-clean prune i18n lint format migrate-up migrate-down
|
||||
|
||||
# Database connection variables
|
||||
DB_HOST ?= localhost
|
||||
@@ -19,10 +19,11 @@ help:
|
||||
@echo " up - Start all services"
|
||||
@echo " down - Stop all services"
|
||||
@echo " db-only - Start only the database"
|
||||
@echo " db-seed - Seed the database with sample data"
|
||||
@echo " logs - Show logs from all services"
|
||||
@echo " db-clean - Clean up all Docker resources"
|
||||
@echo " prune - Clean up everything (containers, images, volumes)"
|
||||
@echo " i18n - Validate translation files"
|
||||
@echo " i18n - List missing keys in translation file (use FILE=path/to/file.json)"
|
||||
@echo " lint - Lint the project"
|
||||
@echo " format - Format the project"
|
||||
@echo " migrate-up - Apply invite-only events migration"
|
||||
@@ -73,6 +74,17 @@ db-only:
|
||||
@echo "Starting only the database..."
|
||||
docker compose up -d postgres
|
||||
|
||||
# Seed the database with sample data
|
||||
db-seed:
|
||||
@echo "Seeding database with sample data..."
|
||||
@if [ -f "database/seed.sql" ]; then \
|
||||
psql "$(DB_URL)" -f database/seed.sql && \
|
||||
echo "Database seeded successfully!"; \
|
||||
else \
|
||||
echo "Seed file not found: database/seed.sql"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# Show logs from all services
|
||||
logs:
|
||||
@echo "Showing logs from all services..."
|
||||
@@ -94,8 +106,10 @@ format:
|
||||
@echo "Formatting the project..."
|
||||
npm run format
|
||||
|
||||
#TODO: not working yet
|
||||
# List missing keys in a translation file
|
||||
i18n:
|
||||
@echo "Validating translation files..."
|
||||
@if [ -n "$(FILE)" ]; then \
|
||||
./scripts/i18n-check.sh $(FILE); \
|
||||
@if [ -z "$(FILE)" ]; then \
|
||||
echo "Error: FILE variable is required. Example: make i18n FILE=src/lib/i18n/it.json"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@./scripts/i18n-check.sh --missing-only $(FILE)
|
||||
|
||||
17
README.md
17
README.md
@@ -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>
|
||||
@@ -59,6 +59,17 @@ make db-only
|
||||
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`.
|
||||
|
||||
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):
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS events (
|
||||
type VARCHAR(20) NOT NULL CHECK (type IN ('limited','unlimited')),
|
||||
attendee_limit INTEGER CHECK (attendee_limit > 0),
|
||||
user_id VARCHAR(100) NOT NULL,
|
||||
visibility VARCHAR(20) NOT NULL DEFAULT 'public' CHECK (visibility IN ('public','private')),
|
||||
visibility VARCHAR(20) NOT NULL DEFAULT 'public' CHECK (visibility IN ('public','private', 'invite-only')),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
@@ -34,6 +34,15 @@ CREATE TABLE IF NOT EXISTS rsvps (
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Invite tokens
|
||||
CREATE TABLE IF NOT EXISTS invite_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id VARCHAR(8) NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||
token VARCHAR(32) NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- =======================================
|
||||
-- Indexes
|
||||
-- =======================================
|
||||
@@ -42,5 +51,9 @@ CREATE INDEX IF NOT EXISTS idx_events_date ON events(date);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_location_type ON events(location_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_rsvps_event_id ON rsvps(event_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_rsvps_user_id ON rsvps(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invite_tokens_event_id ON invite_tokens(event_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invite_tokens_token ON invite_tokens(token);
|
||||
CREATE INDEX IF NOT EXISTS idx_invite_tokens_expires_at ON invite_tokens(expires_at);
|
||||
|
||||
|
||||
COMMIT;
|
||||
@@ -1,25 +0,0 @@
|
||||
-- Migration: Add invite-only events feature
|
||||
-- Created: 2024-12-20
|
||||
-- Description: Adds invite-only visibility option and invite tokens table
|
||||
|
||||
-- Add 'invite-only' to the visibility enum
|
||||
ALTER TABLE events
|
||||
DROP CONSTRAINT IF EXISTS events_visibility_check;
|
||||
|
||||
ALTER TABLE events
|
||||
ADD CONSTRAINT events_visibility_check
|
||||
CHECK (visibility IN ('public', 'private', 'invite-only'));
|
||||
|
||||
-- Create invite_tokens table
|
||||
CREATE TABLE IF NOT EXISTS invite_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id VARCHAR(8) NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||
token VARCHAR(32) NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create indexes for invite_tokens table
|
||||
CREATE INDEX IF NOT EXISTS idx_invite_tokens_event_id ON invite_tokens(event_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invite_tokens_token ON invite_tokens(token);
|
||||
CREATE INDEX IF NOT EXISTS idx_invite_tokens_expires_at ON invite_tokens(expires_at);
|
||||
@@ -1,17 +0,0 @@
|
||||
-- Rollback Migration: Remove invite-only events feature
|
||||
-- Created: 2024-12-20
|
||||
-- Description: Removes invite-only visibility option and invite tokens table
|
||||
|
||||
-- Drop invite_tokens table and its indexes
|
||||
DROP INDEX IF EXISTS idx_invite_tokens_expires_at;
|
||||
DROP INDEX IF EXISTS idx_invite_tokens_token;
|
||||
DROP INDEX IF EXISTS idx_invite_tokens_event_id;
|
||||
DROP TABLE IF EXISTS invite_tokens;
|
||||
|
||||
-- Revert visibility enum to original values
|
||||
ALTER TABLE events
|
||||
DROP CONSTRAINT IF EXISTS events_visibility_check;
|
||||
|
||||
ALTER TABLE events
|
||||
ADD CONSTRAINT events_visibility_check
|
||||
CHECK (visibility IN ('public', 'private'));
|
||||
@@ -37,6 +37,10 @@ services:
|
||||
DATABASE_URL: ${DATABASE_URL:-postgres://cactoide:cactoide_password@postgres:5432/cactoide_database}
|
||||
PORT: 3000
|
||||
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:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
const config = {
|
||||
name: 'Cactoide Genesis',
|
||||
instances: [
|
||||
{
|
||||
url: 'cactoide.org'
|
||||
},
|
||||
{
|
||||
url: 'cactoide.dalev.hu'
|
||||
},
|
||||
{
|
||||
url: 'localhost:5174'
|
||||
},
|
||||
{
|
||||
url: 'localhost:5175'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,191 +1,89 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
# Find keys present in messages.json but missing in a translation file.
|
||||
|
||||
# Translation validation script
|
||||
# Compares a translation file against the source messages.json to find missing keys
|
||||
set -euo pipefail
|
||||
|
||||
set -e
|
||||
SOURCE_DEFAULT="src/lib/i18n/messages.json"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Default paths
|
||||
SOURCE_FILE="src/lib/i18n/messages.json"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# Function to show usage
|
||||
show_usage() {
|
||||
echo "Usage: $0 [LANGUAGE_FILE]"
|
||||
echo ""
|
||||
echo "Validates a translation file against the source messages.json"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " LANGUAGE_FILE Path to the translation file to validate (e.g., src/lib/i18n/it.json)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 src/lib/i18n/it.json"
|
||||
echo " $0 src/lib/i18n/fr.json"
|
||||
echo ""
|
||||
echo "If no file is provided, it will check all .json files in src/lib/i18n/ except messages.json"
|
||||
}
|
||||
|
||||
# Function to get all keys from a JSON file recursively
|
||||
get_keys() {
|
||||
local file="$1"
|
||||
local prefix="$2"
|
||||
|
||||
# Use jq to extract all keys recursively
|
||||
jq -r 'paths(scalars) as $p | $p | join(".")' "$file" | while read -r key; do
|
||||
if [ -n "$prefix" ]; then
|
||||
echo "${prefix}.${key}"
|
||||
else
|
||||
echo "$key"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Function to validate a single translation file
|
||||
validate_file() {
|
||||
local translation_file="$1"
|
||||
local source_file="$2"
|
||||
|
||||
echo -e "${YELLOW}Validating: $translation_file${NC}"
|
||||
echo "----------------------------------------"
|
||||
|
||||
# Check if files exist
|
||||
if [ ! -f "$source_file" ]; then
|
||||
echo -e "${RED}Error: Source file $source_file not found${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$translation_file" ]; then
|
||||
echo -e "${RED}Error: Translation file $translation_file not found${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get all keys from source file
|
||||
local source_keys
|
||||
source_keys=$(get_keys "$source_file")
|
||||
|
||||
# Get all keys from translation file
|
||||
local translation_keys
|
||||
translation_keys=$(get_keys "$translation_file")
|
||||
|
||||
# Find missing keys
|
||||
local missing_keys
|
||||
missing_keys=$(comm -23 <(echo "$source_keys" | sort) <(echo "$translation_keys" | sort))
|
||||
|
||||
# Find extra keys (in translation but not in source)
|
||||
local extra_keys
|
||||
extra_keys=$(comm -13 <(echo "$source_keys" | sort) <(echo "$translation_keys" | sort))
|
||||
|
||||
# Count missing and extra keys
|
||||
local missing_count
|
||||
if [ -z "$missing_keys" ]; then
|
||||
missing_count=0
|
||||
else
|
||||
missing_count=$(echo "$missing_keys" | wc -l | tr -d ' ')
|
||||
fi
|
||||
|
||||
local extra_count
|
||||
if [ -z "$extra_keys" ]; then
|
||||
extra_count=0
|
||||
else
|
||||
extra_count=$(echo "$extra_keys" | wc -l | tr -d ' ')
|
||||
fi
|
||||
|
||||
# Report results
|
||||
if [ "$missing_count" -eq 0 ] && [ "$extra_count" -eq 0 ]; then
|
||||
echo -e "${GREEN} Perfect! All keys match.${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$missing_count" -gt 0 ]; then
|
||||
echo -e "${RED} Missing $missing_count key(s) in translation:${NC}"
|
||||
echo "$missing_keys" | while read -r key; do
|
||||
echo -e " ${RED}• $key${NC}"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ "$extra_count" -gt 0 ]; then
|
||||
echo -e "${YELLOW} Extra $extra_count key(s) in translation (not in source):${NC}"
|
||||
echo "$extra_keys" | while read -r key; do
|
||||
echo -e " ${YELLOW}• $key${NC}"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Return error code if there are missing keys
|
||||
if [ "$missing_count" -gt 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
local translation_file="$1"
|
||||
local source_file="$PROJECT_ROOT/$SOURCE_FILE"
|
||||
local exit_code=0
|
||||
|
||||
# Change to project root directory
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# If no file specified, check all translation files
|
||||
if [ -z "$translation_file" ]; then
|
||||
echo -e "${YELLOW}No file specified. Checking all translation files...${NC}"
|
||||
echo ""
|
||||
|
||||
# Find all .json files in i18n directory except messages.json
|
||||
local files
|
||||
files=$(find src/lib/i18n -name "*.json" -not -name "messages.json" 2>/dev/null || true)
|
||||
|
||||
if [ -z "$files" ]; then
|
||||
echo -e "${YELLOW}No translation files found in src/lib/i18n/${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Validate each file
|
||||
echo "$files" | while read -r file; do
|
||||
if [ -n "$file" ]; then
|
||||
if ! validate_file "$file" "$source_file"; then
|
||||
exit_code=1
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
|
||||
return $exit_code
|
||||
fi
|
||||
|
||||
# Validate the specified file
|
||||
if ! validate_file "$translation_file" "$source_file"; then
|
||||
exit_code=1
|
||||
fi
|
||||
|
||||
return $exit_code
|
||||
usage() {
|
||||
echo "Usage: $0 [--missing-only] <translation.json> [source_messages.json]"
|
||||
echo "Compares <translation.json> against messages.json and prints missing keys."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if jq is installed
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo -e "${RED}Error: jq is required but not installed.${NC}"
|
||||
echo "Please install jq:"
|
||||
echo " macOS: brew install jq"
|
||||
echo " Ubuntu/Debian: sudo apt-get install jq"
|
||||
echo " CentOS/RHEL: sudo yum install jq"
|
||||
exit 1
|
||||
command -v jq >/dev/null 2>&1 || {
|
||||
echo "Error: jq is required but not installed." >&2
|
||||
echo "Please install jq:" >&2
|
||||
echo " macOS: brew install jq" >&2
|
||||
echo " Ubuntu/Debian: sudo apt-get install jq" >&2
|
||||
echo " CentOS/RHEL: sudo yum install jq" >&2
|
||||
exit 127
|
||||
}
|
||||
|
||||
# Parse arguments (handle --missing-only flag for Makefile compatibility)
|
||||
TRANSLATION=""
|
||||
SOURCE="$SOURCE_DEFAULT"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--missing-only|-m)
|
||||
# Flag is accepted but ignored (this script only does missing keys)
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
;;
|
||||
-*)
|
||||
echo "Error: Unknown option: $1" >&2
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$TRANSLATION" ]]; then
|
||||
TRANSLATION="$1"
|
||||
elif [[ "$SOURCE" == "$SOURCE_DEFAULT" ]]; then
|
||||
SOURCE="$1"
|
||||
else
|
||||
echo "Error: Too many arguments" >&2
|
||||
usage
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate arguments
|
||||
[[ -z "$TRANSLATION" ]] && {
|
||||
echo "Error: Translation file is required" >&2
|
||||
usage
|
||||
}
|
||||
|
||||
# Validate files exist
|
||||
[[ -f "$SOURCE" ]] || {
|
||||
echo "Error: Source file not found: $SOURCE" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ -f "$TRANSLATION" ]] || {
|
||||
echo "Error: Translation file not found: $TRANSLATION" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Extract all keys from a JSON file (dot-joined paths to scalar values)
|
||||
keys() {
|
||||
jq -r 'paths(scalars) | join(".")' "$1" | sort -u
|
||||
}
|
||||
|
||||
# Find missing keys: in SOURCE but not in TRANSLATION
|
||||
missing=$(comm -23 <(keys "$SOURCE") <(keys "$TRANSLATION"))
|
||||
|
||||
if [[ -z "$missing" ]]; then
|
||||
echo "No missing keys found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Handle help flag
|
||||
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
|
||||
show_usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run main function
|
||||
main "$1"
|
||||
echo "Missing keys in $(basename "$TRANSLATION"):"
|
||||
echo "$missing" | sed 's/^/ - /'
|
||||
echo
|
||||
echo "Total missing keys: $(echo "$missing" | wc -l | tr -d ' ')"
|
||||
exit 1
|
||||
|
||||
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>
|
||||
13
src/lib/config/federation.config.js
Normal file
13
src/lib/config/federation.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const config = {
|
||||
name: 'Cactoide Genesis',
|
||||
instances: [
|
||||
// {
|
||||
// url: 'cactoide.org'
|
||||
// }
|
||||
// {
|
||||
// url: 'YOUR_INSTANCE_URL'
|
||||
// }
|
||||
]
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,76 +1,18 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { logger } from '$lib/logger';
|
||||
import type { Event } from '$lib/types';
|
||||
|
||||
import config from '../../federation.config.js';
|
||||
|
||||
console.log(config.instances);
|
||||
|
||||
interface FederationConfig {
|
||||
name: string;
|
||||
instances: Array<{ url: string }>;
|
||||
}
|
||||
import config from '$lib/config/federation.config.js';
|
||||
|
||||
interface FederationEventsResponse {
|
||||
events: Array<Event & { federation?: boolean }>;
|
||||
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
|
||||
*/
|
||||
async function fetchEventsFromInstance(instanceUrl: string): Promise<Event[]> {
|
||||
try {
|
||||
// Ensure URL has protocol and append /api/federation/events
|
||||
|
||||
const apiUrl = `http://${instanceUrl}/api/federation/events`;
|
||||
|
||||
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
|
||||
*/
|
||||
export async function fetchAllFederatedEvents(): Promise<Event[]> {
|
||||
const config = await readFederationConfig();
|
||||
|
||||
if (!config || !config.instances || config.instances.length === 0) {
|
||||
logger.debug('No federation config or instances found');
|
||||
return [];
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{ instanceCount: config.instances.length },
|
||||
'Fetching events from federated instances'
|
||||
);
|
||||
|
||||
// Fetch from all instances in parallel
|
||||
const fetchPromises = config.instances.map((instance) => fetchEventsFromInstance(instance.url));
|
||||
|
||||
|
||||
@@ -107,7 +107,10 @@
|
||||
"description": "Create and manage event RSVPs. No registration required, instant sharing.",
|
||||
"mainTitle": "Cactoide(ea)",
|
||||
"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)?🌵",
|
||||
"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",
|
||||
@@ -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.",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { database } from '$lib/database/db';
|
||||
import { events } from '$lib/database/schema';
|
||||
import { eq, count } from 'drizzle-orm';
|
||||
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';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { logger } from '$lib/logger';
|
||||
import federationConfig from '../../../federation.config.js';
|
||||
import federationConfig from '$lib/config/federation.config.js';
|
||||
|
||||
interface InstanceInfo {
|
||||
name: string;
|
||||
|
||||
@@ -130,10 +130,7 @@
|
||||
|
||||
<p class="py-8 text-center text-slate-400">
|
||||
{t('instance.description')}
|
||||
<a
|
||||
href="https://github.com/cactoide/cactoide/blob/main/federation.config.js"
|
||||
class="text-violet-300/80">{t('instance.configFile')}</a
|
||||
>
|
||||
{t('instance.configFile')}
|
||||
{t('instance.file')}
|
||||
</p>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user