Add rudimentary routes

This commit is contained in:
Justin Edmund 2025-09-11 10:44:59 -07:00
parent 8c3198e4b0
commit ff64bc1562
11 changed files with 234 additions and 14 deletions

View file

@ -1,14 +1,26 @@
<script lang="ts"> <script lang="ts">
import favicon from '$lib/assets/favicon.svg' import favicon from '$lib/assets/favicon.svg'
import 'modern-normalize/modern-normalize.css' import 'modern-normalize/modern-normalize.css'
import '$src/app.scss'
export const prerender = false import Navigation from '$lib/components/Navigation.svelte'
let { children } = $props() // Get `data` and `children` from the router via $props()
const { data, children } = $props<{
data: {
isAuthenticated: boolean
account: { username: string; userId: string; role: number } | null
currentUser: unknown | null
}
children: () => any
}>()
</script> </script>
<svelte:head> <svelte:head>
<link rel="icon" href={favicon} /> <link rel="icon" href={favicon} />
</svelte:head> </svelte:head>
{@render children?.()} <main>
<Navigation isAuthenticated={data?.isAuthenticated} username={data?.account?.username} />
{@render children?.()}
</main>

View file

@ -0,0 +1,27 @@
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'
import { profile } from '$lib/api/resources/users'
import { parseParty } from '$lib/api/schemas/party'
import * as partiesApi from '$lib/api/resources/parties'
export const load: PageServerLoad = async ({ fetch, params, url, depends, locals }) => {
depends('app:profile')
const username = params.username
const pageParam = url.searchParams.get('page')
const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1
const tab = url.searchParams.get('tab') === 'favorites' ? 'favorites' : 'teams'
const isOwner = locals.session?.account?.username === username
try {
if (tab === 'favorites' && isOwner) {
const fav = await partiesApi.favorites(fetch as any, { page })
return { user: { username } as any, items: fav.items, page: fav.page, total: fav.total, totalPages: fav.totalPages, perPage: fav.perPage, tab, isOwner }
}
const { user, items, total, totalPages, perPage } = await profile(fetch as any, username, page)
const parties = items.map((p) => parseParty(p))
return { user, items: parties, page, total, totalPages, perPage, tab, isOwner }
} catch (e: any) {
throw error(e?.status || 502, e?.message || 'Failed to load profile')
}
}

View file

@ -0,0 +1,65 @@
<script lang="ts">
import type { PageData } from './$types'
import ExploreGrid from '$lib/components/explore/ExploreGrid.svelte'
const { data } = $props() as { data: PageData }
const page = data.page || 1
const totalPages = data.totalPages || undefined
const tab = data.tab || 'teams'
const isOwner = data.isOwner || false
const avatarFile = data.user?.avatar?.picture || ''
function ensurePng(name: string) {
return /\.png$/i.test(name) ? name : `${name}.png`
}
function to2x(name: string) {
return /\.png$/i.test(name) ? name.replace(/\.png$/i, '@2x.png') : `${name}@2x.png`
}
const avatarSrc = avatarFile ? `/profile/${ensurePng(avatarFile)}` : ''
const avatarSrcSet = avatarFile ? `${avatarSrc} 1x, /profile/${to2x(avatarFile)} 2x` : ''
</script>
<section class="profile">
<header class="header">
{#if data.user?.avatar?.picture}
<img class="avatar" alt={`Avatar of ${data.user.username}`} src={avatarSrc} srcset={avatarSrcSet} width="64" height="64" />
{:else}
<div class="avatar" aria-hidden="true"></div>
{/if}
<div>
<h1>@{data.user.username}</h1>
<nav class="tabs" aria-label="Profile sections">
<a class:active={tab==='teams'} href="?tab=teams" data-sveltekit-preload-data="hover">Teams</a>
{#if isOwner}
<a class:active={tab==='favorites'} href="?tab=favorites" data-sveltekit-preload-data="hover">Favorites</a>
{/if}
</nav>
</div>
</header>
<ExploreGrid items={data.items} />
<nav class="pagination" aria-label="Pagination">
{#if page > 1}
<a rel="prev" href={`?page=${page - 1}`} data-sveltekit-preload-data="hover">Previous</a>
{/if}
{#if totalPages && page < totalPages}
<a rel="next" href={`?page=${page + 1}`} data-sveltekit-preload-data="hover">Next</a>
{/if}
</nav>
</section>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/colors' as *;
.profile { padding: $unit-2x 0; }
.header { display: flex; align-items: center; gap: $unit-2x; margin-bottom: $unit-2x; }
.avatar { width: 64px; height: 64px; border-radius: 50%; background: $grey-80; border: 1px solid $grey-75; object-fit: cover; }
.sub { color: $grey-55; margin: 0; }
.tabs { display: flex; gap: $unit-2x; margin-top: $unit-half; }
.tabs a { text-decoration: none; color: inherit; padding-bottom: 2px; border-bottom: 2px solid transparent; }
.tabs a.active { border-color: #3366ff; color: #3366ff; }
.pagination { display: flex; gap: $unit-2x; padding: $unit-2x 0; }
.pagination a { text-decoration: none; }
</style>

View file

View file

@ -1,15 +1,9 @@
import type { PageServerLoad } from './$types' import type { PageServerLoad } from './$types'
import { redirect } from '@sveltejs/kit' import { redirect } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ parent }) => { export const load: PageServerLoad = async ({ locals }) => {
const { isAuthenticated, account, currentUser } = await parent() const username = locals.session?.account?.username
if (!username) throw redirect(302, '/auth/login')
if (!isAuthenticated) { throw redirect(302, `/${encodeURIComponent(username)}`)
throw redirect(302, '/login?next=/me')
}
return {
account,
user: currentUser
}
} }

View file

@ -0,0 +1,21 @@
<script lang="ts">
export let error: Error & { status?: number, message?: string }
export let status: number
</script>
<section>
<h1>{status === 404 ? 'Team Not Found' : 'Something went wrong'}</h1>
<p>
{status === 404
? 'We could not find a team with that code.'
: (error?.message || 'Please try again later.')}
</p>
</section>
<style>
section { padding: 1.5rem; }
h1 { margin: 0 0 0.5rem 0; font-size: 1.25rem; }
p { margin: 0; color: #555; }
:global(main) { display: block; }
</style>

View file

@ -0,0 +1,34 @@
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'
import { PartyService } from '$lib/services/party.service'
export const load: PageServerLoad = async ({ fetch, params, parent }) => {
const { shortcode } = params
const partyService = new PartyService(fetch)
try {
const party = await partyService.getByShortcode(shortcode)
const parentData = await parent()
const authUserId = (parentData as any)?.user?.id
const canEditServer = partyService.computeEditability(
party,
authUserId,
undefined,
undefined
)
return {
party,
canEditServer: canEditServer.canEdit,
authUserId
}
} catch (err: any) {
console.error('Error loading party:', err)
if (err?.issues) console.error('Validation errors:', err.issues)
if (err.status === 404) throw error(404, 'Team not found')
throw error(err.status || 500, err.message || 'Failed to load team')
}
}

View file

@ -0,0 +1,13 @@
<script lang="ts">
import Party from '$lib/components/party/Party.svelte'
import type { PageData } from './$types'
export let data: PageData
</script>
<Party
initial={data.party}
canEditServer={data.canEditServer}
authUserId={data.authUserId}
/>

View file

@ -0,0 +1,18 @@
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'
import * as parties from '$lib/api/resources/parties'
export const load: PageServerLoad = async ({ fetch, url, depends }) => {
depends('app:parties:list')
const pageParam = url.searchParams.get('page')
const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1
try {
const { items, total, totalPages, perPage } = await parties.list(fetch, { page })
return { items, page, total, totalPages, perPage }
} catch (e: any) {
throw error(e?.status || 502, e?.message || 'Failed to load teams')
}
}

View file

@ -0,0 +1,35 @@
<script lang="ts">
import type { PageData } from './$types'
import ExploreGrid from '$lib/components/explore/ExploreGrid.svelte'
const { data } = $props() as { data: PageData }
const page = data.page || 1
const totalPages = data.totalPages || undefined
</script>
<section class="explore">
<header>
<h1>Explore Teams</h1>
</header>
<ExploreGrid items={data.items} />
<nav class="pagination" aria-label="Pagination">
{#if page > 1}
<a rel="prev" href={`?page=${page - 1}`} data-sveltekit-preload-data="hover">Previous</a>
{/if}
{#if totalPages && page < totalPages}
<a rel="next" href={`?page=${page + 1}`} data-sveltekit-preload-data="hover">Next</a>
{/if}
</nav>
</section>
<style lang="scss">
@use '$src/themes/spacing' as *;
.explore { padding: $unit-2x 0; }
h1 { margin: 0 0 $unit-2x 0; }
.pagination { display: flex; gap: $unit-2x; padding: $unit-2x 0; }
.pagination a { text-decoration: none; }
</style>

View file

@ -0,0 +1 @@
<svelte:options runes={true} />