From 1a1e3b027daa0c79821a62ac06b40355b3f97ef7 Mon Sep 17 00:00:00 2001 From: Vedhant26 Date: Mon, 25 May 2026 00:35:58 +0530 Subject: [PATCH] feat: Add Custom Section Builder for user defined sections (Fix #21) --- src/hooks/useLocalStorage.js | 3 +- src/hooks/useReadmeState.js | 62 +++++++++++++++++++++------ src/pages/ReadmeMaker/EditorPanel.jsx | 18 ++++++++ src/pages/ReadmeMaker/ReadmeMaker.jsx | 10 ++++- src/pages/ReadmeMaker/Sidebar.jsx | 44 +++++++++++++++---- src/utils/markdownUtils.js | 24 +++++++++-- 6 files changed, 132 insertions(+), 29 deletions(-) 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) => ( +