diff --git a/components/modules/snippet-manager-module.tsx b/components/modules/snippet-manager-module.tsx new file mode 100644 index 0000000..0b19c72 --- /dev/null +++ b/components/modules/snippet-manager-module.tsx @@ -0,0 +1,819 @@ +"use client"; + +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Textarea } from "@/components/ui/textarea"; +import { api } from "@/src/lib/trpc/client"; +import { + AlertCircle, + Check, + Clock, + Code2, + Copy, + Download, + Edit, + Plus, + Search, + Star, + Trash2, + Upload, + X, +} from "lucide-react"; +import { useTheme } from "next-themes"; +import { useState } from "react"; +import SyntaxHighlighter from "react-syntax-highlighter"; +import { + stackoverflowDark, + stackoverflowLight, +} from "react-syntax-highlighter/dist/esm/styles/hljs"; +import { toast } from "sonner"; +import { Module } from "../dashboard/module"; + +interface SnippetManagerModuleProps { + isPinned?: boolean; + onTogglePin?: () => void; +} + +interface SnippetFormData { + title: string; + description: string; + language: string; + code: string; + tags: string[]; + isFavorite: boolean; +} + +const COMMON_LANGUAGES = [ + "JavaScript", + "TypeScript", + "Python", + "Java", + "C++", + "C#", + "Go", + "Rust", + "Ruby", + "PHP", + "Swift", + "Kotlin", + "HTML", + "CSS", + "SQL", + "Bash", + "JSON", + "YAML", + "Markdown", + "Other", +]; + +// Map language names to Prism language identifiers +const getLanguageId = (language: string): string => { + const languageMap: Record = { + JavaScript: "javascript", + TypeScript: "typescript", + Python: "python", + Java: "java", + "C++": "cpp", + "C#": "csharp", + Go: "go", + Rust: "rust", + Ruby: "ruby", + PHP: "php", + Swift: "swift", + Kotlin: "kotlin", + HTML: "html", + CSS: "css", + SQL: "sql", + Bash: "bash", + JSON: "json", + YAML: "yaml", + Markdown: "markdown", + }; + return languageMap[language] || "text"; +}; + +export function SnippetManagerModule({ + isPinned, + onTogglePin, +}: SnippetManagerModuleProps) { + // Theme + const { theme } = useTheme(); + + // State + const [dialogOpen, setDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [editingSnippet, setEditingSnippet] = useState(null); + const [snippetToDelete, setSnippetToDelete] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [languageFilter, setLanguageFilter] = useState(""); + const [tagFilter, setTagFilter] = useState(""); + const [favoritesOnly, setFavoritesOnly] = useState(false); + const [copiedId, setCopiedId] = useState(null); + const [newTag, setNewTag] = useState(""); + + // Form state + const [formData, setFormData] = useState({ + title: "", + description: "", + language: "", + code: "", + tags: [], + isFavorite: false, + }); + + // Queries + const { data: snippets, isLoading } = api.snippets.getAll.useQuery({ + search: searchQuery || undefined, + language: languageFilter || undefined, + tag: tagFilter || undefined, + favoritesOnly, + }); + + const { data: recentlyUsed } = api.snippets.getRecentlyUsed.useQuery({ + limit: 5, + }); + + const { data: languages } = api.snippets.getLanguages.useQuery(); + const { data: tags } = api.snippets.getTags.useQuery(); + + // Mutations + const utils = api.useUtils(); + + const createMutation = api.snippets.create.useMutation({ + onSuccess: () => { + toast.success("Snippet created successfully"); + utils.snippets.getAll.invalidate(); + utils.snippets.getLanguages.invalidate(); + utils.snippets.getTags.invalidate(); + handleCloseDialog(); + }, + onError: (error) => { + toast.error(error.message || "Failed to create snippet"); + }, + }); + + const updateMutation = api.snippets.update.useMutation({ + onSuccess: () => { + toast.success("Snippet updated successfully"); + utils.snippets.getAll.invalidate(); + utils.snippets.getLanguages.invalidate(); + utils.snippets.getTags.invalidate(); + handleCloseDialog(); + }, + onError: (error) => { + toast.error(error.message || "Failed to update snippet"); + }, + }); + + const deleteMutation = api.snippets.delete.useMutation({ + onSuccess: () => { + toast.success("Snippet deleted successfully"); + utils.snippets.getAll.invalidate(); + utils.snippets.getLanguages.invalidate(); + utils.snippets.getTags.invalidate(); + setDeleteDialogOpen(false); + setSnippetToDelete(null); + }, + onError: (error) => { + toast.error(error.message || "Failed to delete snippet"); + }, + }); + + const toggleFavoriteMutation = api.snippets.toggleFavorite.useMutation({ + onSuccess: () => { + utils.snippets.getAll.invalidate(); + }, + onError: (error) => { + toast.error(error.message || "Failed to toggle favorite"); + }, + }); + + const markAsUsedMutation = api.snippets.markAsUsed.useMutation({ + onSuccess: () => { + utils.snippets.getRecentlyUsed.invalidate(); + }, + }); + + const importSnippetsMutation = api.snippets.importSnippets.useMutation({ + onSuccess: (data) => { + toast.success(`Imported ${data.count} snippets successfully`); + utils.snippets.getAll.invalidate(); + utils.snippets.getLanguages.invalidate(); + utils.snippets.getTags.invalidate(); + }, + onError: (error) => { + toast.error(error.message || "Failed to import snippets"); + }, + }); + + // Handlers + const handleOpenDialog = (snippetId?: string) => { + if (snippetId) { + const snippet = snippets?.find((s) => s.id === snippetId); + if (snippet) { + setFormData({ + title: snippet.title, + description: snippet.description || "", + language: snippet.language, + code: snippet.code, + tags: snippet.tags, + isFavorite: snippet.isFavorite, + }); + setEditingSnippet(snippetId); + } + } else { + setFormData({ + title: "", + description: "", + language: "", + code: "", + tags: [], + isFavorite: false, + }); + setEditingSnippet(null); + } + setDialogOpen(true); + }; + + const handleCloseDialog = () => { + setDialogOpen(false); + setEditingSnippet(null); + setFormData({ + title: "", + description: "", + language: "", + code: "", + tags: [], + isFavorite: false, + }); + setNewTag(""); + }; + + const handleSubmit = () => { + if (!formData.title.trim()) { + toast.error("Title is required"); + return; + } + if (!formData.language) { + toast.error("Language is required"); + return; + } + if (!formData.code.trim()) { + toast.error("Code is required"); + return; + } + + if (editingSnippet) { + updateMutation.mutate({ + id: editingSnippet, + data: formData, + }); + } else { + createMutation.mutate(formData); + } + }; + + const handleDeleteClick = (id: string) => { + setSnippetToDelete(id); + setDeleteDialogOpen(true); + }; + + const handleDeleteConfirm = () => { + if (snippetToDelete) { + deleteMutation.mutate({ id: snippetToDelete }); + } + }; + + const handleCopy = async (code: string, id: string) => { + await navigator.clipboard.writeText(code); + markAsUsedMutation.mutate({ id }); + setCopiedId(id); + toast.success("Code copied to clipboard"); + setTimeout(() => setCopiedId(null), 2000); + }; + + const handleToggleFavorite = (id: string) => { + toggleFavoriteMutation.mutate({ id }); + }; + + const handleExport = async () => { + try { + const data = await utils.snippets.exportAll.fetch(); + const jsonString = JSON.stringify(data, null, 2); + const blob = new Blob([jsonString], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `snippets-export-${ + new Date().toISOString().split("T")[0] + }.json`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + toast.success("Snippets exported successfully"); + } catch (error) { + toast.error("Failed to export snippets"); + } + }; + + const handleImport = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + try { + const text = await file.text(); + const data = JSON.parse(text); + + if (!Array.isArray(data)) { + toast.error("Invalid JSON format. Expected an array of snippets."); + return; + } + + importSnippetsMutation.mutate({ snippets: data }); + } catch (error) { + toast.error("Failed to parse JSON file"); + } + + event.target.value = ""; + }; + + const handleAddTag = () => { + if (newTag.trim() && !formData.tags.includes(newTag.trim())) { + setFormData({ + ...formData, + tags: [...formData.tags, newTag.trim()], + }); + setNewTag(""); + } + }; + + const handleRemoveTag = (tag: string) => { + setFormData({ + ...formData, + tags: formData.tags.filter((t) => t !== tag), + }); + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + handleAddTag(); + } + }; + + return ( + } + isPinned={isPinned} + onTogglePin={onTogglePin} + > +
+ {/* Header Actions */} +
+ + +
+ + + +
+
+ + {/* Filters */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-9 border-primary/50" + /> +
+ +
+
+ + + +
+ + {(languageFilter || tagFilter) && ( + + )} +
+ + +
+ + + + {/* Recently Used Section */} + {recentlyUsed && recentlyUsed.length > 0 && ( +
+
+ +

+ Recently Used +

+
+
+ {recentlyUsed.map((snippet) => ( + + ))} +
+ +
+ )} + + {/* Snippets List */} +
+ {isLoading ? ( +
+ Loading snippets... +
+ ) : snippets && snippets.length > 0 ? ( + snippets.map((snippet) => ( +
+
+
+
+

+ {snippet.title} +

+ +
+ {snippet.description && ( +

+ {snippet.description} +

+ )} +
+
+ + + +
+
+ +
+ + {snippet.code} + +
+ +
+ + {snippet.language} + + {snippet.tags.map((tag) => ( + + {tag} + + ))} +
+
+ )) + ) : ( + + + + No snippets found. Create your first snippet to get started! + + + )} +
+ + {/* Create/Edit Dialog */} + + + + + {editingSnippet ? "Edit Snippet" : "New Snippet"} + + + {editingSnippet + ? "Update your code snippet" + : "Create a new code snippet"} + + + +
+
+ + + setFormData({ ...formData, title: e.target.value }) + } + placeholder="e.g., Fetch with error handling" + /> +
+ +
+ + + setFormData({ ...formData, description: e.target.value }) + } + placeholder="Optional description of your snippet" + /> +
+ +
+ + +
+ +
+ +