Files
HostKeeper/template/src/components/SnippetsView.tsx
T
2026-07-07 11:53:16 +07:00

351 lines
16 KiB
TypeScript

import React, { useState } from 'react';
import { Snippet } from '../types';
import { Search, Copy, Check, Terminal, Folder, Star, Clock, FolderGit, Cpu, Trash2, Plus, Zap, FileText } from 'lucide-react';
export default function SnippetsView() {
const [searchQuery, setSearchQuery] = useState('');
const [activeCollection, setActiveCollection] = useState<'all' | 'favorites' | 'recent'>('all');
const [activeTag, setActiveTag] = useState<string>('all');
const [copiedId, setCopiedId] = useState<string | null>(null);
// Snippets Initial Database State
const [snippets, setSnippets] = useState<Snippet[]>([
{
id: '1',
title: 'Docker Resource Prune Suite',
description: 'Force clean-up unused container images, volumes, logs, networks, and untagged builds safely.',
code: 'docker system prune -a --volumes --force',
tags: ['Docker', 'DevOps'],
collection: 'favorites',
updatedAt: '2 hours ago',
icon: 'Cpu'
},
{
id: '2',
title: 'Nginx TLS Reverse Proxy Template',
description: 'Standard secure production proxy server configuration block forwarding upstream headers.',
code: `server {
listen 443 ssl http2;
server_name api.domain.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}`,
tags: ['Nginx', 'SSL'],
collection: 'favorites',
updatedAt: 'Yesterday',
icon: 'Folder'
},
{
id: '3',
title: 'Git Force Fetch and Hard Reset',
description: 'Discards local changes completely and resets HEAD to track remote production origin main.',
code: 'git fetch origin && git reset --hard origin/main',
tags: ['Git', 'VCS'],
collection: 'recent',
updatedAt: '3 days ago',
icon: 'FolderGit'
},
{
id: '4',
title: 'Inspect SSL Certification Expiry',
description: 'Checks expiry metrics and issuer of ssl keys on active remote addresses.',
code: 'openssl s_client -connect google.com:443 | openssl x509 -noout -dates',
tags: ['SSL', 'Security'],
collection: 'recent',
updatedAt: 'Last week',
icon: 'Cpu'
}
]);
const [newTitle, setNewTitle] = useState('');
const [newDesc, setNewDesc] = useState('');
const [newCode, setNewCode] = useState('');
const [newTag, setNewTag] = useState('DevOps');
const [showAddForm, setShowAddForm] = useState(false);
const handleCopyCode = (id: string, code: string) => {
navigator.clipboard.writeText(code);
setCopiedId(id);
setTimeout(() => {
setCopiedId(null);
}, 2000);
};
const handleDeleteSnippet = (id: string) => {
setSnippets(prev => prev.filter(s => s.id !== id));
};
const handleCreateSnippet = (e: React.FormEvent) => {
e.preventDefault();
if (!newTitle || !newCode) return;
const added: Snippet = {
id: Date.now().toString(),
title: newTitle,
description: newDesc,
code: newCode,
tags: [newTag],
collection: 'recent',
updatedAt: 'Just now',
icon: 'FileText'
};
setSnippets([added, ...snippets]);
setShowAddForm(false);
setNewTitle('');
setNewDesc('');
setNewCode('');
};
const allTags = ['all', ...Array.from(new Set(snippets.flatMap(s => s.tags)))];
const filteredSnippets = snippets.filter(s => {
const matchesSearch = s.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.code.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCollection = activeCollection === 'all' || s.collection === activeCollection;
const matchesTag = activeTag === 'all' || s.tags.includes(activeTag);
return matchesSearch && matchesCollection && matchesTag;
});
return (
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-fade-in">
{/* Search Header Panel */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-outline-variant/30 pb-4">
<div>
<h2 className="text-xl font-bold text-on-surface tracking-tight">Snippets & Scripts Library</h2>
<p className="text-xs text-on-surface-variant mt-0.5">Quickly reuse and catalog command line shortcuts across remote clusters</p>
</div>
<div className="flex items-center gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-outline w-4 h-4" />
<input
type="text"
placeholder="Search code snippets..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-4 py-1.5 rounded-full bg-surface-container-low border border-outline-variant/30 text-sm focus:border-primary focus:bg-white focus:outline-none focus:ring-1 focus:ring-primary w-48 sm:w-60 transition-all"
/>
</div>
<button
onClick={() => setShowAddForm(true)}
className="flex items-center gap-1.5 bg-primary text-white hover:bg-primary-container hover:text-on-primary-container font-bold text-xs py-2 px-4 rounded-xl shadow-md transition-all cursor-pointer"
>
<Plus className="w-4 h-4" />
New Snippet
</button>
</div>
</div>
{/* Grid Architecture */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Left category rails filter */}
<div className="lg:col-span-3 space-y-5">
<div className="bg-white rounded-2xl border border-outline-variant p-4 space-y-4">
<h4 className="text-xs font-bold text-outline-variant uppercase tracking-wider px-2">Collections</h4>
<div className="space-y-1 text-sm font-semibold">
<button
onClick={() => setActiveCollection('all')}
className={`w-full flex items-center justify-between p-2 rounded-lg transition-colors ${activeCollection === 'all' ? 'bg-primary/10 text-primary' : 'text-on-surface-variant hover:bg-surface-container-low'}`}
>
<span className="flex items-center gap-2"><Folder className="w-4 h-4" /> All Code Blocks</span>
<span className="text-xs text-outline">{snippets.length}</span>
</button>
<button
onClick={() => setActiveCollection('favorites')}
className={`w-full flex items-center justify-between p-2 rounded-lg transition-colors ${activeCollection === 'favorites' ? 'bg-primary/10 text-primary' : 'text-on-surface-variant hover:bg-surface-container-low'}`}
>
<span className="flex items-center gap-2"><Star className="w-4 h-4 text-amber-500 fill-amber-500" /> Starred Favorites</span>
<span className="text-xs text-outline">{snippets.filter(s => s.collection === 'favorites').length}</span>
</button>
<button
onClick={() => setActiveCollection('recent')}
className={`w-full flex items-center justify-between p-2 rounded-lg transition-colors ${activeCollection === 'recent' ? 'bg-primary/10 text-primary' : 'text-on-surface-variant hover:bg-surface-container-low'}`}
>
<span className="flex items-center gap-2"><Clock className="w-4 h-4" /> Recent</span>
<span className="text-xs text-outline">{snippets.filter(s => s.collection === 'recent').length}</span>
</button>
</div>
</div>
<div className="bg-white rounded-2xl border border-outline-variant p-4 space-y-3">
<h4 className="text-xs font-bold text-outline-variant uppercase tracking-wider px-2">Tag Categories</h4>
<div className="flex flex-wrap gap-1.5 p-1">
{allTags.map(tag => (
<button
key={tag}
onClick={() => setActiveTag(tag)}
className={`px-3 py-1 rounded-full text-xs font-bold transition-all ${activeTag === tag ? 'bg-primary text-white' : 'bg-surface-container-low text-on-surface-variant hover:bg-surface-container'}`}
>
{tag === 'all' ? 'All Tags' : tag}
</button>
))}
</div>
</div>
</div>
{/* Right main snippets cards list */}
<div className="lg:col-span-9 space-y-6">
{filteredSnippets.length === 0 ? (
<div className="p-12 border border-dashed border-outline-variant rounded-2xl text-center text-outline">
No matching snippet found in {activeCollection} category.
</div>
) : (
filteredSnippets.map((snippet) => (
<div
key={snippet.id}
className="bg-white border border-outline-variant rounded-2xl overflow-hidden shadow-sm flex flex-col group"
>
<div className="p-5 flex justify-between items-start gap-4">
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<h3 className="font-bold text-on-surface text-base group-hover:text-primary transition-colors">{snippet.title}</h3>
<div className="flex flex-wrap gap-1">
{snippet.tags.map(t => (
<span key={t} className="px-2 py-0.5 bg-surface-container text-[10px] font-bold rounded text-on-surface-variant">
{t}
</span>
))}
</div>
</div>
<p className="text-xs text-on-surface-variant leading-relaxed">{snippet.description}</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<button
onClick={() => handleCopyCode(snippet.id, snippet.code)}
className="p-1.5 border border-outline-variant rounded-lg bg-surface-container-low hover:border-primary text-outline hover:text-primary transition-colors cursor-pointer"
title="Copy code to clipboard"
>
{copiedId === snippet.id ? <Check className="w-4 h-4 text-secondary" /> : <Copy className="w-4 h-4" />}
</button>
<button
onClick={() => handleDeleteSnippet(snippet.id)}
className="p-1.5 border border-outline-variant rounded-lg bg-surface-container-low hover:border-error text-outline hover:text-error transition-colors"
title="Delete code snippet"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
{/* Code syntax terminal container */}
<div className="bg-surface-container-low border-t border-outline-variant/40 p-4 font-mono text-xs text-on-surface relative group/code overflow-x-auto">
<pre className="text-on-surface select-all leading-relaxed whitespace-pre font-medium">{snippet.code}</pre>
<button
onClick={() => handleCopyCode(snippet.id, snippet.code)}
className="absolute right-4 top-4 bg-white/90 backdrop-blur border border-outline-variant shadow px-2.5 py-1 rounded text-[10px] font-mono font-bold hover:border-primary hover:text-primary transition-colors opacity-0 group-hover/code:opacity-100"
>
{copiedId === snippet.id ? 'COPIED ✓' : 'COPY'}
</button>
</div>
<div className="px-5 py-2 border-t border-outline-variant/20 bg-surface-container-lowest flex justify-between items-center text-[10px] text-outline font-semibold">
<span>Last adjusted {snippet.updatedAt}</span>
<span className="flex items-center gap-1 text-primary"><Zap className="w-3 h-3 fill-primary/10" /> Ready to inject to shell</span>
</div>
</div>
))
)}
</div>
</div>
{/* Add Snippet Modal Dialog */}
{showAddForm && (
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm z-[150] flex items-center justify-center p-4">
<div className="bg-white rounded-2xl max-w-lg w-full border border-outline-variant shadow-2xl p-6 space-y-4 animate-scale-up">
<div className="flex justify-between items-center border-b border-outline-variant/30 pb-3">
<h3 className="font-bold text-lg text-on-surface">Store Custom Snippet</h3>
<button
onClick={() => setShowAddForm(false)}
className="text-outline hover:text-primary cursor-pointer p-1 rounded"
>
Cancel
</button>
</div>
<form onSubmit={handleCreateSnippet} className="space-y-4 text-sm">
<div className="flex flex-col gap-1">
<label className="text-xs font-bold text-on-surface-variant">SNIPPET TITLE</label>
<input
type="text"
required
placeholder="e.g. Purge Docker Cache"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
className="w-full bg-surface-container-low border border-outline-variant/40 rounded-lg py-2.5 px-4 focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none transition-all font-medium"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-bold text-on-surface-variant">SHORT DESCRIPTION / USAGE</label>
<input
type="text"
placeholder="e.g. Safe cleanup to recover workspace disk capacity."
value={newDesc}
onChange={(e) => setNewDesc(e.target.value)}
className="w-full bg-surface-container-low border border-outline-variant/40 rounded-lg py-2.5 px-4 focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none transition-all font-medium"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-bold text-on-surface-variant">CODE SYNTAX BLOCK</label>
<textarea
required
rows={4}
placeholder="e.g. systemctl restart nginx"
value={newCode}
onChange={(e) => setNewCode(e.target.value)}
className="w-full bg-surface-container-low border border-outline-variant/40 rounded-lg py-2.5 px-4 focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none transition-all font-mono text-xs"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-bold text-on-surface-variant">PRIMARY TAG CATEGORY</label>
<select
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
className="bg-surface-container-low border border-outline-variant/40 rounded-lg py-2 px-3 focus:outline-none focus:border-primary"
>
<option value="DevOps">DevOps</option>
<option value="Docker">Docker</option>
<option value="Nginx">Nginx</option>
<option value="Git">Git</option>
<option value="SSL">SSL</option>
<option value="Security">Security</option>
<option value="Database">Database</option>
</select>
</div>
<div className="pt-4 flex justify-end gap-3 border-t border-outline-variant/20">
<button
type="button"
onClick={() => setShowAddForm(false)}
className="px-4 py-2 text-xs font-bold text-outline hover:text-on-surface"
>
Cancel
</button>
<button
type="submit"
className="bg-primary text-white py-2 px-5 rounded-lg font-bold text-xs shadow-md hover:bg-primary-container hover:text-on-primary-container transition-all"
>
Save Code Snippet
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}