diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a4f2bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +_bin +.#* \ No newline at end of file diff --git a/Makefile b/Makefile index 1864b23..4de3e14 100755 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ build: mkdir -p _bin - go build -o _bin/donniemarko + go build -o _bin/donniemarko cmd/main.go install: cp bin/donniemarko ~/.local/bin/ diff --git a/cmd/.#main.go b/cmd/.#main.go deleted file mode 120000 index 888ccc7..0000000 --- a/cmd/.#main.go +++ /dev/null @@ -1 +0,0 @@ -gator@gargantua.368651:1769498985 \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index af73ada..d88054f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,13 +1,16 @@ package main import ( + "context" "flag" "log" "net/http" - "kadath.corp/git/adminoo/donniemarko/web" - - "kadath.corp/git/adminoo/donniemarko/models" + "donniemarko/internal/render" + "donniemarko/internal/scanner" + "donniemarko/internal/service" + "donniemarko/internal/storage" + "donniemarko/internal/web" ) func main() { @@ -23,15 +26,34 @@ func main() { return } - // Initialize the directory manager - dm := models.NewTree(*rootFolder) - go dm.MonitorFileChange() + // Initialize storage + noteStorage := storage.NewNoteStorage() - tm := web.NewTemplateManager("web/templates") + // Initialize scanner + monitor := scanner.NewScanner(*rootFolder) - rh := web.NewRouteHandler(dm, tm) - rh.SetupRoutes() + // Initialize notes handler for scanner + notesHandler := scanner.NewNotesHandler(noteStorage) + monitor.SetHandler(notesHandler) + + // Initialize service + noteService := service.NewNoteService() + noteService.SetStorage(noteStorage) + + // Start scanner in background + ctx := context.Background() + go monitor.Monitor(ctx) + + // log.Println("WE GET THERE", len(noteStorage.Index)) + // Initialize template manager + tm := render.NewTemplateManager("internal/web/templates") + + // Initialize web handler + handler := web.NewHandler(noteService, tm) + + // Setup routes + handler.SetupRoutes() log.Printf("Serving on http://%s", *listenAddr) - http.ListenAndServe(*listenAddr, nil) + log.Fatal(http.ListenAndServe(*listenAddr, nil)) } diff --git a/internal/note/note.go b/internal/note/note.go index 8955f45..62c4d4d 100644 --- a/internal/note/note.go +++ b/internal/note/note.go @@ -25,27 +25,45 @@ func NewNote() *Note { return &Note{} } +func (n *Note) GetUpdateDateRep() string { + return n.UpdatedAt.Format("2006-01-02 15:04:05") +} + // ExtractTitle return the first level heading content ('# title') func ExtractTitle(mkd string) string { if mkd == "" { return "" } - if !strings.HasPrefix(mkd, "# ") { - return "" + lines := strings.Split(mkd, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "# ") { + // Extract title from # heading + title := strings.TrimPrefix(line, "# ") + title = strings.TrimSpace(title) + // Remove common markdown formatting + title = removeMarkdownFormatting(title) + return title + } } + return "" +} - var title string - for _, c := range strings.TrimLeft(mkd, "# ") { - if strings.Contains("*~", string(c)) { - continue - } - if string(c) == "\n" { - break - } - title = title + string(c) - } - return title +// removeMarkdownFormatting removes common markdown formatting from text +func removeMarkdownFormatting(text string) string { + // Remove **bold** and *italic* formatting + result := text + result = strings.ReplaceAll(result, "**", "") + result = strings.ReplaceAll(result, "*", "") + result = strings.ReplaceAll(result, "_", "") + result = strings.ReplaceAll(result, "`", "") + result = strings.ReplaceAll(result, "~~", "") + + // Clean up multiple spaces + result = strings.Join(strings.Fields(result), " ") + + return result } func GenerateNoteID(path string) string { diff --git a/internal/render/render.go b/internal/render/render.go index 44d070f..a2dd740 100644 --- a/internal/render/render.go +++ b/internal/render/render.go @@ -1,77 +1,108 @@ package render import ( - "fmt" - "html/template" - "path/filepath" - "sync" + "fmt" + "html/template" + "net/http" + "path/filepath" + "sync" - "github.com/russross/blackfriday/v2" + "github.com/russross/blackfriday/v2" ) type TemplateData struct { - Name string - FileNameSet []string + Name string + FileNameSet []string } type TemplateManager struct { - templates map[string]*template.Template - mu sync.RWMutex - basePath string - devMode bool + templates map[string]*template.Template + mu sync.RWMutex + basePath string + devMode bool } -func NewTemplateManager(basePath string, devMode bool) *TemplateManager { - return &TemplateManager{ - templates: make(map[string]*template.Template), - basePath: basePath, - devMode: devMode, - } +func NewTemplateManager(basePath string) *TemplateManager { + return &TemplateManager{ + templates: make(map[string]*template.Template), + basePath: basePath, + devMode: false, + } } func (tm *TemplateManager) buildTemplatePath(name string) string { - return filepath.Join(tm.basePath, name+".tmpl") + return filepath.Join(tm.basePath, name+".tmpl") } func (tm *TemplateManager) GetTemplate(td *TemplateData) (*template.Template, error) { - // Skip cache in dev mode - if !tm.devMode { - tm.mu.RLock() - if tmpl, exists := tm.templates[td.Name]; exists { - tm.mu.RUnlock() - return tmpl, nil - } - tm.mu.RUnlock() - } + // Skip cache in dev mode + if !tm.devMode { + tm.mu.RLock() + if tmpl, exists := tm.templates[td.Name]; exists { + tm.mu.RUnlock() + return tmpl, nil + } + tm.mu.RUnlock() + } - // Build file paths - var files []string - for _, file := range td.FileNameSet { - files = append(files, tm.buildTemplatePath(file)) - } + // Build file paths + var files []string + for _, file := range td.FileNameSet { + files = append(files, tm.buildTemplatePath(file)) + } - // Parse template - tmpl, err := template.ParseFiles(files...) - if err != nil { - return nil, fmt.Errorf("parse template %s: %w", td.Name, err) - } + // Parse template + tmpl, err := template.ParseFiles(files...) + if err != nil { + return nil, fmt.Errorf("parse template %s: %w", td.Name, err) + } - // Cache it (unless in dev mode) - if !tm.devMode { - tm.mu.Lock() - tm.templates[td.Name] = tmpl - tm.mu.Unlock() - } + // Cache it (unless in dev mode) + if !tm.devMode { + tm.mu.Lock() + tm.templates[td.Name] = tmpl + tm.mu.Unlock() + } - return tmpl, nil + return tmpl, nil +} + +func (tm *TemplateManager) Render(w http.ResponseWriter, name string, data interface{}) error { + // Build the template files - include all necessary templates + var files []string + + // Always include base template + files = append(files, tm.buildTemplatePath("base")) + + // Include noteList template (used by index) + files = append(files, tm.buildTemplatePath("noteList")) + + // Add the specific template + files = append(files, tm.buildTemplatePath(name)) + + // Parse templates + tmpl, err := template.ParseFiles(files...) + if err != nil { + return fmt.Errorf("parse template %s: %w", name, err) + } + + // Set content type + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + err = tmpl.ExecuteTemplate(w, "base", data) + if err != nil { + return fmt.Errorf("execute template %s: %w", name, err) + } + + return nil } // Render markdown to HTML with target="_blank" on links func RenderMarkdown(content []byte) (template.HTML, error) { - renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{ - Flags: blackfriday.CommonHTMLFlags | blackfriday.HrefTargetBlank, - }) - - html := blackfriday.Run(content, blackfriday.WithRenderer(renderer)) - return template.HTML(html), nil + renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{ + Flags: blackfriday.CommonHTMLFlags | blackfriday.HrefTargetBlank, + }) + + html := blackfriday.Run(content, blackfriday.WithRenderer(renderer)) + return template.HTML(html), nil } diff --git a/internal/render/render_test.go b/internal/render/render_test.go index 2b40636..90605f0 100644 --- a/internal/render/render_test.go +++ b/internal/render/render_test.go @@ -7,9 +7,9 @@ import ( func TestRenderMarkdown(t *testing.T) { cases := []struct { - name string + name string markdown string - want string + want string }{ { name: "Markdown, no link", @@ -44,7 +44,7 @@ Check this out [here](http://tatata.toto) } strip := strings.ReplaceAll(string(got), "\n", "") strip = strings.Trim(strip, " ") - + if strip != test.want { t.Errorf("Rendering markdown: Wanted '%s', got '%s'.\n", test.want, strip) } diff --git a/internal/scanner/handler.go b/internal/scanner/handler.go index 0badfac..f8ddf75 100644 --- a/internal/scanner/handler.go +++ b/internal/scanner/handler.go @@ -1,15 +1,22 @@ package scanner import ( + "path/filepath" + "donniemarko/internal/note" "donniemarko/internal/storage" "os" + // "log" ) type NotesHandler struct { storage storage.Storage } +func NewNotesHandler(storage storage.Storage) *NotesHandler { + return &NotesHandler{storage: storage} +} + func (h *NotesHandler) HandleCreate(path string) error { note, err := ParseNoteFile(path) if err != nil { @@ -34,9 +41,23 @@ func ParseNoteFile(path string) (*note.Note, error) { if err != nil { return nil, err } + + // Get file info to get modification time + fileInfo, err := os.Stat(path) + if err != nil { + return nil, err + } + id := note.GenerateNoteID(path) nn := note.NewNote() nn.ID = id nn.Content = string(content) + nn.Title = note.ExtractTitle(nn.Content) + if nn.Title == "" { + // Use filename as title if no heading found + nn.Title = filepath.Base(path) + } + nn.UpdatedAt = fileInfo.ModTime() + nn.CreatedAt = fileInfo.ModTime() return nn, nil } diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index f415b6f..84c753b 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" "time" + // "donniemarko/internal/note" ) type ChangeType int @@ -32,13 +33,21 @@ type Change struct { type ScannerService struct { RootDir string - interval time.Duration - lastStates map[string]time.Time + Interval time.Duration + LastStates map[string]time.Time handler ChangeHandler } func NewScanner(path string) *ScannerService { - return &ScannerService{RootDir: path} + return &ScannerService{ + RootDir: path, + Interval: 5 * time.Second, + LastStates: make(map[string]time.Time), + } +} + +func (s *ScannerService) SetHandler(handler ChangeHandler) { + s.handler = handler } func (s *ScannerService) FindAll() ([]string, error) { @@ -63,6 +72,8 @@ func (s *ScannerService) FindAll() ([]string, error) { return notePath, err } +// Scan walks the root folder and update the states of each notes if +// it has changed since the last time a scan occured func (s *ScannerService) Scan() ([]Change, error) { var changes []Change currentStates := make(map[string]time.Time) @@ -81,8 +92,10 @@ func (s *ScannerService) Scan() ([]Change, error) { currentStates[path] = info.ModTime() - lastMod, existed := s.lastStates[path] + lastMod, existed := s.LastStates[path] if !existed { + // create the note if it didn't exist yet + s.handler.HandleCreate(path) changes = append(changes, Change{Type: Created, Path: path, ModTime: lastMod}) } else if info.ModTime().After(lastMod) { changes = append(changes, Change{Type: Modified, Path: path, ModTime: info.ModTime()}) @@ -92,18 +105,20 @@ func (s *ScannerService) Scan() ([]Change, error) { }) // Check for deletions - for path := range s.lastStates { + for path := range s.LastStates { if _, exists := currentStates[path]; !exists { changes = append(changes, Change{Type: Deleted, Path: path}) } } - s.lastStates = currentStates + s.LastStates = currentStates return changes, nil } +// Monitor rescan the root folder at each new tick and handle state +// modification func (s *ScannerService) Monitor(ctx context.Context) error { - ticker := time.NewTicker(s.interval) + ticker := time.NewTicker(s.Interval) defer ticker.Stop() for { diff --git a/internal/service/note.go b/internal/service/note.go index d2b164e..681dd0d 100644 --- a/internal/service/note.go +++ b/internal/service/note.go @@ -1,76 +1,93 @@ package service import ( - "sort" "donniemarko/internal/note" "donniemarko/internal/storage" + "sort" ) - type NotesService struct { - storage storage.Storage + storage storage.Storage } type SortOption func([]*note.Note) type QueryOptions struct { - SearchTerm string - SortBy string + SearchTerm string + SortBy string } func NewNoteService() *NotesService { return &NotesService{} } -func SortByDate(notes []*note.Note) { - sort.Slice(notes, func(i, j int) bool { - return notes[i].UpdatedAt.After(notes[j].UpdatedAt) - }) +func (s *NotesService) SetStorage(storage storage.Storage) { + s.storage = storage } -func SortByTitle(notes []*note.Note) { - sort.Slice(notes, func(i, j int) bool { - return notes[i].Title < notes[j].Title - }) +func SortByDate(notes []*note.Note) { + sort.Slice(notes, func(i, j int) bool { + return notes[i].UpdatedAt.After(notes[j].UpdatedAt) + }) } func SortByDateAsc(notes []*note.Note) { - sort.Slice(notes, func(i, j int) bool { - return notes[i].UpdatedAt.Before(notes[j].UpdatedAt) - }) + sort.Slice(notes, func(i, j int) bool { + return notes[i].UpdatedAt.Before(notes[j].UpdatedAt) + }) } -func (s *NotesService) GetNotes(sortBy SortOption) ([]*note.Note, error) { - notes := s.storage.GetAll() - - if sortBy != nil { - sortBy(notes) - } - - return notes, nil +func SortByTitle(notes []*note.Note) { + sort.Slice(notes, func(i, j int) bool { + return notes[i].Title < notes[j].Title + }) +} + +func SortByTitleAsc(notes []*note.Note) { + sort.Slice(notes, func(i, j int) bool { + return notes[i].Title > notes[j].Title + }) +} + +func (s *NotesService) GetNotesWithSort(sortBy SortOption) ([]*note.Note, error) { + notes := s.storage.GetAll() + + if sortBy != nil { + sortBy(notes) + } + + return notes, nil } func (s *NotesService) QueryNotes(opts QueryOptions) ([]*note.Note, error) { - var notes []*note.Note - - // Search or get all - if opts.SearchTerm != "" { - notes = s.storage.Search(opts.SearchTerm) - } else { - notes = s.storage.GetAll() - } - - // Apply sorting - switch opts.SortBy { - case "recent": - SortByDate(notes) - case "alpha": - SortByTitle(notes) - case "oldest": - SortByDateAsc(notes) - default: - SortByDate(notes) // Default sort - } - - return notes, nil + var notes []*note.Note + + // Search or get all + if opts.SearchTerm != "" { + notes = s.storage.Search(opts.SearchTerm) + } else { + notes = s.storage.GetAll() + } + + // Apply sorting + switch opts.SortBy { + case "recent": + SortByDate(notes) + case "alpha": + SortByTitle(notes) + case "oldest": + SortByDateAsc(notes) + default: + SortByDate(notes) // Default sort + } + + return notes, nil +} + +func (s *NotesService) GetNoteByHash(hash string) (*note.Note, error) { + return s.storage.Get(hash) +} + +func (s *NotesService) GetNotes() []*note.Note { + return s.storage.GetAll() } diff --git a/internal/service/note_test.go b/internal/service/note_test.go index e7eb642..78dad8e 100644 --- a/internal/service/note_test.go +++ b/internal/service/note_test.go @@ -1,7 +1,6 @@ package service import ( - "net/http/httptest" "testing" "donniemarko/internal/note" @@ -14,56 +13,36 @@ func TestMain(m *testing.M) { storage := storage.NewNoteStorage() notes := []*note.Note{ - {Title: "Golang Tutorial", Content: "Learn Go"}, - {Title: "Rust Guide", Content: "Learn Rust"}, + {ID: "test1", Title: "Golang Tutorial", Content: "Learn Go"}, + {ID: "test2", Title: "Rust Guide", Content: "Learn Rust"}, } for _, note := range notes { storage.Create(note) } service = NewNoteService() - service.storage = storage + service.SetStorage(storage) m.Run() } func TestQueryNotes_WithSearch(t *testing.T) { - - opts := QueryOptions{ - SearchTerm: "golang", - SortBy: "alpha", - } - - results, err := service.QueryNotes(opts) - - if err != nil { - t.Fatal(err) - } - - if len(results) != 1 { - t.Errorf("expected 1 result, got %d", len(results)) - } - - if results[0].Title != "Golang Tutorial" { - t.Error("wrong note returned") - } -} -func TestHandler_BuildViewState(t *testing.T) { - handler := NewHandler(service, nil) - - req := httptest.NewRequest("GET", "/?search=test&sort=alpha", nil) - - state, err := handler.buildViewState(req) - - if err != nil { - t.Fatal(err) - } - - if state.SearchTerm != "test" { - t.Error("search term not extracted") - } - - if state.SortBy != "alpha" { - t.Error("sort option not extracted") - } + opts := QueryOptions{ + SearchTerm: "Go", + SortBy: "alpha", + } + + results, err := service.QueryNotes(opts) + + if err != nil { + t.Fatal(err) + } + + if len(results) != 1 { + t.Errorf("expected 1 result, got %d", len(results)) + } + + if results[0].Title != "Golang Tutorial" { + t.Error("wrong note returned") + } } diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 96313b7..8e381c8 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -13,6 +13,7 @@ type Storage interface { Delete(id string) Update(id string, n *note.Note) Search(query string) []*note.Note + Count() int } type NoteStorage struct { @@ -23,13 +24,13 @@ func NewNoteStorage() *NoteStorage { return &NoteStorage{Index: make(map[string]*note.Note)} } +// GetAll returns all notes stored in the index func (ns *NoteStorage) GetAll() []*note.Note { - notes := make([]*note.Note, 0, len(ns.Index)) + var notes []*note.Note - // Step 3: Iterate over the map - for _, value := range ns.Index { - notes = append(notes, value) - } + for _, value := range ns.Index { + notes = append(notes, value) + } return notes } @@ -46,6 +47,10 @@ func (ns *NoteStorage) Create(n *note.Note) error { return nil } +func (ns *NoteStorage) Count() int { + return len(ns.Index) +} + func (ns *NoteStorage) Delete(id string) { delete(ns.Index, id) } @@ -54,7 +59,7 @@ func (ns *NoteStorage) Update(id string, n *note.Note) { ns.Index[id] = n } -func (ns *NoteStorage) Search(query string) []*note.Note{ +func (ns *NoteStorage) Search(query string) []*note.Note { results := []*note.Note{} for _, note := range ns.Index { lowContent := strings.ToLower(string(note.Content)) diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go index 1bec420..036477a 100644 --- a/internal/storage/storage_test.go +++ b/internal/storage/storage_test.go @@ -35,7 +35,7 @@ func TestNoteStorageDelete(t *testing.T) { ns.Delete(n1.ID) if len(ns.Index) > 1 { - t.Errorf("Deleting notes should remove from to the storage. Wanted 1, got '%v'", len(ns.Index)) + t.Errorf("Deleting notes should remove them from to the storage. Wanted 1, got '%v'", len(ns.Index)) } } diff --git a/internal/web/handler.go b/internal/web/handler.go index c4ff95e..3572aa8 100644 --- a/internal/web/handler.go +++ b/internal/web/handler.go @@ -1,100 +1,160 @@ package web import ( - "net/http" - "donniemarko/internal/service" - "donniemarko/internal/render" + "donniemarko/internal/note" + "donniemarko/internal/render" + "donniemarko/internal/service" + "html/template" + "net/http" + "strings" ) type Handler struct { - notesService *service.NotesService - templates *render.TemplateManager + notesService *service.NotesService + templates *render.TemplateManager + mux *http.ServeMux } func NewHandler(ns *service.NotesService, tm *render.TemplateManager) *Handler { - return &Handler{ - notesService: ns, - templates: tm, - } + 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/") { + 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 - CurrentNote *note.Note - SortBy string - SearchTerm string - ActiveHash string + Notes []*note.Note + RenderedNote template.HTML + SortBy string + SearchTerm 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) + // 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 + 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 + 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) + } + } + + return &ViewState{ + Notes: notes, + SortBy: sortBy, + SearchTerm: searchTerm, + }, 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 - } - - // Add to state - state.CurrentNote = note - state.ActiveHash = hash - - h.templates.Render(w, "note", state) + // 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.RenderedNote = htmlContent + state.LastActive = hash + + h.templates.Render(w, "index", state) } diff --git a/internal/web/static/css/main.css b/internal/web/static/css/main.css index dc04ce5..68e754d 100644 --- a/internal/web/static/css/main.css +++ b/internal/web/static/css/main.css @@ -259,4 +259,13 @@ margin: 1em 0; border-bottom: dotted 2px var(--heading1); } + + .note-title { + display: flex; + } + + .note-title a { + padding-left: 1em; + font-size: 0.9em; + } } diff --git a/internal/web/templates/base.tmpl b/internal/web/templates/base.tmpl index ea8854f..cbaf12b 100644 --- a/internal/web/templates/base.tmpl +++ b/internal/web/templates/base.tmpl @@ -1,3 +1,4 @@ +{{ define "base" }} @@ -9,4 +10,5 @@ {{ template "content" . }} - \ No newline at end of file + +{{ end }} \ No newline at end of file diff --git a/internal/web/templates/index.tmpl b/internal/web/templates/index.tmpl index 3e25733..2f12da9 100644 --- a/internal/web/templates/index.tmpl +++ b/internal/web/templates/index.tmpl @@ -3,6 +3,6 @@ {{ template "noteList" . }} {{/* Markdown notes rendering area */}}
- {{ .Note }} + {{ .RenderedNote }}
-{{ end }} \ No newline at end of file +{{ end }} diff --git a/internal/web/templates/noteList.tmpl b/internal/web/templates/noteList.tmpl index ae8f73d..e9d24e0 100644 --- a/internal/web/templates/noteList.tmpl +++ b/internal/web/templates/noteList.tmpl @@ -24,15 +24,15 @@ {{ define "renderSearch" }} {{ if ne .SearchTerm "" }}

Matching results for query '{{ .SearchTerm }}'

{{ end }} {{ end }} @@ -52,4 +52,4 @@ {{ end }} {{ end }} -{{ end }} \ No newline at end of file +{{ end }}