Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/hooks/useLocalStorage.js
Original file line number Diff line number Diff line change
@@ -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 },
Expand All @@ -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;
Expand Down
62 changes: 48 additions & 14 deletions src/hooks/useReadmeState.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 => {
Expand All @@ -123,19 +124,20 @@ 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);
setSectionState(buildDefaultSectionState());
setSectionOrder(SECTIONS.map(sec => sec.id));
setSelectedTechs(new Set());
setSelectedBadges(new Set(DEFAULT_BADGES));
setCustomSections([]);
setScreenshots([]);
clearData();
}, []);
Expand All @@ -159,13 +161,45 @@ 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,
sectionOrder, updateSectionOrder,
selectedTechs, toggleTech,
selectedBadges, toggleBadge,
screenshots, addScreenshots, removeScreenshot,
customSections, addCustomSection, updateCustomSection, removeCustomSection,
applyTemplate, resetAll, clearSaved,
autoSaved,
};
Expand Down
18 changes: 18 additions & 0 deletions src/pages/ReadmeMaker/EditorPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -400,6 +401,23 @@ export default function EditorPanel({
</div>
</EditorSection>

{customSections && customSections.map((sec, i) => (
<EditorSection key={sec.id} num={`C${i+1}`} title={`Custom Section: ${sec.title || 'Untitled'}`} hidden={!sectionState[sec.id]}>
<div>
<label>SECTION TITLE</label>
<input type="text" placeholder="My Custom Section"
value={sec.title} onChange={e => updateCustomSection(sec.id, 'title', e.target.value)} />
</div>
<div>
<label>CONTENT (Markdown)</label>
<textarea className="textInput" style={{ minHeight: 120 }}
placeholder="Write your custom markdown content here..."
value={sec.content} onChange={e => updateCustomSection(sec.id, 'content', e.target.value)} />
<WordCount text={sec.content} />
</div>
</EditorSection>
))}

</div>
</div>
);
Expand Down
10 changes: 8 additions & 2 deletions src/pages/ReadmeMaker/ReadmeMaker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@ export default function ReadmeMaker() {
selectedTechs, toggleTech,
selectedBadges, toggleBadge,
screenshots, addScreenshots, removeScreenshot,
customSections, addCustomSection, updateCustomSection, removeCustomSection,
applyTemplate, resetAll, clearSaved,
autoSaved,
} = useReadmeState();

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;
Expand Down Expand Up @@ -106,6 +107,9 @@ export default function ReadmeMaker() {
updateSectionOrder={updateSectionOrder}
selectedTechs={selectedTechs}
toggleTech={toggleTech}
customSections={customSections}
addCustomSection={addCustomSection}
removeCustomSection={removeCustomSection}
applyTemplate={handleApplyTemplate}
activeTemplate={activeTemplate}
/>
Expand All @@ -120,6 +124,8 @@ export default function ReadmeMaker() {
screenshots={screenshots}
addScreenshots={addScreenshots}
removeScreenshot={removeScreenshot}
customSections={customSections}
updateCustomSection={updateCustomSection}
/>
<PreviewPanel
currentMd={currentMd}
Expand Down
44 changes: 35 additions & 9 deletions src/pages/ReadmeMaker/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default function Sidebar({
sectionState, toggleSection,
sectionOrder, updateSectionOrder,
selectedTechs, toggleTech,
customSections, addCustomSection, removeCustomSection,
applyTemplate, activeTemplate,
}) {
const [draggedIndex, setDraggedIndex] = useState(null);
Expand Down Expand Up @@ -61,7 +62,15 @@ export default function Sidebar({
<div className="sidebar-label">Sections <span style={{ fontSize: 10, fontWeight: 500, color: 'var(--muted)', marginLeft: 6 }}>(drag to reorder)</span></div>
<div className="section-toggles">
{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 (
<div
Expand All @@ -84,18 +93,35 @@ export default function Sidebar({
<span className="sec-toggle-icon">{sec.icon}</span>
{sec.label}
</div>
<label className="toggle-switch">
<input
type="checkbox"
checked={sectionState[sec.id]}
onChange={e => toggleSection(sec.id, e.target.checked)}
/>
<span className="tslider" />
</label>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{isCustom && (
<button
className="screenshot-item-remove"
style={{ position: 'relative', background: 'transparent', color: 'var(--muted)', top: 0, right: 0 }}
onClick={() => removeCustomSection(sec.id)}
title="Delete Custom Section"
>✕</button>
)}
<label className="toggle-switch">
<input
type="checkbox"
checked={sectionState[sec.id]}
onChange={e => toggleSection(sec.id, e.target.checked)}
/>
<span className="tslider" />
</label>
</div>
</div>
);
})}
</div>
<button
className="hbtn"
style={{ width: '100%', marginTop: 12, justifyContent: 'center' }}
onClick={addCustomSection}
>
+ Add Custom Section
</button>
</div>
</aside>
);
Expand Down
24 changes: 21 additions & 3 deletions src/utils/markdownUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';
}
}
}
});

Expand Down