diff --git a/packages/frontend/src/index.css b/packages/frontend/src/index.css
index 5aa051e1a..877b98d65 100644
--- a/packages/frontend/src/index.css
+++ b/packages/frontend/src/index.css
@@ -1,4 +1,4 @@
-@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Sora:wght@400;500;600;700&family=Manrope:wght@400;500;600;700&display=swap');
@import 'tailwindcss';
@@ -54,18 +54,23 @@
--color-sidebar-border: hsl(var(--sidebar-border));
--color-sidebar-ring: hsl(var(--sidebar-ring));
- /* Discord blurple accent palette */
+ /* Neon Lucky brand palette (neon pink/orange) */
+ --color-lucky-neon-pink: #ec4899;
+ --color-lucky-neon-orange: #fb923c;
+ --color-lucky-neon-purple: #8b5cf6;
+
+ /* Discord blurple accent palette (legacy) */
--color-lucky-blurple-700: #4752c4;
--color-lucky-blurple-600: #5865f2;
--color-lucky-blurple-500: #6d7af4;
--color-lucky-blurple-400: #8891f6;
--color-lucky-blurple-200: #c0c5fb;
- /* Semantic brand tokens (for bg-lucky-brand, text-lucky-brand, etc.) */
- --color-lucky-brand: #5865f2;
- --color-lucky-brand-strong: #4752c4;
- --color-lucky-accent: #5865f2;
- --color-lucky-accent-soft: #6d7af4;
+ /* Semantic brand tokens (now using neon pink) */
+ --color-lucky-brand: #ec4899;
+ --color-lucky-brand-strong: #db2777;
+ --color-lucky-accent: #fb923c;
+ --color-lucky-accent-soft: #fdba74;
/* Status colors */
--color-lucky-success: #23a55a;
@@ -118,8 +123,8 @@
--color-lucky-blue: #5865f2;
--color-lucky-purple: #5865f2;
- --font-lucky-display: 'Inter', 'Segoe UI', system-ui, sans-serif;
- --font-lucky-body: 'Inter', 'Segoe UI', system-ui, sans-serif;
+ --font-lucky-display: 'Sora', 'Segoe UI', system-ui, sans-serif;
+ --font-lucky-body: 'Manrope', 'Segoe UI', system-ui, sans-serif;
--font-lucky-mono: 'JetBrains Mono', 'SFMono-Regular', Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
--font-sans: var(--font-lucky-body);
@@ -372,8 +377,8 @@
--lucky-warning: var(--color-lucky-warning);
--lucky-info: var(--color-lucky-info);
- --lucky-gradient-brand: linear-gradient(135deg, #5865f2 0%, #4752c4 100%);
- --lucky-gradient-brand-soft: linear-gradient(135deg, rgb(88 101 242 / 0.15) 0%, rgb(71 82 196 / 0.05) 100%);
+ --lucky-gradient-brand: linear-gradient(135deg, #ec4899 0%, #fb923c 100%);
+ --lucky-gradient-brand-soft: linear-gradient(135deg, rgb(236 72 153 / 0.15) 0%, rgb(251 146 60 / 0.05) 100%);
--lucky-red: var(--lucky-brand);
--lucky-bg-primary: var(--lucky-surface-canvas);
diff --git a/packages/frontend/src/pages/Landing.test.tsx b/packages/frontend/src/pages/Landing.test.tsx
index b0299409c..a525f043b 100644
--- a/packages/frontend/src/pages/Landing.test.tsx
+++ b/packages/frontend/src/pages/Landing.test.tsx
@@ -1,5 +1,5 @@
import { describe, test, expect, vi, beforeEach } from 'vitest'
-import { render, screen, waitFor } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Landing from './Landing'
import { useAuthStore } from '@/stores/authStore'
@@ -9,7 +9,21 @@ import { api } from '@/services/api'
vi.mock('@/stores/authStore')
vi.mock('@/hooks/usePageMetadata')
-vi.mock('framer-motion')
+vi.mock('framer-motion', async () => {
+ const React = await import('react')
+ const passthrough = (tag: string) =>
+ React.forwardRef(({ children, ...props }: any, ref: any) =>
+ React.createElement(tag, { ...props, ref }, children),
+ )
+ return {
+ motion: new Proxy(
+ {},
+ { get: (_t, prop: string) => passthrough(prop) },
+ ),
+ AnimatePresence: ({ children }: any) => children,
+ useReducedMotion: vi.fn(() => false),
+ }
+})
vi.mock('@/services/api')
const mockLogin = vi.fn()
@@ -49,20 +63,27 @@ describe('Landing', () => {
render( )
- const container = screen.getByText(/The all-in-one Discord bot for your community/i)
+ const container = screen.getByText(/All-in-one Discord bot/i)
.closest('.lucky-shell')
expect(container).toBeInTheDocument()
})
- test('renders hero section with headline and CTA buttons', () => {
+ test('renders hero section with logo, headline and CTA buttons', () => {
render( )
- const headline = screen.getByRole('heading', {
- name: /The all-in-one Discord bot for your community/i,
- })
+ // Check for logo
+ const logo = screen.getByAltText('Lucky Bot')
+ expect(logo).toBeInTheDocument()
+
+ // Check for headline
+ const headline = screen.getByText(/All-in-one Discord bot/i)
expect(headline).toBeInTheDocument()
+ const subheadline = screen.getByText(/for your community/i)
+ expect(subheadline).toBeInTheDocument()
+
+ // Check for CTA buttons
const addToDiscordBtn = screen.getByRole('button', { name: /Add to Discord/i })
const openDashboardBtn = screen.getByRole('button', { name: /Open Dashboard/i })
@@ -70,12 +91,12 @@ describe('Landing', () => {
expect(openDashboardBtn).toBeInTheDocument()
})
- test('displays hero subtitle', () => {
+ test('displays hero subtitle with "Built for vibes" tagline', () => {
render( )
expect(
screen.getByText(
- /Music, moderation, custom commands, auto-mod, and a full web dashboard./i
+ /Music, moderation, custom commands, auto-mod, and a full web dashboard\. Built for vibes\./i
)
).toBeInTheDocument()
})
@@ -97,7 +118,7 @@ describe('Landing', () => {
})
})
- test('renders stats strip with live data', async () => {
+ test('renders stats strip with live data and icons', async () => {
render( )
// Wait for the stats labels to be rendered
@@ -129,26 +150,32 @@ describe('Landing', () => {
})
})
- test('FAQ accordion contains expandable details elements', () => {
+ test('FAQ items are expandable via button clicks', async () => {
render( )
- const faqSection = screen.getByText(/Frequently Asked Questions/)
- expect(faqSection).toBeInTheDocument()
+ const user = userEvent.setup()
+ // Get the first FAQ button
const firstQuestion = screen.getByText('Is Lucky free?')
- const detailsElement = firstQuestion.closest('details')
+ const firstButton = firstQuestion.closest('button')
- expect(detailsElement).toBeInTheDocument()
- expect(detailsElement).toHaveClass('rounded-lg', 'border', 'border-lucky-border')
+ expect(firstButton).toBeInTheDocument()
+ // Click to expand
+ await user.click(firstButton!)
+
+ // Answer should be visible
const answer = screen.getByText('Yes, Lucky is completely free with no premium tier.')
expect(answer).toBeInTheDocument()
})
- test('renders footer with all link sections', () => {
+ test('renders footer with logo and all link sections', () => {
render( )
- expect(screen.getByText('Lucky')).toBeInTheDocument()
+ // Check for footer logo
+ const footerLogo = screen.getAllByAltText('Lucky').find(el => el.closest('footer'))
+ expect(footerLogo).toBeInTheDocument()
+
expect(screen.getByText('Links')).toBeInTheDocument()
expect(screen.getByText('Support')).toBeInTheDocument()
})
@@ -177,18 +204,16 @@ describe('Landing', () => {
})
test('renders Add to Discord button with correct href when clicked', async () => {
+ const openSpy = vi.spyOn(window, 'open').mockReturnValue(null)
render( )
const addToDiscordBtn = screen.getByRole('button', { name: /Add to Discord/i })
expect(addToDiscordBtn).toBeInTheDocument()
- vi.spyOn(window, 'open').mockReturnValue(null)
-
- const user = userEvent.setup()
- await user.click(addToDiscordBtn)
+ fireEvent.click(addToDiscordBtn)
await waitFor(() => {
- expect(window.open).toHaveBeenCalledWith(
+ expect(openSpy).toHaveBeenCalledWith(
expect.stringContaining('discord.com/oauth2/authorize'),
'_blank'
)
@@ -199,9 +224,7 @@ describe('Landing', () => {
render( )
const openDashboardBtn = screen.getByRole('button', { name: /Open Dashboard/i })
-
- const user = userEvent.setup()
- await user.click(openDashboardBtn)
+ fireEvent.click(openDashboardBtn)
await waitFor(() => {
expect(mockLogin).toHaveBeenCalled()
@@ -242,10 +265,24 @@ describe('Landing', () => {
test('renders all footer columns with proper hierarchy', () => {
render( )
- const footerText = screen.getByText('Discord bot with music, moderation, and more.')
+ const footerText = screen.getByText('Discord bot with music, moderation, and more. Built for vibes.')
expect(footerText).toBeInTheDocument()
const needHelpText = screen.getByText(/Need help\? Join our Discord community\./)
expect(needHelpText).toBeInTheDocument()
})
+
+ test('renders Powerful Features heading', () => {
+ render( )
+
+ const featuresHeading = screen.getByRole('heading', { name: /Powerful Features/i })
+ expect(featuresHeading).toBeInTheDocument()
+ })
+
+ test('renders Frequently Asked Questions heading', () => {
+ render( )
+
+ const faqHeading = screen.getByRole('heading', { name: /Frequently Asked Questions/i })
+ expect(faqHeading).toBeInTheDocument()
+ })
})
diff --git a/packages/frontend/src/pages/Landing.tsx b/packages/frontend/src/pages/Landing.tsx
index 0455f2df1..573d8e86c 100644
--- a/packages/frontend/src/pages/Landing.tsx
+++ b/packages/frontend/src/pages/Landing.tsx
@@ -3,14 +3,15 @@ import { useAuthStore } from '@/stores/authStore'
import { usePageMetadata } from '@/hooks/usePageMetadata'
import { useCountUp } from '@/hooks/useCountUp'
import Button from '@/components/ui/Button'
-import { useReducedMotion } from 'framer-motion'
+import { useReducedMotion, motion } from 'framer-motion'
import { api } from '@/services/api'
+import { Music, Shield, Zap, BarChart3, Palette, Sparkles, ChevronDown, Server, Users, Radio } from 'lucide-react'
const CLIENT_ID = '962198089161134131'
const BOT_INVITE_URL = `https://discord.com/oauth2/authorize?client_id=${CLIENT_ID}&scope=bot%20applications.commands&permissions=8`
-const HERO_GRADIENT = 'bg-gradient-to-br from-indigo-950 via-purple-950 to-slate-950'
-const HERO_GRADIENT_ANIMATED = `${HERO_GRADIENT} bg-[length:200%_200%] animate-[gradient_15s_ease_infinite]`
+// Neon glow backdrop gradients
+const NEON_BACKDROP = 'bg-gradient-to-br from-slate-950 via-purple-950 to-slate-950'
export default function Landing() {
const login = useAuthStore((state) => state.login)
@@ -52,193 +53,368 @@ export default function Landing() {
description: 'The all-in-one Discord bot for your community. Music, moderation, custom commands, auto-mod, and a full web dashboard. Free forever.',
})
- const containerClass = prefersReducedMotion ? HERO_GRADIENT : HERO_GRADIENT_ANIMATED
+ const logoAnimation = prefersReducedMotion
+ ? {}
+ : {
+ animate: { scale: [1, 1.03, 1] },
+ transition: { duration: 3, repeat: Infinity, ease: 'easeInOut' },
+ }
return (
- {/* Hero */}
-
-
-
-
- The all-in-one Discord bot for your community
-
-
- Music, moderation, custom commands, auto-mod, and a full web dashboard. Free forever.
-
-
-
-
- window.open(BOT_INVITE_URL, '_blank')}
- variant='primary'
- className='px-8 py-3 h-12 rounded-lg'
- >
- Add to Discord
-
-
- Open Dashboard
-
-
+ {/* Hero Section with Neon Logo */}
+
+
+ {/* Feature Grid */}
+
+
+ {/* Stats Strip */}
+
+
+ {/* FAQ */}
+
+
+ {/* Footer */}
+
+
+ )
+}
+
+// Hero Section with Animated Logo
+function HeroSection({ logoAnimation, onLogin }: { logoAnimation: any; onLogin: () => void }) {
+ const prefersReducedMotion = useReducedMotion()
+
+ return (
+
+ {/* Animated neon glow blobs */}
+ {!prefersReducedMotion && (
+ <>
+
+
+ >
+ )}
+
+
+ {/* Logo */}
+
+
+
+
+ {/* Headline with gradient text */}
+
+
+ All-in-one Discord bot
+ for your community
+
+
+ Music, moderation, custom commands, auto-mod, and a full web dashboard. Built for vibes.
+
-
-
- {/* Feature Grid (Phase 3) */}
-
-
-
-
Powerful Features
-
Everything you need to manage your Discord server
-
-
-
- {/* Music */}
-
-
🎵
-
Music
-
Spotify-powered autoplay with genre matching
-
- {/* Auto-mod */}
-
-
🛡️
-
Auto-mod
-
Spam/caps/link filters with per-guild rules
-
+ {/* CTA Buttons */}
+
+ window.open(BOT_INVITE_URL, '_blank')}
+ variant='primary'
+ className='px-8 py-3 h-12 rounded-lg bg-gradient-to-r from-pink-500 to-orange-400 text-white font-semibold hover:shadow-lg hover:shadow-pink-500/50 transition-all duration-300 border-0'
+ >
+ Add to Discord
+
+
+ Open Dashboard
+
+
+
+
+ )
+}
- {/* Custom Commands */}
-
-
⚙️
-
Custom Commands
-
User-defined responses with variable interpolation
-
+// Feature Grid with Neon Icon Orbs
+function FeatureSection() {
+ const prefersReducedMotion = useReducedMotion()
- {/* Web Dashboard */}
-
-
📊
-
Web Dashboard
-
Full control from your browser
-
+ const features = [
+ {
+ icon: Music,
+ title: 'Music',
+ desc: 'Spotify-powered autoplay with genre matching',
+ color: 'from-pink-500/20 to-pink-600/10',
+ iconColor: 'text-pink-400',
+ },
+ {
+ icon: Shield,
+ title: 'Auto-mod',
+ desc: 'Spam/caps/link filters with per-guild rules',
+ color: 'from-orange-500/20 to-orange-600/10',
+ iconColor: 'text-orange-400',
+ },
+ {
+ icon: Zap,
+ title: 'Custom Commands',
+ desc: 'User-defined responses with variable interpolation',
+ color: 'from-purple-500/20 to-purple-600/10',
+ iconColor: 'text-purple-400',
+ },
+ {
+ icon: BarChart3,
+ title: 'Web Dashboard',
+ desc: 'Full control from your browser',
+ color: 'from-blue-500/20 to-blue-600/10',
+ iconColor: 'text-blue-400',
+ },
+ {
+ icon: Palette,
+ title: 'Embed Builder',
+ desc: 'Rich embeds without JSON',
+ color: 'from-cyan-500/20 to-cyan-600/10',
+ iconColor: 'text-cyan-400',
+ },
+ {
+ icon: Sparkles,
+ title: 'Artist Preferences',
+ desc: 'Tailor autoplay to your taste',
+ color: 'from-amber-500/20 to-amber-600/10',
+ iconColor: 'text-amber-400',
+ },
+ ]
- {/* Embed Builder */}
-
-
🎨
-
Embed Builder
-
Rich embeds without JSON
-
+ return (
+
+ {/* Subtle grid pattern background */}
+
- {/* Artist Preferences */}
-
-
⭐
-
Artist Preferences
-
Tailor autoplay to your taste
-
-
+
+
+ Powerful Features
+ Everything you need to manage your Discord server
+
+
+
+ {features.map((feature, idx) => {
+ const Icon = feature.icon
+ return (
+
+
+
+
+
+
{feature.title}
+
{feature.desc}
+
+
+ )
+ })}
-
-
- {/* Stats Strip (Phase 4 - Live Data) */}
-
-
-
-
-
- {statsLoading ? '---' : `${guildCount.toLocaleString()}${stats?.totalGuilds ? '+' : ''}`}
-
-
Servers
-
-
-
- {statsLoading ? '---' : `${userCount.toLocaleString()}+`}
-
-
Users
-
-
-
- {stats?.serversOnline ? '✓ Online' : statsLoading ? '---' : '✗ Offline'}
-
-
Status
-
-
+
+
+ )
+}
+
+// Stats Strip with Neon Numbers
+function StatsSection({ statsLoading, guildCount, userCount, serversOnline }: any) {
+ const stats = [
+ {
+ label: 'Servers',
+ value: statsLoading ? '---' : `${guildCount.toLocaleString()}${guildCount > 0 ? '+' : ''}`,
+ icon: Server,
+ },
+ {
+ label: 'Users',
+ value: statsLoading ? '---' : `${userCount.toLocaleString()}+`,
+ icon: Users,
+ },
+ {
+ label: 'Status',
+ value: serversOnline ? '✓ Online' : statsLoading ? '---' : '✗ Offline',
+ icon: Radio,
+ isStatus: true,
+ },
+ ]
+
+ return (
+
+
+
+ {stats.map((stat, idx) => {
+ const Icon = stat.icon
+ return (
+
+
+
+
+
+ {stat.value}
+
+ {stat.label}
+
+ )
+ })}
-
-
- {/* FAQ (Phase 5) */}
-
-
-
-
Frequently Asked Questions
-
-
-
- {[
- {
- q: 'Is Lucky free?',
- a: 'Yes, Lucky is completely free with no premium tier.',
- },
- {
- q: 'What commands does Lucky support?',
- a: 'Lucky supports 100+ commands across music, moderation, custom commands, and more.',
- },
- {
- q: 'How does autoplay work?',
- a: 'Autoplay uses Spotify to match similar songs based on artist preferences you set.',
- },
- {
- q: 'Can I self-host Lucky?',
- a: 'Lucky is hosted and managed by us. You cannot self-host the bot, but you can use the dashboard to configure it.',
- },
- {
- q: 'How do I moderate spam?',
- a: 'Configure auto-mod rules for spam, caps, links, and more in the dashboard.',
- },
- {
- q: 'Where do I get support?',
- a: 'Join our Discord support server or open an issue on GitHub.',
- },
- ].map(({ q, a }, idx) => (
-
-
- {q}
- ▼
-
- {a}
-
- ))}
-
+
+
+ )
+}
+
+// FAQ Section with Smooth Accordions
+function FAQSection() {
+ const [openIdx, setOpenIdx] = useState
(null)
+
+ const faqs = [
+ {
+ q: 'Is Lucky free?',
+ a: 'Yes, Lucky is completely free with no premium tier.',
+ },
+ {
+ q: 'What commands does Lucky support?',
+ a: 'Lucky supports 100+ commands across music, moderation, custom commands, and more.',
+ },
+ {
+ q: 'How does autoplay work?',
+ a: 'Autoplay uses Spotify to match similar songs based on artist preferences you set.',
+ },
+ {
+ q: 'Can I self-host Lucky?',
+ a: 'Lucky is hosted and managed by us. You cannot self-host the bot, but you can use the dashboard to configure it.',
+ },
+ {
+ q: 'How do I moderate spam?',
+ a: 'Configure auto-mod rules for spam, caps, links, and more in the dashboard.',
+ },
+ {
+ q: 'Where do I get support?',
+ a: 'Join our Discord support server or open an issue on GitHub.',
+ },
+ ]
+
+ return (
+
+
+
+ Frequently Asked Questions
+
+
+
+ {faqs.map(({ q, a }, idx) => (
+
+ setOpenIdx(openIdx === idx ? null : idx)}
+ className='w-full px-6 py-4 flex items-center justify-between text-left hover:bg-white/5 transition-colors duration-200'
+ >
+ {q}
+
+
+
+
+
+ {a}
+
+
+ ))}
-
-
- {/* Footer (Phase 5) */}
-
-
-
-
-
Lucky
-
Discord bot with music, moderation, and more.
-
-
-
-
Support
-
Need help? Join our Discord community.
+
+
+ )
+}
+
+// Footer with Logo Tile
+function FooterSection() {
+ return (
+
-
+
+ © 2026 Lucky. All rights reserved.
+
+
+
)
}