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

@@ -1,12 +1,13 @@
'use client'
import { useState, useEffect, useRef } from 'react'
import { Note, CheckItem, NOTE_COLORS, NoteColor } from '@/lib/types'
import { useState, useRef } from 'react'
import { Note, CheckItem, NOTE_COLORS, NoteColor, LinkMetadata } from '@/lib/types'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
@@ -17,13 +18,16 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { X, Plus, Palette, Image as ImageIcon, Bell, FileText, Eye } from 'lucide-react'
import { X, Plus, Palette, Image as ImageIcon, Bell, FileText, Eye, Link as LinkIcon } from 'lucide-react'
import { updateNote } from '@/app/actions/notes'
import { fetchLinkMetadata } from '@/app/actions/scrape'
import { cn } from '@/lib/utils'
import { useToast } from '@/components/ui/toast'
import { MarkdownContent } from './markdown-content'
import { LabelManager } from './label-manager'
import { LabelBadge } from './label-badge'
import { ReminderDialog } from './reminder-dialog'
import { EditorImages } from './editor-images'
interface NoteEditorProps {
note: Note
@@ -37,6 +41,7 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
const [checkItems, setCheckItems] = useState<CheckItem[]>(note.checkItems || [])
const [labels, setLabels] = useState<string[]>(note.labels || [])
const [images, setImages] = useState<string[]>(note.images || [])
const [links, setLinks] = useState<LinkMetadata[]>(note.links || [])
const [newLabel, setNewLabel] = useState('')
const [color, setColor] = useState(note.color)
const [isSaving, setIsSaving] = useState(false)
@@ -46,69 +51,80 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
// Reminder state
const [showReminderDialog, setShowReminderDialog] = useState(false)
const [reminderDate, setReminderDate] = useState('')
const [reminderTime, setReminderTime] = useState('')
const [currentReminder, setCurrentReminder] = useState<Date | null>(note.reminder)
// Link state
const [showLinkDialog, setShowLinkDialog] = useState(false)
const [linkUrl, setLinkUrl] = useState('')
const colorClasses = NOTE_COLORS[color as NoteColor] || NOTE_COLORS.default
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files) return
Array.from(files).forEach(file => {
const reader = new FileReader()
reader.onloadend = () => {
setImages(prev => [...prev, reader.result as string])
for (const file of Array.from(files)) {
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.readAsDataURL(file)
})
}
}
const handleRemoveImage = (index: number) => {
setImages(images.filter((_, i) => i !== index))
}
const handleReminderOpen = () => {
if (currentReminder) {
const date = new Date(currentReminder)
setReminderDate(date.toISOString().split('T')[0])
setReminderTime(date.toTimeString().slice(0, 5))
} else {
const tomorrow = new Date(Date.now() + 86400000)
setReminderDate(tomorrow.toISOString().split('T')[0])
setReminderTime('09:00')
const handleAddLink = async () => {
if (!linkUrl) return
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')
setLinks(prev => [...prev, { url: linkUrl, title: linkUrl }])
}
} catch (error) {
console.error('Failed to add link:', error)
addToast('Failed to add link', 'error')
} finally {
setLinkUrl('')
}
setShowReminderDialog(true)
}
const handleReminderSave = () => {
if (!reminderDate || !reminderTime) {
addToast('Please enter date and time', 'warning')
return
}
const dateTimeString = `${reminderDate}T${reminderTime}`
const date = new Date(dateTimeString)
if (isNaN(date.getTime())) {
addToast('Invalid date or time', 'error')
return
}
const handleRemoveLink = (index: number) => {
setLinks(links.filter((_, i) => i !== index))
}
const handleReminderSave = (date: Date) => {
if (date < new Date()) {
addToast('Reminder must be in the future', 'error')
return
}
setCurrentReminder(date)
addToast(`Reminder set for ${date.toLocaleString()}`, 'success')
setShowReminderDialog(false)
}
const handleRemoveReminder = () => {
setCurrentReminder(null)
setShowReminderDialog(false)
addToast('Reminder removed', 'success')
}
@@ -121,6 +137,7 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
checkItems: note.type === 'checklist' ? checkItems : null,
labels,
images,
links,
color,
reminder: currentReminder,
isMarkdown,
@@ -158,13 +175,6 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
setCheckItems(items => items.filter(item => item.id !== id))
}
const handleAddLabel = () => {
if (newLabel.trim() && !labels.includes(newLabel.trim())) {
setLabels([...labels, newLabel.trim()])
setNewLabel('')
}
}
const handleRemoveLabel = (label: string) => {
setLabels(labels.filter(l => l !== label))
}
@@ -191,22 +201,30 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
/>
{/* Images */}
{images.length > 0 && (
<div className="flex flex-col gap-3 mb-4">
{images.map((img, idx) => (
<div key={idx} className="relative group">
<img
src={img}
alt=""
className="h-auto rounded-lg"
/>
<EditorImages images={images} onRemove={handleRemoveImage} />
{/* Link Previews */}
{links.length > 0 && (
<div className="flex flex-col gap-2">
{links.map((link, idx) => (
<div key={idx} className="relative group border rounded-lg overflow-hidden bg-white/50 dark:bg-black/20 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-2 right-2 h-7 w-7 p-0 bg-white/90 hover:bg-white opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => handleRemoveImage(idx)}
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-4 w-4" />
<X className="h-3 w-3" />
</Button>
</div>
))}
@@ -324,7 +342,7 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
<Button
variant="ghost"
size="sm"
onClick={handleReminderOpen}
onClick={() => setShowReminderDialog(true)}
title="Set reminder"
className={currentReminder ? "text-blue-600" : ""}
>
@@ -341,6 +359,16 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
<ImageIcon className="h-4 w-4" />
</Button>
{/* Add Link Button */}
<Button
variant="ghost"
size="sm"
onClick={() => setShowLinkDialog(true)}
title="Add Link"
>
<LinkIcon className="h-4 w-4" />
</Button>
{/* Color Picker */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -393,57 +421,84 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
/>
</DialogContent>
{/* Reminder Dialog */}
<Dialog open={showReminderDialog} onOpenChange={setShowReminderDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Set Reminder</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label htmlFor="reminder-date" className="text-sm font-medium">
Date
</label>
<Input
id="reminder-date"
type="date"
value={reminderDate}
onChange={(e) => setReminderDate(e.target.value)}
className="w-full"
/>
</div>
<div className="space-y-2">
<label htmlFor="reminder-time" className="text-sm font-medium">
Time
</label>
<Input
id="reminder-time"
type="time"
value={reminderTime}
onChange={(e) => setReminderTime(e.target.value)}
className="w-full"
/>
</div>
</div>
<div className="flex justify-between">
<div>
{currentReminder && (
<Button variant="outline" onClick={handleRemoveReminder}>
Remove Reminder
</Button>
)}
</div>
<div className="flex gap-2">
<Button variant="ghost" onClick={() => setShowReminderDialog(false)}>
Cancel
</Button>
<Button onClick={handleReminderSave}>
Set Reminder
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</Dialog>
)
}
<ReminderDialog
open={showReminderDialog}
onOpenChange={setShowReminderDialog}
currentReminder={currentReminder}
onSave={handleReminderSave}
onRemove={handleRemoveReminder}
/>
<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>
</Dialog>
)
}