All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- Sidebar: dynamic brand-accent colors, brainstorm section restyled - AI chat general: popup panel with expand/collapse, hides when contextual AI open - AI chat contextual: tabs reordered (Actions first), X close button, height fix - Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.) - Global color cleanup: emerald/orange hardcoded → brand-accent dynamic - Brainstorm page: orange → brand-accent throughout - PageEntry animation component added to key pages - Floating AI button: bg-brand-accent instead of hardcoded black - i18n: all 15 locales updated with new AI/billing keys - Billing: freemium quota tracking, BYOK, stripe subscription scaffolding - Admin: integrated into new design - AGENTS.md + CLAUDE.md project rules added
211 lines
8.1 KiB
TypeScript
211 lines
8.1 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
|
|
import { UserPlus, Link, Mail, Check, Copy } from 'lucide-react'
|
|
|
|
interface InviteDialogProps {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
onInviteByEmail: (email: string, role: 'editor' | 'viewer') => Promise<any>
|
|
onInviteByLink: () => Promise<any>
|
|
seedIdea: string
|
|
isLoading: boolean
|
|
t: (key: string) => string | undefined
|
|
}
|
|
|
|
export function InviteDialog({
|
|
open,
|
|
onOpenChange,
|
|
onInviteByEmail,
|
|
onInviteByLink,
|
|
seedIdea,
|
|
isLoading,
|
|
t,
|
|
}: InviteDialogProps) {
|
|
const [tab, setTab] = useState<'email' | 'link'>('email')
|
|
const [email, setEmail] = useState('')
|
|
const [role, setRole] = useState<'editor' | 'viewer'>('editor')
|
|
const [linkCopied, setLinkCopied] = useState(false)
|
|
const [emailSent, setEmailSent] = useState(false)
|
|
const [localLoading, setLocalLoading] = useState(false)
|
|
|
|
const handleEmailInvite = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!email.trim()) return
|
|
setLocalLoading(true)
|
|
try {
|
|
const res = await onInviteByEmail(email.trim(), role)
|
|
if (res?.invitedUser) {
|
|
setEmailSent(true)
|
|
setTimeout(() => {
|
|
setEmailSent(false)
|
|
setEmail('')
|
|
}, 2000)
|
|
}
|
|
} finally {
|
|
setLocalLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleLinkCopy = async () => {
|
|
setLocalLoading(true)
|
|
try {
|
|
const res = await onInviteByLink()
|
|
if (res?.inviteUrl) {
|
|
const url = window.location.origin + res.inviteUrl
|
|
if (navigator.clipboard?.writeText) {
|
|
await navigator.clipboard.writeText(url)
|
|
} else {
|
|
const ta = document.createElement('textarea')
|
|
ta.value = url
|
|
ta.style.position = 'fixed'
|
|
ta.style.opacity = '0'
|
|
document.body.appendChild(ta)
|
|
ta.select()
|
|
document.execCommand('copy')
|
|
document.body.removeChild(ta)
|
|
}
|
|
setLinkCopied(true)
|
|
setTimeout(() => setLinkCopied(false), 3000)
|
|
}
|
|
} finally {
|
|
setLocalLoading(false)
|
|
}
|
|
}
|
|
|
|
const busy = isLoading || localLoading
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-md bg-white dark:bg-[#1A1A1A] border-border rounded-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2 text-foreground">
|
|
<div className="w-7 h-7 rounded-lg bg-emerald-500/10 flex items-center justify-center">
|
|
<UserPlus size={14} className="text-emerald-500" />
|
|
</div>
|
|
<span className="font-serif">{t('brainstorm.inviteTitle') || 'Invite to brainstorm'}</span>
|
|
</DialogTitle>
|
|
<DialogDescription className="text-muted-foreground text-xs italic font-serif">
|
|
{seedIdea.length > 60 ? seedIdea.substring(0, 60) + '…' : seedIdea}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="flex gap-1 p-1 bg-foreground/5 rounded-xl mt-2">
|
|
<button
|
|
onClick={() => setTab('email')}
|
|
className={`flex-1 flex items-center justify-center gap-1.5 py-2 px-3 rounded-lg text-[10px] font-bold uppercase tracking-[0.1em] transition-all ${
|
|
tab === 'email'
|
|
? 'bg-white dark:bg-[#2A2A2A] text-foreground shadow-sm'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
<Mail size={12} />
|
|
{t('brainstorm.inviteByEmail') || 'Email'}
|
|
</button>
|
|
<button
|
|
onClick={() => setTab('link')}
|
|
className={`flex-1 flex items-center justify-center gap-1.5 py-2 px-3 rounded-lg text-[10px] font-bold uppercase tracking-[0.1em] transition-all ${
|
|
tab === 'link'
|
|
? 'bg-white dark:bg-[#2A2A2A] text-foreground shadow-sm'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
<Link size={12} />
|
|
{t('brainstorm.inviteByLink') || 'Link'}
|
|
</button>
|
|
</div>
|
|
|
|
{tab === 'email' ? (
|
|
<form onSubmit={handleEmailInvite} className="space-y-4 mt-2">
|
|
<div>
|
|
<label className="text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground mb-1.5 block">
|
|
{t('brainstorm.inviteEmailLabel') || 'Email address'}
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder={t('brainstorm.inviteEmailPlaceholder') || 'colleague@email.com'}
|
|
className="w-full px-4 py-3 text-sm border border-border rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500/40 transition-all"
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground mb-1.5 block">
|
|
{t('brainstorm.inviteRoleLabel') || 'Role'}
|
|
</label>
|
|
<div className="flex gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => setRole('editor')}
|
|
className={`flex-1 py-2.5 rounded-xl text-[10px] font-bold uppercase tracking-[0.1em] border transition-all ${
|
|
role === 'editor'
|
|
? 'border-emerald-500/40 bg-emerald-500/5 text-emerald-600'
|
|
: 'border-border text-muted-foreground hover:border-emerald-500/20'
|
|
}`}
|
|
>
|
|
{t('brainstorm.roleEditor') || 'Editor'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setRole('viewer')}
|
|
className={`flex-1 py-2.5 rounded-xl text-[10px] font-bold uppercase tracking-[0.1em] border transition-all ${
|
|
role === 'viewer'
|
|
? 'border-brand-accent/40 bg-brand-accent/5 text-brand-accent'
|
|
: 'border-border text-muted-foreground hover:border-brand-accent/20'
|
|
}`}
|
|
>
|
|
{t('brainstorm.roleViewer') || 'Viewer'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={!email.trim() || busy}
|
|
className="w-full py-3 bg-emerald-500 hover:bg-emerald-600 text-white text-[10px] font-bold uppercase tracking-[0.15em] rounded-xl disabled:opacity-50 transition-all flex items-center justify-center gap-1.5"
|
|
>
|
|
{emailSent ? (
|
|
<>
|
|
<Check size={12} />
|
|
{t('brainstorm.inviteSent') || 'Invitation sent!'}
|
|
</>
|
|
) : (
|
|
<>
|
|
<Mail size={12} />
|
|
{busy ? '...' : (t('brainstorm.sendInvite') || 'Send invitation')}
|
|
</>
|
|
)}
|
|
</button>
|
|
</form>
|
|
) : (
|
|
<div className="space-y-4 mt-2">
|
|
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
{t('brainstorm.linkDescription') || 'Anyone with this link can join the brainstorm session.'}
|
|
</p>
|
|
<button
|
|
onClick={handleLinkCopy}
|
|
disabled={busy}
|
|
className="w-full py-3 bg-foreground/5 hover:bg-foreground/10 text-foreground text-[10px] font-bold uppercase tracking-[0.15em] rounded-xl transition-all flex items-center justify-center gap-1.5 border border-border"
|
|
>
|
|
{linkCopied ? (
|
|
<>
|
|
<Check size={12} className="text-emerald-500" />
|
|
<span className="text-emerald-500">{t('brainstorm.linkCopied') || 'Link copied!'}</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy size={12} />
|
|
{busy ? '...' : (t('brainstorm.copyLink') || 'Copy invite link')}
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|