feat: comments
fix: use select instead of dropdown for filter on /market
This commit is contained in:
parent
800b5d1a09
commit
bd05b269fe
22 changed files with 2715 additions and 97 deletions
44
website/drizzle/0003_careless_groot.sql
Normal file
44
website/drizzle/0003_careless_groot.sql
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS "comment" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"user_id" integer NOT NULL,
|
||||||
|
"coin_id" integer NOT NULL,
|
||||||
|
"content" text NOT NULL,
|
||||||
|
"likes_count" integer DEFAULT 0 NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"is_deleted" boolean DEFAULT false NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "comment_like" (
|
||||||
|
"user_id" integer NOT NULL,
|
||||||
|
"comment_id" integer NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "comment_like_user_id_comment_id_pk" PRIMARY KEY("user_id","comment_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "comment" ADD CONSTRAINT "comment_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "comment" ADD CONSTRAINT "comment_coin_id_coin_id_fk" FOREIGN KEY ("coin_id") REFERENCES "public"."coin"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "comment_like" ADD CONSTRAINT "comment_like_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "comment_like" ADD CONSTRAINT "comment_like_comment_id_comment_id_fk" FOREIGN KEY ("comment_id") REFERENCES "public"."comment"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "comment_user_id_idx" ON "comment" USING btree ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "comment_coin_id_idx" ON "comment" USING btree ("coin_id");
|
||||||
1
website/drizzle/0004_cloudy_viper.sql
Normal file
1
website/drizzle/0004_cloudy_viper.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE "comment" ALTER COLUMN "content" SET DATA TYPE varchar(500);
|
||||||
893
website/drizzle/meta/0003_snapshot.json
Normal file
893
website/drizzle/meta/0003_snapshot.json
Normal file
|
|
@ -0,0 +1,893 @@
|
||||||
|
{
|
||||||
|
"id": "db075473-f497-4d2e-bf44-58e35b5c3035",
|
||||||
|
"prevId": "446a7a86-4e91-4461-829a-1059470307c4",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.coin": {
|
||||||
|
"name": "coin",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"name": "symbol",
|
||||||
|
"type": "varchar(10)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"name": "icon",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"creator_id": {
|
||||||
|
"name": "creator_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"initial_supply": {
|
||||||
|
"name": "initial_supply",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"circulating_supply": {
|
||||||
|
"name": "circulating_supply",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"current_price": {
|
||||||
|
"name": "current_price",
|
||||||
|
"type": "numeric(20, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"market_cap": {
|
||||||
|
"name": "market_cap",
|
||||||
|
"type": "numeric(30, 2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"volume_24h": {
|
||||||
|
"name": "volume_24h",
|
||||||
|
"type": "numeric(30, 2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'0.00'"
|
||||||
|
},
|
||||||
|
"change_24h": {
|
||||||
|
"name": "change_24h",
|
||||||
|
"type": "numeric(10, 4)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'0.0000'"
|
||||||
|
},
|
||||||
|
"pool_coin_amount": {
|
||||||
|
"name": "pool_coin_amount",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'0.00000000'"
|
||||||
|
},
|
||||||
|
"pool_base_currency_amount": {
|
||||||
|
"name": "pool_base_currency_amount",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'0.00000000'"
|
||||||
|
},
|
||||||
|
"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()"
|
||||||
|
},
|
||||||
|
"is_listed": {
|
||||||
|
"name": "is_listed",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"coin_creator_id_user_id_fk": {
|
||||||
|
"name": "coin_creator_id_user_id_fk",
|
||||||
|
"tableFrom": "coin",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"creator_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"coin_symbol_unique": {
|
||||||
|
"name": "coin_symbol_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"symbol"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"public.comment": {
|
||||||
|
"name": "comment",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"coin_id": {
|
||||||
|
"name": "coin_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"likes_count": {
|
||||||
|
"name": "likes_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"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()"
|
||||||
|
},
|
||||||
|
"is_deleted": {
|
||||||
|
"name": "is_deleted",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"comment_user_id_idx": {
|
||||||
|
"name": "comment_user_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"comment_coin_id_idx": {
|
||||||
|
"name": "comment_coin_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "coin_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"comment_user_id_user_id_fk": {
|
||||||
|
"name": "comment_user_id_user_id_fk",
|
||||||
|
"tableFrom": "comment",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"comment_coin_id_coin_id_fk": {
|
||||||
|
"name": "comment_coin_id_coin_id_fk",
|
||||||
|
"tableFrom": "comment",
|
||||||
|
"tableTo": "coin",
|
||||||
|
"columnsFrom": [
|
||||||
|
"coin_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.comment_like": {
|
||||||
|
"name": "comment_like",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"comment_id": {
|
||||||
|
"name": "comment_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"comment_like_user_id_user_id_fk": {
|
||||||
|
"name": "comment_like_user_id_user_id_fk",
|
||||||
|
"tableFrom": "comment_like",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"comment_like_comment_id_comment_id_fk": {
|
||||||
|
"name": "comment_like_comment_id_comment_id_fk",
|
||||||
|
"tableFrom": "comment_like",
|
||||||
|
"tableTo": "comment",
|
||||||
|
"columnsFrom": [
|
||||||
|
"comment_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"comment_like_user_id_comment_id_pk": {
|
||||||
|
"name": "comment_like_user_id_comment_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"user_id",
|
||||||
|
"comment_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.price_history": {
|
||||||
|
"name": "price_history",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"coin_id": {
|
||||||
|
"name": "coin_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"name": "price",
|
||||||
|
"type": "numeric(20, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"price_history_coin_id_coin_id_fk": {
|
||||||
|
"name": "price_history_coin_id_coin_id_fk",
|
||||||
|
"tableFrom": "price_history",
|
||||||
|
"tableTo": "coin",
|
||||||
|
"columnsFrom": [
|
||||||
|
"coin_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"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()"
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"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": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"public.transaction": {
|
||||||
|
"name": "transaction",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"coin_id": {
|
||||||
|
"name": "coin_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "transaction_type",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"quantity": {
|
||||||
|
"name": "quantity",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_per_coin": {
|
||||||
|
"name": "price_per_coin",
|
||||||
|
"type": "numeric(20, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"total_base_currency_amount": {
|
||||||
|
"name": "total_base_currency_amount",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"transaction_user_id_user_id_fk": {
|
||||||
|
"name": "transaction_user_id_user_id_fk",
|
||||||
|
"tableFrom": "transaction",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"transaction_coin_id_coin_id_fk": {
|
||||||
|
"name": "transaction_coin_id_coin_id_fk",
|
||||||
|
"tableFrom": "transaction",
|
||||||
|
"tableTo": "coin",
|
||||||
|
"columnsFrom": [
|
||||||
|
"coin_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"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()"
|
||||||
|
},
|
||||||
|
"is_admin": {
|
||||||
|
"name": "is_admin",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"is_banned": {
|
||||||
|
"name": "is_banned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"ban_reason": {
|
||||||
|
"name": "ban_reason",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"base_currency_balance": {
|
||||||
|
"name": "base_currency_balance",
|
||||||
|
"type": "numeric(20, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'10000.00000000'"
|
||||||
|
},
|
||||||
|
"bio": {
|
||||||
|
"name": "bio",
|
||||||
|
"type": "varchar(160)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”'"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"user_username_unique": {
|
||||||
|
"name": "user_username_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"public.user_portfolio": {
|
||||||
|
"name": "user_portfolio",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"coin_id": {
|
||||||
|
"name": "coin_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"quantity": {
|
||||||
|
"name": "quantity",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_portfolio_user_id_user_id_fk": {
|
||||||
|
"name": "user_portfolio_user_id_user_id_fk",
|
||||||
|
"tableFrom": "user_portfolio",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"user_portfolio_coin_id_coin_id_fk": {
|
||||||
|
"name": "user_portfolio_coin_id_coin_id_fk",
|
||||||
|
"tableFrom": "user_portfolio",
|
||||||
|
"tableTo": "coin",
|
||||||
|
"columnsFrom": [
|
||||||
|
"coin_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_portfolio_user_id_coin_id_pk": {
|
||||||
|
"name": "user_portfolio_user_id_coin_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"user_id",
|
||||||
|
"coin_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"public.transaction_type": {
|
||||||
|
"name": "transaction_type",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"BUY",
|
||||||
|
"SELL"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
893
website/drizzle/meta/0004_snapshot.json
Normal file
893
website/drizzle/meta/0004_snapshot.json
Normal file
|
|
@ -0,0 +1,893 @@
|
||||||
|
{
|
||||||
|
"id": "dc6eb2ec-7c88-48f3-96b3-7485786fffd2",
|
||||||
|
"prevId": "db075473-f497-4d2e-bf44-58e35b5c3035",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.coin": {
|
||||||
|
"name": "coin",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"name": "symbol",
|
||||||
|
"type": "varchar(10)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"name": "icon",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"creator_id": {
|
||||||
|
"name": "creator_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"initial_supply": {
|
||||||
|
"name": "initial_supply",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"circulating_supply": {
|
||||||
|
"name": "circulating_supply",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"current_price": {
|
||||||
|
"name": "current_price",
|
||||||
|
"type": "numeric(20, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"market_cap": {
|
||||||
|
"name": "market_cap",
|
||||||
|
"type": "numeric(30, 2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"volume_24h": {
|
||||||
|
"name": "volume_24h",
|
||||||
|
"type": "numeric(30, 2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'0.00'"
|
||||||
|
},
|
||||||
|
"change_24h": {
|
||||||
|
"name": "change_24h",
|
||||||
|
"type": "numeric(10, 4)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'0.0000'"
|
||||||
|
},
|
||||||
|
"pool_coin_amount": {
|
||||||
|
"name": "pool_coin_amount",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'0.00000000'"
|
||||||
|
},
|
||||||
|
"pool_base_currency_amount": {
|
||||||
|
"name": "pool_base_currency_amount",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'0.00000000'"
|
||||||
|
},
|
||||||
|
"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()"
|
||||||
|
},
|
||||||
|
"is_listed": {
|
||||||
|
"name": "is_listed",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"coin_creator_id_user_id_fk": {
|
||||||
|
"name": "coin_creator_id_user_id_fk",
|
||||||
|
"tableFrom": "coin",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"creator_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"coin_symbol_unique": {
|
||||||
|
"name": "coin_symbol_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"symbol"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"public.comment": {
|
||||||
|
"name": "comment",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"coin_id": {
|
||||||
|
"name": "coin_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"likes_count": {
|
||||||
|
"name": "likes_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"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()"
|
||||||
|
},
|
||||||
|
"is_deleted": {
|
||||||
|
"name": "is_deleted",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"comment_user_id_idx": {
|
||||||
|
"name": "comment_user_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"comment_coin_id_idx": {
|
||||||
|
"name": "comment_coin_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "coin_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"comment_user_id_user_id_fk": {
|
||||||
|
"name": "comment_user_id_user_id_fk",
|
||||||
|
"tableFrom": "comment",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"comment_coin_id_coin_id_fk": {
|
||||||
|
"name": "comment_coin_id_coin_id_fk",
|
||||||
|
"tableFrom": "comment",
|
||||||
|
"tableTo": "coin",
|
||||||
|
"columnsFrom": [
|
||||||
|
"coin_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.comment_like": {
|
||||||
|
"name": "comment_like",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"comment_id": {
|
||||||
|
"name": "comment_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"comment_like_user_id_user_id_fk": {
|
||||||
|
"name": "comment_like_user_id_user_id_fk",
|
||||||
|
"tableFrom": "comment_like",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"comment_like_comment_id_comment_id_fk": {
|
||||||
|
"name": "comment_like_comment_id_comment_id_fk",
|
||||||
|
"tableFrom": "comment_like",
|
||||||
|
"tableTo": "comment",
|
||||||
|
"columnsFrom": [
|
||||||
|
"comment_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"comment_like_user_id_comment_id_pk": {
|
||||||
|
"name": "comment_like_user_id_comment_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"user_id",
|
||||||
|
"comment_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.price_history": {
|
||||||
|
"name": "price_history",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"coin_id": {
|
||||||
|
"name": "coin_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"name": "price",
|
||||||
|
"type": "numeric(20, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"price_history_coin_id_coin_id_fk": {
|
||||||
|
"name": "price_history_coin_id_coin_id_fk",
|
||||||
|
"tableFrom": "price_history",
|
||||||
|
"tableTo": "coin",
|
||||||
|
"columnsFrom": [
|
||||||
|
"coin_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"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()"
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"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": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"public.transaction": {
|
||||||
|
"name": "transaction",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"coin_id": {
|
||||||
|
"name": "coin_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "transaction_type",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"quantity": {
|
||||||
|
"name": "quantity",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_per_coin": {
|
||||||
|
"name": "price_per_coin",
|
||||||
|
"type": "numeric(20, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"total_base_currency_amount": {
|
||||||
|
"name": "total_base_currency_amount",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"transaction_user_id_user_id_fk": {
|
||||||
|
"name": "transaction_user_id_user_id_fk",
|
||||||
|
"tableFrom": "transaction",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"transaction_coin_id_coin_id_fk": {
|
||||||
|
"name": "transaction_coin_id_coin_id_fk",
|
||||||
|
"tableFrom": "transaction",
|
||||||
|
"tableTo": "coin",
|
||||||
|
"columnsFrom": [
|
||||||
|
"coin_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"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()"
|
||||||
|
},
|
||||||
|
"is_admin": {
|
||||||
|
"name": "is_admin",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"is_banned": {
|
||||||
|
"name": "is_banned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"ban_reason": {
|
||||||
|
"name": "ban_reason",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"base_currency_balance": {
|
||||||
|
"name": "base_currency_balance",
|
||||||
|
"type": "numeric(20, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'10000.00000000'"
|
||||||
|
},
|
||||||
|
"bio": {
|
||||||
|
"name": "bio",
|
||||||
|
"type": "varchar(160)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”'"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"user_username_unique": {
|
||||||
|
"name": "user_username_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"public.user_portfolio": {
|
||||||
|
"name": "user_portfolio",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"coin_id": {
|
||||||
|
"name": "coin_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"quantity": {
|
||||||
|
"name": "quantity",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_portfolio_user_id_user_id_fk": {
|
||||||
|
"name": "user_portfolio_user_id_user_id_fk",
|
||||||
|
"tableFrom": "user_portfolio",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"user_portfolio_coin_id_coin_id_fk": {
|
||||||
|
"name": "user_portfolio_coin_id_coin_id_fk",
|
||||||
|
"tableFrom": "user_portfolio",
|
||||||
|
"tableTo": "coin",
|
||||||
|
"columnsFrom": [
|
||||||
|
"coin_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_portfolio_user_id_coin_id_pk": {
|
||||||
|
"name": "user_portfolio_user_id_coin_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"user_id",
|
||||||
|
"coin_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"public.transaction_type": {
|
||||||
|
"name": "transaction_type",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"BUY",
|
||||||
|
"SELL"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,20 @@
|
||||||
"when": 1748023983269,
|
"when": 1748023983269,
|
||||||
"tag": "0002_parched_silver_sable",
|
"tag": "0002_parched_silver_sable",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1748098802380,
|
||||||
|
"tag": "0003_careless_groot",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1748103716114,
|
||||||
|
"tag": "0004_cloudy_viper",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
261
website/src/lib/components/self/CommentSection.svelte
Normal file
261
website/src/lib/components/self/CommentSection.svelte
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Textarea } from '$lib/components/ui/textarea';
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import * as Avatar from '$lib/components/ui/avatar';
|
||||||
|
import * as HoverCard from '$lib/components/ui/hover-card';
|
||||||
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
|
import { MessageCircle, Send, Loader2, Heart, CalendarDays } from 'lucide-svelte';
|
||||||
|
import { USER_DATA } from '$lib/stores/user-data';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { formatTimeAgo, getPublicUrl } from '$lib/utils';
|
||||||
|
import SignInConfirmDialog from '$lib/components/self/SignInConfirmDialog.svelte';
|
||||||
|
|
||||||
|
const { coinSymbol } = $props<{ coinSymbol: string }>();
|
||||||
|
import type { Comment } from '$lib/types/comment';
|
||||||
|
let comments = $state<Comment[]>([]);
|
||||||
|
let newComment = $state('');
|
||||||
|
let isSubmitting = $state(false);
|
||||||
|
let isLoading = $state(true);
|
||||||
|
let shouldSignIn = $state(false);
|
||||||
|
|
||||||
|
async function loadComments() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/coin/${coinSymbol}/comments`);
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
comments = result.comments;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load comments:', e);
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitComment() {
|
||||||
|
if (!$USER_DATA) {
|
||||||
|
shouldSignIn = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newComment.trim()) return;
|
||||||
|
|
||||||
|
isSubmitting = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/coin/${coinSymbol}/comments`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ content: newComment.trim() })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
comments = [result.comment, ...comments];
|
||||||
|
newComment = '';
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
toast.error(error.message || 'Failed to post comment');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('Failed to post comment');
|
||||||
|
} finally {
|
||||||
|
isSubmitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
submitComment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function toggleLike(commentId: number) {
|
||||||
|
if (!$USER_DATA) {
|
||||||
|
goto('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commentIndex = comments.findIndex((c) => c.id === commentId);
|
||||||
|
if (commentIndex === -1) return;
|
||||||
|
|
||||||
|
const comment = comments[commentIndex];
|
||||||
|
const wasLiked = comment.isLikedByUser;
|
||||||
|
|
||||||
|
comments[commentIndex] = {
|
||||||
|
...comment,
|
||||||
|
isLikedByUser: !wasLiked,
|
||||||
|
likesCount: wasLiked ? comment.likesCount - 1 : comment.likesCount + 1
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch(`/api/coin/${coinSymbol}/comments/${commentId}/like`, {
|
||||||
|
method: wasLiked ? 'DELETE' : 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}).catch(() => {
|
||||||
|
comments[commentIndex] = comment;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLikeClick(commentId: number) {
|
||||||
|
if (!$USER_DATA) {
|
||||||
|
shouldSignIn = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toggleLike(commentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
loadComments();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SignInConfirmDialog bind:open={shouldSignIn} />
|
||||||
|
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title class="flex items-center gap-2">
|
||||||
|
<MessageCircle class="h-5 w-5" />
|
||||||
|
Comments ({comments.length})
|
||||||
|
</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="space-y-4">
|
||||||
|
<!-- Comment Form -->
|
||||||
|
{#if $USER_DATA}
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="relative">
|
||||||
|
<Textarea
|
||||||
|
bind:value={newComment}
|
||||||
|
placeholder="Share your thoughts about this coin..."
|
||||||
|
class="min-h-[80px] w-full break-words pb-8 pr-20"
|
||||||
|
style="word-break: break-word; overflow-wrap: break-word;"
|
||||||
|
maxlength={500}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
/>
|
||||||
|
<kbd
|
||||||
|
class="bg-muted pointer-events-none absolute bottom-2 right-2 hidden h-5 select-none items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-70 sm:flex"
|
||||||
|
>
|
||||||
|
<span class="text-xs">⌘</span>Enter
|
||||||
|
</kbd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground text-xs">
|
||||||
|
{newComment.length}/500 characters
|
||||||
|
</span>
|
||||||
|
<Button onclick={submitComment} disabled={!newComment.trim() || isSubmitting} size="sm">
|
||||||
|
{#if isSubmitting}
|
||||||
|
<Loader2 class="h-4 w-4 animate-spin" />
|
||||||
|
Posting...
|
||||||
|
{:else}
|
||||||
|
<Send class="h-4 w-4" />
|
||||||
|
Post
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-muted-foreground mb-3 text-sm">Sign in to join the discussion</p>
|
||||||
|
<Button onclick={() => goto('/')} size="sm">Sign In</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Comments List -->
|
||||||
|
{#if isLoading}
|
||||||
|
<div class="py-8 text-center">
|
||||||
|
<Loader2 class="mx-auto h-6 w-6 animate-spin" />
|
||||||
|
<p class="text-muted-foreground mt-2 text-sm">Loading comments...</p>
|
||||||
|
</div>
|
||||||
|
{:else if comments.length === 0}
|
||||||
|
<div class="py-8 text-center">
|
||||||
|
<MessageCircle class="text-muted-foreground mx-auto h-12 w-12" />
|
||||||
|
<p class="text-muted-foreground mt-2">
|
||||||
|
No comments yet. Be the first to share your thoughts!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#each comments as comment (comment.id)}
|
||||||
|
<div class="border-border border-b pb-4 last:border-b-0">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<button onclick={() => goto(`/user/${comment.userId}`)} class="cursor-pointer">
|
||||||
|
<Avatar.Root class="h-8 w-8">
|
||||||
|
<Avatar.Image src={getPublicUrl(comment.userImage)} alt={comment.userName} />
|
||||||
|
<Avatar.Fallback>{comment.userName?.charAt(0) || '?'}</Avatar.Fallback>
|
||||||
|
</Avatar.Root>
|
||||||
|
</button>
|
||||||
|
<div class="flex-1 space-y-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<HoverCard.Root>
|
||||||
|
<HoverCard.Trigger
|
||||||
|
class="cursor-pointer font-medium underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-8"
|
||||||
|
onclick={() => goto(`/user/${comment.userId}`)}
|
||||||
|
>
|
||||||
|
{comment.userName}
|
||||||
|
</HoverCard.Trigger>
|
||||||
|
<HoverCard.Content class="w-80" side="top" sideOffset={3}>
|
||||||
|
<div class="flex justify-between space-x-4">
|
||||||
|
<Avatar.Root class="h-14 w-14">
|
||||||
|
<Avatar.Image
|
||||||
|
src={getPublicUrl(comment.userImage)}
|
||||||
|
alt={comment.userName}
|
||||||
|
/>
|
||||||
|
<Avatar.Fallback>{comment.userName?.charAt(0) || '?'}</Avatar.Fallback>
|
||||||
|
</Avatar.Root>
|
||||||
|
<div class="flex-1 space-y-1">
|
||||||
|
<h4 class="text-sm font-semibold">{comment.userName}</h4>
|
||||||
|
<p class="text-muted-foreground text-sm">@{comment.userUsername}</p>
|
||||||
|
{#if comment.userBio}
|
||||||
|
<p class="text-sm">{comment.userBio}</p>
|
||||||
|
{/if}
|
||||||
|
<div class="flex items-center pt-2">
|
||||||
|
<CalendarDays class="mr-2 h-4 w-4 opacity-70" />
|
||||||
|
<span class="text-muted-foreground text-xs">
|
||||||
|
Joined {new Date(comment.userCreatedAt).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HoverCard.Content>
|
||||||
|
</HoverCard.Root>
|
||||||
|
<button onclick={() => goto(`/user/${comment.userId}`)} class="cursor-pointer">
|
||||||
|
<Badge variant="outline" class="text-xs">
|
||||||
|
@{comment.userUsername}
|
||||||
|
</Badge>
|
||||||
|
</button>
|
||||||
|
<span class="text-muted-foreground text-xs">
|
||||||
|
{formatTimeAgo(comment.createdAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="whitespace-pre-wrap break-words text-sm leading-relaxed"
|
||||||
|
style="word-break: break-word; overflow-wrap: break-word;"
|
||||||
|
>
|
||||||
|
{comment.content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => handleLikeClick(comment.id)}
|
||||||
|
class="flex h-auto items-center gap-1 p-2 {comment.isLikedByUser
|
||||||
|
? 'text-red-500 hover:text-red-600'
|
||||||
|
: 'text-muted-foreground hover:text-foreground'}"
|
||||||
|
>
|
||||||
|
<Heart class="h-4 w-4 {comment.isLikedByUser ? 'fill-current' : ''}" />
|
||||||
|
{#if comment.likesCount > 0}
|
||||||
|
<span class="text-xs">{comment.likesCount}</span>
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
34
website/src/lib/components/ui/select/index.ts
Normal file
34
website/src/lib/components/ui/select/index.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
import Group from "./select-group.svelte";
|
||||||
|
import Label from "./select-label.svelte";
|
||||||
|
import Item from "./select-item.svelte";
|
||||||
|
import Content from "./select-content.svelte";
|
||||||
|
import Trigger from "./select-trigger.svelte";
|
||||||
|
import Separator from "./select-separator.svelte";
|
||||||
|
import ScrollDownButton from "./select-scroll-down-button.svelte";
|
||||||
|
import ScrollUpButton from "./select-scroll-up-button.svelte";
|
||||||
|
|
||||||
|
const Root = SelectPrimitive.Root;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Group,
|
||||||
|
Label,
|
||||||
|
Item,
|
||||||
|
Content,
|
||||||
|
Trigger,
|
||||||
|
Separator,
|
||||||
|
ScrollDownButton,
|
||||||
|
ScrollUpButton,
|
||||||
|
//
|
||||||
|
Root as Select,
|
||||||
|
Group as SelectGroup,
|
||||||
|
Label as SelectLabel,
|
||||||
|
Item as SelectItem,
|
||||||
|
Content as SelectContent,
|
||||||
|
Trigger as SelectTrigger,
|
||||||
|
Separator as SelectSeparator,
|
||||||
|
ScrollDownButton as SelectScrollDownButton,
|
||||||
|
ScrollUpButton as SelectScrollUpButton,
|
||||||
|
};
|
||||||
40
website/src/lib/components/ui/select/select-content.svelte
Normal file
40
website/src/lib/components/ui/select/select-content.svelte
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
|
||||||
|
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
sideOffset = 4,
|
||||||
|
portalProps,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||||
|
portalProps?: SelectPrimitive.PortalProps;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Portal {...portalProps}>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
{sideOffset}
|
||||||
|
data-slot="select-content"
|
||||||
|
class={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--bits-select-content-available-height) origin-(--bits-select-content-transform-origin) relative z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
class={cn(
|
||||||
|
"h-(--bits-select-anchor-height) min-w-(--bits-select-anchor-width) w-full scroll-my-1 p-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
7
website/src/lib/components/ui/select/select-group.svelte
Normal file
7
website/src/lib/components/ui/select/select-group.svelte
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Group data-slot="select-group" {...restProps} />
|
||||||
38
website/src/lib/components/ui/select/select-item.svelte
Normal file
38
website/src/lib/components/ui/select/select-item.svelte
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import CheckIcon from "@lucide/svelte/icons/check";
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
children: childrenProp,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
bind:ref
|
||||||
|
{value}
|
||||||
|
data-slot="select-item"
|
||||||
|
class={cn(
|
||||||
|
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ selected, highlighted })}
|
||||||
|
<span class="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
|
{#if selected}
|
||||||
|
<CheckIcon class="size-4" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{#if childrenProp}
|
||||||
|
{@render childrenProp({ selected, highlighted })}
|
||||||
|
{:else}
|
||||||
|
{label || value}
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</SelectPrimitive.Item>
|
||||||
20
website/src/lib/components/ui/select/select-label.svelte
Normal file
20
website/src/lib/components/ui/select/select-label.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="select-label"
|
||||||
|
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon class="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon class="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
18
website/src/lib/components/ui/select/select-separator.svelte
Normal file
18
website/src/lib/components/ui/select/select-separator.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Separator as SeparatorPrimitive } from "bits-ui";
|
||||||
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: SeparatorPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Separator
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-separator"
|
||||||
|
class={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
29
website/src/lib/components/ui/select/select-trigger.svelte
Normal file
29
website/src/lib/components/ui/select/select-trigger.svelte
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
size = "default",
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||||
|
size?: "sm" | "default";
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
|
class={cn(
|
||||||
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 shadow-xs flex w-fit items-center justify-between gap-2 whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<ChevronDownIcon class="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { pgTable, text, timestamp, boolean, decimal, serial, varchar, integer, primaryKey, pgEnum } from "drizzle-orm/pg-core";
|
import { pgTable, text, timestamp, boolean, decimal, serial, varchar, integer, primaryKey, pgEnum, index } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const transactionTypeEnum = pgEnum('transaction_type', ['BUY', 'SELL']);
|
export const transactionTypeEnum = pgEnum('transaction_type', ['BUY', 'SELL']);
|
||||||
|
|
||||||
|
|
@ -106,3 +106,29 @@ export const priceHistory = pgTable("price_history", {
|
||||||
price: decimal("price", { precision: 20, scale: 8 }).notNull(),
|
price: decimal("price", { precision: 20, scale: 8 }).notNull(),
|
||||||
timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow(),
|
timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const comment = pgTable("comment", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }),
|
||||||
|
content: varchar("content", { length: 500 }).notNull(),
|
||||||
|
likesCount: integer("likes_count").notNull().default(0),
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
isDeleted: boolean("is_deleted").default(false).notNull(),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
userIdIdx: index("comment_user_id_idx").on(table.userId),
|
||||||
|
coinIdIdx: index("comment_coin_id_idx").on(table.coinId),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const commentLike = pgTable("comment_like", {
|
||||||
|
userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
commentId: integer("comment_id").notNull().references(() => comment.id, { onDelete: "cascade" }),
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
pk: primaryKey({ columns: [table.userId, table.commentId] }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
|
||||||
21
website/src/lib/types/comment.ts
Normal file
21
website/src/lib/types/comment.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
export interface Comment {
|
||||||
|
id: number;
|
||||||
|
content: string;
|
||||||
|
likesCount: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
userId: number;
|
||||||
|
userName: string;
|
||||||
|
userUsername: string;
|
||||||
|
userImage: string | null;
|
||||||
|
isLikedByUser: boolean;
|
||||||
|
|
||||||
|
userBio: string | null;
|
||||||
|
userCreatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommentLike {
|
||||||
|
userId: number;
|
||||||
|
commentId: number;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
@ -125,4 +125,18 @@ export function formatRelativeTime(timestamp: string | Date): string {
|
||||||
return remainingMonths === 0 ? `${years}y` : `${years}y ${remainingMonths}m`;
|
return remainingMonths === 0 ? `${years}y` : `${years}y ${remainingMonths}m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatTimeAgo(date: string) {
|
||||||
|
const now = new Date();
|
||||||
|
const commentDate = new Date(date);
|
||||||
|
const diffMs = now.getTime() - commentDate.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / (1000 * 60));
|
||||||
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||||
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffMins < 1) return 'Just now';
|
||||||
|
if (diffMins < 60) return `${diffMins}m ago`;
|
||||||
|
if (diffHours < 24) return `${diffHours}h ago`;
|
||||||
|
return `${diffDays}d ago`;
|
||||||
|
}
|
||||||
|
|
||||||
export const formatMarketCap = formatValue;
|
export const formatMarketCap = formatValue;
|
||||||
|
|
|
||||||
125
website/src/routes/api/coin/[coinSymbol]/comments/+server.ts
Normal file
125
website/src/routes/api/coin/[coinSymbol]/comments/+server.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { auth } from '$lib/auth';
|
||||||
|
import { error, json } from '@sveltejs/kit';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { comment, coin, user, commentLike } from '$lib/server/db/schema';
|
||||||
|
import { eq, and, desc, sql } from 'drizzle-orm';
|
||||||
|
|
||||||
|
export async function GET({ params, request }) {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
const { coinSymbol } = params;
|
||||||
|
const normalizedSymbol = coinSymbol.toUpperCase();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [coinData] = await db
|
||||||
|
.select({ id: coin.id })
|
||||||
|
.from(coin)
|
||||||
|
.where(eq(coin.symbol, normalizedSymbol))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!coinData) {
|
||||||
|
return json({ message: 'Coin not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const commentsQuery = db
|
||||||
|
.select({
|
||||||
|
id: comment.id,
|
||||||
|
content: comment.content,
|
||||||
|
likesCount: comment.likesCount,
|
||||||
|
createdAt: comment.createdAt,
|
||||||
|
updatedAt: comment.updatedAt,
|
||||||
|
userId: user.id,
|
||||||
|
userName: user.name,
|
||||||
|
userUsername: user.username,
|
||||||
|
userImage: user.image,
|
||||||
|
userBio: user.bio,
|
||||||
|
userCreatedAt: user.createdAt,
|
||||||
|
isLikedByUser: session?.user ?
|
||||||
|
sql<boolean>`EXISTS(SELECT 1 FROM ${commentLike} WHERE ${commentLike.userId} = ${session.user.id} AND ${commentLike.commentId} = ${comment.id})` :
|
||||||
|
sql<boolean>`FALSE`
|
||||||
|
})
|
||||||
|
.from(comment)
|
||||||
|
.innerJoin(user, eq(comment.userId, user.id))
|
||||||
|
.where(and(eq(comment.coinId, coinData.id), eq(comment.isDeleted, false)))
|
||||||
|
.orderBy(desc(comment.createdAt));
|
||||||
|
|
||||||
|
const comments = await commentsQuery;
|
||||||
|
|
||||||
|
return json({ comments });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch comments:', err);
|
||||||
|
return json({ message: 'Internal server error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST({ request, params }) {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
throw error(401, 'Not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { coinSymbol } = params;
|
||||||
|
const { content } = await request.json();
|
||||||
|
|
||||||
|
if (!content || content.trim().length === 0) {
|
||||||
|
throw error(400, 'Comment content is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.length > 500) {
|
||||||
|
throw error(400, 'Comment must be 500 characters or less');
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedSymbol = coinSymbol.toUpperCase();
|
||||||
|
const userId = Number(session.user.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [coinData] = await db
|
||||||
|
.select({ id: coin.id })
|
||||||
|
.from(coin)
|
||||||
|
.where(eq(coin.symbol, normalizedSymbol))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!coinData) {
|
||||||
|
throw error(404, 'Coin not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [newComment] = await db
|
||||||
|
.insert(comment)
|
||||||
|
.values({
|
||||||
|
userId,
|
||||||
|
coinId: coinData.id,
|
||||||
|
content: content.trim()
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
const [commentWithUser] = await db
|
||||||
|
.select({
|
||||||
|
id: comment.id,
|
||||||
|
content: comment.content,
|
||||||
|
likesCount: comment.likesCount,
|
||||||
|
createdAt: comment.createdAt,
|
||||||
|
updatedAt: comment.updatedAt,
|
||||||
|
userId: comment.userId,
|
||||||
|
userName: user.name,
|
||||||
|
userUsername: user.username,
|
||||||
|
userImage: user.image,
|
||||||
|
userBio: user.bio,
|
||||||
|
userCreatedAt: user.createdAt,
|
||||||
|
isLikedByUser: sql<boolean>`FALSE`
|
||||||
|
})
|
||||||
|
.from(comment)
|
||||||
|
.innerJoin(user, eq(comment.userId, user.id))
|
||||||
|
.where(eq(comment.id, newComment.id))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return json({ comment: commentWithUser });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error creating comment:', e);
|
||||||
|
throw error(500, 'Failed to create comment');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { error, json } from '@sveltejs/kit';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { comment, commentLike, coin } from '$lib/server/db/schema';
|
||||||
|
import { eq, and, sql } from 'drizzle-orm';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { auth } from '$lib/auth';
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request, params }) => {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return json({ message: 'Not authenticated' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const commentId = parseInt(params.id);
|
||||||
|
const { coinSymbol } = params;
|
||||||
|
const userId = Number(session.user.id);
|
||||||
|
|
||||||
|
if (isNaN(commentId)) {
|
||||||
|
return json({ message: 'Invalid comment ID' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify the comment exists and belongs to the specified coin
|
||||||
|
const [commentData] = await db
|
||||||
|
.select()
|
||||||
|
.from(comment)
|
||||||
|
.innerJoin(coin, eq(comment.coinId, coin.id))
|
||||||
|
.where(and(eq(comment.id, commentId), eq(coin.symbol, coinSymbol)));
|
||||||
|
|
||||||
|
if (!commentData) {
|
||||||
|
return json({ message: 'Comment not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user already liked this comment
|
||||||
|
const [existingLike] = await db
|
||||||
|
.select()
|
||||||
|
.from(commentLike)
|
||||||
|
.where(and(eq(commentLike.userId, userId), eq(commentLike.commentId, commentId)));
|
||||||
|
|
||||||
|
if (existingLike) {
|
||||||
|
return json({ message: 'Comment already liked' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add like and increment count
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
await tx.insert(commentLike).values({ userId, commentId });
|
||||||
|
|
||||||
|
await tx
|
||||||
|
.update(comment)
|
||||||
|
.set({ likesCount: sql`${comment.likesCount} + 1` })
|
||||||
|
.where(eq(comment.id, commentId));
|
||||||
|
});
|
||||||
|
|
||||||
|
return json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to like comment:', error);
|
||||||
|
return json({ message: 'Internal server error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DELETE: RequestHandler = async ({ request, params }) => {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
throw error(401, 'Not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
const commentId = parseInt(params.id);
|
||||||
|
const { coinSymbol } = params;
|
||||||
|
const userId = Number(session.user.id);
|
||||||
|
|
||||||
|
if (isNaN(commentId)) {
|
||||||
|
return json({ message: 'Invalid comment ID' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify the comment exists and belongs to the specified coin
|
||||||
|
const [commentData] = await db
|
||||||
|
.select()
|
||||||
|
.from(comment)
|
||||||
|
.innerJoin(coin, eq(comment.coinId, coin.id))
|
||||||
|
.where(and(eq(comment.id, commentId), eq(coin.symbol, coinSymbol)));
|
||||||
|
|
||||||
|
if (!commentData) {
|
||||||
|
return json({ message: 'Comment not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has liked this comment
|
||||||
|
const [existingLike] = await db
|
||||||
|
.select()
|
||||||
|
.from(commentLike)
|
||||||
|
.where(and(eq(commentLike.userId, userId), eq(commentLike.commentId, commentId)));
|
||||||
|
|
||||||
|
if (!existingLike) {
|
||||||
|
return json({ message: 'Comment not liked' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove like and decrement count
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
await tx
|
||||||
|
.delete(commentLike)
|
||||||
|
.where(and(eq(commentLike.userId, userId), eq(commentLike.commentId, commentId)));
|
||||||
|
|
||||||
|
await tx
|
||||||
|
.update(comment)
|
||||||
|
.set({ likesCount: sql`GREATEST(0, ${comment.likesCount} - 1)` })
|
||||||
|
.where(eq(comment.id, commentId));
|
||||||
|
});
|
||||||
|
|
||||||
|
return json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to unlike comment:', error);
|
||||||
|
return json({ message: 'Internal server error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import * as Avatar from '$lib/components/ui/avatar';
|
import * as Avatar from '$lib/components/ui/avatar';
|
||||||
import * as HoverCard from '$lib/components/ui/hover-card';
|
import * as HoverCard from '$lib/components/ui/hover-card';
|
||||||
import TradeModal from '$lib/components/self/TradeModal.svelte';
|
import TradeModal from '$lib/components/self/TradeModal.svelte';
|
||||||
|
import CommentSection from '$lib/components/self/CommentSection.svelte';
|
||||||
import {
|
import {
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
TrendingDown,
|
TrendingDown,
|
||||||
|
|
@ -361,7 +362,7 @@
|
||||||
<div class="flex items-center pt-2">
|
<div class="flex items-center pt-2">
|
||||||
<CalendarDays class="mr-2 h-4 w-4 opacity-70" />
|
<CalendarDays class="mr-2 h-4 w-4 opacity-70" />
|
||||||
<span class="text-muted-foreground text-xs">
|
<span class="text-muted-foreground text-xs">
|
||||||
Joined {new Date(coin.createdAt).toLocaleDateString('en-US', {
|
Joined {new Date(coin.creatorCreatedAt).toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long'
|
month: 'long'
|
||||||
})}
|
})}
|
||||||
|
|
@ -565,6 +566,9 @@
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Comments Section -->
|
||||||
|
<CommentSection {coinSymbol} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import * as Popover from '$lib/components/ui/popover';
|
import * as Popover from '$lib/components/ui/popover';
|
||||||
import * as Pagination from '$lib/components/ui/pagination';
|
import * as Pagination from '$lib/components/ui/pagination';
|
||||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
import * as Select from '$lib/components/ui/select';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
|
|
@ -18,11 +18,11 @@
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronDown,
|
|
||||||
DollarSign,
|
DollarSign,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
ArrowUpDown
|
ArrowUpDown
|
||||||
} from 'lucide-svelte'; import { formatPrice, formatMarketCap, debounce, formatRelativeTime } from '$lib/utils';
|
} from 'lucide-svelte';
|
||||||
|
import { formatPrice, formatMarketCap, debounce, formatRelativeTime } from '$lib/utils';
|
||||||
import { MediaQuery } from 'svelte/reactivity';
|
import { MediaQuery } from 'svelte/reactivity';
|
||||||
import type { CoinData, FilterOption, VolatilityBadge, MarketResponse } from '$lib/types/market';
|
import type { CoinData, FilterOption, VolatilityBadge, MarketResponse } from '$lib/types/market';
|
||||||
|
|
||||||
|
|
@ -165,8 +165,19 @@
|
||||||
fetchMarketData();
|
fetchMarketData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSortOrderChange(newSortOrder: string) {
|
function handleSortOrderChange() {
|
||||||
sortOrder = newSortOrder;
|
currentPage = 1;
|
||||||
|
updateURL();
|
||||||
|
fetchMarketData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePriceFilterChange() {
|
||||||
|
currentPage = 1;
|
||||||
|
updateURL();
|
||||||
|
fetchMarketData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChangeFilterChange() {
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
updateURL();
|
updateURL();
|
||||||
fetchMarketData();
|
fetchMarketData();
|
||||||
|
|
@ -199,20 +210,6 @@
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePriceFilterChange(value: string) {
|
|
||||||
priceFilter = value;
|
|
||||||
currentPage = 1;
|
|
||||||
updateURL();
|
|
||||||
fetchMarketData();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChangeFilterChange(value: string) {
|
|
||||||
changeFilter = value;
|
|
||||||
currentPage = 1;
|
|
||||||
updateURL();
|
|
||||||
fetchMarketData();
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasActiveFilters = $derived(
|
let hasActiveFilters = $derived(
|
||||||
searchQuery !== '' ||
|
searchQuery !== '' ||
|
||||||
priceFilter !== 'all' ||
|
priceFilter !== 'all' ||
|
||||||
|
|
@ -316,89 +313,68 @@
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label class="text-sm font-medium">Sort Order</Label>
|
<Label class="text-sm font-medium">Sort Order</Label>
|
||||||
<DropdownMenu.Root>
|
<Select.Root
|
||||||
<DropdownMenu.Trigger
|
type="single"
|
||||||
class="border-input bg-background ring-offset-background focus-visible:ring-ring flex h-9 w-full items-center justify-between rounded-md border px-3 py-1 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
bind:value={sortOrder}
|
||||||
>
|
onValueChange={handleSortOrderChange}
|
||||||
<div class="flex items-center gap-2">
|
>
|
||||||
<ArrowUpDown class="h-4 w-4" />
|
<Select.Trigger class="w-full">
|
||||||
<span>{currentSortOrderLabel}</span>
|
{currentSortOrderLabel}
|
||||||
</div>
|
</Select.Trigger>
|
||||||
<ChevronDown class="h-4 w-4 opacity-50" />
|
<Select.Content>
|
||||||
</DropdownMenu.Trigger>
|
<Select.Group>
|
||||||
<DropdownMenu.Content class="w-56">
|
{#each sortOrderOptions as option}
|
||||||
{#each sortOrderOptions as option}
|
<Select.Item value={option.value} label={option.label}>
|
||||||
<DropdownMenu.Item
|
{option.label}
|
||||||
onclick={() => handleSortOrderChange(option.value)}
|
</Select.Item>
|
||||||
class="cursor-pointer"
|
{/each}
|
||||||
>
|
</Select.Group>
|
||||||
<ArrowUpDown class="h-4 w-4" />
|
</Select.Content>
|
||||||
<span>{option.label}</span>
|
</Select.Root>
|
||||||
{#if sortOrder === option.value}
|
|
||||||
<div class="bg-primary ml-auto h-2 w-2 rounded-full"></div>
|
|
||||||
{/if}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
{/each}
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label class="text-sm font-medium">Price Range</Label>
|
<Label class="text-sm font-medium">Price Range</Label>
|
||||||
<DropdownMenu.Root>
|
<Select.Root
|
||||||
<DropdownMenu.Trigger
|
type="single"
|
||||||
class="border-input bg-background ring-offset-background focus-visible:ring-ring flex h-9 w-full items-center justify-between rounded-md border px-3 py-1 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
bind:value={priceFilter}
|
||||||
>
|
onValueChange={handlePriceFilterChange}
|
||||||
<div class="flex items-center gap-2">
|
>
|
||||||
<DollarSign class="h-4 w-4" />
|
<Select.Trigger class="w-full">
|
||||||
<span>{currentPriceFilterLabel}</span>
|
{currentPriceFilterLabel}
|
||||||
</div>
|
</Select.Trigger>
|
||||||
<ChevronDown class="h-4 w-4 opacity-50" />
|
<Select.Content>
|
||||||
</DropdownMenu.Trigger>
|
<Select.Group>
|
||||||
<DropdownMenu.Content class="w-56">
|
{#each priceFilterOptions as option}
|
||||||
{#each priceFilterOptions as option}
|
<Select.Item value={option.value} label={option.label}>
|
||||||
<DropdownMenu.Item
|
{option.label}
|
||||||
onclick={() => handlePriceFilterChange(option.value)}
|
</Select.Item>
|
||||||
class="cursor-pointer"
|
{/each}
|
||||||
>
|
</Select.Group>
|
||||||
<DollarSign class="h-4 w-4" />
|
</Select.Content>
|
||||||
<span>{option.label}</span>
|
</Select.Root>
|
||||||
{#if priceFilter === option.value}
|
|
||||||
<div class="bg-primary ml-auto h-2 w-2 rounded-full"></div>
|
|
||||||
{/if}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
{/each}
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label class="text-sm font-medium">24h Change</Label>
|
<Label class="text-sm font-medium">24h Change</Label>
|
||||||
<DropdownMenu.Root>
|
<Select.Root
|
||||||
<DropdownMenu.Trigger
|
type="single"
|
||||||
class="border-input bg-background ring-offset-background focus-visible:ring-ring flex h-9 w-full items-center justify-between rounded-md border px-3 py-1 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
bind:value={changeFilter}
|
||||||
>
|
onValueChange={handleChangeFilterChange}
|
||||||
<div class="flex items-center gap-2">
|
>
|
||||||
<TrendingUp class="h-4 w-4" />
|
<Select.Trigger class="w-full">
|
||||||
<span>{currentChangeFilterLabel}</span>
|
{currentChangeFilterLabel}
|
||||||
</div>
|
</Select.Trigger>
|
||||||
<ChevronDown class="h-4 w-4 opacity-50" />
|
<Select.Content>
|
||||||
</DropdownMenu.Trigger>
|
<Select.Group>
|
||||||
<DropdownMenu.Content class="w-56">
|
{#each changeFilterOptions as option}
|
||||||
{#each changeFilterOptions as option}
|
<Select.Item value={option.value} label={option.label}>
|
||||||
<DropdownMenu.Item
|
{option.label}
|
||||||
onclick={() => handleChangeFilterChange(option.value)}
|
</Select.Item>
|
||||||
class="cursor-pointer"
|
{/each}
|
||||||
>
|
</Select.Group>
|
||||||
<TrendingUp class="h-4 w-4" />
|
</Select.Content>
|
||||||
<span>{option.label}</span>
|
</Select.Root>
|
||||||
{#if changeFilter === option.value}
|
|
||||||
<div class="bg-primary ml-auto h-2 w-2 rounded-full"></div>
|
|
||||||
{/if}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
{/each}
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 pt-2">
|
<div class="flex gap-2 pt-2">
|
||||||
|
|
|
||||||
Reference in a new issue