diff --git a/src/hooks/useLocalStorage.js b/src/hooks/useLocalStorage.js index 1811d90..37c8caf 100644 --- a/src/hooks/useLocalStorage.js +++ b/src/hooks/useLocalStorage.js @@ -1,6 +1,6 @@ import { STORAGE_KEY, PREVIEW_ZOOM_KEY, FIELD_IDS, ZOOM_LEVELS } from '../utils/constants'; -export function saveData({ formData, sectionState, selectedTechs, selectedBadges, sectionOrder }) { +export function saveData({ formData, sectionState, selectedTechs, selectedBadges, sectionOrder, customSections }) { try { const data = { fields: { ...formData }, @@ -9,6 +9,7 @@ export function saveData({ formData, sectionState, selectedTechs, selectedBadges badges: Array.from(selectedBadges), sections: { ...sectionState }, sectionOrder: sectionOrder || [], + customSections: customSections || [], }; localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); return true; diff --git a/src/hooks/useReadmeState.js b/src/hooks/useReadmeState.js index ca5774d..82561c3 100644 --- a/src/hooks/useReadmeState.js +++ b/src/hooks/useReadmeState.js @@ -55,14 +55,15 @@ export function useReadmeState() { ); const [screenshots, setScreenshots] = useState([]); + const [customSections, setCustomSections] = useState(() => saved?.customSections || []); const [autoSaved, setAutoSaved] = useState(false); const saveTimerRef = useRef(null); const autoSaveTimerRef = useRef(null); - const scheduleSave = useCallback((fd, ss, st, sb, so) => { + const scheduleSave = useCallback((fd, ss, st, sb, so, cs) => { clearTimeout(saveTimerRef.current); saveTimerRef.current = setTimeout(() => { - saveData({ formData: fd, sectionState: ss, selectedTechs: st, selectedBadges: sb, sectionOrder: so }); + saveData({ formData: fd, sectionState: ss, selectedTechs: st, selectedBadges: sb, sectionOrder: so, customSections: cs }); setAutoSaved(true); clearTimeout(autoSaveTimerRef.current); autoSaveTimerRef.current = setTimeout(() => setAutoSaved(false), 2000); @@ -72,41 +73,41 @@ export function useReadmeState() { const updateField = useCallback((field, value) => { setFormData(prev => { const next = { ...prev, [field]: value }; - scheduleSave(next, sectionState, selectedTechs, selectedBadges, sectionOrder); + scheduleSave(next, sectionState, selectedTechs, selectedBadges, sectionOrder, customSections); return next; }); - }, [sectionState, selectedTechs, selectedBadges, sectionOrder, scheduleSave]); + }, [sectionState, selectedTechs, selectedBadges, sectionOrder, customSections, scheduleSave]); const toggleSection = useCallback((id, checked) => { setSectionState(prev => { const next = { ...prev, [id]: checked }; - scheduleSave(formData, next, selectedTechs, selectedBadges, sectionOrder); + scheduleSave(formData, next, selectedTechs, selectedBadges, sectionOrder, customSections); return next; }); - }, [formData, selectedTechs, selectedBadges, sectionOrder, scheduleSave]); + }, [formData, selectedTechs, selectedBadges, sectionOrder, customSections, scheduleSave]); const updateSectionOrder = useCallback((newOrder) => { setSectionOrder(newOrder); - scheduleSave(formData, sectionState, selectedTechs, selectedBadges, newOrder); - }, [formData, sectionState, selectedTechs, selectedBadges, scheduleSave]); + scheduleSave(formData, sectionState, selectedTechs, selectedBadges, newOrder, customSections); + }, [formData, sectionState, selectedTechs, selectedBadges, customSections, scheduleSave]); const toggleTech = useCallback((label) => { setSelectedTechs(prev => { const next = new Set(prev); if (next.has(label)) next.delete(label); else next.add(label); - scheduleSave(formData, sectionState, next, selectedBadges, sectionOrder); + scheduleSave(formData, sectionState, next, selectedBadges, sectionOrder, customSections); return next; }); - }, [formData, sectionState, selectedBadges, sectionOrder, scheduleSave]); + }, [formData, sectionState, selectedBadges, sectionOrder, customSections, scheduleSave]); const toggleBadge = useCallback((id) => { setSelectedBadges(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); - scheduleSave(formData, sectionState, selectedTechs, next, sectionOrder); + scheduleSave(formData, sectionState, selectedTechs, next, sectionOrder, customSections); return next; }); - }, [formData, sectionState, selectedTechs, sectionOrder, scheduleSave]); + }, [formData, sectionState, selectedTechs, sectionOrder, customSections, scheduleSave]); const applyTemplate = useCallback((template) => { setFormData(prev => { @@ -123,12 +124,12 @@ export function useReadmeState() { bibtexCitation: template.bibtexCitation || '', }; const nextSections = { ...sectionState, academic: !!template.abstractText }; - scheduleSave(next, nextSections, new Set(template.techs), selectedBadges, sectionOrder); + scheduleSave(next, nextSections, new Set(template.techs), selectedBadges, sectionOrder, customSections); return next; }); setSectionState(prev => ({ ...prev, academic: !!template.abstractText })); setSelectedTechs(new Set(template.techs)); - }, [sectionState, selectedBadges, sectionOrder, scheduleSave]); + }, [sectionState, selectedBadges, sectionOrder, customSections, scheduleSave]); const resetAll = useCallback(() => { setFormData(initialFormData); @@ -136,6 +137,7 @@ export function useReadmeState() { setSectionOrder(SECTIONS.map(sec => sec.id)); setSelectedTechs(new Set()); setSelectedBadges(new Set(DEFAULT_BADGES)); + setCustomSections([]); setScreenshots([]); clearData(); }, []); @@ -159,6 +161,37 @@ export function useReadmeState() { setScreenshots(prev => prev.filter((_, i) => i !== idx)); }, []); + const addCustomSection = useCallback(() => { + const newId = `custom-${Date.now()}`; + const newSection = { id: newId, title: 'New Section', content: '' }; + + setCustomSections(prev => { + const next = [...prev, newSection]; + scheduleSave(formData, { ...sectionState, [newId]: true }, selectedTechs, selectedBadges, [...sectionOrder, newId], next); + return next; + }); + setSectionOrder(prev => [...prev, newId]); + setSectionState(prev => ({ ...prev, [newId]: true })); + }, [formData, sectionState, selectedTechs, selectedBadges, sectionOrder, scheduleSave]); + + const updateCustomSection = useCallback((id, field, value) => { + setCustomSections(prev => { + const next = prev.map(sec => sec.id === id ? { ...sec, [field]: value } : sec); + scheduleSave(formData, sectionState, selectedTechs, selectedBadges, sectionOrder, next); + return next; + }); + }, [formData, sectionState, selectedTechs, selectedBadges, sectionOrder, scheduleSave]); + + const removeCustomSection = useCallback((id) => { + setCustomSections(prev => { + const next = prev.filter(sec => sec.id !== id); + const nextOrder = sectionOrder.filter(secId => secId !== id); + scheduleSave(formData, sectionState, selectedTechs, selectedBadges, nextOrder, next); + return next; + }); + setSectionOrder(prev => prev.filter(secId => secId !== id)); + }, [formData, sectionState, selectedTechs, selectedBadges, sectionOrder, scheduleSave]); + return { formData, updateField, sectionState, toggleSection, @@ -166,6 +199,7 @@ export function useReadmeState() { selectedTechs, toggleTech, selectedBadges, toggleBadge, screenshots, addScreenshots, removeScreenshot, + customSections, addCustomSection, updateCustomSection, removeCustomSection, applyTemplate, resetAll, clearSaved, autoSaved, }; diff --git a/src/pages/ReadmeMaker/EditorPanel.jsx b/src/pages/ReadmeMaker/EditorPanel.jsx index 4fe52ff..8588d61 100644 --- a/src/pages/ReadmeMaker/EditorPanel.jsx +++ b/src/pages/ReadmeMaker/EditorPanel.jsx @@ -34,6 +34,7 @@ export default function EditorPanel({ selectedTechs, toggleTech, selectedBadges, toggleBadge, screenshots, addScreenshots, removeScreenshot, + customSections, updateCustomSection, }) { const fileInputRef = useRef(null); const dropZoneRef = useRef(null); @@ -400,6 +401,23 @@ export default function EditorPanel({ + {customSections && customSections.map((sec, i) => ( + + + SECTION TITLE + updateCustomSection(sec.id, 'title', e.target.value)} /> + + + CONTENT (Markdown) + updateCustomSection(sec.id, 'content', e.target.value)} /> + + + + ))} + ); diff --git a/src/pages/ReadmeMaker/ReadmeMaker.jsx b/src/pages/ReadmeMaker/ReadmeMaker.jsx index 316c09b..d367944 100644 --- a/src/pages/ReadmeMaker/ReadmeMaker.jsx +++ b/src/pages/ReadmeMaker/ReadmeMaker.jsx @@ -20,6 +20,7 @@ export default function ReadmeMaker() { selectedTechs, toggleTech, selectedBadges, toggleBadge, screenshots, addScreenshots, removeScreenshot, + customSections, addCustomSection, updateCustomSection, removeCustomSection, applyTemplate, resetAll, clearSaved, autoSaved, } = useReadmeState(); @@ -27,8 +28,8 @@ export default function ReadmeMaker() { const [activeTemplate, setActiveTemplate] = useState(null); const currentMd = useMemo(() => - generateMarkdown({ formData, sectionState, selectedTechs, selectedBadges, screenshots, sectionOrder }), - [formData, sectionState, selectedTechs, selectedBadges, screenshots, sectionOrder] + generateMarkdown({ formData, sectionState, selectedTechs, selectedBadges, screenshots, sectionOrder, customSections }), + [formData, sectionState, selectedTechs, selectedBadges, screenshots, sectionOrder, customSections] ); const activeSectionCount = Object.values(sectionState).filter(Boolean).length; @@ -106,6 +107,9 @@ export default function ReadmeMaker() { updateSectionOrder={updateSectionOrder} selectedTechs={selectedTechs} toggleTech={toggleTech} + customSections={customSections} + addCustomSection={addCustomSection} + removeCustomSection={removeCustomSection} applyTemplate={handleApplyTemplate} activeTemplate={activeTemplate} /> @@ -120,6 +124,8 @@ export default function ReadmeMaker() { screenshots={screenshots} addScreenshots={addScreenshots} removeScreenshot={removeScreenshot} + customSections={customSections} + updateCustomSection={updateCustomSection} /> Sections (drag to reorder) {sectionOrder.map((id, idx) => { - const sec = SECTIONS.find(s => s.id === id); + let sec = SECTIONS.find(s => s.id === id); + let isCustom = false; + if (!sec && customSections) { + const cSec = customSections.find(c => c.id === id); + if (cSec) { + sec = { id: cSec.id, icon: '✏️', label: cSec.title || 'Custom Section' }; + isCustom = true; + } + } if (!sec) return null; return ( {sec.icon} {sec.label} - - toggleSection(sec.id, e.target.checked)} - /> - - + + {isCustom && ( + removeCustomSection(sec.id)} + title="Delete Custom Section" + >✕ + )} + + toggleSection(sec.id, e.target.checked)} + /> + + + ); })} + + + Add Custom Section + ); diff --git a/src/utils/markdownUtils.js b/src/utils/markdownUtils.js index feb852c..3a15278 100644 --- a/src/utils/markdownUtils.js +++ b/src/utils/markdownUtils.js @@ -89,7 +89,7 @@ export function md2html(md) { return h; } -export function generateMarkdown({ formData, sectionState, selectedTechs, selectedBadges, screenshots, sectionOrder }) { +export function generateMarkdown({ formData, sectionState, selectedTechs, selectedBadges, screenshots, sectionOrder, customSections = [] }) { const { projName, tagline, ghUser: ghUserRaw, repoSlug: repoSlugRaw, description, demoUrl, features, prereqs, installCmds, envVars, @@ -150,6 +150,13 @@ export function generateMarkdown({ formData, sectionState, selectedTechs, select else if (secId === 'contributing') chunk += '- [Contributing](#-contributing)\n'; else if (secId === 'author') chunk += '- [License](#-license)\n- [Author](#-author)\n'; else if (secId === 'support') chunk += '- [Support & Donation](#️-support--donation)\n'; + else if (secId.startsWith('custom-')) { + const cSec = customSections.find(c => c.id === secId); + if (cSec && cSec.title) { + const slug = cSec.title.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-'); + chunk += `- [${cSec.title}](#${slug})\n`; + } + } } }); chunk += '\n---\n\n'; @@ -315,8 +322,19 @@ export function generateMarkdown({ formData, sectionState, selectedTechs, select }; order.forEach(secId => { - if (on(secId) && generators[secId]) { - md += generators[secId](); + if (on(secId)) { + if (generators[secId]) { + md += generators[secId](); + } else if (secId.startsWith('custom-')) { + const cSec = customSections.find(c => c.id === secId); + if (cSec) { + md += `## ${cSec.title || 'Untitled'}\n\n`; + if (cSec.content) { + md += `${cSec.content}\n\n`; + } + md += '---\n\n'; + } + } } });