diff --git a/src/components/panes/SkillsPane.tsx b/src/components/panes/SkillsPane.tsx index 55a7677..849ddf0 100644 --- a/src/components/panes/SkillsPane.tsx +++ b/src/components/panes/SkillsPane.tsx @@ -1,11 +1,11 @@ "use client"; -import { useState, useEffect } from 'react'; -import { ArrowLeft, Trash2 } from 'lucide-react'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; +import { useEffect, useRef, useState } from 'react'; +import { ArrowLeft } from 'lucide-react'; import PaneHeader from './PaneHeader'; import type { BasePaneProps } from './types'; +import SkillCard from '@/components/skills/SkillCard'; +import SkillMarkdown from '@/components/skills/SkillMarkdown'; interface SkillMeta { name: string; @@ -27,12 +27,13 @@ export default function SkillsPane({ const [selectedSkill, setSelectedSkill] = useState(null); const [loading, setLoading] = useState(true); const [deleting, setDeleting] = useState(null); + const detailScrollRef = useRef(null); useEffect(() => { - fetchSkills(); + void fetchSkills(); const handleSkillUpdated = () => { - fetchSkills(); + void fetchSkills(); }; window.addEventListener('skills:updated', handleSkillUpdated); window.addEventListener('guides:updated', handleSkillUpdated); @@ -43,6 +44,12 @@ export default function SkillsPane({ }; }, []); + useEffect(() => { + if (selectedSkill && detailScrollRef.current) { + detailScrollRef.current.scrollTo({ top: 0, behavior: 'auto' }); + } + }, [selectedSkill?.name]); + const fetchSkills = async () => { try { const res = await fetch('/api/skills'); @@ -69,7 +76,7 @@ export default function SkillsPane({ } }; - const handleDeleteSkill = async (name: string, e: React.MouseEvent) => { + const handleDeleteSkill = async (name: string, e: React.MouseEvent) => { e.stopPropagation(); if (!confirm(`Delete skill "${name}"?`)) return; @@ -78,7 +85,7 @@ export default function SkillsPane({ const res = await fetch(`/api/skills/${encodeURIComponent(name)}`, { method: 'DELETE' }); const data = await res.json(); if (data.success) { - fetchSkills(); + await fetchSkills(); if (selectedSkill?.name === name) { setSelectedSkill(null); } @@ -104,6 +111,7 @@ export default function SkillsPane({ {selectedSkill ? (
@@ -131,12 +133,17 @@ export default function SkillsPane({ )} -
+
{loading ? (
Loading...
) : selectedSkill ? ( -
- {selectedSkill.content} +
+
+
{selectedSkill.name}
+
{selectedSkill.description}
+
+
+
) : (
@@ -146,69 +153,13 @@ export default function SkillsPane({
) : ( skills.map((skill) => ( - - + skill={skill} + onSelect={handleSelectSkill} + onDelete={handleDeleteSkill} + deleting={deleting} + /> )) )}
diff --git a/src/components/skills/SkillCard.tsx b/src/components/skills/SkillCard.tsx new file mode 100644 index 0000000..6d0d181 --- /dev/null +++ b/src/components/skills/SkillCard.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { Trash2 } from 'lucide-react'; + +interface SkillMeta { + name: string; + description: string; + immutable: boolean; +} + +interface SkillCardProps { + skill: SkillMeta; + onSelect: (name: string) => void; + onDelete?: (name: string, e: React.MouseEvent) => void; + deleting?: string | null; +} + +export default function SkillCard({ + skill, + onSelect, + onDelete, + deleting = null, +}: SkillCardProps) { + const isDeleting = deleting === skill.name; + + return ( +
{ + e.currentTarget.style.background = '#1a1a1a'; + e.currentTarget.style.borderColor = '#383838'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = '#161616'; + e.currentTarget.style.borderColor = '#222'; + }} + style={{ + display: 'flex', + alignItems: 'center', + gap: '10px', + padding: '10px', + background: '#161616', + border: '1px solid #222', + borderRadius: '8px', + }} + > + + {onDelete && !skill.immutable && ( + + )} +
+ ); +} diff --git a/src/components/skills/SkillMarkdown.tsx b/src/components/skills/SkillMarkdown.tsx new file mode 100644 index 0000000..66995f0 --- /dev/null +++ b/src/components/skills/SkillMarkdown.tsx @@ -0,0 +1,97 @@ +"use client"; + +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; + +export default function SkillMarkdown({ content }: { content: string }) { + return ( +
+ ( +

{children}

+ ), + h2: ({ children }) => ( +

{children}

+ ), + h3: ({ children }) => ( +

{children}

+ ), + p: ({ children }) => ( +

{children}

+ ), + ul: ({ children }) => ( +
    {children}
+ ), + ol: ({ children }) => ( +
    {children}
+ ), + li: ({ children }) => ( +
  • {children}
  • + ), + code: ({ className, children, ...props }) => { + const isInline = !className; + if (isInline) { + return ( + + {children} + + ); + } + return ( + + {children} + + ); + }, + pre: ({ children }) => ( +
    {children}
    + ), + strong: ({ children }) => ( + {children} + ), + hr: () => ( +
    + ), + blockquote: ({ children }) => ( +
    + {children} +
    + ), + }} + > + {content} +
    +
    + ); +}