feat(release): v0.1.0

commit 06ed2c3cbe
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 11:34:24 2026 +0100

    fix: changed detected by scanner but no updated by render layer

commit 01dcaf882a
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 10:19:05 2026 +0100

    feat: VERSION bumb

commit 229223f77a
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 09:53:08 2026 +0100

    feat: filter and search by tag

commit cb11e34798
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 09:41:03 2026 +0100

    feat: tag system

commit 3f5cf0d673
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 09:15:29 2026 +0100

    feat: sqlite storage draft

commit d6617cec02
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 09:04:11 2026 +0100

    feat: metadata draft

commit 7238d02a13
Author: adminoo <git@kadath.corp>
Date:   Mon Feb 2 10:18:42 2026 +0100

    fix: body overflowing

commit 16ff836274
Author: adminoo <git@kadath.corp>
Date:   Mon Feb 2 10:09:01 2026 +0100

    feat: tests for http handlers and render package

commit 36ac3f03aa
Author: adminoo <git@kadath.corp>
Date:   Mon Feb 2 09:45:29 2026 +0100

    feat: Dark theme, placeholder metadata panel

commit e6923fa4f5
Author: adminoo <git@kadath.corp>
Date:   Sun Feb 1 18:26:59 2026 +0100

    fix: uneeded func + uneeded bogus note creation logic

commit 4458ba2d15
Author: adminoo <git@kadath.corp>
Date:   Sun Feb 1 18:26:21 2026 +0100

    feat: log when changing note states

commit 92a6f84540
Author: adminoo <git@kadath.corp>
Date:   Sun Feb 1 16:55:40 2026 +0100

    possibly first working draft

commit e27aadc603
Author: adminoo <git@kadath.corp>
Date:   Sun Feb 1 11:55:16 2026 +0100

    draft shits
This commit is contained in:
2026-02-03 12:01:17 +01:00
parent d17ed8c650
commit 9d1254244f
27 changed files with 2940 additions and 0 deletions

250
internal/web/handler.go Normal file
View File

@ -0,0 +1,250 @@
package web
import (
"donniemarko/internal/note"
"donniemarko/internal/render"
"donniemarko/internal/service"
"html/template"
"net/http"
"net/url"
"strings"
)
type Handler struct {
notesService *service.NotesService
templates *render.TemplateManager
mux *http.ServeMux
}
func NewHandler(ns *service.NotesService, tm *render.TemplateManager) *Handler {
return &Handler{
notesService: ns,
templates: tm,
mux: http.NewServeMux(),
}
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
// Handle root and note list
if path == "/" {
h.handleRoot(w, r)
return
}
// Handle individual notes
if strings.HasPrefix(path, "/notes/") {
if strings.Contains(path, "/tags") {
h.handleTags(w, r)
return
}
h.handleNotes(w, r)
return
}
// Handle 404 for other paths
http.NotFound(w, r)
}
// ViewState is built per-request, not shared
type ViewState struct {
Notes []*note.Note
Note *note.Note
RenderedNote template.HTML
SortBy string
SearchTerm string
TagFilter string
LastActive string
}
func (h *Handler) handleRoot(w http.ResponseWriter, r *http.Request) {
// Build view state from query params
state, err := h.buildViewState(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Render with state
h.templates.Render(w, "index", state)
}
func (h *Handler) buildViewState(r *http.Request) (*ViewState, error) {
query := r.URL.Query()
// Extract params
sortBy := query.Get("sort")
if sortBy == "" {
sortBy = "recent"
}
searchTerm := query.Get("search")
tagFilter := query.Get("tag")
// Get notes from service
var notes []*note.Note
var err error
if searchTerm != "" {
opts := service.QueryOptions{
SearchTerm: searchTerm,
SortBy: sortBy,
}
notes, err = h.notesService.QueryNotes(opts)
if err != nil {
return nil, err
}
} else {
notes = h.notesService.GetNotes()
// Apply sorting
switch sortBy {
case "recent":
service.SortByDate(notes)
case "oldest":
service.SortByDateAsc(notes)
case "alpha":
service.SortByTitle(notes)
case "ralpha":
service.SortByTitleAsc(notes)
default:
service.SortByDate(notes)
}
}
if tagFilter != "" {
notes = filterNotesByTag(notes, tagFilter)
}
return &ViewState{
Notes: notes,
SortBy: sortBy,
SearchTerm: searchTerm,
TagFilter: tagFilter,
}, nil
}
func (h *Handler) SetupRoutes() {
// Set the handler as the main handler for http.DefaultServeMux
http.Handle("/", h)
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("internal/web/static"))))
}
func extractHash(path string) string {
// Extract hash from /notes/{hash}
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) < 2 || parts[0] != "notes" {
return ""
}
return parts[1]
}
func (h *Handler) handleNotes(w http.ResponseWriter, r *http.Request) {
// Build base state
state, err := h.buildViewState(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Extract hash from URL
hash := extractHash(r.URL.Path)
// Get specific note
note, err := h.notesService.GetNoteByHash(hash)
if err != nil {
http.Error(w, "Note not found", http.StatusNotFound)
return
}
// Convert markdown to HTML
htmlContent, err := render.RenderMarkdown([]byte(note.Content))
if err != nil {
http.Error(w, "Failed to render markdown", http.StatusInternalServerError)
return
}
// Add to state
state.Note = note
state.RenderedNote = htmlContent
state.LastActive = hash
h.templates.Render(w, "index", state)
}
func filterNotesByTag(notes []*note.Note, tag string) []*note.Note {
tag = strings.ToLower(strings.TrimSpace(tag))
if tag == "" {
return notes
}
filtered := make([]*note.Note, 0, len(notes))
for _, n := range notes {
for _, t := range n.Tags {
if strings.EqualFold(t, tag) {
filtered = append(filtered, n)
break
}
}
}
return filtered
}
func (h *Handler) handleTags(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
noteID, tag, isRemove := parseTagRoute(r.URL.Path)
if noteID == "" {
http.NotFound(w, r)
return
}
if isRemove {
if tag == "" {
http.Error(w, "Missing tag", http.StatusBadRequest)
return
}
if err := h.notesService.RemoveTag(noteID, tag); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
} else {
if err := r.ParseForm(); err != nil {
http.Error(w, "Invalid form", http.StatusBadRequest)
return
}
tag := r.FormValue("tag")
if tag == "" {
http.Error(w, "Missing tag", http.StatusBadRequest)
return
}
if err := h.notesService.AddTag(noteID, tag); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
http.Redirect(w, r, "/notes/"+noteID, http.StatusSeeOther)
}
func parseTagRoute(path string) (noteID string, tag string, isRemove bool) {
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) < 3 || parts[0] != "notes" || parts[2] != "tags" {
return "", "", false
}
noteID = parts[1]
if len(parts) >= 4 {
decoded, err := url.PathUnescape(parts[3])
if err != nil {
return noteID, "", true
}
return noteID, decoded, true
}
return noteID, "", false
}