mirror of
https://github.com/polaroi8d/cactoide.git
synced 2026-03-21 21:55:27 +00:00
feat: invite only events
This commit is contained in:
109
Makefile
109
Makefile
@@ -1,17 +1,4 @@
|
|||||||
# Cactoide Makefile
|
.PHONY: help build up down db-only logs db-clean prune i18n lint format migrate-up migrate-down
|
||||||
# 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"
|
|
||||||
|
|
||||||
# Database connection variables
|
# Database connection variables
|
||||||
DB_HOST ?= localhost
|
DB_HOST ?= localhost
|
||||||
@@ -26,6 +13,21 @@ MIGRATIONS_DIR = database/migrations
|
|||||||
# Database connection string
|
# Database connection string
|
||||||
DB_URL = postgresql://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)
|
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
|
# Apply invite-only events migration
|
||||||
migrate-up:
|
migrate-up:
|
||||||
@echo "Applying invite-only events migration..."
|
@echo "Applying invite-only events migration..."
|
||||||
@@ -48,47 +50,52 @@ migrate-down:
|
|||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Reset database to initial state
|
# Build the Docker images
|
||||||
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:
|
build:
|
||||||
@echo "Building application..."
|
@echo "Building Docker images..."
|
||||||
npm run build
|
docker compose build
|
||||||
|
|
||||||
# Run tests
|
# Start all services
|
||||||
test:
|
up:
|
||||||
@echo "Running tests..."
|
@echo "Starting all services..."
|
||||||
npm run test
|
docker compose up -d
|
||||||
|
|
||||||
# Install dependencies
|
down:
|
||||||
install:
|
@echo "Stopping all services..."
|
||||||
@echo "Installing dependencies..."
|
docker compose down
|
||||||
npm install
|
|
||||||
|
|
||||||
# Docker commands
|
db-clean:
|
||||||
docker-build:
|
@echo "Cleaning up all Docker resources..."
|
||||||
@echo "Building Docker image..."
|
docker stop cactoide-db && docker rm cactoide-db && docker volume prune -f && docker network prune -f
|
||||||
docker build -t cactoide .
|
|
||||||
|
|
||||||
docker-run:
|
# Start only the database
|
||||||
@echo "Running Docker container..."
|
db-only:
|
||||||
docker run -p 3000:3000 cactoide
|
@echo "Starting only the database..."
|
||||||
|
docker compose up -d postgres
|
||||||
|
|
||||||
# Database setup for development
|
# Show logs from all services
|
||||||
db-setup: install db-reset migrate-up
|
logs:
|
||||||
@echo "Database setup complete!"
|
@echo "Showing logs from all services..."
|
||||||
|
docker compose logs -f
|
||||||
|
|
||||||
# Full development setup
|
|
||||||
setup: install db-setup
|
|
||||||
@echo "Development environment ready!"
|
# Clean up everything (containers, images, volumes)
|
||||||
@echo "Run 'make dev' to start the development server"
|
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_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_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_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;
|
COMMIT;
|
||||||
@@ -41,7 +41,6 @@
|
|||||||
"numberOfGuests": "Numero di Ospiti",
|
"numberOfGuests": "Numero di Ospiti",
|
||||||
"addGuests": "Aggiungi ospiti",
|
"addGuests": "Aggiungi ospiti",
|
||||||
"joinEvent": "Partecipa all'Evento",
|
"joinEvent": "Partecipa all'Evento",
|
||||||
"copyLink": "Copia Link",
|
|
||||||
"addToCalendar": "Aggiungi al Calendario",
|
"addToCalendar": "Aggiungi al Calendario",
|
||||||
"close": "Chiudi",
|
"close": "Chiudi",
|
||||||
"closeModal": "Chiudi finestra",
|
"closeModal": "Chiudi finestra",
|
||||||
@@ -64,7 +63,6 @@
|
|||||||
"eventNotFound": "Evento Non Trovato",
|
"eventNotFound": "Evento Non Trovato",
|
||||||
"eventIsFull": "L'Evento è Pieno!",
|
"eventIsFull": "L'Evento è Pieno!",
|
||||||
"maximumCapacityReached": "Raggiunta la capacità massima",
|
"maximumCapacityReached": "Raggiunta la capacità massima",
|
||||||
"eventLinkCopied": "Link dell'evento copiato negli appunti!",
|
|
||||||
"rsvpAddedSuccessfully": "RSVP aggiunto con successo!",
|
"rsvpAddedSuccessfully": "RSVP aggiunto con successo!",
|
||||||
"removedRsvpSuccessfully": "RSVP rimosso con successo.",
|
"removedRsvpSuccessfully": "RSVP rimosso con successo.",
|
||||||
"anUnexpectedErrorOccurred": "Si è verificato un errore inaspettato.",
|
"anUnexpectedErrorOccurred": "Si è verificato un errore inaspettato.",
|
||||||
@@ -188,8 +186,10 @@
|
|||||||
"noAttendeesYet": "Ancora nessun partecipante",
|
"noAttendeesYet": "Ancora nessun partecipante",
|
||||||
"beFirstToJoin": "Sii il primo a partecipare!",
|
"beFirstToJoin": "Sii il primo a partecipare!",
|
||||||
"copyLinkButton": "Copia Link",
|
"copyLinkButton": "Copia Link",
|
||||||
|
"copyInviteLinkButton": "Copia Link Invito",
|
||||||
"addToCalendarButton": "Aggiungi al Calendario",
|
"addToCalendarButton": "Aggiungi al Calendario",
|
||||||
"eventLinkCopied": "Link dell'evento copiato negli appunti!",
|
"eventLinkCopied": "Link dell'evento copiato negli appunti!",
|
||||||
|
"inviteLinkCopied": "Link invito copiato negli appunti!",
|
||||||
"rsvpAddedSuccessfully": "RSVP aggiunto con successo!",
|
"rsvpAddedSuccessfully": "RSVP aggiunto con successo!",
|
||||||
"removedRsvpSuccessfully": "RSVP rimosso con successo.",
|
"removedRsvpSuccessfully": "RSVP rimosso con successo.",
|
||||||
"failedToAddRsvp": "Impossibile aggiungere RSVP",
|
"failedToAddRsvp": "Impossibile aggiungere RSVP",
|
||||||
@@ -208,7 +208,8 @@
|
|||||||
"viewEventAriaLabel": "Visualizza evento",
|
"viewEventAriaLabel": "Visualizza evento",
|
||||||
"editEventAriaLabel": "Modifica evento",
|
"editEventAriaLabel": "Modifica evento",
|
||||||
"deleteEventAriaLabel": "Elimina evento",
|
"deleteEventAriaLabel": "Elimina evento",
|
||||||
"removeRsvpAriaLabel": "Rimuovi RSVP"
|
"removeRsvpAriaLabel": "Rimuovi RSVP",
|
||||||
|
"inviteLinkExpiresAt": "Questo link scade quando l'evento inizia: {time}"
|
||||||
},
|
},
|
||||||
"discover": {
|
"discover": {
|
||||||
"title": "Scopri Eventi - Cactoide",
|
"title": "Scopri Eventi - Cactoide",
|
||||||
|
|||||||
@@ -42,7 +42,6 @@
|
|||||||
"numberOfGuests": "Number of Guests",
|
"numberOfGuests": "Number of Guests",
|
||||||
"addGuests": "Add guest users",
|
"addGuests": "Add guest users",
|
||||||
"joinEvent": "Join Event",
|
"joinEvent": "Join Event",
|
||||||
"copyLink": "Event link copied to clipboard.",
|
|
||||||
"addToCalendar": "Add to Calendar",
|
"addToCalendar": "Add to Calendar",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"closeModal": "Close modal",
|
"closeModal": "Close modal",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
$: event = data.event;
|
$: event = data.event;
|
||||||
$: rsvps = data.rsvps;
|
$: rsvps = data.rsvps;
|
||||||
$: currentUserId = data.userId;
|
$: currentUserId = data.userId;
|
||||||
|
$: 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) {
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
const eventId = $page.params.id || '';
|
const eventId = $page.params.id || '';
|
||||||
|
|
||||||
const copyEventLink = () => {
|
const copyEventLink = () => {
|
||||||
if (browser) {
|
if (browser && isEventCreator) {
|
||||||
const url = `${$page.url.origin}/event/${eventId}`;
|
const url = `${$page.url.origin}/event/${eventId}`;
|
||||||
navigator.clipboard.writeText(url).then(() => {
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
toastType = 'copy';
|
toastType = 'copy';
|
||||||
@@ -442,12 +443,17 @@
|
|||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="max-w-2xl space-y-3">
|
<div class="max-w-2xl space-y-3">
|
||||||
<button
|
{#if event.visibility !== 'invite-only'}
|
||||||
on:click={copyEventLink}
|
<button
|
||||||
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"
|
on:click={copyEventLink}
|
||||||
>
|
disabled={!isEventCreator}
|
||||||
{t('event.copyLinkButton')}
|
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
|
||||||
</button>
|
? '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
|
<button
|
||||||
on:click={openCalendarModal}
|
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"
|
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 {
|
return {
|
||||||
event: event[0],
|
event: event[0],
|
||||||
inviteToken
|
inviteToken,
|
||||||
|
userId
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,8 @@
|
|||||||
let isSubmitting = false;
|
let isSubmitting = false;
|
||||||
let inviteToken = data.inviteToken;
|
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
|
// 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];
|
||||||
@@ -76,9 +77,12 @@
|
|||||||
const inviteUrl = `${window.location.origin}/event/${data.event.id}/invite/${inviteToken.token}`;
|
const inviteUrl = `${window.location.origin}/event/${data.event.id}/invite/${inviteToken.token}`;
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(inviteUrl);
|
await navigator.clipboard.writeText(inviteUrl);
|
||||||
inviteLinkCopied = true;
|
showInviteLinkToast = true;
|
||||||
setTimeout(() => {
|
|
||||||
inviteLinkCopied = false;
|
// Auto-hide toast after 3 seconds
|
||||||
|
if (toastHideTimer) clearTimeout(toastHideTimer);
|
||||||
|
toastHideTimer = window.setTimeout(() => {
|
||||||
|
showInviteLinkToast = false;
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy invite link:', err);
|
console.error('Failed to copy invite link:', err);
|
||||||
@@ -375,8 +379,8 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Invite Link Section (only for invite-only events) -->
|
<!-- Invite Link Section (only for invite-only events and event creator) -->
|
||||||
{#if eventData.visibility === 'invite-only' && inviteToken}
|
{#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="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>
|
||||||
@@ -395,7 +399,7 @@
|
|||||||
on:click={copyInviteLink}
|
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-amber-300">
|
<p class="text-xs text-amber-300">
|
||||||
@@ -436,3 +440,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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;
|
$: event = data.event;
|
||||||
$: rsvps = data.rsvps;
|
$: rsvps = data.rsvps;
|
||||||
$: currentUserId = data.userId;
|
$: currentUserId = data.userId;
|
||||||
|
$: 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) {
|
||||||
@@ -58,7 +59,7 @@
|
|||||||
const token = $page.params.token || '';
|
const token = $page.params.token || '';
|
||||||
|
|
||||||
const copyInviteLink = () => {
|
const copyInviteLink = () => {
|
||||||
if (browser) {
|
if (browser && isEventCreator) {
|
||||||
const url = `${$page.url.origin}/event/${eventId}/invite/${token}`;
|
const url = `${$page.url.origin}/event/${eventId}/invite/${token}`;
|
||||||
navigator.clipboard.writeText(url).then(() => {
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
success = 'Invite link copied to clipboard!';
|
success = 'Invite link copied to clipboard!';
|
||||||
@@ -415,7 +416,10 @@
|
|||||||
<div class="max-w-2xl space-y-3">
|
<div class="max-w-2xl space-y-3">
|
||||||
<button
|
<button
|
||||||
on:click={copyInviteLink}
|
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')}
|
{t('event.copyInviteLinkButton')}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user