feat(release): v0.2.0
commit 78d6c27c8940da32a6de8e64327c86f74fdaa2eb Author: adminoo <git@kadath.corp> Date: Wed Feb 4 12:59:22 2026 +0100 feat: freebsd log rotation config thingie commit 55af4e6c70122e679272ed247c26e04b1247f694 Author: adminoo <git@kadath.corp> Date: Wed Feb 4 12:58:43 2026 +0100 feat: embed templates, static resolution commit 29c917f929a7378ec29c54315ee2e9f420747787 Author: adminoo <git@kadath.corp> Date: Wed Feb 4 10:44:34 2026 +0100 feat: set log file path commit 294fd3d1549979eab63587ceec6ff5d0978e9afc Author: adminoo <git@kadath.corp> Date: Wed Feb 4 10:23:53 2026 +0100 feat: logging HTTP request commit c9ae80b240d58e1abed7ae3b7b2c3b283a31f1a1 Author: adminoo <git@kadath.corp> Date: Wed Feb 4 09:54:05 2026 +0100 feat: freebsd-specific compile target and scripts commit 86ca154dedd19aa1fe5f571c445dcf17a8396bfa Author: adminoo <git@kadath.corp> Date: Wed Feb 4 09:25:16 2026 +0100 feat: mobile friendly CSS commit 199f4319e0b08a4b6d595d7eb3effb6db6c7beec Author: adminoo <git@kadath.corp> Date: Wed Feb 4 09:25:03 2026 +0100 feat: persisting rendered note commit 865e258237e45d7c542685a4653bcad3c5af259d Author: adminoo <git@kadath.corp> Date: Wed Feb 4 08:06:38 2026 +0100 fix: grouping notes by folder commit 242d1d074c92461f38212b033c7a9e383f9dc550 Author: adminoo <git@kadath.corp> Date: Tue Feb 3 16:52:50 2026 +0100 feat: storage layer logic - Prune notes from db not matching current folder structure at start - Detect file system deletion on start by comparing in-db notes - Prevent updating of in-db notes at start if modification time is not newer - Delete by path commit d75d46bc1ab22bd990d0fdc307e571fe52f0dd99 Author: adminoo <git@kadath.corp> Date: Tue Feb 3 15:27:07 2026 +0100 feat: group notes by root folders commit e1e25a938e717599332f7b40a449d9bb854b673a Author: adminoo <git@kadath.corp> Date: Tue Feb 3 14:24:37 2026 +0100 feat: size in kilobytes commit 61220272a2df2b66c2b8e356ba359ed01de3bd12 Author: adminoo <git@kadath.corp> Date: Tue Feb 3 14:19:40 2026 +0100 feat: styling inputs
This commit is contained in:
25
internal/web/assets.go
Normal file
25
internal/web/assets.go
Normal file
@ -0,0 +1,25 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed templates/*.tmpl static/**
|
||||
var embeddedFS embed.FS
|
||||
|
||||
func TemplatesFS() fs.FS {
|
||||
sub, err := fs.Sub(embeddedFS, "templates")
|
||||
if err != nil {
|
||||
return embeddedFS
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
func StaticFS() fs.FS {
|
||||
sub, err := fs.Sub(embeddedFS, "static")
|
||||
if err != nil {
|
||||
return embeddedFS
|
||||
}
|
||||
return sub
|
||||
}
|
||||
@ -5,8 +5,11 @@ import (
|
||||
"donniemarko/internal/render"
|
||||
"donniemarko/internal/service"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -26,6 +29,7 @@ func NewHandler(ns *service.NotesService, tm *render.TemplateManager) *Handler {
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
log.Printf("request: method=%s path=%s host=%s remote=%s", r.Method, path, r.Host, r.RemoteAddr)
|
||||
|
||||
// Handle root and note list
|
||||
if path == "/" {
|
||||
@ -44,18 +48,27 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Handle 404 for other paths
|
||||
log.Printf("not found: method=%s path=%s host=%s remote=%s", r.Method, path, r.Host, r.RemoteAddr)
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
// ViewState is built per-request, not shared
|
||||
type ViewState struct {
|
||||
Notes []*note.Note
|
||||
Groups []NoteGroup
|
||||
Note *note.Note
|
||||
RenderedNote template.HTML
|
||||
SortBy string
|
||||
SearchTerm string
|
||||
TagFilter string
|
||||
LastActive string
|
||||
// BasePath is an optional URL prefix (e.g. "/donnie") when served behind a reverse proxy.
|
||||
BasePath string
|
||||
}
|
||||
|
||||
type NoteGroup struct {
|
||||
Name string
|
||||
Notes []*note.Note
|
||||
}
|
||||
|
||||
func (h *Handler) handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||
@ -66,8 +79,16 @@ func (h *Handler) handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
active := r.URL.Query().Get("active")
|
||||
if active != "" {
|
||||
_ = h.setActiveNote(state, active)
|
||||
}
|
||||
|
||||
// Render with state
|
||||
h.templates.Render(w, "index", state)
|
||||
if err := h.templates.Render(w, "index", state); err != nil {
|
||||
log.Printf("render error: %v", err)
|
||||
http.Error(w, "Render error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) buildViewState(r *http.Request) (*ViewState, error) {
|
||||
@ -81,6 +102,9 @@ func (h *Handler) buildViewState(r *http.Request) (*ViewState, error) {
|
||||
|
||||
searchTerm := query.Get("search")
|
||||
tagFilter := query.Get("tag")
|
||||
active := query.Get("active")
|
||||
// Preserve proxy prefix so templates can build correct links and asset URLs.
|
||||
basePath := basePathFromRequest(r)
|
||||
|
||||
// Get notes from service
|
||||
var notes []*note.Note
|
||||
@ -119,16 +143,19 @@ func (h *Handler) buildViewState(r *http.Request) (*ViewState, error) {
|
||||
|
||||
return &ViewState{
|
||||
Notes: notes,
|
||||
Groups: groupNotesByFolder(notes),
|
||||
SortBy: sortBy,
|
||||
SearchTerm: searchTerm,
|
||||
TagFilter: tagFilter,
|
||||
LastActive: active,
|
||||
BasePath: basePath,
|
||||
}, 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"))))
|
||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(StaticFS()))))
|
||||
}
|
||||
|
||||
func extractHash(path string) string {
|
||||
@ -169,8 +196,30 @@ func (h *Handler) handleNotes(w http.ResponseWriter, r *http.Request) {
|
||||
state.Note = note
|
||||
state.RenderedNote = htmlContent
|
||||
state.LastActive = hash
|
||||
// Ensure note view carries proxy prefix for links/forms.
|
||||
state.BasePath = basePathFromRequest(r)
|
||||
|
||||
h.templates.Render(w, "index", state)
|
||||
if err := h.templates.Render(w, "index", state); err != nil {
|
||||
log.Printf("render error: %v", err)
|
||||
http.Error(w, "Render error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) setActiveNote(state *ViewState, noteID string) error {
|
||||
note, err := h.notesService.GetNoteByHash(noteID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
htmlContent, err := render.RenderMarkdown([]byte(note.Content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state.Note = note
|
||||
state.RenderedNote = htmlContent
|
||||
state.LastActive = noteID
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterNotesByTag(notes []*note.Note, tag string) []*note.Note {
|
||||
@ -191,6 +240,64 @@ func filterNotesByTag(notes []*note.Note, tag string) []*note.Note {
|
||||
return filtered
|
||||
}
|
||||
|
||||
func groupNotesByFolder(notes []*note.Note) []NoteGroup {
|
||||
groups := make(map[string][]*note.Note)
|
||||
for _, n := range notes {
|
||||
name := rootFolderName(n.Path)
|
||||
groups[name] = append(groups[name], n)
|
||||
}
|
||||
|
||||
if len(groups) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(groups))
|
||||
for name := range groups {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
result := make([]NoteGroup, 0, len(names))
|
||||
for _, name := range names {
|
||||
result = append(result, NoteGroup{
|
||||
Name: name,
|
||||
Notes: groups[name],
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func rootFolderName(path string) string {
|
||||
if path == "" {
|
||||
return "root"
|
||||
}
|
||||
clean := filepath.ToSlash(filepath.Clean(path))
|
||||
if clean == "." || clean == "/" {
|
||||
return "root"
|
||||
}
|
||||
clean = strings.TrimPrefix(clean, "/")
|
||||
parts := strings.Split(clean, "/")
|
||||
if len(parts) == 0 || parts[0] == "." {
|
||||
return "root"
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return "root"
|
||||
}
|
||||
return parts[0]
|
||||
}
|
||||
|
||||
func basePathFromRequest(r *http.Request) string {
|
||||
// X-Forwarded-Prefix is commonly set by reverse proxies for subpath mounts.
|
||||
prefix := strings.TrimSpace(r.Header.Get("X-Forwarded-Prefix"))
|
||||
if prefix == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(prefix, "/") {
|
||||
prefix = "/" + prefix
|
||||
}
|
||||
return strings.TrimRight(prefix, "/")
|
||||
}
|
||||
|
||||
func (h *Handler) handleTags(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
@ -228,7 +335,9 @@ func (h *Handler) handleTags(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/notes/"+noteID, http.StatusSeeOther)
|
||||
// Redirect using proxy prefix when present.
|
||||
basePath := basePathFromRequest(r)
|
||||
http.Redirect(w, r, basePath+"/notes/"+noteID, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func parseTagRoute(path string) (noteID string, tag string, isRemove bool) {
|
||||
|
||||
@ -29,18 +29,21 @@ func newTestEnv(t *testing.T) *testEnv {
|
||||
ID: "a1",
|
||||
Title: "Alpha",
|
||||
Content: "# Alpha\ncontent",
|
||||
Path: "notes/alpha.md",
|
||||
UpdatedAt: time.Date(2025, 12, 31, 10, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
ID: "b2",
|
||||
Title: "Beta",
|
||||
Content: "# Beta\nRust tips",
|
||||
Path: "dev/beta.md",
|
||||
UpdatedAt: time.Date(2026, 1, 1, 10, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
ID: "z9",
|
||||
Title: "Zulu",
|
||||
Content: "# Zulu\nRust book",
|
||||
Path: "notes/zulu.md",
|
||||
UpdatedAt: time.Date(2026, 1, 2, 10, 0, 0, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
@ -83,6 +86,20 @@ func noteMarker(id string) string {
|
||||
return `data-hash="` + id + `"`
|
||||
}
|
||||
|
||||
func groupSection(body, name string) string {
|
||||
header := `<h3 class="note-group">` + name + `</h3>`
|
||||
start := strings.Index(body, header)
|
||||
if start == -1 {
|
||||
return ""
|
||||
}
|
||||
rest := body[start+len(header):]
|
||||
next := strings.Index(rest, `<h3 class="note-group">`)
|
||||
if next == -1 {
|
||||
return rest
|
||||
}
|
||||
return rest[:next]
|
||||
}
|
||||
|
||||
func TestHandlerRoot_DefaultSortRecent(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
|
||||
@ -100,7 +117,24 @@ func TestHandlerRoot_DefaultSortRecent(t *testing.T) {
|
||||
}
|
||||
|
||||
body := rec.Body.String()
|
||||
assertOrder(t, body, []string{noteMarker("z9"), noteMarker("b2"), noteMarker("a1")})
|
||||
if !strings.Contains(body, `<h3 class="note-group">dev</h3>`) {
|
||||
t.Fatalf("expected dev group header")
|
||||
}
|
||||
if !strings.Contains(body, `<h3 class="note-group">notes</h3>`) {
|
||||
t.Fatalf("expected notes group header")
|
||||
}
|
||||
|
||||
devSection := groupSection(body, "dev")
|
||||
if devSection == "" {
|
||||
t.Fatalf("expected dev section")
|
||||
}
|
||||
assertOrder(t, devSection, []string{noteMarker("b2")})
|
||||
|
||||
notesSection := groupSection(body, "notes")
|
||||
if notesSection == "" {
|
||||
t.Fatalf("expected notes section")
|
||||
}
|
||||
assertOrder(t, notesSection, []string{noteMarker("z9"), noteMarker("a1")})
|
||||
}
|
||||
|
||||
func TestHandlerRoot_SortAlpha(t *testing.T) {
|
||||
@ -116,7 +150,17 @@ func TestHandlerRoot_SortAlpha(t *testing.T) {
|
||||
}
|
||||
|
||||
body := rec.Body.String()
|
||||
assertOrder(t, body, []string{noteMarker("a1"), noteMarker("b2"), noteMarker("z9")})
|
||||
devSection := groupSection(body, "dev")
|
||||
if devSection == "" {
|
||||
t.Fatalf("expected dev section")
|
||||
}
|
||||
assertOrder(t, devSection, []string{noteMarker("b2")})
|
||||
|
||||
notesSection := groupSection(body, "notes")
|
||||
if notesSection == "" {
|
||||
t.Fatalf("expected notes section")
|
||||
}
|
||||
assertOrder(t, notesSection, []string{noteMarker("a1"), noteMarker("z9")})
|
||||
}
|
||||
|
||||
func TestHandlerRoot_Search(t *testing.T) {
|
||||
@ -305,3 +349,97 @@ func TestHandlerRoot_TagFilter(t *testing.T) {
|
||||
t.Fatalf("expected matching note to be included")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerRoot_RendersActiveNote(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/?search=rust&active=b2", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
env.handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||
}
|
||||
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "<h1>Beta</h1>") {
|
||||
t.Fatalf("expected rendered active note content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerRoot_RendersRootGroupHeader(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
|
||||
rootNote := ¬e.Note{
|
||||
ID: "r1",
|
||||
Title: "Root Note",
|
||||
Content: "# Root\ncontent",
|
||||
Path: "root.md",
|
||||
UpdatedAt: time.Date(2026, 1, 3, 10, 0, 0, 0, time.UTC),
|
||||
}
|
||||
|
||||
if err := env.storage.Create(rootNote); err != nil {
|
||||
t.Fatalf("create note: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
env.handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||
}
|
||||
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, `<h3 class="note-group">root</h3>`) {
|
||||
t.Fatalf("expected root group header")
|
||||
}
|
||||
if !strings.Contains(body, noteMarker("r1")) {
|
||||
t.Fatalf("expected root note to be rendered")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootFolderName(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{name: "empty", path: "", want: "root"},
|
||||
{name: "dot", path: ".", want: "root"},
|
||||
{name: "file at root", path: "bookmarks.md", want: "root"},
|
||||
{name: "single", path: "notes/file.md", want: "notes"},
|
||||
{name: "nested", path: "dev/ideas/note.md", want: "dev"},
|
||||
{name: "absolute", path: "/writings/notes/a.md", want: "writings"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := rootFolderName(tc.path); got != tc.want {
|
||||
t.Fatalf("expected %q, got %q", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupNotesByFolder(t *testing.T) {
|
||||
notes := []*note.Note{
|
||||
{ID: "a1", Title: "Alpha", Path: "notes/a.md"},
|
||||
{ID: "b2", Title: "Beta", Path: "dev/b.md"},
|
||||
{ID: "c3", Title: "Gamma", Path: "notes/c.md"},
|
||||
}
|
||||
|
||||
groups := groupNotesByFolder(notes)
|
||||
if len(groups) != 2 {
|
||||
t.Fatalf("expected 2 groups, got %d", len(groups))
|
||||
}
|
||||
|
||||
if groups[0].Name != "dev" || len(groups[0].Notes) != 1 {
|
||||
t.Fatalf("unexpected dev group: %+v", groups[0])
|
||||
}
|
||||
if groups[1].Name != "notes" || len(groups[1].Notes) != 2 {
|
||||
t.Fatalf("unexpected notes group: %+v", groups[1])
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +71,10 @@
|
||||
h4 {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.sidebar-toggle, .sidebar-toggle-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* UNIVERSAL */
|
||||
@ -125,7 +129,6 @@
|
||||
header h1 {
|
||||
color: var(--heading1);
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 1em;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
@ -164,7 +167,6 @@
|
||||
}
|
||||
|
||||
.note-title a {
|
||||
padding-left: 0.7em;
|
||||
font-size: 0.95em;
|
||||
color: var(--text1);
|
||||
}
|
||||
@ -175,17 +177,26 @@
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.note-group {
|
||||
margin: 1.2em 0 0.4em;
|
||||
font-size: 0.85em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--heading3);
|
||||
}
|
||||
|
||||
/* SEARCH BAR */
|
||||
.search-form {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
width: 100%;
|
||||
padding: 0.45em;
|
||||
padding: 0.45em 0.6em;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
background: var(--background2);
|
||||
color: var(--text1);
|
||||
}
|
||||
@ -196,6 +207,36 @@
|
||||
box-shadow: 0 0 6px #4ea1ff55;
|
||||
}
|
||||
|
||||
.search-submit,
|
||||
.sort-submit {
|
||||
padding: 0.45em 0.7em;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--background2);
|
||||
color: var(--text1);
|
||||
}
|
||||
|
||||
.search-submit:hover,
|
||||
.sort-submit:hover {
|
||||
background: var(--background-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sort-form {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.sort-dropdown {
|
||||
width: 100%;
|
||||
padding: 0.45em 0.6em;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--background2);
|
||||
color: var(--text1);
|
||||
}
|
||||
|
||||
/* MAIN CONTENT */
|
||||
main {
|
||||
flex: 1;
|
||||
@ -359,3 +400,83 @@
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* MOBILE */
|
||||
@media screen and (max-width: 900px) {
|
||||
body {
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
aside,
|
||||
main,
|
||||
.metadata-panel {
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
aside {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sidebar-toggle-label {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.6em 0.8em;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--background2);
|
||||
color: var(--text1);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: 0.8em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
display: none;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-toggle:checked + .sidebar-toggle-label + .sidebar-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.metadata-panel {
|
||||
display: none;
|
||||
width: 100%;
|
||||
border-left: 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.search-form,
|
||||
.sort-form {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-submit,
|
||||
.sort-submit {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.note-title a {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,10 +5,10 @@
|
||||
<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">
|
||||
<link rel="stylesheet" href="{{ .BasePath }}/static/css/main.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
{{ template "content" . }}
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@ -5,15 +5,15 @@
|
||||
|
||||
<section class="meta-block">
|
||||
<h3>Tags</h3>
|
||||
<form method="POST" action="/notes/{{ .Note.ID }}/tags" class="tag-form">
|
||||
<form method="POST" action="{{ .BasePath }}/notes/{{ .Note.ID }}/tags" class="tag-form">
|
||||
<input type="text" name="tag" class="tag-input" placeholder="Add tag">
|
||||
<input type="submit" value="add" class="tag-submit" />
|
||||
</form>
|
||||
<div class="meta-tags">
|
||||
{{ range .Note.Tags }}
|
||||
<span class="tag-chip">
|
||||
<a class="tag-link" href="/?tag={{ . | urlquery }}">{{ . }}</a>
|
||||
<form method="POST" action="/notes/{{ $.Note.ID }}/tags/{{ . | urlquery }}" class="tag-remove-form">
|
||||
<a class="tag-link" href="{{ $.BasePath }}/?tag={{ . | urlquery }}&active={{ $.Note.ID }}">{{ . }}</a>
|
||||
<form method="POST" action="{{ $.BasePath }}/notes/{{ $.Note.ID }}/tags/{{ . | urlquery }}" class="tag-remove-form">
|
||||
<button type="submit" class="tag-remove" aria-label="Remove tag {{ . }}">×</button>
|
||||
</form>
|
||||
</span>
|
||||
@ -24,29 +24,12 @@
|
||||
<section class="meta-block">
|
||||
<h3>File Info</h3>
|
||||
<ul class="meta-list">
|
||||
<li><strong>Last Modified:</strong>{{ .Note.GetUpdateDateRep }}</li>
|
||||
<li><strong>Size:</strong> {{ .Note.Size }}</li>
|
||||
<li><strong>Path:</strong> {{ .Note.Path }}</li>
|
||||
<li><strong>Hash:</strong> {{ .Note.ID }}</li>
|
||||
<li><strong>Size:</strong> {{ .Note.GetSizeKB }}</li>
|
||||
<li><strong>Last Modified:</strong>{{ .Note.GetUpdateDateRep }}</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="meta-block">
|
||||
<h3>Category</h3>
|
||||
<div class="meta-category">Software Engineering</div>
|
||||
</section>
|
||||
|
||||
<section class="meta-block">
|
||||
<h3>Document Stats</h3>
|
||||
<ul class="meta-list">
|
||||
<li><strong>Word Count:</strong> 542</li>
|
||||
<li><strong>Unique Words:</strong> 211</li>
|
||||
</ul>
|
||||
|
||||
<!-- Placeholder for future stats such as word cloud -->
|
||||
<div class="meta-stats-placeholder">
|
||||
<p>Word cloud / stats visualization<br>(future)</p>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@ -1,39 +1,51 @@
|
||||
{{ 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 notes or tags... (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" . }}
|
||||
<input type="checkbox" id="sidebar-toggle" class="sidebar-toggle">
|
||||
<label for="sidebar-toggle" class="sidebar-toggle-label">Notes</label>
|
||||
<div class="sidebar-content">
|
||||
<header>
|
||||
<h1 class="main-logo"><a href="{{ .BasePath }}/">Donnie Marko</a></h1>
|
||||
<form method="GET" action="{{ .BasePath }}/" class="search-form">
|
||||
<input type="text" name="search" class="search-bar" placeholder="Search notes or tags... (empty query to clear)">
|
||||
{{ if ne .LastActive "" }}<input type="hidden" name="active" value="{{ .LastActive }}">{{ end }}
|
||||
<input type="submit" value="ok" class="search-submit"/>
|
||||
</form>
|
||||
<form method="GET" action="{{ .BasePath }}/" class="sort-form">
|
||||
<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>
|
||||
{{ if ne .LastActive "" }}<input type="hidden" name="active" value="{{ .LastActive }}">{{ end }}
|
||||
<input type="submit" value="sort" class="sort-submit"/>
|
||||
</form>
|
||||
</header>
|
||||
{{ template "renderSearch" . }}
|
||||
</div>
|
||||
</aside>
|
||||
{{ end }}
|
||||
|
||||
{{ define "renderSearch" }}
|
||||
{{ if ne .SearchTerm "" }}<h2>Matching results for query '{{ .SearchTerm }}' (notes or tags)</h2>{{ end }}
|
||||
{{ if ne .TagFilter "" }}<h2>Filtered by tag '{{ .TagFilter }}'</h2>{{ end }}
|
||||
{{ if eq (len .Groups) 0 }}
|
||||
<p class="meta-empty">No notes match your filters.</p>
|
||||
{{ end }}
|
||||
{{ range .Groups }}
|
||||
<h3 class="note-group">{{ .Name }}</h3>
|
||||
<ul class="search-results">
|
||||
{{ range .Notes }}
|
||||
<li {{ if eq .ID $.LastActive }}class="active-note"{{ end }}>
|
||||
<div class="note-title">
|
||||
<a href="/notes/{{ .ID }}" data-hash="{{ .ID }}">{{ if ge (len .Title) 30 }}{{printf "%.30s" .Title }}[...]{{ else }} {{ .Title }}{{ end }}</a>
|
||||
<a href="{{ $.BasePath }}/notes/{{ .ID }}" data-hash="{{ .ID }}">{{ if ge (len .Title) 30 }}{{printf "%.30s" .Title }}[...]{{ else }} {{ .Title }}{{ end }}</a>
|
||||
</div>
|
||||
<span class="last-modified">{{ .GetUpdateDateRep }}</span>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user