Add notebook delete, fix button styling, rename Share button
- Add delete notebook handler with ownership check and CASCADE delete - Rename "Create Share Link" to "Share" - Fix action button heights: use inline-flex + align-items for consistent sizing across <a> and <button> elements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -248,6 +248,23 @@ func (ws *WebServer) handleCreateShare(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, fmt.Sprintf("/notebooks/%d", id), http.StatusFound)
|
http.Redirect(w, r, fmt.Sprintf("/notebooks/%d", id), http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) handleDeleteNotebook(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID := r.Context().Value(userIDKey).(int64)
|
||||||
|
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
|
||||||
|
// Verify ownership before deleting.
|
||||||
|
var ownerID int64
|
||||||
|
err := ws.db.QueryRow("SELECT user_id FROM notebooks WHERE id = ?", id).Scan(&ownerID)
|
||||||
|
if err != nil || ownerID != userID {
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CASCADE deletes pages, strokes, and share_links.
|
||||||
|
_, _ = ws.db.Exec("DELETE FROM notebooks WHERE id = ?", id)
|
||||||
|
http.Redirect(w, r, "/notebooks", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
func (ws *WebServer) handleRevokeShare(w http.ResponseWriter, r *http.Request) {
|
func (ws *WebServer) handleRevokeShare(w http.ResponseWriter, r *http.Request) {
|
||||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
token := r.FormValue("token")
|
token := r.FormValue("token")
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ func Start(cfg Config) (*http.Server, error) {
|
|||||||
r.Get("/notebooks/{id}/pages/{num}/svg", ws.handlePageSVG)
|
r.Get("/notebooks/{id}/pages/{num}/svg", ws.handlePageSVG)
|
||||||
r.Get("/notebooks/{id}/pages/{num}/jpg", ws.handlePageJPG)
|
r.Get("/notebooks/{id}/pages/{num}/jpg", ws.handlePageJPG)
|
||||||
r.Get("/notebooks/{id}/pdf", ws.handleNotebookPDF)
|
r.Get("/notebooks/{id}/pdf", ws.handleNotebookPDF)
|
||||||
|
r.Post("/notebooks/{id}/delete", ws.handleDeleteNotebook)
|
||||||
r.Post("/notebooks/{id}/share", ws.handleCreateShare)
|
r.Post("/notebooks/{id}/share", ws.handleCreateShare)
|
||||||
r.Post("/notebooks/{id}/share/revoke", ws.handleRevokeShare)
|
r.Post("/notebooks/{id}/share/revoke", ws.handleRevokeShare)
|
||||||
r.Get("/logout", ws.handleLogout)
|
r.Get("/logout", ws.handleLogout)
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; }
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; }
|
||||||
.page-thumb { border: 1px solid #ccc; background: #fff; aspect-ratio: 0.773; display: flex; align-items: center; justify-content: center; }
|
.page-thumb { border: 1px solid #ccc; background: #fff; aspect-ratio: 0.773; display: flex; align-items: center; justify-content: center; }
|
||||||
.page-thumb img { width: 100%; height: 100%; object-fit: contain; }
|
.page-thumb img { width: 100%; height: 100%; object-fit: contain; }
|
||||||
.btn { display: inline-block; padding: 0.5rem 1rem; border: 1px solid #111; border-radius: 4px; text-decoration: none; color: #111; background: #fff; cursor: pointer; }
|
.btn { display: inline-flex; align-items: center; padding: 0.5rem 1rem; border: 1px solid #111; border-radius: 4px; text-decoration: none; color: #111; background: #fff; cursor: pointer; font: inherit; font-size: 1rem; line-height: 1.5; box-sizing: border-box; }
|
||||||
.btn:hover { background: #f0f0f0; }
|
.btn:hover { background: #f0f0f0; }
|
||||||
.actions { display: flex; gap: 0.5rem; margin: 1rem 0; }
|
.actions { display: flex; align-items: center; gap: 0.5rem; margin: 1rem 0; }
|
||||||
input[type="text"], input[type="password"] { padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; width: 100%; max-width: 300px; }
|
input[type="text"], input[type="password"] { padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; width: 100%; max-width: 300px; }
|
||||||
label { display: block; margin-bottom: 0.25rem; font-weight: bold; }
|
label { display: block; margin-bottom: 0.25rem; font-weight: bold; }
|
||||||
.form-group { margin-bottom: 1rem; }
|
.form-group { margin-bottom: 1rem; }
|
||||||
|
|||||||
@@ -6,7 +6,11 @@
|
|||||||
<a href="{{.PDFLink}}" class="btn">Download PDF</a>
|
<a href="{{.PDFLink}}" class="btn">Download PDF</a>
|
||||||
{{if not .Shared}}
|
{{if not .Shared}}
|
||||||
<form method="POST" action="/notebooks/{{.ID}}/share" style="display:inline;">
|
<form method="POST" action="/notebooks/{{.ID}}/share" style="display:inline;">
|
||||||
<button type="submit" class="btn">Create Share Link</button>
|
<button type="submit" class="btn">Share</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/notebooks/{{.ID}}/delete" style="display:inline;"
|
||||||
|
onsubmit="return confirm('Delete this notebook from the server?');">
|
||||||
|
<button type="submit" class="btn" style="color: #c62828; border-color: #c62828;">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user