diff --git a/app/snippets/page.tsx b/app/snippets/page.tsx index 62e30ce..bdf3f38 100644 --- a/app/snippets/page.tsx +++ b/app/snippets/page.tsx @@ -1,7 +1,6 @@ "use client"; -import React from "react"; -import { useState, useEffect } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -14,12 +13,13 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Trash2, Copy, Plus } from "lucide-react"; +import { Trash2, Copy, Plus, Search, X } from "lucide-react"; import { Sidebar } from "@/components/Sidebar"; import Loader from "@/components/ui/loader"; import { VersionHistoryPanel } from "@/components/VersionHistory"; import { PermissionsManager } from "@/components/PermissionsManager"; import { useWallet } from "@/components/WalletConnect"; +import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts"; const LANGUAGES = [ "javascript", @@ -60,24 +60,30 @@ interface PaginatedResponse { } const DEFAULT_LIMIT = 20; +const INITIAL_FORM_DATA = { + title: "", + description: "", + code: "", + language: "javascript", + tags: "", +}; export default function SnippetsPage() { const wallet = useWallet(); + const formRef = useRef(null); + const searchInputRef = useRef(null); + const titleInputRef = useRef(null); const [snippets, setSnippets] = useState([]); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [hasMore, setHasMore] = useState(true); const [total, setTotal] = useState(0); const [offset, setOffset] = useState(0); + const [searchQuery, setSearchQuery] = useState(""); + const [activeSearchQuery, setActiveSearchQuery] = useState(""); const [showForm, setShowForm] = useState(false); const [editingId, setEditingId] = useState(null); - const [formData, setFormData] = useState({ - title: "", - description: "", - code: "", - language: "javascript", - tags: "", - }); + const [formData, setFormData] = useState(INITIAL_FORM_DATA); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); @@ -85,7 +91,31 @@ export default function SnippetsPage() { fetchSnippets(); }, []); - const fetchSnippets = async (loadMore = false) => { + const buildSnippetHeaders = useCallback( + (includeJson = false) => { + const headers: Record = {}; + + if (includeJson) { + headers["Content-Type"] = "application/json"; + } + + if (wallet?.publicKey) { + headers["x-wallet-address"] = wallet.publicKey; + } + + if (wallet?.token) { + headers.Authorization = `Bearer ${wallet.token}`; + } + + return headers; + }, + [wallet?.publicKey, wallet?.token], + ); + + const fetchSnippets = async ( + loadMore = false, + query = activeSearchQuery, + ) => { try { if (loadMore) { setLoadingMore(true); @@ -94,7 +124,17 @@ export default function SnippetsPage() { } const currentOffset = loadMore ? offset : 0; - const res = await fetch(`/api/snippets?limit=${DEFAULT_LIMIT}&offset=${currentOffset}`); + const params = new URLSearchParams({ + limit: String(DEFAULT_LIMIT), + offset: String(currentOffset), + }); + const trimmedQuery = query.trim(); + + if (trimmedQuery) { + params.set("keyword", trimmedQuery); + } + + const res = await fetch(`/api/snippets?${params.toString()}`); if (!res.ok) throw new Error("Failed to fetch snippets"); @@ -123,6 +163,83 @@ export default function SnippetsPage() { } }; + const runSearch = async (query: string) => { + const trimmedQuery = query.trim(); + setActiveSearchQuery(trimmedQuery); + setOffset(0); + setHasMore(true); + await fetchSnippets(false, trimmedQuery); + }; + + const handleSearchSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + await runSearch(searchQuery); + }; + + const handleClearSearch = async () => { + setSearchQuery(""); + + if (activeSearchQuery) { + await runSearch(""); + } + + searchInputRef.current?.focus(); + }; + + const startCreateSnippet = useCallback(() => { + setEditingId(null); + setFormData({ ...INITIAL_FORM_DATA }); + setError(null); + setShowForm(true); + }, []); + + const focusSearch = useCallback(() => { + searchInputRef.current?.focus(); + searchInputRef.current?.select(); + }, []); + + const submitSnippetForm = useCallback(() => { + const form = formRef.current; + + if (!form || saving) return; + + if (typeof form.requestSubmit === "function") { + form.requestSubmit(); + return; + } + + form.dispatchEvent( + new Event("submit", { bubbles: true, cancelable: true }), + ); + }, [saving]); + + const shortcuts = useMemo( + () => [ + { + key: "s", + enabled: showForm, + onKeyDown: submitSnippetForm, + }, + { + key: "k", + onKeyDown: focusSearch, + }, + { + key: "n", + onKeyDown: startCreateSnippet, + }, + ], + [focusSearch, saving, showForm, startCreateSnippet, submitSnippetForm], + ); + + useKeyboardShortcuts(shortcuts); + + useEffect(() => { + if (showForm) { + titleInputRef.current?.focus(); + } + }, [editingId, showForm]); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setSaving(true); @@ -139,7 +256,7 @@ export default function SnippetsPage() { editingId ? `/api/snippets/${editingId}` : "/api/snippets", { method: editingId ? "PUT" : "POST", - headers: { "Content-Type": "application/json" }, + headers: buildSnippetHeaders(true), body: JSON.stringify(payload), }, ); @@ -173,7 +290,10 @@ export default function SnippetsPage() { const handleDelete = async (id: string) => { if (!confirm("Delete this snippet?")) return; try { - const res = await fetch(`/api/snippets/${id}`, { method: "DELETE" }); + const res = await fetch(`/api/snippets/${id}`, { + method: "DELETE", + headers: buildSnippetHeaders(), + }); if (!res.ok) throw new Error("Failed to delete"); // Reset pagination and fetch fresh data @@ -197,13 +317,7 @@ export default function SnippetsPage() { setShowForm(false); setEditingId(null); setError(null); - setFormData({ - title: "", - description: "", - code: "", - language: "javascript", - tags: "", - }); + setFormData({ ...INITIAL_FORM_DATA }); }; return ( @@ -222,7 +336,8 @@ export default function SnippetsPage() {

My Snippets

{!showForm && ( + {activeSearchQuery && ( + + )} + + + {/* Form */} {showForm && ( {editingId ? "Edit Snippet" : "Add New Snippet"} -
+ {error && (
{error} @@ -256,6 +413,7 @@ export default function SnippetsPage() { Title {saving ? ( @@ -414,7 +573,8 @@ transition-all duration-200"

{!showForm && (