Add BMAD framework, authentication, and new features
This commit is contained in:
@@ -17,10 +17,12 @@ import {
|
||||
Undo2,
|
||||
Redo2,
|
||||
FileText,
|
||||
Eye
|
||||
Eye,
|
||||
Link as LinkIcon
|
||||
} from 'lucide-react'
|
||||
import { createNote } from '@/app/actions/notes'
|
||||
import { CheckItem, NOTE_COLORS, NoteColor } from '@/lib/types'
|
||||
import { fetchLinkMetadata } from '@/app/actions/scrape'
|
||||
import { CheckItem, NOTE_COLORS, NoteColor, LinkMetadata } from '@/lib/types'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -67,6 +69,7 @@ export function NoteInput() {
|
||||
const [content, setContent] = useState('')
|
||||
const [checkItems, setCheckItems] = useState<CheckItem[]>([])
|
||||
const [images, setImages] = useState<string[]>([])
|
||||
const [links, setLinks] = useState<LinkMetadata[]>([])
|
||||
const [isMarkdown, setIsMarkdown] = useState(false)
|
||||
const [showMarkdownPreview, setShowMarkdownPreview] = useState(false)
|
||||
|
||||
@@ -77,6 +80,8 @@ export function NoteInput() {
|
||||
|
||||
// Reminder dialog
|
||||
const [showReminderDialog, setShowReminderDialog] = useState(false)
|
||||
const [showLinkDialog, setShowLinkDialog] = useState(false)
|
||||
const [linkUrl, setLinkUrl] = useState('')
|
||||
const [reminderDate, setReminderDate] = useState('')
|
||||
const [reminderTime, setReminderTime] = useState('')
|
||||
const [currentReminder, setCurrentReminder] = useState<Date | null>(null)
|
||||
@@ -148,7 +153,7 @@ export function NoteInput() {
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [isExpanded, historyIndex, history])
|
||||
|
||||
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files
|
||||
if (!files) return
|
||||
|
||||
@@ -156,32 +161,70 @@ export function NoteInput() {
|
||||
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
|
||||
const maxSize = 5 * 1024 * 1024 // 5MB
|
||||
|
||||
Array.from(files).forEach(file => {
|
||||
for (const file of Array.from(files)) {
|
||||
// Validation
|
||||
if (!validTypes.includes(file.type)) {
|
||||
addToast(`Invalid file type: ${file.name}. Only JPEG, PNG, GIF, and WebP allowed.`, 'error')
|
||||
return
|
||||
continue
|
||||
}
|
||||
|
||||
if (file.size > maxSize) {
|
||||
addToast(`File too large: ${file.name}. Maximum size is 5MB.`, 'error')
|
||||
return
|
||||
continue
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => {
|
||||
setImages([...images, reader.result as string])
|
||||
// Upload to server
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error('Upload failed')
|
||||
|
||||
const data = await response.json()
|
||||
setImages(prev => [...prev, data.url])
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error)
|
||||
addToast(`Failed to upload ${file.name}`, 'error')
|
||||
}
|
||||
reader.onerror = () => {
|
||||
addToast(`Failed to read file: ${file.name}`, 'error')
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
// Reset input
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
const handleAddLink = async () => {
|
||||
if (!linkUrl) return
|
||||
|
||||
// Optimistic add (or loading state)
|
||||
setShowLinkDialog(false)
|
||||
|
||||
try {
|
||||
const metadata = await fetchLinkMetadata(linkUrl)
|
||||
if (metadata) {
|
||||
setLinks(prev => [...prev, metadata])
|
||||
addToast('Link added', 'success')
|
||||
} else {
|
||||
addToast('Could not fetch link metadata', 'warning')
|
||||
// Fallback: just add the url as title
|
||||
setLinks(prev => [...prev, { url: linkUrl, title: linkUrl }])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to add link:', error)
|
||||
addToast('Failed to add link', 'error')
|
||||
} finally {
|
||||
setLinkUrl('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveLink = (index: number) => {
|
||||
setLinks(links.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const handleReminderOpen = () => {
|
||||
const tomorrow = new Date(Date.now() + 86400000)
|
||||
setReminderDate(tomorrow.toISOString().split('T')[0])
|
||||
@@ -240,6 +283,7 @@ export function NoteInput() {
|
||||
color,
|
||||
isArchived,
|
||||
images: images.length > 0 ? images : undefined,
|
||||
links: links.length > 0 ? links : undefined,
|
||||
reminder: currentReminder,
|
||||
isMarkdown,
|
||||
labels: selectedLabels.length > 0 ? selectedLabels : undefined,
|
||||
@@ -250,6 +294,7 @@ export function NoteInput() {
|
||||
setContent('')
|
||||
setCheckItems([])
|
||||
setImages([])
|
||||
setLinks([])
|
||||
setIsMarkdown(false)
|
||||
setShowMarkdownPreview(false)
|
||||
setHistory([{ title: '', content: '' }])
|
||||
@@ -292,6 +337,7 @@ export function NoteInput() {
|
||||
setContent('')
|
||||
setCheckItems([])
|
||||
setImages([])
|
||||
setLinks([])
|
||||
setIsMarkdown(false)
|
||||
setShowMarkdownPreview(false)
|
||||
setHistory([{ title: '', content: '' }])
|
||||
@@ -369,17 +415,48 @@ export function NoteInput() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Link Previews */}
|
||||
{links.length > 0 && (
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
{links.map((link, idx) => (
|
||||
<div key={idx} className="relative group border rounded-lg overflow-hidden bg-gray-50 dark:bg-zinc-800/50 flex">
|
||||
{link.imageUrl && (
|
||||
<div className="w-24 h-24 flex-shrink-0 bg-cover bg-center" style={{ backgroundImage: `url(${link.imageUrl})` }} />
|
||||
)}
|
||||
<div className="p-2 flex-1 min-w-0 flex flex-col justify-center">
|
||||
<h4 className="font-medium text-sm truncate">{link.title || link.url}</h4>
|
||||
{link.description && <p className="text-xs text-gray-500 truncate">{link.description}</p>}
|
||||
<a href={link.url} target="_blank" rel="noopener noreferrer" className="text-xs text-blue-500 truncate hover:underline block mt-1">
|
||||
{new URL(link.url).hostname}
|
||||
</a>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute top-1 right-1 h-6 w-6 p-0 bg-white/50 hover:bg-white opacity-0 group-hover:opacity-100 transition-opacity rounded-full"
|
||||
onClick={() => handleRemoveLink(idx)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === 'text' ? (
|
||||
<div className="space-y-2">
|
||||
{/* Labels selector */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<LabelSelector
|
||||
selectedLabels={selectedLabels}
|
||||
onLabelsChange={setSelectedLabels}
|
||||
triggerLabel="Tags"
|
||||
align="start"
|
||||
/>
|
||||
</div>
|
||||
{/* Selected Labels Display */}
|
||||
{selectedLabels.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{selectedLabels.map(label => (
|
||||
<LabelBadge
|
||||
key={label}
|
||||
label={label}
|
||||
onRemove={() => setSelectedLabels(prev => prev.filter(l => l !== label))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Markdown toggle button */}
|
||||
{isMarkdown && (
|
||||
@@ -519,6 +596,28 @@ export function NoteInput() {
|
||||
<TooltipContent>Collaborator</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => setShowLinkDialog(true)}
|
||||
title="Add Link"
|
||||
>
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Add Link</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<LabelSelector
|
||||
selectedLabels={selectedLabels}
|
||||
onLabelsChange={setSelectedLabels}
|
||||
triggerLabel=""
|
||||
align="start"
|
||||
/>
|
||||
|
||||
<DropdownMenu>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -676,6 +775,36 @@ export function NoteInput() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={showLinkDialog} onOpenChange={setShowLinkDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Link</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<Input
|
||||
placeholder="https://example.com"
|
||||
value={linkUrl}
|
||||
onChange={(e) => setLinkUrl(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleAddLink()
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => setShowLinkDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleAddLink}>
|
||||
Add
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user