polish(dashboard): downgrade the build-assistant hero to the quiet-sentence promo (#1731)
Replace the boxed Card hero on Hem with AgentPromo, a clone of the SkatteverketPromoCard pattern: one 12.5px muted sentence with the action link at the end, '(beta)' as a word in the sentence, and a per-company 'Dölj förslaget' dismiss persisted in localStorage (erp_agent_promo_dismissed:<companyId>) via useSyncExternalStore. Removes the hover:border-primary/50 opacity border and the arrow translate (both against design.md). Gate (!agentBuilt and checklist dismissed/completed) and hasAi ? /onboarding/agent : /settings/billing routing unchanged; SkatteverketPromoCard mutual exclusion on agentBuilt unchanged. Copy moved to dashboard.agent_promo_* in sv+en. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
64fc7c783d
commit
e1805125af
@@ -0,0 +1,80 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useSyncExternalStore } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useCapability } from '@/contexts/CompanyContext'
|
||||
import { CAPABILITY } from '@/lib/entitlements/keys'
|
||||
|
||||
const dismissKey = (companyId: string) => `erp_agent_promo_dismissed:${companyId}`
|
||||
// storage events only fire in OTHER tabs; this custom event covers the
|
||||
// same-tab dismissal so useSyncExternalStore re-reads localStorage.
|
||||
const DISMISS_EVENT = 'erp-agent-promo-dismissed'
|
||||
|
||||
function subscribeToDismissal(onStoreChange: () => void) {
|
||||
window.addEventListener('storage', onStoreChange)
|
||||
window.addEventListener(DISMISS_EVENT, onStoreChange)
|
||||
return () => {
|
||||
window.removeEventListener('storage', onStoreChange)
|
||||
window.removeEventListener(DISMISS_EVENT, onStoreChange)
|
||||
}
|
||||
}
|
||||
|
||||
interface AgentPromoProps {
|
||||
companyId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Build-assistant nudge on Hem, shown until the company has a verified
|
||||
* agent_profile (the caller gates on that plus the first-run checklist
|
||||
* being dismissed or completed, since the checklist already carries the
|
||||
* assistant as its last step).
|
||||
* Shape: one quiet sentence with the action link at the end and the same
|
||||
* "Dölj" text control the Skatteverket promo uses, not a boxed card. It is
|
||||
* an optional offer, not an exception, so it stays muted (convention 6/12).
|
||||
* Non-payers keep seeing it (conversion surface) but it routes to billing
|
||||
* instead of a build flow that would 403.
|
||||
*/
|
||||
export function AgentPromo({ companyId }: AgentPromoProps) {
|
||||
const t = useTranslations('dashboard')
|
||||
const hasAi = useCapability(CAPABILITY.ai)
|
||||
|
||||
// Server snapshot says dismissed: the promo appears only after hydration,
|
||||
// when localStorage is readable, so server and client never disagree.
|
||||
const dismissed = useSyncExternalStore(
|
||||
subscribeToDismissal,
|
||||
() => localStorage.getItem(dismissKey(companyId)) === 'true',
|
||||
() => true
|
||||
)
|
||||
|
||||
const dismiss = useCallback(() => {
|
||||
localStorage.setItem(dismissKey(companyId), 'true')
|
||||
window.dispatchEvent(new Event(DISMISS_EVENT))
|
||||
}, [companyId])
|
||||
|
||||
if (dismissed) return null
|
||||
|
||||
return (
|
||||
<section className="flex items-start justify-between gap-4">
|
||||
<p className="text-[12.5px] leading-5 text-muted-foreground">
|
||||
{t(hasAi ? 'agent_promo_description' : 'agent_promo_description_upgrade')}{' '}
|
||||
{/* py-3/-my-3: a 44px tap target on a link that still sits inline in
|
||||
the sentence; the negative margin keeps the line box at 20px so
|
||||
nothing shifts. */}
|
||||
<Link
|
||||
href={hasAi ? '/onboarding/agent' : '/settings/billing'}
|
||||
className="inline-block -my-3 whitespace-nowrap py-3 text-foreground underline decoration-border underline-offset-4 transition-colors hover:decoration-foreground"
|
||||
>
|
||||
{t(hasAi ? 'agent_promo_cta' : 'agent_promo_cta_upgrade')}
|
||||
</Link>
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={dismiss}
|
||||
className="-my-3 shrink-0 py-3 text-xs text-muted-foreground underline decoration-border underline-offset-4 transition-colors hover:text-foreground"
|
||||
>
|
||||
{t('agent_promo_dismiss')}
|
||||
</button>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +1,17 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
import { AttnLine } from '@/components/ui/attn-line'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useCapability, useCompany } from '@/contexts/CompanyContext'
|
||||
import { CAPABILITY } from '@/lib/entitlements/keys'
|
||||
import { useCompany } from '@/contexts/CompanyContext'
|
||||
import NewUserChecklist from '@/components/onboarding/NewUserChecklist'
|
||||
import AttGoraSection from '@/components/dashboard/AttGoraSection'
|
||||
import ResumePane from '@/components/dashboard/ResumePane'
|
||||
import BackupHealthBanner from '@/components/dashboard/BackupHealthBanner'
|
||||
import { SkatteverketPromoCard } from '@/components/dashboard/SkatteverketPromoCard'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import { AgentPromo } from '@/components/dashboard/AgentPromo'
|
||||
import type { InitialSetupState, OnboardingProgress } from '@/types'
|
||||
import type { SuggestedMatch, WorklistCounts } from '@/lib/worklist/types'
|
||||
import type { ResumeItem } from '@/lib/worklist/resume'
|
||||
@@ -85,7 +81,6 @@ export default function DashboardContent({
|
||||
sieSweep = null,
|
||||
}: DashboardContentProps) {
|
||||
const t = useTranslations('dashboard')
|
||||
const hasAi = useCapability(CAPABILITY.ai)
|
||||
const { company } = useCompany()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -144,37 +139,13 @@ export default function DashboardContent({
|
||||
sieSweep={sieSweep}
|
||||
/>
|
||||
|
||||
{/* Build-assistant hero: shown only until the company has a verified
|
||||
{/* Build-assistant nudge: shown only until the company has a verified
|
||||
agent_profile, so existing/migrated users get a clear prompt instead
|
||||
of a full-screen onboarding takeover. While the stepped first-run
|
||||
checklist is visible it already carries the assistant as its last
|
||||
step, so the hero waits until that block is dismissed or completed. */}
|
||||
step, so the promo waits until that block is dismissed or completed. */}
|
||||
{!agentBuilt && (initialSetup.dismissedAt || initialSetup.completedAt) && (
|
||||
<section>
|
||||
{/* Non-payers keep seeing the hero (conversion surface) but it
|
||||
routes to billing instead of a build flow that would 403. */}
|
||||
<Link href={hasAi ? '/onboarding/agent' : '/settings/billing'} className="block group">
|
||||
<Card className="transition-colors hover:border-primary/50">
|
||||
<CardContent className="p-6 flex items-center gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-display text-xl leading-tight">Bygg din bokföringsassistent</p>
|
||||
<Badge variant="secondary" className="uppercase tracking-wider">Beta</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{hasAi
|
||||
? 'Några frågor om din verksamhet kalibrerar en assistent som föreslår bokföring åt dig.'
|
||||
: 'Ingår i abonnemanget: en assistent som föreslår bokföring åt dig.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden sm:flex items-center gap-1.5 text-sm font-medium text-foreground group-hover:translate-x-0.5 transition-transform">
|
||||
<span>{hasAi ? 'Kom igång' : 'Uppgradera'}</span>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</section>
|
||||
<AgentPromo companyId={companyId} />
|
||||
)}
|
||||
|
||||
{/* The two panes (concept hem-grid). When nothing is in progress the
|
||||
|
||||
Reference in New Issue
Block a user