Change d'interface plus légére, modification side barre

This commit is contained in:
2025-12-24 16:14:17 +01:00
parent cc1d6880a7
commit 917a31d5a8
46 changed files with 7484 additions and 298 deletions

View File

@ -71,6 +71,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// Public endpoints
if path == "/api/public/list" {
h.handlePublicList(w, r)
return
}
if path == "/api/public/toggle" {
h.handlePublicToggle(w, r)
return
}
// Legacy/HTML endpoints
if strings.HasPrefix(path, "/api/search") {
h.handleSearch(w, r)
@ -295,12 +305,14 @@ func (h *Handler) handleHome(w http.ResponseWriter, r *http.Request) {
Filename string
Content string
IsHome bool
IsPublic bool
Backlinks []BacklinkInfo
Breadcrumb template.HTML
}{
Filename: "🏠 Accueil - Index",
Content: content,
IsHome: true,
IsPublic: false,
Backlinks: nil, // Pas de backlinks pour la page d'accueil
Breadcrumb: h.generateBreadcrumb(""),
}
@ -351,7 +363,10 @@ func (h *Handler) generateHomeMarkdown(r *http.Request) string {
// Section des favoris (après les tags)
h.generateFavoritesSection(&sb, r)
// Section des notes récemment modifiées (après les favoris)
// Section des notes publiques (après les favoris)
h.generatePublicNotesSection(&sb, r)
// Section des notes récemment modifiées (après les notes publiques)
h.generateRecentNotesSection(&sb, r)
// Section de toutes les notes avec accordéon
@ -380,7 +395,7 @@ func (h *Handler) generateTagsSection(sb *strings.Builder) {
sb.WriteString("<div class=\"home-section\">\n")
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('tags')\">\n")
sb.WriteString(" <h2 class=\"home-section-title\">🏷️ Tags</h2>\n")
sb.WriteString(" <h2 class=\"home-section-title\"><i data-lucide=\"tags\" class=\"icon-sm\"></i> Tags</h2>\n")
sb.WriteString(" </div>\n")
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-tags\">\n")
sb.WriteString(" <div class=\"tags-cloud\">\n")
@ -408,7 +423,7 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder, r *http.Request)
sb.WriteString("<div class=\"home-section\">\n")
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('favorites')\">\n")
sb.WriteString(" <h2 class=\"home-section-title\"> " + h.t(r, "favorites.title") + "</h2>\n")
sb.WriteString(" <h2 class=\"home-section-title\"><i data-lucide=\"star\" class=\"icon-sm\"></i> " + h.t(r, "favorites.title") + "</h2>\n")
sb.WriteString(" </div>\n")
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-favorites\">\n")
sb.WriteString(" <div class=\"note-tree favorites-tree\">\n")
@ -420,7 +435,7 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder, r *http.Request)
// Dossier - avec accordéon
sb.WriteString(fmt.Sprintf(" <div class=\"folder indent-level-1\">\n"))
sb.WriteString(fmt.Sprintf(" <div class=\"folder-header\" onclick=\"toggleFolder('%s')\">\n", safeID))
sb.WriteString(fmt.Sprintf(" <span class=\"folder-icon\" id=\"icon-%s\">📁</span>\n", safeID))
sb.WriteString(fmt.Sprintf(" <span class=\"folder-icon\" id=\"icon-%s\"><i data-lucide=\"folder\" class=\"icon-sm\"></i></span>\n", safeID))
sb.WriteString(fmt.Sprintf(" <strong>%s</strong>\n", fav.Title))
sb.WriteString(fmt.Sprintf(" </div>\n"))
sb.WriteString(fmt.Sprintf(" <div class=\"folder-content\" id=\"folder-%s\">\n", safeID))
@ -445,6 +460,44 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder, r *http.Request)
sb.WriteString("</div>\n\n")
}
// generatePublicNotesSection génère la section des notes publiques
func (h *Handler) generatePublicNotesSection(sb *strings.Builder, r *http.Request) {
publicNotes, err := h.loadPublicNotes()
if err != nil || len(publicNotes.Notes) == 0 {
return
}
sb.WriteString("<div class=\"home-section\">\n")
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('public-notes')\">\n")
sb.WriteString(fmt.Sprintf(" <h2 class=\"home-section-title\"><i data-lucide=\"globe\" class=\"icon-sm\"></i> %s (%d)</h2>\n", h.t(r, "publicNotes.title"), len(publicNotes.Notes)))
sb.WriteString(" </div>\n")
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-public-notes\">\n")
sb.WriteString(" <div class=\"public-notes-list\">\n")
for _, note := range publicNotes.Notes {
filename := filepath.Base(note.Path)
htmlFile := filename[:len(filename)-3] + ".html"
publicURL := fmt.Sprintf("/public/%s", htmlFile)
sb.WriteString(" <div class=\"public-note-card\">\n")
sb.WriteString(fmt.Sprintf(" <div class=\"public-note-header\">\n"))
sb.WriteString(fmt.Sprintf(" <a href=\"#\" class=\"public-note-edit\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\" hx-push-url=\"true\" title=\"%s\">", note.Path, h.t(r, "publicNotes.editNote")))
sb.WriteString(fmt.Sprintf(" <span class=\"public-note-title\">%s</span>", note.Title))
sb.WriteString(" </a>\n")
sb.WriteString(fmt.Sprintf(" <a href=\"%s\" target=\"_blank\" class=\"public-note-view\" title=\"%s\">🌐</a>\n", publicURL, h.t(r, "publicNotes.viewPublic")))
sb.WriteString(" </div>\n")
sb.WriteString(fmt.Sprintf(" <div class=\"public-note-meta\">\n"))
sb.WriteString(fmt.Sprintf(" <span class=\"public-note-source\">📄 %s</span>\n", note.Path))
sb.WriteString(fmt.Sprintf(" <span class=\"public-note-date\"><i data-lucide=\"calendar\" class=\"icon-sm\"></i> %s</span>\n", note.PublishedAt.Format("02/01/2006")))
sb.WriteString(" </div>\n")
sb.WriteString(" </div>\n")
}
sb.WriteString(" </div>\n")
sb.WriteString(" </div>\n")
sb.WriteString("</div>\n\n")
}
// generateRecentNotesSection génère la section des notes récemment modifiées
func (h *Handler) generateRecentNotesSection(sb *strings.Builder, r *http.Request) {
recentDocs := h.idx.GetRecentDocuments(5)
@ -477,7 +530,7 @@ func (h *Handler) generateRecentNotesSection(sb *strings.Builder, r *http.Reques
sb.WriteString(fmt.Sprintf(" <a href=\"#\" class=\"recent-note-link\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\" hx-push-url=\"true\">\n", doc.Path))
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-title\">%s</div>\n", doc.Title))
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-meta\">\n"))
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-date\">📅 %s</span>\n", dateStr))
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-date\"><i data-lucide=\"calendar\" class=\"icon-sm\"></i> %s</span>\n", dateStr))
if len(doc.Tags) > 0 {
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-tags\">"))
for i, tag := range doc.Tags {
@ -523,7 +576,7 @@ func (h *Handler) generateFavoriteFolderContent(sb *strings.Builder, folderPath
// Sous-dossier
sb.WriteString(fmt.Sprintf("%s<div class=\"folder %s\">\n", indent, indentClass))
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-header\" onclick=\"toggleFolder('%s')\">\n", indent, safeID))
sb.WriteString(fmt.Sprintf("%s <span class=\"folder-icon\" id=\"icon-%s\">📁</span>\n", indent, safeID))
sb.WriteString(fmt.Sprintf("%s <span class=\"folder-icon\" id=\"icon-%s\"><i data-lucide=\"folder\" class=\"icon-sm\"></i></span>\n", indent, safeID))
sb.WriteString(fmt.Sprintf("%s <strong>%s</strong>\n", indent, name))
sb.WriteString(fmt.Sprintf("%s </div>\n", indent))
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-content\" id=\"folder-%s\">\n", indent, safeID))
@ -579,7 +632,7 @@ func (h *Handler) generateMarkdownTree(sb *strings.Builder, node *TreeNode, dept
indentClass := fmt.Sprintf("indent-level-%d", depth)
sb.WriteString(fmt.Sprintf("%s<div class=\"folder %s\">\n", indent, indentClass))
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-header\" onclick=\"toggleFolder('%s')\">\n", indent, safeID))
sb.WriteString(fmt.Sprintf("%s <span class=\"folder-icon\" id=\"icon-%s\">📁</span>\n", indent, safeID))
sb.WriteString(fmt.Sprintf("%s <span class=\"folder-icon\" id=\"icon-%s\"><i data-lucide=\"folder\" class=\"icon-sm\"></i></span>\n", indent, safeID))
sb.WriteString(fmt.Sprintf("%s <strong>%s</strong>\n", indent, node.Name))
sb.WriteString(fmt.Sprintf("%s </div>\n", indent))
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-content\" id=\"folder-%s\">\n", indent, safeID))
@ -688,12 +741,14 @@ func (h *Handler) createAndRenderNote(w http.ResponseWriter, r *http.Request, fi
Filename string
Content string
IsHome bool
IsPublic bool
Backlinks []BacklinkInfo
}{
Filename: filename,
Content: initialContent,
IsHome: false,
Backlinks: nil, // Pas de backlinks pour une nouvelle note
IsPublic: false, // Nouvelle note, pas publique par défaut
Backlinks: nil, // Pas de backlinks pour une nouvelle note
}
err = h.templates.ExecuteTemplate(w, "editor.html", data)
@ -832,12 +887,14 @@ func (h *Handler) handleGetNote(w http.ResponseWriter, r *http.Request, filename
Filename string
Content string
IsHome bool
IsPublic bool
Backlinks []BacklinkInfo
Breadcrumb template.HTML
}{
Filename: filename,
Content: string(content),
IsHome: false,
IsPublic: h.isPublic(filename),
Backlinks: backlinkData,
Breadcrumb: h.generateBreadcrumb(filename),
}
@ -1347,15 +1404,17 @@ func (h *Handler) handleFolderView(w http.ResponseWriter, r *http.Request) {
// Utiliser le template editor.html
data := struct {
Filename string
Content string
IsHome bool
Backlinks []BacklinkInfo
Filename string
Content string
IsHome bool
IsPublic bool
Backlinks []BacklinkInfo
Breadcrumb template.HTML
}{
Filename: cleanPath,
Content: content,
IsHome: true, // Pas d'édition pour une vue de dossier
IsPublic: false,
Backlinks: nil,
Breadcrumb: h.generateBreadcrumb(cleanPath),
}
@ -1370,7 +1429,7 @@ func (h *Handler) handleFolderView(w http.ResponseWriter, r *http.Request) {
// generateBreadcrumb génère un fil d'Ariane HTML cliquable
func (h *Handler) generateBreadcrumb(path string) template.HTML {
if path == "" {
return template.HTML(`<strong>📁 Racine</strong>`)
return template.HTML(`<strong><i data-lucide="folder" class="icon-sm"></i> Racine</strong>`)
}
parts := strings.Split(filepath.ToSlash(path), "/")
@ -1379,7 +1438,7 @@ func (h *Handler) generateBreadcrumb(path string) template.HTML {
sb.WriteString(`<span class="breadcrumb">`)
// Lien racine
sb.WriteString(`<a href="#" hx-get="/api/home" hx-target="#editor-container" hx-swap="innerHTML" hx-push-url="true" class="breadcrumb-link">📁 Racine</a>`)
sb.WriteString(`<a href="#" hx-get="/api/home" hx-target="#editor-container" hx-swap="innerHTML" hx-push-url="true" class="breadcrumb-link"><i data-lucide="folder" class="icon-sm"></i> Racine</a>`)
// Construire les liens pour chaque partie
currentPath := ""

607
internal/api/public.go Normal file
View File

@ -0,0 +1,607 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/mathieu/personotes/internal/indexer"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
)
// PublicNote représente une note partagée publiquement
type PublicNote struct {
Path string `json:"path"`
Title string `json:"title"`
PublishedAt time.Time `json:"published_at"`
}
// PublicNotesData contient la liste des notes publiques
type PublicNotesData struct {
Notes []PublicNote `json:"notes"`
}
// getPublicDirPath retourne le chemin du dossier public HTML
func (h *Handler) getPublicDirPath() string {
return filepath.Join(h.notesDir, "..", "public")
}
// getPublicNotesFilePath retourne le chemin du fichier .public.json
func (h *Handler) getPublicNotesFilePath() string {
return filepath.Join(h.notesDir, ".public.json")
}
// loadPublicNotes charge les notes publiques depuis le fichier JSON
func (h *Handler) loadPublicNotes() (*PublicNotesData, error) {
path := h.getPublicNotesFilePath()
// Si le fichier n'existe pas, retourner une liste vide
if _, err := os.Stat(path); os.IsNotExist(err) {
return &PublicNotesData{Notes: []PublicNote{}}, nil
}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var publicNotes PublicNotesData
if err := json.Unmarshal(data, &publicNotes); err != nil {
return nil, err
}
// Trier par date de publication (plus récent d'abord)
sort.Slice(publicNotes.Notes, func(i, j int) bool {
return publicNotes.Notes[i].PublishedAt.After(publicNotes.Notes[j].PublishedAt)
})
return &publicNotes, nil
}
// savePublicNotes sauvegarde les notes publiques dans le fichier JSON
func (h *Handler) savePublicNotes(publicNotes *PublicNotesData) error {
path := h.getPublicNotesFilePath()
data, err := json.MarshalIndent(publicNotes, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
// isPublic vérifie si une note est publique
func (h *Handler) isPublic(notePath string) bool {
publicNotes, err := h.loadPublicNotes()
if err != nil {
return false
}
for _, note := range publicNotes.Notes {
if note.Path == notePath {
return true
}
}
return false
}
// ensurePublicDir crée le dossier public s'il n'existe pas
func (h *Handler) ensurePublicDir() error {
publicDir := h.getPublicDirPath()
// Créer public/
if err := os.MkdirAll(publicDir, 0755); err != nil {
return err
}
// Créer public/static/
staticDir := filepath.Join(publicDir, "static")
if err := os.MkdirAll(staticDir, 0755); err != nil {
return err
}
return nil
}
// copyStaticAssets copie les fichiers CSS/fonts nécessaires
func (h *Handler) copyStaticAssets() error {
publicStaticDir := filepath.Join(h.getPublicDirPath(), "static")
// Fichiers à copier
filesToCopy := []string{
"static/theme.css",
"static/themes.css",
}
for _, file := range filesToCopy {
src := file
dst := filepath.Join(publicStaticDir, filepath.Base(file))
if err := copyFile(src, dst); err != nil {
h.logger.Printf("Avertissement: impossible de copier %s: %v", file, err)
// Continuer même si la copie échoue
}
}
return nil
}
// copyFile copie un fichier
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
return err
}
// generateNoteHTML génère le fichier HTML pour une note publique
func (h *Handler) generateNoteHTML(notePath string) error {
// Lire le fichier Markdown
absPath := filepath.Join(h.notesDir, notePath)
content, err := os.ReadFile(absPath)
if err != nil {
return fmt.Errorf("lecture fichier: %w", err)
}
// Extraire le front matter et le contenu
fm, body, err := indexer.ExtractFrontMatterAndBodyFromReader(bytes.NewReader(content))
if err != nil {
// Continuer avec le contenu complet si erreur
body = string(content)
}
// Déterminer le titre
title := filepath.Base(notePath)
if fm.Title != "" {
title = fm.Title
} else {
title = strings.TrimSuffix(title, ".md")
}
// Convertir Markdown en HTML avec goldmark
md := goldmark.New(
goldmark.WithExtensions(
extension.GFM,
extension.Typographer,
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
html.WithHardWraps(),
html.WithXHTML(),
),
)
var buf bytes.Buffer
if err := md.Convert([]byte(body), &buf); err != nil {
return fmt.Errorf("conversion markdown: %w", err)
}
// Générer le HTML complet standalone
htmlContent := h.generateStandaloneHTML(title, buf.String(), fm.Tags, fm.Date)
// Écrire le fichier HTML - structure plate avec seulement le nom du fichier
filename := filepath.Base(notePath)
outputPath := filepath.Join(h.getPublicDirPath(), strings.TrimSuffix(filename, ".md")+".html")
if err := os.WriteFile(outputPath, []byte(htmlContent), 0644); err != nil {
return fmt.Errorf("écriture fichier: %w", err)
}
h.logger.Printf("HTML généré: %s", outputPath)
return nil
}
// deleteNoteHTML supprime le fichier HTML d'une note
func (h *Handler) deleteNoteHTML(notePath string) error {
filename := filepath.Base(notePath)
outputPath := filepath.Join(h.getPublicDirPath(), strings.TrimSuffix(filename, ".md")+".html")
if err := os.Remove(outputPath); err != nil && !os.IsNotExist(err) {
return err
}
h.logger.Printf("HTML supprimé: %s", outputPath)
return nil
}
// generatePublicIndex génère le fichier index.html avec la liste des notes
func (h *Handler) generatePublicIndex() error {
publicNotes, err := h.loadPublicNotes()
if err != nil {
return fmt.Errorf("chargement notes publiques: %w", err)
}
// Enrichir avec les métadonnées
enrichedNotes := []map[string]interface{}{}
for _, note := range publicNotes.Notes {
filename := filepath.Base(note.Path)
item := map[string]interface{}{
"Path": strings.TrimSuffix(filename, ".md") + ".html",
"Title": note.Title,
"PublishedAt": note.PublishedAt.Format("02/01/2006"),
"Tags": []string{},
}
// Essayer de lire le fichier pour obtenir les métadonnées
absPath := filepath.Join(h.notesDir, note.Path)
if content, err := os.ReadFile(absPath); err == nil {
if fm, _, err := indexer.ExtractFrontMatterAndBodyFromReader(bytes.NewReader(content)); err == nil {
if fm.Title != "" {
item["Title"] = fm.Title
}
item["Tags"] = fm.Tags
}
}
enrichedNotes = append(enrichedNotes, item)
}
// Générer le HTML de l'index
htmlContent := h.generateIndexHTML(enrichedNotes)
// Écrire index.html
indexPath := filepath.Join(h.getPublicDirPath(), "index.html")
if err := os.WriteFile(indexPath, []byte(htmlContent), 0644); err != nil {
return fmt.Errorf("écriture index.html: %w", err)
}
h.logger.Printf("Index généré: %s", indexPath)
return nil
}
// handlePublicList retourne la liste des notes publiques
func (h *Handler) handlePublicList(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
return
}
publicNotes, err := h.loadPublicNotes()
if err != nil {
h.logger.Printf("Erreur chargement notes publiques: %v", err)
http.Error(w, "Erreur de chargement", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(publicNotes)
}
// handlePublicToggle bascule le statut public d'une note + génère/supprime HTML
func (h *Handler) handlePublicToggle(w http.ResponseWriter, r *http.Request) {
h.logger.Printf("handlePublicToggle appelé")
if r.Method != http.MethodPost {
h.logger.Printf("Méthode non autorisée: %s", r.Method)
http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
return
}
if err := r.ParseForm(); err != nil {
h.logger.Printf("Erreur ParseForm: %v", err)
http.Error(w, "Formulaire invalide", http.StatusBadRequest)
return
}
path := r.FormValue("path")
h.logger.Printf("Chemin reçu: %s", path)
if path == "" {
h.logger.Printf("Chemin vide")
http.Error(w, "Chemin requis", http.StatusBadRequest)
return
}
// Valider que le fichier existe
absPath := filepath.Join(h.notesDir, path)
if _, err := os.Stat(absPath); os.IsNotExist(err) {
http.Error(w, "Fichier introuvable", http.StatusNotFound)
return
}
publicNotes, err := h.loadPublicNotes()
if err != nil {
h.logger.Printf("Erreur chargement notes publiques: %v", err)
http.Error(w, "Erreur de chargement", http.StatusInternalServerError)
return
}
// Vérifier si la note est déjà publique
isCurrentlyPublic := false
newNotes := []PublicNote{}
for _, note := range publicNotes.Notes {
if note.Path == path {
isCurrentlyPublic = true
} else {
newNotes = append(newNotes, note)
}
}
if isCurrentlyPublic {
// Retirer du public - supprimer le HTML
publicNotes.Notes = newNotes
if err := h.deleteNoteHTML(path); err != nil {
h.logger.Printf("Erreur suppression HTML: %v", err)
}
} else {
// Ajouter au public - générer le HTML
title := filepath.Base(path)
title = strings.TrimSuffix(title, ".md")
// Essayer de lire le titre depuis le fichier
if content, err := os.ReadFile(absPath); err == nil {
if fm, _, err := indexer.ExtractFrontMatterAndBodyFromReader(bytes.NewReader(content)); err == nil && fm.Title != "" {
title = fm.Title
}
}
newNote := PublicNote{
Path: path,
Title: title,
PublishedAt: time.Now(),
}
publicNotes.Notes = append(publicNotes.Notes, newNote)
// Créer les dossiers nécessaires
if err := h.ensurePublicDir(); err != nil {
h.logger.Printf("Erreur création dossiers: %v", err)
http.Error(w, "Erreur de création des dossiers", http.StatusInternalServerError)
return
}
// Copier les assets statiques
if err := h.copyStaticAssets(); err != nil {
h.logger.Printf("Avertissement copie assets: %v", err)
}
// Générer le HTML de la note
if err := h.generateNoteHTML(path); err != nil {
h.logger.Printf("Erreur génération HTML: %v", err)
http.Error(w, "Erreur de génération HTML", http.StatusInternalServerError)
return
}
}
// Sauvegarder le fichier .public.json
if err := h.savePublicNotes(publicNotes); err != nil {
h.logger.Printf("Erreur sauvegarde notes publiques: %v", err)
http.Error(w, "Erreur de sauvegarde", http.StatusInternalServerError)
return
}
// Régénérer l'index
if err := h.generatePublicIndex(); err != nil {
h.logger.Printf("Erreur génération index: %v", err)
}
// Retourner le nouveau statut
status := "private"
if !isCurrentlyPublic {
status = "public"
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": status,
"path": path,
})
}
// generateStandaloneHTML génère un fichier HTML standalone complet
func (h *Handler) generateStandaloneHTML(title, content string, tags []string, date string) string {
tagsHTML := ""
if len(tags) > 0 {
tagsHTML = `<div class="public-tags">`
for _, tag := range tags {
tagsHTML += fmt.Sprintf(`<span class="tag">#%s</span>`, template.HTMLEscapeString(tag))
}
tagsHTML += `</div>`
}
dateHTML := ""
if date != "" {
dateHTML = fmt.Sprintf(`<span><i data-lucide="calendar" class="icon-sm"></i> %s</span>`, template.HTMLEscapeString(date))
}
return fmt.Sprintf(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>%s - PersoNotes</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="static/theme.css" />
<link rel="stylesheet" href="static/themes.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/atom-one-dark.min.css" />
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
body { margin: 0; padding: 0; background: var(--bg-primary, #1e1e1e); color: var(--text-primary, #e0e0e0); font-family: 'JetBrains Mono', monospace; }
.public-view-container { max-width: 800px; margin: 0 auto; padding: 2rem; }
.public-nav { margin-bottom: 2rem; }
.public-nav a { color: var(--accent-blue, #42a5f5); text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; }
.public-nav a:hover { text-decoration: underline; }
.public-header { margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 2px solid var(--bg-tertiary, #2d2d2d); }
.public-title { font-size: 2.5rem; margin: 0 0 1rem 0; color: var(--text-primary, #e0e0e0); }
.public-meta { display: flex; gap: 1.5rem; color: var(--text-secondary, #b0b0b0); font-size: 0.95rem; }
.public-tags { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1rem; }
.tag { background: var(--bg-tertiary, #2d2d2d); color: var(--text-secondary, #b0b0b0); padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.85rem; }
.public-content { line-height: 1.8; color: var(--text-primary, #e0e0e0); font-size: 1.05rem; }
.public-content h1, .public-content h2, .public-content h3, .public-content h4, .public-content h5, .public-content h6 { color: var(--text-primary, #e0e0e0); margin-top: 2rem; margin-bottom: 1rem; }
.public-content h1 { font-size: 2rem; } .public-content h2 { font-size: 1.75rem; } .public-content h3 { font-size: 1.5rem; } .public-content h4 { font-size: 1.25rem; }
.public-content p { margin-bottom: 1rem; }
.public-content a { color: var(--accent-blue, #42a5f5); text-decoration: none; }
.public-content a:hover { text-decoration: underline; }
.public-content pre { background: var(--bg-secondary, #252525); border: 1px solid var(--bg-tertiary, #2d2d2d); border-radius: 6px; padding: 1rem; overflow-x: auto; margin: 1rem 0; }
.public-content code { font-family: 'JetBrains Mono', monospace; background: var(--bg-secondary, #252525); padding: 0.2rem 0.4rem; border-radius: 3px; font-size: 0.9em; }
.public-content pre code { background: none; padding: 0; }
.public-content blockquote { border-left: 4px solid var(--accent-blue, #42a5f5); padding-left: 1rem; margin: 1rem 0; color: var(--text-secondary, #b0b0b0); font-style: italic; }
.public-content ul, .public-content ol { margin: 1rem 0; padding-left: 2rem; }
.public-content li { margin-bottom: 0.5rem; }
.public-content table { width: 100%%; border-collapse: collapse; margin: 1rem 0; }
.public-content th, .public-content td { border: 1px solid var(--bg-tertiary, #2d2d2d); padding: 0.75rem; text-align: left; }
.public-content th { background: var(--bg-secondary, #252525); font-weight: 600; }
.public-content img { max-width: 100%%; height: auto; border-radius: 6px; margin: 1rem 0; }
.public-content hr { border: none; border-top: 2px solid var(--bg-tertiary, #2d2d2d); margin: 2rem 0; }
</style>
</head>
<body>
<div class="public-view-container">
<div class="public-nav">
<a href="index.html">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to public notes
</a>
</div>
<article>
<div class="public-header">
<h1 class="public-title">%s</h1>
<div class="public-meta">%s</div>
%s
</div>
<div class="public-content">
%s
</div>
</article>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
});
</script>
<script>
// Initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
</script>
</body>
</html>`, template.HTMLEscapeString(title), template.HTMLEscapeString(title), dateHTML, tagsHTML, content)
}
// generateIndexHTML génère le HTML de la page d'index
func (h *Handler) generateIndexHTML(notes []map[string]interface{}) string {
notesHTML := ""
if len(notes) > 0 {
notesHTML = `<ul class="notes-list">`
for _, note := range notes {
path, _ := note["Path"].(string)
title, _ := note["Title"].(string)
publishedAt, _ := note["PublishedAt"].(string)
tags := []string{}
if tagsInterface, ok := note["Tags"].([]string); ok {
tags = tagsInterface
}
tagsHTML := ""
if len(tags) > 0 {
tagsHTML = `<div class="note-tags">`
for _, tag := range tags {
tagsHTML += fmt.Sprintf(`<span class="tag">#%s</span>`, template.HTMLEscapeString(tag))
}
tagsHTML += `</div>`
}
notesHTML += fmt.Sprintf(`
<li class="note-item">
<a href="%s">
<h2 class="note-title">%s</h2>
<div class="note-meta">
<span><i data-lucide="calendar" class="icon-sm"></i> Published on %s</span>
</div>
%s
</a>
</li>`, path, template.HTMLEscapeString(title), publishedAt, tagsHTML)
}
notesHTML += `</ul>`
} else {
notesHTML = `
<div class="empty-state">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
<p>No public notes yet</p>
</div>`
}
return fmt.Sprintf(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Public Notes - PersoNotes</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="static/theme.css" />
<link rel="stylesheet" href="static/themes.css" />
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
body { margin: 0; padding: 0; background: var(--bg-primary, #1e1e1e); color: var(--text-primary, #e0e0e0); font-family: 'JetBrains Mono', monospace; }
.public-container { max-width: 900px; margin: 0 auto; padding: 2rem; }
.public-header { text-align: center; margin-bottom: 3rem; padding-bottom: 1.5rem; border-bottom: 2px solid var(--bg-tertiary, #2d2d2d); }
.public-header h1 { font-size: 2.5rem; margin: 0 0 0.5rem 0; color: var(--text-primary, #e0e0e0); }
.public-header p { color: var(--text-secondary, #b0b0b0); font-size: 1.1rem; }
.notes-list { list-style: none; padding: 0; margin: 0; }
.note-item { background: var(--bg-secondary, #252525); border: 1px solid var(--bg-tertiary, #2d2d2d); border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; transition: transform 0.2s, box-shadow 0.2s; }
.note-item:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); }
.note-item a { text-decoration: none; color: inherit; display: block; }
.note-title { font-size: 1.5rem; font-weight: 600; margin: 0 0 0.5rem 0; color: var(--text-primary, #e0e0e0); }
.note-meta { display: flex; gap: 1rem; color: var(--text-secondary, #b0b0b0); font-size: 0.9rem; margin-bottom: 0.5rem; }
.note-tags { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.tag { background: var(--bg-tertiary, #2d2d2d); color: var(--text-secondary, #b0b0b0); padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.85rem; }
.empty-state { text-align: center; padding: 3rem; color: var(--text-secondary, #b0b0b0); }
.empty-state svg { width: 64px; height: 64px; margin-bottom: 1rem; opacity: 0.5; }
</style>
</head>
<body>
<div class="public-container">
<div class="public-header">
<h1><i data-lucide="book-open" class="icon-md"></i> Public Notes</h1>
<p>Discover my shared notes</p>
</div>
%s
</div>
<script>
// Initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
</script>
</body>
</html>`, notesHTML)
}