feat: Implement basic cheat sheet functionality with LaTeX support#13
feat: Implement basic cheat sheet functionality with LaTeX support#13Davictory2003 wants to merge 1 commit intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Implements a basic frontend-only cheat sheet workflow (create/list/view/edit) with Markdown + LaTeX rendering and client-side PDF export, persisting data in localStorage.
Changes:
- Added Create/List/View components with Markdown + KaTeX rendering.
- Added PDF download via
html2canvas+jsPDF. - Added client-side persistence and updated styling + frontend dependencies.
Reviewed changes
Copilot reviewed 7 out of 9 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| package-lock.json | Adds a root lockfile (currently without a root package.json). |
| frontend/src/components/CreateCheatSheet.jsx | Markdown + LaTeX editor with live preview. |
| frontend/src/components/CheatSheetView.jsx | Renders a sheet and supports PDF download. |
| frontend/src/components/CheatSheetList.jsx | Lists saved sheets and supports selection/deletion. |
| frontend/src/App.jsx | Adds view routing/state, localStorage persistence, and wires components. |
| frontend/src/App.css | Adds layout and component styling. |
| frontend/package.json | Adds dependencies for KaTeX/Markdown and PDF generation. |
| frontend/package-lock.json | Locks newly added frontend dependencies. |
| README.md | Updates setup instructions (removes .env guidance). |
Files not reviewed (1)
- frontend/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <h1>Cheat Sheet</h1> | ||
| <p>Backend status: {status ?? 'loading...'}</p> | ||
| <header className="app-header"> | ||
| <h1 onClick={() => setView('list')} style={{cursor: 'pointer'}}>Cheat Sheet Generator</h1> |
There was a problem hiding this comment.
This h1 is clickable but not keyboard accessible and has no semantic role. For accessibility, use a / for navigation, or add role="button", tabIndex={0}, and key handlers (Enter/Space) so keyboard and assistive tech users can activate it.
| <h1 onClick={() => setView('list')} style={{cursor: 'pointer'}}>Cheat Sheet Generator</h1> | |
| <h1 | |
| onClick={() => setView('list')} | |
| onKeyDown={(event) => { | |
| if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') { | |
| event.preventDefault(); | |
| setView('list'); | |
| } | |
| }} | |
| role="button" | |
| tabIndex={0} | |
| style={{cursor: 'pointer'}} | |
| > | |
| Cheat Sheet Generator | |
| </h1> |
| // Add subsequent pages if needed | ||
| while (heightLeft >= 0) { | ||
| position = heightLeft - imgHeight; | ||
| pdf.addPage(); | ||
| pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); | ||
| heightLeft -= pdfHeight; | ||
| } |
There was a problem hiding this comment.
The PDF pagination loop uses while (heightLeft >= 0), which will add an extra blank/duplicate page when the content height fits exactly into an integer number of pages (heightLeft becomes 0 after subtracting pdfHeight). Use a strict > 0 check or adjust the loop to only add pages when there is remaining content.
| heightLeft -= pdfHeight; | ||
| } | ||
|
|
||
| pdf.save(`${sheet.title.replace(/\s+/g, '_')}.pdf`); |
There was a problem hiding this comment.
The download filename assumes sheet.title is a valid string and only replaces whitespace. If title is empty/undefined this will throw, and characters like /\\:*?"<>| can create invalid filenames on some platforms. Consider a safe fallback (e.g., "cheatsheet") and sanitizing disallowed filename characters.
| pdf.save(`${sheet.title.replace(/\s+/g, '_')}.pdf`); | |
| const rawTitle = (sheet && typeof sheet.title === 'string') ? sheet.title : ''; | |
| const baseTitle = rawTitle.trim() || 'cheatsheet'; | |
| const sanitizedBaseTitle = baseTitle | |
| .replace(/[\/\\:*?"<>|]/g, '') // remove characters invalid in many filesystems | |
| .replace(/\s+/g, '_') || 'cheatsheet'; | |
| pdf.save(`${sanitizedBaseTitle}.pdf`); |
| display: flex !important; | ||
| flex-direction: row !important; |
There was a problem hiding this comment.
Using !important for basic layout rules makes overrides/debugging harder and usually indicates a specificity issue elsewhere. Prefer removing !important and adjusting selector specificity or component structure so flex layout applies without forcing it.
| display: flex !important; | |
| flex-direction: row !important; | |
| display: flex; | |
| flex-direction: row; |
| ```bash | ||
| cd backend | ||
| python -m venv venv | ||
| source venv/bin/activate | ||
| pip install -r requirements.txt | ||
| # create a .env file in the backend/ directory with the required settings, for example: | ||
| # DJANGO_SECRET_KEY=your-dev-secret-key | ||
| # DJANGO_DEBUG=True | ||
| # DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 | ||
| python manage.py migrate | ||
| python manage.py runserver |
There was a problem hiding this comment.
The backend settings load configuration from backend/.env and require DJANGO_SECRET_KEY in production (they only fall back to a dev key when DJANGO_DEBUG=True). Removing the README instructions for creating a .env file makes initial setup and deployment requirements less clear; consider re-adding a brief note about optional dev defaults vs required prod env vars.
|
|
||
| useEffect(() => { | ||
| if (sheets.length > 0) { | ||
| localStorage.setItem('cheatSheets', JSON.stringify(sheets)); |
There was a problem hiding this comment.
This effect only writes to localStorage when sheets.length > 0. If sheets becomes an empty array via any path other than the explicit delete handlers (e.g., parse failure recovery, future features), localStorage will retain stale data. Consider writing on every change and removing the key (or setting "[]") when sheets is empty.
| localStorage.setItem('cheatSheets', JSON.stringify(sheets)); | |
| localStorage.setItem('cheatSheets', JSON.stringify(sheets)); | |
| } else { | |
| localStorage.removeItem('cheatSheets'); |
| <span className="sheet-title" onClick={() => onSelect(sheet)}> | ||
| {sheet.title} | ||
| </span> |
There was a problem hiding this comment.
The sheet title is rendered in a with onClick, which is not keyboard accessible and lacks button semantics. Consider using a (styled as a link) or adding role="button", tabIndex={0}, and onKeyDown handlers so users can select a sheet via keyboard.
| <span className="sheet-title" onClick={() => onSelect(sheet)}> | |
| {sheet.title} | |
| </span> | |
| <button | |
| type="button" | |
| className="sheet-title" | |
| onClick={() => onSelect(sheet)} | |
| > | |
| {sheet.title} | |
| </button> |
| import CheatSheetList from './components/CheatSheetList'; | ||
| import CreateCheatSheet from './components/CreateCheatSheet'; | ||
| import CheatSheetView from './components/CheatSheetView'; |
There was a problem hiding this comment.
This file (and other existing frontend entry files like frontend/src/main.jsx) omits semicolons, but the newly added import lines include them. To keep code style consistent and reduce formatting churn, consider removing semicolons (or run the repo's formatter if one is intended to enforce semicolons).
| import CheatSheetList from './components/CheatSheetList'; | |
| import CreateCheatSheet from './components/CreateCheatSheet'; | |
| import CheatSheetView from './components/CheatSheetView'; | |
| import CheatSheetList from './components/CheatSheetList' | |
| import CreateCheatSheet from './components/CreateCheatSheet' | |
| import CheatSheetView from './components/CheatSheetView' |
| setSheets(JSON.parse(savedSheets)); | ||
| } catch (e) { | ||
| console.error("Failed to parse sheets", e); |
There was a problem hiding this comment.
If JSON.parse fails, the invalid "cheatSheets" value is left in localStorage, so the app will hit this error on every load. Consider removing the key (or resetting it) in the catch block after logging, and optionally validate that the parsed value is an array before calling setSheets.
| setSheets(JSON.parse(savedSheets)); | |
| } catch (e) { | |
| console.error("Failed to parse sheets", e); | |
| const parsedSheets = JSON.parse(savedSheets); | |
| if (Array.isArray(parsedSheets)) { | |
| setSheets(parsedSheets); | |
| } else { | |
| console.error("Invalid sheets data in localStorage, expected an array. Resetting."); | |
| localStorage.removeItem('cheatSheets'); | |
| setSheets([]); | |
| } | |
| } catch (e) { | |
| console.error("Failed to parse sheets", e); | |
| localStorage.removeItem('cheatSheets'); |
| const handleDelete = (id) => { | ||
| if (window.confirm('Are you sure you want to delete this cheat sheet?')) { | ||
| const updatedSheets = sheets.filter(s => s.id !== id); | ||
| setSheets(updatedSheets); | ||
| // If we delete all, clear storage or keep empty array | ||
| if (updatedSheets.length === 0) { | ||
| localStorage.removeItem('cheatSheets'); | ||
| } | ||
| if (activeSheet && activeSheet.id === id) { | ||
| setActiveSheet(null); | ||
| setView('list'); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| const handleSelect = (sheet) => { | ||
| setActiveSheet(sheet); | ||
| setView('view'); | ||
| }; |
There was a problem hiding this comment.
handleDelete and handleSelect are defined but the list view passes inline onDelete/onSelect callbacks instead, duplicating logic and making future changes error-prone (e.g., activeSheet cleanup is handled in handleDelete but not in the inline handler). Prefer reusing handleDelete/handleSelect when wiring props to keep behavior consistent.
Added ability to save cheatsheet as pdf, as well as a working markdown and LaTeX editor. Still need to implement prebuilt formulas