mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
commit
dbbb292bb1
64 changed files with 3500 additions and 987 deletions
40
.vscode/launch.json
vendored
40
.vscode/launch.json
vendored
|
|
@ -1,17 +1,27 @@
|
||||||
{
|
{
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Launch Vite DEV server",
|
"name": "Launch server",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeExecutable": "npx",
|
"runtimeArgs": ["dev"],
|
||||||
"runtimeArgs": ["vite"],
|
"runtimeExecutable": "pnpm",
|
||||||
"type": "node",
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"serverReadyAction": {
|
"type": "node",
|
||||||
"action": "debugWithChrome",
|
"console": "integratedTerminal"
|
||||||
"pattern": "Local: http://localhost:([0-9]+)",
|
},
|
||||||
"uriFormat": "http://localhost:%s"
|
{
|
||||||
}
|
"type": "chrome",
|
||||||
}
|
"request": "launch",
|
||||||
]
|
"name": "Launch browser",
|
||||||
|
"url": "http://localhost:5173",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Both",
|
||||||
|
"configurations": ["Launch server", "Launch browser"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
package.json
66
package.json
|
|
@ -3,70 +3,86 @@
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_OPTIONS=\"--inspect\" vite dev --host",
|
"dev": "NODE_OPTIONS=\"--inspect\" vite dev --host",
|
||||||
"build": "vite build",
|
"build": "prisma generate && vite build",
|
||||||
"package": "svelte-kit package",
|
"package": "svelte-kit package",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
|
"postinstall": "prisma generate",
|
||||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
|
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
|
||||||
"format": "prettier --write --plugin-search-dir=. ."
|
"format": "prettier --write --plugin-search-dir=. .",
|
||||||
|
"site:update": "pnpm update -i -L",
|
||||||
|
"db:studio": "prisma studio",
|
||||||
|
"db:push": "prisma db push",
|
||||||
|
"db:seed": "prisma db seed"
|
||||||
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "ts-node --esm prisma/seed.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.33.0",
|
"@playwright/test": "^1.35.1",
|
||||||
"@rgossiaux/svelte-headlessui": "1.0.2",
|
"@rgossiaux/svelte-headlessui": "1.0.2",
|
||||||
"@rgossiaux/svelte-heroicons": "^0.1.2",
|
"@rgossiaux/svelte-heroicons": "^0.1.2",
|
||||||
"@sveltejs/adapter-auto": "^1.0.3",
|
"@sveltejs/adapter-auto": "^1.0.3",
|
||||||
"@sveltejs/adapter-vercel": "^1.0.6",
|
"@sveltejs/adapter-vercel": "^1.0.6",
|
||||||
"@sveltejs/kit": "^1.16.3",
|
"@sveltejs/kit": "^1.20.2",
|
||||||
"@types/cookie": "^0.5.1",
|
"@types/cookie": "^0.5.1",
|
||||||
"@types/node": "^18.16.9",
|
"@types/node": "^18.16.18",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.5",
|
"@typescript-eslint/eslint-plugin": "^5.59.11",
|
||||||
"@typescript-eslint/parser": "^5.59.5",
|
"@typescript-eslint/parser": "^5.59.11",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint": "^8.40.0",
|
"eslint": "^8.42.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-svelte": "^2.28.0",
|
"eslint-plugin-svelte": "^2.30.0",
|
||||||
"just-clone": "^6.2.0",
|
"just-clone": "^6.2.0",
|
||||||
"just-debounce-it": "^3.2.0",
|
"just-debounce-it": "^3.2.0",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.24",
|
||||||
"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-import": "^15.1.0",
|
||||||
"postcss-load-config": "^4.0.1",
|
"postcss-load-config": "^4.0.1",
|
||||||
"postcss-media-minmax": "^5.0.0",
|
"postcss-preset-env": "^8.5.0",
|
||||||
"postcss-nested": "^6.0.1",
|
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"prettier-plugin-svelte": "^2.10.0",
|
"prettier-plugin-svelte": "^2.10.1",
|
||||||
"sass": "^1.62.1",
|
"prisma": "^4.15.0",
|
||||||
|
"sass": "^1.63.4",
|
||||||
"svelte": "^3.59.1",
|
"svelte": "^3.59.1",
|
||||||
"svelte-check": "^2.10.3",
|
"svelte-check": "^2.10.3",
|
||||||
"svelte-preprocess": "^4.10.7",
|
"svelte-preprocess": "^5.0.4",
|
||||||
"sveltekit-superforms": "^0.8.6",
|
"sveltekit-superforms": "^1.0.0",
|
||||||
"tslib": "^2.5.0",
|
"ts-node": "^10.9.1",
|
||||||
|
"tslib": "^2.5.3",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"vite": "^4.3.5",
|
"vite": "^4.3.9",
|
||||||
"vitest": "^0.25.3",
|
"vitest": "^0.25.3",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.12.1",
|
||||||
|
"pnpm": ">=8"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@axiomhq/axiom-node": "^0.12.0",
|
||||||
"@fontsource/fira-mono": "^4.5.10",
|
"@fontsource/fira-mono": "^4.5.10",
|
||||||
"@iconify-icons/line-md": "^1.2.22",
|
"@iconify-icons/line-md": "^1.2.23",
|
||||||
"@iconify-icons/mdi": "^1.2.45",
|
"@iconify-icons/mdi": "^1.2.46",
|
||||||
"@leveluptuts/svelte-side-menu": "^1.0.5",
|
"@leveluptuts/svelte-side-menu": "^1.0.5",
|
||||||
"@leveluptuts/svelte-toy": "^2.0.3",
|
"@leveluptuts/svelte-toy": "^2.0.3",
|
||||||
|
"@lucia-auth/adapter-mysql": "^1.1.1",
|
||||||
|
"@lucia-auth/adapter-prisma": "^2.0.0",
|
||||||
"@lukeed/uuid": "^2.0.1",
|
"@lukeed/uuid": "^2.0.1",
|
||||||
|
"@prisma/client": "4.15.0",
|
||||||
"@types/feather-icons": "^4.29.1",
|
"@types/feather-icons": "^4.29.1",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
"feather-icons": "^4.29.0",
|
"feather-icons": "^4.29.0",
|
||||||
"iconify-icon": "^1.0.7",
|
"iconify-icon": "^1.0.7",
|
||||||
"loader": "^2.1.1",
|
"loader": "^2.1.1",
|
||||||
"open-props": "^1.5.8",
|
"lucia-auth": "^1.8.0",
|
||||||
|
"open-props": "^1.5.9",
|
||||||
"svelte-lazy": "^1.2.1",
|
"svelte-lazy": "^1.2.1",
|
||||||
"svelte-lazy-loader": "^1.0.0",
|
"svelte-lazy-loader": "^1.0.0",
|
||||||
|
"svelte-legos": "^0.2.1",
|
||||||
|
"sveltekit-flash-message": "^0.11.3",
|
||||||
"zod-to-json-schema": "^3.21.1"
|
"zod-to-json-schema": "^3.21.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
1629
pnpm-lock.yaml
1629
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,19 +1,16 @@
|
||||||
const autoprefixer = require('autoprefixer');
|
const postcssPresetEnv = require('postcss-preset-env');
|
||||||
const postcssMediaMinmax = require('postcss-media-minmax');
|
|
||||||
const customMedia = require('postcss-custom-media');
|
|
||||||
const atImport = require('postcss-import');
|
const atImport = require('postcss-import');
|
||||||
const postcssNested = require('postcss-nested');
|
|
||||||
const postcssEnvFunction = require('postcss-env-function');
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
plugins: [
|
plugins: [
|
||||||
autoprefixer(),
|
|
||||||
postcssMediaMinmax,
|
|
||||||
customMedia,
|
|
||||||
atImport(),
|
atImport(),
|
||||||
postcssNested,
|
postcssPresetEnv({
|
||||||
postcssEnvFunction({
|
stage: 2,
|
||||||
importFrom: './src/lib/util/environmentVariables.json'
|
features: {
|
||||||
|
'nesting-rules': true,
|
||||||
|
'custom-media-queries': true,
|
||||||
|
'media-query-ranges': true,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
238
prisma/schema.prisma
Normal file
238
prisma/schema.prisma
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
// 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"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "mysql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
relationMode = "prisma"
|
||||||
|
}
|
||||||
|
|
||||||
|
model Role {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String @unique
|
||||||
|
userRoles UserRole[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserRole {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
user AuthUser @relation(fields: [userId], references: [id])
|
||||||
|
userId String
|
||||||
|
role Role @relation(fields: [roleId], references: [id])
|
||||||
|
roleId String
|
||||||
|
|
||||||
|
@@unique([userId, roleId])
|
||||||
|
@@index([userId])
|
||||||
|
@@index([roleId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model AuthUser {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
username String @unique
|
||||||
|
email String? @unique
|
||||||
|
firstName String?
|
||||||
|
lastName String?
|
||||||
|
roles UserRole[]
|
||||||
|
verified Boolean @default(false)
|
||||||
|
receiveEmail Boolean @default(false)
|
||||||
|
token String? @unique
|
||||||
|
collection Collection?
|
||||||
|
wishlist Wishlist[]
|
||||||
|
theme String @default("system")
|
||||||
|
createdAt DateTime @default(now()) @db.Timestamp(6)
|
||||||
|
updatedAt DateTime @updatedAt @db.Timestamp(6)
|
||||||
|
auth_session AuthSession[]
|
||||||
|
auth_key AuthKey[]
|
||||||
|
|
||||||
|
@@map("auth_user")
|
||||||
|
}
|
||||||
|
|
||||||
|
model AuthSession {
|
||||||
|
id String @id @unique
|
||||||
|
user_id String
|
||||||
|
active_expires BigInt
|
||||||
|
idle_expires BigInt
|
||||||
|
auth_user AuthUser @relation(references: [id], fields: [user_id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([user_id])
|
||||||
|
@@map("auth_session")
|
||||||
|
}
|
||||||
|
|
||||||
|
model AuthKey {
|
||||||
|
id String @id @unique
|
||||||
|
hashed_password String?
|
||||||
|
user_id String
|
||||||
|
primary_key Boolean
|
||||||
|
expires BigInt?
|
||||||
|
auth_user AuthUser @relation(references: [id], fields: [user_id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([user_id])
|
||||||
|
@@map("auth_key")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Collection {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
user_id String @unique
|
||||||
|
auth_user AuthUser @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])
|
||||||
|
game_id String
|
||||||
|
game Game @relation(references: [id], fields: [game_id])
|
||||||
|
times_played Int
|
||||||
|
|
||||||
|
@@index([game_id, collection_id])
|
||||||
|
@@map("collection_items")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Wishlist {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
user_id String
|
||||||
|
auth_user AuthUser @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])
|
||||||
|
game_id String
|
||||||
|
game Game @relation(references: [id], fields: [game_id])
|
||||||
|
|
||||||
|
@@index([game_id, wishlist_id])
|
||||||
|
@@map("wishlist_items")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Game {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
description String?
|
||||||
|
yearPublished Int?
|
||||||
|
minPlayers Int?
|
||||||
|
maxPlayers Int?
|
||||||
|
minPlaytime Int?
|
||||||
|
maxPlaytime Int?
|
||||||
|
minAge Int?
|
||||||
|
imageUrl String?
|
||||||
|
thumbUrl String?
|
||||||
|
url String?
|
||||||
|
rulesUrl String?
|
||||||
|
weightAmount Float?
|
||||||
|
weightUnits String?
|
||||||
|
bggId String?
|
||||||
|
bggUrl String?
|
||||||
|
primary_publisher_id String
|
||||||
|
primaryPublisher Publisher? @relation("PrimaryPublishers", references: [id], fields: [primary_publisher_id])
|
||||||
|
categories Category[]
|
||||||
|
mechanics Mechanic[]
|
||||||
|
designers Designer[]
|
||||||
|
publishers Publisher[]
|
||||||
|
artists Artist[]
|
||||||
|
names GameName[]
|
||||||
|
expansions Expansion[]
|
||||||
|
collection_items CollectionItem[]
|
||||||
|
wishlist_items WishlistItem[]
|
||||||
|
external_id String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([primary_publisher_id])
|
||||||
|
@@map("games")
|
||||||
|
}
|
||||||
|
|
||||||
|
model GameName {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
game_id String
|
||||||
|
game Game @relation(references: [id], fields: [game_id])
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([game_id])
|
||||||
|
@@map("game_names")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Publisher {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
games Game[]
|
||||||
|
primaryPublisher Game[] @relation("PrimaryPublishers")
|
||||||
|
external_id String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("publishers")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Category {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
slug String
|
||||||
|
games Game[]
|
||||||
|
external_id String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("categories")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Mechanic {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
games Game[]
|
||||||
|
external_id String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("mechanics")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Designer {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
games Game[]
|
||||||
|
external_id String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("designers")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Artist {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
games Game[]
|
||||||
|
external_id String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("artists")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Expansion {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
yearPublished Int?
|
||||||
|
baseGame Game? @relation(fields: [base_game_id], references: [id])
|
||||||
|
base_game_id String?
|
||||||
|
external_id String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([base_game_id])
|
||||||
|
@@map("expansions")
|
||||||
|
}
|
||||||
40
prisma/seed.ts
Normal file
40
prisma/seed.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
// import userData from '../src/lib/data.json' assert { type: 'json' };
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log(`Start seeding ...`);
|
||||||
|
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
109
src/app.css
109
src/app.css
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
54
src/app.d.ts
vendored
54
src/app.d.ts
vendored
|
|
@ -1,14 +1,50 @@
|
||||||
// See https://kit.svelte.dev/docs/types#app
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
// and what to do when importing types
|
// and what to do when importing types
|
||||||
declare namespace App {
|
// src/app.d.ts
|
||||||
interface Locals {
|
declare global {
|
||||||
userid: string;
|
namespace App {
|
||||||
|
interface PageData {
|
||||||
|
flash?: { type: 'success' | 'error'; message: string };
|
||||||
|
}
|
||||||
|
interface Locals {
|
||||||
|
auth: import('lucia-auth').AuthRequest;
|
||||||
|
prisma: PrismaClient;
|
||||||
|
user: Lucia.UserAttributes;
|
||||||
|
startTimer: number;
|
||||||
|
error: string;
|
||||||
|
errorId: string;
|
||||||
|
errorStackTrace: string;
|
||||||
|
message: unknown;
|
||||||
|
track: unknown;
|
||||||
|
}
|
||||||
|
interface Error {
|
||||||
|
code?: string;
|
||||||
|
errorId?: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// interface PageData {}
|
|
||||||
|
|
||||||
// interface Error {}
|
|
||||||
|
|
||||||
// interface Platform {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// interface PageData {}
|
||||||
|
// interface Error {}
|
||||||
|
// interface Platform {}
|
||||||
|
|
||||||
|
/// <reference types="lucia-auth" />
|
||||||
|
declare global {
|
||||||
|
namespace Lucia {
|
||||||
|
type Auth = import('$lib/lucia').Auth;
|
||||||
|
type UserAttributes = {
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
role: string;
|
||||||
|
verified: boolean;
|
||||||
|
receiveEmail: boolean;
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS IS IMPORTANT!!!
|
||||||
|
export {};
|
||||||
|
|
|
||||||
111
src/app.postcss
Normal file
111
src/app.postcss
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
/* Write your global styles here, in PostCSS syntax */
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/db/roles.ts
Normal file
32
src/db/roles.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import prisma from '$lib/prisma';
|
||||||
|
|
||||||
|
export async function add_user_to_role(userId: string, roleName: string) {
|
||||||
|
// Find the role by its name
|
||||||
|
const role = await prisma.role.findUnique({
|
||||||
|
where: {
|
||||||
|
name: roleName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
throw new Error(`Role with name ${roleName} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a UserRole entry linking the user and the role
|
||||||
|
const userRole = await prisma.userRole.create({
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: userId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
connect: {
|
||||||
|
id: role.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return userRole;
|
||||||
|
}
|
||||||
56
src/db/users.ts
Normal file
56
src/db/users.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { auth } from '$lib/server/lucia';
|
||||||
|
import prisma from '$lib/prisma';
|
||||||
|
import type { AuthUser } from '@prisma/client';
|
||||||
|
import { add_user_to_role } from './roles';
|
||||||
|
|
||||||
|
export function create_user(user: AuthUser) {
|
||||||
|
return prisma.authUser.create({
|
||||||
|
data: {
|
||||||
|
username: user.username
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function find_or_create_user(user: AuthUser) {
|
||||||
|
const existing_user = await prisma.authUser.findUnique({
|
||||||
|
where: {
|
||||||
|
username: user.username
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (existing_user) {
|
||||||
|
return existing_user;
|
||||||
|
} else {
|
||||||
|
const new_user = await create_user(user);
|
||||||
|
add_user_to_role(new_user.id, 'user');
|
||||||
|
return new_user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function find_user_with_roles(user_id: string) {
|
||||||
|
const user_with_roles = await prisma.authUser.findUnique({
|
||||||
|
where: {
|
||||||
|
id: user_id
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
roles: {
|
||||||
|
select: {
|
||||||
|
role: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!user_with_roles) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
...user_with_roles,
|
||||||
|
roles: user_with_roles.roles.map((user_role) => user_role.role.name)
|
||||||
|
};
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
};
|
|
||||||
52
src/hooks.server.ts
Normal file
52
src/hooks.server.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { sequence } from '@sveltejs/kit/hooks';
|
||||||
|
import { redirect, type HandleServerError, type Handle } from '@sveltejs/kit';
|
||||||
|
import { dev } from '$app/environment';
|
||||||
|
import { auth } from '$lib/server/lucia';
|
||||||
|
import log from '$lib/server/log';
|
||||||
|
import prisma from '$lib/config/prisma';
|
||||||
|
|
||||||
|
export const handleError: HandleServerError = async ({ error, event }) => {
|
||||||
|
const errorId = crypto.randomUUID();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
event.locals.error = error?.toString() || undefined;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
event.locals.errorStackTrace = error?.stack || undefined;
|
||||||
|
event.locals.errorId = errorId;
|
||||||
|
if (!dev) {
|
||||||
|
log(500, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'An unexpected error occurred.',
|
||||||
|
errorId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// export const prismaClient: Handle = async function ({ event, resolve }) {
|
||||||
|
// event.locals.prisma = prisma;
|
||||||
|
// const response = await resolve(event);
|
||||||
|
// return response;
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const authentication: Handle = async function ({ event, resolve }) {
|
||||||
|
const startTimer = Date.now();
|
||||||
|
event.locals.startTimer = startTimer;
|
||||||
|
|
||||||
|
event.locals.auth = auth.handleRequest(event);
|
||||||
|
if (event.locals?.auth) {
|
||||||
|
const { user } = await event.locals.auth.validateUser();
|
||||||
|
event.locals.user = user;
|
||||||
|
// if (event.route.id?.startsWith('/(protected)')) {
|
||||||
|
// if (!user) throw redirect(302, '/auth/sign-in');
|
||||||
|
// if (!user.verified) throw redirect(302, '/auth/verify/email');
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await resolve(event);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handle = sequence(authentication);
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
text-align: start;
|
text-align: start;
|
||||||
background-color: var(--color-btn-primary-active);
|
background-color: var(--color-btn-primary-active);
|
||||||
|
|
||||||
@media (min-width: env(--large-viewport)) {
|
@media (min-width: 1000px) {
|
||||||
min-width: 23.5rem;
|
min-width: 23.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
DialogOverlay,
|
DialogOverlay,
|
||||||
DialogTitle
|
DialogTitle
|
||||||
} from '@rgossiaux/svelte-headlessui';
|
} from '@rgossiaux/svelte-headlessui';
|
||||||
import { boredState } from '$root/lib/stores/boredState';
|
import { boredState } from '$lib/stores/boredState';
|
||||||
import { collectionStore } from '$root/lib/stores/collectionStore';
|
import { collectionStore } from '$lib/stores/collectionStore';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
function clearCollection() {
|
function clearCollection() {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { boredState } from '$root/lib/stores/boredState';
|
import { boredState } from '$lib/stores/boredState';
|
||||||
import { wishlistStore } from '$root/lib/stores/wishlistStore';
|
import { wishlistStore } from '$lib/stores/wishlistStore';
|
||||||
import DefaultDialog from './DefaultDialog.svelte';
|
import DefaultDialog from './DefaultDialog.svelte';
|
||||||
|
|
||||||
function clearWishlist() {
|
function clearWishlist() {
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
|
||||||
button {
|
& button {
|
||||||
display: flex;
|
display: flex;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
DialogOverlay,
|
DialogOverlay,
|
||||||
DialogTitle
|
DialogTitle
|
||||||
} from '@rgossiaux/svelte-headlessui';
|
} from '@rgossiaux/svelte-headlessui';
|
||||||
import { boredState } from '$root/lib/stores/boredState';
|
import { boredState } from '$lib/stores/boredState';
|
||||||
|
|
||||||
export let title: string;
|
export let title: string;
|
||||||
export let description: string;
|
export let description: string;
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@
|
||||||
DialogOverlay,
|
DialogOverlay,
|
||||||
DialogTitle
|
DialogTitle
|
||||||
} from '@rgossiaux/svelte-headlessui';
|
} from '@rgossiaux/svelte-headlessui';
|
||||||
import { boredState } from '$root/lib/stores/boredState';
|
import { boredState } from '$lib/stores/boredState';
|
||||||
import { collectionStore } from '$root/lib/stores/collectionStore';
|
import { collectionStore } from '$lib/stores/collectionStore';
|
||||||
import { removeFromCollection } from '$root/lib/util/manipulateCollection';
|
import { removeFromCollection } from '$lib/util/manipulateCollection';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
function removeGame() {
|
function removeGame() {
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@
|
||||||
DialogOverlay,
|
DialogOverlay,
|
||||||
DialogTitle
|
DialogTitle
|
||||||
} from '@rgossiaux/svelte-headlessui';
|
} from '@rgossiaux/svelte-headlessui';
|
||||||
import { boredState } from '$root/lib/stores/boredState';
|
import { boredState } from '$lib/stores/boredState';
|
||||||
import { wishlistStore } from '$root/lib/stores/wishlistStore';
|
import { wishlistStore } from '$lib/stores/wishlistStore';
|
||||||
import { removeFromWishlist } from '$root/lib/util/manipulateWishlist';
|
import { removeFromWishlist } from '$lib/util/manipulateWishlist';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
function removeGame() {
|
function removeGame() {
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@
|
||||||
import Button from '$lib/components/button/index.svelte';
|
import Button from '$lib/components/button/index.svelte';
|
||||||
import type { GameType, SavedGameType } from '$lib/types';
|
import type { GameType, SavedGameType } from '$lib/types';
|
||||||
import { collectionStore } from '$lib/stores/collectionStore';
|
import { collectionStore } from '$lib/stores/collectionStore';
|
||||||
import { wishlistStore } from '$root/lib/stores/wishlistStore';
|
import { wishlistStore } from '$lib/stores/wishlistStore';
|
||||||
import { addToCollection, removeFromCollection } from '$lib/util/manipulateCollection';
|
import { addToCollection, removeFromCollection } from '$lib/util/manipulateCollection';
|
||||||
import { addToWishlist } from '$lib/util/manipulateWishlist';
|
import { addToWishlist } from '$lib/util/manipulateWishlist';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { binarySearchOnStore } from '$root/lib/util/binarySearchOnStore';
|
import { binarySearchOnStore } from '$lib/util/binarySearchOnStore';
|
||||||
import { convertToSavedGame } from '$root/lib/util/gameMapper';
|
import { convertToSavedGame } from '$lib/util/gameMapper';
|
||||||
|
|
||||||
export let game: GameType | SavedGameType;
|
export let game: GameType | SavedGameType;
|
||||||
export let detailed: boolean = false;
|
export let detailed: boolean = false;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Profile from '../preferences/profile.svelte';
|
import { enhance } from '$app/forms';
|
||||||
|
// import Profile from '../preferences/profile.svelte';
|
||||||
import logo from './bored-game.png';
|
import logo from './bored-game.png';
|
||||||
|
|
||||||
|
export let user: any;
|
||||||
|
console.log('User', user);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
|
|
@ -11,13 +15,32 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- <TextSearch /> -->
|
<!-- <TextSearch /> -->
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/collection" title="Go to your collection" data-sveltekit-preload-data>Collection</a>
|
{#if user}
|
||||||
<a href="/wishlist" title="Go to your wishlist" data-sveltekit-preload-data>Wishlist</a>
|
<a href="/collection" title="Go to your collection" data-sveltekit-preload-data>Collection</a>
|
||||||
<Profile />
|
<a href="/wishlist" title="Go to your wishlist" data-sveltekit-preload-data>Wishlist</a>
|
||||||
|
<form
|
||||||
|
use:enhance
|
||||||
|
action="/auth/signout"
|
||||||
|
method="POST"
|
||||||
|
>
|
||||||
|
<button type="submit" class="btn"
|
||||||
|
><span>Sign out</span></button
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
{#if !user}
|
||||||
|
<a href="/auth/signin">
|
||||||
|
<span class="flex-auto">Sign In</span></a
|
||||||
|
>
|
||||||
|
<a href="/auth/signup">
|
||||||
|
<span class="flex-auto">Sign Up</span></a
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<!-- <Profile /> -->
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="postcss">
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -65,7 +88,6 @@
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
color: var(--heading-color);
|
color: var(--heading-color);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
/* font-size: 0.8rem; */
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { boredState } from '$root/lib/stores/boredState';
|
import { boredState } from '$lib/stores/boredState';
|
||||||
import { collectionStore } from '$root/lib/stores/collectionStore';
|
import { collectionStore } from '$lib/stores/collectionStore';
|
||||||
import { ToastType } from '$root/lib/types';
|
import { ToastType } from '$lib/types';
|
||||||
import { SaveIcon, ShareIcon, TrashIcon } from '@rgossiaux/svelte-heroicons/outline';
|
import { SaveIcon, ShareIcon, TrashIcon } from '@rgossiaux/svelte-heroicons/outline';
|
||||||
import ClearCollectionDialog from '../dialog/ClearCollectionDialog.svelte';
|
import ClearCollectionDialog from '../dialog/ClearCollectionDialog.svelte';
|
||||||
import { toast } from '../toast/toast';
|
import { toast } from '../toast/toast';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { boredState } from '$root/lib/stores/boredState';
|
import { boredState } from '$lib/stores/boredState';
|
||||||
import { wishlistStore } from '$root/lib/stores/wishlistStore';
|
import { wishlistStore } from '$lib/stores/wishlistStore';
|
||||||
import { ToastType } from '$root/lib/types';
|
import { ToastType } from '$lib/types';
|
||||||
import { SaveIcon, ShareIcon, TrashIcon } from '@rgossiaux/svelte-heroicons/outline';
|
import { SaveIcon, ShareIcon, TrashIcon } from '@rgossiaux/svelte-heroicons/outline';
|
||||||
import ClearWishlistDialog from '../dialog/ClearWishlistDialog.svelte';
|
import ClearWishlistDialog from '../dialog/ClearWishlistDialog.svelte';
|
||||||
import { toast } from '../toast/toast';
|
import { toast } from '../toast/toast';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import { collectionStore } from '$lib/stores/collectionStore';
|
import { collectionStore } from '$lib/stores/collectionStore';
|
||||||
import { toast } from '$lib/components/toast/toast';
|
import { toast } from '$lib/components/toast/toast';
|
||||||
import { ToastType, type SavedGameType } from '$lib/types';
|
import { ToastType, type SavedGameType } from '$lib/types';
|
||||||
import { mapSavedGameToGame } from '$root/lib/util/gameMapper';
|
import { mapSavedGameToGame } from '$lib/util/gameMapper';
|
||||||
|
|
||||||
async function getRandomCollectionGame() {
|
async function getRandomCollectionGame() {
|
||||||
if ($collectionStore.length > 0) {
|
if ($collectionStore.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,46 @@
|
||||||
<script lang="ts">
|
<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 { boredState } from '$lib/stores/boredState';
|
||||||
import { gameStore } from '$lib/stores/gameSearchStore';
|
import { gameStore } from '$lib/stores/gameSearchStore';
|
||||||
import { ToastType } from '$root/lib/types';
|
import { ToastType } from '$lib/types';
|
||||||
|
import { superForm } from 'sveltekit-superforms/client';
|
||||||
import { toast } from '../../toast/toast';
|
import { toast } from '../../toast/toast';
|
||||||
|
|
||||||
|
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 submitting = $boredState?.loading;
|
||||||
let checked = true;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
action="/search?/random"
|
action="/search?/random"
|
||||||
method="POST"
|
method="POST"
|
||||||
use:enhance={() => {
|
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<fieldset aria-busy={submitting} disabled={submitting}>
|
<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 class="btn" type="submit" disabled={submitting}>Random Game 🎲</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,35 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { applyAction, enhance, type SubmitFunction } from '$app/forms';
|
|
||||||
import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
|
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
import { applyAction, type SubmitFunction } from '$app/forms';
|
||||||
|
import { superForm } from 'sveltekit-superforms/client';
|
||||||
|
import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
|
||||||
|
import type { SuperValidated } from 'sveltekit-superforms/index';
|
||||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@rgossiaux/svelte-headlessui';
|
import { Disclosure, DisclosureButton, DisclosurePanel } from '@rgossiaux/svelte-headlessui';
|
||||||
import { ChevronRightIcon } from '@rgossiaux/svelte-heroicons/solid';
|
import { ChevronRightIcon } from '@rgossiaux/svelte-heroicons/solid';
|
||||||
import { boredState } from '$lib/stores/boredState';
|
import { boredState } from '$lib/stores/boredState';
|
||||||
import AdvancedSearch from '$lib/components/search/advancedSearch/index.svelte';
|
import AdvancedSearch from '$lib/components/search/advancedSearch/index.svelte';
|
||||||
import { xl, md, sm } from '$lib/stores/mediaQueryStore';
|
import { xl, md, sm } from '$lib/stores/mediaQueryStore';
|
||||||
import { gameStore } from '$root/lib/stores/gameSearchStore';
|
import { gameStore } from '$lib/stores/gameSearchStore';
|
||||||
import { toast } from '../../toast/toast';
|
import { toast } from '../../toast/toast';
|
||||||
import Pagination from '$lib/components/pagination/index.svelte';
|
import Pagination from '$lib/components/pagination/index.svelte';
|
||||||
import Game from '$lib/components/game/index.svelte';
|
import Game from '$lib/components/game/index.svelte';
|
||||||
import { ToastType, type GameType, type SavedGameType } from '$root/lib/types';
|
import { ToastType, type GameType, type SavedGameType } from '$lib/types';
|
||||||
import SkeletonPlaceholder from '../../SkeletonPlaceholder.svelte';
|
// import SkeletonPlaceholder from '../../SkeletonPlaceholder.svelte';
|
||||||
import RemoveCollectionDialog from '../../dialog/RemoveCollectionDialog.svelte';
|
import RemoveCollectionDialog from '../../dialog/RemoveCollectionDialog.svelte';
|
||||||
import RemoveWishlistDialog from '../../dialog/RemoveWishlistDialog.svelte';
|
import RemoveWishlistDialog from '../../dialog/RemoveWishlistDialog.svelte';
|
||||||
|
import type { SearchSchema } from '$lib/zodValidation';
|
||||||
|
|
||||||
interface RemoveGameEvent extends Event {
|
interface RemoveGameEvent extends Event {
|
||||||
detail: GameType | SavedGameType;
|
detail: GameType | SavedGameType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export let form;
|
export let data: SuperValidated<SearchSchema>;
|
||||||
export let errors;
|
const { form, constraints, errors } = superForm(data, {
|
||||||
export let constraints;
|
onSubmit: () => {
|
||||||
|
boredState.update((n) => ({ ...n, loading: true }));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export let showButton: boolean = false;
|
export let showButton: boolean = false;
|
||||||
export let advancedSearch: boolean = false;
|
export let advancedSearch: boolean = false;
|
||||||
|
|
@ -139,22 +145,23 @@
|
||||||
<SuperDebug data={$form} />
|
<SuperDebug data={$form} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form id="search-form" action="/search" method="GET" on:submit={() => {
|
<form id="search-form" action="/search" method="GET">
|
||||||
skip = 0;
|
|
||||||
}}>
|
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<fieldset class="text-search" aria-busy={submitting} disabled={submitting}>
|
<fieldset class="text-search" aria-busy={submitting} disabled={submitting}>
|
||||||
<label for="q">Search</label>
|
<label class="label" for="q">
|
||||||
<input
|
<span>Search</span>
|
||||||
id="q"
|
<input
|
||||||
name="q"
|
id="q"
|
||||||
bind:value={$form.q}
|
class="input"
|
||||||
data-invalid={$errors?.q}
|
name="q"
|
||||||
{...$constraints.q}
|
bind:value={$form.q}
|
||||||
type="text"
|
data-invalid={$errors?.q}
|
||||||
aria-label="Search board games"
|
{...$constraints.q}
|
||||||
placeholder="Search board games"
|
type="search"
|
||||||
|
aria-label="Search board games"
|
||||||
|
placeholder="Search board games"
|
||||||
/>
|
/>
|
||||||
|
</label>
|
||||||
{#if $errors?.q}<span class="invalid">{$errors?.q}</span>{/if}
|
{#if $errors?.q}<span class="invalid">{$errors?.q}</span>{/if}
|
||||||
|
|
||||||
<input id="skip" type="hidden" name="skip" bind:value={$form.skip} />
|
<input id="skip" type="hidden" name="skip" bind:value={$form.skip} />
|
||||||
|
|
@ -206,11 +213,11 @@
|
||||||
<div class="games">
|
<div class="games">
|
||||||
<h1>Games Found:</h1>
|
<h1>Games Found:</h1>
|
||||||
<div class="games-list">
|
<div class="games-list">
|
||||||
{#each placeholderList as game, i}
|
<!-- {#each placeholderList as game, i}
|
||||||
<SkeletonPlaceholder
|
<SkeletonPlaceholder
|
||||||
style="width: 100%; height: 500px; border-radius: var(--borderRadius);"
|
style="width: 100%; height: 500px; border-radius: var(--borderRadius);"
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each} -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -278,7 +285,7 @@
|
||||||
.games {
|
.games {
|
||||||
margin: 2rem 0rem;
|
margin: 2rem 0rem;
|
||||||
|
|
||||||
h1 {
|
& h1 {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -289,15 +296,19 @@
|
||||||
grid-template-columns: repeat(var(--listColumns), minmax(250px, 1fr));
|
grid-template-columns: repeat(var(--listColumns), minmax(250px, 1fr));
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
|
|
||||||
@media screen and (env(--large-viewport) < width <= env(--xxlarge-viewport)) {
|
@media (width >= 1500px) {
|
||||||
--listColumns: 3;
|
--listColumns: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (env(--small-viewport) < width <= env(--large-viewport)) {
|
@media (1000px < width <= 1500px) {
|
||||||
|
--listColumns: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (600px < width <= 1000px) {
|
||||||
--listColumns: 2;
|
--listColumns: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (width <= env(--small-viewport)) {
|
@media (width <= 600px) {
|
||||||
--listColumns: 1;
|
--listColumns: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
82
src/lib/components/signin.svelte
Normal file
82
src/lib/components/signin.svelte
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ConicGradient } from '@skeletonlabs/skeleton';
|
||||||
|
import type { ConicStop } from '@skeletonlabs/skeleton';
|
||||||
|
import { superForm } from 'sveltekit-superforms/client';
|
||||||
|
//import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
|
||||||
|
import { userSchema } from '$lib/config/zod-schemas';
|
||||||
|
import { AlertTriangle } from 'lucide-svelte';
|
||||||
|
import { i } from "@inlang/sdk-js";
|
||||||
|
export let data;
|
||||||
|
const signInSchema = userSchema.pick({ email: true, password: true });
|
||||||
|
const { form, errors, enhance, delayed } = superForm(data.form, {
|
||||||
|
taintedMessage: null,
|
||||||
|
validators: 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="/auth/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="/auth/password/reset" class="font-semibold">{i("forgotPassword")}</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
112
src/lib/components/signup.svelte
Normal file
112
src/lib/components/signup.svelte
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { superForm } from 'sveltekit-superforms/client';
|
||||||
|
import { userSchema } from '$lib/config/zod-schemas';
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
const signUpSchema = userSchema.pick({
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
username: true,
|
||||||
|
email: true,
|
||||||
|
password: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const { form, errors, enhance, delayed } = superForm(data.form, {
|
||||||
|
taintedMessage: null,
|
||||||
|
validators: signUpSchema,
|
||||||
|
delayMs: 0
|
||||||
|
});
|
||||||
|
// $: termsValue = $form.terms as Writable<boolean>;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form method="POST" action="/auth/signup" 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>
|
||||||
4
src/lib/config/constants.ts
Normal file
4
src/lib/config/constants.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { dev } from '$app/environment';
|
||||||
|
export const BASE_URL = dev ? 'http://localhost:5173' : 'https://boredgame.vercel.app';
|
||||||
|
export const APP_NAME = 'Bored Game';
|
||||||
|
export const DOMAIN = 'boredgame.vercel.app';
|
||||||
5
src/lib/config/prisma.ts
Normal file
5
src/lib/config/prisma.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export default prisma;
|
||||||
44
src/lib/config/zod-schemas.ts
Normal file
44
src/lib/config/zod-schemas.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const userSchema = z.object({
|
||||||
|
firstName: z.string().trim().optional(),
|
||||||
|
lastName: z.string().trim().optional(),
|
||||||
|
email: z.string().email({ message: 'Please enter a valid email address' }).optional(),
|
||||||
|
username: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(3, { message: 'Username must be at least 3 characters' })
|
||||||
|
.max(50, { message: 'Username must be less than 50 characters' }),
|
||||||
|
password: z
|
||||||
|
.string({ required_error: 'Password is required' })
|
||||||
|
.trim()
|
||||||
|
.min(8, { message: 'Password must be at least 8 characters' })
|
||||||
|
.max(128, { message: 'Password must be less than 128 characters' }),
|
||||||
|
confirmPassword: z
|
||||||
|
.string({ required_error: 'Confirm Password is required' })
|
||||||
|
.trim()
|
||||||
|
.min(8, { message: 'Confirm Password must be at least 8 characters' }),
|
||||||
|
role: z.enum(['USER', 'ADMIN'], { required_error: 'You must have a role' }).default('USER'),
|
||||||
|
verified: z.boolean().default(false),
|
||||||
|
token: z.string().optional(),
|
||||||
|
receiveEmail: z.boolean().default(false),
|
||||||
|
createdAt: z.date().optional(),
|
||||||
|
updatedAt: z.date().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateUserPasswordSchema = userSchema
|
||||||
|
.pick({ password: true, confirmPassword: true })
|
||||||
|
.superRefine(({ confirmPassword, password }, ctx) => {
|
||||||
|
if (confirmPassword !== password) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: 'custom',
|
||||||
|
message: 'Password and Confirm Password must match',
|
||||||
|
path: ['password']
|
||||||
|
});
|
||||||
|
ctx.addIssue({
|
||||||
|
code: 'custom',
|
||||||
|
message: 'Password and Confirm Password must match',
|
||||||
|
path: ['confirmPassword']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
18
src/lib/data.json
Normal file
18
src/lib/data.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"email": "johndoe@example.com",
|
||||||
|
"username": "johndoe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"firstName": "Jane",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"email": "janedoe@example.com",
|
||||||
|
"username": "janedoe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
4
src/lib/prisma.ts
Normal file
4
src/lib/prisma.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
export default prisma;
|
||||||
70
src/lib/server/log.ts
Normal file
70
src/lib/server/log.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { Client } from '@axiomhq/axiom-node';
|
||||||
|
import { AXIOM_TOKEN, AXIOM_ORG_ID, AXIOM_DATASET } from '$env/static/private';
|
||||||
|
import getAllUrlParams from '$lib/util/getAllUrlParams';
|
||||||
|
import parseTrack from '$lib/util/parseTrack';
|
||||||
|
import parseMessage from '$lib/util/parseMessage';
|
||||||
|
import { DOMAIN } from '$lib/config/constants';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
export default async function log(statusCode: number, event) {
|
||||||
|
try {
|
||||||
|
let level = 'info';
|
||||||
|
if (statusCode >= 400) {
|
||||||
|
level = 'error';
|
||||||
|
}
|
||||||
|
const error = event?.locals?.error || undefined;
|
||||||
|
const errorId = event?.locals?.errorId || undefined;
|
||||||
|
const errorStackTrace = event?.locals?.errorStackTrace || undefined;
|
||||||
|
let urlParams = {};
|
||||||
|
if (event?.url?.search) {
|
||||||
|
urlParams = await getAllUrlParams(event?.url?.search);
|
||||||
|
}
|
||||||
|
let messageEvents = {};
|
||||||
|
if (event?.locals?.message) {
|
||||||
|
messageEvents = await parseMessage(event?.locals?.message);
|
||||||
|
}
|
||||||
|
let trackEvents = {};
|
||||||
|
if (event?.locals?.track) {
|
||||||
|
trackEvents = await parseTrack(event?.locals?.track);
|
||||||
|
}
|
||||||
|
|
||||||
|
let referer = event.request.headers.get('referer');
|
||||||
|
if (referer) {
|
||||||
|
const refererUrl = await new URL(referer);
|
||||||
|
const refererHostname = refererUrl.hostname;
|
||||||
|
if (refererHostname === 'localhost' || refererHostname === DOMAIN) {
|
||||||
|
referer = refererUrl.pathname;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
referer = undefined;
|
||||||
|
}
|
||||||
|
const logData: object = {
|
||||||
|
level: level,
|
||||||
|
method: event.request.method,
|
||||||
|
path: event.url.pathname,
|
||||||
|
status: statusCode,
|
||||||
|
timeInMs: Date.now() - event?.locals?.startTimer,
|
||||||
|
user: event?.locals?.user?.email,
|
||||||
|
userId: event?.locals?.user?.userId,
|
||||||
|
referer: referer,
|
||||||
|
error: error,
|
||||||
|
errorId: errorId,
|
||||||
|
errorStackTrace: errorStackTrace,
|
||||||
|
...urlParams,
|
||||||
|
...messageEvents,
|
||||||
|
...trackEvents
|
||||||
|
};
|
||||||
|
console.log('log: ', JSON.stringify(logData));
|
||||||
|
if (!AXIOM_TOKEN || !AXIOM_ORG_ID || !AXIOM_DATASET) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const client = new Client({
|
||||||
|
token: AXIOM_TOKEN,
|
||||||
|
orgId: AXIOM_ORG_ID
|
||||||
|
});
|
||||||
|
await client.ingestEvents(AXIOM_DATASET, [logData]);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Error Logger: ${JSON.stringify(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/lib/server/lucia.ts
Normal file
30
src/lib/server/lucia.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
// lib/server/lucia.ts
|
||||||
|
import lucia from 'lucia-auth';
|
||||||
|
import { sveltekit } from 'lucia-auth/middleware';
|
||||||
|
import prisma from '@lucia-auth/adapter-prisma';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { dev } from '$app/environment';
|
||||||
|
|
||||||
|
export const auth = lucia({
|
||||||
|
adapter: prisma(new PrismaClient()),
|
||||||
|
env: dev ? 'DEV' : 'PROD',
|
||||||
|
middleware: sveltekit(),
|
||||||
|
transformDatabaseUser: (userData) => {
|
||||||
|
return {
|
||||||
|
userId: userData.id,
|
||||||
|
username: userData.username,
|
||||||
|
email: userData.email,
|
||||||
|
firstName: userData.firstName,
|
||||||
|
lastName: userData.lastName,
|
||||||
|
role: userData.role,
|
||||||
|
verified: userData.verified,
|
||||||
|
receiveEmail: userData.receiveEmail,
|
||||||
|
token: userData.token
|
||||||
|
};
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
debugMode: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Auth = typeof auth;
|
||||||
5
src/lib/util/convertNameToInitials.ts
Normal file
5
src/lib/util/convertNameToInitials.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default function convertNameToInitials(firstName: string, lastName: string): string {
|
||||||
|
const firstInitial = Array.from(firstName)[0];
|
||||||
|
const lastInitial = Array.from(lastName)[0];
|
||||||
|
return `${firstInitial}${lastInitial}`;
|
||||||
|
}
|
||||||
11
src/lib/util/getAllUrlParams.ts
Normal file
11
src/lib/util/getAllUrlParams.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export default async function getAllUrlParams(url: string): Promise<object> {
|
||||||
|
let paramsObj = {};
|
||||||
|
try {
|
||||||
|
url = url?.slice(1); // remove leading ?
|
||||||
|
if (!url) return {}; // if no params return
|
||||||
|
paramsObj = await Object.fromEntries(await new URLSearchParams(url));
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error: ', error);
|
||||||
|
}
|
||||||
|
return paramsObj;
|
||||||
|
}
|
||||||
15
src/lib/util/parseMessage.ts
Normal file
15
src/lib/util/parseMessage.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
export default async function parseMessage(message: unknown): Promise<object> {
|
||||||
|
let messageObj = {};
|
||||||
|
try {
|
||||||
|
if (message) {
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
messageObj = { message: message };
|
||||||
|
} else {
|
||||||
|
messageObj = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error: ', error);
|
||||||
|
}
|
||||||
|
return messageObj;
|
||||||
|
}
|
||||||
15
src/lib/util/parseTrack.ts
Normal file
15
src/lib/util/parseTrack.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
export default async function parseTrack(track: unknown): Promise<object> {
|
||||||
|
let trackObj = {};
|
||||||
|
try {
|
||||||
|
if (track) {
|
||||||
|
if (typeof track === 'string') {
|
||||||
|
trackObj = { track: track };
|
||||||
|
} else {
|
||||||
|
trackObj = track;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error: ', error);
|
||||||
|
}
|
||||||
|
return trackObj;
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,13 @@ export const saved_game_schema = z.object({
|
||||||
playtime: IntegerString(z.number())
|
playtime: IntegerString(z.number())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const list_game_request_schema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
externalId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ListGameSchema = typeof list_game_request_schema;
|
||||||
|
|
||||||
// https://github.com/colinhacks/zod/discussions/330
|
// https://github.com/colinhacks/zod/discussions/330
|
||||||
function IntegerString<schema extends ZodNumber | ZodOptional<ZodNumber>>(schema: schema) {
|
function IntegerString<schema extends ZodNumber | ZodOptional<ZodNumber>>(schema: schema) {
|
||||||
return z.preprocess(
|
return z.preprocess(
|
||||||
|
|
@ -83,6 +90,8 @@ export const search_schema = z
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type SearchSchema = typeof search_schema;
|
||||||
|
|
||||||
export const search_result_schema = z.object({
|
export const search_result_schema = z.object({
|
||||||
client_id: z.string(),
|
client_id: z.string(),
|
||||||
limit: z.number(),
|
limit: z.number(),
|
||||||
|
|
@ -134,6 +143,8 @@ export const search_result_schema = z.object({
|
||||||
fields: z.string()
|
fields: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type SearchResultSchema = typeof search_result_schema;
|
||||||
|
|
||||||
export const game_schema = z.object({
|
export const game_schema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
handle: z.string(),
|
handle: z.string(),
|
||||||
|
|
@ -156,6 +167,82 @@ export const game_schema = z.object({
|
||||||
playtime: z.string()
|
playtime: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const category_schema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mechanics_schema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const gameSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
year_published: z.number().optional(),
|
||||||
|
min_players: z.number().optional(),
|
||||||
|
max_players: z.number().optional(),
|
||||||
|
min_playtime: z.number().optional(),
|
||||||
|
max_playtime: z.number().optional(),
|
||||||
|
min_age: z.number().optional(),
|
||||||
|
image_url: z.string().optional(),
|
||||||
|
thumb_url: z.string().optional(),
|
||||||
|
url: z.string().optional(),
|
||||||
|
rules_url: z.string().optional(),
|
||||||
|
weight_amount: z.number().optional(),
|
||||||
|
weight_units: z.enum(['Medium', 'Heavy']).optional(),
|
||||||
|
categories: z.array(category_schema).optional(),
|
||||||
|
mechanics: z.array(mechanics_schema).optional(),
|
||||||
|
designers: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
publishers: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
artists: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
names: z.array(z.string()).optional(),
|
||||||
|
expansions: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
year_published: z.number().optional()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
primary_publisher: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchResultSchema = z.object({
|
||||||
|
games: z.array(gameSchema),
|
||||||
|
count: z.number()
|
||||||
|
});
|
||||||
|
|
||||||
// export const game_raw_schema_json = zodToJsonSchema(game_schema, {
|
// export const game_raw_schema_json = zodToJsonSchema(game_schema, {
|
||||||
// $refStrategy: 'none',
|
// $refStrategy: 'none',
|
||||||
// });
|
// });
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
<h1>The page you requested doesn't exist! 🤷♂️</h1>
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if $page.status === 404}
|
||||||
|
<h1>The page you requested doesn't exist! 🤷♂️</h1>
|
||||||
|
<h3 class="mt-6"><a href="/">Go Home</a></h3>
|
||||||
|
{:else}
|
||||||
|
<h1 class="h1">Unexpected Error</h1>
|
||||||
|
<h3 class="mt-6">We're investigating the issue.</h3>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $page.error?.errorId}
|
||||||
|
<p class="mt-6">Error ID: {$page.error.errorId}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
h1 {
|
h1 {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export async function load({ url }) {
|
export const load = async ({ url, locals }) => {
|
||||||
return {
|
return {
|
||||||
url: url.pathname
|
url: url.pathname,
|
||||||
|
user: locals.user
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,21 @@
|
||||||
import { navigating } from '$app/stores';
|
import { navigating } from '$app/stores';
|
||||||
import debounce from 'just-debounce-it';
|
import debounce from 'just-debounce-it';
|
||||||
import { Toy } from '@leveluptuts/svelte-toy';
|
import { Toy } from '@leveluptuts/svelte-toy';
|
||||||
|
import 'iconify-icon';
|
||||||
import Analytics from '$lib/components/analytics.svelte';
|
import Analytics from '$lib/components/analytics.svelte';
|
||||||
import Header from '$root/lib/components/header/index.svelte';
|
import Header from '$lib/components/header/index.svelte';
|
||||||
import Footer from '$lib/components/footer.svelte';
|
import Footer from '$lib/components/footer.svelte';
|
||||||
import Loading from '$lib/components/loading.svelte';
|
import Loading from '$lib/components/loading.svelte';
|
||||||
import Transition from '$lib/components/transition/index.svelte';
|
import Transition from '$lib/components/transition/index.svelte';
|
||||||
import Portal from '$lib/Portal.svelte';
|
import Portal from '$lib/Portal.svelte';
|
||||||
import { boredState } from '$lib/stores/boredState';
|
import { boredState } from '$lib/stores/boredState';
|
||||||
import { collectionStore } from '$lib/stores/collectionStore';
|
import { collectionStore } from '$lib/stores/collectionStore';
|
||||||
import { wishlistStore } from '$root/lib/stores/wishlistStore';
|
import { wishlistStore } from '$lib/stores/wishlistStore';
|
||||||
import { gameStore } from '$lib/stores/gameSearchStore';
|
import { gameStore } from '$lib/stores/gameSearchStore';
|
||||||
import { toast } from '$lib/components/toast/toast';
|
import { toast } from '$lib/components/toast/toast';
|
||||||
import Toast from '$lib/components/toast/Toast.svelte';
|
import Toast from '$lib/components/toast/Toast.svelte';
|
||||||
import '$root/styles/styles.pcss';
|
import '$styles/styles.pcss';
|
||||||
import 'iconify-icon';
|
import type { SavedGameType } from '$lib/types';
|
||||||
|
|
||||||
import type { SavedGameType } from '$root/lib/types';
|
|
||||||
|
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if ($navigating) {
|
if ($navigating) {
|
||||||
|
|
@ -77,7 +75,7 @@
|
||||||
<Analytics />
|
<Analytics />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if dev}
|
<!-- {#if dev}
|
||||||
<Toy
|
<Toy
|
||||||
register={{
|
register={{
|
||||||
boredState,
|
boredState,
|
||||||
|
|
@ -87,36 +85,32 @@
|
||||||
toast
|
toast
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if} -->
|
||||||
|
|
||||||
<!-- <Transition transition={{ type: 'fade', duration: 250 }}> -->
|
<div class="wrapper">
|
||||||
<div class="wrapper">
|
<Header user={data.user} />
|
||||||
<Header />
|
<main>
|
||||||
<main>
|
<Transition url={data.url} transition={{ type: 'page' }}>
|
||||||
<Transition url={data.url} transition={{ type: 'page' }}>
|
<slot />
|
||||||
<slot />
|
</Transition>
|
||||||
</Transition>
|
</main>
|
||||||
</main>
|
<Footer />
|
||||||
<Footer />
|
</div>
|
||||||
|
{#if $boredState?.loading}
|
||||||
|
<Portal>
|
||||||
|
<div class="loading">
|
||||||
|
<Loading />
|
||||||
|
<h3>Loading...</h3>
|
||||||
|
</div>
|
||||||
|
<div class="background" />
|
||||||
|
</Portal>
|
||||||
|
{/if}
|
||||||
|
{#if isOpen}
|
||||||
|
<div class="container">
|
||||||
|
<svelte:component this={$boredState?.dialog?.content} />
|
||||||
</div>
|
</div>
|
||||||
{#if $boredState?.loading}
|
{/if}
|
||||||
<Portal>
|
<Toast />
|
||||||
<!-- <Transition transition={{ type: 'fade', duration: 0 }}> -->
|
|
||||||
<div class="loading">
|
|
||||||
<Loading />
|
|
||||||
<h3>Loading...</h3>
|
|
||||||
</div>
|
|
||||||
<!-- </Transition> -->
|
|
||||||
<div class="background" />
|
|
||||||
</Portal>
|
|
||||||
{/if}
|
|
||||||
{#if isOpen}
|
|
||||||
<div class="container">
|
|
||||||
<svelte:component this={$boredState?.dialog?.content} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<Toast />
|
|
||||||
<!-- </Transition> -->
|
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.loading {
|
.loading {
|
||||||
|
|
@ -129,7 +123,7 @@
|
||||||
place-items: center;
|
place-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
h3 {
|
& h3 {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ import { search_schema } from '$lib/zodValidation';
|
||||||
|
|
||||||
export const load = async ({ fetch, url }) => {
|
export const load = async ({ fetch, url }) => {
|
||||||
const formData = Object.fromEntries(url?.searchParams);
|
const formData = Object.fromEntries(url?.searchParams);
|
||||||
|
console.log('formData', formData);
|
||||||
formData.name = formData?.q;
|
formData.name = formData?.q;
|
||||||
const form = await superValidate(formData, search_schema);
|
const form = await superValidate(formData, search_schema);
|
||||||
|
console.log('form', form);
|
||||||
return { form };
|
return { form };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { superForm } from 'sveltekit-superforms/client';
|
|
||||||
import TextSearch from '$lib/components/search/textSearch/index.svelte';
|
import TextSearch from '$lib/components/search/textSearch/index.svelte';
|
||||||
import RandomSearch from '$lib/components/search/random/index.svelte';
|
import RandomSearch from '$lib/components/search/random/index.svelte';
|
||||||
import Random from '$lib/components/random/index.svelte';
|
import Random from '$lib/components/random/index.svelte';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
const { form, errors, constraints } = superForm(data?.form);
|
export let formData;
|
||||||
|
console.log('formData', formData);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -20,11 +20,11 @@
|
||||||
</p>
|
</p>
|
||||||
<p>Or pick a random game!</p>
|
<p>Or pick a random game!</p>
|
||||||
<div class="random-buttons">
|
<div class="random-buttons">
|
||||||
<RandomSearch />
|
<RandomSearch data={data.form} />
|
||||||
<Random />
|
<Random />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<TextSearch showButton advancedSearch {form} {errors} {constraints} />
|
<TextSearch showButton advancedSearch data={data.form} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
||||||
5
src/routes/admin/+layout.server.ts
Normal file
5
src/routes/admin/+layout.server.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const load = async function ({ locals }) {
|
||||||
|
if (!locals?.user?.role?.includes('admin')) throw redirect(302, '/');
|
||||||
|
};
|
||||||
1
src/routes/admin/+layout.svelte
Normal file
1
src/routes/admin/+layout.svelte
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<slot />
|
||||||
0
src/routes/admin/+page.svelte
Normal file
0
src/routes/admin/+page.svelte
Normal file
62
src/routes/auth/signin/+page.server.ts
Normal file
62
src/routes/auth/signin/+page.server.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { fail, redirect } from '@sveltejs/kit';
|
||||||
|
import { setError, superValidate } from 'sveltekit-superforms/server';
|
||||||
|
import { auth } from '$lib/server/lucia';
|
||||||
|
import prisma from '$lib/prisma.js';
|
||||||
|
import { userSchema } from '$lib/config/zod-schemas';
|
||||||
|
import { add_user_to_role } from '$db/roles';
|
||||||
|
|
||||||
|
const signInSchema = userSchema.pick({
|
||||||
|
username: true,
|
||||||
|
password: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export const load = async (event) => {
|
||||||
|
const session = await event.locals.auth.validate();
|
||||||
|
if (session) {
|
||||||
|
throw redirect(302, '/');
|
||||||
|
}
|
||||||
|
const form = await superValidate(event, signInSchema);
|
||||||
|
return {
|
||||||
|
form
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
default: async (event) => {
|
||||||
|
const form = await superValidate(event, signInSchema);
|
||||||
|
|
||||||
|
if (!form.valid) {
|
||||||
|
form.data.password = '';
|
||||||
|
return fail(400, {
|
||||||
|
form
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const key = await auth.useKey('username', form.data.username, form.data.password);
|
||||||
|
const session = await auth.createSession(key.userId);
|
||||||
|
event.locals.auth.setSession(session);
|
||||||
|
|
||||||
|
const user = await prisma.authUser.findUnique({
|
||||||
|
where: {
|
||||||
|
id: session.userId
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
roles: {
|
||||||
|
select: {
|
||||||
|
role: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// TODO: need to return error message to the client
|
||||||
|
console.error(e);
|
||||||
|
form.data.password = '';
|
||||||
|
return setError(form, '', 'The username or password is incorrect.');
|
||||||
|
}
|
||||||
|
form.data.username = '';
|
||||||
|
form.data.password = '';
|
||||||
|
return { form };
|
||||||
|
}
|
||||||
|
};
|
||||||
66
src/routes/auth/signin/+page.svelte
Normal file
66
src/routes/auth/signin/+page.svelte
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { superForm } from 'sveltekit-superforms/client';
|
||||||
|
import { userSchema } from '$lib/config/zod-schemas.js';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
const signInSchema = userSchema.pick({ username: true, password: true });
|
||||||
|
const { form, errors, enhance, delayed } = superForm(data.form, {
|
||||||
|
taintedMessage: null,
|
||||||
|
validators: signInSchema,
|
||||||
|
validationMethod: 'oninput',
|
||||||
|
delayMs: 0,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form method="POST" use:enhance>
|
||||||
|
{#if $errors._errors}
|
||||||
|
<aside class="alert">
|
||||||
|
<div class="alert-message">
|
||||||
|
<h3>There was an error signing in</h3>
|
||||||
|
<p>{$errors._errors}</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="sr-only">Username</span>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
autocomplete="username"
|
||||||
|
data-invalid={$form.username}
|
||||||
|
bind:value={$form.username}
|
||||||
|
class="input"
|
||||||
|
class:input-error={$errors.username}
|
||||||
|
/>
|
||||||
|
{#if $errors.username}
|
||||||
|
<small>{$errors.username}</small>
|
||||||
|
{/if}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="sr-only">Password</span>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
data-invalid={$form.password}
|
||||||
|
bind:value={$form.password}
|
||||||
|
class="input"
|
||||||
|
class:input-error={$errors.password}
|
||||||
|
/>
|
||||||
|
{#if $errors.password}
|
||||||
|
<small>{$errors.password}</small>
|
||||||
|
{/if}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="button">Sign In</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
14
src/routes/auth/signout/+page.server.ts
Normal file
14
src/routes/auth/signout/+page.server.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { auth } from '$lib/server/lucia';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
default: async ({ locals }) => {
|
||||||
|
const session = await locals.auth.validate();
|
||||||
|
if (!session) {
|
||||||
|
throw redirect(302, '/auth/signin');
|
||||||
|
}
|
||||||
|
await auth.invalidateSession(session.sessionId); // invalidate session
|
||||||
|
locals.auth.setSession(null); // remove cookie
|
||||||
|
throw redirect(302, '/auth/signin');
|
||||||
|
}
|
||||||
|
};
|
||||||
72
src/routes/auth/signup/+page.server.ts
Normal file
72
src/routes/auth/signup/+page.server.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { fail, redirect } from '@sveltejs/kit';
|
||||||
|
import { setError, superValidate } from 'sveltekit-superforms/server';
|
||||||
|
import { auth } from '$lib/server/lucia';
|
||||||
|
import { userSchema } from '$lib/config/zod-schemas';
|
||||||
|
import { add_user_to_role } from '$db/roles';
|
||||||
|
|
||||||
|
const signUpSchema = userSchema.pick({
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
email: true,
|
||||||
|
username: true,
|
||||||
|
password: true,
|
||||||
|
terms: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export const load = async (event) => {
|
||||||
|
const session = await event.locals.auth.validate();
|
||||||
|
if (session) {
|
||||||
|
throw redirect(302, '/');
|
||||||
|
}
|
||||||
|
const form = await superValidate(event, signUpSchema);
|
||||||
|
return {
|
||||||
|
form
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
default: async (event) => {
|
||||||
|
const form = await superValidate(event, signUpSchema);
|
||||||
|
|
||||||
|
if (!form.valid) {
|
||||||
|
return fail(400, {
|
||||||
|
form
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding user to the db
|
||||||
|
try {
|
||||||
|
console.log('Creating user');
|
||||||
|
const token = crypto.randomUUID();
|
||||||
|
|
||||||
|
const user = await auth.createUser({
|
||||||
|
primaryKey: {
|
||||||
|
providerId: 'username',
|
||||||
|
providerUserId: form.data.username,
|
||||||
|
password: form.data.password
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
email: form.data.email || '',
|
||||||
|
username: form.data.username,
|
||||||
|
firstName: form.data.firstName || '',
|
||||||
|
lastName: form.data.lastName || '',
|
||||||
|
role: 'USER',
|
||||||
|
verified: false,
|
||||||
|
receiveEmail: false,
|
||||||
|
token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add_user_to_role(user.id, 'user');
|
||||||
|
|
||||||
|
console.log('User', user);
|
||||||
|
|
||||||
|
const session = await auth.createSession(user.userId);
|
||||||
|
event.locals.auth.setSession(session);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return setError(form, 'email', 'Unable to create your account. Please try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { form };
|
||||||
|
}
|
||||||
|
};
|
||||||
144
src/routes/auth/signup/+page.svelte
Normal file
144
src/routes/auth/signup/+page.svelte
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { userSchema } from '$lib/config/zod-schemas.js';
|
||||||
|
import { superForm } from 'sveltekit-superforms/client';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
const signUpSchema = userSchema.pick({
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
username: true,
|
||||||
|
email: true,
|
||||||
|
password: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const { form, errors, constraints, enhance, delayed } = superForm(data.form, {
|
||||||
|
taintedMessage: null,
|
||||||
|
validators: signUpSchema,
|
||||||
|
delayMs: 0,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<form method="POST" action="/auth/signup" 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="email"
|
||||||
|
{...$constraints.username}
|
||||||
|
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"
|
||||||
|
{...$constraints.username}
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
padding: 3rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
border: 0.125rem solid rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
button[type="submit"] {
|
||||||
|
border: 0;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.back {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,14 +1,65 @@
|
||||||
import type { PageServerLoad } from "../$types";
|
// import { redirect } from '@sveltejs/kit';
|
||||||
|
// import { superValidate } from 'sveltekit-superforms/server';
|
||||||
|
// import { search_schema } from '$lib/zodValidation';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ fetch, url }) => {
|
export const load = async ({ fetch, url, locals }) => {
|
||||||
const searchParams = Object.fromEntries(url?.searchParams);
|
// const session = await locals.auth.validate();
|
||||||
const q = searchParams?.q;
|
// if (!session) {
|
||||||
const limit = parseInt(searchParams?.limit) || 10;
|
// throw redirect(302, '/auth/signin');
|
||||||
const skip = parseInt(searchParams?.skip) || 0;
|
// }
|
||||||
|
|
||||||
|
console.log('locals load', locals);
|
||||||
|
// const searchParams = Object.fromEntries(url?.searchParams);
|
||||||
|
// const q = searchParams?.q;
|
||||||
|
// const limit = parseInt(searchParams?.limit) || 10;
|
||||||
|
// const skip = parseInt(searchParams?.skip) || 0;
|
||||||
|
|
||||||
|
// const searchData = {
|
||||||
|
// q,
|
||||||
|
// limit,
|
||||||
|
// skip
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const form = await superValidate(searchData, search_schema);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// let collection = await locals.prisma.collection.findUnique({
|
||||||
|
// where: {
|
||||||
|
// user_id: session.userId
|
||||||
|
// }
|
||||||
|
// include: {
|
||||||
|
// collectionItems: {
|
||||||
|
// where: {
|
||||||
|
// title: {
|
||||||
|
// contains: q,
|
||||||
|
// mode: 'insensitive'
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// take: limit,
|
||||||
|
// skip
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// console.log('collection', collection);
|
||||||
|
// if (!collection) {
|
||||||
|
// collection = await locals.prisma.collection.create({
|
||||||
|
// data: {
|
||||||
|
// user_id: session.userId
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
return {
|
||||||
|
// form,
|
||||||
|
// collection
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
q,
|
// form,
|
||||||
limit,
|
// collection: []
|
||||||
skip
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,78 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tick, onDestroy } from 'svelte';
|
// import { tick, onDestroy } from 'svelte';
|
||||||
import Game from '$lib/components/game/index.svelte';
|
// import Game from '$lib/components/game/index.svelte';
|
||||||
import { collectionStore } from '$lib/stores/collectionStore';
|
// import { collectionStore } from '$lib/stores/collectionStore';
|
||||||
import type { GameType, SavedGameType } from '$root/lib/types';
|
// import type { GameType, SavedGameType } from '$lib/types';
|
||||||
import { boredState } from '$root/lib/stores/boredState';
|
// import { boredState } from '$lib/stores/boredState';
|
||||||
import Pagination from '$root/lib/components/pagination/index.svelte';
|
// import Pagination from '$lib/components/pagination/index.svelte';
|
||||||
import RemoveCollectionDialog from '$root/lib/components/dialog/RemoveCollectionDialog.svelte';
|
// import RemoveCollectionDialog from '$lib/components/dialog/RemoveCollectionDialog.svelte';
|
||||||
import RemoveWishlistDialog from '$root/lib/components/dialog/RemoveWishlistDialog.svelte';
|
// import RemoveWishlistDialog from '$lib/components/dialog/RemoveWishlistDialog.svelte';
|
||||||
import { createSearchStore, searchHandler } from '$root/lib/stores/search';
|
// import { createSearchStore, searchHandler } from '$lib/stores/search';
|
||||||
import type { PageData } from './$types';
|
|
||||||
|
|
||||||
export let data: PageData;
|
export let data;
|
||||||
console.log(`Page data: ${JSON.stringify(data)}`)
|
console.log(`Page data: ${JSON.stringify(data)}`);
|
||||||
|
// let collectionItems = data?.collection?.collectionItems;
|
||||||
|
|
||||||
let gameToRemove: GameType | SavedGameType;
|
// let gameToRemove: GameType | SavedGameType;
|
||||||
let pageSize = 10;
|
// let pageSize = 10;
|
||||||
let page = 1;
|
// let page = 1;
|
||||||
|
|
||||||
const searchStore = createSearchStore($collectionStore);
|
// const searchStore = createSearchStore($collectionStore);
|
||||||
console.log('searchStore', $searchStore);
|
// console.log('searchStore', $searchStore);
|
||||||
|
|
||||||
const unsubscribe = searchStore.subscribe((model) => searchHandler(model));
|
// const unsubscribe = searchStore.subscribe((model) => searchHandler(model));
|
||||||
|
|
||||||
onDestroy(() => {
|
// onDestroy(() => {
|
||||||
unsubscribe();
|
// unsubscribe();
|
||||||
});
|
// });
|
||||||
|
|
||||||
$: skip = (page - 1) * pageSize;
|
// $: skip = (page - 1) * pageSize;
|
||||||
$: gamesShown = $searchStore.data.slice(skip, skip + pageSize);
|
// $: gamesShown = $searchStore.data.slice(skip, skip + pageSize);
|
||||||
$: totalItems = $searchStore.search === '' ? $collectionStore.length : $searchStore.filtered.length;
|
// $: totalItems = $searchStore.search === '' ? $collectionStore.length : $searchStore.filtered.length;
|
||||||
|
|
||||||
interface RemoveGameEvent extends Event {
|
// interface RemoveGameEvent extends Event {
|
||||||
detail: GameType | SavedGameType;
|
// detail: GameType | SavedGameType;
|
||||||
}
|
// }
|
||||||
|
|
||||||
function handleRemoveCollection(event: RemoveGameEvent) {
|
// function handleRemoveCollection(event: RemoveGameEvent) {
|
||||||
console.log('Remove collection event handler');
|
// console.log('Remove collection event handler');
|
||||||
console.log('event', event);
|
// console.log('event', event);
|
||||||
gameToRemove = event?.detail;
|
// gameToRemove = event?.detail;
|
||||||
boredState.update((n) => ({
|
// boredState.update((n) => ({
|
||||||
...n,
|
// ...n,
|
||||||
dialog: { isOpen: true, content: RemoveCollectionDialog, additionalData: gameToRemove }
|
// dialog: { isOpen: true, content: RemoveCollectionDialog, additionalData: gameToRemove }
|
||||||
}));
|
// }));
|
||||||
}
|
// }
|
||||||
|
|
||||||
function handleRemoveWishlist(event: RemoveGameEvent) {
|
// function handleRemoveWishlist(event: RemoveGameEvent) {
|
||||||
console.log('Remove wishlist event handler');
|
// console.log('Remove wishlist event handler');
|
||||||
console.log('event', event);
|
// console.log('event', event);
|
||||||
gameToRemove = event?.detail;
|
// gameToRemove = event?.detail;
|
||||||
boredState.update((n) => ({
|
// boredState.update((n) => ({
|
||||||
...n,
|
// ...n,
|
||||||
dialog: { isOpen: true, content: RemoveWishlistDialog, additionalData: gameToRemove }
|
// dialog: { isOpen: true, content: RemoveWishlistDialog, additionalData: gameToRemove }
|
||||||
}));
|
// }));
|
||||||
}
|
// }
|
||||||
|
|
||||||
async function handleNextPageEvent(event: CustomEvent) {
|
// async function handleNextPageEvent(event: CustomEvent) {
|
||||||
if (+event?.detail?.page === page + 1) {
|
// if (+event?.detail?.page === page + 1) {
|
||||||
page += 1;
|
// page += 1;
|
||||||
}
|
// }
|
||||||
await tick();
|
// await tick();
|
||||||
}
|
// }
|
||||||
|
|
||||||
async function handlePreviousPageEvent(event: CustomEvent) {
|
// async function handlePreviousPageEvent(event: CustomEvent) {
|
||||||
if (+event?.detail?.page === page - 1) {
|
// if (+event?.detail?.page === page - 1) {
|
||||||
page -= 1;
|
// page -= 1;
|
||||||
}
|
// }
|
||||||
await tick();
|
// await tick();
|
||||||
}
|
// }
|
||||||
|
|
||||||
async function handlePerPageEvent(event: CustomEvent) {
|
// async function handlePerPageEvent(event: CustomEvent) {
|
||||||
page = 1;
|
// page = 1;
|
||||||
pageSize = event.detail.pageSize;
|
// pageSize = event.detail.pageSize;
|
||||||
await tick();
|
// await tick();
|
||||||
}
|
// }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -80,9 +80,9 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1>Your Collection</h1>
|
<h1>Your Collection</h1>
|
||||||
<input type="text" id="search" name="search" placeholder="Search Your Collection" bind:value={$searchStore.search} />
|
<!-- <input type="text" id="search" name="search" placeholder="Search Your Collection" bind:value={$searchStore.search} /> -->
|
||||||
|
|
||||||
<div class="games">
|
<!-- <div class="games">
|
||||||
<div class="games-list">
|
<div class="games-list">
|
||||||
{#if $collectionStore.length === 0}
|
{#if $collectionStore.length === 0}
|
||||||
<h2>No games in your collection</h2>
|
<h2>No games in your collection</h2>
|
||||||
|
|
@ -109,9 +109,9 @@
|
||||||
on:perPageEvent={handlePerPageEvent}
|
on:perPageEvent={handlePerPageEvent}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="postcss">
|
||||||
h1 {
|
h1 {
|
||||||
margin: 1.5rem 0rem;
|
margin: 1.5rem 0rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,16 @@
|
||||||
import { collectionStore } from '$lib/stores/collectionStore';
|
import { collectionStore } from '$lib/stores/collectionStore';
|
||||||
import { wishlistStore } from '$lib/stores/wishlistStore';
|
import { wishlistStore } from '$lib/stores/wishlistStore';
|
||||||
import Button from '$lib/components/button/index.svelte';
|
import Button from '$lib/components/button/index.svelte';
|
||||||
import RemoveCollectionDialog from '$root/lib/components/dialog/RemoveCollectionDialog.svelte';
|
import RemoveCollectionDialog from '$lib/components/dialog/RemoveCollectionDialog.svelte';
|
||||||
import { addToCollection } from '$lib/util/manipulateCollection';
|
import { addToCollection } from '$lib/util/manipulateCollection';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { boredState } from '$root/lib/stores/boredState';
|
import { boredState } from '$lib/stores/boredState';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import LinkWithIcon from '$root/lib/components/LinkWithIcon.svelte';
|
import LinkWithIcon from '$lib/components/LinkWithIcon.svelte';
|
||||||
import { addToWishlist } from '$root/lib/util/manipulateWishlist';
|
import { addToWishlist } from '$lib/util/manipulateWishlist';
|
||||||
import RemoveWishlistDialog from '$root/lib/components/dialog/RemoveWishlistDialog.svelte';
|
import RemoveWishlistDialog from '$lib/components/dialog/RemoveWishlistDialog.svelte';
|
||||||
import { binarySearchOnStore } from '$root/lib/util/binarySearchOnStore';
|
import { binarySearchOnStore } from '$lib/util/binarySearchOnStore';
|
||||||
import { convertToSavedGame } from '$root/lib/util/gameMapper';
|
import { convertToSavedGame } from '$lib/util/gameMapper';
|
||||||
|
|
||||||
$: existsInCollection = $collectionStore.find((item: SavedGameType) => item.id === game.id);
|
$: existsInCollection = $collectionStore.find((item: SavedGameType) => item.id === game.id);
|
||||||
$: existsInWishlist = $wishlistStore.find((item: SavedGameType) => item.id === game.id);
|
$: existsInWishlist = $wishlistStore.find((item: SavedGameType) => item.id === game.id);
|
||||||
|
|
@ -205,7 +205,7 @@
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
|
||||||
@media (max-width: env(--medium-viewport)) {
|
@media (max-width: 700px) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +220,7 @@
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: env(--xsmall-viewport)) {
|
@media (max-width: 500px) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import type { Actions, RequestEvent } from '../$types';
|
|
||||||
import { BOARD_GAME_ATLAS_CLIENT_ID } from '$env/static/private';
|
import { BOARD_GAME_ATLAS_CLIENT_ID } from '$env/static/private';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import { superValidate } from 'sveltekit-superforms/server';
|
import { superValidate } from 'sveltekit-superforms/server';
|
||||||
|
|
@ -6,7 +5,7 @@ import type { GameType, SearchQuery } from '$lib/types';
|
||||||
import { mapAPIGameToBoredGame } from '$lib/util/gameMapper';
|
import { mapAPIGameToBoredGame } from '$lib/util/gameMapper';
|
||||||
import { search_schema } from '$lib/zodValidation';
|
import { search_schema } from '$lib/zodValidation';
|
||||||
|
|
||||||
async function searchForGames(urlQueryParams) {
|
async function searchForGames(urlQueryParams: SearchQuery) {
|
||||||
try {
|
try {
|
||||||
const url = `https://api.boardgameatlas.com/api/search${
|
const url = `https://api.boardgameatlas.com/api/search${
|
||||||
urlQueryParams ? `?${urlQueryParams}` : ''
|
urlQueryParams ? `?${urlQueryParams}` : ''
|
||||||
|
|
@ -27,7 +26,7 @@ async function searchForGames(urlQueryParams) {
|
||||||
let totalCount = 0;
|
let totalCount = 0;
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const gameResponse = await response.json();
|
const gameResponse = await response.json();
|
||||||
const gameList = gameResponse?.games;
|
const gameList: GameType[] = gameResponse?.games;
|
||||||
totalCount = gameResponse?.count;
|
totalCount = gameResponse?.count;
|
||||||
console.log('totalCount', totalCount);
|
console.log('totalCount', totalCount);
|
||||||
gameList.forEach((game) => {
|
gameList.forEach((game) => {
|
||||||
|
|
@ -109,8 +108,9 @@ export const load = async ({ fetch, url }) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions = {
|
||||||
random: async ({ request }: RequestEvent): Promise<any> => {
|
random: async ({ request }): Promise<any> => {
|
||||||
|
const form = await superValidate(request, search_schema);
|
||||||
const queryParams: SearchQuery = {
|
const queryParams: SearchQuery = {
|
||||||
order_by: 'rank',
|
order_by: 'rank',
|
||||||
ascending: false,
|
ascending: false,
|
||||||
|
|
@ -127,47 +127,9 @@ export const actions: Actions = {
|
||||||
|
|
||||||
const urlQueryParams = new URLSearchParams(newQueryParams);
|
const urlQueryParams = new URLSearchParams(newQueryParams);
|
||||||
|
|
||||||
try {
|
|
||||||
const url = `https://api.boardgameatlas.com/api/search${
|
|
||||||
urlQueryParams ? `?${urlQueryParams}` : ''
|
|
||||||
}`;
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'get',
|
|
||||||
headers: {
|
|
||||||
'content-type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// console.log('board game response', response);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.log('Status not 200', response.status);
|
|
||||||
throw error(response.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
|
||||||
const gameResponse = await response.json();
|
|
||||||
// console.log('gameResponse', gameResponse);
|
|
||||||
const gameList = gameResponse?.games;
|
|
||||||
const totalCount = gameResponse?.count;
|
|
||||||
console.log('totalCount', totalCount);
|
|
||||||
const games: GameType[] = [];
|
|
||||||
gameList.forEach((game) => {
|
|
||||||
game.players = `${game.min_players}-${game.max_players}`;
|
|
||||||
game.playtime = `${game.min_playtime}-${game.max_playtime}`;
|
|
||||||
games.push(mapAPIGameToBoredGame(game));
|
|
||||||
});
|
|
||||||
|
|
||||||
// console.log('returning from search', games)
|
|
||||||
|
|
||||||
return {
|
|
||||||
games
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`Error searching board games ${e}`);
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
games: []
|
form,
|
||||||
|
searchData: await searchForGames(urlQueryParams)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
import TextSearch from '$lib/components/search/textSearch/index.svelte';
|
import TextSearch from '$lib/components/search/textSearch/index.svelte';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
const { form, errors, constraints } = superForm(data?.form);
|
|
||||||
|
|
||||||
$: if (data?.searchData?.games) {
|
$: if (data?.searchData?.games) {
|
||||||
gameStore.removeAll();
|
gameStore.removeAll();
|
||||||
|
|
@ -13,5 +12,5 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="game-search">
|
<div class="game-search">
|
||||||
<TextSearch showButton advancedSearch {form} {errors} {constraints} />
|
<TextSearch showButton advancedSearch data={data.form} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
95
src/routes/wishlist/+page.server.ts
Normal file
95
src/routes/wishlist/+page.server.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { superValidate } from 'sveltekit-superforms/server';
|
||||||
|
import prisma from '$lib/prisma.js';
|
||||||
|
import { list_game_request_schema } from '$lib/zodValidation';
|
||||||
|
|
||||||
|
export async function load({ params, locals }) {
|
||||||
|
const session = await locals.auth.validate();
|
||||||
|
if (!session) {
|
||||||
|
throw redirect(302, '/auth/signin');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let wishlists = await prisma.wishlist.findMany({
|
||||||
|
where: {
|
||||||
|
user_id: session.userId
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
items: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wishlists.length === 0) {
|
||||||
|
const wishlist = await prisma.wishlist.create({
|
||||||
|
data: {
|
||||||
|
user_id: session.userId,
|
||||||
|
name: 'My Wishlist'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wishlists.push(wishlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
wishlists
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
// Add game to a wishlist
|
||||||
|
add: async (event) => {
|
||||||
|
const { params, locals, request } = event;
|
||||||
|
const form = await superValidate(event, list_game_request_schema);
|
||||||
|
|
||||||
|
const session = await locals.auth.validate();
|
||||||
|
if (!session) {
|
||||||
|
throw redirect(302, '/auth/signin');
|
||||||
|
}
|
||||||
|
|
||||||
|
const game = await prisma.game.findUnique({
|
||||||
|
where: {
|
||||||
|
id: form.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// if (!game) {
|
||||||
|
// throw redirect(302, '/404');
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (game) {
|
||||||
|
const wishlist = await prisma.wishlist.create({
|
||||||
|
data: {
|
||||||
|
user_id: session.userId,
|
||||||
|
name: form.name
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
form
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// Create new wishlist
|
||||||
|
create: async ({ params, locals, request }) => {
|
||||||
|
const session = await locals.auth.validate();
|
||||||
|
if (!session) {
|
||||||
|
throw redirect(302, '/auth/signin');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Delete a wishlist
|
||||||
|
delete: async ({ params, locals, request }) => {
|
||||||
|
const session = await locals.auth.validate();
|
||||||
|
if (!session) {
|
||||||
|
throw redirect(302, '/auth/signin');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Remove game from a wishlist
|
||||||
|
remove: async ({ params, locals, request }) => {
|
||||||
|
const session = await locals.auth.validate();
|
||||||
|
if (!session) {
|
||||||
|
throw redirect(302, '/auth/signin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,70 +1,72 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tick, onDestroy } from 'svelte';
|
// import { tick, onDestroy } from 'svelte';
|
||||||
import Game from '$lib/components/game/index.svelte';
|
// import Game from '$lib/components/game/index.svelte';
|
||||||
import { wishlistStore } from '$lib/stores/wishlistStore';
|
// import { wishlistStore } from '$lib/stores/wishlistStore';
|
||||||
import type { GameType, SavedGameType } from '$root/lib/types';
|
// import type { GameType, SavedGameType } from '$lib/types';
|
||||||
import { boredState } from '$root/lib/stores/boredState';
|
// import { boredState } from '$lib/stores/boredState';
|
||||||
import Pagination from '$root/lib/components/pagination/index.svelte';
|
// import Pagination from '$lib/components/pagination/index.svelte';
|
||||||
import RemoveWishlistDialog from '$root/lib/components/dialog/RemoveWishlistDialog.svelte';
|
// import RemoveWishlistDialog from '$lib/components/dialog/RemoveWishlistDialog.svelte';
|
||||||
import RemoveCollectionDialog from '$root/lib/components/dialog/RemoveCollectionDialog.svelte';
|
// import RemoveCollectionDialog from '$lib/components/dialog/RemoveCollectionDialog.svelte';
|
||||||
import { createSearchStore, searchHandler } from '$root/lib/stores/search';
|
// import { createSearchStore, searchHandler } from '$lib/stores/search';
|
||||||
|
|
||||||
let gameToRemove: GameType | SavedGameType;
|
// let gameToRemove: GameType | SavedGameType;
|
||||||
let pageSize = 10;
|
// let pageSize = 10;
|
||||||
let page = 1;
|
// let page = 1;
|
||||||
|
|
||||||
const searchStore = createSearchStore($wishlistStore);
|
// const searchStore = createSearchStore($wishlistStore);
|
||||||
console.log('searchStore', $searchStore);
|
// console.log('searchStore', $searchStore);
|
||||||
|
|
||||||
const unsubscribe = searchStore.subscribe((model) => searchHandler(model));
|
// const unsubscribe = searchStore.subscribe((model) => searchHandler(model));
|
||||||
|
|
||||||
onDestroy(() => {
|
// onDestroy(() => {
|
||||||
unsubscribe();
|
// unsubscribe();
|
||||||
});
|
// });
|
||||||
|
|
||||||
$: skip = (page - 1) * pageSize;
|
// $: skip = (page - 1) * pageSize;
|
||||||
$: gamesShown = $searchStore.filtered.slice(skip, skip + pageSize);
|
// $: gamesShown = $searchStore.filtered.slice(skip, skip + pageSize);
|
||||||
$: totalItems = $searchStore.search === '' ? $wishlistStore.length : $searchStore.filtered.length;
|
// $: totalItems = $searchStore.search === '' ? $wishlistStore.length : $searchStore.filtered.length;
|
||||||
|
|
||||||
interface RemoveGameEvent extends Event {
|
// interface RemoveGameEvent extends Event {
|
||||||
detail: GameType | SavedGameType;
|
// detail: GameType | SavedGameType;
|
||||||
}
|
// }
|
||||||
|
|
||||||
function handleRemoveCollection(event: RemoveGameEvent) {
|
// function handleRemoveCollection(event: RemoveGameEvent) {
|
||||||
gameToRemove = event?.detail;
|
// gameToRemove = event?.detail;
|
||||||
boredState.update((n) => ({
|
// boredState.update((n) => ({
|
||||||
...n,
|
// ...n,
|
||||||
dialog: { isOpen: true, content: RemoveCollectionDialog, additionalData: gameToRemove }
|
// dialog: { isOpen: true, content: RemoveCollectionDialog, additionalData: gameToRemove }
|
||||||
}));
|
// }));
|
||||||
}
|
// }
|
||||||
|
|
||||||
function handleRemoveWishlist(event: RemoveGameEvent) {
|
// function handleRemoveWishlist(event: RemoveGameEvent) {
|
||||||
gameToRemove = event?.detail;
|
// gameToRemove = event?.detail;
|
||||||
boredState.update((n) => ({
|
// boredState.update((n) => ({
|
||||||
...n,
|
// ...n,
|
||||||
dialog: { isOpen: true, content: RemoveWishlistDialog, additionalData: gameToRemove }
|
// dialog: { isOpen: true, content: RemoveWishlistDialog, additionalData: gameToRemove }
|
||||||
}));
|
// }));
|
||||||
}
|
// }
|
||||||
|
|
||||||
async function handleNextPageEvent(event: CustomEvent) {
|
// async function handleNextPageEvent(event: CustomEvent) {
|
||||||
if (+event?.detail?.page === page + 1) {
|
// if (+event?.detail?.page === page + 1) {
|
||||||
page += 1;
|
// page += 1;
|
||||||
}
|
// }
|
||||||
await tick();
|
// await tick();
|
||||||
}
|
// }
|
||||||
|
|
||||||
async function handlePreviousPageEvent(event: CustomEvent) {
|
// async function handlePreviousPageEvent(event: CustomEvent) {
|
||||||
if (+event?.detail?.page === page - 1) {
|
// if (+event?.detail?.page === page - 1) {
|
||||||
page -= 1;
|
// page -= 1;
|
||||||
}
|
// }
|
||||||
await tick();
|
// await tick();
|
||||||
}
|
// }
|
||||||
|
|
||||||
async function handlePerPageEvent(event: CustomEvent) {
|
// async function handlePerPageEvent(event: CustomEvent) {
|
||||||
page = 1;
|
// page = 1;
|
||||||
pageSize = event.detail.pageSize;
|
// pageSize = event.detail.pageSize;
|
||||||
await tick();
|
// await tick();
|
||||||
}
|
// }
|
||||||
|
export let data;
|
||||||
|
const wishlists = data.wishlists || [];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -72,7 +74,10 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1>Your Wishlist</h1>
|
<h1>Your Wishlist</h1>
|
||||||
<input type="text" id="search" name="search" placeholder="Search Your Wishlist" bind:value={$searchStore.search} />
|
{#each wishlists as wishlist}
|
||||||
|
<h2>{wishlist.name}</h2>
|
||||||
|
{/each}
|
||||||
|
<!-- <input type="text" id="search" name="search" placeholder="Search Your Wishlist" bind:value={$searchStore.search} />
|
||||||
|
|
||||||
<div class="games">
|
<div class="games">
|
||||||
<div class="games-list">
|
<div class="games-list">
|
||||||
|
|
@ -101,7 +106,7 @@
|
||||||
on:perPageEvent={handlePerPageEvent}
|
on:perPageEvent={handlePerPageEvent}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
h1 {
|
h1 {
|
||||||
|
|
|
||||||
|
|
@ -1,181 +1,181 @@
|
||||||
import { invalid, type RequestEvent } from '@sveltejs/kit';
|
import { invalid, type RequestEvent } from '@sveltejs/kit';
|
||||||
import { BOARD_GAME_ATLAS_CLIENT_ID } from '$env/static/private';
|
import { BOARD_GAME_ATLAS_CLIENT_ID } from '$env/static/private';
|
||||||
import type { GameType, SearchQuery } from "$root/lib/types";
|
import type { GameType, SearchQuery } from '$lib/types';
|
||||||
import { mapAPIGameToBoredGame } from "$root/lib/util/gameMapper";
|
import { mapAPIGameToBoredGame } from '$lib/util/gameMapper';
|
||||||
|
|
||||||
interface Actions {
|
interface Actions {
|
||||||
[key: string]: any // Action
|
[key: string]: any; // Action
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Games: Actions = {
|
export const Games: Actions = {
|
||||||
search: async ({ request, locals }: RequestEvent): Promise<any> => {
|
search: async ({ request, locals }: RequestEvent): Promise<any> => {
|
||||||
console.log("In search action specific")
|
console.log('In search action specific');
|
||||||
// Do things in here
|
// Do things in here
|
||||||
const form = await request.formData();
|
const form = await request.formData();
|
||||||
console.log('action form', form);
|
console.log('action form', form);
|
||||||
const queryParams: SearchQuery = {
|
const queryParams: SearchQuery = {
|
||||||
order_by: 'rank',
|
order_by: 'rank',
|
||||||
ascending: false,
|
ascending: false,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
skip: 0,
|
skip: 0,
|
||||||
client_id: BOARD_GAME_ATLAS_CLIENT_ID,
|
client_id: BOARD_GAME_ATLAS_CLIENT_ID,
|
||||||
fuzzy_match: true,
|
fuzzy_match: true,
|
||||||
name: ''
|
name: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
const name = form.has('name') ? form.get('name') : await request?.text();
|
const name = form.has('name') ? form.get('name') : await request?.text();
|
||||||
console.log('name', name);
|
console.log('name', name);
|
||||||
if (name) {
|
if (name) {
|
||||||
queryParams.name = `${name}`;
|
queryParams.name = `${name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newQueryParams: Record<string, string> = {};
|
const newQueryParams: Record<string, string> = {};
|
||||||
for (const key in queryParams) {
|
for (const key in queryParams) {
|
||||||
console.log('key', key);
|
console.log('key', key);
|
||||||
console.log('queryParams[key]', queryParams[key]);
|
console.log('queryParams[key]', queryParams[key]);
|
||||||
newQueryParams[key] = `${queryParams[key]}`;
|
newQueryParams[key] = `${queryParams[key]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlQueryParams = new URLSearchParams(newQueryParams);
|
const urlQueryParams = new URLSearchParams(newQueryParams);
|
||||||
console.log('urlQueryParams', urlQueryParams);
|
console.log('urlQueryParams', urlQueryParams);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
throw new Error("test error");
|
throw new Error('test error');
|
||||||
// const url = `https://api.boardgameatlas.com/api/search${urlQueryParams ? `?${urlQueryParams}` : ''
|
// const url = `https://api.boardgameatlas.com/api/search${urlQueryParams ? `?${urlQueryParams}` : ''
|
||||||
// }`;
|
// }`;
|
||||||
// const response = await fetch(url, {
|
// const response = await fetch(url, {
|
||||||
// method: 'get',
|
// method: 'get',
|
||||||
// headers: {
|
// headers: {
|
||||||
// 'content-type': 'application/json'
|
// 'content-type': 'application/json'
|
||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
// console.log('board game response', response);
|
// console.log('board game response', response);
|
||||||
// if (response.status !== 200) {
|
// if (response.status !== 200) {
|
||||||
// console.log('Status not 200', response.status)
|
// console.log('Status not 200', response.status)
|
||||||
// invalid(response.status, {});
|
// invalid(response.status, {});
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// if (response.status === 200) {
|
// if (response.status === 200) {
|
||||||
// const gameResponse = await response.json();
|
// const gameResponse = await response.json();
|
||||||
// console.log('gameResponse', gameResponse);
|
// console.log('gameResponse', gameResponse);
|
||||||
// const gameList = gameResponse?.games;
|
// const gameList = gameResponse?.games;
|
||||||
// const totalCount = gameResponse?.count;
|
// const totalCount = gameResponse?.count;
|
||||||
// console.log('totalCount', totalCount);
|
// console.log('totalCount', totalCount);
|
||||||
// const games: GameType[] = [];
|
// const games: GameType[] = [];
|
||||||
// gameList.forEach((game) => {
|
// gameList.forEach((game) => {
|
||||||
// games.push(mapAPIGameToBoredGame(game));
|
// games.push(mapAPIGameToBoredGame(game));
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// console.log('returning from search')
|
// console.log('returning from search')
|
||||||
|
|
||||||
// return {
|
// return {
|
||||||
// games,
|
// games,
|
||||||
// totalCount: games.length
|
// totalCount: games.length
|
||||||
// };
|
// };
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// return {
|
// return {
|
||||||
// games: [],
|
// games: [],
|
||||||
// totalCount: 0
|
// totalCount: 0
|
||||||
// };
|
// };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`Error searching board games ${e}`);
|
console.log(`Error searching board games ${e}`);
|
||||||
return invalid(400, { reason: 'Exception' })
|
return invalid(400, { reason: 'Exception' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// const id = form.get('id');
|
// const id = form.get('id');
|
||||||
// const ids = form.get('ids');
|
// const ids = form.get('ids');
|
||||||
// const minAge = form.get('minAge');
|
// const minAge = form.get('minAge');
|
||||||
// const minPlayers = form.get('minPlayers');
|
// const minPlayers = form.get('minPlayers');
|
||||||
// const maxPlayers = form.get('maxPlayers');
|
// const maxPlayers = form.get('maxPlayers');
|
||||||
// const exactMinAge = form.get('exactMinAge') || false;
|
// const exactMinAge = form.get('exactMinAge') || false;
|
||||||
// const exactMinPlayers = form.get('exactMinPlayers') || false;
|
// const exactMinPlayers = form.get('exactMinPlayers') || false;
|
||||||
// const exactMaxPlayers = form.get('exactMaxPlayers') || false;
|
// const exactMaxPlayers = form.get('exactMaxPlayers') || false;
|
||||||
// const random = form.get('random') === 'on' || false;
|
// const random = form.get('random') === 'on' || false;
|
||||||
|
|
||||||
// if (minAge) {
|
// if (minAge) {
|
||||||
// if (exactMinAge) {
|
// if (exactMinAge) {
|
||||||
// queryParams.min_age = +minAge;
|
// queryParams.min_age = +minAge;
|
||||||
// } else {
|
// } else {
|
||||||
// queryParams.gt_min_age = +minAge === 1 ? 0 : +minAge - 1;
|
// queryParams.gt_min_age = +minAge === 1 ? 0 : +minAge - 1;
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// if (minPlayers) {
|
// if (minPlayers) {
|
||||||
// if (exactMinPlayers) {
|
// if (exactMinPlayers) {
|
||||||
// queryParams.min_players = +minPlayers;
|
// queryParams.min_players = +minPlayers;
|
||||||
// } else {
|
// } else {
|
||||||
// queryParams.gt_min_players = +minPlayers === 1 ? 0 : +minPlayers - 1;
|
// queryParams.gt_min_players = +minPlayers === 1 ? 0 : +minPlayers - 1;
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// if (maxPlayers) {
|
// if (maxPlayers) {
|
||||||
// if (exactMaxPlayers) {
|
// if (exactMaxPlayers) {
|
||||||
// queryParams.max_players = +maxPlayers;
|
// queryParams.max_players = +maxPlayers;
|
||||||
// } else {
|
// } else {
|
||||||
// queryParams.lt_max_players = +maxPlayers + 1;
|
// queryParams.lt_max_players = +maxPlayers + 1;
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// if (id) {
|
// if (id) {
|
||||||
// queryParams.ids = new Array(`${id}`);
|
// queryParams.ids = new Array(`${id}`);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// if (ids) {
|
// if (ids) {
|
||||||
// // TODO: Pass in ids array from localstorage / game store
|
// // TODO: Pass in ids array from localstorage / game store
|
||||||
// queryParams.ids = new Array(ids);
|
// queryParams.ids = new Array(ids);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// queryParams.random = random;
|
// queryParams.random = random;
|
||||||
// console.log('queryParams', queryParams);
|
// console.log('queryParams', queryParams);
|
||||||
|
|
||||||
// const newQueryParams: Record<string, string> = {};
|
// const newQueryParams: Record<string, string> = {};
|
||||||
// for (const key in queryParams) {
|
// for (const key in queryParams) {
|
||||||
// newQueryParams[key] = `${queryParams[key as keyof typeof queryParams]}`;
|
// newQueryParams[key] = `${queryParams[key as keyof typeof queryParams]}`;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// const urlQueryParams = new URLSearchParams(newQueryParams);
|
// const urlQueryParams = new URLSearchParams(newQueryParams);
|
||||||
|
|
||||||
// const url = `https://api.boardgameatlas.com/api/search${urlQueryParams ? `?${urlQueryParams}` : ''
|
// const url = `https://api.boardgameatlas.com/api/search${urlQueryParams ? `?${urlQueryParams}` : ''
|
||||||
// }`;
|
// }`;
|
||||||
// const response = await fetch(url, {
|
// const response = await fetch(url, {
|
||||||
// method: 'get',
|
// method: 'get',
|
||||||
// headers: {
|
// headers: {
|
||||||
// 'content-type': 'application/json'
|
// 'content-type': 'application/json'
|
||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
// console.log('response status', response.status);
|
// console.log('response status', response.status);
|
||||||
// console.log('board game response action', response);
|
// console.log('board game response action', response);
|
||||||
// if (response.status === 404) {
|
// if (response.status === 404) {
|
||||||
// // user hasn't created a todo list.
|
// // user hasn't created a todo list.
|
||||||
// // start with an empty array
|
// // start with an empty array
|
||||||
// return {
|
// return {
|
||||||
// success: true,
|
// success: true,
|
||||||
// games: [],
|
// games: [],
|
||||||
// totalCount: 0
|
// totalCount: 0
|
||||||
// };
|
// };
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// if (response.status === 200) {
|
// if (response.status === 200) {
|
||||||
// const gameResponse = await response.json();
|
// const gameResponse = await response.json();
|
||||||
// console.log('gameResponse', gameResponse);
|
// console.log('gameResponse', gameResponse);
|
||||||
// const gameList = gameResponse?.games;
|
// const gameList = gameResponse?.games;
|
||||||
// const games: GameType[] = [];
|
// const games: GameType[] = [];
|
||||||
// gameList.forEach((game: GameType) => {
|
// gameList.forEach((game: GameType) => {
|
||||||
// games.push(mapAPIGameToBoredGame(game));
|
// games.push(mapAPIGameToBoredGame(game));
|
||||||
// });
|
// });
|
||||||
// console.log('action games', games);
|
// console.log('action games', games);
|
||||||
// return {
|
// return {
|
||||||
// games,
|
// games,
|
||||||
// totalCount: games.length
|
// totalCount: games.length
|
||||||
// };
|
// };
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// return { success: false };
|
// return { success: false };
|
||||||
// }
|
// }
|
||||||
// create: async function create({ request, locals }): Promise<any> {
|
// create: async function create({ request, locals }): Promise<any> {
|
||||||
// const data = await getFormDataObject<any>(request);
|
// const data = await getFormDataObject<any>(request);
|
||||||
// return data;
|
// return data;
|
||||||
// }
|
// }
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
@import 'reset.pcss';
|
@import 'reset.pcss';
|
||||||
@import 'global.pcss';
|
@import 'global.pcss';
|
||||||
@import '$root/styles/theme.pcss';
|
@import 'theme.pcss';
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,25 @@
|
||||||
|
import preprocess from 'svelte-preprocess';
|
||||||
import adapter from '@sveltejs/adapter-vercel';
|
import adapter from '@sveltejs/adapter-vercel';
|
||||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||||
// for more information about preprocessors
|
// for more information about preprocessors
|
||||||
preprocess: [
|
preprocess: [vitePreprocess()],
|
||||||
vitePreprocess({
|
vitePlugin: {
|
||||||
postcss: true,
|
inspector: true,
|
||||||
}),
|
},
|
||||||
],
|
kit: {
|
||||||
kit: {
|
adapter: adapter(),
|
||||||
adapter: adapter(),
|
alias: {
|
||||||
alias: {
|
$db: './src/db',
|
||||||
$root: './src'
|
$assets: './src/assets',
|
||||||
},
|
$lib: './src/lib',
|
||||||
},
|
$styles: './src/styles',
|
||||||
vitePlugin: {
|
$themes: './src/themes'
|
||||||
experimental: {
|
}
|
||||||
inspector: {
|
},
|
||||||
toggleKeyCombo: 'control-alt-shift',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
||||||
18
tailwind.config.cjs
Normal file
18
tailwind.config.cjs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
// 1. Apply the dark mode class setting:
|
||||||
|
darkMode: 'class',
|
||||||
|
content: [
|
||||||
|
'./src/**/*.{html,js,svelte,ts}',
|
||||||
|
// 2. Append the path for the Skeleton NPM package and files:
|
||||||
|
require('path').join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}')
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
require('@tailwindcss/forms'),
|
||||||
|
// 3. Append the Skeleton plugin to the end of this list
|
||||||
|
...require('@skeletonlabs/skeleton/tailwind/skeleton.cjs')()
|
||||||
|
]
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue