Merge pull request #14 from BradNut/drizzle

Merge Drizzle to shadcn
This commit is contained in:
Bradley Shellnut 2024-03-05 06:24:26 +00:00 committed by GitHub
commit 196dfd48f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
137 changed files with 25129 additions and 2501 deletions

20
drizzle.config.ts Normal file
View file

@ -0,0 +1,20 @@
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/schema.ts',
out: './drizzle',
driver: 'pg',
dbCredentials: {
host: process.env.DATABASE_HOST || 'localhost',
port: Number(process.env.DATABASE_PORT) || 5432,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE || 'boredgame',
ssl: true
},
// Print all statements
verbose: true,
// Always as for confirmation
strict: true
});

View file

@ -0,0 +1,233 @@
CREATE TABLE IF NOT EXISTS "artists" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"name" varchar(255),
"slug" varchar(255),
"external_id" integer,
"created_at" timestamp with time zone DEFAULT (now(6)),
"updated_at" timestamp with time zone DEFAULT (now(6))
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "artists_to_games" (
"artist_id" varchar(255),
"game_id" varchar(255)
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "categories" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"name" varchar(255),
"slug" varchar(255),
"external_id" integer,
"created_at" timestamp with time zone DEFAULT (now(6)),
"updated_at" timestamp with time zone DEFAULT (now(6))
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "categories_to_games" (
"category_id" varchar(255),
"game_id" varchar(255)
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "collection_items" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"collection_id" varchar(255) NOT NULL,
"game_id" varchar(255) NOT NULL,
"created_at" timestamp with time zone DEFAULT (now(6)),
"updated_at" timestamp with time zone DEFAULT (now(6))
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "collections" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"user_id" varchar(255) NOT NULL,
"created_at" timestamp with time zone DEFAULT (now(6)),
"updated_at" timestamp with time zone DEFAULT (now(6))
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "designers" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"name" varchar(255),
"slug" varchar(255),
"external_id" integer,
"created_at" timestamp with time zone DEFAULT (now(6)),
"updated_at" timestamp with time zone DEFAULT (now(6))
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "designers_to_games" (
"designer_id" varchar(255),
"game_id" varchar(255)
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "expansions" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"base_game_id" varchar(255) NOT NULL,
"game_id" varchar(255) NOT NULL,
"created_at" timestamp with time zone DEFAULT (now(6)),
"updated_at" timestamp with time zone DEFAULT (now(6))
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "games" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"name" varchar(255),
"slug" varchar(255),
"description" text,
"year_published" integer,
"min_players" integer,
"max_players" integer,
"playtime" integer,
"min_playtime" integer,
"max_playtime" integer,
"min_age" integer,
"image_url" varchar(255),
"thumb_url" varchar(255),
"url" varchar(255),
"external_id" integer,
"last_sync_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT (now(6)),
"updated_at" timestamp with time zone DEFAULT (now(6)),
CONSTRAINT "games_external_id_unique" UNIQUE("external_id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "mechanics" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"name" varchar(255),
"slug" varchar(255),
"external_id" integer,
"created_at" timestamp with time zone DEFAULT (now(6)),
"updated_at" timestamp with time zone DEFAULT (now(6))
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "mechanics_to_games" (
"mechanic_id" varchar(255),
"game_id" varchar(255)
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "publishers" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"name" varchar(255),
"slug" varchar(255),
"external_id" integer,
"created_at" timestamp with time zone DEFAULT (now(6)),
"updated_at" timestamp with time zone DEFAULT (now(6))
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "publishers_to_games" (
"publisher_id" varchar(255),
"game_id" varchar(255)
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "roles" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"name" varchar(255),
CONSTRAINT "roles_name_unique" UNIQUE("name")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "sessions" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"user_id" varchar(255) NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"ip_country" varchar(255),
"ip_address" varchar(255)
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "user_roles" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"user_id" varchar(255) NOT NULL,
"role_id" varchar(255) NOT NULL,
"created_at" timestamp with time zone DEFAULT (now(6)),
"updated_at" timestamp with time zone DEFAULT (now(6))
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "users" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"username" varchar(255),
"hashed_password" varchar(255),
"email" varchar(255),
"first_name" varchar(255),
"last_name" varchar(255),
"verified" boolean DEFAULT false,
"receive_email" boolean DEFAULT false,
"theme" varchar(255) DEFAULT 'system',
"created_at" timestamp DEFAULT (now(6)),
"updated_at" timestamp DEFAULT (now(6)),
CONSTRAINT "users_username_unique" UNIQUE("username"),
CONSTRAINT "users_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "wishlist_items" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"wishlist_id" varchar(255) NOT NULL,
"game_id" varchar(255) NOT NULL,
"created_at" timestamp with time zone DEFAULT (now(6)),
"updated_at" timestamp with time zone DEFAULT (now(6))
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "wishlists" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"user_id" varchar(255) NOT NULL,
"created_at" timestamp with time zone DEFAULT (now(6)),
"updated_at" timestamp with time zone DEFAULT (now(6))
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "collection_items" ADD CONSTRAINT "collection_items_collection_id_collections_id_fk" FOREIGN KEY ("collection_id") REFERENCES "collections"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "collection_items" ADD CONSTRAINT "collection_items_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "collections" ADD CONSTRAINT "collections_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "expansions" ADD CONSTRAINT "expansions_base_game_id_games_id_fk" FOREIGN KEY ("base_game_id") REFERENCES "games"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "expansions" ADD CONSTRAINT "expansions_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "wishlist_items" ADD CONSTRAINT "wishlist_items_wishlist_id_wishlists_id_fk" FOREIGN KEY ("wishlist_id") REFERENCES "wishlists"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "wishlist_items" ADD CONSTRAINT "wishlist_items_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "wishlists" ADD CONSTRAINT "wishlists_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,51 @@
ALTER TABLE "artists" ALTER COLUMN "created_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "artists" ALTER COLUMN "created_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "artists" ALTER COLUMN "updated_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "artists" ALTER COLUMN "updated_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "categories" ALTER COLUMN "created_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "categories" ALTER COLUMN "created_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "categories" ALTER COLUMN "updated_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "categories" ALTER COLUMN "updated_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "collection_items" ALTER COLUMN "created_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "collection_items" ALTER COLUMN "created_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "collection_items" ALTER COLUMN "updated_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "collection_items" ALTER COLUMN "updated_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "collections" ALTER COLUMN "created_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "collections" ALTER COLUMN "created_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "collections" ALTER COLUMN "updated_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "collections" ALTER COLUMN "updated_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "designers" ALTER COLUMN "created_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "designers" ALTER COLUMN "created_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "designers" ALTER COLUMN "updated_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "designers" ALTER COLUMN "updated_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "expansions" ALTER COLUMN "created_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "expansions" ALTER COLUMN "created_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "expansions" ALTER COLUMN "updated_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "expansions" ALTER COLUMN "updated_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "games" ALTER COLUMN "last_sync_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "games" ALTER COLUMN "created_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "games" ALTER COLUMN "created_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "games" ALTER COLUMN "updated_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "games" ALTER COLUMN "updated_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "mechanics" ALTER COLUMN "created_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "mechanics" ALTER COLUMN "created_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "mechanics" ALTER COLUMN "updated_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "mechanics" ALTER COLUMN "updated_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "publishers" ALTER COLUMN "created_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "publishers" ALTER COLUMN "created_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "publishers" ALTER COLUMN "updated_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "publishers" ALTER COLUMN "updated_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "user_roles" ALTER COLUMN "created_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "user_roles" ALTER COLUMN "created_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "user_roles" ALTER COLUMN "updated_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "user_roles" ALTER COLUMN "updated_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "created_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "updated_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "wishlist_items" ALTER COLUMN "created_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "wishlist_items" ALTER COLUMN "created_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "wishlist_items" ALTER COLUMN "updated_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "wishlist_items" ALTER COLUMN "updated_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "wishlists" ALTER COLUMN "created_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "wishlists" ALTER COLUMN "created_at" SET DEFAULT (now());--> statement-breakpoint
ALTER TABLE "wishlists" ALTER COLUMN "updated_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "wishlists" ALTER COLUMN "updated_at" SET DEFAULT (now());

View file

@ -0,0 +1,27 @@
ALTER TABLE "artists" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "artists" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "categories" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "categories" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "collection_items" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "collection_items" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "collections" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "collections" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "designers" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "designers" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "expansions" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "expansions" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "games" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "games" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "mechanics" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "mechanics" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "publishers" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "publishers" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "user_roles" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "user_roles" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "wishlist_items" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "wishlist_items" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "wishlists" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "wishlists" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "games" ADD COLUMN "text_searchable_index" "tsvector";

View file

@ -0,0 +1 @@
CREATE INDEX IF NOT EXISTS "text_searchable_idx" ON "games" ("text_searchable_index");

View file

@ -0,0 +1,30 @@
DO $$ BEGIN
CREATE TYPE "external_id_type" AS ENUM('game', 'category', 'mechanic', 'publisher', 'designer', 'artist');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "external_ids" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"type" varchar(255),
"external_id" varchar(255)
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "game_external_ids" (
"game_id" varchar(255) NOT NULL,
"external_id" varchar(255) NOT NULL
);
--> statement-breakpoint
ALTER TABLE "games" DROP CONSTRAINT "games_external_id_unique";--> statement-breakpoint
ALTER TABLE "games" DROP COLUMN IF EXISTS "external_id";--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "game_external_ids" ADD CONSTRAINT "game_external_ids_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "game_external_ids" ADD CONSTRAINT "game_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "external_ids"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,16 @@
ALTER TABLE "game_external_ids" RENAME TO "games_to_external_ids";--> statement-breakpoint
ALTER TABLE "games_to_external_ids" DROP CONSTRAINT "game_external_ids_game_id_games_id_fk";
--> statement-breakpoint
ALTER TABLE "games_to_external_ids" DROP CONSTRAINT "game_external_ids_external_id_external_ids_id_fk";
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "games_to_external_ids" ADD CONSTRAINT "games_to_external_ids_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "games_to_external_ids" ADD CONSTRAINT "games_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "external_ids"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,2 @@
ALTER TABLE "external_ids" ALTER COLUMN "type" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "external_ids" ALTER COLUMN "external_id" SET NOT NULL;

View file

@ -0,0 +1,71 @@
CREATE TABLE IF NOT EXISTS "categories_to_external_ids" (
"category_id" varchar(255) NOT NULL,
"external_id" varchar(255) NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "expansions_to_external_ids" (
"expansion_id" varchar(255) NOT NULL,
"external_id" varchar(255) NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "mechanics_to_external_ids" (
"mechanic_id" varchar(255) NOT NULL,
"external_id" varchar(255) NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "publishers_to_external_ids" (
"publisher_id" varchar(255) NOT NULL,
"external_id" varchar(255) NOT NULL
);
--> statement-breakpoint
DROP TABLE "artists";--> statement-breakpoint
DROP TABLE "artists_to_games";--> statement-breakpoint
DROP TABLE "designers";--> statement-breakpoint
DROP TABLE "designers_to_games";--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "categories_to_external_ids" ADD CONSTRAINT "categories_to_external_ids_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "categories"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "categories_to_external_ids" ADD CONSTRAINT "categories_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "external_ids"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "expansions_to_external_ids" ADD CONSTRAINT "expansions_to_external_ids_expansion_id_expansions_id_fk" FOREIGN KEY ("expansion_id") REFERENCES "expansions"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "expansions_to_external_ids" ADD CONSTRAINT "expansions_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "external_ids"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mechanics_to_external_ids" ADD CONSTRAINT "mechanics_to_external_ids_mechanic_id_mechanics_id_fk" FOREIGN KEY ("mechanic_id") REFERENCES "mechanics"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mechanics_to_external_ids" ADD CONSTRAINT "mechanics_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "external_ids"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "publishers_to_external_ids" ADD CONSTRAINT "publishers_to_external_ids_publisher_id_publishers_id_fk" FOREIGN KEY ("publisher_id") REFERENCES "publishers"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "publishers_to_external_ids" ADD CONSTRAINT "publishers_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "external_ids"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,7 @@
DO $$ BEGIN
CREATE TYPE "type" AS ENUM('game', 'category', 'mechanic', 'publisher', 'designer', 'artist');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TABLE "external_ids" ALTER COLUMN "type" SET DATA TYPE type;

View file

@ -0,0 +1,7 @@
DO $$ BEGIN
CREATE TYPE "external_id_type" AS ENUM('game', 'category', 'mechanic', 'publisher', 'designer', 'artist');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TABLE "external_ids" ALTER COLUMN "type" SET DATA TYPE external_id_type;

View file

@ -0,0 +1 @@
ALTER TABLE "collection_items" ADD COLUMN "times_played" integer DEFAULT 0;

View file

@ -0,0 +1,97 @@
ALTER TABLE "categories_to_external_ids" DROP CONSTRAINT "categories_to_external_ids_category_id_categories_id_fk";
--> statement-breakpoint
ALTER TABLE "expansions" DROP CONSTRAINT "expansions_base_game_id_games_id_fk";
--> statement-breakpoint
ALTER TABLE "expansions_to_external_ids" DROP CONSTRAINT "expansions_to_external_ids_expansion_id_expansions_id_fk";
--> statement-breakpoint
ALTER TABLE "games_to_external_ids" DROP CONSTRAINT "games_to_external_ids_game_id_games_id_fk";
--> statement-breakpoint
ALTER TABLE "mechanics_to_external_ids" DROP CONSTRAINT "mechanics_to_external_ids_mechanic_id_mechanics_id_fk";
--> statement-breakpoint
ALTER TABLE "publishers_to_external_ids" DROP CONSTRAINT "publishers_to_external_ids_publisher_id_publishers_id_fk";
--> statement-breakpoint
ALTER TABLE "categories_to_games" ALTER COLUMN "category_id" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "categories_to_games" ALTER COLUMN "game_id" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "mechanics_to_games" ALTER COLUMN "mechanic_id" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "mechanics_to_games" ALTER COLUMN "game_id" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "publishers_to_games" ALTER COLUMN "publisher_id" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "publishers_to_games" ALTER COLUMN "game_id" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "categories_to_external_ids" ADD CONSTRAINT "categories_to_external_ids_category_id_external_id_pk" PRIMARY KEY("category_id","external_id");--> statement-breakpoint
ALTER TABLE "categories_to_games" ADD CONSTRAINT "categories_to_games_category_id_game_id_pk" PRIMARY KEY("category_id","game_id");--> statement-breakpoint
ALTER TABLE "expansions_to_external_ids" ADD CONSTRAINT "expansions_to_external_ids_expansion_id_external_id_pk" PRIMARY KEY("expansion_id","external_id");--> statement-breakpoint
ALTER TABLE "games_to_external_ids" ADD CONSTRAINT "games_to_external_ids_game_id_external_id_pk" PRIMARY KEY("game_id","external_id");--> statement-breakpoint
ALTER TABLE "mechanics_to_external_ids" ADD CONSTRAINT "mechanics_to_external_ids_mechanic_id_external_id_pk" PRIMARY KEY("mechanic_id","external_id");--> statement-breakpoint
ALTER TABLE "mechanics_to_games" ADD CONSTRAINT "mechanics_to_games_mechanic_id_game_id_pk" PRIMARY KEY("mechanic_id","game_id");--> statement-breakpoint
ALTER TABLE "publishers_to_external_ids" ADD CONSTRAINT "publishers_to_external_ids_publisher_id_external_id_pk" PRIMARY KEY("publisher_id","external_id");--> statement-breakpoint
ALTER TABLE "publishers_to_games" ADD CONSTRAINT "publishers_to_games_publisher_id_game_id_pk" PRIMARY KEY("publisher_id","game_id");--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "categories_to_external_ids" ADD CONSTRAINT "categories_to_external_ids_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "categories"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "categories_to_games" ADD CONSTRAINT "categories_to_games_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "categories"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "categories_to_games" ADD CONSTRAINT "categories_to_games_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "expansions" ADD CONSTRAINT "expansions_base_game_id_games_id_fk" FOREIGN KEY ("base_game_id") REFERENCES "games"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "expansions_to_external_ids" ADD CONSTRAINT "expansions_to_external_ids_expansion_id_expansions_id_fk" FOREIGN KEY ("expansion_id") REFERENCES "expansions"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "games_to_external_ids" ADD CONSTRAINT "games_to_external_ids_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mechanics_to_external_ids" ADD CONSTRAINT "mechanics_to_external_ids_mechanic_id_mechanics_id_fk" FOREIGN KEY ("mechanic_id") REFERENCES "mechanics"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mechanics_to_games" ADD CONSTRAINT "mechanics_to_games_mechanic_id_mechanics_id_fk" FOREIGN KEY ("mechanic_id") REFERENCES "mechanics"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mechanics_to_games" ADD CONSTRAINT "mechanics_to_games_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "publishers_to_external_ids" ADD CONSTRAINT "publishers_to_external_ids_publisher_id_publishers_id_fk" FOREIGN KEY ("publisher_id") REFERENCES "publishers"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "publishers_to_games" ADD CONSTRAINT "publishers_to_games_publisher_id_publishers_id_fk" FOREIGN KEY ("publisher_id") REFERENCES "publishers"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "publishers_to_games" ADD CONSTRAINT "publishers_to_games_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,2 @@
ALTER TABLE "users" ALTER COLUMN "created_at" SET DATA TYPE timestamp (6) with time zone;--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "updated_at" SET DATA TYPE timestamp (6) with time zone;

View file

@ -0,0 +1,3 @@
ALTER TABLE "categories" DROP COLUMN IF EXISTS "external_id";--> statement-breakpoint
ALTER TABLE "mechanics" DROP COLUMN IF EXISTS "external_id";--> statement-breakpoint
ALTER TABLE "publishers" DROP COLUMN IF EXISTS "external_id";

View file

@ -0,0 +1 @@
DROP TABLE "expansions_to_external_ids";

View file

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS "password_reset_tokens" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"user_id" varchar(255) NOT NULL,
"expires_at" timestamp (6) with time zone,
"created_at" timestamp (6) with time zone DEFAULT now()
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "password_reset_tokens" ADD CONSTRAINT "password_reset_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

118
drizzle/meta/_journal.json Normal file
View file

@ -0,0 +1,118 @@
{
"version": "5",
"dialect": "pg",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1707437865821,
"tag": "0000_oval_wolverine",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1707438055782,
"tag": "0001_giant_tomorrow_man",
"breakpoints": true
},
{
"idx": 2,
"version": "5",
"when": 1707524139123,
"tag": "0002_sour_silverclaw",
"breakpoints": true
},
{
"idx": 3,
"version": "5",
"when": 1707526808124,
"tag": "0003_thick_tinkerer",
"breakpoints": true
},
{
"idx": 4,
"version": "5",
"when": 1707932397672,
"tag": "0004_fancy_umar",
"breakpoints": true
},
{
"idx": 5,
"version": "5",
"when": 1707932466413,
"tag": "0005_uneven_lifeguard",
"breakpoints": true
},
{
"idx": 6,
"version": "5",
"when": 1707932522909,
"tag": "0006_light_corsair",
"breakpoints": true
},
{
"idx": 7,
"version": "5",
"when": 1707951501716,
"tag": "0007_same_valeria_richards",
"breakpoints": true
},
{
"idx": 8,
"version": "5",
"when": 1708105454143,
"tag": "0008_complete_manta",
"breakpoints": true
},
{
"idx": 9,
"version": "5",
"when": 1708105890146,
"tag": "0009_equal_christian_walker",
"breakpoints": true
},
{
"idx": 10,
"version": "5",
"when": 1708243232524,
"tag": "0010_flat_mister_sinister",
"breakpoints": true
},
{
"idx": 11,
"version": "5",
"when": 1708330668971,
"tag": "0011_gigantic_mister_sinister",
"breakpoints": true
},
{
"idx": 12,
"version": "5",
"when": 1708330799655,
"tag": "0012_dizzy_lethal_legion",
"breakpoints": true
},
{
"idx": 13,
"version": "5",
"when": 1708453431550,
"tag": "0013_clever_monster_badoon",
"breakpoints": true
},
{
"idx": 14,
"version": "5",
"when": 1708479971410,
"tag": "0014_organic_morlocks",
"breakpoints": true
},
{
"idx": 15,
"version": "5",
"when": 1709344835732,
"tag": "0015_awesome_gabe_jones",
"breakpoints": true
}
]
}

View file

@ -16,57 +16,61 @@
"lint": "prettier --plugin-search-dir . --check . && eslint .", "lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write .", "format": "prettier --plugin-search-dir . --write .",
"site:update": "pnpm update -i -L", "site:update": "pnpm update -i -L",
"db:studio": "prisma studio", "generate": "drizzle-kit generate:pg",
"db:push": "prisma db push", "migrate": "tsx ./src/migrate.ts",
"db:generate": "prisma generate", "seed": "tsx ./src/seed.ts",
"db:seed": "prisma db seed", "push": "drizzle-kit push:pg"
"i-changed-the-schema": "pnpm run db:push && pnpm run db:generate"
}, },
"prisma": { "prisma": {
"seed": "node --loader ts-node/esm prisma/seed.ts" "seed": "node --loader ts-node/esm prisma/seed.ts"
}, },
"devDependencies": { "devDependencies": {
"@melt-ui/pp": "^0.3.0", "@melt-ui/pp": "^0.3.0",
"@melt-ui/svelte": "^0.73.0", "@melt-ui/svelte": "^0.75.2",
"@playwright/test": "^1.41.2", "@playwright/test": "^1.42.0",
"@resvg/resvg-js": "^2.6.0", "@resvg/resvg-js": "^2.6.0",
"@sveltejs/adapter-auto": "^3.1.1", "@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/enhanced-img": "^0.1.8", "@sveltejs/enhanced-img": "^0.1.8",
"@sveltejs/kit": "^2.5.0", "@sveltejs/kit": "^2.5.2",
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"@types/node": "^20.11.17", "@types/node": "^20.11.24",
"@types/pg": "^8.11.2",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^6.21.0",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.18",
"eslint": "^8.56.0", "dotenv": "^16.4.5",
"drizzle-kit": "^0.20.14",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1", "eslint-plugin-svelte": "^2.35.1",
"just-clone": "^6.2.0", "just-clone": "^6.2.0",
"just-debounce-it": "^3.2.0", "just-debounce-it": "^3.2.0",
"postcss": "^8.4.35", "postcss": "^8.4.35",
"postcss-import": "^16.0.0", "postcss-import": "^16.0.1",
"postcss-load-config": "^5.0.3", "postcss-load-config": "^5.0.3",
"postcss-preset-env": "^9.3.0", "postcss-preset-env": "^9.4.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-svelte": "^3.2.2",
"prisma": "^5.9.1", "prisma": "^5.9.1",
"sass": "^1.70.0", "sass": "^1.71.1",
"satori": "^0.10.13", "satori": "^0.10.13",
"satori-html": "^0.3.2", "satori-html": "^0.3.2",
"svelte": "^4.2.10", "svelte": "^4.2.12",
"svelte-check": "^3.6.4", "svelte-check": "^3.6.6",
"svelte-meta-tags": "^3.1.0", "svelte-meta-tags": "^3.1.1",
"svelte-preprocess": "^5.1.3", "svelte-preprocess": "^5.1.3",
"svelte-sequential-preprocessor": "^2.0.1", "svelte-sequential-preprocessor": "^2.0.1",
"sveltekit-flash-message": "^2.4.1", "sveltekit-flash-message": "^2.4.2",
"sveltekit-superforms": "^1.13.4", "sveltekit-rate-limiter": "^0.4.3",
"sveltekit-superforms": "^2.7.0",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tslib": "^2.6.1", "tslib": "^2.6.1",
"tsx": "^4.7.1",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.1.1", "vite": "^5.1.5",
"vitest": "^1.2.2", "vitest": "^1.3.1",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"type": "module", "type": "module",
@ -75,37 +79,45 @@
"pnpm": ">=8" "pnpm": ">=8"
}, },
"dependencies": { "dependencies": {
"@fontsource/fira-mono": "^5.0.8", "@fontsource/fira-mono": "^5.0.12",
"@iconify-icons/line-md": "^1.2.26", "@iconify-icons/line-md": "^1.2.26",
"@iconify-icons/mdi": "^1.2.47", "@iconify-icons/mdi": "^1.2.47",
"@lucia-auth/adapter-drizzle": "^1.0.2",
"@lucia-auth/adapter-prisma": "4.0.0", "@lucia-auth/adapter-prisma": "4.0.0",
"@lukeed/uuid": "^2.0.1", "@lukeed/uuid": "^2.0.1",
"@neondatabase/serverless": "^0.9.0",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@planetscale/database": "^1.16.0",
"@prisma/client": "^5.9.1", "@prisma/client": "^5.9.1",
"@sentry/sveltekit": "^7.88.0", "@sentry/sveltekit": "^7.100.1",
"@sveltejs/adapter-vercel": "^5.1.0", "@sveltejs/adapter-vercel": "^5.1.0",
"@types/feather-icons": "^4.29.4", "@types/feather-icons": "^4.29.4",
"@vercel/og": "^0.5.20", "@vercel/og": "^0.5.20",
"bits-ui": "^0.17.0", "bits-ui": "^0.19.3",
"boardgamegeekclient": "^1.9.1", "boardgamegeekclient": "^1.9.1",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"cookie": "^0.6.0", "cookie": "^0.6.0",
"drizzle-orm": "^0.29.4",
"feather-icons": "^4.29.1", "feather-icons": "^4.29.1",
"formsnap": "^0.4.3", "formsnap": "^0.5.1",
"html-entities": "^2.4.0", "html-entities": "^2.5.2",
"iconify-icon": "^2.0.0", "iconify-icon": "^2.0.0",
"just-kebab-case": "^4.2.0", "just-kebab-case": "^4.2.0",
"loader": "^2.1.1", "loader": "^2.1.1",
"lucia": "3.0.1", "lucia": "3.0.1",
"lucide-svelte": "^0.323.0", "lucide-svelte": "^0.344.0",
"open-props": "^1.6.18", "mysql2": "^3.9.2",
"oslo": "^1.1.0", "nanoid": "^5.0.6",
"open-props": "^1.6.20",
"oslo": "^1.1.3",
"pg": "^8.11.3",
"postgres": "^3.4.3",
"radix-svelte": "^0.9.0", "radix-svelte": "^0.9.0",
"svelte-french-toast": "^1.2.0", "svelte-french-toast": "^1.2.0",
"svelte-lazy-loader": "^1.0.0", "svelte-lazy-loader": "^1.0.0",
"tailwind-merge": "^2.2.1", "tailwind-merge": "^2.2.1",
"tailwind-variants": "^0.1.20", "tailwind-variants": "^0.2.0",
"tailwindcss-animate": "^1.0.6", "tailwindcss-animate": "^1.0.6",
"zod-to-json-schema": "^3.22.4" "zod-to-json-schema": "^3.22.4"
} }

File diff suppressed because it is too large Load diff

View file

@ -158,7 +158,6 @@ model Game {
image_url String? image_url String?
thumb_url String? thumb_url String?
url String? url String?
rules_url String?
categories Category[] categories Category[]
mechanics Mechanic[] mechanics Mechanic[]
designers Designer[] designers Designer[]

View file

@ -4,7 +4,7 @@
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="description" content="Bored? Find a game! Bored Game!" /> <meta name="description" content="Bored? Find a game! Bored Game!" />
<link rel="icon" href="%sveltekit.assets%/favicon-bored.png" /> <link rel="icon" href="%sveltekit.assets%/favicon-bored-game.svg" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<script> <script>
// const htmlElement = document.documentElement; // const htmlElement = document.documentElement;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -1,12 +1,9 @@
<script>
import { PUBLIC_SITE_URL } from "$env/static/public";
</script>
<footer> <footer>
<p>Built by <a target="__blank" href="https://bradleyshellnut.com">Bradley Shellnut</a></p> <p>Bored Game &copy; {new Date().getFullYear()} | Built by <a target="__blank" href="https://bradleyshellnut.com">Bradley Shellnut</a> | {PUBLIC_SITE_URL}</p>
<p>
<a
target="__blank"
href="https://www.flaticon.com/free-icons/board-game"
title="board game icons">Board game icons created by Freepik - Flaticon</a
>
</p>
</footer> </footer>
<style lang="postcss"> <style lang="postcss">

View file

@ -1,23 +1,24 @@
<script lang="ts"> <script lang="ts">
import { applyAction, enhance } from '$app/forms'; import { applyAction, enhance } from '$app/forms';
import toast from 'svelte-french-toast';
import { ListChecks, ListTodo, LogOut, User } from 'lucide-svelte'; import { ListChecks, ListTodo, LogOut, User } from 'lucide-svelte';
import * as DropdownMenu from "$components/ui/dropdown-menu"; import * as DropdownMenu from "$components/ui/dropdown-menu";
import * as Avatar from "$components/ui/avatar"; import * as Avatar from "$components/ui/avatar";
import Logo from '$components/logo.svelte';
import { invalidateAll } from '$app/navigation'; import { invalidateAll } from '$app/navigation';
import toast from 'svelte-french-toast'; import Logo from '$components/logo.svelte';
export let user; export let user: User | null;
let avatar = user?.username.slice(0, 1).toUpperCase() || '?'; let avatar = user?.username.slice(0, 1).toUpperCase() || '?';
</script> </script>
<header> <header>
<div class="corner"> <div class="corner">
<a href="/" class="logo" title="Home"> <a href="/" title="Home">
<div class="logo-image"> <div class="logo-image">
<Logo /> <Logo />
</div> </div>
Bored Game
</a> </a>
</div> </div>
<!-- <TextSearch /> --> <!-- <TextSearch /> -->
@ -113,23 +114,23 @@
} }
.corner { .corner {
width: 3em;
height: 3em;
margin-left: 1rem; margin-left: 1rem;
} }
.corner a { .corner a {
display: flex; display: flex;
align-items: center; place-items: center;
justify-content: center; gap: 0.5rem;
width: 100%; width: 100%;
height: 100%; height: 100%;
font-size: 1.125rem;
line-height: 1.75rem;
font-weight: 500;
} }
.logo-image { .logo-image {
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
object-fit: contain;
} }
nav { nav {
@ -154,8 +155,4 @@
text-decoration: underline; text-decoration: underline;
color: var(--accent-color); color: var(--accent-color);
} }
.separator {
@apply m-[5px] h-[1px] bg-black;
}
</style> </style>

View file

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
export let kind = 'primary'; export let kind = 'primary';
export let size;
export let icon = false; export let icon = false;
export let disabled = false; export let disabled = false;
</script> </script>
@ -27,18 +26,4 @@
min-width: 23.5rem; min-width: 23.5rem;
} }
} }
.danger {
background-color: var(--warning);
}
.danger:hover {
background-color: var(--warning-hover);
}
.btn-icon {
display: grid;
grid-template-columns: repeat(2, auto);
gap: 1rem;
}
</style> </style>

View file

@ -1,5 +1,14 @@
<script lang="ts"> <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-go-game" viewBox="0 0 24 24"
import logo from '$lib/assets/bored-game.png'; stroke-width="1" stroke="var(--fg)" fill="none" stroke-linecap="round" stroke-linejoin="round">
</script> <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M6 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<img src={logo} alt="Bored Game Home" /> <path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M6 18m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M18 18m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M3 12h7m4 0h7" />
<path d="M3 6h1m4 0h13" />
<path d="M3 18h1m4 0h8m4 0h1" />
<path d="M6 3v1m0 4v8m0 4v1" />
<path d="M12 3v7m0 4v7" />
<path d="M18 3v13m0 4v1" />
</svg>

Before

Width:  |  Height:  |  Size: 118 B

After

Width:  |  Height:  |  Size: 671 B

View file

@ -1,38 +1,54 @@
<script lang="ts"> <script lang="ts">
import type { SuperValidated } from 'sveltekit-superforms'; import { superForm, type Infer, type SuperValidated } from 'sveltekit-superforms';
import { search_schema, type SearchSchema } from '$lib/zodValidation'; import { search_schema, type SearchSchema } from '$lib/zodValidation';
import * as Form from "$lib/components/ui/form"; import * as Form from "$lib/components/ui/form";
import { zodClient } from 'sveltekit-superforms/adapters';
import Input from '$components/ui/input/input.svelte';
import Checkbox from '$components/ui/checkbox/checkbox.svelte';
export let form: SuperValidated<SearchSchema>; export let data: SuperValidated<Infer<SearchSchema>>;
const form = superForm(data, {
validators: zodClient(search_schema),
});
const { form: formData } = form;
</script> </script>
<search> <search>
<Form.Root id="search-form" action="/search" method="GET" data-sveltekit-reload {form} schema={search_schema} let:config> <form id="search-form" action="/search" method="GET" data-sveltekit-reload>
<fieldset> <fieldset>
<Form.Item> <Form.Field {form} name="q">
<Form.Field {config} name="q"> <Form.Control let:attrs>
<Form.Label for="label">Search</Form.Label> <Form.Label>Search</Form.Label>
<Form.Input /> <Input {...attrs} bind:value={$formData.q} />
<Form.Validation /> </Form.Control>
</Form.Field> <Form.FieldErrors />
<Form.Field {config} name="skip"> </Form.Field>
<Form.Input type="hidden" /> <Form.Field {form} name="skip">
</Form.Field> <Form.Control let:attrs>
<Form.Field {config} name="limit"> <Input type="hidden" />
<Form.Input type="hidden" /> </Form.Control>
</Form.Field> </Form.Field>
</Form.Item> <Form.Field {form} name="limit">
<Form.Control let:attrs>
<Input type="hidden" />
</Form.Control>
</Form.Field>
</fieldset> </fieldset>
<fieldset> <fieldset>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<Form.Field {config} name="exact"> <Form.Field {form} name="exact">
<Form.Label>Exact Search</Form.Label> <Form.Control let:attrs>
<Form.Checkbox class="mt-0" /> <Form.Label>Exact Search</Form.Label>
<Checkbox {...attrs} class="mt-0" bind:checked={$formData.exact} />
<input name={attrs.name} value={$formData.exact} hidden />
</Form.Control>
</Form.Field> </Form.Field>
</div> </div>
</fieldset> </fieldset>
<Form.Button>Submit</Form.Button> <Form.Button>Submit</Form.Button>
</Form.Root> </form>
</search> </search>
<style lang="postcss"> <style lang="postcss">

View file

@ -1,16 +1,18 @@
<script lang="ts"> <script lang="ts">
import { zodClient } from 'sveltekit-superforms/adapters';
import { ConicGradient } from '@skeletonlabs/skeleton'; import { ConicGradient } from '@skeletonlabs/skeleton';
import type { ConicStop } from '@skeletonlabs/skeleton'; import type { ConicStop } from '@skeletonlabs/skeleton';
import { i } from "@inlang/sdk-js";
import { superForm } from 'sveltekit-superforms/client'; import { superForm } from 'sveltekit-superforms/client';
//import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte'; //import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
import { userSchema } from '$lib/config/zod-schemas'; import { userSchema } from '$lib/validations/zod-schemas';
import { AlertTriangle } from 'lucide-svelte'; import { AlertTriangle } from 'lucide-svelte';
import { i } from "@inlang/sdk-js"; import { signInSchema } from '$lib/validations/auth';
export let data; export let data;
const signInSchema = userSchema.pick({ email: true, password: true });
const { form, errors, enhance, delayed } = superForm(data.form, { const { form, errors, enhance, delayed } = superForm(data.form, {
taintedMessage: null, taintedMessage: null,
validators: signInSchema, validators: zodClient(signInSchema),
delayMs: 0 delayMs: 0
}); });
const conicStops: ConicStop[] = [ const conicStops: ConicStop[] = [

View file

@ -1,19 +1,13 @@
<script lang="ts"> <script lang="ts">
import { zodClient } from 'sveltekit-superforms/adapters';
import { superForm } from 'sveltekit-superforms/client'; import { superForm } from 'sveltekit-superforms/client';
import { userSchema } from '$lib/config/zod-schemas'; import { signUpSchema } from '$lib/validations/auth';
export let data; export let data;
const signUpSchema = userSchema.pick({ const { form, errors, enhance } = superForm(data.form, {
firstName: true,
lastName: true,
username: true,
email: true,
password: true
});
const { form, errors, enhance, delayed } = superForm(data.form, {
taintedMessage: null, taintedMessage: null,
validators: signUpSchema, validators: zodClient(signUpSchema),
delayMs: 0 delayMs: 0
}); });
// $: termsValue = $form.terms as Writable<boolean>; // $: termsValue = $form.terms as Writable<boolean>;

View file

@ -9,10 +9,7 @@
</script> </script>
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
class={cn( class={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...$$restProps} {...$$restProps}
> >
<slot /> <slot />

View file

@ -11,10 +11,7 @@
<AvatarPrimitive.Root <AvatarPrimitive.Root
{delayMs} {delayMs}
class={cn( class={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...$$restProps} {...$$restProps}
> >
<slot /> <slot />

View file

@ -9,5 +9,5 @@ export {
// //
Root as Avatar, Root as Avatar,
Image as AvatarImage, Image as AvatarImage,
Fallback as AvatarFallback Fallback as AvatarFallback,
}; };

View file

@ -12,19 +12,19 @@ const buttonVariants = tv({
"border border-input bg-background hover:bg-accent hover:text-accent-foreground", "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline" link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-10 px-4 py-2", default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3", sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8", lg: "h-11 rounded-md px-8",
icon: "h-10 w-10" icon: "h-10 w-10",
} },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
size: "default" size: "default",
} },
}); });
type Variant = VariantProps<typeof buttonVariants>["variant"]; type Variant = VariantProps<typeof buttonVariants>["variant"];
@ -45,5 +45,5 @@ export {
Root as Button, Root as Button,
type Props as ButtonProps, type Props as ButtonProps,
type Events as ButtonEvents, type Events as ButtonEvents,
buttonVariants buttonVariants,
}; };

View file

@ -9,10 +9,7 @@
</script> </script>
<div <div
class={cn( class={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...$$restProps} {...$$restProps}
> >
<slot /> <slot />

View file

@ -18,7 +18,7 @@ export {
Description as CardDescription, Description as CardDescription,
Footer as CardFooter, Footer as CardFooter,
Header as CardHeader, Header as CardHeader,
Title as CardTitle Title as CardTitle,
}; };
export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";

View file

@ -6,7 +6,7 @@
export let transition: $$Props["transition"] = slide; export let transition: $$Props["transition"] = slide;
export let transitionConfig: $$Props["transitionConfig"] = { export let transitionConfig: $$Props["transitionConfig"] = {
duration: 150 duration: 150,
}; };
</script> </script>

View file

@ -11,5 +11,5 @@ export {
// //
Root as Collapsible, Root as Collapsible,
Content as CollapsibleContent, Content as CollapsibleContent,
Trigger as CollapsibleTrigger Trigger as CollapsibleTrigger,
}; };

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import { cn } from "$lib/utils"; import { cn } from "$lib/utils";
import { Check } from "lucide-svelte";
type $$Props = DropdownMenuPrimitive.CheckboxItemProps; type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents; type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
@ -14,7 +14,7 @@
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
bind:checked bind:checked
class={cn( class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className className
)} )}
{...$$restProps} {...$$restProps}

View file

@ -6,6 +6,7 @@
type $$Events = DropdownMenuPrimitive.ContentEvents; type $$Events = DropdownMenuPrimitive.ContentEvents;
let className: $$Props["class"] = undefined; let className: $$Props["class"] = undefined;
export let sideOffset: $$Props["sideOffset"] = 4;
export let transition: $$Props["transition"] = flyAndScale; export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined; export let transitionConfig: $$Props["transitionConfig"] = undefined;
export { className as class }; export { className as class };
@ -14,6 +15,7 @@
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
{transition} {transition}
{transitionConfig} {transitionConfig}
{sideOffset}
class={cn( class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none", "z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none",
className className

View file

@ -14,7 +14,7 @@
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
class={cn( class={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
inset && "pl-8", inset && "pl-8",
className className
)} )}

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils"; import { cn } from "$lib/utils";
import { Circle } from "lucide-svelte";
type $$Props = DropdownMenuPrimitive.RadioItemProps; type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents; type $$Events = DropdownMenuPrimitive.RadioItemEvents;
@ -13,7 +13,7 @@
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
class={cn( class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className className
)} )}
{value} {value}

View file

@ -8,9 +8,6 @@
export { className as class }; export { className as class };
</script> </script>
<span <span class={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...$$restProps}>
class={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...$$restProps}
>
<slot /> <slot />
</span> </span>

View file

@ -9,7 +9,7 @@
export let transition: $$Props["transition"] = flyAndScale; export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = { export let transitionConfig: $$Props["transitionConfig"] = {
x: -10, x: -10,
y: 0 y: 0,
}; };
export { className as class }; export { className as class };
</script> </script>

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils"; import { cn } from "$lib/utils";
import { ChevronRight } from "lucide-svelte";
type $$Props = DropdownMenuPrimitive.SubTriggerProps & { type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean; inset?: boolean;
@ -15,7 +15,7 @@
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
class={cn( class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent", "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground",
inset && "pl-8", inset && "pl-8",
className className
)} )}

View file

@ -44,5 +44,5 @@ export {
RadioGroup as DropdownMenuRadioGroup, RadioGroup as DropdownMenuRadioGroup,
SubContent as DropdownMenuSubContent, SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger, SubTrigger as DropdownMenuSubTrigger,
CheckboxItem as DropdownMenuCheckboxItem CheckboxItem as DropdownMenuCheckboxItem,
}; };

View file

@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import * as Button from "$lib/components/ui/button"; import * as Button from "$lib/components/ui/button";
type $$Props = Button.Props; type $$Props = Button.Props;
type $$Events = Button.Events;
</script> </script>
<Button.Root type="submit" {...$$restProps} on:click on:keydown> <Button.Root type="submit" {...$$restProps}>
<slot /> <slot />
</Button.Root> </Button.Root>

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Form as FormPrimitive } from "formsnap"; import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils"; import { cn } from "$lib/utils";
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
@ -8,6 +8,10 @@
export { className as class }; export { className as class };
</script> </script>
<FormPrimitive.Description class={cn("text-sm text-muted-foreground", className)} {...$$restProps}> <FormPrimitive.Description
<slot /> class={cn("text-sm text-muted-foreground", className)}
{...$$restProps}
let:descriptionAttrs
>
<slot {descriptionAttrs} />
</FormPrimitive.Description> </FormPrimitive.Description>

View file

@ -0,0 +1,26 @@
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPathLeaves, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = unknown;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils";
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.ElementField {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
</FormPrimitive.ElementField>

View file

@ -0,0 +1,26 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils";
type $$Props = FormPrimitive.FieldErrorsProps & {
errorClasses?: string | undefined | null;
};
let className: $$Props["class"] = undefined;
export { className as class };
export let errorClasses: $$Props["class"] = undefined;
</script>
<FormPrimitive.FieldErrors
class={cn("text-sm font-medium text-destructive", className)}
{...$$restProps}
let:errors
let:fieldErrorsAttrs
let:errorAttrs
>
<slot {errors} {fieldErrorsAttrs} {errorAttrs}>
{#each errors as error}
<div {...errorAttrs} class={cn(errorClasses)}>{error}</div>
{/each}
</slot>
</FormPrimitive.FieldErrors>

View file

@ -0,0 +1,26 @@
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = unknown;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils";
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Field {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
</FormPrimitive.Field>

View file

@ -0,0 +1,31 @@
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = unknown;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils";
type $$Props = FormPrimitive.FieldsetProps<T, U>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Fieldset
{form}
{name}
let:constraints
let:errors
let:tainted
let:value
class={cn("space-y-2", className)}
>
<slot {constraints} {errors} {tainted} {value} />
</FormPrimitive.Fieldset>

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { Label as LabelPrimitive } from "bits-ui"; import type { Label as LabelPrimitive } from "bits-ui";
import { getFormField } from "formsnap"; import { getFormControl } from "formsnap";
import { cn } from "$lib/utils"; import { cn } from "$lib/utils";
import { Label } from "$lib/components/ui/label"; import { Label } from "$lib/components/ui/label";
@ -9,9 +9,9 @@
let className: $$Props["class"] = undefined; let className: $$Props["class"] = undefined;
export { className as class }; export { className as class };
const { errors, ids } = getFormField(); const { labelAttrs } = getFormControl();
</script> </script>
<Label for={$ids.input} class={cn($errors && "text-destructive", className)} {...$$restProps}> <Label {...$labelAttrs} class={cn("data-[fs-error]:text-destructive", className)} {...$$restProps}>
<slot /> <slot {labelAttrs} />
</Label> </Label>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils";
type $$Props = FormPrimitive.LegendProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Legend
{...$$restProps}
class={cn("text-sm font-medium leading-none data-[fs-error]:text-destructive", className)}
let:legendAttrs
>
<slot {legendAttrs} />
</FormPrimitive.Legend>

View file

@ -1,82 +1,33 @@
import { Form as FormPrimitive, getFormField } from "formsnap"; import * as FormPrimitive from "formsnap";
import * as RadioGroupComp from "$lib/components/ui/radio-group";
import * as SelectComp from "$lib/components/ui/select";
import type { Writable } from "svelte/store";
import Item from "./form-item.svelte";
import Input from "./form-input.svelte";
import Textarea from "./form-textarea.svelte";
import Description from "./form-description.svelte"; import Description from "./form-description.svelte";
import Label from "./form-label.svelte"; import Label from "./form-label.svelte";
import Validation from "./form-validation.svelte"; import FieldErrors from "./form-field-errors.svelte";
import Checkbox from "./form-checkbox.svelte"; import Field from "./form-field.svelte";
import Switch from "./form-switch.svelte"; import Fieldset from "./form-fieldset.svelte";
import NativeSelect from "./form-native-select.svelte"; import Legend from "./form-legend.svelte";
import RadioGroup from "./form-radio-group.svelte"; import ElementField from "./form-element-field.svelte";
import Select from "./form-select.svelte";
import SelectTrigger from "./form-select-trigger.svelte";
import Button from "./form-button.svelte"; import Button from "./form-button.svelte";
const Root = FormPrimitive.Root;
const Field = FormPrimitive.Field;
const Control = FormPrimitive.Control; const Control = FormPrimitive.Control;
const RadioItem = RadioGroupComp.Item;
const NativeRadio = FormPrimitive.Radio;
const SelectContent = SelectComp.Content;
const SelectLabel = SelectComp.Label;
const SelectGroup = SelectComp.Group;
const SelectItem = SelectComp.Item;
const SelectSeparator = SelectComp.Separator;
export type TextareaGetFormField = Omit<ReturnType<typeof getFormField>, "value"> & {
value: Writable<string>;
};
export { export {
Root,
Field, Field,
Control, Control,
Item,
Input,
Label, Label,
Button, Button,
Switch, FieldErrors,
Select,
Checkbox,
Textarea,
Validation,
RadioGroup,
RadioItem,
Description, Description,
SelectContent, Fieldset,
SelectLabel, Legend,
SelectGroup, ElementField,
SelectItem,
SelectSeparator,
SelectTrigger,
NativeSelect,
NativeRadio,
// //
Root as Form,
Field as FormField, Field as FormField,
Control as FormControl, Control as FormControl,
Item as FormItem,
Input as FormInput,
Textarea as FormTextarea,
Description as FormDescription, Description as FormDescription,
Label as FormLabel, Label as FormLabel,
Validation as FormValidation, FieldErrors as FormFieldErrors,
NativeSelect as FormNativeSelect, Fieldset as FormFieldset,
NativeRadio as FormNativeRadio, Legend as FormLegend,
Checkbox as FormCheckbox, ElementField as FormElementField,
Switch as FormSwitch, Button as FormButton,
RadioGroup as FormRadioGroup,
RadioItem as FormRadioItem,
Select as FormSelect,
SelectContent as FormSelectContent,
SelectLabel as FormSelectLabel,
SelectGroup as FormSelectGroup,
SelectItem as FormSelectItem,
SelectSeparator as FormSelectSeparator,
SelectTrigger as FormSelectTrigger,
Button as FormButton
}; };

View file

@ -3,5 +3,5 @@ import Root from "./label.svelte";
export { export {
Root, Root,
// //
Root as Label Root as Label,
}; };

View file

@ -20,5 +20,5 @@ export {
Link as PaginationLink, Link as PaginationLink,
PrevButton as PaginationPrevButton, PrevButton as PaginationPrevButton,
NextButton as PaginationNextButton, NextButton as PaginationNextButton,
Ellipsis as PaginationEllipsis Ellipsis as PaginationEllipsis,
}; };

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import MoreHorizontal from "lucide-svelte/icons/more-horizontal";
import { cn } from "$lib/utils"; import { cn } from "$lib/utils";
import { MoreHorizontal } from "lucide-svelte";
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLSpanElement>; type $$Props = HTMLAttributes<HTMLSpanElement>;

View file

@ -23,7 +23,7 @@
class={cn( class={cn(
buttonVariants({ buttonVariants({
variant: isActive ? "outline" : "ghost", variant: isActive ? "outline" : "ghost",
size size,
}), }),
className className
)} )}

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { Pagination as PaginationPrimitive } from "bits-ui"; import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
import { cn } from "$lib/utils"; import { cn } from "$lib/utils";
import { ChevronRight } from "lucide-svelte";
type $$Props = PaginationPrimitive.NextButtonProps; type $$Props = PaginationPrimitive.NextButtonProps;
type $$Events = PaginationPrimitive.NextButtonEvents; type $$Events = PaginationPrimitive.NextButtonEvents;

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { Pagination as PaginationPrimitive } from "bits-ui"; import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
import { cn } from "$lib/utils"; import { cn } from "$lib/utils";
import { ChevronLeft } from "lucide-svelte";
type $$Props = PaginationPrimitive.PrevButtonProps; type $$Props = PaginationPrimitive.PrevButtonProps;
type $$Events = PaginationPrimitive.PrevButtonEvents; type $$Events = PaginationPrimitive.PrevButtonEvents;

View file

@ -28,7 +28,7 @@
asChild asChild
{...$$restProps} {...$$restProps}
> >
<nav {...builder} class={cn("mx-auto flex flex-col w-full items-center", className)}> <nav {...builder} class={cn("mx-auto flex w-full flex-col items-center", className)}>
<slot {pages} {range} {currentPage} /> <slot {pages} {range} {currentPage} />
</nav> </nav>
</PaginationPrimitive.Root> </PaginationPrimitive.Root>

View file

@ -1,120 +0,0 @@
import { z } from 'zod';
export type ListGame = {
id: string;
game_name: string;
game_id: string;
collection_id: string;
wishlist_id: string;
times_played: number;
thumb_url: string | null;
in_collection: boolean;
in_wishlist: boolean;
};
export const modifyListGameSchema = z.object({
id: z.string()
});
export type ModifyListGame = typeof modifyListGameSchema;
export const userSchema = z.object({
firstName: z.string().trim().optional(),
lastName: z.string().trim().optional(),
email: z.string().email({ message: 'Please enter a valid email address' }).optional(),
username: z
.string()
.trim()
.min(3, { message: 'Username must be at least 3 characters' })
.max(50, { message: 'Username must be less than 50 characters' }),
password: z
.string({ required_error: 'Password is required' })
.trim()
.min(8, { message: 'Password must be at least 8 characters' })
.max(128, { message: 'Password must be less than 128 characters' }),
confirm_password: z
.string({ required_error: 'Confirm Password is required' })
.trim()
.min(8, { message: 'Confirm Password must be at least 8 characters' }),
role: z.enum(['USER', 'ADMIN'], { required_error: 'You must have a role' }).default('USER'),
verified: z.boolean().default(false),
token: z.string().optional(),
receiveEmail: z.boolean().default(false),
createdAt: z.date().optional(),
updatedAt: z.date().optional()
});
export const signUpSchema = userSchema
.pick({
firstName: true,
lastName: true,
email: true,
username: true,
password: true,
confirm_password: true,
terms: true
})
.superRefine(({ confirm_password, password }, ctx) => {
if (confirm_password !== password) {
// ctx.addIssue({
// code: 'custom',
// message: 'Password and Confirm Password must match',
// path: ['password']
// });
ctx.addIssue({
code: 'custom',
message: ' Password and Confirm Password must match',
path: ['confirm_password']
});
}
});
export const signInSchema = userSchema.pick({
username: true,
password: true
});
export const updateUserPasswordSchema = userSchema
.pick({ password: true, confirm_password: true })
.superRefine(({ confirm_password, password }, ctx) => {
if (confirm_password !== password) {
ctx.addIssue({
code: 'custom',
message: 'Password and Confirm Password must match',
path: ['password']
});
ctx.addIssue({
code: 'custom',
message: 'Password and Confirm Password must match',
path: ['confirm_password']
});
}
});
export const changeUserPasswordSchema = z
.object({
current_password: z.string({ required_error: 'Current Password is required' }),
password: z
.string({ required_error: 'Password is required' })
.trim()
.min(8, { message: 'Password must be at least 8 characters' })
.max(128, { message: 'Password must be less than 128 characters' }),
confirm_password: z
.string({ required_error: 'Confirm Password is required' })
.trim()
.min(8, { message: 'Confirm Password must be at least 8 characters' })
})
.superRefine(({ confirm_password, password }, ctx) => {
if (confirm_password !== password) {
ctx.addIssue({
code: 'custom',
message: 'Password and Confirm Password must match',
path: ['password']
});
ctx.addIssue({
code: 'custom',
message: 'Password and Confirm Password must match',
path: ['confirm_password']
});
}
});

30
src/lib/drizzle.ts Normal file
View file

@ -0,0 +1,30 @@
import { drizzle } from 'drizzle-orm/node-postgres';
import pg from 'pg';
import {
DATABASE_USER,
DATABASE_PASSWORD,
DATABASE_HOST,
DATABASE_DB,
DATABASE_PORT,
} from '$env/static/private';
import * as schema from '../schema';
// create the connection
const pool = new pg.Pool({
user: DATABASE_USER,
password: DATABASE_PASSWORD,
host: DATABASE_HOST,
port: Number(DATABASE_PORT).valueOf(),
database: DATABASE_DB,
ssl: true,
});
// user: DATABASE_USER,
// password: DATABASE_PASSWORD,
// host: DATABASE_HOST,
// port: Number(DATABASE_PORT).valueOf(),
// database: DATABASE_DB
const db = drizzle(pool, { schema });
export default db;

1
src/lib/flashMessages.ts Normal file
View file

@ -0,0 +1 @@
export const notSignedInMessage = { type: 'error', message: 'You are not signed in' } as const;

View file

@ -5,7 +5,7 @@ import { dev } from '$app/environment';
import { read } from '$app/server'; import { read } from '$app/server';
// we use a Vite plugin to turn this import into the result of fs.readFileSync during build // we use a Vite plugin to turn this import into the result of fs.readFileSync during build
import firaSansSemiBold from '$lib/fonts/FiraSans-SemiBold.ttf'; import firaSansSemiBold from '$lib/fonts/FiraSans-Bold.ttf';
const fontData = read(firaSansSemiBold).arrayBuffer(); const fontData = read(firaSansSemiBold).arrayBuffer();

View file

@ -0,0 +1,19 @@
import db from "$lib/drizzle";
import { eq } from "drizzle-orm";
import { password_reset_tokens } from "../../schema";
import { generateId } from "lucia";
import { TimeSpan, createDate } from "oslo";
export async function createPasswordResetToken(userId: string): Promise<string> {
// optionally invalidate all existing tokens
await db.delete(password_reset_tokens).where(eq(password_reset_tokens.user_id, userId));
const tokenId = generateId(40);
await db
.insert(password_reset_tokens)
.values({
id: tokenId,
user_id: userId,
expires_at: createDate(new TimeSpan(2, "h"))
});
return tokenId;
}

View file

@ -1,10 +1,11 @@
// lib/server/lucia.ts // lib/server/lucia.ts
import { Lucia, TimeSpan } from 'lucia'; import { Lucia, TimeSpan } from 'lucia';
import { PrismaAdapter } from '@lucia-auth/adapter-prisma'; import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import { dev } from '$app/environment'; import { dev } from '$app/environment';
import prisma_client from '$lib/prisma'; import db from '$lib/drizzle';
import { sessions, users } from '../../schema';
const adapter = new PrismaAdapter(prisma_client.session, prisma_client.user); const adapter = new DrizzlePostgreSQLAdapter(db, sessions, users);
export const lucia = new Lucia(adapter, { export const lucia = new Lucia(adapter, {
getSessionAttributes: (attributes) => { getSessionAttributes: (attributes) => {

View file

@ -26,6 +26,7 @@
--input: 20 5.9% 90%; --input: 20 5.9% 90%;
--ring: 20 14.3% 4.1%; --ring: 20 14.3% 4.1%;
--radius: 0.5rem; --radius: 0.5rem;
--fg: #2c3e50;
} }
.dark { .dark {
--background: 20 14.3% 4.1%; --background: 20 14.3% 4.1%;
@ -47,6 +48,7 @@
--border: 12 6.5% 15.1%; --border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%; --input: 12 6.5% 15.1%;
--ring: 35.5 91.7% 32.9%; --ring: 35.5 91.7% 32.9%;
--fg: #ffff;
} }
} }

View file

@ -0,0 +1,3 @@
export const normalizeEmail = (email: string): string => {
return decodeURIComponent(email).toLowerCase().trim();
};

View file

@ -0,0 +1,77 @@
import kebabCase from 'just-kebab-case';
import db from '$lib/drizzle';
import { externalIds, type Mechanics, type Categories, categories, categoriesToExternalIds } from '../../../schema';
import { eq } from 'drizzle-orm';
import { error } from '@sveltejs/kit';
import { PUBLIC_SITE_URL } from '$env/static/public';
export async function createCategory(locals: App.Locals, category: Categories, externalId: string) {
if (!category || !externalId || externalId === '') {
error(400, 'Invalid Request');
}
try {
const dbExternalId = await db.query.externalIds.findFirst({
where: eq(externalIds.externalId, externalId)
});
if (dbExternalId) {
const foundCategory = await db
.select({
id: categories.id,
name: categories.name,
slug: categories.slug
})
.from(categories)
.leftJoin(categoriesToExternalIds, eq(categoriesToExternalIds.externalId, externalId));
console.log('Mechanic already exists', foundCategory);
if (foundCategory.length > 0) {
console.log('Mechanic name', foundCategory[0].name);
return new Response('Mechanic already exists', {
headers: {
'Content-Type': 'application/json',
Location: `${PUBLIC_SITE_URL}/api/mechanic/${foundCategory[0].id}`
},
status: 409
});
}
}
let dbCategory: Mechanics[] = [];
console.log('Creating category', JSON.stringify(category, null, 2));
await db.transaction(async (transaction) => {
dbCategory = await transaction
.insert(categories)
.values({
name: category.name,
slug: kebabCase(category.name || category.slug || '')
})
.returning();
const dbExternalIds = await transaction
.insert(externalIds)
.values({
externalId,
type: 'category'
})
.returning({ id: externalIds.id });
await transaction.insert(categoriesToExternalIds).values({
categoryId: dbCategory[0].id,
externalId: dbExternalIds[0].id
});
});
if (dbCategory.length === 0) {
return new Response('Could not create category', {
status: 500
});
}
console.log('Created category', JSON.stringify(dbCategory[0], null, 2));
return new Response(JSON.stringify(dbCategory[0]), {
status: 201,
});
} catch (e) {
console.error(e);
throw new Error('Something went wrong creating Category');
}
}

View file

@ -0,0 +1,178 @@
import { error } from '@sveltejs/kit';
import { and, eq } from 'drizzle-orm';
import db from '$lib/drizzle';
import { type Expansions, expansions } from '../../../schema';
import { PUBLIC_SITE_URL } from '$env/static/public';
export async function createExpansion(locals: App.Locals, expansion: Expansions) {
if (!expansion || expansion?.base_game_id === '' || expansion?.game_id === '') {
error(400, 'Invalid Request');
}
try {
const foundExpansion = await db.query.expansions
.findFirst({
where: and(eq(expansions.base_game_id, expansion.base_game_id), eq(expansions.game_id, expansion.game_id)),
columns: {
id: true,
game_id: true,
base_game_id: true
}
});
console.log('Expansion already exists', foundExpansion);
if (foundExpansion) {
console.log('Expansion Game ID', foundExpansion.game_id);
return new Response('Expansion already exists', {
headers: {
'Content-Type': 'application/json',
Location: `${PUBLIC_SITE_URL}/api/game/${foundExpansion.game_id}`
},
status: 409
});
}
console.log('Creating expansion', JSON.stringify(expansion, null, 2));
const dbExpansion = await db
.insert(expansions)
.values({
base_game_id: expansion.base_game_id,
game_id: expansion.game_id,
})
.returning();
if (dbExpansion.length === 0) {
return new Response('Could not create expansion', {
status: 500
});
}
console.log('Created expansion', JSON.stringify(dbExpansion[0], null, 2));
return new Response(JSON.stringify(dbExpansion[0]), {
status: 201,
});
} catch (e) {
console.error(e);
throw new Error('Something went wrong creating Expansion');
}
}
// export async function createExpansion(
// locals: App.Locals,
// game: Game,
// externalExpansion: BggLinkDto,
// gameIsExpansion: boolean,
// eventFetch: Function
// ) {
// try {
// let dbExpansionGame = await prisma.game.findUnique({
// where: {
// external_id: externalExpansion.id
// }
// });
// if (!dbExpansionGame) {
// const externalGameResponse = await eventFetch(
// `/api/external/game/${externalExpansion.id}?simplified=true`
// );
// if (externalGameResponse.ok) {
// const externalGame = await externalGameResponse.json();
// console.log('externalGame', externalGame);
// const boredGame = mapAPIGameToBoredGame(externalGame);
// dbExpansionGame = await createOrUpdateGameMinimal(locals, boredGame);
// } else {
// throw new Error(
// `${gameIsExpansion ? 'Base game' : 'Expansion game'} not found and failed to create.`
// );
// }
// }
// let dbExpansion;
// let baseGameId;
// let gameId;
// if (gameIsExpansion) {
// console.log(
// 'External expansion is expansion. Looking for base game',
// JSON.stringify(game, null, 2)
// );
// dbExpansion = await prisma.expansion.findFirst({
// where: {
// game_id: dbExpansionGame.id
// },
// select: {
// id: true,
// base_game_id: true,
// game_id: true
// }
// });
// baseGameId = game.id;
// gameId = dbExpansionGame.id;
// } else {
// console.log(
// 'External Expansion is base game. Looking for expansion',
// JSON.stringify(game, null, 2)
// );
// dbExpansion = await prisma.expansion.findFirst({
// where: {
// base_game_id: dbExpansionGame.id
// },
// select: {
// id: true,
// base_game_id: true,
// game_id: true
// }
// });
// baseGameId = dbExpansionGame.id;
// gameId = game.id;
// }
// if (dbExpansion) {
// console.log('Expansion already exists', JSON.stringify(dbExpansion, null, 2));
// return dbExpansion;
// }
// console.log(`Creating expansion. baseGameId: ${baseGameId}, gameId: ${gameId}`);
// const expansion = await prisma.expansion.create({
// data: {
// base_game_id: baseGameId,
// game_id: gameId
// }
// });
// console.log('Created expansion', JSON.stringify(expansion, null, 2));
// if (gameIsExpansion) {
// console.log('Connecting current game to expansion');
// await prisma.game.update({
// where: {
// id: gameId
// },
// data: {
// expansions: {
// connect: {
// id: expansion.id
// }
// }
// }
// });
// } else {
// console.log('Connecting current game to base game');
// await prisma.game.update({
// where: {
// id: baseGameId
// },
// data: {
// expansions: {
// connect: {
// id: expansion.id
// }
// }
// }
// });
// }
// return expansion;
// } catch (e) {
// console.error(e);
// throw new Error('Something went wrong creating Expansion');
// }
// }

View file

@ -0,0 +1,439 @@
import kebabCase from 'just-kebab-case';
import db from '$lib/drizzle';
import { externalIds, gamesToExternalIds, type Games, games } from '../../../schema';
import { eq } from 'drizzle-orm';
import { error } from '@sveltejs/kit';
import { PUBLIC_SITE_URL } from '$env/static/public';
export async function getGame(locals: App.Locals, id: string) {
if (!id || id === '') {
error(400, 'Invalid Request');
}
try {
return await db.query.games.findFirst({
where: eq(games.id, id)
});
} catch (e) {
console.error(e);
return new Response('Could not get games', {
status: 500
});
}
}
export async function createGame(locals: App.Locals, game: Games, externalId: string) {
if (!game || !externalId || externalId === '') {
error(400, 'Invalid Request');
}
try {
const dbExternalId = await db.query.externalIds.findFirst({
where: eq(externalIds.externalId, externalId)
});
if (dbExternalId) {
const foundGame = await db
.select({
id: games.id,
name: games.name,
slug: games.slug
})
.from(games)
.leftJoin(gamesToExternalIds, eq(gamesToExternalIds.externalId, externalId));
console.log('Game already exists', foundGame);
if (foundGame.length > 0) {
console.log('Game name', foundGame[0].name);
return new Response('Game already exists', {
headers: {
'Content-Type': 'application/json',
Location: `${PUBLIC_SITE_URL}/api/game/${foundGame[0].id}`
},
status: 409
});
}
}
let dbGames: Games[] = [];
console.log('Creating game', JSON.stringify(game, null, 2));
await db.transaction(async (transaction) => {
dbGames = await transaction
.insert(games)
.values({
name: game.name,
slug: kebabCase(game.name || game.slug || ''),
description: game.description,
year_published: game.year_published,
url: game.url,
image_url: game.image_url,
thumb_url: game.thumb_url,
min_age: game.min_age,
min_players: game.min_players,
max_players: game.max_players,
min_playtime: game.min_playtime,
max_playtime: game.max_playtime
})
.returning();
const dbExternalIds = await transaction
.insert(externalIds)
.values({
externalId,
type: 'game'
})
.returning({ id: externalIds.id });
await transaction.insert(gamesToExternalIds).values({
gameId: dbGames[0].id,
externalId: dbExternalIds[0].id
});
});
if (dbGames.length === 0) {
return new Response('Could not create game', {
status: 500
});
}
console.log('Created game', JSON.stringify(dbGames[0], null, 2));
return new Response(JSON.stringify(dbGames[0]), {
status: 201,
});
} catch (e) {
console.error(e);
throw new Error('Something went wrong creating Game');
}
}
export async function createOrUpdateGameMinimal(locals: App.Locals, game: Games, externalId: string) {
if (!game || !externalId || externalId === '') {
error(400, 'Invalid Request');
}
console.log('Creating or updating minimal game data', JSON.stringify(game, null, 2));
const externalUrl = `https://boardgamegeek.com/boardgame/${externalId}`;
try {
let dbGames: Games[] = [];
console.log('Creating game', JSON.stringify(game, null, 2));
await db.transaction(async (transaction) => {
dbGames = await transaction
.insert(games)
.values({
name: game.name,
slug: kebabCase(game.name || game.slug || ''),
description: game.description,
year_published: game.year_published,
url: externalUrl,
image_url: game.image_url,
thumb_url: game.thumb_url,
min_age: game.min_age,
min_players: game.min_players,
max_players: game.max_players,
min_playtime: game.min_playtime,
max_playtime: game.max_playtime
})
.onConflictDoUpdate({
target: games.id,
set: {
name: game.name,
slug: kebabCase(game.name || game.slug || ''),
description: game.description,
year_published: game.year_published,
url: externalUrl,
image_url: game.image_url,
thumb_url: game.thumb_url,
min_age: game.min_age,
min_players: game.min_players,
max_players: game.max_players,
min_playtime: game.min_playtime,
max_playtime: game.max_playtime
}
})
.returning();
const dbExternalIds = await transaction
.insert(externalIds)
.values({
externalId,
type: 'game'
})
.onConflictDoNothing()
.returning({ id: externalIds.id });
await transaction.insert(gamesToExternalIds).values({
gameId: dbGames[0].id,
externalId: dbExternalIds[0].id
}).onConflictDoNothing();
});
if (dbGames.length === 0) {
return new Response('Could not create game', {
status: 500
});
}
console.log('Created game', JSON.stringify(dbGames[0], null, 2));
return new Response(JSON.stringify(dbGames[0]), {
status: 201,
});
} catch (e) {
console.error(e);
throw new Error('Something went wrong creating Game');
}
}
export async function createOrUpdateGame(locals: App.Locals, game: Games, externalId: string) {
if (!game || !externalId || externalId === '') {
error(400, 'Invalid Request');
}
try {
const externalUrl = `https://boardgamegeek.com/boardgame/${externalId}`;
const dbExternalId = await db.query.externalIds.findFirst({
where: eq(externalIds.externalId, externalId)
});
if (dbExternalId) {
const foundGame = await db
.select({
id: games.id,
name: games.name,
slug: games.slug
})
.from(games)
.leftJoin(gamesToExternalIds, eq(gamesToExternalIds.externalId, externalId));
console.log('Game already exists', foundGame);
if (foundGame.length > 0) {
console.log('Game name', foundGame[0].name);
return new Response('Game already exists', {
headers: {
'Content-Type': 'application/json',
Location: `${PUBLIC_SITE_URL}/api/game/${foundGame[0].id}`
},
status: 409
});
}
}
let dbGames: Games[] = [];
console.log('Creating game', JSON.stringify(game, null, 2));
await db.transaction(async (transaction) => {
dbGames = await transaction
.insert(games)
.values({
name: game.name,
slug: kebabCase(game.name || game.slug || ''),
description: game.description,
year_published: game.year_published,
url: game.url,
image_url: game.image_url,
thumb_url: game.thumb_url,
min_age: game.min_age,
min_players: game.min_players,
max_players: game.max_players,
min_playtime: game.min_playtime,
max_playtime: game.max_playtime
})
.onConflictDoUpdate({
target: games.id,
set: {
name: game.name,
slug: kebabCase(game.name || game.slug || ''),
description: game.description,
year_published: game.year_published,
url: externalUrl,
image_url: game.image_url,
thumb_url: game.thumb_url,
min_age: game.min_age,
min_players: game.min_players,
max_players: game.max_players,
min_playtime: game.min_playtime,
max_playtime: game.max_playtime
}
})
.returning();
const dbExternalIds = await transaction
.insert(externalIds)
.values({
externalId,
type: 'game'
})
.onConflictDoNothing()
.returning({ id: externalIds.id });
await transaction.insert(gamesToExternalIds).values({
gameId: dbGames[0].id,
externalId: dbExternalIds[0].id
}).onConflictDoNothing();
});
if (dbGames.length === 0) {
return new Response('Could not create game', {
status: 500
});
}
console.log('Created game', JSON.stringify(dbGames[0], null, 2));
return new Response(JSON.stringify(dbGames[0]), {
status: 201,
});
} catch (e) {
console.error(e);
throw new Error('Something went wrong creating Game');
}
}
export async function updateGame(locals: App.Locals, game: Games, id: string) {
if (!game || !id || id === '') {
error(400, 'Invalid Request');
}
try {
const dbGame = await db
.update(games)
.set({
name: game.name,
slug: kebabCase(game.name || game.slug || ''),
description: game.description,
year_published: game.year_published,
url: game.url,
image_url: game.image_url,
thumb_url: game.thumb_url,
min_age: game.min_age,
min_players: game.min_players,
max_players: game.max_players,
min_playtime: game.min_playtime,
max_playtime: game.max_playtime
})
.where(eq(games.id, id))
.returning();
return new Response(JSON.stringify(dbGame[0]), {
headers: {
'Content-Type': 'application/json'
}
});
} catch (e) {
console.error(e);
return new Response('Could not get publishers', {
status: 500
});
}
}
// console.log('Creating or updating game', JSON.stringify(game, null, 2));
// const categoryIds = game.categories;
// const mechanicIds = game.mechanics;
// const publisherIds = game.publishers;
// const designerIds = game.designers;
// const artistIds = game.artists;
// // const expansionIds = game.expansions;
// const externalUrl = `https://boardgamegeek.com/boardgame/${game.external_id}`;
// console.log('categoryIds', categoryIds);
// console.log('mechanicIds', mechanicIds);
// await db.transaction(async (transaction) => {
// const dbGame = await db.transaction(async (transaction) => {
// transaction.insert(games).values({
// name: game.name,
// slug: kebabCase(game.name || ''),
// description: game.description,
// external_id: game.external_id,
// url: externalUrl,
// thumb_url: game.thumb_url,
// image_url: game.image_url,
// min_age: game.min_age || 0,
// min_players: game.min_players || 0,
// max_players: game.max_players || 0,
// min_playtime: game.min_playtime || 0,
// max_playtime: game.max_playtime || 0,
// year_published: game.year_published || 0,
// last_sync_at: new Date(),
// }).onConflictDoUpdate({
// target: games.id, set: {
// name: game.name,
// slug: kebabCase(game.name),
// description: game.description,
// external_id: game.external_id,
// url: externalUrl,
// thumb_url: game.thumb_url,
// image_url: game.image_url,
// min_age: game.min_age || 0,
// min_players: game.min_players || 0,
// max_players: game.max_players || 0,
// min_playtime: game.min_playtime || 0,
// max_playtime: game.max_playtime || 0,
// year_published: game.year_published || 0,
// last_sync_at: new Date(),
// }
// }).returning();
// });
// // TODO: Connect to everything else
// });
// await db.insert(games).values({
// include: {
// mechanics: true,
// publishers: true,
// designers: true,
// artists: true,
// expansions: true
// },
// where: {
// external_id: game.external_id
// },
// create: {
// name: game.name,
// slug: kebabCase(game.name),
// description: game.description,
// external_id: game.external_id,
// url: externalUrl,
// thumb_url: game.thumb_url,
// image_url: game.image_url,
// min_age: game.min_age || 0,
// min_players: game.min_players || 0,
// max_players: game.max_players || 0,
// min_playtime: game.min_playtime || 0,
// max_playtime: game.max_playtime || 0,
// year_published: game.year_published || 0,
// last_sync_at: new Date(),
// categories: {
// connect: categoryIds
// },
// mechanics: {
// connect: mechanicIds
// },
// publishers: {
// connect: publisherIds
// },
// designers: {
// connect: designerIds
// },
// artists: {
// connect: artistIds
// }
// },
// update: {
// name: game.name,
// slug: kebabCase(game.name),
// description: game.description,
// external_id: game.external_id,
// url: externalUrl,
// thumb_url: game.thumb_url,
// image_url: game.image_url,
// min_age: game.min_age || 0,
// min_players: game.min_players || 0,
// max_players: game.max_players || 0,
// min_playtime: game.min_playtime || 0,
// max_playtime: game.max_playtime || 0,
// year_published: game.year_published || 0,
// last_sync_at: new Date(),
// categories: {
// connect: categoryIds
// },
// mechanics: {
// connect: mechanicIds
// },
// publishers: {
// connect: publisherIds
// },
// designers: {
// connect: designerIds
// },
// artists: {
// connect: artistIds
// }
// }
// });

View file

@ -0,0 +1,77 @@
import kebabCase from 'just-kebab-case';
import db from '$lib/drizzle';
import { externalIds, mechanics, mechanicsToExternalIds, type Mechanics } from '../../../schema';
import { eq } from 'drizzle-orm';
import { error } from '@sveltejs/kit';
import { PUBLIC_SITE_URL } from '$env/static/public';
export async function createMechanic(locals: App.Locals, mechanic: Mechanics, externalId: string) {
if (!mechanic || !externalId || externalId === '') {
error(400, 'Invalid Request');
}
try {
const dbExternalId = await db.query.externalIds.findFirst({
where: eq(externalIds.externalId, externalId)
});
if (dbExternalId) {
const foundMechanic = await db
.select({
id: mechanics.id,
name: mechanics.name,
slug: mechanics.slug
})
.from(mechanics)
.leftJoin(mechanicsToExternalIds, eq(mechanicsToExternalIds.externalId, externalId));
console.log('Mechanic already exists', foundMechanic);
if (foundMechanic.length > 0) {
console.log('Mechanic name', foundMechanic[0].name);
return new Response('Mechanic already exists', {
headers: {
'Content-Type': 'application/json',
Location: `${PUBLIC_SITE_URL}/api/mechanic/${foundMechanic[0].id}`
},
status: 409
});
}
}
let dbMechanics: Mechanics[] = [];
console.log('Creating mechanic', JSON.stringify(mechanic, null, 2));
await db.transaction(async (transaction) => {
dbMechanics = await transaction
.insert(mechanics)
.values({
name: mechanic.name,
slug: kebabCase(mechanic.name || mechanic.slug || '')
})
.returning();
const dbExternalIds = await transaction
.insert(externalIds)
.values({
externalId,
type: 'mechanic'
})
.returning({ id: externalIds.id });
await transaction.insert(mechanicsToExternalIds).values({
mechanicId: dbMechanics[0].id,
externalId: dbExternalIds[0].id
});
});
if (dbMechanics.length === 0) {
return new Response('Could not create mechanic', {
status: 500
});
}
console.log('Created mechanic', JSON.stringify(dbMechanics[0], null, 2));
return new Response(JSON.stringify(dbMechanics[0]), {
status: 201,
});
} catch (e) {
console.error(e);
throw new Error('Something went wrong creating Mechanic');
}
}

View file

@ -0,0 +1,125 @@
import { error } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import kebabCase from 'just-kebab-case';
import db from '$lib/drizzle';
import {
externalIds,
publishersToExternalIds,
type Publishers,
publishers,
} from '../../../schema';
import { PUBLIC_SITE_URL } from '$env/static/public';
export async function getPublisher(locals: App.Locals, id: string) {
const publisher = await db.select().from(publishers).where(eq(publishers.id, id));
if (publisher.length === 0) {
error(404, 'not found');
}
return new Response(JSON.stringify(publisher[0]), {
headers: {
'Content-Type': 'application/json'
}
});
}
export async function updatePublisher(locals: App.Locals, publisher: Publishers, id: string) {
if (!publisher || publisher.name === '' || !id || id === '') {
error(400, 'Invalid Request');
}
try {
const dbPublisher = await db
.update(publishers)
.set({
name: publisher.name,
slug: kebabCase(publisher.name || '')
})
.where(eq(publishers.id, id))
.returning();
return new Response(JSON.stringify(dbPublisher[0]), {
headers: {
'Content-Type': 'application/json'
}
});
} catch (e) {
console.error(e);
return new Response('Could not get publishers', {
status: 500
});
}
}
export async function createPublisher(
locals: App.Locals,
publisher: Publishers,
externalId: string
) {
if (!publisher || !externalId || externalId === '') {
error(400, 'Invalid Request');
}
try {
const dbExternalId = await db.query.externalIds.findFirst({
where: eq(externalIds.externalId, externalId)
});
if (dbExternalId) {
const foundPublisher = await db
.select({
id: publishers.id,
name: publishers.name,
slug: publishers.slug
})
.from(publishers)
.leftJoin(publishersToExternalIds, eq(publishersToExternalIds.externalId, externalId));
console.log('Publisher already exists', foundPublisher);
if (foundPublisher.length > 0) {
console.log('Publisher name', foundPublisher[0].name);
return new Response('Publisher already exists', {
headers: {
'Content-Type': 'application/json',
Location: `${PUBLIC_SITE_URL}/api/publisher/${foundPublisher[0].id}`
},
status: 409
});
}
}
let dbPublishers: Publishers[] = [];
console.log('Creating publisher', JSON.stringify(publisher, null, 2));
await db.transaction(async (transaction) => {
dbPublishers = await transaction
.insert(publishers)
.values({
name: publisher.name,
slug: kebabCase(publisher.name || publisher.slug || '')
})
.returning();
const dbExternalIds = await transaction
.insert(externalIds)
.values({
externalId,
type: 'publisher'
})
.returning({ id: externalIds.id });
await transaction.insert(publishersToExternalIds).values({
publisherId: dbPublishers[0].id,
externalId: dbExternalIds[0].id
});
});
if (dbPublishers.length === 0) {
return new Response('Could not create publisher', {
status: 500
});
}
console.log('Created publisher', JSON.stringify(dbPublishers[0], null, 2));
return new Response(JSON.stringify(dbPublishers[0]), {
status: 201,
});
} catch (e) {
console.error(e);
throw new Error('Something went wrong creating Publisher');
}
}

View file

@ -1,456 +0,0 @@
import type { Game } from '@prisma/client';
import kebabCase from 'just-kebab-case';
import type { BggLinkDto } from 'boardgamegeekclient/dist/esm/dto/concrete/subdto';
import prisma from '$lib/prisma';
import { mapAPIGameToBoredGame } from './gameMapper';
export async function createArtist(locals: App.Locals, externalArtist: BggLinkDto) {
try {
let dbArtist = await prisma.artist.findFirst({
where: {
external_id: externalArtist.id
},
select: {
id: true,
name: true,
slug: true,
external_id: true
}
});
if (dbArtist) {
console.log('Artist already exists', dbArtist.name);
return dbArtist;
}
console.log('Creating artist', JSON.stringify(externalArtist, null, 2));
let artist = await prisma.artist.create({
data: {
name: externalArtist.value,
external_id: externalArtist.id,
slug: kebabCase(externalArtist.value)
},
select: {
id: true,
name: true,
slug: true,
external_id: true
}
});
console.log('Created artist', JSON.stringify(artist, null, 2));
return artist;
} catch (e) {
console.error(e);
throw new Error('Something went wrong creating Artist');
}
}
export async function createDesigner(locals: App.Locals, externalDesigner: BggLinkDto) {
try {
let dbDesigner = await prisma.designer.findFirst({
where: {
external_id: externalDesigner.id
},
select: {
id: true,
name: true,
slug: true,
external_id: true
}
});
if (dbDesigner) {
console.log('Designer already exists', dbDesigner.name);
return dbDesigner;
}
console.log('Creating designer', JSON.stringify(externalDesigner, null, 2));
let designer = await prisma.designer.create({
data: {
name: externalDesigner.value,
external_id: externalDesigner.id,
slug: kebabCase(externalDesigner.value)
},
select: {
id: true,
name: true,
slug: true,
external_id: true
}
});
console.log('Created designer', JSON.stringify(designer, null, 2));
return designer;
} catch (e) {
console.error(e);
throw new Error('Something went wrong creating Designer');
}
}
export async function createPublisher(locals: App.Locals, externalPublisher: BggLinkDto) {
try {
let dbPublisher = await prisma.publisher.findFirst({
where: {
external_id: externalPublisher.id
},
select: {
id: true,
name: true,
slug: true,
external_id: true
}
});
if (dbPublisher) {
console.log('Publisher already exists', dbPublisher.name);
return dbPublisher;
}
console.log('Creating publisher', JSON.stringify(externalPublisher, null, 2));
let publisher = await prisma.publisher.create({
data: {
name: externalPublisher.value,
external_id: externalPublisher.id,
slug: kebabCase(externalPublisher.value)
},
select: {
id: true,
name: true,
slug: true,
external_id: true
}
});
console.log('Created publisher', JSON.stringify(publisher, null, 2));
return publisher;
} catch (e) {
console.error(e);
throw new Error('Something went wrong creating Publisher');
}
}
export async function createCategory(locals: App.Locals, externalCategory: BggLinkDto) {
try {
let dbCategory = await prisma.category.findFirst({
where: {
external_id: externalCategory.id
},
select: {
id: true,
name: true,
slug: true,
external_id: true
}
});
if (dbCategory) {
console.log('Category already exists', dbCategory.name);
return dbCategory;
}
console.log('Creating category', JSON.stringify(externalCategory, null, 2));
let category = await prisma.category.create({
data: {
name: externalCategory.value,
external_id: externalCategory.id,
slug: kebabCase(externalCategory.value)
},
select: {
id: true,
name: true,
slug: true,
external_id: true
}
});
console.log('Created category', JSON.stringify(category, null, 2));
return category;
} catch (e) {
console.error(e);
throw new Error('Something went wrong creating Category');
}
}
export async function createMechanic(locals: App.Locals, externalMechanic: BggLinkDto) {
try {
let dbMechanic = await prisma.mechanic.findFirst({
where: {
external_id: externalMechanic.id
},
select: {
id: true,
name: true,
slug: true,
external_id: true
}
});
if (dbMechanic) {
console.log('Mechanic already exists', dbMechanic.name);
return dbMechanic;
}
console.log('Creating mechanic', JSON.stringify(externalMechanic, null, 2));
let mechanic = await prisma.mechanic.upsert({
where: {
external_id: externalMechanic.id
},
create: {
name: externalMechanic.value,
external_id: externalMechanic.id,
slug: kebabCase(externalMechanic.value)
},
update: {
name: externalMechanic.value,
slug: kebabCase(externalMechanic.value)
}
});
console.log('Created mechanic', JSON.stringify(mechanic, null, 2));
return mechanic;
} catch (e) {
console.error(e);
throw new Error('Something went wrong creating Mechanic');
}
}
export async function createExpansion(
locals: App.Locals,
game: Game,
externalExpansion: BggLinkDto,
gameIsExpansion: boolean,
eventFetch: Function
) {
try {
let dbExpansionGame = await prisma.game.findUnique({
where: {
external_id: externalExpansion.id
}
});
if (!dbExpansionGame) {
const externalGameResponse = await eventFetch(
`/api/external/game/${externalExpansion.id}?simplified=true`
);
if (externalGameResponse.ok) {
const externalGame = await externalGameResponse.json();
console.log('externalGame', externalGame);
let boredGame = mapAPIGameToBoredGame(externalGame);
dbExpansionGame = await createOrUpdateGameMinimal(locals, boredGame);
} else {
throw new Error(
`${gameIsExpansion ? 'Base game' : 'Expansion game'} not found and failed to create.`
);
}
}
let dbExpansion;
let baseGameId;
let gameId;
if (gameIsExpansion) {
console.log(
'External expansion is expansion. Looking for base game',
JSON.stringify(game, null, 2)
);
dbExpansion = await prisma.expansion.findFirst({
where: {
game_id: dbExpansionGame.id
},
select: {
id: true,
base_game_id: true,
game_id: true
}
});
baseGameId = game.id;
gameId = dbExpansionGame.id;
} else {
console.log(
'External Expansion is base game. Looking for expansion',
JSON.stringify(game, null, 2)
);
dbExpansion = await prisma.expansion.findFirst({
where: {
base_game_id: dbExpansionGame.id
},
select: {
id: true,
base_game_id: true,
game_id: true
}
});
baseGameId = dbExpansionGame.id;
gameId = game.id;
}
if (dbExpansion) {
console.log('Expansion already exists', JSON.stringify(dbExpansion, null, 2));
return dbExpansion;
}
console.log(`Creating expansion. baseGameId: ${baseGameId}, gameId: ${gameId}`);
let expansion = await prisma.expansion.create({
data: {
base_game_id: baseGameId,
game_id: gameId
}
});
console.log('Created expansion', JSON.stringify(expansion, null, 2));
if (gameIsExpansion) {
console.log('Connecting current game to expansion');
await prisma.game.update({
where: {
id: gameId
},
data: {
expansions: {
connect: {
id: expansion.id
}
}
}
});
} else {
console.log('Connecting current game to base game');
await prisma.game.update({
where: {
id: baseGameId
},
data: {
expansions: {
connect: {
id: expansion.id
}
}
}
});
}
return expansion;
} catch (e) {
console.error(e);
throw new Error('Something went wrong creating Expansion');
}
}
export async function createOrUpdateGameMinimal(locals: App.Locals, game: Game) {
console.log('Creating or updating minimal game data', JSON.stringify(game, null, 2));
const externalUrl = `https://boardgamegeek.com/boardgame/${game.external_id}`;
return await prisma.game.upsert({
where: {
external_id: game.external_id
},
create: {
name: game.name,
slug: kebabCase(game.name),
description: game.description,
external_id: game.external_id,
url: externalUrl,
thumb_url: game.thumb_url,
image_url: game.image_url,
min_age: game.min_age || 0,
min_players: game.min_players || 0,
max_players: game.max_players || 0,
min_playtime: game.min_playtime || 0,
max_playtime: game.max_playtime || 0,
year_published: game.year_published || 0
},
update: {
name: game.name,
slug: kebabCase(game.name),
description: game.description,
external_id: game.external_id,
url: externalUrl,
thumb_url: game.thumb_url,
image_url: game.image_url,
min_age: game.min_age || 0,
min_players: game.min_players || 0,
max_players: game.max_players || 0,
min_playtime: game.min_playtime || 0,
max_playtime: game.max_playtime || 0,
year_published: game.year_published || 0
}
});
}
export async function createOrUpdateGame(locals: App.Locals, game: Game) {
console.log('Creating or updating game', JSON.stringify(game, null, 2));
const categoryIds = game.categories;
const mechanicIds = game.mechanics;
const publisherIds = game.publishers;
const designerIds = game.designers;
const artistIds = game.artists;
// const expansionIds = game.expansions;
const externalUrl = `https://boardgamegeek.com/boardgame/${game.external_id}`;
console.log('categoryIds', categoryIds);
console.log('mechanicIds', mechanicIds);
return await prisma.game.upsert({
include: {
mechanics: true,
publishers: true,
designers: true,
artists: true,
expansions: true
},
where: {
external_id: game.external_id
},
create: {
name: game.name,
slug: kebabCase(game.name),
description: game.description,
external_id: game.external_id,
url: externalUrl,
thumb_url: game.thumb_url,
image_url: game.image_url,
min_age: game.min_age || 0,
min_players: game.min_players || 0,
max_players: game.max_players || 0,
min_playtime: game.min_playtime || 0,
max_playtime: game.max_playtime || 0,
year_published: game.year_published || 0,
last_sync_at: new Date(),
categories: {
connect: categoryIds
},
mechanics: {
connect: mechanicIds
},
publishers: {
connect: publisherIds
},
designers: {
connect: designerIds
},
artists: {
connect: artistIds
}
},
update: {
name: game.name,
slug: kebabCase(game.name),
description: game.description,
external_id: game.external_id,
url: externalUrl,
thumb_url: game.thumb_url,
image_url: game.image_url,
min_age: game.min_age || 0,
min_players: game.min_players || 0,
max_players: game.max_players || 0,
min_playtime: game.min_playtime || 0,
max_playtime: game.max_playtime || 0,
year_published: game.year_published || 0,
last_sync_at: new Date(),
categories: {
connect: categoryIds
},
mechanics: {
connect: mechanicIds
},
publishers: {
connect: publisherIds
},
designers: {
connect: designerIds
},
artists: {
connect: artistIds
}
}
});
}

View file

@ -1,6 +1,7 @@
import type { GameType, SavedGameType } from '$lib/types'; import type { GameType, SavedGameType } from '$lib/types';
import type { Game } from '@prisma/client'; import type { Game } from '@prisma/client';
import kebabCase from 'just-kebab-case'; import kebabCase from 'just-kebab-case';
import type { Games } from '../../schema';
export function convertToSavedGame(game: GameType | SavedGameType): SavedGameType { export function convertToSavedGame(game: GameType | SavedGameType): SavedGameType {
return { return {
@ -43,10 +44,9 @@ export function mapSavedGameToGame(game: SavedGameType): GameType {
}; };
} }
export function mapAPIGameToBoredGame(game: GameType): Game { export function mapAPIGameToBoredGame(game: GameType): Games {
// TODO: Fix types // TODO: Fix types
return { return {
external_id: game.external_id,
name: game.name, name: game.name,
slug: kebabCase(game.name), slug: kebabCase(game.name),
thumb_url: game.thumbnail, thumb_url: game.thumbnail,
@ -57,7 +57,6 @@ export function mapAPIGameToBoredGame(game: GameType): Game {
min_playtime: game.min_playtime, min_playtime: game.min_playtime,
max_playtime: game.max_playtime, max_playtime: game.max_playtime,
min_age: game.min_age, min_age: game.min_age,
description: game.description, description: game.description
playtime: game.playing_time
}; };
} }

View file

@ -0,0 +1,106 @@
import { z } from 'zod';
import { userSchema } from './zod-schemas';
export const profileSchema = userSchema.pick({
firstName: true,
lastName: true,
username: true
});
export const changeEmailSchema = userSchema.pick({
email: true,
});
export const changeUserPasswordSchema = z
.object({
current_password: z.string({ required_error: 'Current Password is required' }),
password: z.string({ required_error: 'Password is required' }).trim(),
confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim()
})
.superRefine(({ confirm_password, password }, ctx) => {
refinePasswords(confirm_password, password, ctx);
});
export type ChangeUserPasswordSchema = typeof changeUserPasswordSchema;
export const updateUserPasswordSchema = userSchema
.pick({ password: true, confirm_password: true })
.superRefine(({ confirm_password, password }, ctx) => {
refinePasswords(confirm_password, password, ctx);
});
export const refinePasswords = async function (
confirm_password: string,
password: string,
ctx: z.RefinementCtx
) {
comparePasswords(confirm_password, password, ctx);
checkPasswordStrength(password, ctx);
};
const comparePasswords = async function (
confirm_password: string,
password: string,
ctx: z.RefinementCtx
) {
if (confirm_password !== password) {
ctx.addIssue({
code: 'custom',
message: 'Password and Confirm Password must match',
path: ['confirm_password']
});
}
};
const checkPasswordStrength = async function (password: string, ctx: z.RefinementCtx) {
const minimumLength = password.length < 8;
const maximumLength = password.length > 128;
const containsUppercase = (ch: string) => /[A-Z]/.test(ch);
const containsLowercase = (ch: string) => /[a-z]/.test(ch);
const containsSpecialChar = (ch: string) => /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/.test(ch);
let countOfUpperCase = 0,
countOfLowerCase = 0,
countOfNumbers = 0,
countOfSpecialChar = 0;
for (let i = 0; i < password.length; i++) {
const char = password.charAt(i);
if (!isNaN(+char)) {
countOfNumbers++;
} else if (containsUppercase(char)) {
countOfUpperCase++;
} else if (containsLowercase(char)) {
countOfLowerCase++;
} else if (containsSpecialChar(char)) {
countOfSpecialChar++;
}
}
let errorMessage = 'Your password:';
if (countOfLowerCase < 1) {
errorMessage = ' Must have at least one lowercase letter. ';
}
if (countOfNumbers < 1) {
errorMessage += ' Must have at least one number. ';
}
if (countOfUpperCase < 1) {
errorMessage += ' Must have at least one uppercase letter. ';
}
if (countOfSpecialChar < 1) {
errorMessage += ' Must have at least one special character.';
}
if (minimumLength) {
errorMessage += ' Be at least 8 characters long.';
}
if (maximumLength) {
errorMessage += ' Be less than 128 characters long.';
}
if (errorMessage.length > 'Your password:'.length) {
ctx.addIssue({
code: 'custom',
message: errorMessage,
path: ['password']
});
}
};

View file

@ -0,0 +1,21 @@
import { refinePasswords } from "./account";
import { userSchema } from "./zod-schemas";
export const signUpSchema = userSchema
.pick({
firstName: true,
lastName: true,
email: true,
username: true,
password: true,
confirm_password: true,
terms: true
})
.superRefine(({ confirm_password, password }, ctx) => {
refinePasswords(confirm_password, password, ctx);
});
export const signInSchema = userSchema.pick({
username: true,
password: true
});

View file

@ -0,0 +1,44 @@
import { z } from 'zod';
export type ListGame = {
id: string;
game_name: string;
game_id: string;
collection_id: string;
wishlist_id: string;
times_played: number;
thumb_url: string | null;
in_collection: boolean;
in_wishlist: boolean;
};
export const modifyListGameSchema = z.object({
id: z.string()
});
export type ModifyListGame = typeof modifyListGameSchema;
export const userSchema = z.object({
firstName: z.string().trim().optional(),
lastName: z.string().trim().optional(),
email: z.string()
.trim()
.max(64, { message: 'Email must be less than 64 characters' })
.optional(),
username: z
.string()
.trim()
.min(3, { message: 'Username must be at least 3 characters' })
.max(50, { message: 'Username must be less than 50 characters' }),
password: z
.string({ required_error: 'Password is required' })
.trim(),
confirm_password: z
.string({ required_error: 'Confirm Password is required' })
.trim(),
verified: z.boolean().default(false),
token: z.string().optional(),
receiveEmail: z.boolean().default(false),
createdAt: z.date().optional(),
updatedAt: z.date().optional()
});

View file

@ -117,6 +117,10 @@ export const search_schema = z
export type SearchSchema = typeof search_schema; export type SearchSchema = typeof search_schema;
export const BggForm = z.object({
link: z.string().trim().startsWith('https://boardgamegeek.com/boardgame/')
})
export const collection_search_schema = Search.extend({ export const collection_search_schema = Search.extend({
collection_id: z.string() collection_id: z.string()
}); });
@ -222,7 +226,6 @@ const gameSchema = z.object({
image_url: z.string().optional(), image_url: z.string().optional(),
thumb_url: z.string().optional(), thumb_url: z.string().optional(),
url: z.string().optional(), url: z.string().optional(),
rules_url: z.string().optional(),
weight_amount: z.number().optional(), weight_amount: z.number().optional(),
weight_units: z.enum(['Medium', 'Heavy']).optional(), weight_units: z.enum(['Medium', 'Heavy']).optional(),
categories: z.array(category_schema).optional(), categories: z.array(category_schema).optional(),

25
src/migrate.ts Normal file
View file

@ -0,0 +1,25 @@
import 'dotenv/config';
import postgres from 'postgres';
import { drizzle } from 'drizzle-orm/postgres-js';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
const connection = postgres({
host: process.env.DATABASE_HOST || 'localhost',
port: 5432,
user: process.env.DATABASE_USER || 'root',
password: process.env.DATABASE_PASSWORD || '',
database: process.env.DATABASE_DB || 'boredgame',
ssl: 'require',
max: 1
});
const db = drizzle(connection);
try {
await migrate(db, { migrationsFolder: 'drizzle' });
console.log('Migrations complete');
} catch (e) {
console.error(e);
}
await connection.end();
process.exit();

View file

@ -1,6 +1,12 @@
import { redirect } from '@sveltejs/kit'; import { redirect } from 'sveltekit-flash-message/server'
import type { PageServerData } from './$types'; import type { PageServerLoad } from './$types';
import { notSignedInMessage } from '$lib/flashMessages';
export const load: PageServerData = async function ({ locals }) { export async function load(event) {
if (!locals?.user?.role?.includes('admin')) redirect(302, '/'); const { locals } = event;
if (!locals?.user?.role?.includes('admin')) {
redirect(302, '/login', notSignedInMessage, event);
}
return {}
}; };

View file

@ -1,14 +1,19 @@
import { type Actions, error, fail, redirect } from '@sveltejs/kit'; import { type Actions, error, fail } from '@sveltejs/kit';
import { and, eq } from 'drizzle-orm';
import { superValidate } from 'sveltekit-superforms/server'; import { superValidate } from 'sveltekit-superforms/server';
import prisma from '$lib/prisma'; import { zod } from 'sveltekit-superforms/adapters';
import { modifyListGameSchema, type ListGame } from '$lib/config/zod-schemas.js'; import { redirect } from 'sveltekit-flash-message/server'
import { modifyListGameSchema, type ListGame } from '$lib/validations/zod-schemas';
import { search_schema } from '$lib/zodValidation.js'; import { search_schema } from '$lib/zodValidation.js';
import type { PageServerLoad } from './$types'; import db from '$lib/drizzle';
import { collection_items, collections, games } from '../../../../schema';
import { notSignedInMessage } from '$lib/flashMessages';
export const load: PageServerLoad = async ({ fetch, url, locals }) => { export async function load(event) {
const { url, locals } = event;
const user = locals.user; const user = locals.user;
if (!user) { if (!user) {
redirect(302, '/login'); redirect(302, '/login', notSignedInMessage, event);
} }
// console.log('locals load', locals); // console.log('locals load', locals);
@ -24,14 +29,12 @@ export const load: PageServerLoad = async ({ fetch, url, locals }) => {
skip skip
}; };
const searchForm = await superValidate(searchData, search_schema); const searchForm = await superValidate(searchData, zod(search_schema));
const listManageForm = await superValidate(modifyListGameSchema); const listManageForm = await superValidate(zod(modifyListGameSchema));
try { try {
let collection = await prisma.collection.findUnique({ const collection = await db.query.collections.findFirst({
where: { where: eq(collections.user_id, user.id)
user_id: user.id
}
}); });
console.log('collection', collection); console.log('collection', collection);
@ -45,27 +48,25 @@ export const load: PageServerLoad = async ({ fetch, url, locals }) => {
// }); // });
} }
let collection_items = await prisma.collectionItem.findMany({ const collectionItems = await db.query.collection_items.findMany({
where: { where: eq(collection_items.collection_id, collection.id),
collection_id: collection.id with: {
},
include: {
game: { game: {
select: { columns: {
id: true, id: true,
name: true, name: true,
thumb_url: true thumb_url: true
} }
} }
}, },
skip, offset: skip,
take: limit limit
}); });
console.log('collection_items', collection_items); console.log('collection_items', collectionItems);
let collectionItems: ListGame[] = []; const items: ListGame[] = [];
for (const item of collection_items) { for (const item of collectionItems) {
console.log('item', item); console.log('item', item);
const game = item.game; const game = item.game;
if (game) { if (game) {
@ -77,14 +78,14 @@ export const load: PageServerLoad = async ({ fetch, url, locals }) => {
times_played: item.times_played, times_played: item.times_played,
in_collection: true in_collection: true
}; };
collectionItems.push(collectionItem); items.push(collectionItem);
} }
} }
return { return {
searchForm, searchForm,
listManageForm, listManageForm,
collection: collectionItems collection: items
}; };
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -100,18 +101,15 @@ export const load: PageServerLoad = async ({ fetch, url, locals }) => {
export const actions: Actions = { export const actions: Actions = {
// Add game to a wishlist // Add game to a wishlist
add: async (event) => { add: async (event) => {
const { params, locals, request } = event; const form = await superValidate(event, zod(modifyListGameSchema));
const form = await superValidate(event, modifyListGameSchema);
if (!event.locals.user) { if (!event.locals.user) {
throw fail(401); throw fail(401);
} }
const user = event.locals.user; const user = event.locals.user;
let game = await prisma.game.findUnique({ const game = await db.query.games.findFirst({
where: { where: eq(games.id, form.data.id)
id: form.data.id
}
}); });
if (!game) { if (!game) {
@ -125,10 +123,8 @@ export const actions: Actions = {
} }
try { try {
const collection = await prisma.collection.findUnique({ const collection = await db.query.collections.findFirst({
where: { where: eq(collections.user_id, user.id)
user_id: user.id
}
}); });
if (!collection) { if (!collection) {
@ -136,12 +132,10 @@ export const actions: Actions = {
return error(404, 'Wishlist not found'); return error(404, 'Wishlist not found');
} }
await prisma.collectionItem.create({ await db.insert(collection_items).values({
data: { game_id: game.id,
game_id: game.id, collection_id: collection.id,
collection_id: collection.id, times_played: 0
times_played: 0
}
}); });
return { return {
@ -153,14 +147,14 @@ export const actions: Actions = {
} }
}, },
// Create new wishlist // Create new wishlist
create: async ({ params, locals, request }) => { create: async ({ locals }) => {
if (!locals.user) { if (!locals.user) {
throw fail(401); throw fail(401);
} }
return error(405, 'Method not allowed'); return error(405, 'Method not allowed');
}, },
// Delete a wishlist // Delete a wishlist
delete: async ({ params, locals, request }) => { delete: async ({ locals }) => {
if (!locals.user) { if (!locals.user) {
throw fail(401); throw fail(401);
} }
@ -168,17 +162,15 @@ export const actions: Actions = {
}, },
// Remove game from a wishlist // Remove game from a wishlist
remove: async (event) => { remove: async (event) => {
const { params, locals, request } = event; const { locals } = event;
const form = await superValidate(event, modifyListGameSchema); const form = await superValidate(event, zod(modifyListGameSchema));
if (!locals.user) { if (!locals.user) {
throw fail(401); throw fail(401);
} }
let game = await prisma.game.findUnique({ const game = await db.query.games.findFirst({
where: { where: eq(games.id, form.data.id)
id: form.data.id
}
}); });
if (!game) { if (!game) {
@ -187,10 +179,8 @@ export const actions: Actions = {
} }
try { try {
const collection = await prisma.collection.findUnique({ const collection = await db.query.collections.findFirst({
where: { where: eq(collections.user_id, locals.user.id)
user_id: locals.user.id
}
}); });
if (!collection) { if (!collection) {
@ -198,12 +188,10 @@ export const actions: Actions = {
return error(404, 'Collection not found'); return error(404, 'Collection not found');
} }
await prisma.collectionItem.delete({ await db.delete(collection_items).where(and(
where: { eq(collection_items.collection_id, collection.id),
collection_id: collection.id, eq(collection_items.game_id, game.id)
game_id: game.id ));
}
});
return { return {
form form

Some files were not shown because too many files have changed in this diff Show more