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 { m } from '$lib/paraglide/messages'
import { page } from '$app/stores'
import { goto } from '$app/navigation'
import Button from './ui/Button.svelte'
import Icon from './Icon.svelte'
import DropdownItem from './ui/dropdown/DropdownItem.svelte'
@ -46,6 +47,23 @@
function isDatabaseNavSelected(href: string): boolean {
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>
<nav aria-label="Global">
@ -118,9 +136,7 @@
{#if isAuth}
<DropdownMenu.Separator class="dropdown-separator" />
<DropdownItem asChild>
<form method="post" action="/auth/logout">
<button type="submit">{m.nav_logout()}</button>
</form>
<button onclick={handleLogout}>{m.nav_logout()}</button>
</DropdownItem>
{/if}
</DropdownMenu.Content>
@ -152,7 +168,20 @@
flex-direction: row;
justify-content: space-between;
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 {
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 UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
import { getCharacterImage } from '$lib/features/database/detail/image'
import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte'
interface Props {
item?: GridCharacter
@ -23,6 +24,7 @@
canEdit: () => boolean
getEditKey: () => string | null
services: { gridService: any; partyService: any }
openPicker?: (opts: { type: 'character' | 'weapon' | 'summon'; position: number; item?: any }) => void
}
const ctx = getContext<PartyCtx>('party')
@ -72,8 +74,11 @@
}
function viewDetails() {
// TODO: Implement view details modal
console.log('View details for:', item)
if (!item) return
openDetailsSidebar({
type: 'character',
item
})
}
function replace() {
@ -93,7 +98,7 @@
<div
class="frame character cell"
class:editable={ctx?.canEdit()}
onclick={() => ctx?.canEdit() && replace()}
onclick={() => viewDetails()}
>
<img
class="image"
@ -217,15 +222,11 @@
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&.editable {
cursor: pointer;
&:hover {
border-color: var(--primary-color, #0066cc);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
transform: scale(1.01);
}
&:hover {
opacity: 0.95;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
}

View file

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

View file

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

View file

@ -8,6 +8,8 @@
import { sidebar } from '$lib/stores/sidebar.svelte'
import { Tooltip } from 'bits-ui'
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()
// Use a more flexible type that allows additional properties from child pages
@ -16,6 +18,22 @@
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
beforeNavigate(() => {
sidebar.close()
@ -27,11 +45,16 @@
</svelte:head>
<Tooltip.Provider>
<div class="app-container">
<main class:sidebar-open={sidebar.isOpen}>
<div class="app-container" class:sidebar-open={sidebar.isOpen}>
<div class="nav-wrapper">
<Navigation isAuthenticated={data?.isAuthenticated} username={data?.account?.username} role={data?.account?.role} />
{@render children?.()}
</main>
</div>
<div class="main-pane">
<main class="main-content">
{@render children?.()}
</main>
</div>
<Sidebar
open={sidebar.isOpen}
@ -49,18 +72,163 @@
<style lang="scss">
@use '$src/themes/effects' as *;
@use '$src/themes/layout' as *;
@use '$src/themes/spacing' as *;
:root {
--sidebar-width: 420px;
}
.app-container {
display: flex;
min-height: 100vh;
flex-direction: column;
height: 100vh;
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 {
flex: 1;
min-width: 0;
overflow-x: auto;
transition: margin-right $duration-slide ease-in-out;
// Mobile adjustments
@media (max-width: 768px) {
.app-container {
.main-pane {
.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>

View file

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

View file

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