update ui components for better interaction

This commit is contained in:
Justin Edmund 2025-09-23 22:09:39 -07:00
parent d6b868a9fd
commit 0c03332988
8 changed files with 854 additions and 310 deletions

View file

@ -4,6 +4,7 @@
import { localizeHref } from '$lib/paraglide/runtime' import { localizeHref } from '$lib/paraglide/runtime'
import { m } from '$lib/paraglide/messages' import { m } from '$lib/paraglide/messages'
import { page } from '$app/stores' import { page } from '$app/stores'
import { goto } from '$app/navigation'
import Button from './ui/Button.svelte' import Button from './ui/Button.svelte'
import Icon from './Icon.svelte' import Icon from './Icon.svelte'
import DropdownItem from './ui/dropdown/DropdownItem.svelte' import DropdownItem from './ui/dropdown/DropdownItem.svelte'
@ -46,6 +47,23 @@
function isDatabaseNavSelected(href: string): boolean { function isDatabaseNavSelected(href: string): boolean {
return $page.url.pathname === href || $page.url.pathname.startsWith(href + '/') return $page.url.pathname === href || $page.url.pathname.startsWith(href + '/')
} }
// Handle logout
async function handleLogout() {
try {
const response = await fetch('/auth/logout', {
method: 'POST',
credentials: 'include'
})
if (response.ok) {
// Navigate to login page after successful logout
await goto('/login')
}
} catch (error) {
console.error('Logout failed:', error)
}
}
</script> </script>
<nav aria-label="Global"> <nav aria-label="Global">
@ -118,9 +136,7 @@
{#if isAuth} {#if isAuth}
<DropdownMenu.Separator class="dropdown-separator" /> <DropdownMenu.Separator class="dropdown-separator" />
<DropdownItem asChild> <DropdownItem asChild>
<form method="post" action="/auth/logout"> <button onclick={handleLogout}>{m.nav_logout()}</button>
<button type="submit">{m.nav_logout()}</button>
</form>
</DropdownItem> </DropdownItem>
{/if} {/if}
</DropdownMenu.Content> </DropdownMenu.Content>
@ -152,7 +168,20 @@
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding-top: spacing.$unit-2x; padding: spacing.$unit-2x;
max-width: var(--main-max-width);
margin: 0 auto;
width: 100%;
// Match database layout width
&.database-layout {
max-width: 1400px;
}
// Responsive padding
@media (max-width: 768px) {
padding: spacing.$unit;
}
ul { ul {
background-color: var(--menu-bg); background-color: var(--menu-bg);

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@
import { ContextMenu as ContextMenuBase } from 'bits-ui' import { ContextMenu as ContextMenuBase } from 'bits-ui'
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte' import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
import { getCharacterImage } from '$lib/features/database/detail/image' import { getCharacterImage } from '$lib/features/database/detail/image'
import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte'
interface Props { interface Props {
item?: GridCharacter item?: GridCharacter
@ -23,6 +24,7 @@
canEdit: () => boolean canEdit: () => boolean
getEditKey: () => string | null getEditKey: () => string | null
services: { gridService: any; partyService: any } services: { gridService: any; partyService: any }
openPicker?: (opts: { type: 'character' | 'weapon' | 'summon'; position: number; item?: any }) => void
} }
const ctx = getContext<PartyCtx>('party') const ctx = getContext<PartyCtx>('party')
@ -72,8 +74,11 @@
} }
function viewDetails() { function viewDetails() {
// TODO: Implement view details modal if (!item) return
console.log('View details for:', item) openDetailsSidebar({
type: 'character',
item
})
} }
function replace() { function replace() {
@ -93,7 +98,7 @@
<div <div
class="frame character cell" class="frame character cell"
class:editable={ctx?.canEdit()} class:editable={ctx?.canEdit()}
onclick={() => ctx?.canEdit() && replace()} onclick={() => viewDetails()}
> >
<img <img
class="image" class="image"
@ -217,15 +222,11 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer;
&.editable { &:hover {
cursor: pointer; opacity: 0.95;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
&:hover {
border-color: var(--primary-color, #0066cc);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
transform: scale(1.01);
}
} }
} }

View file

@ -7,6 +7,7 @@
import { ContextMenu as ContextMenuBase } from 'bits-ui' import { ContextMenu as ContextMenuBase } from 'bits-ui'
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte' import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
import { getSummonImage } from '$lib/features/database/detail/image' import { getSummonImage } from '$lib/features/database/detail/image'
import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte'
interface Props { interface Props {
item?: GridSummon item?: GridSummon
@ -55,8 +56,11 @@
} }
function viewDetails() { function viewDetails() {
// TODO: Implement view details modal if (!item) return
console.log('View details for:', item) openDetailsSidebar({
type: 'summon',
item
})
} }
function replace() { function replace() {
@ -79,7 +83,7 @@
class:friend={item?.friend || position === 6} class:friend={item?.friend || position === 6}
class:cell={!((item?.main || position === -1) || (item?.friend || position === 6))} class:cell={!((item?.main || position === -1) || (item?.friend || position === 6))}
class:editable={ctx?.canEdit()} class:editable={ctx?.canEdit()}
onclick={() => ctx?.canEdit() && replace()} onclick={() => viewDetails()}
> >
<img <img
class="image" class="image"
@ -212,20 +216,11 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer;
&.editable { &:hover {
cursor: pointer; opacity: 0.95;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
&:hover {
border-color: var(--primary-color, #0066cc);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
transform: scale(1.02);
}
}
&.summon.main.editable:hover,
&.summon.friend.editable:hover {
transform: scale(1.01);
} }
} }

View file

@ -7,6 +7,7 @@
import { ContextMenu as ContextMenuBase } from 'bits-ui' import { ContextMenu as ContextMenuBase } from 'bits-ui'
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte' import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
import { getWeaponImage } from '$lib/features/database/detail/image' import { getWeaponImage } from '$lib/features/database/detail/image'
import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte'
interface Props { interface Props {
item?: GridWeapon item?: GridWeapon
@ -59,8 +60,11 @@
} }
function viewDetails() { function viewDetails() {
// TODO: Implement view details modal if (!item) return
console.log('View details for:', item) openDetailsSidebar({
type: 'weapon',
item
})
} }
function replace() { function replace() {
@ -82,6 +86,7 @@
class:main={item?.mainhand || position === -1} class:main={item?.mainhand || position === -1}
class:cell={!(item?.mainhand || position === -1)} class:cell={!(item?.mainhand || position === -1)}
class:editable={ctx?.canEdit()} class:editable={ctx?.canEdit()}
onclick={() => viewDetails()}
> >
<img <img
class="image" class="image"
@ -210,19 +215,11 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer;
&.editable { &:hover {
cursor: pointer; opacity: 0.95;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
&:hover {
border-color: var(--primary-color, #0066cc);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
transform: scale(1.02);
}
}
&.weapon.main.editable:hover {
transform: scale(1.01);
} }
} }

View file

@ -8,6 +8,8 @@
import { sidebar } from '$lib/stores/sidebar.svelte' import { sidebar } from '$lib/stores/sidebar.svelte'
import { Tooltip } from 'bits-ui' import { Tooltip } from 'bits-ui'
import { beforeNavigate } from '$app/navigation' import { beforeNavigate } from '$app/navigation'
import { authStore } from '$lib/stores/auth.store'
import { browser } from '$app/environment'
// Get `data` and `children` from the router via $props() // Get `data` and `children` from the router via $props()
// Use a more flexible type that allows additional properties from child pages // Use a more flexible type that allows additional properties from child pages
@ -16,6 +18,22 @@
children: () => any children: () => any
}>() }>()
// Initialize auth store from server data immediately on load to ensure
// Authorization headers are available for client-side API calls
// Run immediately, not in effect to avoid timing issues
if (browser) {
if (data?.auth) {
console.log('[+layout] Initializing authStore with token:', data.auth.accessToken ? 'present' : 'missing')
authStore.initFromServer(
data.auth.accessToken,
data.auth.user,
data.auth.expiresAt
)
} else {
console.warn('[+layout] No auth data available to initialize authStore')
}
}
// Close sidebar when navigating to a different page // Close sidebar when navigating to a different page
beforeNavigate(() => { beforeNavigate(() => {
sidebar.close() sidebar.close()
@ -27,11 +45,16 @@
</svelte:head> </svelte:head>
<Tooltip.Provider> <Tooltip.Provider>
<div class="app-container"> <div class="app-container" class:sidebar-open={sidebar.isOpen}>
<main class:sidebar-open={sidebar.isOpen}> <div class="nav-wrapper">
<Navigation isAuthenticated={data?.isAuthenticated} username={data?.account?.username} role={data?.account?.role} /> <Navigation isAuthenticated={data?.isAuthenticated} username={data?.account?.username} role={data?.account?.role} />
{@render children?.()} </div>
</main>
<div class="main-pane">
<main class="main-content">
{@render children?.()}
</main>
</div>
<Sidebar <Sidebar
open={sidebar.isOpen} open={sidebar.isOpen}
@ -49,18 +72,163 @@
<style lang="scss"> <style lang="scss">
@use '$src/themes/effects' as *; @use '$src/themes/effects' as *;
@use '$src/themes/layout' as *;
@use '$src/themes/spacing' as *;
:root {
--sidebar-width: 420px;
}
.app-container { .app-container {
display: flex; display: flex;
min-height: 100vh; flex-direction: column;
height: 100vh;
width: 100%; width: 100%;
overflow-x: hidden; position: relative;
overflow: hidden;
// Fixed navigation wrapper with blur effect
.nav-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
width: 100vw;
// Single blur layer with gradient mask for progressive effect
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 80px; // Taller to test the progressive effect
// Color gradient for the background
background: linear-gradient(
to bottom,
color-mix(in srgb, var(--background) 85%, transparent) 0%,
color-mix(in srgb, var(--background) 60%, transparent) 50%,
color-mix(in srgb, var(--background) 20%, transparent) 85%,
transparent 100%
);
// Single blur value applied to entire element
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
// Mask gradient to fade out the blur effect progressively
mask-image: linear-gradient(
to bottom,
black 0%,
black 40%,
transparent 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
black 0%,
black 40%,
transparent 100%
);
pointer-events: none;
z-index: 1;
}
// Navigation content above the blur layer
:global(nav) {
position: relative;
z-index: 2;
}
}
// Main pane with content
.main-pane {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
transition: margin-right $duration-slide ease-in-out;
position: relative;
height: 100%;
// Main content area with independent scroll - content starts at top
.main-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
position: relative;
padding-top: 56px; // Space for fixed navigation to match blur height
// Smooth scrolling
scroll-behavior: smooth;
// Better scrollbar styling
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: var(--bg-secondary, #f1f1f1);
}
&::-webkit-scrollbar-thumb {
background: var(--border-primary, #888);
border-radius: 4px;
&:hover {
background: var(--text-secondary, #555);
}
}
}
}
// When sidebar is open, adjust main pane width
&.sidebar-open {
.main-pane {
margin-right: var(--sidebar-width, 420px);
// Mobile: don't adjust margin, use overlay
@media (max-width: 768px) {
margin-right: 0;
}
}
}
} }
main { // Mobile adjustments
flex: 1; @media (max-width: 768px) {
min-width: 0; .app-container {
overflow-x: auto; .main-pane {
transition: margin-right $duration-slide ease-in-out; .main-content {
// Improve mobile scrolling performance
-webkit-overflow-scrolling: touch;
}
}
// Overlay backdrop when sidebar is open on mobile
&.sidebar-open::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 99;
animation: fadeIn $duration-quick ease-out;
}
}
}
// Fade in animation for mobile backdrop
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
} }
</style> </style>

View file

@ -2,32 +2,32 @@ import type { PageServerLoad } from './$types'
import { PartyService } from '$lib/services/party.service' import { PartyService } from '$lib/services/party.service'
export const load: PageServerLoad = async ({ params, fetch, locals }) => { export const load: PageServerLoad = async ({ params, fetch, locals }) => {
// Get auth data directly from locals instead of parent() // Get auth data directly from locals instead of parent()
const authUserId = locals.session?.account?.userId const authUserId = locals.session?.account?.userId
// Try to fetch party data on the server // Try to fetch party data on the server
const partyService = new PartyService(fetch) const partyService = new PartyService(fetch)
let partyFound = false let partyFound = false
let party = null let party = null
let canEdit = false let canEdit = false
try { try {
// Fetch the party // Fetch the party
party = await partyService.getByShortcode(params.id) party = await partyService.getByShortcode(params.id)
partyFound = true partyFound = true
// Determine if user can edit // Determine if user can edit
canEdit = authUserId ? party.user?.id === authUserId : false canEdit = authUserId ? party.user?.id === authUserId : false
} catch (err) { } catch (err) {
// Error is expected for test/invalid IDs // Error is expected for test/invalid IDs
} }
// Return party data with explicit serialization // Return party data with explicit serialization
return { return {
party: party ? structuredClone(party) : null, party: party ? structuredClone(party) : null,
canEdit: Boolean(canEdit), canEdit: Boolean(canEdit),
partyFound, partyFound,
authUserId: authUserId || null authUserId: authUserId || null
} }
} }

View file

@ -6,7 +6,7 @@
</script> </script>
{#if data?.party} {#if data?.party}
<Party party={data.party} canEdit={data.canEdit || false} /> <Party party={data.party} canEdit={data.canEdit || false} authUserId={data.authUserId} />
{:else} {:else}
<div> <div>
<h1>Party not found</h1> <h1>Party not found</h1>