draft shits
This commit is contained in:
100
internal/web/handler.go
Normal file
100
internal/web/handler.go
Normal 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)
|
||||
}
|
||||
262
internal/web/static/css/main.css
Normal file
262
internal/web/static/css/main.css
Normal 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);
|
||||
}
|
||||
}
|
||||
12
internal/web/templates/base.tmpl
Normal file
12
internal/web/templates/base.tmpl
Normal 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>
|
||||
8
internal/web/templates/index.tmpl
Normal file
8
internal/web/templates/index.tmpl
Normal file
@ -0,0 +1,8 @@
|
||||
{{ define "content" }}
|
||||
{{/* List of notes and searching utilities */}}
|
||||
{{ template "noteList" . }}
|
||||
{{/* Markdown notes rendering area */}}
|
||||
<main>
|
||||
{{ .Note }}
|
||||
</main>
|
||||
{{ end }}
|
||||
55
internal/web/templates/noteList.tmpl
Normal file
55
internal/web/templates/noteList.tmpl
Normal 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 }}
|
||||
Reference in New Issue
Block a user