update ui components for better interaction
This commit is contained in:
parent
d6b868a9fd
commit
0c03332988
8 changed files with 854 additions and 310 deletions
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue