draft shits

This commit is contained in:
2026-02-01 11:55:16 +01:00
parent d17ed8c650
commit e27aadc603
22 changed files with 1341 additions and 0 deletions

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

@ -0,0 +1,100 @@
package web
import (
"net/http"
"donniemarko/internal/service"
"donniemarko/internal/render"
)
type Handler struct {
notesService *service.NotesService
templates *render.TemplateManager
}
func NewHandler(ns *service.NotesService, tm *render.TemplateManager) *Handler {
return &Handler{
notesService: ns,
templates: tm,
}
}
// ViewState is built per-request, not shared
type ViewState struct {
Notes []*note.Note
CurrentNote *note.Note
SortBy string
SearchTerm string
ActiveHash 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")
// Get notes from service
var notes []*note.Note
if searchTerm != "" {
notes = h.notesService.Search(searchTerm)
} else {
notes = h.notesService.GetNotes()
}
// Apply sorting
switch sortBy {
case "recent":
service.SortByDate(notes)
case "alpha":
service.SortByTitle(notes)
}
return &ViewState{
Notes: notes,
SortBy: sortBy,
SearchTerm: searchTerm,
}, nil
}
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
}
// Add to state
state.CurrentNote = note
state.ActiveHash = hash
h.templates.Render(w, "note", state)
}

View File

@ -0,0 +1,262 @@
:root {
--background1: #ffffff;
--background2: #f0f0f0;
--background3: #c4c3c3;
--heading1: #2a2a2a;
--heading2: #4a4a4a;
--heading3: #7c7b7b;
--text1: #5a5a5a;
--code1: #6a6a6a;
--url: #090b0e;
--border-color: #d0d0d0;
--font-main: "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-size-base: 16px;
--line-height: 1.5;
--padding-main: 1cm;
--max-width-main: 210mm;
}
@page {
size: A4;
}
@media print {
body {
margin: 2cm 1.5cm 2cm 1.5cm;
}
/* Don't display the list of notes in the printed page */
aside,
.note-metadata {
display: none;
}
main {
margin-top: 0;
break-after: always;
}
h1 {
margin-top: 0;
}
}
@media screen {
aside,
main {
height: 100vh;
overflow-y: auto;
}
h1,
h2,
h3,
h4 {
margin-top: 1.5em;
}
}
@media all {
body {
display: flex;
background: var(--background1);
margin: 0;
font-family: var(--font-main);
font-size: var(--font-size-base);
line-height: var(--line-height);
color: var(--text1);
}
a {
color: var(--url);
font-weight: bold;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
pre {
background: var(--background2);
padding: 0.8em;
}
pre code {
padding: 0;
}
code {
line-break: loose;
background-color: var(--background2);
padding: 0.2em 0.4em;
border-radius: 3px;
font-family: monospace;
color: var(--code1);
overflow-x: auto;
text-wrap: wrap;
}
header h1 {
color: var(--heading1);
font-size: 2em;
border-bottom: 1px solid var(--border-color);
padding-bottom: 1em;
margin: 0;
text-align: center;
}
aside {
flex: 0 0 400px;
width: 250px;
border-right: 1px solid var(--border-color);
padding: var(--padding-main);
background-color: #f5f5f5;
}
aside ul {
list-style: none;
padding: 0;
margin: 0;
}
aside ul li {
padding: 0.5em 0;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: end;
}
aside ul li:last-child {
border-bottom: none;
}
aside ul li a {
display: block;
transition: background-color 0.2s;
}
aside ul li a:hover {
background-color: var(--background2);
}
main {
flex: 1;
margin: 0 auto;
padding: var(--padding-main);
overflow-y: auto;
max-height: 100%;
}
main a {
text-decoration: underline;
}
main ul {
padding: 0;
margin: 0 1em;
}
main h1 {
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.3em;
text-transform: uppercase;
text-align: center;
}
main h2 {
padding: 0.7em 0;
border-bottom: 2px dotted var(--border-color);
}
main h3, main h4 {
color: var(--heading3);
font-size: 0.9em;
}
h1,
h2,
h3,
h4 {
margin-bottom: 0.5em;
line-height: 1.2;
}
h1 {
color: var(--heading1);
font-size: 1.5em;
}
h2 {
color: var(--heading1);
font-size: 1.2em;
}
h3 {
color: var(--heading2);
font-size: 1em;
}
p {
margin-bottom: 1em;
}
blockquote {
margin: 1em 0;
padding: 0 1em;
border-left: 3px solid var(--border-color);
color: var(--heading2);
}
.active-note {
background: var(--background3);
}
.last-modified {
font-size: 0.8em;
}
.folder {
font-weight: bold;
width: 100%;
}
.folder span {
padding: 0 1em;
}
.search-form {
display: flex;
margin-bottom: 1em;
}
.search-form > * {
display: inline;
}
/* Add this to the end of the file */
.search-bar {
width: 100%;
padding: 0.5em;
border: 1px solid var(--border-color);
border-radius: 4px;
font-family: var(--font-main);
font-size: var(--font-size-base);
color: var(--text1);
}
.search-bar:focus {
outline: none;
border-color: var(--heading1);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.search-results {
margin-bottom: 1em;
margin: 1em 0;
border-bottom: dotted 2px var(--heading1);
}
}

View File

@ -0,0 +1,12 @@
<!doctype html>
<html class="no-js" lang="en">
<head>
<meta charset="utf-8">
<title>Donnie Marko</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/css/main.css" type="text/css">
</head>
<body>
{{ template "content" . }}
</body>
</html>

View File

@ -0,0 +1,8 @@
{{ define "content" }}
{{/* List of notes and searching utilities */}}
{{ template "noteList" . }}
{{/* Markdown notes rendering area */}}
<main>
{{ .Note }}
</main>
{{ end }}

View File

@ -0,0 +1,55 @@
{{ define "noteList" }}
<aside>
<header>
<h1 class="main-logo"><a href="/">Donnie Marko</a></h1>
<form method="GET" action="/" class="search-form">
<input type="text" name="search" class="search-bar" placeholder="Search... (empty query to clear)">
<input type="submit" value="ok"/>
</form>
<form method="GET" action="/">
<select name="sort" value="sort" class="sort-dropdown">
<option value="" disabled {{ if eq "" .SortBy }}selected{{ end }}>Sort by</option>
<option value="recent" {{ if eq "recent" .SortBy }}selected{{ end }}>Recent</option>
<option value="oldest" {{ if eq "oldest" .SortBy }}selected{{ end }}>Oldest</option>
<option value="alpha" {{ if eq "alpha" .SortBy }}selected{{ end }}>Alphabetical</option>
<option value="ralpha" {{ if eq "ralpha" .SortBy }}selected{{ end }}>Reverse Alphabetical</option>
</select>
<input type="submit" value="sort" />
</form>
</header>
{{ template "renderSearch" . }}
</aside>
{{ end }}
{{ define "renderSearch" }}
{{ if ne .SearchTerm "" }}<h2>Matching results for query '{{ .SearchTerm }}'</h2>{{ end }}
<ul class="search-results">
{{ range .Nodes }}
{{ if .IsEnd }}
<li {{ if eq .Hash $.LastActive }}class="active-note"{{ end }}>
<input type="checkbox"/>
<a href="/notes/{{ .Hash }}" data-hash="{{ .Hash }}">{{ .Title }}</a>
<span class="last-modified">{{ .LastModified }}</span>
</li>
{{ end }}
{{ end }}
</ul>
{{ end }}
{{/* not used for now, the opposition between flat list from hashmap and tree structure is confusing */}}
{{ define "renderTree" }}
<ul>
{{ range . }}
{{ if .IsEnd }}
<li><input type="checkbox"/><a href="/notes/{{ .Hash }}" data-hash="{{ .Hash }}">{{ .Title }}</a><span class="last-modified">{{ .LastModified }}</span></li>
{{ else }}
{{ if .Children }}
<li><div class="folder"><input type="checkbox"/><span class="folder">{{ .Path }}</span></folder>
{{ template "renderTree" .Children }}
</li>
{{ end }}
{{ end }}
{{ end }}
</ul>
{{ end }}