Compare commits
13 Commits
logic-over
...
abed2792dc
| Author | SHA1 | Date | |
|---|---|---|---|
|
abed2792dc
|
|||
|
5d45ec754a
|
|||
|
1a7703b63b
|
|||
|
b7eb7ad1ad
|
|||
|
81645a453a
|
|||
|
deebeb056f
|
|||
|
0c1c9d202d
|
|||
|
ae6a96d73b
|
|||
|
577a3cab56
|
|||
|
d67b9b7911
|
|||
|
e79d574359
|
|||
|
92457f90e8
|
|||
|
|
2122511959 |
32
.dockerignore
Normal file
32
.dockerignore
Normal file
@@ -0,0 +1,32 @@
|
||||
node_modules
|
||||
.svelte-kit
|
||||
build
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.docker
|
||||
.git
|
||||
.gitignore
|
||||
.prettierrc
|
||||
.prettierignore
|
||||
.eslintrc
|
||||
.editorconfig
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
pnpm-debug.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.log
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*.swn
|
||||
coverage
|
||||
.nyc_output
|
||||
dist
|
||||
logs
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
README.md
|
||||
AGENTS.md
|
||||
76
Dockerfile
Normal file
76
Dockerfile
Normal file
@@ -0,0 +1,76 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json package-lock.json* ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Set build-time environment variables
|
||||
ARG DATABASE_URL
|
||||
ARG GOOGLE_CLIENT_ID
|
||||
ARG GOOGLE_CLIENT_SECRET
|
||||
ARG R2_ACCOUNT_ID
|
||||
ARG R2_ACCESS_KEY_ID
|
||||
ARG R2_SECRET_ACCESS_KEY
|
||||
ARG R2_BUCKET_NAME
|
||||
ARG GOOGLE_MAPS_API_KEY
|
||||
ARG VAPID_PUBLIC_KEY
|
||||
ARG VAPID_PRIVATE_KEY
|
||||
ARG VAPID_SUBJECT
|
||||
|
||||
ENV DATABASE_URL=${DATABASE_URL}
|
||||
ENV GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
||||
ENV GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
||||
ENV R2_ACCOUNT_ID=${R2_ACCOUNT_ID}
|
||||
ENV R2_ACCESS_KEY_ID=${R2_ACCESS_KEY_ID}
|
||||
ENV R2_SECRET_ACCESS_KEY=${R2_SECRET_ACCESS_KEY}
|
||||
ENV R2_BUCKET_NAME=${R2_BUCKET_NAME}
|
||||
ENV GOOGLE_MAPS_API_KEY=${GOOGLE_MAPS_API_KEY}
|
||||
ENV VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY}
|
||||
ENV VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
|
||||
ENV VAPID_SUBJECT=${VAPID_SUBJECT}
|
||||
|
||||
# Build the app
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json package-lock.json* ./
|
||||
|
||||
# Install production dependencies only
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# Copy built app from builder
|
||||
COPY --from=builder /app/build ./build
|
||||
|
||||
# Copy drizzle migrations and config
|
||||
COPY --from=builder /app/drizzle ./drizzle
|
||||
COPY --from=builder /app/drizzle.config.ts ./drizzle.config.ts
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV ORIGIN=http://localhost:3000
|
||||
|
||||
# Use entrypoint script
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
|
||||
# Start the app
|
||||
CMD ["node", "build"]
|
||||
64
docker-compose.yml
Normal file
64
docker-compose.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: serengo-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: serengo
|
||||
POSTGRES_PASSWORD: serengo_password
|
||||
POSTGRES_DB: serengo
|
||||
ports:
|
||||
- '5432:5432'
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U serengo']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
- DATABASE_URL=postgresql://serengo:serengo_password@postgres:5432/serengo
|
||||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
||||
- R2_ACCOUNT_ID=${R2_ACCOUNT_ID}
|
||||
- R2_ACCESS_KEY_ID=${R2_ACCESS_KEY_ID}
|
||||
- R2_SECRET_ACCESS_KEY=${R2_SECRET_ACCESS_KEY}
|
||||
- R2_BUCKET_NAME=${R2_BUCKET_NAME}
|
||||
- GOOGLE_MAPS_API_KEY=${GOOGLE_MAPS_API_KEY}
|
||||
- VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY}
|
||||
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
|
||||
- VAPID_SUBJECT=${VAPID_SUBJECT}
|
||||
container_name: serengo-app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '3000:3000'
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=postgresql://serengo:serengo_password@postgres:5432/serengo
|
||||
- ORIGIN=http://localhost:3000
|
||||
# Add your environment variables here or use env_file
|
||||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
||||
- R2_ACCOUNT_ID=${R2_ACCOUNT_ID}
|
||||
- R2_ACCESS_KEY_ID=${R2_ACCESS_KEY_ID}
|
||||
- R2_SECRET_ACCESS_KEY=${R2_SECRET_ACCESS_KEY}
|
||||
- R2_BUCKET_NAME=${R2_BUCKET_NAME}
|
||||
- GOOGLE_MAPS_API_KEY=${GOOGLE_MAPS_API_KEY}
|
||||
- VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY}
|
||||
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
|
||||
- VAPID_SUBJECT=${VAPID_SUBJECT}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
# Uncomment to use .env file
|
||||
# env_file:
|
||||
# - .env
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
30
docker-entrypoint.sh
Normal file
30
docker-entrypoint.sh
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "Running database migrations..."
|
||||
|
||||
# Run migrations using the drizzle migration files
|
||||
node -e "
|
||||
const { drizzle } = require('drizzle-orm/postgres-js');
|
||||
const postgres = require('postgres');
|
||||
const { migrate } = require('drizzle-orm/postgres-js/migrator');
|
||||
|
||||
async function runMigrations() {
|
||||
const migrationClient = postgres(process.env.DATABASE_URL, { max: 1 });
|
||||
const db = drizzle(migrationClient);
|
||||
|
||||
console.log('Starting migration...');
|
||||
await migrate(db, { migrationsFolder: './drizzle' });
|
||||
console.log('Migration completed successfully!');
|
||||
|
||||
await migrationClient.end();
|
||||
}
|
||||
|
||||
runMigrations().catch((err) => {
|
||||
console.error('Migration failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
"
|
||||
|
||||
echo "Starting application..."
|
||||
exec "$@"
|
||||
15
drizzle/0008_common_supreme_intelligence.sql
Normal file
15
drizzle/0008_common_supreme_intelligence.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE "location" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"latitude" text NOT NULL,
|
||||
"longitude" text NOT NULL,
|
||||
"location_name" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "find" ADD COLUMN "location_id" text NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "location" ADD CONSTRAINT "location_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "find" ADD CONSTRAINT "find_location_id_location_id_fk" FOREIGN KEY ("location_id") REFERENCES "public"."location"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "find" DROP COLUMN "latitude";--> statement-breakpoint
|
||||
ALTER TABLE "find" DROP COLUMN "longitude";--> statement-breakpoint
|
||||
ALTER TABLE "find" DROP COLUMN "location_name";
|
||||
829
drizzle/meta/0008_snapshot.json
Normal file
829
drizzle/meta/0008_snapshot.json
Normal file
@@ -0,0 +1,829 @@
|
||||
{
|
||||
"id": "5654d58b-23f8-48cb-9933-5ac32141b75e",
|
||||
"prevId": "1dbab94c-004e-4d34-b171-408bb1d36c91",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.find": {
|
||||
"name": "find",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"location_id": {
|
||||
"name": "location_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"category": {
|
||||
"name": "category",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"is_public": {
|
||||
"name": "is_public",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": 1
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"find_location_id_location_id_fk": {
|
||||
"name": "find_location_id_location_id_fk",
|
||||
"tableFrom": "find",
|
||||
"tableTo": "location",
|
||||
"columnsFrom": [
|
||||
"location_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"find_user_id_user_id_fk": {
|
||||
"name": "find_user_id_user_id_fk",
|
||||
"tableFrom": "find",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.find_comment": {
|
||||
"name": "find_comment",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"find_id": {
|
||||
"name": "find_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"find_comment_find_id_find_id_fk": {
|
||||
"name": "find_comment_find_id_find_id_fk",
|
||||
"tableFrom": "find_comment",
|
||||
"tableTo": "find",
|
||||
"columnsFrom": [
|
||||
"find_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"find_comment_user_id_user_id_fk": {
|
||||
"name": "find_comment_user_id_user_id_fk",
|
||||
"tableFrom": "find_comment",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.find_like": {
|
||||
"name": "find_like",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"find_id": {
|
||||
"name": "find_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"find_like_find_id_find_id_fk": {
|
||||
"name": "find_like_find_id_find_id_fk",
|
||||
"tableFrom": "find_like",
|
||||
"tableTo": "find",
|
||||
"columnsFrom": [
|
||||
"find_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"find_like_user_id_user_id_fk": {
|
||||
"name": "find_like_user_id_user_id_fk",
|
||||
"tableFrom": "find_like",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.find_media": {
|
||||
"name": "find_media",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"find_id": {
|
||||
"name": "find_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"thumbnail_url": {
|
||||
"name": "thumbnail_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"fallback_url": {
|
||||
"name": "fallback_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"fallback_thumbnail_url": {
|
||||
"name": "fallback_thumbnail_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"order_index": {
|
||||
"name": "order_index",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"find_media_find_id_find_id_fk": {
|
||||
"name": "find_media_find_id_find_id_fk",
|
||||
"tableFrom": "find_media",
|
||||
"tableTo": "find",
|
||||
"columnsFrom": [
|
||||
"find_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.friendship": {
|
||||
"name": "friendship",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"friend_id": {
|
||||
"name": "friend_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"friendship_user_id_user_id_fk": {
|
||||
"name": "friendship_user_id_user_id_fk",
|
||||
"tableFrom": "friendship",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"friendship_friend_id_user_id_fk": {
|
||||
"name": "friendship_friend_id_user_id_fk",
|
||||
"tableFrom": "friendship",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"friend_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.location": {
|
||||
"name": "location",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"latitude": {
|
||||
"name": "latitude",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"longitude": {
|
||||
"name": "longitude",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"location_name": {
|
||||
"name": "location_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"location_user_id_user_id_fk": {
|
||||
"name": "location_user_id_user_id_fk",
|
||||
"tableFrom": "location",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.notification": {
|
||||
"name": "notification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"message": {
|
||||
"name": "message",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"data": {
|
||||
"name": "data",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"is_read": {
|
||||
"name": "is_read",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"notification_user_id_user_id_fk": {
|
||||
"name": "notification_user_id_user_id_fk",
|
||||
"tableFrom": "notification",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.notification_preferences": {
|
||||
"name": "notification_preferences",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"friend_requests": {
|
||||
"name": "friend_requests",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": true
|
||||
},
|
||||
"friend_accepted": {
|
||||
"name": "friend_accepted",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": true
|
||||
},
|
||||
"find_liked": {
|
||||
"name": "find_liked",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": true
|
||||
},
|
||||
"find_commented": {
|
||||
"name": "find_commented",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": true
|
||||
},
|
||||
"push_enabled": {
|
||||
"name": "push_enabled",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"notification_preferences_user_id_user_id_fk": {
|
||||
"name": "notification_preferences_user_id_user_id_fk",
|
||||
"tableFrom": "notification_preferences",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.notification_subscription": {
|
||||
"name": "notification_subscription",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"endpoint": {
|
||||
"name": "endpoint",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"p256dh_key": {
|
||||
"name": "p256dh_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"auth_key": {
|
||||
"name": "auth_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"is_active": {
|
||||
"name": "is_active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"notification_subscription_user_id_user_id_fk": {
|
||||
"name": "notification_subscription_user_id_user_id_fk",
|
||||
"tableFrom": "notification_subscription",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"age": {
|
||||
"name": "age",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"google_id": {
|
||||
"name": "google_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"profile_picture_url": {
|
||||
"name": "profile_picture_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_username_unique": {
|
||||
"name": "user_username_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"username"
|
||||
]
|
||||
},
|
||||
"user_google_id_unique": {
|
||||
"name": "user_google_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"google_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,13 @@
|
||||
"when": 1762522687342,
|
||||
"tag": "0007_grey_dark_beast",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "7",
|
||||
"when": 1765885558230,
|
||||
"tag": "0008_common_supreme_intelligence",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
"@lucide/svelte": "^0.544.0",
|
||||
"@oslojs/crypto": "^1.0.1",
|
||||
"@oslojs/encoding": "^1.1.0",
|
||||
"@sveltejs/adapter-node": "^5.4.0",
|
||||
"@sveltejs/kit": "^2.22.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
@@ -32,7 +33,6 @@
|
||||
"bits-ui": "^2.11.4",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-kit": "^0.30.2",
|
||||
"drizzle-orm": "^0.40.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-storybook": "^9.1.8",
|
||||
@@ -59,6 +59,7 @@
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@sveltejs/adapter-vercel": "^5.10.2",
|
||||
"arctic": "^3.7.0",
|
||||
"drizzle-orm": "^0.40.0",
|
||||
"lucide-svelte": "^0.553.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"postgres": "^3.4.5",
|
||||
|
||||
@@ -13,9 +13,9 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
!origin ||
|
||||
origin.includes('localhost') ||
|
||||
origin.includes('127.0.0.1') ||
|
||||
origin.includes('serengo.ziasvannes.tech')
|
||||
origin.includes('serengo.zias.be')
|
||||
) {
|
||||
// Allow in development and serengo.ziasvannes.tech
|
||||
// Allow in development and serengo.zias.be
|
||||
}
|
||||
// In production, you would add: else if (origin !== 'yourdomain.com') { return new Response('Forbidden', { status: 403 }); }
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
let title = $state('');
|
||||
let description = $state('');
|
||||
let locationName = $state('');
|
||||
let category = $state('cafe');
|
||||
let isPublic = $state(true);
|
||||
let selectedFiles = $state<FileList | null>(null);
|
||||
@@ -93,7 +92,6 @@
|
||||
locationId,
|
||||
title: title.trim(),
|
||||
description: description.trim() || null,
|
||||
locationName: locationName.trim() || null,
|
||||
category,
|
||||
isPublic,
|
||||
media: uploadedMedia
|
||||
@@ -118,7 +116,6 @@
|
||||
function resetForm() {
|
||||
title = '';
|
||||
description = '';
|
||||
locationName = '';
|
||||
category = 'cafe';
|
||||
isPublic = true;
|
||||
selectedFiles = null;
|
||||
@@ -177,15 +174,6 @@
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<Label for="location-name">Location name (optional)</Label>
|
||||
<Input
|
||||
name="location-name"
|
||||
placeholder="Café Central, Brussels"
|
||||
bind:value={locationName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div class="field">
|
||||
<Label for="category">Category</Label>
|
||||
@@ -279,7 +267,6 @@
|
||||
width: 40%;
|
||||
max-width: 600px;
|
||||
min-width: 500px;
|
||||
height: calc(100vh - 100px);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
|
||||
@@ -459,7 +459,6 @@
|
||||
width: 40%;
|
||||
max-width: 600px;
|
||||
min-width: 500px;
|
||||
height: calc(100vh - 100px);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
<style>
|
||||
.find-card {
|
||||
backdrop-filter: blur(10px);
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
let latitude = $state('');
|
||||
let longitude = $state('');
|
||||
let locationName = $state('');
|
||||
let isSubmitting = $state(false);
|
||||
let useManualLocation = $state(false);
|
||||
|
||||
@@ -59,7 +60,8 @@
|
||||
},
|
||||
body: JSON.stringify({
|
||||
latitude: lat,
|
||||
longitude: lng
|
||||
longitude: lng,
|
||||
locationName: locationName.trim() || null
|
||||
})
|
||||
});
|
||||
|
||||
@@ -85,6 +87,7 @@
|
||||
}
|
||||
|
||||
function handlePlaceSelected(place: PlaceResult) {
|
||||
locationName = place.name;
|
||||
latitude = place.latitude.toString();
|
||||
longitude = place.longitude.toString();
|
||||
}
|
||||
@@ -100,6 +103,7 @@
|
||||
function resetForm() {
|
||||
latitude = '';
|
||||
longitude = '';
|
||||
locationName = '';
|
||||
useManualLocation = false;
|
||||
}
|
||||
|
||||
@@ -158,6 +162,16 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<Label for="location-name">Location Name (Optional)</Label>
|
||||
<Input
|
||||
name="location-name"
|
||||
type="text"
|
||||
placeholder="Café Central, Brussels"
|
||||
bind:value={locationName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if useManualLocation || (!latitude && !longitude)}
|
||||
<div class="field-group">
|
||||
<div class="field">
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import FindsList from '../finds/FindsList.svelte';
|
||||
import { Button } from '$lib/components/button';
|
||||
import { goto } from '$app/navigation';
|
||||
import EditFindModal from '../finds/EditFindModal.svelte';
|
||||
|
||||
interface Find {
|
||||
id: string;
|
||||
@@ -16,6 +18,8 @@
|
||||
likeCount?: number;
|
||||
isLikedByUser?: boolean;
|
||||
isFromFriend?: boolean;
|
||||
latitude?: string;
|
||||
longitude?: string;
|
||||
media?: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
@@ -47,6 +51,8 @@
|
||||
let { isOpen, location, currentUserId, onClose, onCreateFind }: Props = $props();
|
||||
|
||||
let isMobile = $state(false);
|
||||
let showEditModal = $state(false);
|
||||
let findToEdit = $state<Find | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
@@ -64,6 +70,35 @@
|
||||
function handleCreateFind() {
|
||||
onCreateFind?.();
|
||||
}
|
||||
|
||||
function handleFindExplore(findId: string) {
|
||||
goto(`/finds/${findId}`);
|
||||
}
|
||||
|
||||
function handleFindEdit(findData: any) {
|
||||
const find = location?.finds?.find((f) => f.id === findData.id);
|
||||
if (find) {
|
||||
findToEdit = find;
|
||||
showEditModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditModalClose() {
|
||||
showEditModal = false;
|
||||
findToEdit = null;
|
||||
}
|
||||
|
||||
function handleFindUpdated() {
|
||||
showEditModal = false;
|
||||
findToEdit = null;
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function handleFindDeleted() {
|
||||
showEditModal = false;
|
||||
findToEdit = null;
|
||||
window.location.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isOpen && location}
|
||||
@@ -94,12 +129,17 @@
|
||||
<FindsList
|
||||
finds={location.finds.map((find) => ({
|
||||
id: find.id,
|
||||
locationId: find.locationId,
|
||||
title: find.title,
|
||||
description: find.description,
|
||||
category: find.category,
|
||||
locationName: find.locationName,
|
||||
latitude: find.latitude,
|
||||
longitude: find.longitude,
|
||||
isPublic: find.isPublic,
|
||||
userId: find.userId,
|
||||
username: find.username,
|
||||
profilePictureUrl: find.profilePictureUrl,
|
||||
user: {
|
||||
username: find.username,
|
||||
profilePictureUrl: find.profilePictureUrl
|
||||
@@ -110,6 +150,8 @@
|
||||
}))}
|
||||
hideTitle={true}
|
||||
{currentUserId}
|
||||
onFindExplore={handleFindExplore}
|
||||
onEdit={handleFindEdit}
|
||||
/>
|
||||
{:else}
|
||||
<div class="empty-state">
|
||||
@@ -147,6 +189,32 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showEditModal && findToEdit}
|
||||
<EditFindModal
|
||||
isOpen={showEditModal}
|
||||
find={{
|
||||
id: findToEdit.id,
|
||||
title: findToEdit.title,
|
||||
description: findToEdit.description || null,
|
||||
latitude: findToEdit.latitude || location?.latitude || '0',
|
||||
longitude: findToEdit.longitude || location?.longitude || '0',
|
||||
locationName: findToEdit.locationName || null,
|
||||
category: findToEdit.category || null,
|
||||
isPublic: findToEdit.isPublic ?? 1,
|
||||
media: (findToEdit.media || []).map((m) => ({
|
||||
id: m.id,
|
||||
type: m.type,
|
||||
url: m.url,
|
||||
thumbnailUrl: m.thumbnailUrl || null,
|
||||
orderIndex: m.orderIndex ?? null
|
||||
}))
|
||||
}}
|
||||
onClose={handleEditModalClose}
|
||||
onFindUpdated={handleFindUpdated}
|
||||
onFindDeleted={handleFindDeleted}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.modal-container {
|
||||
position: fixed;
|
||||
@@ -155,7 +223,6 @@
|
||||
width: 40%;
|
||||
max-width: 600px;
|
||||
min-width: 500px;
|
||||
height: calc(100vh - 100px);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
|
||||
@@ -104,6 +104,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
max-height: calc(100vh - 200px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
|
||||
@@ -560,7 +560,6 @@
|
||||
width: 40%;
|
||||
max-width: 600px;
|
||||
min-width: 500px;
|
||||
height: calc(100vh - 100px);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: hsl(var(--background));
|
||||
background: hsl(var(--background) / 0.95);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
@@ -281,7 +281,7 @@
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
margin-top: 0.25rem;
|
||||
backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.suggestions-header {
|
||||
@@ -290,7 +290,7 @@
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--muted));
|
||||
background: hsl(var(--muted) / 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
@@ -301,7 +301,7 @@
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
background: hsl(var(--background));
|
||||
background: hsl(var(--background) / 0.95);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
@@ -314,7 +314,7 @@
|
||||
}
|
||||
|
||||
.suggestion-item:hover:not(:disabled) {
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
background: hsl(var(--muted) / 0.8);
|
||||
}
|
||||
|
||||
.suggestion-item:disabled {
|
||||
|
||||
@@ -29,6 +29,7 @@ export const location = pgTable('location', {
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
latitude: text('latitude').notNull(), // Using text for precision
|
||||
longitude: text('longitude').notNull(), // Using text for precision
|
||||
locationName: text('location_name'), // e.g., "Café Belga, Brussels"
|
||||
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull()
|
||||
});
|
||||
|
||||
@@ -43,7 +44,6 @@ export const find = pgTable('find', {
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
title: text('title').notNull(),
|
||||
description: text('description'),
|
||||
locationName: text('location_name'), // e.g., "Café Belga, Brussels"
|
||||
category: text('category'), // e.g., "cafe", "restaurant", "park", "landmark"
|
||||
isPublic: integer('is_public').default(1), // Using integer for boolean (1 = true, 0 = false)
|
||||
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull(),
|
||||
|
||||
@@ -4,5 +4,5 @@ import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET } from '$env/static/private';
|
||||
export const google = new Google(
|
||||
GOOGLE_CLIENT_ID,
|
||||
GOOGLE_CLIENT_SECRET,
|
||||
'https://serengo.ziasvannes.tech/login/google/callback'
|
||||
'https://serengo.zias.be/login/google/callback'
|
||||
);
|
||||
|
||||
@@ -4,23 +4,12 @@
|
||||
import { Header } from '$lib';
|
||||
import { page } from '$app/state';
|
||||
import { Toaster } from '$lib/components/sonner/index.js';
|
||||
import { Skeleton } from '$lib/components/skeleton';
|
||||
import LocationManager from '$lib/components/map/LocationManager.svelte';
|
||||
import NotificationManager from '$lib/components/notifications/NotificationManager.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
let { children, data } = $props();
|
||||
let isLoginRoute = $derived(page.url.pathname.startsWith('/login'));
|
||||
let showHeader = $derived(!isLoginRoute && data?.user);
|
||||
let isLoading = $state(false);
|
||||
|
||||
// Handle loading state only on client to prevent hydration mismatch
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
isLoading = !isLoginRoute && !data?.user && data !== null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -51,40 +40,6 @@
|
||||
|
||||
{#if showHeader && data.user}
|
||||
<Header user={data.user} />
|
||||
{:else if isLoading}
|
||||
<header class="header-skeleton">
|
||||
<div class="header-content">
|
||||
<Skeleton class="h-8 w-32" />
|
||||
<Skeleton class="h-10 w-10 rounded-full" />
|
||||
</div>
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
{@render children?.()}
|
||||
|
||||
<style>
|
||||
.header-skeleton {
|
||||
padding: 0 20px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-skeleton {
|
||||
padding: 0 16px;
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -321,7 +321,6 @@
|
||||
width: 40%;
|
||||
max-width: 1000px;
|
||||
min-width: 500px;
|
||||
height: calc(100vh - 100px);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
|
||||
@@ -70,7 +70,7 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
||||
locationId: find.locationId,
|
||||
title: find.title,
|
||||
description: find.description,
|
||||
locationName: find.locationName,
|
||||
locationName: location.locationName,
|
||||
category: find.category,
|
||||
isPublic: find.isPublic,
|
||||
createdAt: find.createdAt,
|
||||
@@ -98,9 +98,10 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
||||
})
|
||||
.from(find)
|
||||
.innerJoin(user, eq(find.userId, user.id))
|
||||
.innerJoin(location, eq(find.locationId, location.id))
|
||||
.leftJoin(findLike, eq(find.id, findLike.findId))
|
||||
.where(whereConditions)
|
||||
.groupBy(find.id, user.username, user.profilePictureUrl)
|
||||
.groupBy(find.id, user.username, user.profilePictureUrl, location.locationName)
|
||||
.orderBy(order === 'desc' ? desc(find.createdAt) : find.createdAt);
|
||||
|
||||
// Get media for all finds
|
||||
@@ -198,7 +199,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
const { locationId, title, description, locationName, category, isPublic, media } = data;
|
||||
const { locationId, title, description, category, isPublic, media } = data;
|
||||
|
||||
if (!title || !locationId) {
|
||||
throw error(400, 'Title and locationId are required');
|
||||
@@ -234,7 +235,6 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
userId: locals.user.id,
|
||||
title,
|
||||
description,
|
||||
locationName,
|
||||
category,
|
||||
isPublic: isPublic ? 1 : 0
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@ export const GET: RequestHandler = async ({ params, locals }) => {
|
||||
description: find.description,
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
locationName: find.locationName,
|
||||
locationName: location.locationName,
|
||||
category: find.category,
|
||||
isPublic: find.isPublic,
|
||||
createdAt: find.createdAt,
|
||||
@@ -50,6 +50,7 @@ export const GET: RequestHandler = async ({ params, locals }) => {
|
||||
find.id,
|
||||
location.latitude,
|
||||
location.longitude,
|
||||
location.locationName,
|
||||
user.username,
|
||||
user.profilePictureUrl
|
||||
)
|
||||
|
||||
@@ -66,6 +66,7 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
||||
id: location.id,
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
locationName: location.locationName,
|
||||
createdAt: location.createdAt,
|
||||
userId: location.userId,
|
||||
username: user.username,
|
||||
@@ -109,7 +110,6 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
||||
id: find.id,
|
||||
title: find.title,
|
||||
description: find.description,
|
||||
locationName: find.locationName,
|
||||
category: find.category,
|
||||
isPublic: find.isPublic,
|
||||
createdAt: find.createdAt,
|
||||
@@ -239,7 +239,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
const { latitude, longitude } = data;
|
||||
const { latitude, longitude, locationName } = data;
|
||||
|
||||
if (!latitude || !longitude) {
|
||||
throw error(400, 'Latitude and longitude are required');
|
||||
@@ -254,7 +254,8 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
id: locationId,
|
||||
userId: locals.user.id,
|
||||
latitude: latitude.toString(),
|
||||
longitude: longitude.toString()
|
||||
longitude: longitude.toString(),
|
||||
locationName: locationName || null
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
||||
@@ -123,6 +123,34 @@
|
||||
|
||||
// Get first media for OG image
|
||||
let ogImage = $derived(data.find?.media?.[0]?.url || '');
|
||||
|
||||
// Convert find to location format for map marker
|
||||
let findAsLocation = $derived(
|
||||
data.find
|
||||
? [
|
||||
{
|
||||
id: data.find.id,
|
||||
latitude: data.find.latitude,
|
||||
longitude: data.find.longitude,
|
||||
createdAt: new Date(data.find.createdAt),
|
||||
userId: data.find.userId,
|
||||
user: {
|
||||
id: data.find.userId,
|
||||
username: data.find.username
|
||||
},
|
||||
finds: [
|
||||
{
|
||||
id: data.find.id,
|
||||
title: data.find.title,
|
||||
description: data.find.description || undefined,
|
||||
isPublic: data.find.isPublic ?? 1,
|
||||
media: data.find.media || []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
: []
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -164,8 +192,10 @@
|
||||
<!-- Fullscreen map -->
|
||||
<div class="map-section">
|
||||
<Map
|
||||
autoCenter={true}
|
||||
autoCenter={false}
|
||||
center={[parseFloat(data.find?.longitude || '0'), parseFloat(data.find?.latitude || '0')]}
|
||||
zoom={15}
|
||||
locations={findAsLocation}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -457,7 +487,6 @@
|
||||
width: 40%;
|
||||
max-width: 1000px;
|
||||
min-width: 500px;
|
||||
height: calc(100vh - 100px);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
|
||||
@@ -7,7 +7,7 @@ Disallow: /_app/
|
||||
Disallow: /.svelte-kit/
|
||||
|
||||
# Sitemap location
|
||||
Sitemap: https://serengo.ziasvannes.tech/sitemap.xml
|
||||
Sitemap: https://serengo.zias.be/sitemap.xml
|
||||
|
||||
# Crawl delay for polite crawling
|
||||
Crawl-delay: 1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import adapter from '@sveltejs/adapter-vercel';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
@@ -13,7 +13,7 @@ const config = {
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter(),
|
||||
csrf: {
|
||||
trustedOrigins: ['http://localhost:3000', 'https://serengo.ziasvannes.tech']
|
||||
trustedOrigins: ['http://localhost:3000', 'https://serengo.zias.be']
|
||||
},
|
||||
alias: {
|
||||
'@/*': './src/lib/*'
|
||||
|
||||
@@ -4,7 +4,7 @@ import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
preview: { allowedHosts: ['ziasvannes.tech'] },
|
||||
preview: { allowedHosts: ['zias.be'] },
|
||||
build: {
|
||||
target: 'es2020',
|
||||
cssCodeSplit: true
|
||||
|
||||
Reference in New Issue
Block a user