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
20 changes: 14 additions & 6 deletions packages/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,22 @@ const LastFmPage = lazy(() => import('./pages/LastFm'))
const SpotifyPage = lazy(() => import('./pages/Spotify'))
const TermsOfServicePage = lazy(() => import('./pages/TermsOfService'))
const PrivacyPolicyPage = lazy(() => import('./pages/PrivacyPolicy'))
const DocsPage = lazy(() => import('./pages/Docs'))
const ChangelogPage = lazy(() => import('./pages/Changelog'))

const LEGAL_PATHS = [
const PUBLIC_PATH_PREFIXES = [
'/terms-of-service',
'/terms',
'/privacy-policy',
'/privacy',
'/docs',
'/changelog',
]

function isLegalPath(pathname: string) {
return LEGAL_PATHS.includes(pathname)
function isPublicPath(pathname: string) {
return PUBLIC_PATH_PREFIXES.some(
(p) => pathname === p || pathname.startsWith(`${p}/`),
)
}

function ForbiddenModulePage({ module }: { module: ModuleKey }) {
Expand Down Expand Up @@ -218,13 +224,15 @@ function AuthenticatedRoutes() {
)
}

function LegalRoutes() {
function PublicRoutes() {
return (
<Routes>
<Route path='/terms-of-service' element={<TermsOfServicePage />} />
<Route path='/terms' element={<TermsOfServicePage />} />
<Route path='/privacy-policy' element={<PrivacyPolicyPage />} />
<Route path='/privacy' element={<PrivacyPolicyPage />} />
<Route path='/docs' element={<DocsPage />} />
<Route path='/changelog' element={<ChangelogPage />} />
<Route
path='*'
element={<Navigate to='/terms-of-service' replace />}
Expand All @@ -248,12 +256,12 @@ function App() {
.catch(() => setIsReady(true))
}, [checkAuth])

if (isLegalPath(location.pathname)) {
if (isPublicPath(location.pathname)) {
return (
<div className='dark'>
<ErrorBoundary>
<Suspense fallback={<PageLoader />}>
<LegalRoutes />
<PublicRoutes />
</Suspense>
</ErrorBoundary>
<Analytics />
Expand Down
159 changes: 159 additions & 0 deletions packages/frontend/src/components/DocsShell/DocsShell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { useEffect, useState, type ReactNode } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { ArrowUpRight } from 'lucide-react'
import PublicHeader from './PublicHeader'
import { useActiveHeading } from '@/hooks/useActiveHeading'

export type DocsNavItem = {
label: string
href: string
external?: boolean
}

export type DocsNavGroup = {
heading: string
items: DocsNavItem[]
}

export type DocsTocItem = {
id: string
label: string
depth?: 2 | 3
}

type Props = {
nav: DocsNavGroup[]
toc?: DocsTocItem[]
breadcrumb: string
title: string
lastUpdated?: string
children: ReactNode
}

export default function DocsShell({ nav, toc, breadcrumb, title, lastUpdated, children }: Props) {

Check warning on line 33 in packages/frontend/src/components/DocsShell/DocsShell.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=LucasSantana-Dev_Lucky&issues=AZ4tLp0VfdkBgWhNqzp9&open=AZ4tLp0VfdkBgWhNqzp9&pullRequest=868
const location = useLocation()
const [sidebarOpen, setSidebarOpen] = useState(false)
const activeId = useActiveHeading(toc?.map((t) => t.id) ?? [])

useEffect(() => {
setSidebarOpen(false)
}, [location.pathname])

return (
<div className='min-h-screen bg-lucky-surface-canvas text-white'>
<PublicHeader
breadcrumb={breadcrumb}
sidebarOpen={sidebarOpen}
onToggleSidebar={() => setSidebarOpen((v) => !v)}
/>

<div className='mx-auto grid max-w-7xl gap-0 px-0 md:px-6 lg:grid-cols-[16rem_minmax(0,1fr)_14rem]'>
<DocsSidebar nav={nav} open={sidebarOpen} />

<main className='min-w-0 px-4 py-10 md:px-8 md:py-14 lg:px-10'>
<article className='mx-auto max-w-2xl'>
<header className='mb-10 border-b border-lucky-border-soft pb-6'>
<p className='mb-3 font-mono text-[11px] uppercase tracking-[0.22em] text-lucky-text-muted'>
{breadcrumb}
</p>
<h1 className='text-3xl font-bold tracking-tight text-lucky-text-strong md:text-4xl'>{title}</h1>
{lastUpdated ? (
<p className='mt-3 font-mono text-xs text-lucky-text-muted'>
last updated: {lastUpdated}
</p>
) : null}
</header>
<div className='docs-prose'>{children}</div>
</article>
</main>

{toc?.length ? <DocsToc toc={toc} activeId={activeId} /> : <div className='hidden lg:block' />}
</div>
</div>
)
}

function DocsSidebar({ nav, open }: { nav: DocsNavGroup[]; open: boolean }) {

Check warning on line 76 in packages/frontend/src/components/DocsShell/DocsShell.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=LucasSantana-Dev_Lucky&issues=AZ4tLp0VfdkBgWhNqzp-&open=AZ4tLp0VfdkBgWhNqzp-&pullRequest=868
const { pathname } = useLocation()

return (
<aside
className={`${open ? 'block' : 'hidden'} lg:block border-r border-lucky-border-soft bg-lucky-surface-canvas`}
aria-label='Documentation navigation'
>
<div className='sticky top-14 max-h-[calc(100vh-3.5rem)] overflow-y-auto px-4 py-8 md:px-6'>
<nav className='space-y-7'>
{nav.map((group) => (
<div key={group.heading}>
<h4 className='mb-3 font-mono text-[10px] uppercase tracking-[0.22em] text-lucky-text-muted'>
{group.heading}
</h4>
<ul className='space-y-0.5'>
{group.items.map((item) => {
const isActive = !item.external && pathname === item.href
const base =
'group inline-flex w-full items-center justify-between gap-2 rounded-md px-2.5 py-1.5 text-sm transition-colors'
const inactiveCls = `${base} text-lucky-text-body hover:bg-lucky-surface-panel hover:text-lucky-text-strong`
const activeCls = `${base} bg-lucky-surface-panel text-lucky-text-strong shadow-[inset_2px_0_0_var(--color-lucky-brand)]`
if (item.external) {
return (
<li key={item.href}>
<a
href={item.href}
target='_blank'
rel='noreferrer'
className={inactiveCls}
>
<span>{item.label}</span>
<ArrowUpRight size={11} aria-hidden className='text-lucky-text-muted' />
</a>
</li>
)
}
return (
<li key={item.href}>
<Link to={item.href} className={isActive ? activeCls : inactiveCls}>
<span>{item.label}</span>
</Link>
</li>
)
})}
</ul>
</div>
))}
</nav>
</div>
</aside>
)
}

function DocsToc({ toc, activeId }: { toc: DocsTocItem[]; activeId: string | null }) {

Check warning on line 130 in packages/frontend/src/components/DocsShell/DocsShell.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=LucasSantana-Dev_Lucky&issues=AZ4tLp0VfdkBgWhNqzp_&open=AZ4tLp0VfdkBgWhNqzp_&pullRequest=868
return (
<aside className='hidden lg:block' aria-label='On this page'>
<div className='sticky top-14 max-h-[calc(100vh-3.5rem)] overflow-y-auto px-6 py-10'>
<h4 className='mb-3 font-mono text-[10px] uppercase tracking-[0.22em] text-lucky-text-muted'>
On this page
</h4>
<ul className='space-y-1.5 border-l border-lucky-border-soft pl-3'>
{toc.map((item) => {
const isActive = activeId === item.id
return (
<li key={item.id} className={item.depth === 3 ? 'pl-3' : ''}>
<a
href={`#${item.id}`}
className={`inline-block text-xs leading-snug transition-colors ${
isActive
? 'text-lucky-brand font-medium'
: 'text-lucky-text-muted hover:text-lucky-text-strong'
}`}
>
{item.label}
</a>
</li>
)
})}
</ul>
</div>
</aside>
)
}
106 changes: 106 additions & 0 deletions packages/frontend/src/components/DocsShell/PublicHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Link, useLocation } from 'react-router-dom'
import { ArrowUpRight, Menu, X } from 'lucide-react'

type NavKey = 'docs' | 'changelog' | 'github'

type Props = {
breadcrumb: string
sidebarOpen?: boolean
onToggleSidebar?: () => void
/** Optional override for which top-nav item is highlighted. */
activeNav?: NavKey
}

const GITHUB_URL = 'https://github.com/LucasSantana-Dev/Lucky'

/**
* Shared sticky header for /docs, /changelog, /terms, /privacy.
*
* Right-side nav links auto-derive active state from the URL when `activeNav`
* is omitted.
*/
export default function PublicHeader({
breadcrumb,
sidebarOpen = false,
onToggleSidebar,
activeNav,
}: Props) {

Check warning on line 27 in packages/frontend/src/components/DocsShell/PublicHeader.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=LucasSantana-Dev_Lucky&issues=AZ4tZst4MCNCfLPD6cLa&open=AZ4tZst4MCNCfLPD6cLa&pullRequest=868
const { pathname } = useLocation()
const inferred: NavKey | null = pathname.startsWith('/docs')
? 'docs'
: pathname.startsWith('/changelog')
? 'changelog'
: null

Check warning on line 33 in packages/frontend/src/components/DocsShell/PublicHeader.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=LucasSantana-Dev_Lucky&issues=AZ4tZst4MCNCfLPD6cLb&open=AZ4tZst4MCNCfLPD6cLb&pullRequest=868
const current = activeNav ?? inferred

const linkBase =
'hidden sm:inline-flex items-center rounded-md px-2.5 py-1.5 transition-colors'
const linkInactive = 'hover:bg-lucky-surface-panel hover:text-lucky-text-strong'
const linkActive = 'bg-lucky-surface-panel text-lucky-text-strong'

return (
<header className='sticky top-0 z-30 border-b border-lucky-border-soft bg-lucky-surface-canvas/85 backdrop-blur supports-[backdrop-filter]:bg-lucky-surface-canvas/65'>
<div className='mx-auto flex h-14 max-w-7xl items-center justify-between gap-3 px-4 md:px-6'>
<div className='flex items-center gap-2'>
{onToggleSidebar ? (
<button
onClick={onToggleSidebar}
aria-label={
sidebarOpen
? 'Close navigation'
: 'Open navigation'
}
className='lg:hidden inline-flex h-9 w-9 items-center justify-center rounded-md text-lucky-text-muted hover:bg-lucky-surface-panel hover:text-lucky-text-strong focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-lucky-brand'
>
{sidebarOpen ? <X size={16} /> : <Menu size={16} />}
</button>
) : null}
<Link
to='/'
className='inline-flex items-center gap-2 text-lucky-text-strong hover:text-lucky-brand transition-colors'
>
<img
src='/lucky-logo.png'
alt='Lucky'
width='24'
height='24'
className='h-6 w-6 rounded-full'
loading='eager'
/>
<span className='font-mono text-sm font-semibold tracking-tight'>
lucky<span className='text-lucky-brand'>.</span>
</span>
</Link>
<span className='ml-1 hidden text-lucky-border-strong md:inline'>
/
</span>
<span className='hidden font-mono text-xs uppercase tracking-[0.2em] text-lucky-text-muted md:inline'>
{breadcrumb}
</span>
</div>
<nav className='flex items-center gap-1 font-mono text-xs text-lucky-text-muted'>
<Link
to='/docs'
className={`${linkBase} ${current === 'docs' ? linkActive : linkInactive}`}
>
docs
</Link>
<Link
to='/changelog'
className={`${linkBase} ${current === 'changelog' ? linkActive : linkInactive}`}
>
changelog
</Link>
<a
href={GITHUB_URL}
target='_blank'
rel='noreferrer'
className='inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 hover:bg-lucky-surface-panel hover:text-lucky-text-strong transition-colors'
>
github <ArrowUpRight size={11} aria-hidden />
</a>
</nav>
</div>
</header>
)
}
33 changes: 33 additions & 0 deletions packages/frontend/src/components/DocsShell/legalNav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { DocsNavGroup } from './DocsShell'

export const LEGAL_NAV: DocsNavGroup[] = [
{
heading: 'Legal',
items: [
{ label: 'Terms of Service', href: '/terms' },
{ label: 'Privacy Policy', href: '/privacy' },
],
},
{
heading: 'Product',
items: [
{ label: 'Docs', href: '/docs' },
{ label: 'Changelog', href: '/changelog' },
],
},
{
heading: 'External',
items: [
{
label: 'GitHub',
href: 'https://github.com/LucasSantana-Dev/Lucky',
external: true,
},
{
label: 'Issues',
href: 'https://github.com/LucasSantana-Dev/Lucky/issues',
external: true,
},
],
},
]
2 changes: 1 addition & 1 deletion packages/frontend/src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ function Layout({ children }: LayoutProps) {
</a>
<Sidebar />
<div className='flex min-w-0 flex-1 flex-col'>
<header className='lucky-shell-header sticky top-0 z-20 border-b border-lucky-border bg-lucky-bg-primary/92 relative'>
<header className='lucky-shell-header sticky top-0 z-30 border-b border-lucky-border bg-lucky-bg-primary relative'>
<div className='mx-auto flex w-full max-w-[1400px] items-center justify-between gap-4 px-4 py-3.5 md:px-6 md:py-4'>
<div className='min-w-0'>
<h1 className='type-title text-lucky-text-primary leading-tight'>
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/components/Music/AutoplayGenres.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default function AutoplayGenres({ guildId }: AutoplayGenresProps) {
try {
setError(null)
const response = await apiClient.get<{ genres: string[] }>(
`/api/guilds/${guildId}/autoplay/genres`,
`/guilds/${guildId}/autoplay/genres`,
)
setGenres(response.data.genres || [])
} catch (err) {
Expand Down Expand Up @@ -92,7 +92,7 @@ export default function AutoplayGenres({ guildId }: AutoplayGenresProps) {
setIsLoading(true)
try {
setError(null)
await apiClient.put(`/api/guilds/${guildId}/autoplay/genres`, {
await apiClient.put(`/guilds/${guildId}/autoplay/genres`, {
genres: updatedGenres,
})
setGenres(updatedGenres)
Expand Down
Loading
Loading