phase ii and phase iii
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user