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
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 32 additions & 1 deletion packages/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,38 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lucky - Discord Bot Management</title>
<title>Lucky — Discord Bot with Music, Moderation & Dashboard</title>
<meta name="description" content="The all-in-one Discord bot for your community. Music, moderation, custom commands, auto-mod, and a full web dashboard. Free forever." />

<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:title" content="Lucky — Discord Bot with Music, Moderation & Dashboard" />
<meta property="og:description" content="The all-in-one Discord bot for your community. Music, moderation, custom commands, auto-mod, and a full web dashboard. Free forever." />
<meta property="og:image" content="/favicon.png" />
<meta property="og:url" content="https://lucky.lucassantana.tech" />

<!-- Twitter Card -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="Lucky — Discord Bot with Music, Moderation & Dashboard" />
<meta name="twitter:description" content="The all-in-one Discord bot for your community. Music, moderation, custom commands, auto-mod, and a full web dashboard. Free forever." />
<meta name="twitter:image" content="/favicon.png" />

<!-- JSON-LD Schema -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Lucky",
"description": "The all-in-one Discord bot for your community",
"applicationCategory": "CommunicationApplication",
"operatingSystem": "Web",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
}
}
</script>
</head>
<body>
<div id="root"></div>
Expand Down
125 changes: 120 additions & 5 deletions packages/frontend/src/App.authRoutes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ vi.mock('./components/Layout/Layout', () => ({
),
}))

vi.mock('./pages/Landing', () => ({
default: () => <h1>Landing Page</h1>,
}))

vi.mock('./pages/Login', () => ({
default: () => <h1>Login Page</h1>,
}))
Expand Down Expand Up @@ -118,27 +122,34 @@ describe('App authenticated routing', () => {
mockGuildStore()
})

test('renders login page for unauthenticated non-legal routes', async () => {
renderAt('/unknown')
test('renders landing page for unauthenticated root route', async () => {
renderAt('/')
expect(
await screen.findByRole('heading', { name: 'Landing Page' }),
).toBeInTheDocument()
})

test('renders login page for unauthenticated /login route', async () => {
renderAt('/login')
expect(
await screen.findByRole('heading', { name: 'Login Page' }),
).toBeInTheDocument()
})

test('renders login page when auth check rejects', async () => {
test('renders landing page when auth check rejects and navigating to /', async () => {
const checkAuth = vi.fn().mockRejectedValue(new Error('auth failed'))
mockAuthStore({
checkAuth,
})

renderAt('/unknown')
renderAt('/')

await waitFor(() => {
expect(checkAuth).toHaveBeenCalled()
})

expect(
await screen.findByRole('heading', { name: 'Login Page' }),
await screen.findByRole('heading', { name: 'Landing Page' }),
).toBeInTheDocument()
})

Expand Down Expand Up @@ -319,4 +330,108 @@ describe('App authenticated routing', () => {
}),
).toBeInTheDocument()
})

test('renders landing page for unauthenticated / with verified content', async () => {
renderAt('/')

const landingHeading = await screen.findByRole('heading', {
name: 'Landing Page',
})
expect(landingHeading).toBeInTheDocument()
expect(landingHeading).toBeVisible()
})

test('renders dashboard for authenticated / route', async () => {
mockAuthStore({ isAuthenticated: true })
mockGuildStore({
selectedGuild: {
id: '123',
name: 'Guild',
effectiveAccess: MANAGE_ACCESS,
},
memberContextLoading: false,
})

renderAt('/')

expect(
await screen.findByTestId('layout'),
).toBeInTheDocument()
})

test('redirects unauthenticated user accessing /dashboard to the landing page', async () => {
renderAt('/dashboard')

expect(
await screen.findByRole('heading', { name: 'Landing Page' }),
).toBeInTheDocument()
})

test('/login route is accessible when unauthenticated', async () => {
renderAt('/login')

expect(
await screen.findByRole('heading', { name: 'Login Page' }),
).toBeInTheDocument()
})

test('unauthenticated user accessing authenticated route redirects to landing', async () => {
renderAt('/moderation')

expect(
await screen.findByRole('heading', { name: 'Landing Page' }),
).toBeInTheDocument()
})

test('handles auth check errors gracefully during init', async () => {
const checkAuth = vi.fn().mockRejectedValue(new Error('Connection failed'))
mockAuthStore({ checkAuth })
mockGuildStore()

renderAt('/servers')

await waitFor(() => {
expect(checkAuth).toHaveBeenCalled()
})

expect(
await screen.findByRole('heading', { name: 'Landing Page' }),
).toBeInTheDocument()
})

test('shows page loader while auth is loading', async () => {
const checkAuth = vi.fn<() => Promise<void>>(() => new Promise(() => {}))
mockAuthStore({ isLoading: true, checkAuth })

renderAt('/')

expect(screen.getByRole('status')).toBeInTheDocument()
expect(screen.queryByRole('heading', { name: /Landing Page|Dashboard/ })).not.toBeInTheDocument()
})

test('authenticated user landing on wildcard route gets redirected to dashboard', async () => {
mockAuthStore({ isAuthenticated: true })
mockGuildStore({
selectedGuild: {
id: '123',
name: 'Guild',
effectiveAccess: MANAGE_ACCESS,
},
memberContextLoading: false,
})

renderAt('/unknown-route')

await waitFor(() => {
expect(screen.getByTestId('layout')).toBeInTheDocument()
})
})

test('legal routes (/terms, /privacy) work for unauthenticated users', async () => {
renderAt('/terms')

await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument()
})
})
})
4 changes: 3 additions & 1 deletion packages/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import EmptyState from './components/ui/EmptyState'
import { hasModuleAccess } from './lib/rbac'
import type { AccessMode, ModuleKey } from './types'

const LandingPage = lazy(() => import('./pages/Landing'))
const LoginPage = lazy(() => import('./pages/Login'))
const ServersPage = lazy(() => import('./pages/ServersPage'))
const DashboardPage = lazy(() => import('./pages/DashboardOverview'))
Expand Down Expand Up @@ -266,7 +267,8 @@ function App() {
<ErrorBoundary>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path='/' element={<LoginPage />} />
<Route path='/' element={<LandingPage />} />
<Route path='/login' element={<LoginPage />} />
<Route
path='*'
element={<Navigate to='/' replace />}
Expand Down
Loading
Loading