mirror of
https://github.com/polaroi8d/cactoide.git
synced 2026-03-22 14:15:28 +00:00
Compare commits
29 Commits
feat/i18n-
...
feat/db-fa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb5543fb9b | ||
|
|
706e6cf712 | ||
|
|
c6decaa0d1 | ||
|
|
d193283b11 | ||
|
|
accfd540f0 | ||
|
|
9b1ef64618 | ||
|
|
c340088434 | ||
|
|
984c296725 | ||
|
|
9acfa08ea8 | ||
|
|
45cb95f6a8 | ||
|
|
8426bd5704 | ||
|
|
b9833db3bb | ||
|
|
b3572293ba | ||
|
|
c1752efe4b | ||
|
|
491d0020bd | ||
|
|
5c1182dc66 | ||
|
|
638b5ff1ca | ||
|
|
069ca11917 | ||
|
|
4675fa4623 | ||
|
|
af88d6462b | ||
|
|
ef6005e648 | ||
|
|
11875b4a1e | ||
|
|
22038f7779 | ||
|
|
de2cb07a15 | ||
|
|
ffc29b9c24 | ||
|
|
4860b9439c | ||
|
|
d10af13134 | ||
|
|
c98260efec | ||
|
|
a40b83c2b3 |
@@ -13,3 +13,5 @@ DATABASE_URL="postgres://cactoide:cactoide_password@localhost:5432/cactoide_data
|
|||||||
APP_VERSION=latest
|
APP_VERSION=latest
|
||||||
PORT=3000
|
PORT=3000
|
||||||
HOSTNAME=0.0.0.0
|
HOSTNAME=0.0.0.0
|
||||||
|
|
||||||
|
PUBLIC_LANDING_INFO=true
|
||||||
|
|||||||
2
.github/workflows/build-and-push.yml
vendored
2
.github/workflows/build-and-push.yml
vendored
@@ -60,6 +60,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
build-args: |
|
||||||
|
PUBLIC_LANDING_INFO=${{ vars.PUBLIC_LANDING_INFO }}
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
|
|||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -28,6 +28,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Build application
|
- name: Build application
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
env:
|
||||||
|
PUBLIC_LANDING_INFO: ${{ vars.PUBLIC_LANDING_INFO }}
|
||||||
|
|
||||||
- name: Test build output
|
- name: Test build output
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -3,25 +3,27 @@ WORKDIR /app
|
|||||||
COPY package*.json .
|
COPY package*.json .
|
||||||
|
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
|
ARG PUBLIC_LANDING_INFO
|
||||||
|
ENV PUBLIC_LANDING_INFO=$PUBLIC_LANDING_INFO
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
RUN npm prune --production
|
RUN npm prune --production
|
||||||
|
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /app/build build/
|
COPY --from=builder /app/build build/
|
||||||
COPY --from=builder /app/node_modules node_modules/
|
COPY --from=builder /app/node_modules node_modules/
|
||||||
COPY package.json .
|
COPY package.json .
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
ENV PORT 3000
|
ENV PORT 3000
|
||||||
ENV HOSTNAME "0.0.0.0"
|
ENV HOSTNAME "0.0.0.0"
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||||
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/healthz || exit 1
|
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/healthz || exit 1
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
CMD [ "node", "build" ]
|
CMD [ "node", "build" ]
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -55,7 +55,18 @@ Your app will be available at `http://localhost:5173`. You can use the Makefile
|
|||||||
|
|
||||||
Use the `database/seed.sql` if you want to populate your database with dummy data.
|
Use the `database/seed.sql` if you want to populate your database with dummy data.
|
||||||
|
|
||||||
### i18n
|
### Options
|
||||||
|
|
||||||
|
#### 1. Landing page option
|
||||||
|
|
||||||
|
Supports a conditional landing page display based on the `PUBLIC_LANDING_INFO` environment variable. If you don't want to show your users the cactoide landing page, just use the `PUBLIC_LANDING_INFO=false` variable. This will automatically remove the landing home page and redirect users to the `/discover` page.
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
|
||||||
|
- Creating a minimal discovery-focused experience
|
||||||
|
- Customizing the user journey based on deployment environment
|
||||||
|
|
||||||
|
#### 2. i18n
|
||||||
|
|
||||||
There is no proper i18n implemented, we have an `/i18n` folder with specific languages. To use an existing translation, just rename the language code JSON file to `messages.json` and you are ready to go. If you would like to add a new translation (which is really appreciated), just create a new `<language_code>.json` file and add the translations from the `messages.json`.
|
There is no proper i18n implemented, we have an `/i18n` folder with specific languages. To use an existing translation, just rename the language code JSON file to `messages.json` and you are ready to go. If you would like to add a new translation (which is really appreciated), just create a new `<language_code>.json` file and add the translations from the `messages.json`.
|
||||||
|
|
||||||
@@ -71,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 isn’t 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
92
package-lock.json
generated
@@ -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"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "cactoide",
|
"name": "cactoide",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.3",
|
"version": "0.1.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -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
44
src/hooks.server.ts
Normal 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);
|
||||||
|
};
|
||||||
@@ -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';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { t } from '$lib/i18n/i18n.js';
|
import { t } from '$lib/i18n/i18n.js';
|
||||||
|
import { PUBLIC_LANDING_INFO } from '$env/static/public';
|
||||||
|
|
||||||
// Check if current page is active
|
// Check if current page is active
|
||||||
const isActive = (path: string): boolean => {
|
const isActive = (path: string): boolean => {
|
||||||
@@ -24,12 +25,14 @@
|
|||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<div class="md:flex md:items-center md:space-x-8">
|
<div class="md:flex md:items-center md:space-x-8">
|
||||||
<button
|
{#if PUBLIC_LANDING_INFO !== 'false'}
|
||||||
on:click={() => goto('/')}
|
<button
|
||||||
class={isActive('/') ? 'text-violet-400' : 'cursor-pointer'}
|
on:click={() => goto('/')}
|
||||||
>
|
class={isActive('/') ? 'text-violet-400' : 'cursor-pointer'}
|
||||||
{t('navigation.home')}
|
>
|
||||||
</button>
|
{t('navigation.home')}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
on:click={() => goto('/discover')}
|
on:click={() => goto('/discover')}
|
||||||
|
|||||||
104
src/lib/database/healthCheck.ts
Normal file
104
src/lib/database/healthCheck.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import Navbar from '$lib/components/Navbar.svelte';
|
import Navbar from '$lib/components/Navbar.svelte';
|
||||||
import { t } from '$lib/i18n/i18n.js';
|
import { t } from '$lib/i18n/i18n.js';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data, children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<main class="relative z-10">
|
<main class="relative z-10">
|
||||||
<slot />
|
{@render children?.()}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
|
|||||||
11
src/routes/+page.server.ts
Normal file
11
src/routes/+page.server.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { PUBLIC_LANDING_INFO } from '$env/static/public';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async () => {
|
||||||
|
if (PUBLIC_LANDING_INFO === 'false') {
|
||||||
|
throw redirect(302, '/discover');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user