Change d'interface plus légére, modification side barre
This commit is contained in:
@ -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
607
internal/api/public.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user