Merge pull request #12 from BradNut/auth

Introduce Auth
This commit is contained in:
Bradley Shellnut 2023-06-17 22:08:44 +00:00 committed by GitHub
commit dbbb292bb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 3500 additions and 987 deletions

40
.vscode/launch.json vendored
View file

@ -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"]
}
]
} }

View file

@ -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"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -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
View 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
View 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();
});

View file

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

54
src/app.d.ts vendored
View file

@ -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
View 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
View 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
View 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;
}

View file

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

52
src/hooks.server.ts Normal file
View 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);

View file

@ -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;
} }
} }

View file

@ -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() {

View file

@ -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;

View file

@ -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;

View file

@ -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() {

View file

@ -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() {

View file

@ -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;

View file

@ -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;

View file

@ -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';

View file

@ -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';

View file

@ -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) {

View file

@ -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>

View file

@ -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;
} }
} }

View 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>

View 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>

View 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
View file

@ -0,0 +1,5 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default prisma;

View 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
View 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
View 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
View 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
View 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;

View 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}`;
}

View 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;
}

View 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;
}

View 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;
}

View file

@ -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',
// }); // });

View file

@ -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 {

View file

@ -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
}; };
} };

View file

@ -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;
} }
} }

View file

@ -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 };
}; };

View file

@ -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">

View 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, '/');
};

View file

@ -0,0 +1 @@
<slot />

View file

View 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 };
}
};

View 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>

View 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');
}
};

View 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 };
}
};

View 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>

View file

@ -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
}; };
}; };

View file

@ -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%;

View file

@ -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;
} }
} }

View file

@ -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)
}; };
} }
}; };

View file

@ -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>

View 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');
}
}
};

View file

@ -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 {

View file

@ -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;
// } // }
} };

View file

@ -1,3 +1,3 @@
@import 'reset.pcss'; @import 'reset.pcss';
@import 'global.pcss'; @import 'global.pcss';
@import '$root/styles/theme.pcss'; @import 'theme.pcss';

View file

@ -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
View 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')()
]
};