Compare commits

...

15 Commits

Author SHA1 Message Date
Levente Orban
eb5543fb9b feat: fail fast if db not connecting 2025-10-23 09:47:27 +02:00
Levente Orban
706e6cf712 chore(deps-dev): bump vite from 7.1.10 to 7.1.11 in the npm_and_yarn group across 1 directory 2025-10-21 10:12:34 +02:00
dependabot[bot]
c6decaa0d1 chore(deps-dev): bump vite in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 7.1.10 to 7.1.11
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.11/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.11
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-21 00:22:12 +00:00
Levente Orban
d193283b11 fix: creating an event and showing the wrong date 2025-10-20 21:22:30 +02:00
Levente Orban
accfd540f0 fix: creating an event and showing the wrong date 2025-10-20 11:43:34 +02:00
Levente Orban
9b1ef64618 fix: creating an event and showing the wrong date 2025-10-20 11:25:35 +02:00
Levente Orban
c340088434 fix: userId not generated in the first visit 2025-10-20 10:32:33 +02:00
Levente Orban
984c296725 fix: userId not generated in the first visit 2025-10-20 10:18:20 +02:00
Levente Orban
9acfa08ea8 chore(deps-dev): bump vite from 7.1.2 to 7.1.10 in the npm_and_yarn group across 1 directory 2025-10-20 08:49:24 +02:00
Levente Orban
45cb95f6a8 chore(deps): bump devalue from 5.1.1 to 5.4.1 in the npm_and_yarn group across 1 directory 2025-10-20 08:49:13 +02:00
dependabot[bot]
8426bd5704 chore(deps-dev): bump vite in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 7.1.2 to 7.1.10
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.10/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.10
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-19 07:00:04 +00:00
dependabot[bot]
b9833db3bb chore(deps): bump devalue in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [devalue](https://github.com/sveltejs/devalue).


Updates `devalue` from 5.1.1 to 5.4.1
- [Release notes](https://github.com/sveltejs/devalue/releases)
- [Changelog](https://github.com/sveltejs/devalue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/devalue/compare/v5.1.1...v5.4.1)

---
updated-dependencies:
- dependency-name: devalue
  dependency-version: 5.4.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-19 07:00:03 +00:00
Levente Orban
b3572293ba chore: add support me section to README.md 2025-10-15 15:54:42 +02:00
Levente Orban
c1752efe4b chore: add support me section to README.md 2025-10-15 10:44:41 +02:00
Levente Orban
491d0020bd fix: remove dev omit 2025-10-10 21:58:23 +02:00
11 changed files with 279 additions and 47 deletions

View File

@@ -82,6 +82,19 @@ make i18n
make i18n FILE=src/lib/i18n/it.json make i18n FILE=src/lib/i18n/it.json
``` ```
### Support
Cactoide is an open-source project licensed under `AGPL-3.0`. Its growth and development are possible thanks to the amazing support of the community. This project is the result of many late nights, weekends, and after-hours work.
It isnt backed by a big company. Development depends on the support and generosity of people like you. With your help, I can focus more on making Cactoide even better and building tools that make coding more enjoyable.
You can support in a few ways:
- Send a one-time donation via [paypal.me/zenoazurben](paypal.me/zenoazurben)
- Reach me directly: leventeorb[@]gmail.com
If you enjoy using Cactoide, or if your business depends on it, please consider sponsoring its development. Your support keeps the project alive, improves it for everyone, and helps create educational content like blog posts and videos for the whole Cactoide community.
### License ### License
This project is licensed under the `AGPL-3.0 License` - see the [LICENSE](./LICENSE) file for details. This project is licensed under the `AGPL-3.0 License` - see the [LICENSE](./LICENSE) file for details.

92
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "event-cactus", "name": "event-cactus",
"version": "0.0.1", "version": "0.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "event-cactus", "name": "event-cactus",
"version": "0.0.1", "version": "0.1.1",
"dependencies": { "dependencies": {
"@sveltejs/adapter-node": "^5.3.1", "@sveltejs/adapter-node": "^5.3.1",
"drizzle-orm": "^0.44.5", "drizzle-orm": "^0.44.5",
@@ -35,7 +35,7 @@
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"typescript-eslint": "^8.20.0", "typescript-eslint": "^8.20.0",
"vite": "^7.0.4" "vite": "^7.1.11"
} }
}, },
"node_modules/@drizzle-team/brocli": { "node_modules/@drizzle-team/brocli": {
@@ -1949,6 +1949,66 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.4.5",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.0.4",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.4.5",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.0.4",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@tybys/wasm-util": "^0.10.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.0",
"dev": true,
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.12", "version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz",
@@ -2602,9 +2662,9 @@
} }
}, },
"node_modules/devalue": { "node_modules/devalue": {
"version": "5.1.1", "version": "5.4.1",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.4.1.tgz",
"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", "integrity": "sha512-YtoaOfsqjbZQKGIMRYDWKjUmSB4VJ/RElB+bXZawQAQYAo4xu08GKTMVlsZDTF6R2MbAgjcAQRPI5eIyRAT2OQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/drizzle-kit": { "node_modules/drizzle-kit": {
@@ -4737,13 +4797,13 @@
} }
}, },
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.14", "version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fdir": "^6.4.4", "fdir": "^6.5.0",
"picomatch": "^4.0.2" "picomatch": "^4.0.3"
}, },
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
@@ -4864,17 +4924,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.1.2", "version": "7.1.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
"integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==", "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.6", "fdir": "^6.5.0",
"picomatch": "^4.0.3", "picomatch": "^4.0.3",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rollup": "^4.43.0", "rollup": "^4.43.0",
"tinyglobby": "^0.2.14" "tinyglobby": "^0.2.15"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"

View File

@@ -35,7 +35,7 @@
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"typescript-eslint": "^8.20.0", "typescript-eslint": "^8.20.0",
"vite": "^7.0.4" "vite": "^7.1.11"
}, },
"dependencies": { "dependencies": {
"@sveltejs/adapter-node": "^5.3.1", "@sveltejs/adapter-node": "^5.3.1",

44
src/hooks.server.ts Normal file
View File

@@ -0,0 +1,44 @@
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { generateUserId } from '$lib/generateUserId.js';
import { ensureDatabaseConnection } from '$lib/database/healthCheck';
// Global flag to track if database health check has been performed
let dbHealthCheckPerformed = false;
export const handle: Handle = async ({ event, resolve }) => {
// Perform database health check only once during application startup
if (!dbHealthCheckPerformed) {
try {
await ensureDatabaseConnection({
maxRetries: 3,
baseDelay: 1000,
maxDelay: 10000,
timeout: 5000
});
dbHealthCheckPerformed = true;
} catch (error) {
console.error('Database health check failed:', error);
// The ensureDatabaseConnection function will exit the process
// if the database is unavailable, so this catch is just for safety
process.exit(1);
}
}
const cactoideUserId = event.cookies.get('cactoideUserId');
const userId = generateUserId();
const DAYS = 400; // practical upper bound in many browsers for cookies
const MAX_AGE = 60 * 60 * 24 * DAYS;
const PATH = '/';
if (!cactoideUserId) {
console.debug(`There is no cactoideUserId cookie, generating new one...`);
event.cookies.set('cactoideUserId', userId, { path: PATH, maxAge: MAX_AGE });
} else {
console.debug(`cactoideUserId: ${cactoideUserId}`);
console.debug(`cactoideUserId cookie found, using existing one...`);
}
return resolve(event);
};

View File

@@ -15,7 +15,10 @@ export interface CalendarEvent {
* Formats a date and time string for iCal format (UTC) * Formats a date and time string for iCal format (UTC)
*/ */
export const formatDateForICal = (date: string, time: string): string => { export const formatDateForICal = (date: string, time: string): string => {
const eventDate = new Date(`${date}T${time}`); // Parse date and time as local timezone to avoid timezone issues
const [year, month, day] = date.split('-').map(Number);
const [hours, minutes, seconds] = time.split(':').map(Number);
const eventDate = new Date(year, month - 1, day, hours, minutes, seconds || 0);
return eventDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; return eventDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
}; };

View File

@@ -0,0 +1,104 @@
import postgres from 'postgres';
import { env } from '$env/dynamic/private';
interface HealthCheckOptions {
maxRetries?: number;
baseDelay?: number;
maxDelay?: number;
timeout?: number;
}
interface HealthCheckResult {
success: boolean;
error?: string;
attempts: number;
duration?: number;
}
/**
* Performs a database health check with retry logic and exponential backoff
*/
export async function checkDatabaseHealth(
options: HealthCheckOptions = {}
): Promise<HealthCheckResult> {
const {
maxRetries = 3,
baseDelay = 1000, // 1 second
maxDelay = 10000, // 10 seconds
timeout = 5000 // 5 seconds
} = options;
if (!env.DATABASE_URL) {
console.error('DATABASE_URL environment variable is not set');
return {
success: false,
error: 'DATABASE_URL environment variable is not set',
attempts: 0
};
}
let lastError: Error | null = null;
console.log(`Starting database health check (max retries: ${maxRetries})`);
for (let attempt = 1; attempt <= maxRetries; attempt++) {
console.log(`Attempt ${attempt}/${maxRetries} - Testing database connection`);
try {
// Create a new connection for the health check
const client = postgres(env.DATABASE_URL, {
max: 1,
idle_timeout: 20,
connect_timeout: timeout / 1000, // Convert to seconds
onnotice: () => {} // Suppress notices
});
// Test the connection with a simple query
await client`SELECT 1 as health_check`;
await client.end();
console.log(`Database connection successful on attempt ${attempt}.`);
return {
success: true,
attempts: attempt
};
} catch (error) {
lastError = error as Error;
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(`Connection failed on attempt ${attempt}: ${errorMessage}`);
// Don't wait after the last attempt
if (attempt < maxRetries) {
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
console.log(`Waiting ${delay}ms before retry...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
const finalError = lastError?.message || 'Unknown database connection error';
console.error(`All ${maxRetries} attempts failed. Final error: ${finalError}`);
return {
success: false,
error: finalError,
attempts: maxRetries
};
}
/**
* Runs database health check and exits the process if it fails
*/
export async function ensureDatabaseConnection(options?: HealthCheckOptions): Promise<void> {
const result = await checkDatabaseHealth(options);
if (!result.success) {
console.error('Database connection failed after all retry attempts');
console.error(`Error: ${result.error}`);
console.error(`Attempts made: ${result.attempts}`);
console.error('Exiting application...');
process.exit(1);
}
}

View File

@@ -1,11 +1,14 @@
import type { Event } from './types'; import type { Event } from './types';
export const formatDate = (dateString: string): string => { export const formatDate = (dateString: string): string => {
const date = new Date(dateString); // Parse the date string as local date to avoid timezone issues
const year = date.getFullYear(); // Split the date string and create a Date object in local timezone
const month = String(date.getMonth() + 1).padStart(2, '0'); const [year, month, day] = dateString.split('-').map(Number);
const day = String(date.getDate()).padStart(2, '0'); const date = new Date(year, month - 1, day); // month is 0-indexed in Date constructor
return `${year}/${month}/${day}`; const formattedYear = date.getFullYear();
const formattedMonth = String(date.getMonth() + 1).padStart(2, '0');
const formattedDay = String(date.getDate()).padStart(2, '0');
return `${formattedYear}/${formattedMonth}/${formattedDay}`;
}; };
export const formatTime = (timeString: string): string => { export const formatTime = (timeString: string): string => {
@@ -17,7 +20,10 @@ export const formatTime = (timeString: string): string => {
export const isEventInTimeRange = (event: Event, timeFilter: string): boolean => { export const isEventInTimeRange = (event: Event, timeFilter: string): boolean => {
if (timeFilter === 'any') return true; if (timeFilter === 'any') return true;
const eventDate = new Date(`${event.date}T${event.time}`); // Parse date and time as local timezone to avoid timezone issues
const [year, month, day] = event.date.split('-').map(Number);
const [hours, minutes, seconds] = event.time.split(':').map(Number);
const eventDate = new Date(year, month - 1, day, hours, minutes, seconds || 0);
const now = new Date(); const now = new Date();
// Handle temporal status filters // Handle temporal status filters

View File

@@ -1,20 +1,5 @@
import { generateUserId } from '$lib/generateUserId.js';
export function load({ cookies }) { export function load({ cookies }) {
const cactoideUserId = cookies.get('cactoideUserId'); const cactoideUserId = cookies.get('cactoideUserId');
const userId = generateUserId();
const DAYS = 400; // practical upper bound in many browsers for cookies
const MAX_AGE = 60 * 60 * 24 * DAYS;
const PATH = '/';
if (!cactoideUserId) {
console.debug(`There is no cactoideUserId cookie, generating new one...`);
cookies.set('cactoideUserId', userId, { path: PATH, maxAge: MAX_AGE });
} else {
console.debug(`cactoideUserId: ${cactoideUserId}`);
console.debug(`cactoideUserId cookie found, using existing one...`);
}
return { return {
cactoideUserId cactoideUserId

View File

@@ -56,7 +56,13 @@ export const actions: Actions = {
}); });
} }
if (new Date(date) < new Date()) { // Check if date is in the past using local timezone
const [year, month, day] = date.split('-').map(Number);
const eventDate = new Date(year, month - 1, day);
const today = new Date();
today.setHours(0, 0, 0, 0);
if (eventDate < today) {
return fail(400, { return fail(400, {
error: 'Date cannot be in the past.', error: 'Date cannot be in the past.',
values: { values: {
@@ -105,7 +111,7 @@ export const actions: Actions = {
type: type, type: type,
attendeeLimit: type === 'limited' ? parseInt(attendeeLimit) : null, attendeeLimit: type === 'limited' ? parseInt(attendeeLimit) : null,
visibility: visibility, visibility: visibility,
userId: userId userId: userId!
}) })
.catch((error) => { .catch((error) => {
console.error('Unexpected error', error); console.error('Unexpected error', error);

View File

@@ -1,11 +1,14 @@
<script lang="ts"> <script lang="ts">
import type { Event, EventType } from '$lib/types'; import type { Event, EventType } from '$lib/types';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import type { PageData } from '../$types';
import { formatTime, formatDate, isEventInTimeRange } from '$lib/dateHelpers'; import { formatTime, formatDate, isEventInTimeRange } from '$lib/dateHelpers';
import { t } from '$lib/i18n/i18n.js'; import { t } from '$lib/i18n/i18n.js';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
type DiscoverPageData = {
events: Event[];
};
let publicEvents: Event[] = []; let publicEvents: Event[] = [];
let error = ''; let error = '';
let searchQuery = ''; let searchQuery = '';
@@ -16,7 +19,7 @@
let showFilters = false; let showFilters = false;
let fuse: Fuse<Event>; let fuse: Fuse<Event>;
export let data: PageData; export let data: DiscoverPageData;
// Use the server-side data // Use the server-side data
$: publicEvents = data?.events || []; $: publicEvents = data?.events || [];
@@ -67,8 +70,15 @@
// Sort events by date and time // Sort events by date and time
events = events.sort((a, b) => { events = events.sort((a, b) => {
const dateA = new Date(`${a.date}T${a.time}`); // Parse dates as local timezone to avoid timezone issues
const dateB = new Date(`${b.date}T${b.time}`); const parseEventDateTime = (event: Event) => {
const [year, month, day] = event.date.split('-').map(Number);
const [hours, minutes, seconds] = event.time.split(':').map(Number);
return new Date(year, month - 1, day, hours, minutes, seconds || 0);
};
const dateA = parseEventDateTime(a);
const dateB = parseEventDateTime(b);
if (selectedSortOrder === 'asc') { if (selectedSortOrder === 'asc') {
return dateA.getTime() - dateB.getTime(); return dateA.getTime() - dateB.getTime();

View File

@@ -86,8 +86,9 @@ export const actions: Actions = {
}); });
} }
// Check if date is in the past (but allow editing past events for corrections) // Check if date is in the past using local timezone (but allow editing past events for corrections)
const eventDate = new Date(date); const [year, month, day] = date.split('-').map(Number);
const eventDate = new Date(year, month - 1, day);
const today = new Date(); const today = new Date();
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);