add ProfileHeader component, add collection link to profile nav

This commit is contained in:
Justin Edmund 2025-12-02 15:07:01 -08:00
parent 9ed505623e
commit 35b0560749
3 changed files with 131 additions and 144 deletions

View file

@ -0,0 +1,112 @@
<script lang="ts">
import { getAvatarSrc, getAvatarSrcSet } from '$lib/utils/avatar'
interface Props {
username: string
avatarPicture?: string
title?: string
activeTab: 'teams' | 'favorites' | 'collection'
isOwner?: boolean
}
let { username, avatarPicture = '', title, activeTab, isOwner = false }: Props = $props()
const avatarSrc = $derived(getAvatarSrc(avatarPicture))
const avatarSrcSet = $derived(getAvatarSrcSet(avatarPicture))
const displayTitle = $derived(title || username)
</script>
<header class="header">
{#if avatarPicture}
<img
class="avatar"
alt={`Avatar of ${username}`}
src={avatarSrc}
srcset={avatarSrcSet}
width="64"
height="64"
/>
{:else}
<div class="avatar" aria-hidden="true"></div>
{/if}
<div class="header-content">
<h1>{displayTitle}</h1>
<nav class="tabs" aria-label="Profile sections">
<a
class:active={activeTab === 'teams' || activeTab === 'favorites'}
href="/{username}"
data-sveltekit-preload-data="hover"
>
Teams
</a>
{#if isOwner}
<a
class:active={activeTab === 'favorites'}
href="/{username}?tab=favorites"
data-sveltekit-preload-data="hover"
>
Favorites
</a>
{/if}
<a
class:active={activeTab === 'collection'}
href="/{username}/collection/characters"
data-sveltekit-preload-data="hover"
>
Collection
</a>
</nav>
</div>
</header>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/colors' as *;
.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;
flex-shrink: 0;
}
.header-content {
flex: 1;
}
h1 {
margin: 0 0 $unit-half;
font-size: 24px;
}
.tabs {
display: flex;
gap: $unit-2x;
}
.tabs a {
text-decoration: none;
color: var(--text-secondary);
padding-bottom: 2px;
border-bottom: 2px solid transparent;
&:hover {
color: var(--text-primary);
}
&.active {
border-color: var(--accent-color, #3366ff);
color: var(--accent-color, #3366ff);
}
}
</style>

View file

@ -2,8 +2,8 @@
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 { getAvatarSrc, getAvatarSrcSet } from '$lib/utils/avatar'
import { IsInViewport } from 'runed'
import Icon from '$lib/components/Icon.svelte'
import Button from '$lib/components/ui/Button.svelte'
@ -11,10 +11,7 @@
const { data } = $props() as { data: PageData }
const tab = $derived(data.tab || 'teams')
const isOwner = $derived(data.isOwner || false)
const avatarFile = $derived(data.user?.avatar?.picture || '')
const avatarSrc = $derived(getAvatarSrc(avatarFile))
const avatarSrcSet = $derived(getAvatarSrcSet(avatarFile))
const activeTab = $derived<'teams' | 'favorites'>(tab === 'favorites' ? 'favorites' : 'teams')
// Note: Type assertion needed because favorites and parties queries have different
// result structures (items vs results) but we handle both in the items $derived
@ -94,35 +91,12 @@
</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>
<ProfileHeader
username={data.user.username}
avatarPicture={data.user?.avatar?.picture}
{activeTab}
{isOwner}
/>
{#if partiesQuery.isLoading}
<div class="loading">
@ -170,39 +144,6 @@
.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;
}
.empty,
.end,

View file

@ -2,18 +2,14 @@
import type { LayoutData } from './$types'
import { page } from '$app/stores'
import { goto } from '$app/navigation'
import { getAvatarSrc, getAvatarSrcSet } from '$lib/utils/avatar'
import ProfileHeader from '$lib/components/profile/ProfileHeader.svelte'
import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
let { data, children }: { data: LayoutData; children: any } = $props()
const avatarFile = $derived(data.user?.avatar?.picture || '')
const avatarSrc = $derived(getAvatarSrc(avatarFile))
const avatarSrcSet = $derived(getAvatarSrcSet(avatarFile))
// Determine active tab from URL path
const activeTab = $derived.by(() => {
// Determine active entity type from URL path
const activeEntityType = $derived.by(() => {
const path = $page.url.pathname
if (path.includes('/weapons')) return 'weapons'
if (path.includes('/summons')) return 'summons'
@ -32,31 +28,17 @@
</svelte:head>
<section class="collection">
<header class="header">
{#if data.user?.avatar?.picture}
<img
class="avatar"
alt={`Avatar of ${username}`}
src={avatarSrc}
srcset={avatarSrcSet}
width="64"
height="64"
/>
{:else}
<div class="avatar" aria-hidden="true"></div>
{/if}
<div class="header-content">
<h1>{username}'s Collection</h1>
<nav class="tabs" aria-label="Profile sections">
<a href="/{username}" data-sveltekit-preload-data="hover">Teams</a>
<a href="/{username}/collection/characters" class="active">Collection</a>
</nav>
</div>
</header>
<ProfileHeader
{username}
avatarPicture={data.user?.avatar?.picture}
title="{username}'s Collection"
activeTab="collection"
isOwner={data.isOwner}
/>
<!-- Entity type segmented control -->
<nav class="entity-nav" aria-label="Collection type">
<SegmentedControl value={activeTab} onValueChange={handleTabChange} gap={true}>
<SegmentedControl value={activeEntityType} onValueChange={handleTabChange} gap={true}>
<Segment value="characters">
Characters
</Segment>
@ -76,59 +58,11 @@
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/colors' as *;
.collection {
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;
flex-shrink: 0;
}
.header-content {
flex: 1;
}
h1 {
margin: 0 0 $unit-half;
font-size: 24px;
}
.tabs {
display: flex;
gap: $unit-2x;
}
.tabs a {
text-decoration: none;
color: var(--text-secondary);
padding-bottom: 2px;
border-bottom: 2px solid transparent;
&:hover {
color: var(--text-primary);
}
&.active {
border-color: var(--accent-color, #3366ff);
color: var(--accent-color, #3366ff);
}
}
.entity-nav {
margin-bottom: $unit-2x;
max-width: 500px;