From b8c88d7a581bd3c9505c146fa935c3ee8b6b2497 Mon Sep 17 00:00:00 2001 From: Zias van Nes Date: Thu, 6 Nov 2025 12:38:32 +0100 Subject: [PATCH] feat:comments added comments feature. - this makes sure the user has the ability to comment on a certain find. it includes functionality to the api-sync layer so that multiple components can stay in sync with the comments state. - it also added the needed db migrations to the finds - it adds some ui components needed to create, list and view comments --- drizzle/0006_strange_firebird.sql | 11 + drizzle/meta/0006_snapshot.json | 521 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + src/lib/components/Comment.svelte | 141 +++++ src/lib/components/CommentForm.svelte | 117 ++++ src/lib/components/CommentsList.svelte | 200 +++++++ src/lib/components/FindCard.svelte | 33 +- src/lib/server/db/schema.ts | 15 + src/lib/stores/api-sync.ts | 234 ++++++++ src/routes/+page.svelte | 32 +- .../api/finds/[findId]/comments/+server.ts | 119 ++++ .../api/finds/comments/[commentId]/+server.ts | 46 ++ 12 files changed, 1442 insertions(+), 34 deletions(-) create mode 100644 drizzle/0006_strange_firebird.sql create mode 100644 drizzle/meta/0006_snapshot.json create mode 100644 src/lib/components/Comment.svelte create mode 100644 src/lib/components/CommentForm.svelte create mode 100644 src/lib/components/CommentsList.svelte create mode 100644 src/routes/api/finds/[findId]/comments/+server.ts create mode 100644 src/routes/api/finds/comments/[commentId]/+server.ts diff --git a/drizzle/0006_strange_firebird.sql b/drizzle/0006_strange_firebird.sql new file mode 100644 index 0000000..9d4dc77 --- /dev/null +++ b/drizzle/0006_strange_firebird.sql @@ -0,0 +1,11 @@ +CREATE TABLE "find_comment" ( + "id" text PRIMARY KEY NOT NULL, + "find_id" text NOT NULL, + "user_id" text NOT NULL, + "content" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "find_comment" ADD CONSTRAINT "find_comment_find_id_find_id_fk" FOREIGN KEY ("find_id") REFERENCES "public"."find"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "find_comment" ADD CONSTRAINT "find_comment_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/meta/0006_snapshot.json b/drizzle/meta/0006_snapshot.json new file mode 100644 index 0000000..bde298c --- /dev/null +++ b/drizzle/meta/0006_snapshot.json @@ -0,0 +1,521 @@ +{ + "id": "f487a86b-4f25-4d3c-a667-e383c352f3cc", + "prevId": "e0be0091-df6b-48be-9d64-8b4108d91651", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.find": { + "name": "find", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "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 + }, + "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 + }, + "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_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.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": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index fe58885..183feaf 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1760631798851, "tag": "0005_rapid_warpath", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1762428302491, + "tag": "0006_strange_firebird", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/components/Comment.svelte b/src/lib/components/Comment.svelte new file mode 100644 index 0000000..4085d32 --- /dev/null +++ b/src/lib/components/Comment.svelte @@ -0,0 +1,141 @@ + + +
+
+ +
+ +
+
+ @{comment.user.username} + {formatDate(comment.createdAt)} +
+ +
+ {comment.content} +
+
+ + {#if showDeleteButton} +
+ +
+ {/if} +
+ + diff --git a/src/lib/components/CommentForm.svelte b/src/lib/components/CommentForm.svelte new file mode 100644 index 0000000..19a2b7e --- /dev/null +++ b/src/lib/components/CommentForm.svelte @@ -0,0 +1,117 @@ + + +
+
+ + + +
+ + {#if content.length > 450} +
500}> + {content.length}/500 +
+ {/if} +
+ + diff --git a/src/lib/components/CommentsList.svelte b/src/lib/components/CommentsList.svelte new file mode 100644 index 0000000..96ffffa --- /dev/null +++ b/src/lib/components/CommentsList.svelte @@ -0,0 +1,200 @@ + + +{#snippet loadingSkeleton()} +
+ {#each Array(3) as _} +
+ +
+ + +
+
+ {/each} +
+{/snippet} + +
+ {#if collapsed} + + {/if} + + {#if isExpanded} +
+ {#if $commentsState.isLoading && !hasLoadedComments} + {@render loadingSkeleton()} + {:else if $commentsState.error} +
+ Failed to load comments. + +
+ {:else} +
+ {#each $commentsState.comments as comment (comment.id)} + + {:else} +
No comments yet. Be the first to comment!
+ {/each} +
+ {/if} + + +
+ {/if} +
+ + diff --git a/src/lib/components/FindCard.svelte b/src/lib/components/FindCard.svelte index 54793af..6e80801 100644 --- a/src/lib/components/FindCard.svelte +++ b/src/lib/components/FindCard.svelte @@ -4,7 +4,8 @@ import LikeButton from '$lib/components/LikeButton.svelte'; import VideoPlayer from '$lib/components/VideoPlayer.svelte'; import ProfilePicture from '$lib/components/ProfilePicture.svelte'; - import { MoreHorizontal, MessageCircle, Share } from '@lucide/svelte'; + import CommentsList from '$lib/components/CommentsList.svelte'; + import { Ellipsis, MessageCircle, Share } from '@lucide/svelte'; interface FindCardProps { id: string; @@ -23,6 +24,8 @@ }>; likeCount?: number; isLiked?: boolean; + commentCount?: number; + currentUserId?: string; onExplore?: (id: string) => void; } @@ -36,12 +39,20 @@ media, likeCount = 0, isLiked = false, + commentCount = 0, + currentUserId, onExplore }: FindCardProps = $props(); + let showComments = $state(false); + function handleExplore() { onExplore?.(id); } + + function toggleComments() { + showComments = !showComments; + }
@@ -73,7 +84,7 @@
@@ -120,9 +131,9 @@
-
+ + + {#if showComments} +
+ +
+ {/if}