phase ii and phase iii

This commit is contained in:
juanatsap
2025-11-12 18:55:06 +00:00
parent d36b67d1f1
commit 8f2704e10a
9 changed files with 1303 additions and 191 deletions
+258
View File
@@ -84,6 +84,17 @@ func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
// Get current year
currentYear := time.Now().Year()
// Read user preferences from cookies
cvLength := getPreferenceCookie(r, "cv-length", "short")
cvLogos := getPreferenceCookie(r, "cv-logos", "show")
cvTheme := getPreferenceCookie(r, "cv-theme", "default")
// Prepare CV length class
cvLengthClass := "cv-short"
if cvLength == "long" {
cvLengthClass = "cv-long"
}
// Prepare template data
data := map[string]interface{}{
"CV": cv,
@@ -96,6 +107,9 @@ func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
"CanonicalURL": fmt.Sprintf("https://juan.andres.morenorub.io/?lang=%s", lang),
"AlternateEN": "https://juan.andres.morenorub.io/?lang=en",
"AlternateES": "https://juan.andres.morenorub.io/?lang=es",
"CVLengthClass": cvLengthClass,
"ShowLogos": (cvLogos == "show"),
"ThemeClean": (cvTheme == "clean"),
}
// Render template
@@ -566,3 +580,247 @@ func getGitRepoFirstCommitDate(repoPath string) string {
return ""
}
// ==============================================================================
// HTMX ENDPOINTS - Phase 2
// ==============================================================================
// prepareTemplateData prepares common template data used across handlers
func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, error) {
// Load CV data
cv, err := models.LoadCV(lang)
if err != nil {
return nil, err
}
// Load UI translations
ui, err := models.LoadUI(lang)
if err != nil {
return nil, err
}
// Calculate duration for each experience
for i := range cv.Experience {
cv.Experience[i].Duration = calculateDuration(
cv.Experience[i].StartDate,
cv.Experience[i].EndDate,
cv.Experience[i].Current,
lang,
)
}
// Process projects for dynamic dates
for i := range cv.Projects {
processProjectDates(&cv.Projects[i], lang)
}
// Split skills between left and right sidebars
skillsLeft, skillsRight := splitSkills(cv.Skills.Technical)
// Calculate years of experience
yearsOfExperience := calculateYearsOfExperience()
// Get current year
currentYear := time.Now().Year()
// Prepare template data
data := map[string]interface{}{
"CV": cv,
"UI": ui,
"Lang": lang,
"SkillsLeft": skillsLeft,
"SkillsRight": skillsRight,
"YearsOfExperience": yearsOfExperience,
"CurrentYear": currentYear,
"CanonicalURL": fmt.Sprintf("https://juan.andres.morenorub.io/?lang=%s", lang),
"AlternateEN": "https://juan.andres.morenorub.io/?lang=en",
"AlternateES": "https://juan.andres.morenorub.io/?lang=es",
}
return data, nil
}
// getPreferenceCookie gets a preference cookie value, returns default if not found
func getPreferenceCookie(r *http.Request, name string, defaultValue string) string {
cookie, err := r.Cookie(name)
if err != nil {
return defaultValue
}
return cookie.Value
}
// setPreferenceCookie sets a preference cookie (1 year expiry)
func setPreferenceCookie(w http.ResponseWriter, name string, value string) {
http.SetCookie(w, &http.Cookie{
Name: name,
Value: value,
Path: "/",
MaxAge: 365 * 24 * 60 * 60, // 1 year
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Secure: false, // Set to true in production with HTTPS
})
}
// ToggleLength handles CV length toggle (short/long)
func (h *CVHandler) ToggleLength(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Get current state
currentLength := getPreferenceCookie(r, "cv-length", "short")
// Toggle state
newLength := "long"
if currentLength == "long" {
newLength = "short"
}
// Save new state
setPreferenceCookie(w, "cv-length", newLength)
// Get language
lang := r.URL.Query().Get("lang")
if lang == "" {
lang = getPreferenceCookie(r, "cv-language", "en")
}
// Prepare template data
data, err := h.prepareTemplateData(lang)
if err != nil {
HandleError(w, r, DataLoadError(err, "CV"))
return
}
// Add length class to data
if newLength == "long" {
data["CVLengthClass"] = "cv-long"
} else {
data["CVLengthClass"] = "cv-short"
}
// Also read and preserve logo preference
cvLogos := getPreferenceCookie(r, "cv-logos", "show")
data["ShowLogos"] = (cvLogos == "show")
// Render cv-content template
tmpl, err := h.templates.Render("cv-content.html")
if err != nil {
HandleError(w, r, TemplateError(err, "cv-content.html"))
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := tmpl.Execute(w, data); err != nil {
HandleError(w, r, TemplateError(err, "cv-content.html"))
return
}
}
// ToggleLogos handles logo visibility toggle
func (h *CVHandler) ToggleLogos(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Get current state
currentLogos := getPreferenceCookie(r, "cv-logos", "show")
// Toggle state
newLogos := "hide"
if currentLogos == "hide" {
newLogos = "show"
}
// Save new state
setPreferenceCookie(w, "cv-logos", newLogos)
// Get language
lang := r.URL.Query().Get("lang")
if lang == "" {
lang = getPreferenceCookie(r, "cv-language", "en")
}
// Prepare template data
data, err := h.prepareTemplateData(lang)
if err != nil {
HandleError(w, r, DataLoadError(err, "CV"))
return
}
// Add logos class to data
data["ShowLogos"] = (newLogos == "show")
// Also read and preserve length preference
cvLength := getPreferenceCookie(r, "cv-length", "short")
if cvLength == "long" {
data["CVLengthClass"] = "cv-long"
} else {
data["CVLengthClass"] = "cv-short"
}
// Render cv-content template
tmpl, err := h.templates.Render("cv-content.html")
if err != nil {
HandleError(w, r, TemplateError(err, "cv-content.html"))
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := tmpl.Execute(w, data); err != nil {
HandleError(w, r, TemplateError(err, "cv-content.html"))
return
}
}
// ToggleTheme handles theme toggle (default/clean)
func (h *CVHandler) ToggleTheme(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Get current state
currentTheme := getPreferenceCookie(r, "cv-theme", "default")
// Toggle state
newTheme := "clean"
if currentTheme == "clean" {
newTheme = "default"
}
// Save new state
setPreferenceCookie(w, "cv-theme", newTheme)
// Get language
lang := r.URL.Query().Get("lang")
if lang == "" {
lang = getPreferenceCookie(r, "cv-language", "en")
}
// Prepare template data
data, err := h.prepareTemplateData(lang)
if err != nil {
HandleError(w, r, DataLoadError(err, "CV"))
return
}
// Add theme class to data
data["ThemeClean"] = (newTheme == "clean")
// Render full page to update container class
tmpl, err := h.templates.Render("index.html")
if err != nil {
HandleError(w, r, TemplateError(err, "index.html"))
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := tmpl.Execute(w, data); err != nil {
HandleError(w, r, TemplateError(err, "index.html"))
return
}
}
+5
View File
@@ -17,6 +17,11 @@ func Setup(cvHandler *handlers.CVHandler, healthHandler *handlers.HealthHandler)
mux.HandleFunc("/cv", cvHandler.CVContent)
mux.HandleFunc("/health", healthHandler.Check)
// HTMX endpoints for interactive controls
mux.HandleFunc("/toggle/length", cvHandler.ToggleLength)
mux.HandleFunc("/toggle/logos", cvHandler.ToggleLogos)
mux.HandleFunc("/toggle/theme", cvHandler.ToggleTheme)
// Protected PDF endpoint with rate limiting (3 requests/minute per IP)
pdfRateLimiter := middleware.NewRateLimiter(3, 1*time.Minute)
protectedPDFHandler := middleware.OriginChecker(