mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Fixing get all wishlists, style wishlist cards, and fix TOTP.
This commit is contained in:
parent
fc4396ccdd
commit
4032838f49
12 changed files with 68 additions and 68 deletions
|
|
@ -80,7 +80,7 @@
|
||||||
"@hono/zod-validator": "^0.2.2",
|
"@hono/zod-validator": "^0.2.2",
|
||||||
"@iconify-icons/line-md": "^1.2.30",
|
"@iconify-icons/line-md": "^1.2.30",
|
||||||
"@iconify-icons/mdi": "^1.2.48",
|
"@iconify-icons/mdi": "^1.2.48",
|
||||||
"@internationalized/date": "^3.5.5",
|
"@internationalized/date": "^3.5.6",
|
||||||
"@lucia-auth/adapter-drizzle": "^1.1.0",
|
"@lucia-auth/adapter-drizzle": "^1.1.0",
|
||||||
"@lukeed/uuid": "^2.0.1",
|
"@lukeed/uuid": "^2.0.1",
|
||||||
"@neondatabase/serverless": "^0.9.5",
|
"@neondatabase/serverless": "^0.9.5",
|
||||||
|
|
@ -97,7 +97,7 @@
|
||||||
"@types/feather-icons": "^4.29.4",
|
"@types/feather-icons": "^4.29.4",
|
||||||
"bits-ui": "^0.21.13",
|
"bits-ui": "^0.21.13",
|
||||||
"boardgamegeekclient": "^1.9.1",
|
"boardgamegeekclient": "^1.9.1",
|
||||||
"bullmq": "^5.14.0",
|
"bullmq": "^5.15.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cookie": "^0.6.0",
|
"cookie": "^0.6.0",
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ importers:
|
||||||
specifier: ^1.2.48
|
specifier: ^1.2.48
|
||||||
version: 1.2.48
|
version: 1.2.48
|
||||||
'@internationalized/date':
|
'@internationalized/date':
|
||||||
specifier: ^3.5.5
|
specifier: ^3.5.6
|
||||||
version: 3.5.5
|
version: 3.5.6
|
||||||
'@lucia-auth/adapter-drizzle':
|
'@lucia-auth/adapter-drizzle':
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0(drizzle-orm@0.32.2(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.0(pg-native@3.2.0))(postgres@3.4.4))(lucia@3.2.0)
|
version: 1.1.0(drizzle-orm@0.32.2(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.0(pg-native@3.2.0))(postgres@3.4.4))(lucia@3.2.0)
|
||||||
|
|
@ -78,8 +78,8 @@ importers:
|
||||||
specifier: ^1.9.1
|
specifier: ^1.9.1
|
||||||
version: 1.9.1
|
version: 1.9.1
|
||||||
bullmq:
|
bullmq:
|
||||||
specifier: ^5.14.0
|
specifier: ^5.15.0
|
||||||
version: 5.14.0
|
version: 5.15.0
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.0
|
specifier: ^0.7.0
|
||||||
version: 0.7.0
|
version: 0.7.0
|
||||||
|
|
@ -1415,8 +1415,8 @@ packages:
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@internationalized/date@3.5.5':
|
'@internationalized/date@3.5.6':
|
||||||
resolution: {integrity: sha512-H+CfYvOZ0LTJeeLOqm19E3uj/4YjrmOFtBufDHPfvtI80hFAMqtrp7oCACpe4Cil5l8S0Qu/9dYfZc/5lY8WQQ==}
|
resolution: {integrity: sha512-jLxQjefH9VI5P9UQuqB6qNKnvFt1Ky1TPIzHGsIlCi7sZZoMR8SdYbBGRvM0y+Jtb+ez4ieBzmiAUcpmPYpyOw==}
|
||||||
|
|
||||||
'@ioredis/commands@1.2.0':
|
'@ioredis/commands@1.2.0':
|
||||||
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
|
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
|
||||||
|
|
@ -2366,8 +2366,8 @@ packages:
|
||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
bullmq@5.14.0:
|
bullmq@5.15.0:
|
||||||
resolution: {integrity: sha512-qxZHtRuGEp0oHM1aNokuZ4gA0xr6vcZQPe1OLuQoDTuhaEXB4faxApUoo85v/PHnzrniAAqNT9kqD+UBbmECDQ==}
|
resolution: {integrity: sha512-h53shVjx8s6wxYGtUfzAfENpSP7N5T0D4PMTvbZncozLjb8yUKhopfpa7PmcpQfq7SSO9dm/OZ9XQuGOCSGNug==}
|
||||||
|
|
||||||
bytes@3.1.2:
|
bytes@3.1.2:
|
||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
|
|
@ -5668,7 +5668,7 @@ snapshots:
|
||||||
'@img/sharp-win32-x64@0.33.5':
|
'@img/sharp-win32-x64@0.33.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@internationalized/date@3.5.5':
|
'@internationalized/date@3.5.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@swc/helpers': 0.5.13
|
'@swc/helpers': 0.5.13
|
||||||
|
|
||||||
|
|
@ -5746,7 +5746,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/core': 1.6.7
|
'@floating-ui/core': 1.6.7
|
||||||
'@floating-ui/dom': 1.6.10
|
'@floating-ui/dom': 1.6.10
|
||||||
'@internationalized/date': 3.5.5
|
'@internationalized/date': 3.5.6
|
||||||
dequal: 2.0.3
|
dequal: 2.0.3
|
||||||
focus-trap: 7.5.4
|
focus-trap: 7.5.4
|
||||||
nanoid: 5.0.7
|
nanoid: 5.0.7
|
||||||
|
|
@ -5756,7 +5756,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/core': 1.6.7
|
'@floating-ui/core': 1.6.7
|
||||||
'@floating-ui/dom': 1.6.10
|
'@floating-ui/dom': 1.6.10
|
||||||
'@internationalized/date': 3.5.5
|
'@internationalized/date': 3.5.6
|
||||||
dequal: 2.0.3
|
dequal: 2.0.3
|
||||||
focus-trap: 7.5.4
|
focus-trap: 7.5.4
|
||||||
nanoid: 5.0.7
|
nanoid: 5.0.7
|
||||||
|
|
@ -6577,7 +6577,7 @@ snapshots:
|
||||||
|
|
||||||
bits-ui@0.21.15(svelte@5.0.0-next.175):
|
bits-ui@0.21.15(svelte@5.0.0-next.175):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@internationalized/date': 3.5.5
|
'@internationalized/date': 3.5.6
|
||||||
'@melt-ui/svelte': 0.76.2(svelte@5.0.0-next.175)
|
'@melt-ui/svelte': 0.76.2(svelte@5.0.0-next.175)
|
||||||
nanoid: 5.0.7
|
nanoid: 5.0.7
|
||||||
svelte: 5.0.0-next.175
|
svelte: 5.0.0-next.175
|
||||||
|
|
@ -6631,7 +6631,7 @@ snapshots:
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
bullmq@5.14.0:
|
bullmq@5.15.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
cron-parser: 4.9.0
|
cron-parser: 4.9.0
|
||||||
ioredis: 5.4.1
|
ioredis: 5.4.1
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,11 @@ export class CollectionsRepository {
|
||||||
async findAllByUserId(userId: string, db = this.drizzle.db) {
|
async findAllByUserId(userId: string, db = this.drizzle.db) {
|
||||||
return db.query.collections.findMany({
|
return db.query.collections.findMany({
|
||||||
where: eq(collections.user_id, userId),
|
where: eq(collections.user_id, userId),
|
||||||
|
columns: {
|
||||||
|
cuid: true,
|
||||||
|
name: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export class RecoveryCodesRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAllByUserId(userId: string, db = this.drizzle.db) {
|
async findAllByUserId(userId: string, db = this.drizzle.db) {
|
||||||
return db.query.recoveryCodesTable.findFirst({
|
return db.query.recoveryCodesTable.findMany({
|
||||||
where: eq(recoveryCodesTable.userId, userId),
|
where: eq(recoveryCodesTable.userId, userId),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export class WishlistsRepository {
|
||||||
columns: {
|
columns: {
|
||||||
cuid: true,
|
cuid: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
createdAt: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { CredentialsRepository } from '$lib/server/api/repositories/credentials.repository'
|
import { CredentialsRepository } from '$lib/server/api/repositories/credentials.repository'
|
||||||
import { HMAC } from 'oslo/crypto'
|
|
||||||
import { decodeHex, encodeHexLowerCase } from '@oslojs/encoding'
|
import { decodeHex, encodeHexLowerCase } from '@oslojs/encoding'
|
||||||
import { verifyTOTP } from '@oslojs/otp'
|
import { verifyTOTP } from '@oslojs/otp'
|
||||||
import { inject, injectable } from 'tsyringe'
|
import { inject, injectable } from 'tsyringe'
|
||||||
|
|
@ -22,12 +21,11 @@ export class TotpService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(userId: string) {
|
async create(userId: string) {
|
||||||
const twoFactorSecret = await new HMAC('SHA-1').generateKey()
|
const secret = new Uint8Array(20)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.credentialsRepository.create({
|
return await this.credentialsRepository.create({
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
secret_data: encodeHexLowerCase(twoFactorSecret),
|
secret_data: encodeHexLowerCase(crypto.getRandomValues(secret)),
|
||||||
type: 'totp',
|
type: 'totp',
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Card from '$components/ui/card'
|
import * as Card from '$components/ui/card'
|
||||||
|
|
||||||
const { data } = $props()
|
const { data } = $props()
|
||||||
let collections = data?.collections || []
|
let collections = data?.collections || []
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -10,13 +11,12 @@ let collections = data?.collections || []
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Your Collections</h1>
|
<h1>Your Collections</h1>
|
||||||
|
|
||||||
<div class="collection-list">
|
<div class="collection-list">
|
||||||
{#if collections.length === 0}
|
{#if collections.length === 0}
|
||||||
<h2>You have no collections</h2>
|
<h2>You have no collections</h2>
|
||||||
{:else}
|
{:else}
|
||||||
{#each collections as collection}
|
{#each collections as collection}
|
||||||
<Card.Root>
|
<Card.Root class="shadow-sm hover:shadow-md transition-shadow duration-300 ease-in-out">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>{collection.name}</Card.Title>
|
<Card.Title>{collection.name}</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
|
@ -25,10 +25,6 @@ let collections = data?.collections || []
|
||||||
<p>Created at: {new Date(collection.createdAt).toLocaleString()}</p>
|
<p>Created at: {new Date(collection.createdAt).toLocaleString()}</p>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
<!-- <div class="collection grid gap-0.5">
|
|
||||||
<h2><a href="/collections/{collection.cuid}">{collection.name}</a></h2>
|
|
||||||
<h3>Created at: {new Date(collection.createdAt).toLocaleString()}</h3>
|
|
||||||
</div> -->
|
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,12 @@ let { children } = $props()
|
||||||
.security-nav {
|
.security-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
@media (width <= 1000px) {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
|
@media (width > 1000px) {
|
||||||
width: 16rem;
|
width: 16rem;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
@ -43,6 +48,7 @@ let { children } = $props()
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-right: 1px solid #ddd;
|
border-right: 1px solid #ddd;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { notSignedInMessage } from '$lib/flashMessages'
|
import { notSignedInMessage } from '$lib/flashMessages'
|
||||||
import env from '$lib/server/api/common/env'
|
import env from '$lib/server/api/common/env'
|
||||||
|
import { decodeHex, encodeBase32 } from '@oslojs/encoding'
|
||||||
|
import { createTOTPKeyURI } from '@oslojs/otp'
|
||||||
import { type Actions, fail } from '@sveltejs/kit'
|
import { type Actions, fail } from '@sveltejs/kit'
|
||||||
import kebabCase from 'just-kebab-case'
|
import kebabCase from 'just-kebab-case'
|
||||||
import { encodeBase32, decodeHex } from '@oslojs/encoding'
|
|
||||||
import { createTOTPKeyURI } from '@oslojs/otp'
|
|
||||||
import QRCode from 'qrcode'
|
import QRCode from 'qrcode'
|
||||||
import { redirect } from 'sveltekit-flash-message/server'
|
import { redirect } from 'sveltekit-flash-message/server'
|
||||||
import { zod } from 'sveltekit-superforms/adapters'
|
import { zod } from 'sveltekit-superforms/adapters'
|
||||||
|
|
@ -63,7 +63,7 @@ export const load: PageServerLoad = async (event) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const decodedHexSecret = decodeHex(createdTotpCredentials.secret_data)
|
const decodedHexSecret = decodeHex(createdTotpCredentials.secret_data)
|
||||||
const secret = encodeBase32(new TextEncoder().encode(decodedHexSecret))
|
const secret = encodeBase32(decodedHexSecret)
|
||||||
const intervalInSeconds = 30
|
const intervalInSeconds = 30
|
||||||
const digits = 6
|
const digits = 6
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { notSignedInMessage } from '$lib/flashMessages.js'
|
import { notSignedInMessage } from '$lib/flashMessages.js'
|
||||||
import { gamesTable, wishlist_items, wishlistsTable } from '$lib/server/api/databases/tables'
|
import { gamesTable, wishlist_items, wishlistsTable } from '$lib/server/api/databases/tables'
|
||||||
import { db } from '$lib/server/api/packages/drizzle'
|
import { db } from '$lib/server/api/packages/drizzle'
|
||||||
import { userNotAuthenticated } from '$lib/server/auth-utils'
|
|
||||||
import { modifyListGameSchema } from '$lib/validations/zod-schemas'
|
import { modifyListGameSchema } from '$lib/validations/zod-schemas'
|
||||||
import { type Actions, error, fail } from '@sveltejs/kit'
|
import { type Actions, error, fail } from '@sveltejs/kit'
|
||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
|
|
@ -17,15 +16,8 @@ export async function load(event) {
|
||||||
throw redirect(302, '/login', notSignedInMessage, event)
|
throw redirect(302, '/login', notSignedInMessage, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userWishlists = await db.query.wishlists.findMany({
|
const { data } = await locals.api.wishlists.$get().then(locals.parseApiResponse)
|
||||||
columns: {
|
const userWishlists = data?.wishlists
|
||||||
cuid: true,
|
|
||||||
name: true,
|
|
||||||
createdAt: true,
|
|
||||||
},
|
|
||||||
where: eq(wishlistsTable.user_id, authedUser.id),
|
|
||||||
})
|
|
||||||
console.log('wishlists', userWishlists)
|
|
||||||
|
|
||||||
if (userWishlists?.length === 0) {
|
if (userWishlists?.length === 0) {
|
||||||
console.log('Wishlists not found')
|
console.log('Wishlists not found')
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import * as Card from '$components/ui/card'
|
||||||
|
|
||||||
const { data } = $props()
|
const { data } = $props()
|
||||||
const { wishlists = [] } = data
|
const { wishlists = [] } = data
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -8,23 +10,27 @@ const { wishlists = [] } = data
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Your wishlistsTable</h1>
|
<h1>Your wishlists</h1>
|
||||||
|
|
||||||
<div class="wishlists">
|
|
||||||
<div class="wishlist-list">
|
<div class="wishlist-list">
|
||||||
{#if wishlists.length === 0}
|
{#if wishlists.length === 0}
|
||||||
<h2>You have no wishlistsTable</h2>
|
<h2>You have no wishlists</h2>
|
||||||
{:else}
|
{:else}
|
||||||
{#each wishlists as wishlist}
|
{#each wishlists as wishlist}
|
||||||
<div class="collection grid gap-0.5">
|
<Card.Root class="shadow-sm hover:shadow-md transition-shadow duration-300 ease-in-out">
|
||||||
<h2><a href="/wishlists/{wishlist.cuid}">{wishlist.name}</a></h2>
|
<a href="/wishlists/{wishlist.cuid}">
|
||||||
<h3>Created at: {new Date(wishlist.created_at).toLocaleString()}</h3>
|
<Card.Header>
|
||||||
</div>
|
<Card.Title>{wishlist.name}</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<h3>Created at: {new Date(wishlist.createdAt).toLocaleString()}</h3>
|
||||||
|
</Card.Content>
|
||||||
|
</a>
|
||||||
|
</Card.Root>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
h1 {
|
h1 {
|
||||||
|
|
@ -32,10 +38,6 @@ const { wishlists = [] } = data
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wishlists {
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wishlist-list {
|
.wishlist-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(200px, 1fr));
|
grid-template-columns: repeat(3, minmax(200px, 1fr));
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const welcomeName = $derived.by(() => {
|
||||||
welcomeName += data?.user?.firstName
|
welcomeName += data?.user?.firstName
|
||||||
}
|
}
|
||||||
if (data?.user?.lastName) {
|
if (data?.user?.lastName) {
|
||||||
welcomeName += ' ' + data?.user?.lastName
|
welcomeName = welcomeName.length === 0 ? data?.user?.lastName : welcomeName
|
||||||
}
|
}
|
||||||
|
|
||||||
if (welcomeName.length === 0) {
|
if (welcomeName.length === 0) {
|
||||||
|
|
@ -23,7 +23,7 @@ const welcomeName = $derived.by(() => {
|
||||||
{#if user}
|
{#if user}
|
||||||
<h1>Welcome, {welcomeName}!</h1>
|
<h1>Welcome, {welcomeName}!</h1>
|
||||||
<div>
|
<div>
|
||||||
<h2>You wishlistsTable:</h2>
|
<h2>You wishlists:</h2>
|
||||||
{#each wishlists as wishlist}
|
{#each wishlists as wishlist}
|
||||||
<a href="/wishlists/{wishlist.cuid}">{wishlist.name}</a>
|
<a href="/wishlists/{wishlist.cuid}">{wishlist.name}</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue