feat: separate guides into system and custom sections

Add system/custom guide separation with section headers, lock icons,
delete functionality for custom guides, and usage counter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2026-02-08 12:51:44 +11:00
co-authored by Claude Opus 4.6
parent 7bf94d4c61
commit 549476274d
2 changed files with 160 additions and 37 deletions
+24 -1
View File
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { readGuide } from '@/services/guides/guideService';
import { readGuide, deleteGuide } from '@/services/guides/guideService';
export const runtime = 'nodejs';
@@ -25,3 +25,26 @@ export async function GET(
);
}
}
export async function DELETE(
_request: NextRequest,
{ params }: { params: Promise<{ name: string }> }
) {
try {
const { name } = await params;
const result = deleteGuide(name);
if (!result.success) {
return NextResponse.json(
{ success: false, error: result.error },
{ status: 400 }
);
}
return NextResponse.json({ success: true, message: `Guide "${name}" deleted` });
} catch (error) {
console.error('[API /guides/[name] DELETE] error:', error);
return NextResponse.json(
{ success: false, error: error instanceof Error ? error.message : 'Failed to delete guide' },
{ status: 500 }
);
}
}
+114 -14
View File
@@ -1,7 +1,7 @@
"use client";
import { useState, useEffect } from 'react';
import { ArrowLeft } from 'lucide-react';
import { ArrowLeft, Lock, Trash2 } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import PaneHeader from './PaneHeader';
@@ -10,6 +10,7 @@ import type { BasePaneProps } from './types';
interface GuideMeta {
name: string;
description: string;
immutable: boolean;
}
interface Guide extends GuideMeta {
@@ -27,6 +28,7 @@ export default function GuidesPane({
const [guides, setGuides] = useState<GuideMeta[]>([]);
const [selectedGuide, setSelectedGuide] = useState<Guide | null>(null);
const [loading, setLoading] = useState(true);
const [deleting, setDeleting] = useState<string | null>(null);
useEffect(() => {
fetchGuides();
@@ -62,10 +64,34 @@ export default function GuidesPane({
}
};
const handleDeleteGuide = async (name: string, e: React.MouseEvent) => {
e.stopPropagation();
if (!confirm(`Delete guide "${name}"?`)) return;
setDeleting(name);
try {
const res = await fetch(`/api/guides/${encodeURIComponent(name)}`, { method: 'DELETE' });
const data = await res.json();
if (data.success) {
fetchGuides();
if (selectedGuide?.name === name) {
setSelectedGuide(null);
}
}
} catch (err) {
console.error('[GuidesPane] Failed to delete guide:', err);
} finally {
setDeleting(null);
}
};
const handleBack = () => {
setSelectedGuide(null);
};
const systemGuides = guides.filter(g => g.immutable);
const userGuides = guides.filter(g => !g.immutable);
return (
<div style={{
height: '100%',
@@ -75,7 +101,7 @@ export default function GuidesPane({
overflow: 'hidden',
}}>
<PaneHeader slot={slot} onCollapse={onCollapse} onSwapPanes={onSwapPanes} tabBar={tabBar}>
{selectedGuide && (
{selectedGuide ? (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<button
onClick={handleBack}
@@ -94,10 +120,15 @@ export default function GuidesPane({
>
<ArrowLeft size={16} />
</button>
<span style={{ color: '#ccc', fontSize: '13px', fontWeight: 500 }}>
<span style={{ color: '#ccc', fontSize: '13px', fontWeight: 500, display: 'flex', alignItems: 'center', gap: '4px' }}>
{selectedGuide.immutable && <Lock size={12} style={{ color: '#22c55e' }} />}
{selectedGuide.name}
</span>
</div>
) : (
<span style={{ color: '#666', fontSize: '11px' }}>
{userGuides.length} of 10 custom guides
</span>
)}
</PaneHeader>
@@ -188,14 +219,62 @@ export default function GuidesPane({
No guides found
</div>
) : (
guides.map((guide) => (
<button
<>
{systemGuides.length > 0 && (
<div style={{ color: '#555', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.5px', padding: '4px 0' }}>
System Guides
</div>
)}
{systemGuides.map((guide) => (
<GuideCard
key={guide.name}
onClick={() => handleSelectGuide(guide.name)}
guide={guide}
onSelect={handleSelectGuide}
onDelete={handleDeleteGuide}
deleting={deleting}
/>
))}
{userGuides.length > 0 && (
<div style={{ color: '#555', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.5px', padding: '8px 0 4px 0' }}>
Custom Guides
</div>
)}
{userGuides.map((guide) => (
<GuideCard
key={guide.name}
guide={guide}
onSelect={handleSelectGuide}
onDelete={handleDeleteGuide}
deleting={deleting}
/>
))}
</>
)}
</div>
)}
</div>
</div>
);
}
function GuideCard({
guide,
onSelect,
onDelete,
deleting,
}: {
guide: GuideMeta;
onSelect: (name: string) => void;
onDelete: (name: string, e: React.MouseEvent) => void;
deleting: string | null;
}) {
return (
<button
onClick={() => onSelect(guide.name)}
style={{
display: 'flex',
flexDirection: 'column',
gap: '4px',
alignItems: 'center',
gap: '8px',
padding: '12px',
background: '#161616',
border: '1px solid #222',
@@ -213,18 +292,39 @@ export default function GuidesPane({
e.currentTarget.style.borderColor = '#222';
}}
>
{guide.immutable && (
<Lock size={12} style={{ color: '#22c55e', flexShrink: 0 }} />
)}
<div style={{ flex: 1, minWidth: 0 }}>
<span style={{ color: '#ddd', fontSize: '13px', fontWeight: 500 }}>
{guide.name}
</span>
<span style={{ color: '#777', fontSize: '12px', lineHeight: '1.4' }}>
<span style={{ color: '#777', fontSize: '12px', lineHeight: '1.4', display: 'block', marginTop: '2px' }}>
{guide.description}
</span>
</div>
{!guide.immutable && (
<button
onClick={(e) => onDelete(guide.name, e)}
disabled={deleting === guide.name}
style={{
background: 'none',
border: 'none',
color: '#555',
cursor: 'pointer',
padding: '4px',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
flexShrink: 0,
opacity: deleting === guide.name ? 0.3 : 1,
}}
onMouseEnter={e => { e.currentTarget.style.color = '#ef4444'; }}
onMouseLeave={e => { e.currentTarget.style.color = '#555'; }}
>
<Trash2 size={14} />
</button>
))
)}
</div>
)}
</div>
</div>
</button>
);
}