feat: Implement Cheat Sheet Editor with LaTeX support#15
feat: Implement Cheat Sheet Editor with LaTeX support#15Davictory2003 wants to merge 1 commit intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Implements a new in-browser cheat sheet editor in the frontend with Markdown + LaTeX rendering and an option to download the preview as a PDF.
Changes:
- Added
CreateCheatSheeteditor component with live preview (React Markdown + KaTeX) and PDF export (html2canvas + jsPDF). - Updated the app shell to persist the current cheat sheet in
localStorageand render the new editor UI. - Added editor styling and new frontend dependencies; adjusted root centering CSS.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/index.css | Removes centered layout by commenting out text-align: center. |
| frontend/src/components/CreateCheatSheet.jsx | New editor component with Markdown+LaTeX preview and PDF download logic. |
| frontend/src/App.jsx | Replaces backend health check UI with cheat sheet editor + localStorage persistence. |
| frontend/src/App.css | Adds layout and component styles for the editor and preview. |
| frontend/package.json | Adds dependencies for Markdown/LaTeX rendering and PDF generation. |
| frontend/package-lock.json | Locks the newly added dependency tree. |
| README.md | Removes backend .env setup snippet from local dev instructions. |
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.
| 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 README no longer mentions how to configure runtime environment variables for the backend. Since backend/cheat_sheet/settings.py loads backend/.env and supports DJANGO_DEBUG, DJANGO_SECRET_KEY, and DJANGO_ALLOWED_HOSTS, it would be helpful to keep (or replace) a brief example of these variables for local setup.
| margin: 0 auto; | ||
| padding: 2rem; | ||
| text-align: center; | ||
| /* text-align: center; */ |
There was a problem hiding this comment.
Leaving text-align: center as a commented-out rule in index.css adds noise and makes it unclear what the intended default layout is. Prefer removing the line entirely (or moving it behind a more specific selector) if centering is no longer needed.
| /* text-align: center; */ |
| display: flex !important; | ||
| flex-direction: row !important; |
There was a problem hiding this comment.
.editor-container uses display: flex !important and flex-direction: row !important. If there isn’t a specific specificity/override problem, the !important flags will make future styling harder. Prefer removing !important and adjusting selector specificity instead.
| display: flex !important; | |
| flex-direction: row !important; | |
| display: flex; | |
| flex-direction: row; |
| try { | ||
| // Create canvas from the preview element | ||
| // Temporarily remove styles that might mess up the PDF | ||
| const originalStyle = previewRef.current.style.cssText; | ||
| // Force clean background and no border for capture | ||
| previewRef.current.style.border = 'none'; | ||
| previewRef.current.style.boxShadow = 'none'; | ||
| previewRef.current.style.background = '#ffffff'; | ||
|
|
||
| const canvas = await html2canvas(previewRef.current, { | ||
| scale: 2, | ||
| useCORS: true, | ||
| backgroundColor: '#ffffff', | ||
| logging: false, | ||
| x: 0, | ||
| y: 0, | ||
| width: previewRef.current.offsetWidth, | ||
| height: previewRef.current.offsetHeight | ||
| }); | ||
|
|
||
| // Restore original styles | ||
| previewRef.current.style.cssText = originalStyle; |
There was a problem hiding this comment.
handleDownloadPDF mutates previewRef.current.style but restores it only after html2canvas succeeds; if html2canvas throws, the preview will remain borderless/white for the rest of the session. Move the style restoration into a finally block (or restore inside catch as well) so it always runs.
| const [title, setTitle] = useState(initialData ? initialData.title : ''); | ||
| const [content, setContent] = useState(initialData ? initialData.content : ''); | ||
| const previewRef = useRef(null); | ||
|
|
||
| useEffect(() => { | ||
| if (initialData) { | ||
| if (initialData.title) setTitle(initialData.title); | ||
| if (initialData.content) setContent(initialData.content); | ||
| } |
There was a problem hiding this comment.
The useEffect only updates state when initialData.title/content are truthy, so switching to an empty string (or a saved sheet missing fields) won’t update the editor and can leave title/content as undefined, which then gets passed to controlled inputs. Prefer setting setTitle(initialData?.title ?? '') and setContent(initialData?.content ?? '') unconditionally when initialData changes (and similarly use nullish coalescing in the initial useState).
| const [title, setTitle] = useState(initialData ? initialData.title : ''); | |
| const [content, setContent] = useState(initialData ? initialData.content : ''); | |
| const previewRef = useRef(null); | |
| useEffect(() => { | |
| if (initialData) { | |
| if (initialData.title) setTitle(initialData.title); | |
| if (initialData.content) setContent(initialData.content); | |
| } | |
| const [title, setTitle] = useState(initialData?.title ?? ''); | |
| const [content, setContent] = useState(initialData?.content ?? ''); | |
| const previewRef = useRef(null); | |
| useEffect(() => { | |
| setTitle(initialData?.title ?? ''); | |
| setContent(initialData?.content ?? ''); |
| @@ -0,0 +1,133 @@ | |||
| import React, { useState, useEffect, useRef } from 'react'; | |||
There was a problem hiding this comment.
import React, ... imports the default React binding but it isn’t used anywhere in this file (new JSX transform doesn’t require it). Remove the unused default import to avoid lint/build warnings.
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { useState, useEffect, useRef } from 'react'; |
| const imgProps = pdf.getImageProperties(imgData); | ||
| const pdfWidth = pdf.internal.pageSize.getWidth(); | ||
| const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width; | ||
|
|
||
| // Add image to PDF - if height is greater than page, it will cut off. | ||
| pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight); |
There was a problem hiding this comment.
This implementation renders the entire preview to a single image and adds it to one A4 page; when the rendered height exceeds the page it will be truncated (as noted in the comment). If “Download as PDF” is a supported feature, consider splitting the canvas across multiple pages (or scaling to fit) so longer cheat sheets export correctly.
| const imgProps = pdf.getImageProperties(imgData); | |
| const pdfWidth = pdf.internal.pageSize.getWidth(); | |
| const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width; | |
| // Add image to PDF - if height is greater than page, it will cut off. | |
| pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight); | |
| const pdfWidth = pdf.internal.pageSize.getWidth(); | |
| const pdfPageHeight = pdf.internal.pageSize.getHeight(); | |
| // Scale the canvas image to fit the PDF width | |
| const canvasWidth = canvas.width; | |
| const canvasHeight = canvas.height; | |
| const scale = pdfWidth / canvasWidth; | |
| const scaledCanvasHeight = canvasHeight * scale; | |
| if (scaledCanvasHeight <= pdfPageHeight) { | |
| // Content fits on a single page; keep existing behavior | |
| pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, scaledCanvasHeight); | |
| } else { | |
| // Content exceeds a single page; split canvas into multiple pages | |
| const pageHeightInCanvasPx = pdfPageHeight / scale; | |
| let renderedHeight = 0; | |
| while (renderedHeight < canvasHeight) { | |
| // Create a temporary canvas for the current page slice | |
| const pageCanvas = document.createElement('canvas'); | |
| pageCanvas.width = canvasWidth; | |
| pageCanvas.height = Math.min(pageHeightInCanvasPx, canvasHeight - renderedHeight); | |
| const pageCtx = pageCanvas.getContext('2d'); | |
| // Draw the current slice from the full canvas into the page canvas | |
| pageCtx.drawImage( | |
| canvas, | |
| 0, | |
| renderedHeight, | |
| canvasWidth, | |
| pageCanvas.height, | |
| 0, | |
| 0, | |
| canvasWidth, | |
| pageCanvas.height | |
| ); | |
| const pageData = pageCanvas.toDataURL('image/png'); | |
| const pageHeightScaled = pageCanvas.height * scale; | |
| if (renderedHeight > 0) { | |
| pdf.addPage(); | |
| } | |
| pdf.addImage(pageData, 'PNG', 0, 0, pdfWidth, pageHeightScaled); | |
| renderedHeight += pageHeightInCanvasPx; | |
| } | |
| } |
| import CreateCheatSheet from './components/CreateCheatSheet'; | ||
|
|
||
| function App() { | ||
| const [status, setStatus] = useState(null) | ||
| const [cheatSheet, setCheatSheet] = useState({ title: '', content: '' }); |
There was a problem hiding this comment.
New code introduces semicolons, which is inconsistent with the existing frontend style (e.g., frontend/src/main.jsx:1-10 omits semicolons). Consider removing the semicolons here (and in CreateCheatSheet.jsx) to keep formatting consistent across the codebase.
|
|
||
| // Add image to PDF - if height is greater than page, it will cut off. | ||
| pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight); | ||
| pdf.save(`${title || 'cheat-sheet'}.pdf`); |
There was a problem hiding this comment.
The saved filename uses the raw title string; titles containing characters like /, \, : etc. can produce invalid or confusing filenames on some platforms. Consider sanitizing title to a safe filename (and trimming whitespace) before passing it to pdf.save.
| <CreateCheatSheet | ||
| initialData={cheatSheet} | ||
| onSave={handleSave} | ||
| onCancel={() => {}} |
There was a problem hiding this comment.
onCancel is passed to CreateCheatSheet as an empty callback, but the child component doesn’t use onCancel at all. Removing this prop (or implementing a real cancel flow) would simplify the API surface and avoid dead code paths.
| onCancel={() => {}} |
Supports downloading as PDF