Add BMAD framework, authentication, and new features

This commit is contained in:
2026-01-08 21:23:23 +01:00
parent f07d28aefd
commit 15a95fb319
1298 changed files with 73308 additions and 154901 deletions

View File

@@ -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>
</>
)
}