Merge pull request #15 from BradNut/shadcn-svelte

Move to Shadcn Svelte and Drizzle
This commit is contained in:
Bradley Shellnut 2024-03-05 06:26:21 +00:00 committed by GitHub
commit 6503659352
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
329 changed files with 37002 additions and 3522 deletions

View file

@ -1,31 +1,30 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: ['plugin:svelte/recommended'],
plugins: ['@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
// Parse the `<script>` in `.svelte` as TypeScript by adding the following configuration.
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
settings: {
'svelte3/typescript': () => require('typescript')
},
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
project: './tsconfig.json',
extraFileExtensions: ['.svelte'] // This is a required setting in `@typescript-eslint/parser` v4.24.0.
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
}
};
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};

6
.gitignore vendored
View file

@ -5,8 +5,12 @@ node_modules
/package
.env
.env.*
*.xdp*
!.env.example
.vercel
.output
.idea
.fleet
.fleet
# Sentry Config File
.sentryclirc

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
v20

46
.vscode/launch.json vendored
View file

@ -1,17 +1,31 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Vite DEV server",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": ["vite"],
"type": "node",
"serverReadyAction": {
"action": "debugWithChrome",
"pattern": "Local: http://localhost:([0-9]+)",
"uriFormat": "http://localhost:%s"
}
}
]
}
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch server",
"request": "launch",
"runtimeArgs": ["dev"],
"runtimeExecutable": "pnpm",
"skipFiles": ["<node_internals>/**"],
"type": "node",
"console": "integratedTerminal"
},
{
"type": "chrome",
"request": "launch",
"name": "Launch browser",
"url": "http://127.0.0.1:5173",
"runtimeExecutable": "/home/bshellnu/.local/share/flatpak/app/org.chromium.Chromium/current/active/files/chromium/chrome",
"webRoot": "${workspaceFolder}"
}
],
"compounds": [
{
"name": "Both",
"configurations": ["Launch server", "Launch browser"]
}
]
}

View file

@ -1,3 +1,3 @@
{
"cSpell.words": ["kickstarter", "msrp"]
"cSpell.words": ["iconify", "kickstarter", "lucide", "msrp", "pcss"]
}

13
components.json Normal file
View file

@ -0,0 +1,13 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.js",
"css": "src/app.postcss",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
}
}

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

@ -1,10 +0,0 @@
module.exports = {
environmentVariables: {
'--xsmall-viewport': '480px',
'--small-viewport': '640px',
'--medium-viewport': '768px',
'--large-viewport': '1024px',
'--xlarge-viewport': '1280px',
'--xxlarge-viewport': '1536px',
}
}

View file

@ -1,65 +1,124 @@
{
"name": "boredgame",
"version": "0.0.2",
"private": "true",
"scripts": {
"dev": "NODE_OPTIONS=\"--inspect\" vite dev --host",
"build": "vite build",
"build": "prisma generate && vite build",
"package": "svelte-kit package",
"preview": "vite preview",
"test": "playwright test",
"check": "svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
"format": "prettier --write --plugin-search-dir=. ."
"test:ui": "svelte-kit sync && playwright test --ui",
"postinstall": "prisma generate",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test:unit": "vitest",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write .",
"site:update": "pnpm update -i -L",
"generate": "drizzle-kit generate:pg",
"migrate": "tsx ./src/migrate.ts",
"seed": "tsx ./src/seed.ts",
"push": "drizzle-kit push:pg"
},
"prisma": {
"seed": "node --loader ts-node/esm prisma/seed.ts"
},
"devDependencies": {
"@playwright/test": "^1.33.0",
"@rgossiaux/svelte-headlessui": "1.0.2",
"@rgossiaux/svelte-heroicons": "^0.1.2",
"@sveltejs/adapter-auto": "^1.0.3",
"@sveltejs/adapter-vercel": "^1.0.6",
"@sveltejs/kit": "^1.16.2",
"@types/cookie": "^0.5.1",
"@types/node": "^18.16.4",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"autoprefixer": "^10.4.14",
"eslint": "^8.40.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-svelte": "^2.27.3",
"@melt-ui/pp": "^0.3.0",
"@melt-ui/svelte": "^0.75.2",
"@playwright/test": "^1.42.0",
"@resvg/resvg-js": "^2.6.0",
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/enhanced-img": "^0.1.8",
"@sveltejs/kit": "^2.5.2",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/cookie": "^0.6.0",
"@types/node": "^20.11.24",
"@types/pg": "^8.11.2",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"autoprefixer": "^10.4.18",
"dotenv": "^16.4.5",
"drizzle-kit": "^0.20.14",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"just-clone": "^6.2.0",
"just-debounce-it": "^3.2.0",
"postcss": "^8.4.23",
"postcss-color-functional-notation": "^4.2.4",
"postcss-custom-media": "^9.1.3",
"postcss-env-function": "^4.0.6",
"postcss-import": "^15.1.0",
"postcss-load-config": "^4.0.1",
"postcss-media-minmax": "^5.0.0",
"postcss-nested": "^6.0.1",
"prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.0",
"sass": "^1.62.1",
"svelte": "^3.59.0",
"svelte-check": "^2.10.3",
"svelte-preprocess": "^4.10.7",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"vite": "^4.3.5",
"vitest": "^0.25.3"
"postcss": "^8.4.35",
"postcss-import": "^16.0.1",
"postcss-load-config": "^5.0.3",
"postcss-preset-env": "^9.4.0",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2",
"prisma": "^5.9.1",
"sass": "^1.71.1",
"satori": "^0.10.13",
"satori-html": "^0.3.2",
"svelte": "^4.2.12",
"svelte-check": "^3.6.6",
"svelte-meta-tags": "^3.1.1",
"svelte-preprocess": "^5.1.3",
"svelte-sequential-preprocessor": "^2.0.1",
"sveltekit-flash-message": "^2.4.2",
"sveltekit-rate-limiter": "^0.4.3",
"sveltekit-superforms": "^2.7.0",
"tailwindcss": "^3.4.1",
"ts-node": "^10.9.2",
"tslib": "^2.6.1",
"tsx": "^4.7.1",
"typescript": "^5.3.3",
"vite": "^5.1.5",
"vitest": "^1.3.1",
"zod": "^3.22.4"
},
"type": "module",
"engines": {
"node": ">=18.0.0 <19.0.0 || >=20.0.0 <21.0.0",
"pnpm": ">=8"
},
"dependencies": {
"@fontsource/fira-mono": "^4.5.10",
"@leveluptuts/svelte-side-menu": "^1.0.5",
"@leveluptuts/svelte-toy": "^2.0.3",
"@fontsource/fira-mono": "^5.0.12",
"@iconify-icons/line-md": "^1.2.26",
"@iconify-icons/mdi": "^1.2.47",
"@lucia-auth/adapter-drizzle": "^1.0.2",
"@lucia-auth/adapter-prisma": "4.0.0",
"@lukeed/uuid": "^2.0.1",
"@types/feather-icons": "^4.29.1",
"cookie": "^0.5.0",
"feather-icons": "^4.29.0",
"open-props": "^1.5.8",
"@neondatabase/serverless": "^0.9.0",
"@paralleldrive/cuid2": "^2.2.2",
"@planetscale/database": "^1.16.0",
"@prisma/client": "^5.9.1",
"@sentry/sveltekit": "^7.100.1",
"@sveltejs/adapter-vercel": "^5.1.0",
"@types/feather-icons": "^4.29.4",
"@vercel/og": "^0.5.20",
"bits-ui": "^0.19.3",
"boardgamegeekclient": "^1.9.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"cookie": "^0.6.0",
"drizzle-orm": "^0.29.4",
"feather-icons": "^4.29.1",
"formsnap": "^0.5.1",
"html-entities": "^2.5.2",
"iconify-icon": "^2.0.0",
"just-kebab-case": "^4.2.0",
"loader": "^2.1.1",
"lucia": "3.0.1",
"lucide-svelte": "^0.344.0",
"mysql2": "^3.9.2",
"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",
"svelte-french-toast": "^1.2.0",
"svelte-lazy-loader": "^1.0.0",
"zod": "^3.21.4",
"zod-to-json-schema": "^3.21.0"
"tailwind-merge": "^2.2.1",
"tailwind-variants": "^0.2.0",
"tailwindcss-animate": "^1.0.6",
"zod-to-json-schema": "^3.22.4"
}
}
}

View file

@ -4,7 +4,9 @@ const config: PlaywrightTestConfig = {
webServer: {
command: 'npm run build && npm run preview',
port: 4173
}
},
testDir: 'tests',
testMatch: /(.+\.)?(test|spec)\.[jt]s/
};
export default config;

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,23 @@
const tailwindcss = require("tailwindcss");
const tailwindNesting = require('tailwindcss/nesting');
const autoprefixer = require('autoprefixer');
const postcssMediaMinmax = require('postcss-media-minmax');
const customMedia = require('postcss-custom-media');
const postcssPresetEnv = require('postcss-preset-env');
const atImport = require('postcss-import');
const postcssNested = require('postcss-nested');
const postcssEnvFunction = require('postcss-env-function');
const config = {
plugins: [
autoprefixer(),
postcssMediaMinmax,
customMedia,
atImport(),
postcssNested,
postcssEnvFunction(),
]
plugins: [
atImport(),
tailwindNesting(),
tailwindcss(),
postcssPresetEnv({
stage: 3,
features: {
'nesting-rules': false,
'custom-media-queries': true,
'media-query-ranges': true
}
}),
] //Some plugins, like tailwindcss/nesting, need to run before Tailwind, tailwindcss(), //But others, like autoprefixer, need to run after, autoprefixer]
};
module.exports = config;

256
prisma/categories.json Normal file
View file

@ -0,0 +1,256 @@
{
"categories": [
{
"name": "Abstract Strategy"
},
{
"name": "Action / Dexterity"
},
{
"name": "Adventure"
},
{
"name": "Age of Reason"
},
{
"name": "American Civil War"
},
{
"name": "American Indian Wars"
},
{
"name": "American Revolutionary War"
},
{
"name": "American West"
},
{
"name": "Ancient"
},
{
"name": "Animals"
},
{
"name": "Arabian"
},
{
"name": "Aviation / Flight"
},
{
"name": "Bluffing"
},
{
"name": "Book"
},
{
"name": "Card Game"
},
{
"name": "Children's Game"
},
{
"name": "City Building"
},
{
"name": "Civil War"
},
{
"name": "Civilization"
},
{
"name": "Collectible Components"
},
{
"name": "Comic Book / Strip"
},
{
"name": "Deduction"
},
{
"name": "Dice"
},
{
"name": "Economic"
},
{
"name": "Educational"
},
{
"name": "Electronic"
},
{
"name": "Environmental"
},
{
"name": "Expansion for Base-game"
},
{
"name": "Exploration"
},
{
"name": "Fan Expansion"
},
{
"name": "Fantasy"
},
{
"name": "Farming"
},
{
"name": "Fighting"
},
{
"name": "Game System"
},
{
"name": "Horror"
},
{
"name": "Humor"
},
{
"name": "Industry / Manufacturing"
},
{
"name": "Korean War"
},
{
"name": "Mafia"
},
{
"name": "Math"
},
{
"name": "Mature / Adult"
},
{
"name": "Maze"
},
{
"name": "Medical"
},
{
"name": "Medieval"
},
{
"name": "Memory"
},
{
"name": "Miniatures"
},
{
"name": "Modern Warefare"
},
{
"name": "Movies / TV / Radio Theme"
},
{
"name": "Murder/Mystery"
},
{
"name": "Music"
},
{
"name": "Mythology"
},
{
"name": "Napoleonic"
},
{
"name": "Nautical"
},
{
"name": "Negotiation"
},
{
"name": "Novel-based"
},
{
"name": "Number"
},
{
"name": "Party Game"
},
{
"name": "Pike and Shot"
},
{
"name": "Pirates"
},
{
"name": "Political"
},
{
"name": "Post-Napoleonic"
},
{
"name": "Prehistoric"
},
{
"name": "Print & Play"
},
{
"name": "Puzzle"
},
{
"name": "Racing"
},
{
"name": "Real-time"
},
{
"name": "Religious"
},
{
"name": "Renaissance"
},
{
"name": "Science Fiction"
},
{
"name": "Space Exploration"
},
{
"name": "Spies/Secret Agents"
},
{
"name": "Sports"
},
{
"name": "Territory Building"
},
{
"name": "Trains"
},
{
"name": "Transportation"
},
{
"name": "Travel"
},
{
"name": "Trivia"
},
{
"name": "Video Game Theme"
},
{
"name": "Vietnam War"
},
{
"name": "Wargame"
},
{
"name": "Word Game"
},
{
"name": "World War I"
},
{
"name": "World War II"
},
{
"name": "Zombies"
}
]
}

577
prisma/mechanics.json Normal file
View file

@ -0,0 +1,577 @@
{
"mechanics": [
{
"name": "Acting"
},
{
"name": "Action Drafting"
},
{
"name": "Action Points"
},
{
"name": "Action Queue"
},
{
"name": "Action Retrieval"
},
{
"name": "Action Timer"
},
{
"name": "Action/Event"
},
{
"name": "Advantage Token"
},
{
"name": "Alliances"
},
{
"name": "Area Majority / Influence"
},
{
"name": "Area Movement"
},
{
"name": "Area-Impulse"
},
{
"name": "Auction Compensation"
},
{
"name": "Auction: Dexterity"
},
{
"name": "Auction: Dutch"
},
{
"name": "Auction: Dutch Priority"
},
{
"name": "Auction: English"
},
{
"name": "Auction: Fixed Placement"
},
{
"name": "Auction: Multiple Lot"
},
{
"name": "Auction: Once Around"
},
{
"name": "Auction: Sealed Bid"
},
{
"name": "Auction: Turn Order Until Pass"
},
{
"name": "Auction/Bidding"
},
{
"name": "Automatic Resource Growth"
},
{
"name": "Betting and Bluffing"
},
{
"name": "Bias"
},
{
"name": "Bids As Wagers"
},
{
"name": "Bingo"
},
{
"name": "Bribery"
},
{
"name": "Campaign / Battle Card Driven"
},
{
"name": "Card Play Conflict Resolution"
},
{
"name": "Catch the Leader"
},
{
"name": "Chaining"
},
{
"name": "Chit-Pull System"
},
{
"name": "Closed Drafting"
},
{
"name": "Closed Economy Auction"
},
{
"name": "Command Cards"
},
{
"name": "Commodity Speculation"
},
{
"name": "Communication Limits"
},
{
"name": "Connections"
},
{
"name": "Constrained Bidding"
},
{
"name": "Contracts"
},
{
"name": "Cooperative Game"
},
{
"name": "Crayon Rail System"
},
{
"name": "Critical Hits and Failures"
},
{
"name": "Cube Tower"
},
{
"name": "Deck Construction"
},
{
"name": "Deck, Bag, and Pool Building"
},
{
"name": "Deduction"
},
{
"name": "Delayed Purchase"
},
{
"name": "Dice Rolling"
},
{
"name": "Die Icon Resolution"
},
{
"name": "Different Dice Movement"
},
{
"name": "Drawing"
},
{
"name": "Elapsed Real Time Ending"
},
{
"name": "Enclosure"
},
{
"name": "End Game Bonuses"
},
{
"name": "Events"
},
{
"name": "Finale Ending"
},
{
"name": "Flicking"
},
{
"name": "Follow"
},
{
"name": "Force Commitment"
},
{
"name": "Grid Coverage"
},
{
"name": "Grid Movement"
},
{
"name": "Hand Management"
},
{
"name": "Hexagon Grid"
},
{
"name": "Hidden Movement"
},
{
"name": "Hidden Roles"
},
{
"name": "Hidden Victory Points"
},
{
"name": "Highest-Lowest Scoring"
},
{
"name": "Hot Potato"
},
{
"name": "I Cut, You Choose"
},
{
"name": "Impulse Movement"
},
{
"name": "Income"
},
{
"name": "Increase Value of Unchosen Resources"
},
{
"name": "Induction"
},
{
"name": "Interrupts"
},
{
"name": "Investment"
},
{
"name": "Kill Steal"
},
{
"name": "King of the Hill"
},
{
"name": "Ladder Climbing"
},
{
"name": "Layering"
},
{
"name": "Legacy Game"
},
{
"name": "Line Drawing"
},
{
"name": "Line of Sight"
},
{
"name": "Loans"
},
{
"name": "Lose a Turn"
},
{
"name": "Mancala"
},
{
"name": "Map Addition"
},
{
"name": "Map Deformation"
},
{
"name": "Map Reduction"
},
{
"name": "Market"
},
{
"name": "Matching"
},
{
"name": "Measurement Movement"
},
{
"name": "Melding and Splaying"
},
{
"name": "Memory"
},
{
"name": "Minimap Resolution"
},
{
"name": "Modular Board"
},
{
"name": "Move Through Deck"
},
{
"name": "Movement Points"
},
{
"name": "Movement Template"
},
{
"name": "Moving Multiple Units"
},
{
"name": "Multi-Use Cards"
},
{
"name": "Multiple Maps"
},
{
"name": "Narrative Choice / Paragraph"
},
{
"name": "Negotiation"
},
{
"name": "Neighbor Scope"
},
{
"name": "Network and Route Building"
},
{
"name": "Once-Per-Game Abilities"
},
{
"name": "Open Drafting"
},
{
"name": "Order Counters"
},
{
"name": "Ordering"
},
{
"name": "Ownership"
},
{
"name": "Paper-and-Pencil"
},
{
"name": "Passed Action Token"
},
{
"name": "Pattern Building"
},
{
"name": "Pattern Movement"
},
{
"name": "Pattern Recognition"
},
{
"name": "Physical Removal"
},
{
"name": "Pick-up and Deliver"
},
{
"name": "Pieces as Map"
},
{
"name": "Player Elimination"
},
{
"name": "Player Judge"
},
{
"name": "Point to Point Movement"
},
{
"name": "Predictive Bid"
},
{
"name": "Prisoner's Dilemma"
},
{
"name": "Programmed Movement"
},
{
"name": "Push Your Luck"
},
{
"name": "Questions and Answers"
},
{
"name": "Race"
},
{
"name": "Random Production"
},
{
"name": "Ratio / Combat Results Table"
},
{
"name": "Re-rolling and Locking"
},
{
"name": "Real-Time"
},
{
"name": "Relative Movement"
},
{
"name": "Resource Queue"
},
{
"name": "Resource to Move"
},
{
"name": "Rock-Paper-Scissors"
},
{
"name": "Role Playing"
},
{
"name": "Roles with Asymmetric Information"
},
{
"name": "Roll / Spin and Move"
},
{
"name": "Rondel"
},
{
"name": "Scenario / Mission / Campaign Game"
},
{
"name": "Score-and-Reset Game"
},
{
"name": "Secret Unit Deployment"
},
{
"name": "Selection Order Bid"
},
{
"name": "Semi-Cooperative Game"
},
{
"name": "Set Collection"
},
{
"name": "Simulation"
},
{
"name": "Simultaneous Action Selection"
},
{
"name": "Singing"
},
{
"name": "Single Loser Game"
},
{
"name": "Slide/Push"
},
{
"name": "Solo / Solitaire Game"
},
{
"name": "Speed Matching"
},
{
"name": "Square Grid"
},
{
"name": "Stacking and Balancing"
},
{
"name": "Stat Check Resolution"
},
{
"name": "Static Capture"
},
{
"name": "Stock Holding"
},
{
"name": "Storytelling"
},
{
"name": "Sudden Death Ending"
},
{
"name": "Tags"
},
{
"name": "Take That"
},
{
"name": "Targeted Clues"
},
{
"name": "Team-Based Game"
},
{
"name": "Tech Trees / Tech Tracks"
},
{
"name": "Three Dimensional Movement"
},
{
"name": "Tile Placement"
},
{
"name": "Track Movement"
},
{
"name": "Trading"
},
{
"name": "Traitor Game"
},
{
"name": "Trick-taking"
},
{
"name": "Tug of War"
},
{
"name": "Turn Order: Auction"
},
{
"name": "Turn Order: Claim Action"
},
{
"name": "Turn Order: Pass Order"
},
{
"name": "Turn Order: Progressive"
},
{
"name": "Turn Order: Random"
},
{
"name": "Turn Order: Role Order"
},
{
"name": "Turn Order: Stat-Based"
},
{
"name": "Turn Order: Time Track"
},
{
"name": "Variable Phase Order"
},
{
"name": "Variable Player Powers"
},
{
"name": "Variable Set-up"
},
{
"name": "Victory Points as a Resource"
},
{
"name": "Voting"
},
{
"name": "Worker Placement"
},
{
"name": "Worker Placement with Dice Workers"
},
{
"name": "Worker Placement, Different Worker Types"
},
{
"name": "Zone of Control"
}
]
}

272
prisma/schema.prisma Normal file
View file

@ -0,0 +1,272 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
previewFeatures = ["fullTextSearch", "fullTextIndex", "relationJoins"]
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
relationMode = "prisma"
}
model Role {
id String @id @default(cuid())
name String @unique
userRoles UserRole[]
@@map("roles")
}
model UserRole {
id String @id @default(cuid())
user User @relation(fields: [user_id], references: [id])
user_id String
role Role @relation(fields: [role_id], references: [id])
role_id String
created_at DateTime @default(now()) @db.Timestamp(6)
updated_at DateTime @updatedAt @db.Timestamp(6)
@@unique([user_id, role_id])
@@index([user_id])
@@index([role_id])
@@map("user_roles")
}
model User {
id String @id @default(cuid())
username String @unique
hashed_password String?
email String? @unique
firstName String?
lastName String?
roles UserRole[]
verified Boolean @default(false)
receiveEmail Boolean @default(false)
collection Collection?
wishlist Wishlist?
list List[]
theme String @default("system")
created_at DateTime @default(now()) @db.Timestamp(6)
updated_at DateTime @updatedAt @db.Timestamp(6)
sessions Session[]
@@map("users")
}
model Session {
id String @id @unique
userId String
ip_country String
ip_address String
expiresAt DateTime
user User @relation(references: [id], fields: [userId], onDelete: Cascade)
@@index([userId])
@@map("sessions")
}
model Collection {
id String @id @default(cuid())
user_id String @unique
user User @relation(references: [id], fields: [user_id])
items CollectionItem[]
@@index([user_id])
@@map("collections")
}
model CollectionItem {
id String @id @default(cuid())
collection_id String
collection Collection @relation(references: [id], fields: [collection_id], onDelete: Cascade)
game_id String @unique
game Game @relation(references: [id], fields: [game_id])
times_played Int
@@index([game_id, collection_id])
@@index([game_id])
@@index([collection_id])
@@map("collection_items")
}
model Wishlist {
id String @id @default(cuid())
user_id String @unique
user User @relation(references: [id], fields: [user_id])
items WishlistItem[]
@@index([user_id])
@@map("wishlists")
}
model WishlistItem {
id String @id @default(cuid())
wishlist_id String
wishlist Wishlist @relation(references: [id], fields: [wishlist_id], onDelete: Cascade)
game_id String @unique
game Game @relation(references: [id], fields: [game_id])
created_at DateTime @default(now()) @db.Timestamp(6)
updated_at DateTime @updatedAt @db.Timestamp(6)
@@index([game_id, wishlist_id])
@@index([game_id])
@@index([wishlist_id])
@@map("wishlist_items")
}
model List {
id String @id @default(cuid())
name String
user_id String @unique
user User @relation(references: [id], fields: [user_id])
items ListItem[]
@@index([user_id])
@@map("lists")
}
model ListItem {
id String @id @default(cuid())
list_id String
list List @relation(references: [id], fields: [list_id], onDelete: Cascade)
game_id String @unique
game Game @relation(references: [id], fields: [game_id])
created_at DateTime @default(now()) @db.Timestamp(6)
updated_at DateTime @updatedAt @db.Timestamp(6)
@@index([game_id, list_id])
@@index([game_id])
@@index([list_id])
@@map("list_items")
}
model Game {
id String @id @default(cuid())
name String
slug String
description String? @db.LongText
year_published Int? @db.Year
min_players Int?
max_players Int?
playtime Int?
min_playtime Int?
max_playtime Int?
min_age Int?
image_url String?
thumb_url String?
url String?
categories Category[]
mechanics Mechanic[]
designers Designer[]
publishers Publisher[]
artists Artist[]
names GameName[]
expansions Expansion[] @relation("BaseToExpansion")
expansion_of Expansion[] @relation("ExpansionToBase")
collection_items CollectionItem[]
wishlist_items WishlistItem[]
list_items ListItem[]
external_id Int @unique
last_sync_at DateTime? @db.Timestamp(6)
created_at DateTime @default(now()) @db.Timestamp(6)
updated_at DateTime @updatedAt @db.Timestamp(6)
@@fulltext([name])
@@fulltext([slug])
@@map("games")
}
model GameName {
id String @id @default(cuid())
name String
slug String
game_id String
game Game @relation(references: [id], fields: [game_id])
created_at DateTime @default(now()) @db.Timestamp(6)
updated_at DateTime @updatedAt @db.Timestamp(6)
@@index([game_id])
@@map("game_names")
}
model Publisher {
id String @id @default(cuid())
name String
slug String
external_id Int @unique
games Game[]
created_at DateTime @default(now()) @db.Timestamp(6)
updated_at DateTime @updatedAt @db.Timestamp(6)
@@fulltext([name])
@@map("publishers")
}
model Category {
id String @id @default(cuid())
name String
slug String
games Game[]
external_id Int @unique
created_at DateTime @default(now()) @db.Timestamp(6)
updated_at DateTime @updatedAt @db.Timestamp(6)
@@fulltext([name])
@@map("categories")
}
model Mechanic {
id String @id @default(cuid())
name String
slug String
games Game[]
external_id Int @unique
created_at DateTime @default(now()) @db.Timestamp(6)
updated_at DateTime @updatedAt @db.Timestamp(6)
@@fulltext([name])
@@map("mechanics")
}
model Designer {
id String @id @default(cuid())
name String
slug String
external_id Int @unique
games Game[]
created_at DateTime @default(now()) @db.Timestamp(6)
updated_at DateTime @updatedAt @db.Timestamp(6)
@@fulltext([name])
@@map("designers")
}
model Artist {
id String @id @default(cuid())
name String
slug String @unique
external_id Int @unique
games Game[]
created_at DateTime @default(now()) @db.Timestamp(6)
updated_at DateTime @updatedAt @db.Timestamp(6)
@@fulltext([name])
@@map("artists")
}
model Expansion {
id String @id @default(cuid())
base_game Game @relation(name: "BaseToExpansion", fields: [base_game_id], references: [id])
base_game_id String
game Game @relation(name: "ExpansionToBase", fields: [game_id], references: [id])
game_id String
created_at DateTime @default(now()) @db.Timestamp(6)
updated_at DateTime @updatedAt @db.Timestamp(6)
@@index([base_game_id])
@@index([game_id])
@@map("expansions")
}

84
prisma/seed.ts Normal file
View file

@ -0,0 +1,84 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log(`Start seeding ...`);
console.log('Creating roles ...');
const existingRoles = await prisma.role.findMany();
if (existingRoles.length === 0) {
await prisma.role.createMany({
data: [{ name: 'admin' }, { name: 'user' }]
});
console.log('Roles created.');
} else {
console.log('Roles already exist. No action taken.');
}
if (!await prisma.publisher.findFirst({
where: {
external_id: 9999
}
})) {
console.log('Publisher does not exist. Creating...');
await prisma.publisher.create({
data: {
name: 'Unknown',
slug: 'unknown',
external_id: 9999
}
});
}
if (!await prisma.designer.findFirst({
where: {
external_id: 9999
}
})) {
console.log('Designer does not exist. Creating...');
await prisma.designer.create({
data: {
name: 'Unknown',
slug: 'unknown',
external_id: 9999
}
});
}
if (!await prisma.artist.findFirst({
where: {
external_id: 9999
}
})) {
console.log('Artist does not exist. Creating...');
await prisma.artist.create({
data: {
name: 'Unknown',
slug: 'unknown',
external_id: 9999
}
});
}
// for (const p of userData) {
// const user = await prisma.user.create({
// data: {
// firstName: p.user.firstName,
// lastName: p.user.lastName,
// email: p.user.email,
// username: p.user.username
// }
// });
// console.log(`Created user with id: ${user.id}`);
// }
console.log(`Seeding finished.`);
}
main()
.catch(async (e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View file

@ -1,109 +0,0 @@
@import '@fontsource/fira-mono';
:root {
font-family: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--font-mono: 'Fira Mono', monospace;
--pure-white: #ffffff;
--primary-color: #b9c6d2;
--secondary-color: #d0dde9;
--tertiary-color: #edf0f8;
--accent-color: #ff3e00;
--heading-color: rgba(0, 0, 0, 0.7);
--text-color: #444444;
--background-without-opacity: rgba(255, 255, 255, 0.7);
--column-width: 42rem;
--column-margin-top: 4rem;
--z-highest: 100;
--cardBorderRadius: 12px;
}
body {
min-height: 100vh;
margin: 0;
background-color: var(--primary-color);
background: linear-gradient(
180deg,
var(--primary-color) 0%,
var(--secondary-color) 10.45%,
var(--tertiary-color) 41.35%
);
}
body::before {
content: '';
width: 80vw;
height: 100vh;
position: absolute;
top: 0;
left: 10vw;
z-index: -1;
background: radial-gradient(
50% 50% at 50% 50%,
var(--pure-white) 0%,
rgba(255, 255, 255, 0) 100%
);
opacity: 0.05;
}
#svelte {
min-height: 100vh;
display: flex;
flex-direction: column;
}
h1,
h2,
p {
font-weight: 400;
color: var(--heading-color);
}
p {
line-height: 1.5;
}
a {
color: var(--accent-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
h1 {
font-size: 2rem;
text-align: center;
}
h2 {
font-size: 1rem;
}
pre {
font-size: 16px;
font-family: var(--font-mono);
background-color: rgba(255, 255, 255, 0.45);
border-radius: 3px;
box-shadow: 2px 2px 6px rgb(255 255 255 / 25%);
padding: 0.5em;
overflow-x: auto;
color: var(--text-color);
}
input,
button {
font-size: inherit;
font-family: inherit;
}
button:focus:not(:focus-visible) {
outline: none;
}
@media (min-width: 720px) {
h1 {
font-size: 2.4rem;
}
}

57
src/app.d.ts vendored
View file

@ -1,14 +1,55 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
interface Locals {
userid: string;
import type { PrismaClient, User } from '@prisma/client';
type User = Omit<User, 'created_at' | 'updated_at'>;
// src/app.d.ts
declare global {
namespace App {
interface PageData {
flash?: { type: 'success' | 'error' | 'info'; message: string };
}
interface Locals {
auth: import('lucia').AuthRequest;
user: import('lucia').User | null;
session: import('lucia').Session | null;
prisma: PrismaClient;
startTimer: number;
ip: string;
country: string;
error: string;
errorId: string;
errorStackTrace: string;
message: unknown;
track: unknown;
}
interface Error {
code?: string;
errorId?: string;
}
}
// interface PageData {}
// interface Error {}
// interface Platform {}
interface Document {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
startViewTransition: (callback: any) => void; // Add your custom property/method here
}
}
// interface PageData {}
// interface Error {}
// interface Platform {}
// /// <reference types="lucia" />
// declare global {
// namespace Lucia {
// type Auth = import('$lib/server/lucia').Auth;
// type DatabaseUserAttributes = User;
// type DatabaseSessionAttributes = {};
// }
// }
// THIS IS IMPORTANT!!!
export {};

View file

@ -1,39 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="robots" content="noindex, nofollow" />
<meta charset="utf-8" />
<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" />
<script>
const htmlElement = document.documentElement;
const userTheme = localStorage.theme;
const userFont = localStorage.font;
// const htmlElement = document.documentElement;
// const userTheme = localStorage.theme;
// const userFont = localStorage.font;
const prefersDarkMode = window.matchMedia('prefers-color-scheme: dark').matches;
const prefersLightMode = window.matchMedia('prefers-color-scheme: light').matches;
// const prefersDarkMode = window.matchMedia('prefers-color-scheme: dark').matches;
// const prefersLightMode = window.matchMedia('prefers-color-scheme: light').matches;
// check if the user set a theme
if (userTheme) {
htmlElement.dataset.theme = userTheme;
}
// // check if the user set a theme
// if (userTheme) {
// htmlElement.dataset.theme = userTheme;
// }
// otherwise check for user preference
if (!userTheme && prefersDarkMode) {
htmlElement.dataset.theme = '🌛 Night';
localStorage.theme = '🌛 Night';
}
// // otherwise check for user preference
// if (!userTheme && prefersDarkMode) {
// htmlElement.dataset.theme = '🌛 Night';
// localStorage.theme = '🌛 Night';
// }
if (!userTheme && prefersLightMode) {
htmlElement.dataset.theme = '☀️ Daylight';
localStorage.theme = '☀️ Daylight';
}
// if (!userTheme && prefersLightMode) {
// htmlElement.dataset.theme = '☀️ Daylight';
// localStorage.theme = '☀️ Daylight';
// }
// if nothing is set default to dark mode
if (!userTheme && !prefersDarkMode && !prefersLightMode) {
htmlElement.dataset.theme = '🌛 Night';
localStorage.theme = '🌛 Night';
}
// // if nothing is set default to dark mode
// if (!userTheme && !prefersDarkMode && !prefersLightMode) {
// htmlElement.dataset.theme = '🌛 Night';
// localStorage.theme = '🌛 Night';
// }
</script>
%sveltekit.head%
</head>

24
src/hooks.client.ts Normal file
View file

@ -0,0 +1,24 @@
// import { dev } from '$app/environment';
// import { handleErrorWithSentry, Replay } from '@sentry/sveltekit';
// import * as Sentry from '@sentry/sveltekit';
// TODO: Fix Sentry
// Sentry.init({
// dsn: 'https://742e43279df93a3c4a4a78c12eb1f879@o4506057768632320.ingest.sentry.io/4506057770401792',
// tracesSampleRate: 1.0,
// // This sets the sample rate to be 10%. You may want this to be 100% while
// // in development and sample at a lower rate in production
// replaysSessionSampleRate: 0.1,
// // If the entire session is not sampled, use the below sample rate to sample
// // sessions when an error occurs.
// replaysOnErrorSampleRate: 1.0,
// // If you don't want to use Session Replay, just remove the line below:
// integrations: [new Replay()],
// environment: dev ? 'development' : 'production'
// });
// // If you have a custom error handler, pass it to `handleErrorWithSentry`
// export const handleError = handleErrorWithSentry();

View file

@ -1,16 +0,0 @@
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
let userid = event.cookies.get('userid');
if (!userid) {
// if this is the first time the user has visited this app,
// set a cookie so that we recognise them when they return
userid = crypto.randomUUID();
event.cookies.set('userid', userid, { path: '/' });
}
event.locals.userid = userid;
return resolve(event);
};

60
src/hooks.server.ts Normal file
View file

@ -0,0 +1,60 @@
// import * as Sentry from '@sentry/sveltekit';
import { sequence } from '@sveltejs/kit/hooks';
import type { Handle } from '@sveltejs/kit';
import { dev } from '$app/environment';
import { lucia } from '$lib/server/auth';
// TODO: Fix Sentry as it is not working on SvelteKit v2
// Sentry.init({
// dsn: 'https://742e43279df93a3c4a4a78c12eb1f879@o4506057768632320.ingest.sentry.io/4506057770401792',
// tracesSampleRate: 1,
// environment: dev ? 'development' : 'production',
// enabled: !dev
// });
export const authentication: Handle = async function ({ event, resolve }) {
const startTimer = Date.now();
event.locals.startTimer = startTimer;
const ip = event.request.headers.get('x-forwarded-for') as string;
const country = event.request.headers.get('x-vercel-ip-country') as string;
event.locals.ip = dev ? '127.0.0.1' : ip; // || event.getClientAddress();
event.locals.country = dev ? 'us' : country;
const sessionId = event.cookies.get(lucia.sessionCookieName);
if (!sessionId) {
event.locals.user = null;
event.locals.session = null;
return resolve(event);
}
const { session, user } = await lucia.validateSession(sessionId);
if (session && session.fresh) {
const sessionCookie = lucia.createSessionCookie(session.id);
console.log('sessionCookie', JSON.stringify(sessionCookie, null, 2));
// sveltekit types deviates from the de-facto standard
// you can use 'as any' too
event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: '.',
...sessionCookie.attributes
});
}
if (!session) {
const sessionCookie = lucia.createBlankSessionCookie();
console.log('blank sessionCookie', JSON.stringify(sessionCookie, null, 2));
event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: ".",
...sessionCookie.attributes
});
}
event.locals.user = user;
event.locals.session = session;
return resolve(event);
};
export const handle: Handle = sequence(
// Sentry.sentryHandle(),
authentication
);
// export const handleError = Sentry.handleErrorWithSentry();

View file

@ -1,103 +0,0 @@
<script lang="ts">
import { spring } from 'svelte/motion';
let count = 0;
const displayed_count = spring();
$: displayed_count.set(count);
$: offset = modulo($displayed_count, 1);
function modulo(n: number, m: number) {
// handle negative numbers
return ((n % m) + m) % m;
}
</script>
<div class="counter">
<button on:click={() => (count -= 1)} aria-label="Decrease the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5" />
</svg>
</button>
<div class="counter-viewport">
<div class="counter-digits" style="transform: translate(0, {100 * offset}%)">
<strong class="hidden" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong>
<strong>{Math.floor($displayed_count)}</strong>
</div>
</div>
<button on:click={() => (count += 1)} aria-label="Increase the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" />
</svg>
</button>
</div>
<style>
.counter {
display: flex;
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin: 1rem 0;
}
.counter button {
width: 2em;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border: 0;
background-color: transparent;
touch-action: manipulation;
color: var(--text-color);
font-size: 2rem;
}
.counter button:hover {
background-color: var(--secondary-color);
}
svg {
width: 25%;
height: 25%;
}
path {
vector-effect: non-scaling-stroke;
stroke-width: 2px;
stroke: var(--text-color);
}
.counter-viewport {
width: 8em;
height: 4em;
overflow: hidden;
text-align: center;
position: relative;
}
.counter-viewport strong {
position: absolute;
display: flex;
width: 100%;
height: 100%;
font-weight: 400;
color: var(--accent-color);
font-size: 4rem;
align-items: center;
justify-content: center;
}
.counter-digits {
position: absolute;
width: 100%;
height: 100%;
}
.hidden {
top: -100%;
user-select: none;
}
</style>

View file

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -0,0 +1,112 @@
<script lang="ts">
import { enhance } from "$app/forms";
import { fly } from "svelte/transition";
import { createSelect, melt } from "@melt-ui/svelte";
import { Check, ChevronDown, MinusCircle, PlusCircle } from "lucide-svelte";
import { Button } from '$components/ui/button';
import type { Collection, Wishlist } from "@prisma/client";
export let game_id: string;
export let collection: Collection;
export let wishlist: Wishlist;
export let in_wishlist = false;
export let in_collection = false;
export let lists = [];
// const handleChange = ({ curr, next }) => {
// console.log({ curr, next });
// return next;
// }
// const {
// elements: { trigger, menu, option, label, group, groupLabel },
// states: { valueLabel, open },
// helpers: { isSelected },
// } = createSelect({
// forceVisible: true,
// onValueChange: handleChange
// });
// console.log({ in_collection, in_wishlist });
// let options: Record<string, string> = {};
// let list_of_lists = [];
// if (!in_collection) {
// options[collection.id] = 'Add to collection';
// }
// if (!in_wishlist) {
// options[wishlist.id] = 'Add to wishlist';
// }
// lists.forEach((list) => {
// if (!list?.in_list) {
// options[list.id] = list.name;
// }
// });
</script>
<div class="flex gap-1">
{#if in_wishlist}
<form method="POST" action={`/wishlist?/remove`} use:enhance>
<input type="hidden" name="id" value={game_id} />
<Button class="flex gap-1" variant="destructive" type="submit">
<MinusCircle class="square-5" /> Remove from wishlist
</Button>
</form>
{:else}
<form method="POST" action='/wishlist?/add' use:enhance>
<input type="hidden" name="id" value={game_id} />
<Button class="flex gap-1" type="submit">
<PlusCircle class="square-5" /> Add to wishlist
</Button>
</form>
{/if}
{#if in_collection}
<form method="POST" action='/collection?/remove' use:enhance>
<input type="hidden" name="id" value={game_id} />
<Button class="flex gap-1" type="submit" variant="destructive">
<MinusCircle class="square-5" /> Remove from collection
</Button>
</form>
{:else}
<form method="POST" action='/collection?/add' use:enhance>
<input type="hidden" name="id" value={game_id} />
<Button class="flex gap-1" type="submit">
<PlusCircle class="square-5" /> Add to collection
</Button>
</form>
{/if}
</div>
<!-- <div class="flex flex-col gap-1"> -->
<!-- svelte-ignore a11y-label-has-associated-control - $label contains the 'for' attribute -->
<!-- <button
class="flex h-10 min-w-[220px] items-center justify-between rounded-md bg-white px-3 py-2 text-black-500 transition-opacity hover:opacity-90"
use:melt={$trigger}
aria-label="Wishlist"
>
{$valueLabel || 'Add to...'}
<ChevronDown class="square-5" />
</button>
{#if $open}
<div
class="z-10 flex max-h-[360px] flex-col overflow-y-auto rounded-md bg-white p-1 focus:!ring-0"
use:melt={$menu}
transition:fly={{ duration: 150, y: -5 }}
>
{#each Object.entries(options) as [key, value]}
<div
class="flex relative cursor-pointer rounded-md py-1 pl-8 pr-4 text-neutral-800 focus:z-10 focus:text-purple-700 data-[highlighted]:bg-purple-50 data-[selected]:bg-purple-100 data-[highlighted]:text-purple-900 data-[selected]:text-purple-900"
use:melt={$option({ value: key, label: value })}
>
{value}
{#if $isSelected(key)}
<div class="check">
<Check class="square-4" />
</div>
{/if}
</div>
{/each}
</div>
{/if}
</div> -->

View file

@ -0,0 +1,27 @@
<script>
import { PUBLIC_SITE_URL } from "$env/static/public";
</script>
<footer>
<p>Bored Game &copy; {new Date().getFullYear()} | Built by <a target="__blank" href="https://bradleyshellnut.com">Bradley Shellnut</a> | {PUBLIC_SITE_URL}</p>
</footer>
<style lang="postcss">
footer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 40px;
}
footer a {
font-weight: bold;
}
@media (min-width: 480px) {
footer {
padding: 40px 0;
}
}
</style>

View file

@ -0,0 +1,64 @@
<script lang="ts">
import type { GameType, SavedGameType } from '$lib/types';
import * as Card from "$lib/components/ui/card";
import type { CollectionItem } from '@prisma/client';
export let game: GameType | CollectionItem;
export let detailed: boolean = false;
export let variant: 'default' | 'compact' = 'default';
// Naive and assumes description is only on our GameType at the moment
function isGameType(game: GameType | SavedGameType): game is GameType {
return (game as GameType).description !== undefined;
}
</script>
<article class="grid grid-template-cols-2 gap-4">
<Card.Root class={variant === 'compact' ? 'game-card-compact' : ''}>
<Card.Header>
<Card.Title class="game-card-header">
<span style:--transition-name="game-name-{game.slug}">
{game.name}
{#if game?.year_published}
({game?.year_published})
{/if}
</span>
</Card.Title>
</Card.Header>
<Card.Content class={variant === 'compact' ? 'pt-6' : ''}>
<a
class="thumbnail"
href={`/game/${game.id}`}
title={`View ${game.name}`}
data-sveltekit-preload-data
>
<img src={game.thumb_url} alt={`Image of ${game.name}`} loading="lazy" decoding="async" />
<div class="game-details">
{#if game?.players}
<p>Players: {game.players}</p>
<p>Time: {game.playtime} minutes</p>
{#if isGameType(game) && game?.min_age}
<p>Min Age: {game.min_age}</p>
{/if}
{#if detailed && isGameType(game) && game?.description}
<div class="description">{@html game.description}</div>
{/if}
{/if}
</div>
</a>
</Card.Content>
</Card.Root>
</article>
<style lang="postcss">
:global(.game-card-compact) {
display: flex;
place-items: center;
.game-card-header {
span {
view-transition-name: var(--transition-name);
}
}
}
</style>

View file

@ -0,0 +1,158 @@
<script lang="ts">
import { applyAction, enhance } from '$app/forms';
import toast from 'svelte-french-toast';
import { ListChecks, ListTodo, LogOut, User } from 'lucide-svelte';
import * as DropdownMenu from "$components/ui/dropdown-menu";
import * as Avatar from "$components/ui/avatar";
import { invalidateAll } from '$app/navigation';
import Logo from '$components/logo.svelte';
export let user: User | null;
let avatar = user?.username.slice(0, 1).toUpperCase() || '?';
</script>
<header>
<div class="corner">
<a href="/" title="Home">
<div class="logo-image">
<Logo />
</div>
Bored Game
</a>
</div>
<!-- <TextSearch /> -->
<nav>
{#if user}
<a href="/collection" title="Go to your collection" data-sveltekit-preload-data>Collection</a>
<a href="/wishlist" title="Go to your wishlist" data-sveltekit-preload-data>Wishlist</a>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Avatar.Root asChild>
<Avatar.Fallback class="text-3xl font-medium text-magnum-700 h-16 w-16 bg-neutral-100">
{avatar}
</Avatar.Fallback>
</Avatar.Root>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.Label>My Account</DropdownMenu.Label>
<DropdownMenu.Separator />
<a href="/profile">
<DropdownMenu.Item>
<User class="mr-2 h-4 w-4" />
<span>Profile</span>
</DropdownMenu.Item>
</a>
<a href="/collection">
<DropdownMenu.Item>
<ListChecks class="mr-2 h-4 w-4" />
<span>Collection</span>
</DropdownMenu.Item>
</a>
<a href="/wishlist">
<DropdownMenu.Item>
<ListTodo class="mr-2 h-4 w-4" />
<span>Wishlist</span>
</DropdownMenu.Item>
</a>
<form
use:enhance={() => {
return async ({ result }) => {
console.log(result);
if (result.type === 'success' || result.type === 'redirect') {
toast.success('Logged Out');
} else if (result.type === 'error') {
console.log(result);
toast.error(`Error: ${result.error.message}`);
} else {
toast.error(`Something went wrong.`);
console.log(result);
}
await invalidateAll();
await applyAction(result);
};
}}
action="/logout"
method="POST"
>
<button type="submit" class="">
<DropdownMenu.Item>
<div class="flex items-center gap-1">
<LogOut class="mr-2 h-4 w-4"/>
<span>Sign out</span>
</div>
</DropdownMenu.Item>
</button>
</form>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
{/if}
{#if !user}
<a href="/login">
<span class="flex-auto">Login</span></a
>
<a href="/sign-up">
<span class="flex-auto">Sign Up</span></a
>
{/if}
</nav>
</header>
<style lang="postcss">
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--containerPadding);
font-size: 1.6rem;
@media (max-width: 1000px) {
padding-top: 1.25rem;
}
}
.corner {
margin-left: 1rem;
}
.corner a {
display: flex;
place-items: center;
gap: 0.5rem;
width: 100%;
height: 100%;
font-size: 1.125rem;
line-height: 1.75rem;
font-weight: 500;
}
.logo-image {
width: 2rem;
height: 2rem;
}
nav {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin: 1rem;
--background: rgba(255, 255, 255, 0.7);
}
nav a {
color: var(--heading-color);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
text-decoration: none;
transition: color 0.2s linear;
}
a:hover {
text-decoration: underline;
color: var(--accent-color);
}
</style>

View file

@ -19,5 +19,6 @@
grid-template-columns: repeat(2, auto);
place-items: center;
gap: 0.25rem;
margin: 1rem;
}
</style>

View file

@ -0,0 +1,9 @@
<script lang="ts">
import { Button } from '$components/ui/button';
</script>
<Button type="submit">Add to wishlist</Button>
<style lang="postcss">
</style>

View file

@ -1,6 +1,5 @@
<script lang="ts">
export let kind = 'primary';
export let size;
export let icon = false;
export let disabled = false;
</script>
@ -9,29 +8,22 @@
<slot />
</button>
<style>
<style lang="postcss">
button {
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 4px;
border-radius: 10px;
margin: 0;
padding: 1rem;
min-width: 20rem;
max-width: 30rem;
min-height: 6.2rem;
text-align: start;
background-color: var(--color-btn-primary-active);
}
.danger {
background-color: var(--warning);
}
.danger:hover {
background-color: var(--warning-hover);
}
.btn-icon {
display: grid;
grid-template-columns: repeat(2, auto);
gap: 1rem;
@media (min-width: 1000px) {
min-width: 23.5rem;
}
}
</style>

View file

@ -1,13 +1,13 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import {
Dialog,
DialogDescription,
DialogOverlay,
DialogTitle
} from '@rgossiaux/svelte-headlessui';
import { boredState } from '$root/lib/stores/boredState';
import { collectionStore } from '$root/lib/stores/collectionStore';
// import {
// Dialog,
// DialogDescription,
// DialogOverlay,
// DialogTitle
// } from '@rgossiaux/svelte-headlessui';
import { boredState } from '$lib/stores/boredState';
import { collectionStore } from '$lib/stores/collectionStore';
import { browser } from '$app/environment';
function clearCollection() {
@ -21,18 +21,18 @@
$: isOpen = $boredState?.dialog?.isOpen;
</script>
<Dialog
<!-- <Dialog
open={isOpen}
on:close={() => {
boredState.update((n) => ({ ...n, dialog: { isOpen: false } }));
}}
static
>
<div transition:fade>
<DialogOverlay class="dialog-overlay" />
> -->
<div transition:fade|global>
<!-- <DialogOverlay class="dialog-overlay" /> -->
<div class="dialog">
<DialogTitle>Clear collection</DialogTitle>
<DialogDescription>Are you sure you want to clear your collection?</DialogDescription>
<!-- <DialogTitle>Clear collection</DialogTitle> -->
<!-- <DialogDescription>Are you sure you want to clear your collection?</DialogDescription> -->
<div class="dialog-footer">
<button class="remove" on:click={clearCollection}>Clear</button>
@ -44,7 +44,7 @@
</div>
</div>
</div>
</Dialog>
<!-- </Dialog> -->
<style lang="scss">
.dialog {

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { browser } from '$app/environment';
import { boredState } from '$root/lib/stores/boredState';
import { wishlistStore } from '$root/lib/stores/wishlistStore';
import { boredState } from '$lib/stores/boredState';
import { wishlistStore } from '$lib/stores/wishlistStore';
import DefaultDialog from './DefaultDialog.svelte';
function clearWishlist() {
@ -48,7 +48,7 @@
gap: 2rem;
margin: 1rem 0;
button {
& button {
display: flex;
place-content: center;
gap: 1rem;

View file

@ -1,13 +1,13 @@
<script lang="ts">
import { type SvelteComponent, createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
import {
Dialog,
DialogDescription,
DialogOverlay,
DialogTitle
} from '@rgossiaux/svelte-headlessui';
import { boredState } from '$root/lib/stores/boredState';
// import {
// Dialog,
// DialogDescription,
// DialogOverlay,
// DialogTitle
// } from '@rgossiaux/svelte-headlessui';
import { boredState } from '$lib/stores/boredState';
export let title: string;
export let description: string;
@ -16,7 +16,7 @@
export let passive = false;
export let primaryButtonText = '';
export let primaryButtonDisabled = false;
export let primaryButtonIcon: typeof SvelteComponent = undefined;
export let primaryButtonIcon: typeof SvelteComponent<any> = undefined;
export let primaryButtonIconDescription = '';
export let secondaryButtonText = '';
@ -25,19 +25,19 @@
$: isOpen = $boredState?.dialog?.isOpen;
</script>
<Dialog
<!-- <Dialog
open={isOpen}
on:close={() => {
dispatch('close');
}}
static
>
<div transition:fade>
<DialogOverlay class="dialog-overlay" />
> -->
<div transition:fade|global>
<!-- <DialogOverlay class="dialog-overlay" /> -->
<div class="dialog">
<DialogTitle>{title}</DialogTitle>
<!-- <DialogTitle>{title}</DialogTitle> -->
{#if description}
<DialogDescription>{description}</DialogDescription>
<!-- <DialogDescription>{description}</DialogDescription> -->
{/if}
<div class="dialog-footer">
@ -72,7 +72,7 @@
</div>
</div>
</div>
</Dialog>
<!-- </Dialog> -->
<style lang="scss">
.dialog {

View file

@ -1,14 +1,19 @@
<script lang="ts">
import { fade } from 'svelte/transition';
// import { Button, buttonVariants } from '$components/ui/button';
import { Button, buttonVariants } from '$components/ui/button';
import {
Dialog,
DialogDescription,
DialogOverlay,
DialogTitle
} from '@rgossiaux/svelte-headlessui';
import { boredState } from '$root/lib/stores/boredState';
import { collectionStore } from '$root/lib/stores/collectionStore';
import { removeFromCollection } from '$root/lib/util/manipulateCollection';
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from '$components/ui/dialog';
import { boredState } from '$lib/stores/boredState';
import { collectionStore } from '$lib/stores/collectionStore';
import { removeFromCollection } from '$lib/utils/manipulateCollection';
import { browser } from '$app/environment';
function removeGame() {
@ -24,14 +29,42 @@
$: isOpen = $boredState?.dialog?.isOpen;
</script>
<Dialog
<Dialog modal={true}>
<DialogTrigger class={buttonVariants({ variant: "outline" })}>
Remove from collection
</DialogTrigger>
<DialogContent class="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Remove from collection</DialogTitle>
<DialogDescription>
Are you sure you want to remove from your collection?
</DialogDescription>
</DialogHeader>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">Name</Label>
<Input id="name" value="Pedro Duarte" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">Username</Label>
<Input id="username" value="@peduarte" class="col-span-3" />
</div>
</div>
<DialogFooter>
<Button type="submit">Remove</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- <Dialog
open={isOpen}
on:close={() => {
boredState.update((n) => ({ ...n, dialog: { isOpen: false } }));
}}
static
>
<div transition:fade>
<div transition:fade|global>
<DialogOverlay class="dialog-overlay" />
<div class="dialog">
<DialogTitle>Remove from collection</DialogTitle>
@ -47,7 +80,7 @@
</div>
</div>
</div>
</Dialog>
</Dialog> -->
<style lang="scss">
.dialog {

View file

@ -1,14 +1,14 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import {
Dialog,
DialogDescription,
DialogOverlay,
DialogTitle
} from '@rgossiaux/svelte-headlessui';
import { boredState } from '$root/lib/stores/boredState';
import { wishlistStore } from '$root/lib/stores/wishlistStore';
import { removeFromWishlist } from '$root/lib/util/manipulateWishlist';
// import {
// Dialog,
// DialogDescription,
// DialogOverlay,
// DialogTitle
// } from '@rgossiaux/svelte-headlessui';
import { boredState } from '$lib/stores/boredState';
import { wishlistStore } from '$lib/stores/wishlistStore';
import { removeFromWishlist } from '$lib/utils/manipulateWishlist';
import { browser } from '$app/environment';
function removeGame() {
@ -24,18 +24,18 @@
$: isOpen = $boredState?.dialog?.isOpen;
</script>
<Dialog
<!-- <Dialog
open={isOpen}
on:close={() => {
boredState.update((n) => ({ ...n, dialog: { isOpen: false } }));
}}
static
>
<div transition:fade>
<DialogOverlay class="dialog-overlay" />
> -->
<div transition:fade|global>
<!-- <DialogOverlay class="dialog-overlay" /> -->
<div class="dialog">
<DialogTitle>Remove from wishlist</DialogTitle>
<DialogDescription>Are you sure you want to remove from your wishlist?</DialogDescription>
<!-- <DialogTitle>Remove from wishlist</DialogTitle> -->
<!-- <DialogDescription>Are you sure you want to remove from your wishlist?</DialogDescription> -->
<div class="dialog-footer">
<button class="remove" on:click={removeGame}>Remove</button>
@ -47,7 +47,7 @@
</div>
</div>
</div>
</Dialog>
<!-- </Dialog> -->
<style lang="scss">
.dialog {

View file

@ -1,30 +0,0 @@
<footer>
<p>Built by <a target="__blank" href="https://bradleyshellnut.com">Bradley Shellnut</a></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>
<style lang="postcss">
footer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 40px;
}
footer a {
font-weight: bold;
}
@media (min-width: 480px) {
footer {
padding: 40px 0;
}
}
</style>

View file

@ -1,187 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { Image } from 'svelte-lazy-loader';
import { fade } from 'svelte/transition';
import { MinusCircleIcon, PlusCircleIcon } from '@rgossiaux/svelte-heroicons/outline';
import type { GameType, SavedGameType } from '$lib/types';
import { collectionStore } from '$lib/stores/collectionStore';
import { wishlistStore } from '$root/lib/stores/wishlistStore';
import { addToCollection, removeFromCollection } from '$lib/util/manipulateCollection';
import { addToWishlist } from '$lib/util/manipulateWishlist';
import { browser } from '$app/environment';
export let game: GameType | SavedGameType;
export let detailed: boolean = false;
const dispatch = createEventDispatcher();
function removeGameFromWishlist() {
dispatch('handleRemoveWishlist', game);
}
function removeGameFromCollection() {
dispatch('handleRemoveCollection', game);
}
// Naive and assumes description is only on our GameType at the moment
function isGameType(game: GameType | SavedGameType): game is GameType {
return (game as GameType).description !== undefined;
}
// function lazy(img: HTMLImageElement) {
// function loaded() {
// img.classList.add('loaded');
// img.classList.remove('loading');
// }
// if (img.complete) {
// loaded();
// } else {
// img.classList.add('loading');
// img.onload = () => loaded();
// }
// }
$: existsInCollection = $collectionStore.find((item: SavedGameType) => item.id === game.id);
$: existsInWishlist = $wishlistStore.find((item: SavedGameType) => item.id === game.id);
</script>
<article class="game-container" transition:fade>
<h2>{game.name}</h2>
<a
class="thumbnail"
href={`/game/${game.id}`}
title={`View ${game.name}`}
data-sveltekit-preload-data
>
<!-- <Image src={game.thumb_url} alt={`Image of ${game.name}`} /> -->
<img src={game.thumb_url} alt={`Image of ${game.name}`} loading="lazy" decoding="async" />
<!-- loading="lazy" decoding="async" -->
</a>
<div class="game-details">
<p>Players: {game.players}</p>
<p>Time: {game.playtime} minutes</p>
{#if isGameType(game) && game?.min_age}
<p>Min Age: {game.min_age}</p>
{/if}
{#if detailed && isGameType(game) && game?.description}
<div class="description">{@html game.description}</div>
{/if}
</div>
<div class="game-buttons">
{#if existsInCollection}
<button
aria-label="Remove from collection"
class="btn remove"
type="button"
on:click={() => {
removeGameFromCollection();
}}><span>Remove from Collection</span> <MinusCircleIcon width="24" height="24" /></button
>
{:else}
<button
aria-label="Add to collection"
class="btn"
type="button"
on:click={() => {
addToCollection(game);
if (browser) {
localStorage.collection = JSON.stringify($collectionStore);
}
}}><span>Add to collection</span> <PlusCircleIcon width="24" height="24" /></button
>
{/if}
{#if existsInWishlist}
<button
aria-label="Remove from wishlist"
class="btn remove"
type="button"
on:click={() => {
removeGameFromWishlist();
}}><span>Remove from Wishlist</span> <MinusCircleIcon width="24" height="24" /></button
>
{:else}
<button
aria-label="Add to wishlist"
class="btn"
type="button"
on:click={() => {
addToWishlist(game);
if (browser) {
localStorage.wishlist = JSON.stringify($wishlistStore);
}
}}><span>Add to wishlist</span> <PlusCircleIcon width="24" height="24" /></button
>
{/if}
</div>
</article>
<style lang="scss">
img {
max-height: 200px;
}
button {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
width: 100%;
border-radius: 10px;
padding: 1rem;
background-color: var(--color-btn-primary-active);
}
.game-container {
display: grid;
grid-template-rows: repeat(auto-fill, 1fr);
place-items: center;
text-align: center;
@media (max-width: 650px) {
max-width: none;
}
gap: var(--spacing-16);
padding: var(--spacing-16) var(--spacing-16);
transition: all 0.3s;
border-radius: 8px;
background-color: var(--primary);
&:hover {
background-color: hsla(222, 9%, 65%, 1);
}
/* .game-info {
display: grid;
place-items: center;
gap: 0.75rem;
margin: 0.2rem;
} */
.game-details {
p,
a {
padding: 0.25rem;
}
}
.game-buttons {
display: grid;
gap: 1rem;
.btn {
max-height: 100px;
text-align: start;
}
.remove {
background-color: var(--warning);
&:hover {
background-color: var(--warning-hover);
}
}
}
}
</style>

View file

@ -1,79 +0,0 @@
<script lang="ts">
import Profile from '../preferences/profile.svelte';
import logo from './bored-game.png';
</script>
<header>
<div class="corner">
<a href="/" title="Home">
<img src={logo} alt="Bored Game Home" />
</a>
</div>
<nav>
<a href="/collection" title="Go to your collection" data-sveltekit-preload-data>Collection</a>
<a href="/wishlist" title="Go to your wishlist" data-sveltekit-preload-data>Wishlist</a>
<Profile />
</nav>
</header>
<style lang="scss">
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--containerPadding);
font-size: 1.6rem;
@media (max-width: 1000px) {
padding-top: 1.25rem;
}
}
.corner {
width: 3em;
height: 3em;
}
.corner a {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.corner img {
width: 2em;
height: 2em;
object-fit: contain;
}
nav {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin: 1rem;
--background: rgba(255, 255, 255, 0.7);
}
nav a {
display: flex;
height: 100%;
align-items: center;
padding: 0 1em;
color: var(--heading-color);
font-weight: 700;
/* font-size: 0.8rem; */
text-transform: uppercase;
letter-spacing: 0.1em;
text-decoration: none;
transition: color 0.2s linear;
}
a:hover {
text-decoration: underline;
color: var(--accent-color);
}
</style>

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.1566,22.8189c-10.4-14.8851-30.94-19.2971-45.7914-9.8348L22.2825,29.6078A29.9234,29.9234,0,0,0,8.7639,49.6506a31.5136,31.5136,0,0,0,3.1076,20.2318A30.0061,30.0061,0,0,0,7.3953,81.0653a31.8886,31.8886,0,0,0,5.4473,24.1157c10.4022,14.8865,30.9423,19.2966,45.7914,9.8348L84.7167,98.3921A29.9177,29.9177,0,0,0,98.2353,78.3493,31.5263,31.5263,0,0,0,95.13,58.117a30,30,0,0,0,4.4743-11.1824,31.88,31.88,0,0,0-5.4473-24.1157" style="fill:#ff3e00"/><path d="M45.8171,106.5815A20.7182,20.7182,0,0,1,23.58,98.3389a19.1739,19.1739,0,0,1-3.2766-14.5025,18.1886,18.1886,0,0,1,.6233-2.4357l.4912-1.4978,1.3363.9815a33.6443,33.6443,0,0,0,10.203,5.0978l.9694.2941-.0893.9675a5.8474,5.8474,0,0,0,1.052,3.8781,6.2389,6.2389,0,0,0,6.6952,2.485,5.7449,5.7449,0,0,0,1.6021-.7041L69.27,76.281a5.4306,5.4306,0,0,0,2.4506-3.631,5.7948,5.7948,0,0,0-.9875-4.3712,6.2436,6.2436,0,0,0-6.6978-2.4864,5.7427,5.7427,0,0,0-1.6.7036l-9.9532,6.3449a19.0329,19.0329,0,0,1-5.2965,2.3259,20.7181,20.7181,0,0,1-22.2368-8.2427,19.1725,19.1725,0,0,1-3.2766-14.5024,17.9885,17.9885,0,0,1,8.13-12.0513L55.8833,23.7472a19.0038,19.0038,0,0,1,5.3-2.3287A20.7182,20.7182,0,0,1,83.42,29.6611a19.1739,19.1739,0,0,1,3.2766,14.5025,18.4,18.4,0,0,1-.6233,2.4357l-.4912,1.4978-1.3356-.98a33.6175,33.6175,0,0,0-10.2037-5.1l-.9694-.2942.0893-.9675a5.8588,5.8588,0,0,0-1.052-3.878,6.2389,6.2389,0,0,0-6.6952-2.485,5.7449,5.7449,0,0,0-1.6021.7041L37.73,51.719a5.4218,5.4218,0,0,0-2.4487,3.63,5.7862,5.7862,0,0,0,.9856,4.3717,6.2437,6.2437,0,0,0,6.6978,2.4864,5.7652,5.7652,0,0,0,1.602-.7041l9.9519-6.3425a18.978,18.978,0,0,1,5.2959-2.3278,20.7181,20.7181,0,0,1,22.2368,8.2427,19.1725,19.1725,0,0,1,3.2766,14.5024,17.9977,17.9977,0,0,1-8.13,12.0532L51.1167,104.2528a19.0038,19.0038,0,0,1-5.3,2.3287" style="fill:#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,11 +1,11 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption
} from '@rgossiaux/svelte-headlessui';
// import {
// Listbox,
// ListboxButton,
// ListboxOptions,
// ListboxOption
// } from '@rgossiaux/svelte-headlessui';
const shows = [
{ id: 1, name: 'Cowboy Bebop', completed: false },
@ -22,8 +22,8 @@
<h4>Listbox</h4>
<div class="listbox">
<Listbox value={selected} on:change={(event) => (selected = event.detail)} let:open>
<ListboxButton class="button">
<!-- <Listbox value={selected} on:change={(event) => (selected = event.detail)} let:open> -->
<!-- <ListboxButton class="button"> -->
<span>{selected.name}</span>
<svg
width="20"
@ -40,25 +40,25 @@
clip-rule="evenodd"
/>
</svg>
</ListboxButton>
<!-- </ListboxButton> -->
{#if open}
<div transition:fade={{ duration: 200 }}>
<ListboxOptions class="options">
<div transition:fade|global={{ duration: 200 }}>
<!-- <ListboxOptions class="options"> -->
{#each shows as anime (anime.id)}
<ListboxOption
<!-- <ListboxOption
class="option"
value={anime}
disabled={anime.completed}
let:active
let:selected
>
> -->
<span class:active class:selected>{anime.name}</span>
</ListboxOption>
<!-- </ListboxOption> -->
{/each}
</ListboxOptions>
<!-- </ListboxOptions> -->
</div>
{/if}
</Listbox>
<!-- </Listbox> -->
</div>
<!-- ... -->

View file

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-go-game" viewBox="0 0 24 24"
stroke-width="1" stroke="var(--fg)" fill="none" stroke-linecap="round" stroke-linejoin="round">
<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" />
<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>

After

Width:  |  Height:  |  Size: 671 B

View file

@ -2,17 +2,17 @@
// Based on https://carbon-components-svelte.onrender.com/components/Pagination
import { afterUpdate, createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
import {
Listbox,
ListboxButton,
ListboxOption,
ListboxOptions
} from '@rgossiaux/svelte-headlessui';
import {
CheckIcon,
ChevronLeftIcon,
ChevronRightIcon
} from '@rgossiaux/svelte-heroicons/outline';
// import {
// Listbox,
// ListboxButton,
// ListboxOption,
// ListboxOptions
// } from '@rgossiaux/svelte-headlessui';
// import {
// CheckIcon,
// ChevronLeftIcon,
// ChevronRightIcon
// } from '@rgossiaux/svelte-heroicons/outline';
const dispatch = createEventDispatcher();
@ -41,36 +41,36 @@
<div class="container">
<div class="listbox">
<p>Per-page:</p>
<Listbox
<!-- <Listbox
class="list-box"
value={pageSize || 10}
on:change={(e) => {
dispatch('perPageEvent', { pageSize: e.detail, page });
}}
let:open
>
<ListboxButton>{pageSize || 10}</ListboxButton>
{#if open}
<div transition:fade={{ duration: 100 }}>
<ListboxOptions static class="options">
> -->
<!-- <ListboxButton>{pageSize || 10}</ListboxButton> -->
<!-- {#if open} -->
<div transition:fade|global={{ duration: 100 }}>
<!-- <ListboxOptions static class="options"> -->
{#each pageSizes as size (size)}
<ListboxOption
<!-- <ListboxOption
value={`${size}`}
disabled={pageSizeInputDisabled}
class={({ active }) => (active ? 'active option' : 'option')}
style="display: flex; gap: 1rem; padding: 1rem;"
let:selected
>
{#if selected}
<CheckIcon height="24" width="24" />
{/if}
<span class="size-option" class:selected>{size.toString()}</span>
</ListboxOption>
> -->
<!-- {#if selected} -->
<!-- <CheckIcon height="24" width="24" /> -->
<!-- {/if} -->
<!-- <span class="size-option" class:selected>{size.toString()}</span> -->
<!-- </ListboxOption> -->
{/each}
</ListboxOptions>
<!-- </ListboxOptions> -->
</div>
{/if}
</Listbox>
<!-- {/if} -->
<!-- </Listbox> -->
</div>
<p>
Page {page || 1} of {totalPages || 1}
@ -90,7 +90,7 @@
dispatch('previousPageEvent', { page });
}}
>
<ChevronLeftIcon width="24" height="24" />
<!-- <ChevronLeftIcon width="24" height="24" /> -->
<p class="word">{backwardText || 'Prev'}</p>
</button>
<button
@ -103,7 +103,7 @@
}}
>
<p class="word">{forwardText || 'Next'}</p>
<ChevronRightIcon width="24" height="24" />
<!-- <ChevronRightIcon width="24" height="24" /> -->
</button>
</div>
</div>

View file

@ -1,9 +1,9 @@
<script lang="ts">
import { browser } from '$app/environment';
import { boredState } from '$root/lib/stores/boredState';
import { collectionStore } from '$root/lib/stores/collectionStore';
import { ToastType } from '$root/lib/types';
import { SaveIcon, ShareIcon, TrashIcon } from '@rgossiaux/svelte-heroicons/outline';
import { boredState } from '$lib/stores/boredState';
import { collectionStore } from '$lib/stores/collectionStore';
import { ToastType } from '$lib/types';
// import { SaveIcon, ShareIcon, TrashIcon } from '@rgossiaux/svelte-heroicons/outline';
import ClearCollectionDialog from '../dialog/ClearCollectionDialog.svelte';
import { toast } from '../toast/toast';
@ -48,13 +48,15 @@
</div>
<div class="collection-buttons">
<button type="button" aria-label="Export Collection" on:click={() => exportCollection()}
><ShareIcon width="24" height="24" />Export</button
>
<!-- <ShareIcon width="24" height="24" /> -->
Export</button
>
<!-- <button type="button" aria-label="Save Collection" on:click={() => saveCollection()}
><SaveIcon width="24" height="24" />Save</button
> -->
<button type="button" aria-label="Clear saved collection" on:click={() => clearCollection()}>
<TrashIcon width="24" height="24" />Clear
<!-- <TrashIcon width="24" height="24" />Clear -->
</button>
</div>
</div>

View file

@ -1,9 +1,9 @@
<script lang="ts">
import { browser } from '$app/environment';
import { boredState } from '$root/lib/stores/boredState';
import { wishlistStore } from '$root/lib/stores/wishlistStore';
import { ToastType } from '$root/lib/types';
import { SaveIcon, ShareIcon, TrashIcon } from '@rgossiaux/svelte-heroicons/outline';
import { boredState } from '$lib/stores/boredState';
import { wishlistStore } from '$lib/stores/wishlistStore';
import { ToastType } from '$lib/types';
// import { SaveIcon, ShareIcon, TrashIcon } from '@rgossiaux/svelte-heroicons/outline';
import ClearWishlistDialog from '../dialog/ClearWishlistDialog.svelte';
import { toast } from '../toast/toast';
@ -48,13 +48,15 @@
</div>
<div class="wishlist-buttons">
<button type="button" aria-label="Export Wishlist" on:click={() => exportWishlist()}
><ShareIcon width="24" height="24" />Export</button
>
<!-- <ShareIcon width="24" height="24" /> -->
Export</button
>
<!-- <button type="button" aria-label="Save Wishlist" on:click={() => saveWishlist()}
><SaveIcon width="24" height="24" />Save</button
> -->
<button type="button" aria-label="Clear saved wishlist" on:click={() => clearWishlist()}>
<TrashIcon width="24" height="24" />Clear
<!-- <TrashIcon width="24" height="24" />Clear -->
</button>
</div>
</div>

View file

@ -1,22 +1,35 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { Popover, PopoverButton, PopoverPanel } from '@rgossiaux/svelte-headlessui';
import { CogIcon } from '@rgossiaux/svelte-heroicons/outline';
import { collectionStore } from '$root/lib/stores/collectionStore';
import cogOutline from '@iconify-icons/mdi/cog-outline';
import Themes from './themes.svelte';
import GameCollection from './gameCollection.svelte';
import GameWishlist from './gameWishlist.svelte';
</script>
<div class="container">
<Popover let:open class="popover">
<PopoverButton aria-label="Preferences">
<CogIcon width="24" height="24" />
</PopoverButton>
<!-- <Popover let:open class="popover">
<PopoverButton aria-label="Preferences"> -->
<!-- <CogIcon width="24" height="24" /> -->
<!-- <iconify-icon icon="mdi:cog-outline"
width="24" height="24"
style={open ?
'transform: rotate(90deg); transition: transform 0.5s ease;'
: 'transform: rotate(0deg); transition: transform 0.5s ease;'
}
></iconify-icon> -->
<iconify-icon
icon={cogOutline}
width="24" height="24"
style={open ?
'transform: rotate(90deg); transition: transform 0.5s ease;'
: 'transform: rotate(0deg); transition: transform 0.5s ease;'
}
/>
<!-- </PopoverButton> -->
{#if open}
<div transition:fade={{ duration: 100 }}>
<PopoverPanel class="popover-panel" static>
<div transition:fade|global={{ duration: 100 }}>
<!-- <PopoverPanel class="popover-panel" static> -->
<div class="preferences">
<svg
width="24"
@ -46,10 +59,10 @@
<GameWishlist />
</div>
</div>
</PopoverPanel>
<!-- </PopoverPanel> -->
</div>
{/if}
</Popover>
<!-- </Popover> -->
</div>
<style lang="scss">

View file

@ -1,12 +1,12 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { browser } from '$app/environment';
import {
Listbox,
ListboxButton,
ListboxOption,
ListboxOptions
} from '@rgossiaux/svelte-headlessui';
// import {
// Listbox,
// ListboxButton,
// ListboxOption,
// ListboxOptions
// } from '@rgossiaux/svelte-headlessui';
type Themes = Record<string, { name: string }>;
@ -61,8 +61,8 @@
<div class="theme">
<div class="listbox">
<Listbox value={selectedTheme} on:change={handleChange} let:open>
<ListboxButton class="button">
<!-- <Listbox value={selectedTheme} on:change={handleChange} let:open> -->
<!-- <ListboxButton class="button"> -->
<span>{selectedTheme.name}</span>
<span>
@ -82,22 +82,22 @@
/>
</svg>
</span>
</ListboxButton>
<!-- </ListboxButton> -->
{#if open}
<div transition:fade={{ duration: 100 }}>
<ListboxOptions class="options" static>
<div transition:fade|global={{ duration: 100 }}>
<!-- <ListboxOptions class="options" static> -->
{#each Object.entries(themes) as [key, theme] (key)}
<ListboxOption value={theme} let:active let:selected>
<!-- <ListboxOption value={theme} let:active let:selected> -->
<span class="option" class:active class:selected>
{theme.name}
</span>
</ListboxOption>
<!-- </ListboxOption> -->
{/each}
</ListboxOptions>
<!-- </ListboxOptions> -->
</div>
{/if}
</Listbox>
<!-- </Listbox> -->
</div>
</div>

View file

@ -4,7 +4,7 @@
import { collectionStore } from '$lib/stores/collectionStore';
import { toast } from '$lib/components/toast/toast';
import { ToastType, type SavedGameType } from '$lib/types';
import { mapSavedGameToGame } from '$root/lib/util/gameMapper';
import { mapSavedGameToGame } from '$lib/utils/gameMapper';
async function getRandomCollectionGame() {
if ($collectionStore.length > 0) {

View file

@ -0,0 +1,55 @@
<script lang="ts">
import { superForm, type Infer, type SuperValidated } from 'sveltekit-superforms';
import { search_schema, type SearchSchema } from '$lib/zodValidation';
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 data: SuperValidated<Infer<SearchSchema>>;
const form = superForm(data, {
validators: zodClient(search_schema),
});
const { form: formData } = form;
</script>
<search>
<form id="search-form" action="/search" method="GET" data-sveltekit-reload>
<fieldset>
<Form.Field {form} name="q">
<Form.Control let:attrs>
<Form.Label>Search</Form.Label>
<Input {...attrs} bind:value={$formData.q} />
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="skip">
<Form.Control let:attrs>
<Input type="hidden" />
</Form.Control>
</Form.Field>
<Form.Field {form} name="limit">
<Form.Control let:attrs>
<Input type="hidden" />
</Form.Control>
</Form.Field>
</fieldset>
<fieldset>
<div class="flex items-center space-x-2">
<Form.Field {form} name="exact">
<Form.Control let:attrs>
<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>
</div>
</fieldset>
<Form.Button>Submit</Form.Button>
</form>
</search>
<style lang="postcss">
</style>

View file

@ -1,17 +1,17 @@
<script lang="ts">
import type { ActionData } from './$types';
import { boredState } from '$lib/stores/boredState';
import type { PageData } from '.svelte-kit/types/src/routes/$types';
export let data: PageData;
console.log('advanced search data', data);
export let form;
export let errors;
export let constraints;
console.log('advanced search data', $form);
let submitting = $boredState?.loading;
let minAge = +data?.minAge || 1;
let minPlayers = +data?.minPlayers || 1;
let maxPlayers = +data?.maxPlayers || 1;
let exactMinPlayers = Boolean(data?.exactMinPlayers) || false;
let exactMaxPlayers = Boolean(data?.exactMaxPlayers) || false;
let minAge = +$form?.minAge || 1;
let minPlayers = +$form?.minPlayers || 1;
let maxPlayers = +$form?.maxPlayers || 1;
let exactMinPlayers = Boolean($form?.exactMinPlayers) || false;
let exactMaxPlayers = Boolean($form?.exactMaxPlayers) || false;
</script>
<fieldset class="advanced-search" aria-busy={submitting} disabled={submitting}>
@ -20,10 +20,10 @@
Min Age
<input id="minAge" name="minAge" bind:value={minAge} type="number" min={1} max={120} />
</label>
{#if data?.errors?.minAge}
{#if $errors?.minAge}
<div id="minPlayers-error" class="error">
<p aria-label={`Error: ${data?.errors?.minAge}`} class="center">
{data?.errors?.minAge}
<p aria-label={`Error: ${$errors?.minAge}`} class="center">
{$errors?.minAge}
</p>
</div>
{/if}
@ -50,10 +50,10 @@
bind:value={exactMinPlayers}
/>
</label>
{#if data?.errors?.minPlayers}
{#if $errors?.minPlayers}
<div id="minPlayers-error" class="error">
<p aria-label={`Error: ${data?.errors?.minPlayers}`} class="center">
{data?.errors?.minPlayers}
<p aria-label={`Error: ${$errors?.minPlayers}`} class="center">
{$errors?.minPlayers}
</p>
</div>
{/if}
@ -80,10 +80,10 @@
bind:value={exactMaxPlayers}
/>
</label>
{#if data?.error?.id === 'maxPlayers'}
{#if $errors?.id === 'maxPlayers'}
<div id="maxPlayers-error" class="error">
<p aria-label={`Error: ${data.error.message}`} class="center">
Error: {data.error.message}
<p aria-label={`Error: ${$errors.message}`} class="center">
Error: {$errors.message}
</p>
</div>
{/if}

View file

@ -1,47 +1,47 @@
<script lang="ts">
import { applyAction, enhance } from '$app/forms';
import type { SuperValidated } from 'sveltekit-superforms/index';
import type { SearchSchema } from '$lib/zodValidation';
import { boredState } from '$lib/stores/boredState';
import { gameStore } from '$lib/stores/gameSearchStore';
import { ToastType } from '$root/lib/types';
import { toast } from '../../toast/toast';
import { superForm } from 'sveltekit-superforms/client';
import { Button } from '$components/ui/button';
export let data: SuperValidated<SearchSchema>;
const { enhance } = superForm(data, {
onSubmit: () => {
gameStore.removeAll();
boredState.update((n) => ({ ...n, loading: true }));
},
onResult: ({ result, formEl, cancel }) => {
boredState.update((n) => ({ ...n, loading: false }));
if (result.type === 'success') {
gameStore.addAll(result?.data?.searchData?.games);
} else {
cancel();
}
},
// onUpdated: ({ form }) => {
// if ($gameStore.length <= 0) {
// toast.send('No results found 😿', {
// duration: 3000,
// type: ToastType.ERROR,
// dismissible: true
// });
// }
// }
});
let submitting = $boredState?.loading;
let checked = true;
</script>
<form
action="/search?/random"
method="POST"
use:enhance={() => {
gameStore.removeAll();
boredState.update((n) => ({ ...n, loading: true }));
return async ({ result }) => {
console.log('result', result);
boredState.update((n) => ({ ...n, loading: false }));
// `result` is an `ActionResult` object
if (result.type === 'success') {
// console.log('In success');
const resultGames = result?.data?.games;
if (resultGames?.length <= 0) {
toast.send('No results found 😿', {
duration: 3000,
type: ToastType.ERROR,
dismissible: true
});
}
gameStore.addAll(resultGames);
// console.log(`Frontend result random: ${JSON.stringify(result)}`);
await applyAction(result);
} else {
// console.log('Invalid');
await applyAction(result);
}
};
}}
use:enhance
>
<fieldset aria-busy={submitting} disabled={submitting}>
<!-- <input type="checkbox" id="random" name="random" hidden {checked} /> -->
<button class="btn" type="submit" disabled={submitting}>Random Game 🎲</button>
<Button type="submit" disabled={submitting}>Random Game 🎲</Button>
<!-- <button class="btn" type="submit" disabled={submitting}>Random Game 🎲</button> -->
</fieldset>
</form>

View file

@ -1,296 +0,0 @@
<script lang="ts">
import { tick } from 'svelte';
import { applyAction, enhance, type SubmitFunction } from '$app/forms';
import type { ActionData, PageData } from './$types';
import { fade } from 'svelte/transition';
import { Disclosure, DisclosureButton, DisclosurePanel } from '@rgossiaux/svelte-headlessui';
import { ChevronRightIcon } from '@rgossiaux/svelte-heroicons/solid';
import { boredState } from '$lib/stores/boredState';
import AdvancedSearch from '$lib/components/search/advancedSearch/index.svelte';
import { xl, md, sm } from '$lib/stores/mediaQueryStore';
import { gameStore } from '$root/lib/stores/gameSearchStore';
import { toast } from '../../toast/toast';
import Pagination from '$lib/components/pagination/index.svelte';
import Game from '$lib/components/game/index.svelte';
import { ToastType, type GameType, type SavedGameType } from '$root/lib/types';
import SkeletonPlaceholder from '../../SkeletonPlaceholder.svelte';
import RemoveCollectionDialog from '../../dialog/RemoveCollectionDialog.svelte';
import RemoveWishlistDialog from '../../dialog/RemoveWishlistDialog.svelte';
interface RemoveGameEvent extends Event {
detail: GameType | SavedGameType;
}
export let data: PageData;
// console.log('search page data', data);
export let form: ActionData;
// console.log('search page form', form);
const errors = data?.errors;
export let showButton: boolean = false;
export let advancedSearch: boolean = false;
let gameToRemove: GameType | SavedGameType;
let numberOfGameSkeleton = 1;
let submitButton: HTMLElement;
let pageSize = +data?.limit || 10;
let totalItems = +data?.totalCount || 0;
let offset = +data?.skip || 0;
let page = Math.floor(offset / pageSize) + 1 || 1;
let submitting = $boredState?.loading;
let name = data?.name || '';
let disclosureOpen = errors || false;
$: skip = (page - 1) * pageSize;
$: showPagination = $gameStore?.length > 1;
if ($xl) {
numberOfGameSkeleton = 8;
} else if ($md) {
numberOfGameSkeleton = 3;
} else if ($sm) {
numberOfGameSkeleton = 2;
} else {
numberOfGameSkeleton = 1;
}
let placeholderList = [...Array(numberOfGameSkeleton).keys()];
if (form?.error) {
disclosureOpen = true;
}
async function handleNextPageEvent(event: CustomEvent) {
if (+event?.detail?.page === page + 1) {
page += 1;
}
await tick();
submitButton.click();
}
async function handlePreviousPageEvent(event: CustomEvent) {
if (+event?.detail?.page === page - 1) {
page -= 1;
}
await tick();
submitButton.click();
}
async function handlePerPageEvent(event: CustomEvent) {
page = 1;
pageSize = event.detail.pageSize;
await tick();
submitButton.click();
}
function handleRemoveCollection(event: RemoveGameEvent) {
gameToRemove = event?.detail;
boredState.update((n) => ({
...n,
dialog: { isOpen: true, content: RemoveCollectionDialog, additionalData: gameToRemove }
}));
}
function handleRemoveWishlist(event: RemoveGameEvent) {
gameToRemove = event?.detail;
boredState.update((n) => ({
...n,
dialog: { isOpen: true, content: RemoveWishlistDialog, additionalData: gameToRemove }
}));
}
const submitSearch: SubmitFunction = ({ form, data, action, cancel }) => {
const { name } = Object.fromEntries(data);
if (!disclosureOpen && name?.length === 0) {
toast.send('Please enter a search term', {
duration: 3000,
type: ToastType.ERROR,
dismissible: true
});
cancel();
return;
}
gameStore.removeAll();
boredState.update((n) => ({ ...n, loading: true }));
return async ({ result }) => {
boredState.update((n) => ({ ...n, loading: false }));
// `result` is an `ActionResult` object
if (result.type === 'error') {
toast.send('Error!', { duration: 3000, type: ToastType.ERROR, dismissible: true });
await applyAction(result);
} else if (result.type === 'success') {
gameStore.removeAll();
gameStore.addAll(result?.data?.games);
totalItems = result?.data?.totalCount;
// toast.send('Success!', { duration: 3000, type: ToastType.INFO, dismissible: true });
await applyAction(result);
} else {
await applyAction(result);
}
};
};
// TODO: Keep all Pagination Values on back and forth browser
// TODO: Add cache for certain number of pages so back and forth doesn't request data again
</script>
<form id="search-form" action="/search" method="get" on:submit={() => {
skip = 0;
}}>
<div class="search">
<fieldset class="text-search" aria-busy={submitting} disabled={submitting}>
<label for="q">
Search
<input
id="q"
name="q"
bind:value={name}
type="text"
aria-label="Search boardgame"
placeholder="Search boardgame"
/>
</label>
<input id="skip" type="hidden" name="skip" bind:value={skip} />
<input id="limit" type="hidden" name="limit" bind:value={pageSize} />
</fieldset>
{#if advancedSearch}
<Disclosure>
<DisclosureButton
class="disclosure-button"
on:click={() => (disclosureOpen = !disclosureOpen)}
>
<span>Advanced Search?</span>
<ChevronRightIcon
class="icon disclosure-icon"
style={disclosureOpen
? 'transform: rotate(90deg); transition: transform 0.5s ease;'
: 'transform: rotate(0deg); transition: transform 0.5s ease;'}
/>
</DisclosureButton>
{#if disclosureOpen}
<div transition:fade>
<!-- Using `static`, `DisclosurePanel` is always rendered,
and ignores the `open` state -->
<DisclosurePanel static>
<AdvancedSearch {data} />
</DisclosurePanel>
</div>
{/if}
</Disclosure>
{/if}
</div>
{#if showButton}
<button
id="search-submit"
class="btn"
type="submit"
disabled={submitting}
bind:this={submitButton}
>
Submit
</button>
{/if}
</form>
{#if $boredState.loading}
<div class="games">
<h1>Games Found:</h1>
<div class="games-list">
{#each placeholderList as game, i}
<SkeletonPlaceholder
style="width: 100%; height: 500px; border-radius: var(--borderRadius);"
/>
{/each}
</div>
</div>
{:else}
<div class="games">
<h1>Games Found:</h1>
<div class="games-list">
{#if $gameStore?.length > 0}
{#each $gameStore as game (game.id)}
<Game
on:handleRemoveWishlist={handleRemoveWishlist}
on:handleRemoveCollection={handleRemoveCollection}
{game}
/>
{/each}
{:else}
<h2>Sorry no games found!</h2>
{/if}
</div>
{#if showPagination && $gameStore?.length > 0}
<Pagination
{pageSize}
{page}
{totalItems}
forwardText="Next"
backwardText="Prev"
pageSizes={[10, 25, 50, 100]}
on:nextPageEvent={handleNextPageEvent}
on:previousPageEvent={handlePreviousPageEvent}
on:perPageEvent={handlePerPageEvent}
/>
{/if}
</div>
{/if}
<style lang="postcss">
.search {
display: grid;
gap: 1rem;
}
:global(.disclosure-button) {
display: flex;
gap: 0.25rem;
place-items: center;
}
button {
padding: 1rem;
margin: 1.5rem 0;
}
label {
display: grid;
grid-template-columns: auto auto;
gap: 1rem;
place-content: start;
place-items: center;
@media (max-width: 850px) {
display: flex;
flex-wrap: wrap;
}
}
.games {
margin: 2rem 0rem;
h1 {
margin-bottom: 2rem;
}
}
.games-list {
display: grid;
--listColumns: 4;
grid-template-columns: repeat(var(--listColumns), minmax(200px, 1fr));
gap: 2rem;
@media screen and (800px < width <= 1200px) {
--listColumns: 3;
}
@media screen and (650px < width <= 800px) {
--listColumns: 2;
}
@media screen and (width <= 650px) {
--listColumns: 1;
}
}
</style>

View file

@ -0,0 +1,84 @@
<script lang="ts">
import { zodClient } from 'sveltekit-superforms/adapters';
import { ConicGradient } from '@skeletonlabs/skeleton';
import type { ConicStop } from '@skeletonlabs/skeleton';
import { i } from "@inlang/sdk-js";
import { superForm } from 'sveltekit-superforms/client';
//import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
import { userSchema } from '$lib/validations/zod-schemas';
import { AlertTriangle } from 'lucide-svelte';
import { signInSchema } from '$lib/validations/auth';
export let data;
const { form, errors, enhance, delayed } = superForm(data.form, {
taintedMessage: null,
validators: zodClient(signInSchema),
delayMs: 0
});
const conicStops: ConicStop[] = [
{ color: 'transparent', start: 0, end: 25 },
{ color: 'rgb(var(--color-primary-900))', start: 75, end: 100 }
];
</script>
<form method="POST" action="/sign-in" use:enhance>
<!--<SuperDebug data={$form} />-->
{#if $errors._errors}
<aside class="alert variant-filled-error mt-6">
<!-- Icon -->
<div><AlertTriangle size="42" /></div>
<!-- Message -->
<div class="alert-message">
<h3 class="h3">{i("signinProblem")}</h3>
<p>{$errors._errors}</p>
</div>
</aside>
{/if}
<div class="mt-6">
<label class="label">
<span class="sr-only">{i("email")}</span>
<input
id="email"
name="email"
type="email"
placeholder="{i("email")}"
autocomplete="email"
data-invalid={$errors.email}
bind:value={$form.email}
class="input"
class:input-error={$errors.email}
/>
{#if $errors.email}
<small>{$errors.email}</small>
{/if}
</label>
</div>
<div class="mt-6">
<label class="label">
<span class="sr-only">{i("password")}</span>
<input
id="password"
name="password"
type="password"
placeholder="{i("password")}"
data-invalid={$errors.password}
bind:value={$form.password}
class="input"
class:input-error={$errors.password}
/>
{#if $errors.password}
<small>{$errors.password}</small>
{/if}
</label>
</div>
<div class="mt-6">
<button type="submit" class="btn variant-filled-primary w-full"
>{#if $delayed}<ConicGradient stops={conicStops} spin width="w-6" />{:else}{i("signin")}{/if}</button
>
</div>
<div class="flex flex-row justify-center items-center mt-10">
<a href="/password/reset" class="font-semibold">{i("forgotPassword")}</a>
</div>
</form>

View file

@ -0,0 +1,106 @@
<script lang="ts">
import { zodClient } from 'sveltekit-superforms/adapters';
import { superForm } from 'sveltekit-superforms/client';
import { signUpSchema } from '$lib/validations/auth';
export let data;
const { form, errors, enhance } = superForm(data.form, {
taintedMessage: null,
validators: zodClient(signUpSchema),
delayMs: 0
});
// $: termsValue = $form.terms as Writable<boolean>;
</script>
<form method="POST" action="/sign-up" use:enhance>
<h1>Signup user</h1>
<label class="label">
<span class="sr-only">First Name</span>
<input
id="firstName"
name="firstName"
type="text"
placeholder="First Name"
autocomplete="given-name"
data-invalid={$errors.firstName}
bind:value={$form.firstName}
class="input"
class:input-error={$errors.firstName}
/>
{#if $errors.firstName}
<small>{$errors.firstName}</small>
{/if}
</label>
<label class="label">
<span class="sr-only">Last Name</span>
<input
id="lastName"
name="lastName"
type="text"
placeholder="Last Name"
autocomplete="family-name"
data-invalid={$errors.lastName}
bind:value={$form.lastName}
class="input"
class:input-error={$errors.lastName}
/>
{#if $errors.lastName}
<small>{$errors.lastName}</small>
{/if}
</label>
<label class="label">
<span class="sr-only">Email</span>
<input
id="email"
name="email"
type="email"
placeholder="Email"
autocomplete="email"
data-invalid={$errors.email}
bind:value={$form.email}
class="input"
class:input-error={$errors.email}
/>
{#if $errors.email}
<small>{$errors.email}</small>
{/if}
</label>
<label class="label">
<span class="sr-only">Username</span>
<input
id="username"
name="username"
type="username"
placeholder="Username"
autocomplete="uername"
data-invalid={$errors.username}
bind:value={$form.username}
class="input"
class:input-error={$errors.username}
/>
{#if $errors.username}
<small>{$errors.username}</small>
{/if}
</label>
<label class="label">
<span class="sr-only">password</span>
<input
id="password"
name="password"
type="password"
placeholder="password"
data-invalid={$errors.password}
bind:value={$form.password}
class="input"
class:input-error={$errors.password}
/>
{#if $errors.password}
<small>{$errors.password}</small>
{/if}
</label>
<button type="submit">Signup</button>
<a class="back" href="/"> or Cancel </a>
</form>

View file

@ -0,0 +1,98 @@
<script lang="ts">
export let header: string;
export let page: string;
export let image: string;
export let content: string;
export let width = 1200;
export let height = 630;
export let url: string;
</script>
<div class="social-card" style={`width: ${width}px; height: ${height}px;`}>
<div class="social-card-header">
<div class="social-card-title">
<div class="social-card-image">
<img src={image || '/images/bored-game.png'} alt="Bored Game" />
</div>
<h1>{header}</h1>
</div>
</div>
<div class="social-card-content">
<h2 class="page">{page}</h2>
<p class="content">{content}</p>
</div>
<div class="social-card-footer">
<footer>
<p>Bored Game &copy; {new Date().getFullYear()} | Built by Bradley Shellnut | {url || 'https://boredgame.vercel.app'}</p>
</footer>
</div>
</div>
<style>
@font-face {
font-family: 'Fira Sans';
src: url('/src/lib/FiraSans-Bold.ttf');
}
.social-card {
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #DFDBE5;
background-image: url("data:image/svg+xml,%3Csvg width='64' height='64' viewBox='0 0 64 64' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 16c4.418 0 8-3.582 8-8s-3.582-8-8-8-8 3.582-8 8 3.582 8 8 8zm0-2c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6zm33.414-6l5.95-5.95L45.95.636 40 6.586 34.05.636 32.636 2.05 38.586 8l-5.95 5.95 1.414 1.414L40 9.414l5.95 5.95 1.414-1.414L41.414 8zM40 48c4.418 0 8-3.582 8-8s-3.582-8-8-8-8 3.582-8 8 3.582 8 8 8zm0-2c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6zM9.414 40l5.95-5.95-1.414-1.414L8 38.586l-5.95-5.95L.636 34.05 6.586 40l-5.95 5.95 1.414 1.414L8 41.414l5.95 5.95 1.414-1.414L9.414 40z' fill='%239C92AC' fill-opacity='0.17' fill-rule='evenodd'/%3E%3C/svg%3E");
}
.social-card-header {
display: flex;
padding: 1.5rem;
margin-top: 0.375rem;
flex-direction: column;
}
.social-card-title {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 1.875rem; /* 30px */
line-height: 2.25rem; /* 36px */
font-weight: 700;
letter-spacing: -0.025em;
}
.social-card-image {
display: flex;
width: 4rem;
height: 4rem;
}
.social-card-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
padding: 1.5rem;
padding-top: 0;
}
.page {
font-size: 3.75rem; /* 60px */
line-height: 1;
font-weight: 700;
text-transform: uppercase;
}
.content {
font-size: 1.25rem; /* 20px */
line-height: 1.75rem; /* 28px */
}
.social-card-footer {
display: flex;
padding: 1.5rem;
padding-top: 0;
align-items: center;
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 700;
}
</style>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { fly, fade } from 'svelte/transition';
import { flip } from 'svelte/animate';
import Portal from '../../Portal.svelte';
import Portal from '$lib/Portal.svelte';
import ToastMessage from './ToastMessage.svelte';
import { toast } from './toast';
</script>
@ -10,11 +10,13 @@
<div class="toast-wrapper">
{#each $toast as toastData (toastData.id)}
<div
role="button"
tabindex="0"
aria-label={toastData.dismissible ? 'Click to dismiss' : `${toastData.message}`}
on:click={() => toastData.dismissible && toast.remove(toastData.id)}
on:keydown={() => toastData.dismissible && toast.remove(toastData.id)}
in:fly={{ opacity: 0, x: 100 }}
out:fade
in:fly|global={{ opacity: 0, x: 100 }}
out:fade|global
animate:flip
class={`toast ${toastData.type.toLowerCase()}`}
>

View file

@ -4,40 +4,40 @@ import { ToastType } from '$lib/types';
// Custom store
const newToast = () => {
const { subscribe, update } = writable<ToastData[]>([]);
const { subscribe, update } = writable<ToastData[]>([]);
function send(
message: string,
{
duration = 2000,
type = ToastType.INFO,
autoDismiss = true,
dismissible = false,
showButton = false
} = {}
) {
const id = Math.floor(Math.random() * 1000);
function send(
message: string,
{
duration = 2000,
type = ToastType.INFO,
autoDismiss = true,
dismissible = false,
showButton = false
} = {}
) {
const id = Math.floor(Math.random() * 1000);
const newMessage: ToastData = {
id,
duration,
autoDismiss,
dismissible,
showButton,
type,
message
};
update((store) => [...store, newMessage]);
}
const newMessage: ToastData = {
id,
duration,
autoDismiss,
dismissible,
showButton,
type,
message
};
update((store) => [...store, newMessage]);
}
function remove(id: number) {
update((store) => {
const newStore = store.filter((item: ToastData) => item.id !== id);
return [...newStore];
});
}
function remove(id: number) {
update((store) => {
const newStore = store.filter((item: ToastData) => item.id !== id);
return [...newStore];
});
}
return { subscribe, send, remove };
return { subscribe, send, remove };
};
export const toast = newToast();

View file

@ -1,16 +1,16 @@
<script>
import { Switch } from '@rgossiaux/svelte-headlessui';
// import { Switch } from '@rgossiaux/svelte-headlessui';
let enabled = false;
</script>
<Switch
<!-- <Switch
checked={enabled}
on:change={(e) => (enabled = e.detail)}
class={enabled ? 'switch switch-enabled' : 'switch switch-disabled'}
>
<span class="sr-only">Dark Mode</span>
<span class="toggle" class:toggle-on={enabled} class:toggle-off={!enabled} />
</Switch>
</Switch> -->
<style>
:global(.switch) {

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { fade, fly } from 'svelte/transition';
import { fade } from 'svelte/transition';
interface Transition {
type: 'fade' | 'stagger' | 'page';
@ -14,7 +14,7 @@
{#if transition.type === 'page' && url}
<div class="transition" style="display: grid;">
{#key url}
<div style="grid-row: 1 / -1; grid-column: 1 / -1;" in:fade={{ duration: 400, delay: 400 }} out:fade={{ duration: 400}}>
<div style="grid-row: 1 / -1; grid-column: 1 / -1;" in:fade|global={{ duration: 400, delay: 400 }} out:fade|global={{ duration: 400}}>
<slot />
</div>
{/key}

View file

@ -0,0 +1,13 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("text-sm [&_p]:leading-relaxed", className)} {...$$restProps}>
<slot />
</div>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { HTMLAttributes } from "svelte/elements";
import type { HeadingLevel } from ".";
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
level?: HeadingLevel;
};
let className: $$Props["class"] = undefined;
export let level: $$Props["level"] = "h5";
export { className as class };
</script>
<svelte:element
this={level}
class={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
</svelte:element>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { HTMLAttributes } from "svelte/elements";
import { alertVariants, type Variant } from ".";
type $$Props = HTMLAttributes<HTMLDivElement> & {
variant?: Variant;
};
let className: $$Props["class"] = undefined;
export let variant: $$Props["variant"] = "default";
export { className as class };
</script>
<div
class={cn(alertVariants({ variant }), className)}
{...$$restProps}
role="alert"
>
<slot />
</div>

View file

@ -0,0 +1,33 @@
import { tv, type VariantProps } from "tailwind-variants";
import Root from "./alert.svelte";
import Description from "./alert-description.svelte";
import Title from "./alert-title.svelte";
export const alertVariants = tv({
base: "relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11",
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive"
}
},
defaultVariants: {
variant: "default"
}
});
export type Variant = VariantProps<typeof alertVariants>["variant"];
export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
export {
Root,
Description,
Title,
//
Root as Alert,
Description as AlertDescription,
Title as AlertTitle
};

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
type $$Props = AvatarPrimitive.FallbackProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Fallback
class={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
{...$$restProps}
>
<slot />
</AvatarPrimitive.Fallback>

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