add ProfileHeader component, add collection link to profile nav
This commit is contained in:
parent
9ed505623e
commit
35b0560749
3 changed files with 131 additions and 144 deletions
112
src/lib/components/profile/ProfileHeader.svelte
Normal file
112
src/lib/components/profile/ProfileHeader.svelte
Normal 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>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue