move favorites to separate route
This commit is contained in:
parent
aee62522e9
commit
0c973785d1
4 changed files with 223 additions and 76 deletions
|
|
@ -8,27 +8,12 @@ export const load: PageServerLoad = async ({ params, url, depends, locals }) =>
|
|||
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 userAdapter.getFavorites({ 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 userAdapter.getProfile(username, page)
|
||||
const parties = items.map((p) => parseParty(p))
|
||||
return { user, items: parties, page, total, totalPages, perPage, tab, isOwner }
|
||||
return { user, items: parties, page, total, totalPages, perPage, isOwner }
|
||||
} catch (e: any) {
|
||||
throw error(e?.status || 502, e?.message || 'Failed to load profile')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,76 +3,42 @@
|
|||
import { createInfiniteQuery } from '@tanstack/svelte-query'
|
||||
import ExploreGrid from '$lib/components/explore/ExploreGrid.svelte'
|
||||
import ProfileHeader from '$lib/components/profile/ProfileHeader.svelte'
|
||||
import { userQueries, type FavoritesPageResult } from '$lib/api/queries/user.queries'
|
||||
import { userQueries } from '$lib/api/queries/user.queries'
|
||||
import { crewStore } from '$lib/stores/crew.store.svelte'
|
||||
import { IsInViewport } from 'runed'
|
||||
import Icon from '$lib/components/Icon.svelte'
|
||||
import Button from '$lib/components/ui/Button.svelte'
|
||||
|
||||
const { data } = $props() as { data: PageData }
|
||||
const tab = $derived(data.tab || 'teams')
|
||||
const { data }: { data: PageData } = $props()
|
||||
const isOwner = $derived(data.isOwner || false)
|
||||
const activeTab = $derived<'teams' | 'favorites'>(tab === 'favorites' ? 'favorites' : 'teams')
|
||||
|
||||
// Crew info for invite functionality
|
||||
const viewerCrewRole = $derived(crewStore.membership?.role ?? null)
|
||||
const viewerCrewId = $derived(crewStore.crew?.id ?? null)
|
||||
|
||||
// Note: Type assertion needed because favorites and parties queries have different
|
||||
// result structures (items vs results) but we handle both in the items $derived
|
||||
const getQueryOptions = () => {
|
||||
const isFavorites = tab === 'favorites' && isOwner
|
||||
|
||||
if (isFavorites) {
|
||||
return {
|
||||
...userQueries.favorites(),
|
||||
initialData: data.items
|
||||
? {
|
||||
pages: [
|
||||
{
|
||||
items: data.items,
|
||||
page: data.page || 1,
|
||||
totalPages: data.totalPages,
|
||||
total: data.total,
|
||||
perPage: data.perPage || 20
|
||||
}
|
||||
],
|
||||
pageParams: [1]
|
||||
const partiesQuery = createInfiniteQuery(() => ({
|
||||
...userQueries.parties(data.user?.username ?? ''),
|
||||
enabled: !!data.user?.username,
|
||||
initialData: data.items
|
||||
? {
|
||||
pages: [
|
||||
{
|
||||
results: data.items,
|
||||
page: data.page || 1,
|
||||
totalPages: data.totalPages ?? 1,
|
||||
total: data.total ?? data.items.length,
|
||||
perPage: data.perPage || 20
|
||||
}
|
||||
: undefined,
|
||||
initialDataUpdatedAt: 0
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...userQueries.parties(data.user?.username ?? ''),
|
||||
enabled: !!data.user?.username,
|
||||
initialData: data.items
|
||||
? {
|
||||
pages: [
|
||||
{
|
||||
results: data.items,
|
||||
page: data.page || 1,
|
||||
totalPages: data.totalPages,
|
||||
total: data.total,
|
||||
perPage: data.perPage || 20
|
||||
}
|
||||
],
|
||||
pageParams: [1]
|
||||
}
|
||||
: undefined,
|
||||
initialDataUpdatedAt: 0
|
||||
}
|
||||
}
|
||||
const partiesQuery = createInfiniteQuery(getQueryOptions as () => ReturnType<typeof userQueries.favorites>)
|
||||
],
|
||||
pageParams: [1]
|
||||
}
|
||||
: undefined,
|
||||
initialDataUpdatedAt: 0
|
||||
}))
|
||||
|
||||
const items = $derived(() => {
|
||||
if (!partiesQuery.data?.pages) return data.items || []
|
||||
const isFavorites = tab === 'favorites' && isOwner
|
||||
if (isFavorites) {
|
||||
return partiesQuery.data.pages.flatMap((page) => (page as any).items ?? [])
|
||||
}
|
||||
return partiesQuery.data.pages.flatMap((page) => (page as any).results ?? [])
|
||||
return partiesQuery.data.pages.flatMap((page) => page.results ?? [])
|
||||
})
|
||||
|
||||
const isEmpty = $derived(!partiesQuery.isLoading && items().length === 0)
|
||||
|
|
@ -96,12 +62,20 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.user.username}'s Teams | Hensei</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="profile">
|
||||
<ProfileHeader
|
||||
username={data.user.username}
|
||||
userId={data.user?.id}
|
||||
avatarPicture={data.user?.avatar?.picture}
|
||||
{activeTab}
|
||||
element={data.user?.avatar?.element}
|
||||
granblueId={data.user?.granblueId}
|
||||
showCrewGamertag={data.user?.showCrewGamertag}
|
||||
crewGamertag={data.user?.crewGamertag}
|
||||
activeTab="teams"
|
||||
{isOwner}
|
||||
{viewerCrewRole}
|
||||
{viewerCrewId}
|
||||
|
|
@ -110,17 +84,17 @@
|
|||
{#if partiesQuery.isLoading}
|
||||
<div class="loading">
|
||||
<Icon name="loader-2" size={32} />
|
||||
<p>Loading {tab}...</p>
|
||||
<p>Loading teams...</p>
|
||||
</div>
|
||||
{:else if partiesQuery.isError}
|
||||
<div class="error">
|
||||
<Icon name="alert-circle" size={32} />
|
||||
<p>Failed to load {tab}: {partiesQuery.error?.message || 'Unknown error'}</p>
|
||||
<p>Failed to load teams: {partiesQuery.error?.message || 'Unknown error'}</p>
|
||||
<Button size="small" onclick={() => partiesQuery.refetch()}>Retry</Button>
|
||||
</div>
|
||||
{:else if isEmpty}
|
||||
<div class="empty">
|
||||
<p>{tab === 'favorites' ? 'No favorite teams yet' : 'No teams found'}</p>
|
||||
<p>No teams found</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="profile-grid">
|
||||
|
|
@ -139,7 +113,7 @@
|
|||
|
||||
{#if !partiesQuery.hasNextPage && items().length > 0}
|
||||
<div class="end">
|
||||
<p>You've seen all {tab === 'favorites' ? 'favorites' : 'teams'}!</p>
|
||||
<p>You've seen all teams!</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
21
src/routes/(app)/[username]/favorites/+page.server.ts
Normal file
21
src/routes/(app)/[username]/favorites/+page.server.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { PageServerLoad } from './$types'
|
||||
import { error, redirect } from '@sveltejs/kit'
|
||||
import { userAdapter } from '$lib/api/adapters/user.adapter'
|
||||
|
||||
export const load: PageServerLoad = async ({ params, locals }) => {
|
||||
const username = params.username
|
||||
const isOwner = locals.session?.account?.username === username
|
||||
|
||||
// Only the owner can view their favorites
|
||||
if (!isOwner) {
|
||||
throw redirect(302, `/${username}`)
|
||||
}
|
||||
|
||||
try {
|
||||
// Just get the user info - favorites are fetched client-side
|
||||
const { user } = await userAdapter.getProfile(username, 1)
|
||||
return { user, isOwner }
|
||||
} catch (e: any) {
|
||||
throw error(e?.status || 502, e?.message || 'Failed to load profile')
|
||||
}
|
||||
}
|
||||
167
src/routes/(app)/[username]/favorites/+page.svelte
Normal file
167
src/routes/(app)/[username]/favorites/+page.svelte
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types'
|
||||
import { createInfiniteQuery } from '@tanstack/svelte-query'
|
||||
import ExploreGrid from '$lib/components/explore/ExploreGrid.svelte'
|
||||
import ProfileHeader from '$lib/components/profile/ProfileHeader.svelte'
|
||||
import { userQueries } from '$lib/api/queries/user.queries'
|
||||
import { IsInViewport } from 'runed'
|
||||
import Icon from '$lib/components/Icon.svelte'
|
||||
import Button from '$lib/components/ui/Button.svelte'
|
||||
|
||||
const { data }: { data: PageData } = $props()
|
||||
|
||||
const favoritesQuery = createInfiniteQuery(() => userQueries.favorites())
|
||||
|
||||
const items = $derived(() => {
|
||||
if (!favoritesQuery.data?.pages) return []
|
||||
return favoritesQuery.data.pages.flatMap((page) => page.items ?? [])
|
||||
})
|
||||
|
||||
const isEmpty = $derived(!favoritesQuery.isLoading && items().length === 0)
|
||||
const showSentinel = $derived(favoritesQuery.hasNextPage && !favoritesQuery.isFetchingNextPage)
|
||||
|
||||
let sentinelEl = $state<HTMLElement>()
|
||||
|
||||
const inViewport = new IsInViewport(() => sentinelEl, {
|
||||
rootMargin: '300px'
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (
|
||||
inViewport.current &&
|
||||
favoritesQuery.hasNextPage &&
|
||||
!favoritesQuery.isFetchingNextPage &&
|
||||
!favoritesQuery.isLoading
|
||||
) {
|
||||
favoritesQuery.fetchNextPage()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.user.username}'s Favorites | Hensei</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="profile">
|
||||
<ProfileHeader
|
||||
username={data.user.username}
|
||||
userId={data.user?.id}
|
||||
avatarPicture={data.user?.avatar?.picture}
|
||||
element={data.user?.avatar?.element}
|
||||
granblueId={data.user?.granblueId}
|
||||
showCrewGamertag={data.user?.showCrewGamertag}
|
||||
crewGamertag={data.user?.crewGamertag}
|
||||
activeTab="favorites"
|
||||
isOwner={true}
|
||||
/>
|
||||
|
||||
{#if favoritesQuery.isLoading}
|
||||
<div class="loading">
|
||||
<Icon name="loader-2" size={32} />
|
||||
<p>Loading favorites...</p>
|
||||
</div>
|
||||
{:else if favoritesQuery.isError}
|
||||
<div class="error">
|
||||
<Icon name="alert-circle" size={32} />
|
||||
<p>Failed to load favorites: {favoritesQuery.error?.message || 'Unknown error'}</p>
|
||||
<Button size="small" onclick={() => favoritesQuery.refetch()}>Retry</Button>
|
||||
</div>
|
||||
{:else if isEmpty}
|
||||
<div class="empty">
|
||||
<p>No favorite teams yet</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="profile-grid">
|
||||
<ExploreGrid items={items()} />
|
||||
|
||||
{#if showSentinel}
|
||||
<div class="load-more-sentinel" bind:this={sentinelEl}></div>
|
||||
{/if}
|
||||
|
||||
{#if favoritesQuery.isFetchingNextPage}
|
||||
<div class="loading-more">
|
||||
<Icon name="loader-2" size={20} />
|
||||
<span>Loading more...</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !favoritesQuery.hasNextPage && items().length > 0}
|
||||
<div class="end">
|
||||
<p>You've seen all favorites!</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/spacing' as *;
|
||||
@use '$src/themes/colors' as *;
|
||||
|
||||
.profile {
|
||||
padding: $unit-2x 0;
|
||||
}
|
||||
|
||||
.empty,
|
||||
.end,
|
||||
.error {
|
||||
text-align: center;
|
||||
padding: $unit-4x;
|
||||
color: var(--text-secondary);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--text-error, #dc2626);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $unit;
|
||||
padding: $unit-4x;
|
||||
color: var(--text-secondary);
|
||||
|
||||
:global(svg) {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.load-more-sentinel {
|
||||
height: 1px;
|
||||
margin-top: $unit;
|
||||
}
|
||||
|
||||
.loading-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $unit;
|
||||
padding: $unit-2x;
|
||||
color: var(--text-secondary);
|
||||
|
||||
:global(svg) {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in a new issue