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 type { PageData } from './$types'
|
||||||
import { createInfiniteQuery } from '@tanstack/svelte-query'
|
import { createInfiniteQuery } from '@tanstack/svelte-query'
|
||||||
import ExploreGrid from '$lib/components/explore/ExploreGrid.svelte'
|
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 { userQueries } from '$lib/api/queries/user.queries'
|
||||||
import { getAvatarSrc, getAvatarSrcSet } from '$lib/utils/avatar'
|
|
||||||
import { IsInViewport } from 'runed'
|
import { IsInViewport } from 'runed'
|
||||||
import Icon from '$lib/components/Icon.svelte'
|
import Icon from '$lib/components/Icon.svelte'
|
||||||
import Button from '$lib/components/ui/Button.svelte'
|
import Button from '$lib/components/ui/Button.svelte'
|
||||||
|
|
@ -11,10 +11,7 @@
|
||||||
const { data } = $props() as { data: PageData }
|
const { data } = $props() as { data: PageData }
|
||||||
const tab = $derived(data.tab || 'teams')
|
const tab = $derived(data.tab || 'teams')
|
||||||
const isOwner = $derived(data.isOwner || false)
|
const isOwner = $derived(data.isOwner || false)
|
||||||
|
const activeTab = $derived<'teams' | 'favorites'>(tab === 'favorites' ? 'favorites' : 'teams')
|
||||||
const avatarFile = $derived(data.user?.avatar?.picture || '')
|
|
||||||
const avatarSrc = $derived(getAvatarSrc(avatarFile))
|
|
||||||
const avatarSrcSet = $derived(getAvatarSrcSet(avatarFile))
|
|
||||||
|
|
||||||
// Note: Type assertion needed because favorites and parties queries have different
|
// Note: Type assertion needed because favorites and parties queries have different
|
||||||
// result structures (items vs results) but we handle both in the items $derived
|
// result structures (items vs results) but we handle both in the items $derived
|
||||||
|
|
@ -94,35 +91,12 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="profile">
|
<section class="profile">
|
||||||
<header class="header">
|
<ProfileHeader
|
||||||
{#if data.user?.avatar?.picture}
|
username={data.user.username}
|
||||||
<img
|
avatarPicture={data.user?.avatar?.picture}
|
||||||
class="avatar"
|
{activeTab}
|
||||||
alt={`Avatar of ${data.user.username}`}
|
{isOwner}
|
||||||
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>
|
|
||||||
|
|
||||||
{#if partiesQuery.isLoading}
|
{#if partiesQuery.isLoading}
|
||||||
<div class="loading">
|
<div class="loading">
|
||||||
|
|
@ -170,39 +144,6 @@
|
||||||
.profile {
|
.profile {
|
||||||
padding: $unit-2x 0;
|
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,
|
.empty,
|
||||||
.end,
|
.end,
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,14 @@
|
||||||
import type { LayoutData } from './$types'
|
import type { LayoutData } from './$types'
|
||||||
import { page } from '$app/stores'
|
import { page } from '$app/stores'
|
||||||
import { goto } from '$app/navigation'
|
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 SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
|
||||||
import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
|
import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
|
||||||
|
|
||||||
let { data, children }: { data: LayoutData; children: any } = $props()
|
let { data, children }: { data: LayoutData; children: any } = $props()
|
||||||
|
|
||||||
const avatarFile = $derived(data.user?.avatar?.picture || '')
|
// Determine active entity type from URL path
|
||||||
const avatarSrc = $derived(getAvatarSrc(avatarFile))
|
const activeEntityType = $derived.by(() => {
|
||||||
const avatarSrcSet = $derived(getAvatarSrcSet(avatarFile))
|
|
||||||
|
|
||||||
// Determine active tab from URL path
|
|
||||||
const activeTab = $derived.by(() => {
|
|
||||||
const path = $page.url.pathname
|
const path = $page.url.pathname
|
||||||
if (path.includes('/weapons')) return 'weapons'
|
if (path.includes('/weapons')) return 'weapons'
|
||||||
if (path.includes('/summons')) return 'summons'
|
if (path.includes('/summons')) return 'summons'
|
||||||
|
|
@ -32,31 +28,17 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section class="collection">
|
<section class="collection">
|
||||||
<header class="header">
|
<ProfileHeader
|
||||||
{#if data.user?.avatar?.picture}
|
{username}
|
||||||
<img
|
avatarPicture={data.user?.avatar?.picture}
|
||||||
class="avatar"
|
title="{username}'s Collection"
|
||||||
alt={`Avatar of ${username}`}
|
activeTab="collection"
|
||||||
src={avatarSrc}
|
isOwner={data.isOwner}
|
||||||
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>
|
|
||||||
|
|
||||||
<!-- Entity type segmented control -->
|
<!-- Entity type segmented control -->
|
||||||
<nav class="entity-nav" aria-label="Collection type">
|
<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">
|
<Segment value="characters">
|
||||||
Characters
|
Characters
|
||||||
</Segment>
|
</Segment>
|
||||||
|
|
@ -76,59 +58,11 @@
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '$src/themes/spacing' as *;
|
@use '$src/themes/spacing' as *;
|
||||||
@use '$src/themes/colors' as *;
|
|
||||||
|
|
||||||
.collection {
|
.collection {
|
||||||
padding: $unit-2x 0;
|
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 {
|
.entity-nav {
|
||||||
margin-bottom: $unit-2x;
|
margin-bottom: $unit-2x;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue