forked from jmug/cactoide
feat: invite only events
This commit is contained in:
109
Makefile
109
Makefile
@@ -1,17 +1,4 @@
|
||||
# Cactoide Makefile
|
||||
# Database and application management commands
|
||||
|
||||
.PHONY: help migrate-up migrate-down db-reset dev build test
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "Available commands:"
|
||||
@echo " migrate-up - Apply invite-only events migration"
|
||||
@echo " migrate-down - Rollback invite-only events migration"
|
||||
@echo " db-reset - Reset database to initial state"
|
||||
@echo " dev - Start development server"
|
||||
@echo " build - Build the application"
|
||||
@echo " test - Run tests"
|
||||
.PHONY: help build up down db-only logs db-clean prune i18n lint format migrate-up migrate-down
|
||||
|
||||
# Database connection variables
|
||||
DB_HOST ?= localhost
|
||||
@@ -26,6 +13,21 @@ MIGRATIONS_DIR = database/migrations
|
||||
# Database connection string
|
||||
DB_URL = postgresql://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)
|
||||
|
||||
help:
|
||||
@echo "Available commands:"
|
||||
@echo " build - Docker build the application"
|
||||
@echo " up - Start all services"
|
||||
@echo " down - Stop all services"
|
||||
@echo " db-only - Start only the database"
|
||||
@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 " lint - Lint the project"
|
||||
@echo " format - Format the project"
|
||||
@echo " migrate-up - Apply invite-only events migration"
|
||||
@echo " migrate-down - Rollback invite-only events migration"
|
||||
|
||||
# Apply invite-only events migration
|
||||
migrate-up:
|
||||
@echo "Applying invite-only events migration..."
|
||||
@@ -48,47 +50,52 @@ migrate-down:
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# Reset database to initial state
|
||||
db-reset:
|
||||
@echo "Resetting database..."
|
||||
@psql "$(DB_URL)" -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;"
|
||||
@psql "$(DB_URL)" -f database/init.sql
|
||||
@echo "Database reset complete!"
|
||||
|
||||
# Development server
|
||||
dev:
|
||||
@echo "Starting development server..."
|
||||
npm run dev
|
||||
|
||||
# Build application
|
||||
# Build the Docker images
|
||||
build:
|
||||
@echo "Building application..."
|
||||
npm run build
|
||||
@echo "Building Docker images..."
|
||||
docker compose build
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
@echo "Running tests..."
|
||||
npm run test
|
||||
# Start all services
|
||||
up:
|
||||
@echo "Starting all services..."
|
||||
docker compose up -d
|
||||
|
||||
# Install dependencies
|
||||
install:
|
||||
@echo "Installing dependencies..."
|
||||
npm install
|
||||
down:
|
||||
@echo "Stopping all services..."
|
||||
docker compose down
|
||||
|
||||
# Docker commands
|
||||
docker-build:
|
||||
@echo "Building Docker image..."
|
||||
docker build -t cactoide .
|
||||
db-clean:
|
||||
@echo "Cleaning up all Docker resources..."
|
||||
docker stop cactoide-db && docker rm cactoide-db && docker volume prune -f && docker network prune -f
|
||||
|
||||
docker-run:
|
||||
@echo "Running Docker container..."
|
||||
docker run -p 3000:3000 cactoide
|
||||
# Start only the database
|
||||
db-only:
|
||||
@echo "Starting only the database..."
|
||||
docker compose up -d postgres
|
||||
|
||||
# Database setup for development
|
||||
db-setup: install db-reset migrate-up
|
||||
@echo "Database setup complete!"
|
||||
# Show logs from all services
|
||||
logs:
|
||||
@echo "Showing logs from all services..."
|
||||
docker compose logs -f
|
||||
|
||||
# Full development setup
|
||||
setup: install db-setup
|
||||
@echo "Development environment ready!"
|
||||
@echo "Run 'make dev' to start the development server"
|
||||
|
||||
|
||||
# Clean up everything (containers, images, volumes)
|
||||
prune:
|
||||
@echo "Cleaning up all Docker resources..."
|
||||
docker compose down -v --rmi all
|
||||
|
||||
|
||||
lint:
|
||||
@echo "Linting the project..."
|
||||
npm run lint
|
||||
|
||||
format:
|
||||
@echo "Formatting the project..."
|
||||
npm run format
|
||||
|
||||
#TODO: not working yet
|
||||
i18n:
|
||||
@echo "Validating translation files..."
|
||||
@if [ -n "$(FILE)" ]; then \
|
||||
./scripts/i18n-check.sh $(FILE); \
|
||||
|
||||
@@ -42,25 +42,5 @@ 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);
|
||||
|
||||
-- =======================================
|
||||
-- Triggers (updated_at maintenance)
|
||||
-- =======================================
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS update_events_updated_at ON events;
|
||||
CREATE TRIGGER update_events_updated_at
|
||||
BEFORE UPDATE ON events
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
COMMIT;
|
||||
@@ -41,7 +41,6 @@
|
||||
"numberOfGuests": "Numero di Ospiti",
|
||||
"addGuests": "Aggiungi ospiti",
|
||||
"joinEvent": "Partecipa all'Evento",
|
||||
"copyLink": "Copia Link",
|
||||
"addToCalendar": "Aggiungi al Calendario",
|
||||
"close": "Chiudi",
|
||||
"closeModal": "Chiudi finestra",
|
||||
@@ -64,7 +63,6 @@
|
||||
"eventNotFound": "Evento Non Trovato",
|
||||
"eventIsFull": "L'Evento è Pieno!",
|
||||
"maximumCapacityReached": "Raggiunta la capacità massima",
|
||||
"eventLinkCopied": "Link dell'evento copiato negli appunti!",
|
||||
"rsvpAddedSuccessfully": "RSVP aggiunto con successo!",
|
||||
"removedRsvpSuccessfully": "RSVP rimosso con successo.",
|
||||
"anUnexpectedErrorOccurred": "Si è verificato un errore inaspettato.",
|
||||
@@ -188,8 +186,10 @@
|
||||
"noAttendeesYet": "Ancora nessun partecipante",
|
||||
"beFirstToJoin": "Sii il primo a partecipare!",
|
||||
"copyLinkButton": "Copia Link",
|
||||
"copyInviteLinkButton": "Copia Link Invito",
|
||||
"addToCalendarButton": "Aggiungi al Calendario",
|
||||
"eventLinkCopied": "Link dell'evento copiato negli appunti!",
|
||||
"inviteLinkCopied": "Link invito copiato negli appunti!",
|
||||
"rsvpAddedSuccessfully": "RSVP aggiunto con successo!",
|
||||
"removedRsvpSuccessfully": "RSVP rimosso con successo.",
|
||||
"failedToAddRsvp": "Impossibile aggiungere RSVP",
|
||||
@@ -208,7 +208,8 @@
|
||||
"viewEventAriaLabel": "Visualizza evento",
|
||||
"editEventAriaLabel": "Modifica evento",
|
||||
"deleteEventAriaLabel": "Elimina evento",
|
||||
"removeRsvpAriaLabel": "Rimuovi RSVP"
|
||||
"removeRsvpAriaLabel": "Rimuovi RSVP",
|
||||
"inviteLinkExpiresAt": "Questo link scade quando l'evento inizia: {time}"
|
||||
},
|
||||
"discover": {
|
||||
"title": "Scopri Eventi - Cactoide",
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
"numberOfGuests": "Number of Guests",
|
||||
"addGuests": "Add guest users",
|
||||
"joinEvent": "Join Event",
|
||||
"copyLink": "Event link copied to clipboard.",
|
||||
"addToCalendar": "Add to Calendar",
|
||||
"close": "Close",
|
||||
"closeModal": "Close modal",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
$: event = data.event;
|
||||
$: rsvps = data.rsvps;
|
||||
$: currentUserId = data.userId;
|
||||
$: isEventCreator = event.user_id === currentUserId;
|
||||
|
||||
// Create calendar event object when event data changes
|
||||
$: if (event && browser) {
|
||||
@@ -77,7 +78,7 @@
|
||||
const eventId = $page.params.id || '';
|
||||
|
||||
const copyEventLink = () => {
|
||||
if (browser) {
|
||||
if (browser && isEventCreator) {
|
||||
const url = `${$page.url.origin}/event/${eventId}`;
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
toastType = 'copy';
|
||||
@@ -442,12 +443,17 @@
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="max-w-2xl space-y-3">
|
||||
{#if event.visibility !== 'invite-only'}
|
||||
<button
|
||||
on:click={copyEventLink}
|
||||
class="hover:bg-violet-400/70' w-full rounded-sm border-2 border-violet-500 bg-violet-400/20 px-4 py-3 py-4 font-bold font-medium font-semibold text-white shadow-lg transition-all duration-200 hover:scale-105"
|
||||
disabled={!isEventCreator}
|
||||
class="w-full rounded-sm border-2 px-4 py-3 py-4 font-bold font-medium font-semibold text-white shadow-lg transition-all duration-200 hover:scale-105 {isEventCreator
|
||||
? 'border-violet-500 bg-violet-400/20 hover:bg-violet-400/70'
|
||||
: 'cursor-not-allowed border-gray-500 bg-gray-600/20 hover:bg-gray-600/30'}"
|
||||
>
|
||||
{t('event.copyLinkButton')}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
on:click={openCalendarModal}
|
||||
class="hover:bg-violet-400/70' w-full rounded-sm border-2 border-violet-500 bg-violet-400/20 px-4 py-3 py-4 font-bold font-medium font-semibold text-white shadow-lg transition-all duration-200 hover:scale-105"
|
||||
|
||||
@@ -45,7 +45,8 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
|
||||
|
||||
return {
|
||||
event: event[0],
|
||||
inviteToken
|
||||
inviteToken,
|
||||
userId
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
let isSubmitting = false;
|
||||
let inviteToken = data.inviteToken;
|
||||
|
||||
let inviteLinkCopied = false;
|
||||
let showInviteLinkToast = false;
|
||||
let toastHideTimer: number | null = null;
|
||||
|
||||
// Get today's date in YYYY-MM-DD format for min attribute
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
@@ -76,9 +77,12 @@
|
||||
const inviteUrl = `${window.location.origin}/event/${data.event.id}/invite/${inviteToken.token}`;
|
||||
try {
|
||||
await navigator.clipboard.writeText(inviteUrl);
|
||||
inviteLinkCopied = true;
|
||||
setTimeout(() => {
|
||||
inviteLinkCopied = false;
|
||||
showInviteLinkToast = true;
|
||||
|
||||
// Auto-hide toast after 3 seconds
|
||||
if (toastHideTimer) clearTimeout(toastHideTimer);
|
||||
toastHideTimer = window.setTimeout(() => {
|
||||
showInviteLinkToast = false;
|
||||
}, 3000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy invite link:', err);
|
||||
@@ -375,8 +379,8 @@
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<!-- Invite Link Section (only for invite-only events) -->
|
||||
{#if eventData.visibility === 'invite-only' && inviteToken}
|
||||
<!-- Invite Link Section (only for invite-only events and event creator) -->
|
||||
{#if eventData.visibility === 'invite-only' && inviteToken && data.event.userId === data.userId}
|
||||
<div class="rounded-sm border border-amber-500/30 bg-amber-900/20 p-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-amber-400">Invite Link</h3>
|
||||
@@ -395,7 +399,7 @@
|
||||
on:click={copyInviteLink}
|
||||
class="rounded-sm border border-amber-300 bg-amber-200 px-3 py-2 text-sm font-medium text-amber-900 hover:bg-amber-300"
|
||||
>
|
||||
{inviteLinkCopied ? t('common.success') : t('common.copyLink')}
|
||||
{t('event.copyInviteLinkButton')}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-amber-300">
|
||||
@@ -436,3 +440,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invite Link Toast -->
|
||||
{#if showInviteLinkToast}
|
||||
<div
|
||||
class="fixed right-4 bottom-4 z-40 w-128 rounded-sm border border-yellow-500/30 bg-yellow-900/20 p-4 text-yellow-400"
|
||||
>
|
||||
{t('event.inviteLinkCopied')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
$: event = data.event;
|
||||
$: rsvps = data.rsvps;
|
||||
$: currentUserId = data.userId;
|
||||
$: isEventCreator = event.user_id === currentUserId;
|
||||
|
||||
// Create calendar event object when event data changes
|
||||
$: if (event && browser) {
|
||||
@@ -58,7 +59,7 @@
|
||||
const token = $page.params.token || '';
|
||||
|
||||
const copyInviteLink = () => {
|
||||
if (browser) {
|
||||
if (browser && isEventCreator) {
|
||||
const url = `${$page.url.origin}/event/${eventId}/invite/${token}`;
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
success = 'Invite link copied to clipboard!';
|
||||
@@ -415,7 +416,10 @@
|
||||
<div class="max-w-2xl space-y-3">
|
||||
<button
|
||||
on:click={copyInviteLink}
|
||||
class="hover:bg-violet-400/70' w-full rounded-sm border-2 border-violet-500 bg-violet-400/20 px-4 py-3 py-4 font-bold font-medium font-semibold text-white shadow-lg transition-all duration-200 hover:scale-105"
|
||||
disabled={!isEventCreator}
|
||||
class="w-full rounded-sm border-2 px-4 py-3 py-4 font-bold font-medium font-semibold text-white shadow-lg transition-all duration-200 hover:scale-105 {isEventCreator
|
||||
? 'border-violet-500 bg-violet-400/20 hover:bg-violet-400/70'
|
||||
: 'cursor-not-allowed border-gray-500 bg-gray-600/20 hover:bg-gray-600/30'}"
|
||||
>
|
||||
{t('event.copyInviteLinkButton')}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user