Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
callback state can be validated from cookie or query for split-origin setups
- Autoplay Last.fm scrobbling now falls back to stored requester metadata so
recommended tracks keep the original requester attribution
- Frontend shell now initializes guild selection on all authenticated routes,
so the server selector is populated right after login instead of only after
visiting pages that manually triggered guild loading (PR #162)

### Changed

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ For hosted frontend deployments, set `VITE_API_BASE_URL` to your backend API ori
(example: `https://api.yourdomain.com/api`) to avoid auth/API loop misrouting.
Last.fm dashboard connect links use this same API base, so split-origin
deployments should keep `VITE_API_BASE_URL` aligned with the public backend.
Authenticated frontend shell routes now bootstrap guild selection globally, so
the server selector is populated immediately after login without requiring a
visit to `/servers` first.
Without `VITE_API_BASE_URL`, frontend uses same-origin `/api` for
`*.lucassantana.tech` hosts and `api.luk-homeserver.com.br` for
`*.luk-homeserver.com.br`.
Expand Down
28 changes: 7 additions & 21 deletions packages/frontend/src/components/Layout/Layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { describe, test, expect, vi, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import Layout from './Layout'
import { useGuildStore } from '@/stores/guildStore'
import { useGuildSelection } from '@/hooks/useGuildSelection'
import type { Guild } from '@/types'

vi.mock('@/stores/guildStore')
vi.mock('@/hooks/useGuildSelection')
vi.mock('./Sidebar', () => ({
default: () => <aside data-testid='sidebar'>Sidebar</aside>,
}))
Expand All @@ -23,18 +23,10 @@ const mockGuild: Guild = {
describe('Layout', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useGuildStore).mockReturnValue({
vi.mocked(useGuildSelection).mockReturnValue({
guilds: [mockGuild],
selectedGuild: mockGuild,
selectedGuildId: mockGuild.id,
isLoading: false,
serverSettings: null,
serverListing: null,
fetchGuilds: vi.fn(),
selectGuild: vi.fn(),
setSelectedGuild: vi.fn(),
updateServerSettings: vi.fn(),
updateServerListing: vi.fn(),
})
})

Expand Down Expand Up @@ -75,23 +67,17 @@ describe('Layout', () => {

expect(screen.getByText('Music Player')).toBeInTheDocument()
expect(
screen.getByText('Manage queue, autoplay, and real-time playback controls.'),
screen.getByText(
'Manage queue, autoplay, and real-time playback controls.',
),
).toBeInTheDocument()
})

test('renders fallback route copy without selected guild', () => {
vi.mocked(useGuildStore).mockReturnValue({
vi.mocked(useGuildSelection).mockReturnValue({
guilds: [],
selectedGuild: null,
selectedGuildId: null,
isLoading: false,
serverSettings: null,
serverListing: null,
fetchGuilds: vi.fn(),
selectGuild: vi.fn(),
setSelectedGuild: vi.fn(),
updateServerSettings: vi.fn(),
updateServerListing: vi.fn(),
})

renderLayout('/unknown')
Expand Down
40 changes: 27 additions & 13 deletions packages/frontend/src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type ReactNode } from 'react'
import { useLocation } from 'react-router-dom'
import Sidebar from './Sidebar'
import { useGuildStore } from '@/stores/guildStore'
import { useGuildSelection } from '@/hooks/useGuildSelection'

interface LayoutProps {
children: ReactNode
Expand All @@ -15,11 +15,13 @@ interface RouteCopy {
const ROUTE_COPY: Record<string, RouteCopy> = {
'/': {
title: 'Dashboard',
subtitle: 'Operational overview and key status signals for your server.',
subtitle:
'Operational overview and key status signals for your server.',
},
'/servers': {
title: 'Servers',
subtitle: 'Review installation status and manage your eligible communities.',
subtitle:
'Review installation status and manage your eligible communities.',
},
'/lastfm': {
title: 'Last.fm',
Expand All @@ -38,19 +40,23 @@ function getRouteCopy(pathname: string): RouteCopy {
if (pathname.startsWith('/music')) {
return {
title: 'Music Player',
subtitle: 'Manage queue, autoplay, and real-time playback controls.',
subtitle:
'Manage queue, autoplay, and real-time playback controls.',
}
}

return ROUTE_COPY[pathname] ?? {
title: 'Lucky Dashboard',
subtitle: 'Configure modules, moderation, and engagement workflows.',
}
return (
ROUTE_COPY[pathname] ?? {
title: 'Lucky Dashboard',
subtitle:
'Configure modules, moderation, and engagement workflows.',
}
)
}

function Layout({ children }: LayoutProps) {
const location = useLocation()
const { selectedGuild } = useGuildStore()
const { selectedGuild } = useGuildSelection()
const routeCopy = getRouteCopy(location.pathname)

return (
Expand All @@ -60,13 +66,21 @@ function Layout({ children }: LayoutProps) {
<header className='sticky top-0 z-20 border-b border-lucky-border bg-lucky-bg-primary/70 backdrop-blur'>
<div className='mx-auto flex w-full max-w-[1400px] flex-wrap items-end justify-between gap-4 px-4 py-4 md:px-8'>
<div className='space-y-1'>
<p className='type-meta text-lucky-text-tertiary'>Lucky control center</p>
<h1 className='type-title text-lucky-text-primary'>{routeCopy.title}</h1>
<p className='type-body-sm text-lucky-text-secondary'>{routeCopy.subtitle}</p>
<p className='type-meta text-lucky-text-tertiary'>
Lucky control center
</p>
<h1 className='type-title text-lucky-text-primary'>
{routeCopy.title}
</h1>
<p className='type-body-sm text-lucky-text-secondary'>
{routeCopy.subtitle}
</p>
</div>
{selectedGuild && (
<div className='surface-panel min-w-[220px] px-4 py-2.5'>
<p className='type-meta text-lucky-text-tertiary'>Active server</p>
<p className='type-meta text-lucky-text-tertiary'>
Active server
</p>
<p className='type-body-sm truncate text-lucky-text-primary'>
{selectedGuild.name}
</p>
Expand Down
Loading