chore: remove dead code — 8 components, 5 libs, 4 API routes, 4 npm packages, 30+ scripts, dead CSS, dead exports
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 5s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 5s
Removed unused components: - brainstorm-canvas, brainstorm-create-dialog, invite-dialog, manual-idea-dialog - note-inline-editor, profile-page-header, quota-paywall, label-management-dialog Removed dead lib files: - api-auth.ts, color-harmony-recommendation.ts, label-storage.ts, modern-color-options.ts - hooks/use-card-size-mode.ts Removed dead API routes: - ai/test-chat, ai/test-embeddings, ai/test-tags, admin/randomize-labels Removed unused npm packages: - cmdk, novel, tippy.js, react-force-graph-2d Cleaned dead CSS from globals.css: - acrylic-*, win11-shadow-*, muuri-grid/item, ai-glass, ai-tab-indicator, ai-send-btn, sidebar-view-toggle, memento-sidebar-depth Removed 29 orphan scripts and 3 root orphan files Cleaned dead exports from 8 lib files: - NOTE_TYPE_CONFIG, getPublishableKey, PROVIDER_DEFAULTS, useNotes/useNote/invalidateNote, etc.
This commit is contained in:
@@ -1,386 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { BrainstormSession, BrainstormIdea } from '@/types/brainstorm'
|
||||
import {
|
||||
useExpandIdea,
|
||||
useDismissIdea,
|
||||
useConvertIdea,
|
||||
useExportBrainstorm,
|
||||
brainstormQuotaMessageKey,
|
||||
} from '@/hooks/use-brainstorm'
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Sparkles, X, FileText, Download } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
interface GraphNode {
|
||||
id: string
|
||||
name: string
|
||||
val: number
|
||||
color: string
|
||||
borderColor: string
|
||||
wave: number
|
||||
status: string
|
||||
idea: BrainstormIdea
|
||||
x?: number
|
||||
y?: number
|
||||
__bckgDimensions?: [number, number]
|
||||
}
|
||||
|
||||
interface GraphLink {
|
||||
source: string | GraphNode
|
||||
target: string | GraphNode
|
||||
color: string
|
||||
}
|
||||
|
||||
const WAVE_COLORS: Record<number, string> = {
|
||||
0: '#ffffff',
|
||||
1: '#f97316',
|
||||
2: '#3b82f6',
|
||||
3: '#a855f7',
|
||||
}
|
||||
|
||||
const WAVE_BORDER: Record<number, string> = {
|
||||
0: '#e5e5e5',
|
||||
1: '#ea580c',
|
||||
2: '#2563eb',
|
||||
3: '#9333ea',
|
||||
}
|
||||
|
||||
const STATUS_ALPHA: Record<string, number> = {
|
||||
active: 1,
|
||||
dismissed: 0.25,
|
||||
converted: 0.9,
|
||||
}
|
||||
|
||||
interface BrainstormCanvasProps {
|
||||
session: BrainstormSession
|
||||
}
|
||||
|
||||
export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
|
||||
const { t } = useLanguage()
|
||||
const fgRef = useRef<any>(null)
|
||||
const [selectedIdea, setSelectedIdea] = useState<BrainstormIdea | null>(null)
|
||||
const [isSheetOpen, setIsSheetOpen] = useState(false)
|
||||
const expandIdea = useExpandIdea(session.id)
|
||||
const dismissIdea = useDismissIdea(session.id)
|
||||
const convertIdea = useConvertIdea(session.id)
|
||||
const exportBrainstorm = useExportBrainstorm(session.id)
|
||||
|
||||
const { graphData } = useMemo(() => {
|
||||
const nodes: GraphNode[] = []
|
||||
const links: GraphLink[] = []
|
||||
|
||||
nodes.push({
|
||||
id: 'seed',
|
||||
name: session.seedIdea.length > 30 ? session.seedIdea.slice(0, 30) + '...' : session.seedIdea,
|
||||
val: 25,
|
||||
color: WAVE_COLORS[0],
|
||||
borderColor: WAVE_BORDER[0],
|
||||
wave: 0,
|
||||
status: 'active',
|
||||
idea: {
|
||||
id: 'seed',
|
||||
sessionId: session.id,
|
||||
waveNumber: 0,
|
||||
title: session.seedIdea,
|
||||
description: t('brainstorm.originalSeedDescription'),
|
||||
connectionToSeed: null,
|
||||
noveltyScore: null,
|
||||
parentIdeaId: null,
|
||||
convertedToNoteId: null,
|
||||
relatedNoteIds: null,
|
||||
status: 'active',
|
||||
positionX: null,
|
||||
positionY: null,
|
||||
createdAt: session.createdAt,
|
||||
} as BrainstormIdea,
|
||||
})
|
||||
|
||||
for (const idea of session.ideas) {
|
||||
const parentNode = idea.parentIdeaId
|
||||
? idea.parentIdeaId
|
||||
: 'seed'
|
||||
|
||||
nodes.push({
|
||||
id: idea.id,
|
||||
name: idea.title,
|
||||
val: idea.status === 'dismissed' ? 8 : 15,
|
||||
color: WAVE_COLORS[idea.waveNumber] || WAVE_COLORS[3],
|
||||
borderColor: idea.status === 'converted' ? '#22c55e' : (WAVE_BORDER[idea.waveNumber] || WAVE_BORDER[3]),
|
||||
wave: idea.waveNumber,
|
||||
status: idea.status,
|
||||
idea,
|
||||
})
|
||||
|
||||
const linkColor = WAVE_COLORS[idea.waveNumber] || WAVE_COLORS[3]
|
||||
links.push({
|
||||
source: parentNode,
|
||||
target: idea.id,
|
||||
color: idea.status === 'dismissed' ? '#444444' : linkColor,
|
||||
})
|
||||
}
|
||||
|
||||
return { graphData: { nodes, links } }
|
||||
}, [session, t])
|
||||
|
||||
const handleNodeClick = useCallback((node: any) => {
|
||||
setSelectedIdea(node.idea)
|
||||
setIsSheetOpen(true)
|
||||
}, [])
|
||||
|
||||
const handleExpand = useCallback(async () => {
|
||||
if (!selectedIdea || selectedIdea.id === 'seed') return
|
||||
try {
|
||||
await expandIdea.mutateAsync({ ideaId: selectedIdea.id })
|
||||
toast.success(t('brainstorm.toastExpandSuccess'))
|
||||
} catch (err: unknown) {
|
||||
const quotaKey = brainstormQuotaMessageKey(err)
|
||||
toast.error(quotaKey ? t(quotaKey) : (err instanceof Error ? err.message : t('brainstorm.toastExpandFailed')))
|
||||
}
|
||||
}, [selectedIdea, expandIdea, t])
|
||||
|
||||
const handleDismiss = useCallback(async () => {
|
||||
if (!selectedIdea || selectedIdea.id === 'seed') return
|
||||
try {
|
||||
await dismissIdea.mutateAsync(selectedIdea.id)
|
||||
setIsSheetOpen(false)
|
||||
setSelectedIdea(null)
|
||||
toast.success(t('brainstorm.toastDismissSuccess'))
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || t('brainstorm.toastDismissFailed'))
|
||||
}
|
||||
}, [selectedIdea, dismissIdea, t])
|
||||
|
||||
const handleConvert = useCallback(async () => {
|
||||
if (!selectedIdea || selectedIdea.id === 'seed') return
|
||||
try {
|
||||
await convertIdea.mutateAsync(selectedIdea.id)
|
||||
toast.success(t('brainstorm.toastConvertSuccess'))
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || t('brainstorm.toastConvertFailed'))
|
||||
}
|
||||
}, [selectedIdea, convertIdea, t])
|
||||
|
||||
const handleExport = useCallback(async () => {
|
||||
try {
|
||||
const note = await exportBrainstorm.mutateAsync()
|
||||
toast.success(t('brainstorm.toastExportNoteSuccess'))
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || t('brainstorm.toastExportFailed'))
|
||||
}
|
||||
}, [exportBrainstorm, t])
|
||||
|
||||
const paintNode = useCallback((node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
|
||||
const label = node.name
|
||||
const fontSize = Math.max(12 / globalScale, 4)
|
||||
ctx.font = `${fontSize}px Sans-Serif`
|
||||
const textWidth = ctx.measureText(label).width
|
||||
const nodeSize = node.val
|
||||
|
||||
const bgDimensions: [number, number] = [textWidth + nodeSize * 0.8, nodeSize * 1.2]
|
||||
node.__bckgDimensions = bgDimensions
|
||||
|
||||
const alpha = STATUS_ALPHA[node.status] || 1
|
||||
ctx.globalAlpha = alpha
|
||||
|
||||
ctx.fillStyle = node.color
|
||||
ctx.strokeStyle = node.borderColor
|
||||
ctx.lineWidth = node.status === 'converted' ? 3 / globalScale : 1.5 / globalScale
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(
|
||||
node.x! - bgDimensions[0] / 2,
|
||||
node.y! - bgDimensions[1] / 2,
|
||||
bgDimensions[0],
|
||||
bgDimensions[1],
|
||||
nodeSize * 0.3
|
||||
)
|
||||
ctx.fill()
|
||||
ctx.stroke()
|
||||
|
||||
ctx.fillStyle = node.wave === 0 ? '#000000' : '#ffffff'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(label, node.x!, node.y!)
|
||||
|
||||
if (node.status === 'converted') {
|
||||
ctx.fillStyle = '#22c55e'
|
||||
ctx.font = `${fontSize * 1.2}px Sans-Serif`
|
||||
ctx.fillText('✓', node.x! + bgDimensions[0] / 2 - fontSize * 0.5, node.y! - bgDimensions[1] / 2 + fontSize * 0.5)
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
}, [])
|
||||
|
||||
const waveLegend = useMemo(
|
||||
() => [
|
||||
{ label: t('brainstorm.legendSeed'), color: WAVE_COLORS[0] },
|
||||
{ label: t('brainstorm.legendVariations'), color: WAVE_COLORS[1] },
|
||||
{ label: t('brainstorm.legendAnalogies'), color: WAVE_COLORS[2] },
|
||||
{ label: t('brainstorm.legendDisruptions'), color: WAVE_COLORS[3] },
|
||||
],
|
||||
[t]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full bg-zinc-950 rounded-xl overflow-hidden">
|
||||
<ForceGraph2D
|
||||
ref={fgRef}
|
||||
graphData={graphData}
|
||||
nodeCanvasObject={paintNode}
|
||||
nodePointerAreaPaint={(node: any, color: string, ctx: CanvasRenderingContext2D) => {
|
||||
const dims = node.__bckgDimensions
|
||||
if (!dims) return
|
||||
ctx.fillStyle = color
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(node.x! - dims[0] / 2, node.y! - dims[1] / 2, dims[0], dims[1], (node.val || 10) * 0.3)
|
||||
ctx.fill()
|
||||
}}
|
||||
onNodeClick={handleNodeClick}
|
||||
linkColor={(link: any) => link.color}
|
||||
linkWidth={1}
|
||||
linkDirectionalArrowLength={3}
|
||||
linkDirectionalArrowRelPos={1}
|
||||
backgroundColor="#09090b"
|
||||
nodeVal={(node: any) => node.val}
|
||||
cooldownTicks={100}
|
||||
enableNodeDrag={true}
|
||||
enableZoomInteraction={true}
|
||||
enablePanInteraction={true}
|
||||
warmupTicks={50}
|
||||
/>
|
||||
|
||||
<div className="absolute top-4 left-4 flex flex-col gap-1.5">
|
||||
{waveLegend.map(w => (
|
||||
<div key={w.label} className="flex items-center gap-2 text-xs text-zinc-400">
|
||||
<div className="w-3 h-3 rounded-sm border border-zinc-600" style={{ backgroundColor: w.color }} />
|
||||
<span>{w.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="absolute top-4 right-4">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="bg-zinc-800 border-zinc-700 text-zinc-300 hover:bg-zinc-700 hover:text-white"
|
||||
onClick={handleExport}
|
||||
disabled={exportBrainstorm.isPending}
|
||||
>
|
||||
<Download size={14} className="mr-1" />
|
||||
{exportBrainstorm.isPending ? t('brainstorm.exporting') : t('brainstorm.export')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
|
||||
<SheetContent className="bg-zinc-900 border-zinc-800 text-white w-96">
|
||||
{selectedIdea && (
|
||||
<>
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-white text-left">
|
||||
{selectedIdea.id === 'seed' ? session.seedIdea : selectedIdea.title}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="mt-6 space-y-4">
|
||||
{selectedIdea.description && selectedIdea.id !== 'seed' && (
|
||||
<div>
|
||||
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">{t('brainstorm.ideaDetailDescription')}</p>
|
||||
<p className="text-sm text-zinc-300">{selectedIdea.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedIdea.connectionToSeed && (
|
||||
<div>
|
||||
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">{t('brainstorm.ideaDetailConnection')}</p>
|
||||
<p className="text-sm text-zinc-300">{selectedIdea.connectionToSeed}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(selectedIdea.noveltyScore != null && selectedIdea.noveltyScore > 0) && (
|
||||
<div>
|
||||
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">{t('brainstorm.ideaDetailNovelty')}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-2 bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-orange-500 via-brand-accent to-purple-500 rounded-full"
|
||||
style={{ width: `${Math.min(100, Math.max(0, selectedIdea.noveltyScore * 10))}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-zinc-400">{selectedIdea.noveltyScore}/10</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedIdea.waveNumber > 0 && (
|
||||
<div>
|
||||
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">{t('brainstorm.ideaDetailWave')}</p>
|
||||
<span
|
||||
className="inline-block px-2 py-0.5 rounded text-xs font-medium"
|
||||
style={{
|
||||
backgroundColor: WAVE_COLORS[selectedIdea.waveNumber] + '30',
|
||||
color: WAVE_COLORS[selectedIdea.waveNumber],
|
||||
}}
|
||||
>
|
||||
{selectedIdea.waveNumber === 1
|
||||
? t('brainstorm.waveFlavorVariation')
|
||||
: selectedIdea.waveNumber === 2
|
||||
? t('brainstorm.waveFlavorAnalogy')
|
||||
: t('brainstorm.waveFlavorDisruption')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedIdea.status === 'converted' && (
|
||||
<div className="p-3 bg-green-500/10 border border-green-500/20 rounded-lg">
|
||||
<p className="text-xs text-green-400 flex items-center gap-1">
|
||||
<FileText size={12} />
|
||||
{t('brainstorm.convertedToNoteStatus')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedIdea.id !== 'seed' && selectedIdea.status === 'active' && (
|
||||
<div className="flex flex-col gap-2 pt-4 border-t border-zinc-800">
|
||||
<Button
|
||||
onClick={handleExpand}
|
||||
disabled={expandIdea.isPending}
|
||||
className="w-full bg-orange-600 hover:bg-orange-700 text-white"
|
||||
>
|
||||
<Sparkles size={14} className="mr-1" />
|
||||
{expandIdea.isPending ? t('brainstorm.deepening') : t('brainstorm.deepen')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConvert}
|
||||
disabled={convertIdea.isPending}
|
||||
className="w-full bg-brand-accent hover:bg-blue-700 text-white"
|
||||
>
|
||||
<FileText size={14} className="mr-1" />
|
||||
{convertIdea.isPending ? t('brainstorm.converting') : t('brainstorm.extract')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDismiss}
|
||||
disabled={dismissIdea.isPending}
|
||||
variant="outline"
|
||||
className="w-full border-zinc-700 text-zinc-400 hover:text-white hover:bg-zinc-800"
|
||||
>
|
||||
<X size={14} className="mr-1" />
|
||||
{t('brainstorm.dismiss')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
interface BrainstormCreateDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSubmit: (seedIdea: string) => void
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export function BrainstormCreateDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSubmit,
|
||||
isLoading,
|
||||
}: BrainstormCreateDialogProps) {
|
||||
const { t } = useLanguage()
|
||||
const [seedIdea, setSeedIdea] = useState('')
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!seedIdea.trim()) return
|
||||
onSubmit(seedIdea.trim())
|
||||
setSeedIdea('')
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md bg-white dark:bg-zinc-900 border-border">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Sparkles size={18} className="text-orange-500" />
|
||||
{t('brainstorm.newBrainstorm')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground uppercase tracking-wider mb-1 block">
|
||||
{t('brainstorm.seedLabel')}
|
||||
</label>
|
||||
<textarea
|
||||
value={seedIdea}
|
||||
onChange={(e) => setSeedIdea(e.target.value)}
|
||||
placeholder={t('brainstorm.ideaPromptDetailed')}
|
||||
className="w-full h-28 px-3 py-2 text-sm border border-border rounded-lg bg-transparent focus:outline-none focus:ring-2 focus:ring-orange-500/30 resize-none"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
{t('brainstorm.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!seedIdea.trim() || isLoading}
|
||||
className="bg-orange-600 hover:bg-orange-700 text-white"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Sparkles size={14} className="mr-1 animate-spin" />
|
||||
{t('brainstorm.generating')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles size={14} className="mr-1" />
|
||||
{t('brainstorm.startBrainstorm')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
|
||||
import { Lightbulb } from 'lucide-react'
|
||||
|
||||
interface ManualIdeaDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSubmit: (title: string, description?: string) => void
|
||||
isLoading?: boolean
|
||||
parentIdeaTitle?: string | null
|
||||
t: (key: string) => string | undefined
|
||||
}
|
||||
|
||||
export function ManualIdeaDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSubmit,
|
||||
isLoading,
|
||||
parentIdeaTitle,
|
||||
t,
|
||||
}: ManualIdeaDialogProps) {
|
||||
const [title, setTitle] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!title.trim()) return
|
||||
onSubmit(title.trim(), description.trim() || undefined)
|
||||
setTitle('')
|
||||
setDescription('')
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
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-brand-accent/10 flex items-center justify-center">
|
||||
<Lightbulb size={14} className="text-brand-accent" />
|
||||
</div>
|
||||
<span className="font-serif">{t('brainstorm.addIdea') || 'Add an idea'}</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground text-xs">
|
||||
{parentIdeaTitle
|
||||
? `${t('brainstorm.respondsTo') || 'Responds to'} « ${parentIdeaTitle.length > 40 ? parentIdeaTitle.substring(0, 40) + '…' : parentIdeaTitle} »`
|
||||
: (t('brainstorm.manualIdeaDesc') || 'Share your idea with the brainstorm canvas')
|
||||
}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} 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.manualIdeaTitle') || 'Title'}
|
||||
</label>
|
||||
<input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder={t('brainstorm.manualIdeaTitlePlaceholder') || 'Your idea in a few words...'}
|
||||
className="w-full px-4 py-3 text-sm border border-border rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-brand-accent/20 focus:border-brand-accent/40 transition-all font-serif"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground mb-1.5 block">
|
||||
{t('brainstorm.manualIdeaDescLabel') || 'Description (optional)'}
|
||||
</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder={t('brainstorm.manualIdeaDescPlaceholder') || 'Elaborate on your idea...'}
|
||||
className="w-full h-24 px-4 py-3 text-sm border border-border rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-brand-accent/20 focus:border-brand-accent/40 resize-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="px-4 py-2 text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground hover:text-foreground rounded-xl hover:bg-foreground/5 transition-all"
|
||||
>
|
||||
{t('brainstorm.cancel') || 'Cancel'}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!title.trim() || isLoading}
|
||||
className="px-5 py-2.5 bg-brand-accent hover:bg-brand-accent text-white text-[10px] font-bold uppercase tracking-[0.15em] rounded-xl disabled:opacity-50 transition-all flex items-center gap-1.5"
|
||||
>
|
||||
<Lightbulb size={12} />
|
||||
{isLoading ? (t('brainstorm.adding') || 'Adding...') : (t('brainstorm.addIdea') || 'Add idea')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user