feat: redesign dashboard home into overview with quick actions

- personal greeting hero (first name) with a quick-actions row: Send SMS
  (primary, -> messaging), Add device and New API key (both open the
  generate-key/QR flow; "Add device" keeps the user's mental-model label),
  and Quick Start
- webhooks summary row: active-webhook count linking to /dashboard/webhooks,
  keeping a discoverable mobile path now that Webhooks left the tab bar and
  its section moved off Home
- Devices and API Keys cards show counts in their headers
- mobile-first: actions wrap under the greeting at 375px; verified visually

Build, 25 unit tests, and 13 e2e green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
isra el
2026-07-11 07:41:22 +03:00
co-authored by Claude Opus 4.8
parent 516e626213
commit bdae87aaf8
4 changed files with 114 additions and 19 deletions
@@ -128,7 +128,14 @@ export default function ApiKeys() {
<GenerateApiKey ref={addApiKeyRef} showTrigger={false} />
<Card className='min-w-0 max-w-full'>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-lg'>API Keys</CardTitle>
<CardTitle className='text-lg'>
API Keys
{!isPending && !error && (
<span className='ml-2 text-sm font-normal text-muted-foreground'>
{apiKeys?.length ?? 0}
</span>
)}
</CardTitle>
<div className='flex items-center gap-1'>
<Button
variant='ghost'
@@ -108,7 +108,14 @@ export default function DeviceList() {
<GenerateApiKey ref={addDeviceKeyRef} showTrigger={false} />
<Card className='min-w-0 max-w-full'>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-lg'>Registered Devices</CardTitle>
<CardTitle className='text-lg'>
Registered Devices
{!isPending && !error && (
<span className='ml-2 text-sm font-normal text-muted-foreground'>
{devices?.length ?? 0}
</span>
)}
</CardTitle>
<Button
variant='outline'
size='sm'
+90 -15
View File
@@ -1,31 +1,104 @@
'use client'
import { useRef } from 'react'
import Link from 'next/link'
import { useSession } from 'next-auth/react'
import {
ArrowRight,
ArrowUpRightIcon,
KeyRound,
Plus,
Send,
Webhook,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import DeviceList from './(components)/device-list'
import Overview from './(components)/overview'
import ApiKeys from './(components)/api-keys'
import { useSession } from 'next-auth/react'
import { HomeIcon, ArrowUpRightIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
import GenerateApiKey, {
type GenerateApiKeyHandle,
} from './(components)/generate-api-key'
import { useWebhooks } from '@/lib/api'
// Compact path to webhooks: it left the mobile tab bar and its management
// section moved to /dashboard/webhooks, so Home keeps a discoverable link.
function WebhooksSummaryRow() {
const { data: webhooks } = useWebhooks()
const activeCount = webhooks?.filter((w) => w.isActive).length ?? 0
return (
<Link
href='/dashboard/webhooks'
className='group flex items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 shadow-sm transition-colors hover:bg-muted/50'
>
<div className='rounded-full bg-primary/10 p-2'>
<Webhook className='h-4 w-4 text-primary' />
</div>
<div className='min-w-0 flex-1'>
<p className='text-sm font-medium'>Webhooks</p>
<p className='truncate text-xs text-muted-foreground'>
{activeCount > 0
? `${activeCount} active webhook${activeCount === 1 ? '' : 's'}`
: 'Get notified at your endpoints when SMS events happen'}
</p>
</div>
<ArrowRight className='h-4 w-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5' />
</Link>
)
}
export default function DashboardPage() {
const { data: session } = useSession()
const apiKeyFlowRef = useRef<GenerateApiKeyHandle>(null)
return (
<div className='flex-1 space-y-6 p-6 md:p-8'>
<div className='space-y-1'>
<div className='flex items-center justify-between'>
<div className='flex items-center space-x-2'>
<HomeIcon className='h-6 w-6 text-primary' />
<h2 className='text-3xl font-bold tracking-tight'>Dashboard</h2>
</div>
<Button variant="outline" size="sm" onClick={() => window.open('https://textbee.dev/quickstart', '_blank')}>
<ArrowUpRightIcon className="mr-2 h-4 w-4" />
<div className='flex-1 space-y-6 p-4 sm:p-6 md:p-8'>
{/* Registering a device = generating a key + scanning its QR, so both
quick actions open the same flow with the user's mental-model label. */}
<GenerateApiKey ref={apiKeyFlowRef} showTrigger={false} />
<div className='flex flex-col gap-4 md:flex-row md:items-end md:justify-between'>
<div className='space-y-1'>
<h2 className='text-2xl sm:text-3xl font-bold tracking-tight'>
Welcome back, {session?.user?.name?.split(' ')[0] || 'there'}
</h2>
<p className='text-muted-foreground'>
Here's what's happening with your SMS gateway
</p>
</div>
<div className='flex flex-wrap items-center gap-2'>
<Button asChild size='sm'>
<Link href='/dashboard/messaging'>
<Send className='h-4 w-4' />
Send SMS
</Link>
</Button>
<Button
variant='outline'
size='sm'
onClick={() => apiKeyFlowRef.current?.open()}
>
<Plus className='h-4 w-4' />
Add device
</Button>
<Button
variant='outline'
size='sm'
onClick={() => apiKeyFlowRef.current?.open()}
>
<KeyRound className='h-4 w-4' />
New API key
</Button>
<Button
variant='ghost'
size='sm'
onClick={() => window.open('https://textbee.dev/quickstart', '_blank')}
>
<ArrowUpRightIcon className='h-4 w-4' />
Quick Start
</Button>
</div>
<p className='text-muted-foreground'>
Welcome back, {session?.user?.name || 'User'}
</p>
</div>
<div className='space-y-6'>
@@ -35,6 +108,8 @@ export default function DashboardPage() {
<DeviceList />
<ApiKeys />
</div>
<WebhooksSummaryRow />
</div>
</div>
)
+8 -2
View File
@@ -29,10 +29,16 @@ test.describe('dashboard (mocked API, no real backend)', () => {
await page.goto('/dashboard')
await expect(
page.getByRole('heading', { name: 'Dashboard', level: 2 })
page.getByRole('heading', { name: 'Welcome back, Test', level: 2 })
).toBeVisible()
await expect(page.getByText('Welcome back, Test User')).toBeVisible()
// Quick actions row.
await expect(page.getByRole('link', { name: 'Send SMS' })).toBeVisible()
// Total SMS Sent stat from the mocked gateway stats fixture (12,840).
await expect(page.getByText('12,840')).toBeVisible()
// Webhooks summary row keeps a mobile path to /dashboard/webhooks
// (fixtures have 1 active webhook).
await expect(
page.getByRole('link', { name: /active webhook/ })
).toBeVisible()
})
})