-
Notifications
You must be signed in to change notification settings - Fork 3
Implemented a dashboard for managing sheets #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
e9d5de9
f4fe220
f727b59
0433c7b
a4111a8
1462605
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # Generated by Django 6.0.2 on 2026-04-21 19:29 | ||
|
|
||
| import django.db.models.deletion | ||
| from django.conf import settings | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('api', '0002_cheatsheet_selected_formulas'), | ||
| migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name='cheatsheet', | ||
| name='user', | ||
| field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='cheat_sheets', to=settings.AUTH_USER_MODEL), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,10 @@ | ||
| from rest_framework.decorators import api_view, action | ||
| from rest_framework.decorators import api_view, action, permission_classes | ||
| from rest_framework.response import Response | ||
| from rest_framework import status, viewsets | ||
| from django.http import FileResponse | ||
| from django.contrib.auth.models import User | ||
| from rest_framework.generics import CreateAPIView | ||
| from rest_framework.permissions import AllowAny | ||
| from rest_framework.permissions import AllowAny, IsAuthenticated | ||
| from django.shortcuts import get_object_or_404 | ||
| import subprocess | ||
| import tempfile | ||
|
|
@@ -129,6 +129,7 @@ def generate_sheet(request): | |
|
|
||
|
|
||
| @api_view(["POST"]) | ||
| @permission_classes([IsAuthenticated]) | ||
| def compile_latex(request): | ||
|
Comment on lines
131
to
133
|
||
| """ | ||
| POST /api/compile/ | ||
|
|
@@ -209,6 +210,13 @@ class CheatSheetViewSet(viewsets.ModelViewSet): | |
| """ | ||
| queryset = CheatSheet.objects.all() | ||
| serializer_class = CheatSheetSerializer | ||
| permission_classes = [IsAuthenticated] | ||
|
|
||
| def get_queryset(self): | ||
| return self.queryset.filter(user=self.request.user).order_by('-updated_at') | ||
|
Comment on lines
+213
to
+216
|
||
|
|
||
| def perform_create(self, serializer): | ||
| serializer.save(user=self.request.user) | ||
|
|
||
| @action(detail=False, methods=['post'], url_path='from-template') | ||
| def from_template(self, request): | ||
|
|
@@ -226,6 +234,7 @@ def from_template(self, request): | |
|
|
||
| cheatsheet = CheatSheet.objects.create( | ||
| title=title, | ||
| user=request.user, | ||
| template=template, | ||
| latex_content=template.latex_content, | ||
| margins=template.default_margins, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ import { Routes, Route, Link, Navigate } from 'react-router-dom'; | |
| import AuthContext from './context/AuthContext'; | ||
| import Login from './components/Login'; | ||
| import SignUp from './components/SignUp'; | ||
| import Dashboard from './components/Dashboard'; | ||
| import './App.css' | ||
| import CreateCheatSheet from './components/CreateCheatSheet'; | ||
|
|
||
|
|
@@ -48,7 +49,7 @@ function App() { | |
| localStorage.setItem('theme', theme); | ||
| }, [theme]); | ||
|
|
||
| const { user, logoutUser } = useContext(AuthContext); | ||
| const { user, authTokens, logoutUser } = useContext(AuthContext); | ||
|
|
||
| const toggleTheme = () => { | ||
| setTheme(prev => prev === 'dark' ? 'light' : 'dark'); | ||
|
|
@@ -57,6 +58,8 @@ function App() { | |
| const handleReset = () => { | ||
| setCheatSheet(DEFAULT_SHEET); | ||
| localStorage.setItem('currentCheatSheet', JSON.stringify(DEFAULT_SHEET)); | ||
| localStorage.removeItem('cheatSheetData'); | ||
| localStorage.removeItem('cheatSheetLatex'); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
|
|
@@ -90,7 +93,10 @@ function App() { | |
| const sheetId = nextSheet.id; | ||
| const response = await fetch(sheetId ? `/api/cheatsheets/${sheetId}/` : '/api/cheatsheets/', { | ||
| method: sheetId ? 'PATCH' : 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'Authorization': `Bearer ${authTokens?.access}` | ||
| }, | ||
|
Comment on lines
94
to
+99
|
||
| body: JSON.stringify({ | ||
| title: nextSheet.title, | ||
| latex_content: nextSheet.content, | ||
|
|
@@ -128,6 +134,22 @@ function App() { | |
| } | ||
| }; | ||
|
|
||
| const handleEditSheet = (sheet) => { | ||
| const editSheet = { | ||
| id: sheet.id, | ||
| title: sheet.title, | ||
| content: sheet.latex_content, | ||
| columns: sheet.columns, | ||
| margins: sheet.margins, | ||
| fontSize: sheet.font_size, | ||
| selectedFormulas: sheet.selected_formulas || [], | ||
| }; | ||
| setCheatSheet(editSheet); | ||
| localStorage.setItem('currentCheatSheet', JSON.stringify(editSheet)); | ||
| localStorage.removeItem('cheatSheetData'); | ||
| localStorage.removeItem('cheatSheetLatex'); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="App"> | ||
| <header className="app-header"> | ||
|
|
@@ -142,6 +164,8 @@ function App() { | |
| {user ? ( | ||
| <> | ||
| <span style={{ fontWeight: 'bold' }}>Hi, {user.username || 'User'}</span> | ||
| <Link to="/" className="btn primary" onClick={handleReset}>Create New Sheet</Link> | ||
| <Link to="/dashboard" className="btn">My Sheets</Link> | ||
| <button onClick={logoutUser} className="btn">Log Out</button> | ||
| </> | ||
| ) : ( | ||
|
|
@@ -169,6 +193,11 @@ function App() { | |
| /> | ||
| </PrivateRoute> | ||
| } /> | ||
| <Route path="/dashboard" element={ | ||
| <PrivateRoute> | ||
| <Dashboard onEditSheet={handleEditSheet} onCreateNewSheet={handleReset} /> | ||
| </PrivateRoute> | ||
| } /> | ||
| <Route path="/login" element={<Login />} /> | ||
| <Route path="/signup" element={<SignUp />} /> | ||
| </Routes> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| import React, { useState, useEffect, useContext } from 'react'; | ||
| import { useNavigate } from 'react-router-dom'; | ||
| import AuthContext from '../context/AuthContext'; | ||
| import '../styles/Dashboard.css'; | ||
|
|
||
| const Dashboard = ({ onEditSheet, onCreateNewSheet }) => { | ||
| const [sheets, setSheets] = useState([]); | ||
| const [loading, setLoading] = useState(true); | ||
| const [error, setError] = useState(''); | ||
| const { authTokens } = useContext(AuthContext); | ||
| const navigate = useNavigate(); | ||
|
|
||
| useEffect(() => { | ||
| const fetchSheets = async () => { | ||
| try { | ||
| const response = await fetch('/api/cheatsheets/', { | ||
| headers: { | ||
| 'Authorization': `Bearer ${authTokens?.access}`, | ||
| }, | ||
| }); | ||
|
Comment on lines
+20
to
+24
|
||
|
|
||
| if (!response.ok) { | ||
| throw new Error('Failed to load cheat sheets'); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
| setSheets(data); | ||
| } catch (err) { | ||
| setError(err.message); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| fetchSheets(); | ||
| }, [authTokens]); | ||
|
|
||
| const handleEdit = (sheet) => { | ||
| onEditSheet(sheet); | ||
| navigate('/'); | ||
| }; | ||
|
|
||
| const handleDelete = async (id) => { | ||
| if (!window.confirm('Are you sure you want to delete this cheat sheet?')) return; | ||
|
|
||
| try { | ||
| const response = await fetch(`/api/cheatsheets/${id}/`, { | ||
| method: 'DELETE', | ||
| headers: { | ||
| 'Authorization': `Bearer ${authTokens?.access}`, | ||
| }, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error('Failed to delete cheat sheet'); | ||
| } | ||
|
|
||
| setSheets(sheets.filter((sheet) => sheet.id !== id)); | ||
|
Davictory2003 marked this conversation as resolved.
Outdated
|
||
| } catch (err) { | ||
| alert(err.message); | ||
| } | ||
| }; | ||
|
|
||
| const handleDownload = async (sheet) => { | ||
| try { | ||
| const response = await fetch('/api/compile/', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'Authorization': `Bearer ${authTokens?.access}`, | ||
| }, | ||
| body: JSON.stringify({ cheat_sheet_id: sheet.id }), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorData = await response.json().catch(() => ({})); | ||
| throw new Error(errorData.error || 'Failed to generate PDF'); | ||
| } | ||
|
|
||
| const blob = await response.blob(); | ||
| const url = window.URL.createObjectURL(blob); | ||
| const a = document.createElement('a'); | ||
| a.href = url; | ||
| a.download = `${sheet.title || 'cheat_sheet'}.pdf`; | ||
| document.body.appendChild(a); | ||
| a.click(); | ||
| a.remove(); | ||
| window.URL.revokeObjectURL(url); | ||
| } catch (err) { | ||
| alert(err.message); | ||
| } | ||
| }; | ||
|
|
||
| if (loading) return <div className="dashboard-loading">Loading your sheets...</div>; | ||
| if (error) return <div className="dashboard-error">Error: {error}</div>; | ||
|
|
||
| return ( | ||
| <div className="dashboard-container"> | ||
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}> | ||
| <h2>My Cheat Sheets</h2> | ||
| <button className="btn primary" onClick={() => { onCreateNewSheet(); navigate('/'); }}>Create New Sheet</button> | ||
| </div> | ||
| {sheets.length === 0 ? ( | ||
| <div className="empty-state"> | ||
| <p>You haven't saved any cheat sheets yet.</p> | ||
| <button className="btn primary" onClick={() => { onCreateNewSheet(); navigate('/'); }}>Create Your First Sheet</button> | ||
| </div> | ||
| ) : ( | ||
| <div className="sheets-grid"> | ||
| {sheets.map((sheet) => ( | ||
| <div key={sheet.id} className="sheet-card"> | ||
| <h3>{sheet.title || 'Untitled Sheet'}</h3> | ||
| <p className="sheet-meta"> | ||
| Created: {new Date(sheet.created_at).toLocaleDateString()}<br/> | ||
| Last modified: {new Date(sheet.updated_at).toLocaleDateString()} | ||
| </p> | ||
| <div className="sheet-actions"> | ||
| <button className="btn small" onClick={() => handleEdit(sheet)}>Edit</button> | ||
| <button className="btn small primary" onClick={() => handleDownload(sheet)}>Download PDF</button> | ||
| <button className="btn small danger" onClick={() => handleDelete(sheet.id)}>Delete</button> | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default Dashboard; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The model imports
Userdirectly for theCheatSheet.userFK. For better compatibility with a swappable user model, prefer referencingsettings.AUTH_USER_MODEL(and avoid importingdjango.contrib.auth.models.Userin models). This keeps the model consistent with the migration which targetssettings.AUTH_USER_MODEL.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied in commit
1462605:CheatSheet.usernow referencessettings.AUTH_USER_MODELinstead of importingUserdirectly, keeping the model consistent with the migration.