feat:rating
This commit is contained in:
15
drizzle/0009_lazy_monster_badoon.sql
Normal file
15
drizzle/0009_lazy_monster_badoon.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE "find_rating" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"find_id" text NOT NULL,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"rating" integer 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" ADD COLUMN "rating" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "find" ADD COLUMN "rating_count" integer DEFAULT 0;--> statement-breakpoint
|
||||||
|
ALTER TABLE "location" ADD COLUMN "average_rating" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "location" ADD COLUMN "rating_count" integer DEFAULT 0;--> statement-breakpoint
|
||||||
|
ALTER TABLE "find_rating" ADD CONSTRAINT "find_rating_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_rating" ADD CONSTRAINT "find_rating_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
933
drizzle/meta/0009_snapshot.json
Normal file
933
drizzle/meta/0009_snapshot.json
Normal file
@@ -0,0 +1,933 @@
|
|||||||
|
{
|
||||||
|
"id": "30fb4a72-dd57-46c3-99e4-9e01f36acce0",
|
||||||
|
"prevId": "5654d58b-23f8-48cb-9933-5ac32141b75e",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"name": "rating",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"rating_count": {
|
||||||
|
"name": "rating_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"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.find_rating": {
|
||||||
|
"name": "find_rating",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"name": "rating",
|
||||||
|
"type": "integer",
|
||||||
|
"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_rating_find_id_find_id_fk": {
|
||||||
|
"name": "find_rating_find_id_find_id_fk",
|
||||||
|
"tableFrom": "find_rating",
|
||||||
|
"tableTo": "find",
|
||||||
|
"columnsFrom": [
|
||||||
|
"find_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"find_rating_user_id_user_id_fk": {
|
||||||
|
"name": "find_rating_user_id_user_id_fk",
|
||||||
|
"tableFrom": "find_rating",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_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
|
||||||
|
},
|
||||||
|
"average_rating": {
|
||||||
|
"name": "average_rating",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"rating_count": {
|
||||||
|
"name": "rating_count",
|
||||||
|
"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": {
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,6 +64,13 @@
|
|||||||
"when": 1765885558230,
|
"when": 1765885558230,
|
||||||
"tag": "0008_common_supreme_intelligence",
|
"tag": "0008_common_supreme_intelligence",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1765894394394,
|
||||||
|
"tag": "0009_lazy_monster_badoon",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
import VideoPlayer from '../media/VideoPlayer.svelte';
|
import VideoPlayer from '../media/VideoPlayer.svelte';
|
||||||
import ProfilePicture from '../profile/ProfilePicture.svelte';
|
import ProfilePicture from '../profile/ProfilePicture.svelte';
|
||||||
import CommentsList from './CommentsList.svelte';
|
import CommentsList from './CommentsList.svelte';
|
||||||
|
import Rating from './Rating.svelte';
|
||||||
import { Ellipsis, MessageCircle, Share, Edit, Trash2 } from '@lucide/svelte';
|
import { Ellipsis, MessageCircle, Share, Edit, Trash2 } from '@lucide/svelte';
|
||||||
import { apiSync } from '$lib/stores/api-sync';
|
import { apiSync } from '$lib/stores/api-sync';
|
||||||
|
|
||||||
@@ -39,6 +40,9 @@
|
|||||||
likeCount?: number;
|
likeCount?: number;
|
||||||
isLiked?: boolean;
|
isLiked?: boolean;
|
||||||
commentCount?: number;
|
commentCount?: number;
|
||||||
|
rating?: number | null;
|
||||||
|
ratingCount?: number;
|
||||||
|
userRating?: number | null;
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
onExplore?: (id: string) => void;
|
onExplore?: (id: string) => void;
|
||||||
onDeleted?: () => void;
|
onDeleted?: () => void;
|
||||||
@@ -61,6 +65,9 @@
|
|||||||
likeCount = 0,
|
likeCount = 0,
|
||||||
isLiked = false,
|
isLiked = false,
|
||||||
commentCount = 0,
|
commentCount = 0,
|
||||||
|
rating,
|
||||||
|
ratingCount = 0,
|
||||||
|
userRating,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
onExplore,
|
onExplore,
|
||||||
onDeleted,
|
onDeleted,
|
||||||
@@ -237,6 +244,18 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Rating Section -->
|
||||||
|
{#if currentUserId}
|
||||||
|
<div class="rating-section">
|
||||||
|
<Rating
|
||||||
|
findId={id}
|
||||||
|
initialRating={rating ? rating / 100 : 0}
|
||||||
|
initialCount={ratingCount}
|
||||||
|
{userRating}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Comments Section -->
|
<!-- Comments Section -->
|
||||||
{#if showComments}
|
{#if showComments}
|
||||||
<div class="comments-section">
|
<div class="comments-section">
|
||||||
@@ -427,6 +446,13 @@
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Rating Section */
|
||||||
|
.rating-section {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-top: 1px solid hsl(var(--border));
|
||||||
|
background: hsl(var(--muted) / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
/* Comments Section */
|
/* Comments Section */
|
||||||
.comments-section {
|
.comments-section {
|
||||||
padding: 0 1rem 1rem 1rem;
|
padding: 0 1rem 1rem 1rem;
|
||||||
|
|||||||
156
src/lib/components/finds/Rating.svelte
Normal file
156
src/lib/components/finds/Rating.svelte
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Star } from 'lucide-svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
findId: string;
|
||||||
|
initialRating?: number;
|
||||||
|
initialCount?: number;
|
||||||
|
userRating?: number | null;
|
||||||
|
onRatingChange?: (rating: number) => void;
|
||||||
|
readonly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
findId,
|
||||||
|
initialRating = 0,
|
||||||
|
initialCount = 0,
|
||||||
|
userRating = null,
|
||||||
|
onRatingChange,
|
||||||
|
readonly = false
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let rating = $state(initialRating);
|
||||||
|
let ratingCount = $state(initialCount);
|
||||||
|
let currentUserRating = $state(userRating);
|
||||||
|
let hoverRating = $state(0);
|
||||||
|
let isSubmitting = $state(false);
|
||||||
|
|
||||||
|
async function handleRate(stars: number) {
|
||||||
|
if (readonly || isSubmitting) return;
|
||||||
|
|
||||||
|
isSubmitting = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/finds/${findId}/rate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ rating: stars })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to rate find');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
rating = data.rating / 100;
|
||||||
|
ratingCount = data.ratingCount;
|
||||||
|
currentUserRating = stars;
|
||||||
|
|
||||||
|
if (onRatingChange) {
|
||||||
|
onRatingChange(stars);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rating find:', error);
|
||||||
|
} finally {
|
||||||
|
isSubmitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayRating() {
|
||||||
|
if (readonly) {
|
||||||
|
return rating;
|
||||||
|
}
|
||||||
|
return hoverRating || rating;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="rating-container">
|
||||||
|
<div class="stars">
|
||||||
|
{#each [1, 2, 3, 4, 5] as star}
|
||||||
|
<button
|
||||||
|
class="star-button"
|
||||||
|
class:filled={star <= getDisplayRating()}
|
||||||
|
class:user-rated={!readonly && currentUserRating && star <= currentUserRating}
|
||||||
|
class:readonly
|
||||||
|
disabled={readonly || isSubmitting}
|
||||||
|
onclick={() => handleRate(star)}
|
||||||
|
onmouseenter={() => !readonly && (hoverRating = star)}
|
||||||
|
onmouseleave={() => !readonly && (hoverRating = 0)}
|
||||||
|
aria-label={`Rate ${star} star${star > 1 ? 's' : ''}`}
|
||||||
|
>
|
||||||
|
<Star
|
||||||
|
class="star-icon"
|
||||||
|
fill={star <= getDisplayRating() ? 'currentColor' : 'none'}
|
||||||
|
size={readonly ? 16 : 24}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if ratingCount > 0}
|
||||||
|
<span class="rating-info">
|
||||||
|
{(rating || 0).toFixed(1)} ({ratingCount}
|
||||||
|
{ratingCount === 1 ? 'rating' : 'ratings'})
|
||||||
|
</span>
|
||||||
|
{:else if !readonly}
|
||||||
|
<span class="rating-info">Be the first to rate</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.rating-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #94a3b8;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-button:not(.readonly):hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-button.filled {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-button.user-rated {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-button.readonly {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-info {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.star-icon) {
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -9,3 +9,4 @@ export { default as FindPreview } from './FindPreview.svelte';
|
|||||||
export { default as FindsFilter } from './FindsFilter.svelte';
|
export { default as FindsFilter } from './FindsFilter.svelte';
|
||||||
export { default as FindsList } from './FindsList.svelte';
|
export { default as FindsList } from './FindsList.svelte';
|
||||||
export { default as LikeButton } from './LikeButton.svelte';
|
export { default as LikeButton } from './LikeButton.svelte';
|
||||||
|
export { default as Rating } from './Rating.svelte';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { formatDistance } from '$lib/utils/distance';
|
import { formatDistance } from '$lib/utils/distance';
|
||||||
|
import { Star } from 'lucide-svelte';
|
||||||
|
|
||||||
interface Location {
|
interface Location {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -10,6 +11,8 @@
|
|||||||
username: string;
|
username: string;
|
||||||
profilePictureUrl?: string | null;
|
profilePictureUrl?: string | null;
|
||||||
findCount: number;
|
findCount: number;
|
||||||
|
averageRating?: number | null;
|
||||||
|
ratingCount?: number;
|
||||||
distance?: number;
|
distance?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +87,13 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>{location.findCount} {location.findCount === 1 ? 'find' : 'finds'}</span>
|
<span>{location.findCount} {location.findCount === 1 ? 'find' : 'finds'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if location.averageRating && (location.ratingCount || 0) > 0}
|
||||||
|
<div class="meta-item rating">
|
||||||
|
<Star size={16} fill="currentColor" class="star-icon" />
|
||||||
|
<span>{(location.averageRating / 100).toFixed(1)} ({location.ratingCount})</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -188,6 +198,14 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.meta-item.rating {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.star-icon) {
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.explore-button {
|
.explore-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { pgTable, integer, text, timestamp, boolean, jsonb } from 'drizzle-orm/pg-core';
|
import { pgTable, integer, text, timestamp, boolean, jsonb, real } from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
export const user = pgTable('user', {
|
export const user = pgTable('user', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
@@ -30,6 +30,8 @@ export const location = pgTable('location', {
|
|||||||
latitude: text('latitude').notNull(), // Using text for precision
|
latitude: text('latitude').notNull(), // Using text for precision
|
||||||
longitude: text('longitude').notNull(), // Using text for precision
|
longitude: text('longitude').notNull(), // Using text for precision
|
||||||
locationName: text('location_name'), // e.g., "Café Belga, Brussels"
|
locationName: text('location_name'), // e.g., "Café Belga, Brussels"
|
||||||
|
averageRating: integer('average_rating'), // Average rating (1-5 scale, stored as integer * 100 for precision)
|
||||||
|
ratingCount: integer('rating_count').default(0), // Total number of finds with ratings at this location
|
||||||
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull()
|
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,6 +47,8 @@ export const find = pgTable('find', {
|
|||||||
title: text('title').notNull(),
|
title: text('title').notNull(),
|
||||||
description: text('description'),
|
description: text('description'),
|
||||||
category: text('category'), // e.g., "cafe", "restaurant", "park", "landmark"
|
category: text('category'), // e.g., "cafe", "restaurant", "park", "landmark"
|
||||||
|
rating: integer('rating'), // Average rating for this find (1-5 stars, stored as integer * 100)
|
||||||
|
ratingCount: integer('rating_count').default(0), // Number of ratings for this find
|
||||||
isPublic: integer('is_public').default(1), // Using integer for boolean (1 = true, 0 = false)
|
isPublic: integer('is_public').default(1), // Using integer for boolean (1 = true, 0 = false)
|
||||||
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull(),
|
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull(),
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull()
|
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull()
|
||||||
@@ -75,6 +79,19 @@ export const findLike = pgTable('find_like', {
|
|||||||
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull()
|
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const findRating = pgTable('find_rating', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
findId: text('find_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => find.id, { onDelete: 'cascade' }),
|
||||||
|
userId: text('user_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: 'cascade' }),
|
||||||
|
rating: integer('rating').notNull(), // 1-5 stars (stored as 100-500 for precision)
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull()
|
||||||
|
});
|
||||||
|
|
||||||
export const friendship = pgTable('friendship', {
|
export const friendship = pgTable('friendship', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
@@ -146,6 +163,7 @@ export type Location = typeof location.$inferSelect;
|
|||||||
export type Find = typeof find.$inferSelect;
|
export type Find = typeof find.$inferSelect;
|
||||||
export type FindMedia = typeof findMedia.$inferSelect;
|
export type FindMedia = typeof findMedia.$inferSelect;
|
||||||
export type FindLike = typeof findLike.$inferSelect;
|
export type FindLike = typeof findLike.$inferSelect;
|
||||||
|
export type FindRating = typeof findRating.$inferSelect;
|
||||||
export type FindComment = typeof findComment.$inferSelect;
|
export type FindComment = typeof findComment.$inferSelect;
|
||||||
export type Friendship = typeof friendship.$inferSelect;
|
export type Friendship = typeof friendship.$inferSelect;
|
||||||
export type Notification = typeof notification.$inferSelect;
|
export type Notification = typeof notification.$inferSelect;
|
||||||
@@ -156,6 +174,7 @@ export type LocationInsert = typeof location.$inferInsert;
|
|||||||
export type FindInsert = typeof find.$inferInsert;
|
export type FindInsert = typeof find.$inferInsert;
|
||||||
export type FindMediaInsert = typeof findMedia.$inferInsert;
|
export type FindMediaInsert = typeof findMedia.$inferInsert;
|
||||||
export type FindLikeInsert = typeof findLike.$inferInsert;
|
export type FindLikeInsert = typeof findLike.$inferInsert;
|
||||||
|
export type FindRatingInsert = typeof findRating.$inferInsert;
|
||||||
export type FindCommentInsert = typeof findComment.$inferInsert;
|
export type FindCommentInsert = typeof findComment.$inferInsert;
|
||||||
export type FriendshipInsert = typeof friendship.$inferInsert;
|
export type FriendshipInsert = typeof friendship.$inferInsert;
|
||||||
export type NotificationInsert = typeof notification.$inferInsert;
|
export type NotificationInsert = typeof notification.$inferInsert;
|
||||||
|
|||||||
162
src/routes/api/finds/[findId]/rate/+server.ts
Normal file
162
src/routes/api/finds/[findId]/rate/+server.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { findRating, find, location } from '$lib/server/db/schema';
|
||||||
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
import { encodeBase64url } from '@oslojs/encoding';
|
||||||
|
|
||||||
|
function generateRatingId(): string {
|
||||||
|
const bytes = crypto.getRandomValues(new Uint8Array(15));
|
||||||
|
return encodeBase64url(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/finds/[findId]/rate - Rate a find
|
||||||
|
export async function POST({
|
||||||
|
params,
|
||||||
|
request,
|
||||||
|
locals
|
||||||
|
}: {
|
||||||
|
params: { findId: string };
|
||||||
|
request: Request;
|
||||||
|
locals: { user: { id: string } };
|
||||||
|
}) {
|
||||||
|
if (!locals.user) {
|
||||||
|
throw error(401, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const findId = params.findId;
|
||||||
|
|
||||||
|
if (!findId) {
|
||||||
|
throw error(400, 'Find ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const ratingValue = body.rating;
|
||||||
|
|
||||||
|
// Validate rating (1-5 stars)
|
||||||
|
if (!ratingValue || ratingValue < 1 || ratingValue > 5) {
|
||||||
|
throw error(400, 'Rating must be between 1 and 5');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to integer representation (100-500)
|
||||||
|
const ratingInt = Math.round(ratingValue * 100);
|
||||||
|
|
||||||
|
// Check if find exists
|
||||||
|
const findRecord = await db.select().from(find).where(eq(find.id, findId)).limit(1);
|
||||||
|
|
||||||
|
if (findRecord.length === 0) {
|
||||||
|
throw error(404, 'Find not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has already rated this find
|
||||||
|
const existingRating = await db
|
||||||
|
.select()
|
||||||
|
.from(findRating)
|
||||||
|
.where(and(eq(findRating.findId, findId), eq(findRating.userId, locals.user.id)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existingRating.length > 0) {
|
||||||
|
// Update existing rating
|
||||||
|
await db
|
||||||
|
.update(findRating)
|
||||||
|
.set({
|
||||||
|
rating: ratingInt,
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
.where(eq(findRating.id, existingRating[0].id));
|
||||||
|
} else {
|
||||||
|
// Create new rating
|
||||||
|
const ratingId = generateRatingId();
|
||||||
|
await db.insert(findRating).values({
|
||||||
|
id: ratingId,
|
||||||
|
findId,
|
||||||
|
userId: locals.user.id,
|
||||||
|
rating: ratingInt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate average rating for the find
|
||||||
|
const ratings = await db.select().from(findRating).where(eq(findRating.findId, findId));
|
||||||
|
|
||||||
|
const avgRating = ratings.reduce((sum, r) => sum + (r.rating || 0), 0) / ratings.length;
|
||||||
|
const roundedAvgRating = Math.round(avgRating);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(find)
|
||||||
|
.set({
|
||||||
|
rating: roundedAvgRating,
|
||||||
|
ratingCount: ratings.length,
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
.where(eq(find.id, findId));
|
||||||
|
|
||||||
|
// Recalculate location average rating
|
||||||
|
const locationId = findRecord[0].locationId;
|
||||||
|
const locationFinds = await db.select().from(find).where(eq(find.locationId, locationId));
|
||||||
|
|
||||||
|
// Only include finds that have ratings
|
||||||
|
const ratedFinds = locationFinds.filter((f) => f.rating !== null && (f.ratingCount || 0) > 0);
|
||||||
|
|
||||||
|
if (ratedFinds.length > 0) {
|
||||||
|
const locationAvgRating =
|
||||||
|
ratedFinds.reduce((sum, f) => sum + (f.rating || 0), 0) / ratedFinds.length;
|
||||||
|
const roundedLocationAvgRating = Math.round(locationAvgRating);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(location)
|
||||||
|
.set({
|
||||||
|
averageRating: roundedLocationAvgRating,
|
||||||
|
ratingCount: ratedFinds.length
|
||||||
|
})
|
||||||
|
.where(eq(location.id, locationId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({
|
||||||
|
success: true,
|
||||||
|
rating: roundedAvgRating,
|
||||||
|
ratingCount: ratings.length
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error rating find:', err);
|
||||||
|
throw error(500, 'Failed to rate find');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/finds/[findId]/rate - Get user's rating for a find
|
||||||
|
export async function GET({
|
||||||
|
params,
|
||||||
|
locals
|
||||||
|
}: {
|
||||||
|
params: { findId: string };
|
||||||
|
locals: { user: { id: string } };
|
||||||
|
}) {
|
||||||
|
if (!locals.user) {
|
||||||
|
throw error(401, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const findId = params.findId;
|
||||||
|
|
||||||
|
if (!findId) {
|
||||||
|
throw error(400, 'Find ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userRating = await db
|
||||||
|
.select()
|
||||||
|
.from(findRating)
|
||||||
|
.where(and(eq(findRating.findId, findId), eq(findRating.userId, locals.user.id)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (userRating.length === 0) {
|
||||||
|
return json({ rating: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert from integer representation to float (100-500 -> 1-5)
|
||||||
|
const ratingValue = (userRating[0].rating || 0) / 100;
|
||||||
|
|
||||||
|
return json({ rating: ratingValue });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error getting user rating:', err);
|
||||||
|
throw error(500, 'Failed to get rating');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user