Skip to content
Closed
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
2 changes: 1 addition & 1 deletion packages/frontend/src/pages/CustomCommands.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ describe('CustomCommandsPage', () => {

const coinflipCard = screen
.getByText('/coinflip')
.closest('[class*="p-4"]')
.closest('[class*="surface-panel"]')
expect(coinflipCard).toHaveClass('opacity-60')
})

Expand Down
226 changes: 114 additions & 112 deletions packages/frontend/src/pages/CustomCommands.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { useState, useEffect, useMemo } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Terminal, Search, X, Code } from 'lucide-react'
import Card from '@/components/ui/Card'
import { Search, X, Code, ChevronDown } from 'lucide-react'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Switch } from '@/components/ui/switch'
Expand Down Expand Up @@ -30,9 +28,8 @@
const [commands, setCommands] = useState<Command[]>([])
const [loading, setLoading] = useState(true)
const [searchQuery, setSearchQuery] = useState('')
const [selectedCategory, setSelectedCategory] = useState<string | null>(
null,
)
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
const [expandedCommand, setExpandedCommand] = useState<string | null>(null)

useEffect(() => {
if (!selectedGuild?.id) return
Expand All @@ -53,9 +50,7 @@
const matchesSearch =
!searchQuery ||
cmd.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
cmd.description
.toLowerCase()
.includes(searchQuery.toLowerCase())
cmd.description.toLowerCase().includes(searchQuery.toLowerCase())
const matchesCat =
!selectedCategory || cmd.category === selectedCategory
return matchesSearch && matchesCat
Expand All @@ -79,7 +74,7 @@
if (!selectedGuild) {
return (
<div className='flex flex-col items-center justify-center h-[60vh] text-center'>
<Terminal className='w-16 h-16 text-lucky-text-tertiary mb-4' />
<Code className='w-16 h-16 text-lucky-text-tertiary mb-4' />
<h2 className='type-h2 text-lucky-text-primary mb-2'>
No Server Selected
</h2>
Expand All @@ -92,6 +87,7 @@

return (
<div className='space-y-6'>
{/* Header */}
<header>
<h1 className='type-h1 text-lucky-text-primary'>
Custom Commands
Expand All @@ -101,38 +97,36 @@
</p>
</header>

{/* Filters */}
<Card className='p-4'>
<div className='flex flex-col sm:flex-row gap-3'>
<div className='relative flex-1'>
<Search className='absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-lucky-text-tertiary' />
<Input
placeholder='Search commands...'
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className='pl-9 bg-lucky-bg-tertiary border-lucky-border text-white placeholder:text-lucky-text-tertiary'
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className='absolute right-3 top-1/2 -translate-y-1/2 text-lucky-text-tertiary hover:text-white'
>
<X className='w-4 h-4' />
</button>
)}
</div>
{/* Search + Filters */}
<div className='surface-panel rounded-lg p-4 space-y-3 border border-lucky-border'>
<div className='relative'>
<Search className='absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-lucky-text-tertiary' />
<Input
placeholder='Search commands...'
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className='pl-9 bg-lucky-bg-tertiary border-lucky-border text-lucky-text-primary placeholder:text-lucky-text-tertiary'
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className='absolute right-3 top-1/2 -translate-y-1/2 text-lucky-text-tertiary hover:text-lucky-text-primary transition-colors'
>
<X className='w-4 h-4' />
</button>
)}
</div>

{/* Category chips */}
{categories.length > 0 && (
<div className='flex flex-wrap gap-1.5 mt-3'>
<div className='flex flex-wrap gap-2'>
<button
onClick={() => setSelectedCategory(null)}
className={cn(
'px-2.5 py-1 rounded-full text-xs font-medium transition-all border',
'px-3 py-1.5 rounded-full text-xs font-medium transition-all border',
!selectedCategory
? 'bg-lucky-red/15 text-lucky-red border-lucky-red/30'
: 'bg-lucky-bg-tertiary text-lucky-text-secondary border-lucky-border hover:text-white',
? 'bg-lucky-red/20 text-lucky-red border-lucky-red/40'
: 'bg-lucky-bg-active text-lucky-text-secondary border-lucky-border hover:bg-lucky-surface-elevated',
)}
>
All ({commands.length})
Expand All @@ -146,102 +140,110 @@
)
}
className={cn(
'px-2.5 py-1 rounded-full text-xs font-medium transition-all border',
'px-3 py-1.5 rounded-full text-xs font-medium transition-all border',
selectedCategory === cat
? CATEGORY_COLORS[cat] ||
'bg-lucky-bg-active text-white border-lucky-border'
: 'bg-lucky-bg-tertiary text-lucky-text-secondary border-lucky-border hover:text-white',
'bg-lucky-brand/20 text-lucky-brand border-lucky-brand/40'
: 'bg-lucky-bg-active text-lucky-text-secondary border-lucky-border hover:bg-lucky-surface-elevated',
)}
>
{cat} (
{
commands.filter((c) => c.category === cat)
.length
}
)
{cat} ({commands.filter((c) => c.category === cat).length})
</button>
))}
</div>
)}
</Card>
</div>

{/* Commands Grid */}
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3'>
{/* Commands List */}
<div className='space-y-1'>
{loading ? (
Array.from({ length: 9 }).map((_, i) => (
<Card key={i} className='p-4 space-y-3'>
<div className='flex items-center gap-3'>
<Skeleton className='w-8 h-8 rounded-lg' />
<div className='flex-1'>
<Skeleton className='h-4 w-24 mb-1' />
<Skeleton className='h-3 w-16' />
</div>
<Skeleton className='w-10 h-5 rounded-full' />
Array.from({ length: 6 }).map((_, i) => (
<div
key={i}

Check warning on line 162 in packages/frontend/src/pages/CustomCommands.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use Array index in keys

See more on https://sonarcloud.io/project/issues?id=LucasSantana-Dev_Lucky&issues=AZ5Tmj86i-BUK6nuCkEz&open=AZ5Tmj86i-BUK6nuCkEz&pullRequest=969
className='surface-panel rounded-lg p-4 border border-lucky-border flex items-center gap-3'
>
<Skeleton className='w-8 h-8 rounded' />
<div className='flex-1'>
<Skeleton className='h-4 w-40 mb-2' />
<Skeleton className='h-3 w-60' />
</div>
<Skeleton className='h-3 w-full' />
</Card>
<Skeleton className='w-10 h-6 rounded' />
</div>
))
) : filtered.length > 0 ? (
<AnimatePresence mode='popLayout'>
{filtered.map((cmd, i) => (
<motion.div
key={cmd.id}
layout
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.15, delay: i * 0.02 }}
filtered.map((cmd) => (
<div
key={cmd.id}
className={cn(
'surface-panel rounded-lg border border-lucky-border transition-all',
!cmd.enabled && 'opacity-60',
)}
>
{/* Row header */}
<button
onClick={() =>
setExpandedCommand(
expandedCommand === cmd.id ? null : cmd.id,
)
}
className='w-full px-4 py-3 flex items-center gap-3 hover:bg-lucky-surface-elevated transition-colors'
>
<Card
className={cn(
'p-4 transition-all hover:border-lucky-border/80',
!cmd.enabled && 'opacity-60',
)}
>
<div className='flex items-start gap-3'>
<div className='p-2 rounded-lg bg-lucky-bg-active shrink-0'>
<Code className='w-4 h-4 text-lucky-text-secondary' />
</div>
<div className='flex-1 min-w-0'>
<div className='flex items-center gap-2'>
<h3 className='type-body-sm font-semibold text-lucky-text-primary truncate'>
/{cmd.name}
</h3>
<Badge
variant='outline'
className={cn(
'text-[9px] uppercase border',
CATEGORY_COLORS[
cmd.category
] ||
'bg-lucky-bg-tertiary text-lucky-text-secondary border-lucky-border',
)}
>
{cmd.category}
</Badge>
</div>
<p className='text-xs text-lucky-text-tertiary mt-1 line-clamp-2'>
{cmd.description}
</p>
</div>
<Switch
checked={cmd.enabled}
onCheckedChange={() =>
handleToggle(cmd)
}
/>
<div className='p-2 rounded bg-lucky-bg-active shrink-0'>
<Code className='w-4 h-4 text-lucky-text-secondary' />
</div>
<div className='flex-1 min-w-0 text-left'>
<div className='flex items-center gap-2 mb-1'>
<h3 className='type-body-sm font-semibold text-lucky-text-primary truncate'>
/{cmd.name}
</h3>
<Badge
className={cn(
'text-[10px] uppercase border shrink-0',
CATEGORY_COLORS[cmd.category] ||
'bg-lucky-bg-active text-lucky-text-secondary border-lucky-border',
)}
>
{cmd.category}
</Badge>
</div>
</Card>
</motion.div>
))}
</AnimatePresence>
<p className='text-xs text-lucky-text-tertiary line-clamp-1'>
{cmd.description}
</p>
</div>
<div className='flex items-center gap-2 shrink-0'>
<Switch
checked={cmd.enabled}
onCheckedChange={() => handleToggle(cmd)}
/>
<ChevronDown
className={cn(
'w-4 h-4 text-lucky-text-tertiary transition-transform',
expandedCommand === cmd.id && 'rotate-180',
)}
/>
</div>
</button>

{/* Expanded details */}
{expandedCommand === cmd.id && (
<div className='border-t border-lucky-border px-4 py-3 bg-lucky-bg-tertiary/30 text-xs text-lucky-text-secondary space-y-2'>
<div>
<p className='font-medium text-lucky-text-primary mb-1'>
Description
</p>
<p>{cmd.description}</p>
</div>
</div>
)}
</div>
))
) : (
<div className='col-span-full py-16 text-center'>
<Terminal className='w-12 h-12 text-lucky-text-tertiary mx-auto mb-3' />
<p className='text-sm text-lucky-text-secondary'>
<div className='surface-panel rounded-lg p-12 border border-lucky-border text-center'>
<Code className='w-12 h-12 text-lucky-text-tertiary mx-auto mb-3' />
<p className='text-sm text-lucky-text-secondary mb-1'>
No commands found
</p>
<p className='text-xs text-lucky-text-tertiary mt-1'>
<p className='text-xs text-lucky-text-tertiary'>
{searchQuery || selectedCategory
? 'Try adjusting your filters'
: 'Commands will appear here'}
Expand Down
22 changes: 16 additions & 6 deletions packages/frontend/src/pages/GuildAutomation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ describe('GuildAutomation', () => {

await waitFor(() => {
expect(screen.getByText('Guild Automation')).toBeInTheDocument()
expect(screen.getByText('Automation Status')).toBeInTheDocument()
expect(screen.getByText(/Automation Status/)).toBeInTheDocument()
})
})

Expand Down Expand Up @@ -362,7 +362,7 @@ describe('GuildAutomation', () => {

await waitFor(() => screen.getByText('Manifest'))

const expandButton = screen.getByText('Expand')
const expandButton = screen.getByRole('button', { name: /Expand/i })
await user.click(expandButton)

await waitFor(() => {
Expand Down Expand Up @@ -392,7 +392,7 @@ describe('GuildAutomation', () => {

await waitFor(() => screen.getByText('Manifest'))

const expandButton = screen.getByText('Expand')
const expandButton = screen.getByRole('button', { name: /Expand/i })
await user.click(expandButton)

await waitFor(() => screen.getByText('Save Manifest'))
Expand Down Expand Up @@ -425,7 +425,7 @@ describe('GuildAutomation', () => {

await waitFor(() => screen.getByText('Manifest'))

const expandButton = screen.getByText('Expand')
const expandButton = screen.getByRole('button', { name: /Expand/i })
await user.click(expandButton)

await waitFor(() => screen.getByText('Save Manifest'))
Expand Down Expand Up @@ -515,6 +515,7 @@ describe('GuildAutomation', () => {
})

test('displays error message in run card when error exists', async () => {
const user = userEvent.setup()
const runWithError: AutomationRun = {
...mockRun,
status: 'failed',
Expand All @@ -534,6 +535,15 @@ describe('GuildAutomation', () => {
</MemoryRouter>,
)

// Wait for the run card to render
await waitFor(() => {
expect(screen.getByText('plan')).toBeInTheDocument()
})

// Expand the run card to show the error message
const runCard = screen.getByText('plan').closest('button')
await user.click(runCard!)

await waitFor(() => {
expect(
screen.getByText('Failed to create role: permission denied'),
Expand All @@ -556,7 +566,7 @@ describe('GuildAutomation', () => {
</MemoryRouter>,
)

await waitFor(() => screen.getByText('Automation Status'))
await waitFor(() => screen.getByText(/Automation Status/))

vi.clearAllMocks()

Expand Down Expand Up @@ -612,7 +622,7 @@ describe('GuildAutomation', () => {

await waitFor(() => screen.getByText('Manifest'))

const expandButton = screen.getByText('Expand')
const expandButton = screen.getByRole('button', { name: /Expand/i })
await user.click(expandButton)

await waitFor(() => {
Expand Down
Loading
Loading