diff --git a/src/app/api/capture/route.ts b/src/app/api/capture/route.ts new file mode 100644 index 0000000..f8ade0c --- /dev/null +++ b/src/app/api/capture/route.ts @@ -0,0 +1,25 @@ +import { NextRequest } from 'next/server' +import { z } from 'zod' +import { createSuccessResponse, createErrorResponse } from '@/lib/errors' + +const captureSchema = z.object({ + title: z.string().min(1).max(500), + url: z.string().url().optional(), + selection: z.string().optional(), + source: z.enum(['bookmarklet', 'extension', 'api']).default('api'), +}) + +export async function POST(req: NextRequest) { + try { + const body = await req.json() + const result = captureSchema.safeParse(body) + + if (!result.success) { + return createErrorResponse(result.error) + } + + return createSuccessResponse(result.data) + } catch (error) { + return createErrorResponse(error) + } +} diff --git a/src/app/capture/page.tsx b/src/app/capture/page.tsx new file mode 100644 index 0000000..c88626a --- /dev/null +++ b/src/app/capture/page.tsx @@ -0,0 +1,195 @@ +'use client' + +import { useState, useEffect, Suspense } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import { toast } from 'sonner' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' +import { Card } from '@/components/ui/card' +import { Loader2, Bookmark } from 'lucide-react' + +function CaptureForm() { + const router = useRouter() + const searchParams = useSearchParams() + const [title, setTitle] = useState('') + const [url, setUrl] = useState('') + const [selection, setSelection] = useState('') + const [content, setContent] = useState('') + const [tags, setTags] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [isDragging, setIsDragging] = useState(false) + + useEffect(() => { + const titleParam = searchParams.get('title') || '' + const urlParam = searchParams.get('url') || '' + const selectionParam = searchParams.get('selection') || '' + + setTitle(titleParam) + setUrl(urlParam) + setSelection(selectionParam) + + // Pre-fill content with captured web content + if (selectionParam) { + setContent(`## Web Selection\n\n${selectionParam}\n\n## Source\n\n${urlParam}`) + } else { + setContent(`## Source\n\n${urlParam}`) + } + }, [searchParams]) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!content.trim() || isLoading) return + + setIsLoading(true) + try { + // Build the full content with optional tags + const fullContent = tags.trim() + ? `${content}\n\n## Tags\n\n${tags.trim().split(',').map(t => `#${t.trim()}`).join(' ')}` + : content + + const response = await fetch('/api/notes/quick', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + text: `rec: ${title || url}\n\n${fullContent}`, + }), + }) + + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Error creating note') + } + + toast.success('Nota creada desde web', { + description: title || url, + }) + router.push('/notes') + router.refresh() + } catch (error) { + toast.error('Error', { + description: error instanceof Error ? error.message : 'No se pudo crear la nota', + }) + } finally { + setIsLoading(false) + } + } + + const bookmarkletCode = `javascript:var title = document.title; var url = location.href; var selection = window.getSelection().toString(); var params = new URLSearchParams({title, url, selection}); window.open('/capture?' + params.toString(), '_blank');` + + const handleDragStart = (e: React.DragEvent) => { + e.dataTransfer.setData('text/plain', bookmarkletCode) + e.dataTransfer.effectAllowed = 'copy' + setIsDragging(true) + } + + const handleDragEnd = () => { + setIsDragging(false) + } + + return ( +
Arrastra este botón a tu barra de marcadores:
+ +