feat: add Udemy courses and fix footer i18n + golangci-lint errors
- Add 5 Udemy courses with PDF certificate links (Go, Fyne, HTMX) - Fix cv-footer.html to use CV data instead of hardcoded values - Add i18n labels for footer (linkedin, github, domestika, email, phone) - Fix 11 golangci-lint errors: - errcheck: proper Close() error handling in email/security/tests - staticcheck: simplify return and merge variable declaration - unused: remove legacy sendEmail and formatMessage functions
This commit is contained in:
@@ -736,6 +736,23 @@
|
|||||||
],
|
],
|
||||||
"courseID": "codecademy-certifications"
|
"courseID": "codecademy-certifications"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Udemy Certifications",
|
||||||
|
"institution": "Udemy",
|
||||||
|
"courseLogo": "udemy.png",
|
||||||
|
"location": "Online",
|
||||||
|
"date": "2024-2025",
|
||||||
|
"duration": "Various",
|
||||||
|
"shortDescription": "Professional development courses in Go programming and modern web technologies through Udemy's comprehensive learning platform.",
|
||||||
|
"responsibilities": [
|
||||||
|
"<iconify-icon icon='simple-icons:go' width='60' height='60' class='default-company-icon' style='color: #00ADD8;'></iconify-icon><div><strong><a href='/static/pdf/udemy/Go - The Complete Guide.pdf' target='_blank'>Go - The Complete Guide</a></strong> <em>2024</em>: Comprehensive Go programming course covering fundamentals, concurrency, testing, and building production-ready applications</div>",
|
||||||
|
"<iconify-icon icon='simple-icons:go' width='60' height='60' class='default-company-icon' style='color: #00ADD8;'></iconify-icon><div><strong><a href='/static/pdf/udemy/Building a module in Go.pdf' target='_blank'>Building a Module in Go</a></strong> <em>2024</em>: Deep dive into Go modules, dependency management, versioning, and creating reusable packages</div>",
|
||||||
|
"<iconify-icon icon='simple-icons:go' width='60' height='60' class='default-company-icon' style='color: #00ADD8;'></iconify-icon><div><strong><a href='/static/pdf/udemy/Up and Running with Concurrency in Go.pdf' target='_blank'>Up and Running with Concurrency in Go</a></strong> <em>2024</em>: Advanced Go concurrency patterns including goroutines, channels, mutexes, and building concurrent applications</div>",
|
||||||
|
"<iconify-icon icon='simple-icons:go' width='60' height='60' class='default-company-icon' style='color: #00ADD8;'></iconify-icon><div><strong><a href='/static/pdf/udemy/Building GUI Applications with Fyne and Go.pdf' target='_blank'>Building GUI Applications with Fyne and Go</a></strong> <em>2024</em>: Desktop application development using the Fyne toolkit, creating cross-platform GUI applications with Go</div>",
|
||||||
|
"<iconify-icon icon='simple-icons:htmx' width='60' height='60' class='default-company-icon' style='color: #3366CC;'></iconify-icon><div><strong><a href='/static/pdf/udemy/HTMX - The Practical Guide.pdf' target='_blank'>HTMX - The Practical Guide</a></strong> <em>2024</em>: Modern web development with HTMX, building dynamic web applications with minimal JavaScript using hypermedia patterns</div>"
|
||||||
|
],
|
||||||
|
"courseID": "udemy-certifications"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "LinkedIn Learning Certifications",
|
"title": "LinkedIn Learning Certifications",
|
||||||
"institution": "LinkedIn Learning",
|
"institution": "LinkedIn Learning",
|
||||||
|
|||||||
@@ -741,6 +741,23 @@
|
|||||||
],
|
],
|
||||||
"courseID": "certificaciones-codecademy"
|
"courseID": "certificaciones-codecademy"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Certificaciones Udemy",
|
||||||
|
"institution": "Udemy",
|
||||||
|
"courseLogo": "udemy.png",
|
||||||
|
"location": "Online",
|
||||||
|
"date": "2024-2025",
|
||||||
|
"duration": "Varios",
|
||||||
|
"shortDescription": "Cursos de desarrollo profesional en programación Go y tecnologías web modernas a través de la plataforma de aprendizaje integral de Udemy.",
|
||||||
|
"responsibilities": [
|
||||||
|
"<iconify-icon icon='simple-icons:go' width='60' height='60' class='default-company-icon' style='color: #00ADD8;'></iconify-icon><div><strong><a href='/static/pdf/udemy/Go - The Complete Guide.pdf' target='_blank'>Go - The Complete Guide</a></strong> <em>2024</em>: Curso completo de programación Go cubriendo fundamentos, concurrencia, testing y construcción de aplicaciones listas para producción</div>",
|
||||||
|
"<iconify-icon icon='simple-icons:go' width='60' height='60' class='default-company-icon' style='color: #00ADD8;'></iconify-icon><div><strong><a href='/static/pdf/udemy/Building a module in Go.pdf' target='_blank'>Building a Module in Go</a></strong> <em>2024</em>: Profundización en módulos Go, gestión de dependencias, versionado y creación de paquetes reutilizables</div>",
|
||||||
|
"<iconify-icon icon='simple-icons:go' width='60' height='60' class='default-company-icon' style='color: #00ADD8;'></iconify-icon><div><strong><a href='/static/pdf/udemy/Up and Running with Concurrency in Go.pdf' target='_blank'>Up and Running with Concurrency in Go</a></strong> <em>2024</em>: Patrones avanzados de concurrencia en Go incluyendo goroutines, channels, mutexes y construcción de aplicaciones concurrentes</div>",
|
||||||
|
"<iconify-icon icon='simple-icons:go' width='60' height='60' class='default-company-icon' style='color: #00ADD8;'></iconify-icon><div><strong><a href='/static/pdf/udemy/Building GUI Applications with Fyne and Go.pdf' target='_blank'>Building GUI Applications with Fyne and Go</a></strong> <em>2024</em>: Desarrollo de aplicaciones de escritorio usando el toolkit Fyne, creando aplicaciones GUI multiplataforma con Go</div>",
|
||||||
|
"<iconify-icon icon='simple-icons:htmx' width='60' height='60' class='default-company-icon' style='color: #3366CC;'></iconify-icon><div><strong><a href='/static/pdf/udemy/HTMX - The Practical Guide.pdf' target='_blank'>HTMX - The Practical Guide</a></strong> <em>2024</em>: Desarrollo web moderno con HTMX, construyendo aplicaciones web dinámicas con JavaScript mínimo usando patrones hypermedia</div>"
|
||||||
|
],
|
||||||
|
"courseID": "certificaciones-udemy"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Certificaciones LinkedIn Learning",
|
"title": "Certificaciones LinkedIn Learning",
|
||||||
"institution": "LinkedIn Learning",
|
"institution": "LinkedIn Learning",
|
||||||
|
|||||||
+6
-1
@@ -36,7 +36,12 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"viewOnGithub": "View this project on GitHub",
|
"viewOnGithub": "View this project on GitHub",
|
||||||
"lastUpdated": "Last updated"
|
"lastUpdated": "Last updated",
|
||||||
|
"linkedin": "linkedin_",
|
||||||
|
"github": "github_",
|
||||||
|
"domestika": "domestika_",
|
||||||
|
"email": "email@",
|
||||||
|
"phone": "phone#"
|
||||||
},
|
},
|
||||||
"portfolio": {
|
"portfolio": {
|
||||||
"seeAllProjects": "See all projects on my",
|
"seeAllProjects": "See all projects on my",
|
||||||
|
|||||||
+6
-1
@@ -36,7 +36,12 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"viewOnGithub": "Ver este proyecto en GitHub",
|
"viewOnGithub": "Ver este proyecto en GitHub",
|
||||||
"lastUpdated": "Última actualización"
|
"lastUpdated": "Última actualización",
|
||||||
|
"linkedin": "linkedin_",
|
||||||
|
"github": "github_",
|
||||||
|
"domestika": "domestika_",
|
||||||
|
"email": "email@",
|
||||||
|
"phone": "teléfono#"
|
||||||
},
|
},
|
||||||
"portfolio": {
|
"portfolio": {
|
||||||
"seeAllProjects": "Ver todos los proyectos en mi",
|
"seeAllProjects": "Ver todos los proyectos en mi",
|
||||||
|
|||||||
@@ -44,11 +44,7 @@ func isTextBrowser(r *http.Request) bool {
|
|||||||
|
|
||||||
// Check Accept header - if client prefers text/plain
|
// Check Accept header - if client prefers text/plain
|
||||||
accept := r.Header.Get("Accept")
|
accept := r.Header.Get("Accept")
|
||||||
if strings.HasPrefix(accept, "text/plain") {
|
return strings.HasPrefix(accept, "text/plain")
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
@@ -72,10 +68,7 @@ func (h *CVHandler) PlainText(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check icons parameter (default: true)
|
// Check icons parameter (default: true)
|
||||||
showIcons := true
|
showIcons := r.URL.Query().Get("icons") != "false"
|
||||||
if r.URL.Query().Get("icons") == "false" {
|
|
||||||
showIcons = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare template data using shared helper (loads CV data)
|
// Prepare template data using shared helper (loads CV data)
|
||||||
data, err := h.prepareTemplateData(langCode)
|
data, err := h.prepareTemplateData(langCode)
|
||||||
|
|||||||
@@ -132,7 +132,11 @@ func logToSecurityFile(eventJSON []byte) {
|
|||||||
log.Printf("WARNING: Failed to open security log file: %v", err)
|
log.Printf("WARNING: Failed to open security log file: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer func() {
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
log.Printf("WARNING: Failed to close security log file: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Write event with newline
|
// Write event with newline
|
||||||
if _, err := f.Write(eventJSON); err != nil {
|
if _, err := f.Write(eventJSON); err != nil {
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ type Sections struct {
|
|||||||
type Footer struct {
|
type Footer struct {
|
||||||
ViewOnGithub string `json:"viewOnGithub"`
|
ViewOnGithub string `json:"viewOnGithub"`
|
||||||
LastUpdated string `json:"lastUpdated"`
|
LastUpdated string `json:"lastUpdated"`
|
||||||
|
Linkedin string `json:"linkedin"`
|
||||||
|
Github string `json:"github"`
|
||||||
|
Domestika string `json:"domestika"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Portfolio labels
|
// Portfolio labels
|
||||||
|
|||||||
@@ -224,74 +224,7 @@ func (e *EmailService) sendMultipartEmail(subject, htmlBody, textBody, replyTo s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer func() { _ = client.Close() }()
|
||||||
|
|
||||||
// Authenticate
|
|
||||||
if err = client.Auth(auth); err != nil {
|
|
||||||
return fmt.Errorf("SMTP authentication failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set sender and recipient
|
|
||||||
if err = client.Mail(from); err != nil {
|
|
||||||
return fmt.Errorf("failed to set sender: %w", err)
|
|
||||||
}
|
|
||||||
if err = client.Rcpt(to); err != nil {
|
|
||||||
return fmt.Errorf("failed to set recipient: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send message
|
|
||||||
w, err := client.Data()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get data writer: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = w.Write([]byte(message))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to write message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = w.Close()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to close writer: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return client.Quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendEmail sends an email using SMTP (plain text only - legacy)
|
|
||||||
func (e *EmailService) sendEmail(subject, body string) error {
|
|
||||||
// Validate config
|
|
||||||
if e.config.SMTPHost == "" || e.config.SMTPPort == "" {
|
|
||||||
return fmt.Errorf("SMTP configuration incomplete")
|
|
||||||
}
|
|
||||||
if e.config.SMTPUser == "" || e.config.SMTPPassword == "" {
|
|
||||||
return fmt.Errorf("SMTP credentials missing")
|
|
||||||
}
|
|
||||||
if e.config.ToEmail == "" {
|
|
||||||
return fmt.Errorf("recipient email not configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build email message
|
|
||||||
from := e.config.FromEmail
|
|
||||||
if from == "" {
|
|
||||||
from = e.config.SMTPUser
|
|
||||||
}
|
|
||||||
|
|
||||||
to := e.config.ToEmail
|
|
||||||
message := e.formatMessage(from, to, subject, body)
|
|
||||||
|
|
||||||
// SMTP server address
|
|
||||||
addr := fmt.Sprintf("%s:%s", e.config.SMTPHost, e.config.SMTPPort)
|
|
||||||
|
|
||||||
// Setup authentication
|
|
||||||
auth := smtp.PlainAuth("", e.config.SMTPUser, e.config.SMTPPassword, e.config.SMTPHost)
|
|
||||||
|
|
||||||
// Connect to SMTP server with TLS
|
|
||||||
client, err := e.connectSMTP(addr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
// Authenticate
|
// Authenticate
|
||||||
if err = client.Auth(auth); err != nil {
|
if err = client.Auth(auth); err != nil {
|
||||||
@@ -342,7 +275,7 @@ func (e *EmailService) connectSMTP(addr string) (*smtp.Client, error) {
|
|||||||
}
|
}
|
||||||
client, err := smtp.NewClient(conn, e.config.SMTPHost)
|
client, err := smtp.NewClient(conn, e.config.SMTPHost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
_ = conn.Close()
|
||||||
return nil, fmt.Errorf("SMTP client creation failed: %w", err)
|
return nil, fmt.Errorf("SMTP client creation failed: %w", err)
|
||||||
}
|
}
|
||||||
return client, nil
|
return client, nil
|
||||||
@@ -411,23 +344,3 @@ func (e *EmailService) formatMultipartMessage(from, to, replyTo, subject, htmlBo
|
|||||||
|
|
||||||
return message.String()
|
return message.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatMessage formats an email message with proper headers (plain text only)
|
|
||||||
func (e *EmailService) formatMessage(from, to, subject, body string) string {
|
|
||||||
headers := make(map[string]string)
|
|
||||||
headers["From"] = from
|
|
||||||
headers["To"] = to
|
|
||||||
headers["Subject"] = subject
|
|
||||||
headers["MIME-Version"] = "1.0"
|
|
||||||
headers["Content-Type"] = "text/plain; charset=\"utf-8\""
|
|
||||||
headers["Date"] = time.Now().Format(time.RFC1123Z)
|
|
||||||
|
|
||||||
var message strings.Builder
|
|
||||||
for k, v := range headers {
|
|
||||||
message.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
|
|
||||||
}
|
|
||||||
message.WriteString("\r\n")
|
|
||||||
message.WriteString(body)
|
|
||||||
|
|
||||||
return message.String()
|
|
||||||
}
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -5,38 +5,38 @@
|
|||||||
<div class="actual-content">
|
<div class="actual-content">
|
||||||
<ul class="footer-content">
|
<ul class="footer-content">
|
||||||
<li>
|
<li>
|
||||||
<div class="footer-label">linkedin_</div>
|
<div class="footer-label">{{.UI.Footer.Linkedin}}</div>
|
||||||
<div class="footer-separator"><i class="fa fa-circle"></i></div>
|
<div class="footer-separator"><i class="fa fa-circle"></i></div>
|
||||||
<div class="footer-value">
|
<div class="footer-value">
|
||||||
<a href="{{.CV.Personal.LinkedIn}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.LinkedIn}}</a>
|
<a href="{{.CV.Personal.LinkedIn}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.LinkedIn}}</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div class="footer-label">github_</div>
|
<div class="footer-label">{{.UI.Footer.Github}}</div>
|
||||||
<div class="footer-separator"><i class="fa fa-circle"></i></div>
|
<div class="footer-separator"><i class="fa fa-circle"></i></div>
|
||||||
<div class="footer-value">
|
<div class="footer-value">
|
||||||
<a href="{{.CV.Personal.GitHub}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.GitHub}}</a>
|
<a href="{{.CV.Personal.GitHub}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.GitHub}}</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div class="footer-label">domestika_</div>
|
<div class="footer-label">{{.UI.Footer.Domestika}}</div>
|
||||||
<div class="footer-separator"><i class="fa fa-circle"></i></div>
|
<div class="footer-separator"><i class="fa fa-circle"></i></div>
|
||||||
<div class="footer-value">
|
<div class="footer-value">
|
||||||
<a href="{{.CV.Personal.Domestika}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.Domestika}}</a>
|
<a href="{{.CV.Personal.Domestika}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.Domestika}}</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div class="footer-label">email@</div>
|
<div class="footer-label">{{.UI.Footer.Email}}</div>
|
||||||
<div class="footer-separator"><i class="fa fa-circle"></i></div>
|
<div class="footer-separator"><i class="fa fa-circle"></i></div>
|
||||||
<div class="footer-value">
|
<div class="footer-value">
|
||||||
<a href="mailto:{{.CV.Personal.Email}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.Email}}</a>
|
<a href="mailto:{{.CV.Personal.Email}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.Email}}</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div class="footer-label">phone#</div>
|
<div class="footer-label">{{.UI.Footer.Phone}}</div>
|
||||||
<div class="footer-separator"><i class="fa fa-circle"></i></div>
|
<div class="footer-separator"><i class="fa fa-circle"></i></div>
|
||||||
<div class="footer-value">
|
<div class="footer-value">
|
||||||
<a href="tel:+34676875420" target="_blank" rel="noopener noreferrer">+34 676 875 420</a>
|
<a href="tel:{{.CV.Personal.Phone}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.Phone}}</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func TestSMTPConnection(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("TLS dial failed: %v", err)
|
t.Fatalf("TLS dial failed: %v", err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer func() { _ = conn.Close() }()
|
||||||
t.Log("TLS connection established (port 465 - implicit SSL)")
|
t.Log("TLS connection established (port 465 - implicit SSL)")
|
||||||
} else {
|
} else {
|
||||||
// STARTTLS
|
// STARTTLS
|
||||||
@@ -47,7 +47,7 @@ func TestSMTPConnection(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("SMTP dial failed: %v", err)
|
t.Fatalf("SMTP dial failed: %v", err)
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer func() { _ = client.Close() }()
|
||||||
|
|
||||||
if err := client.StartTLS(tlsConfig); err != nil {
|
if err := client.StartTLS(tlsConfig); err != nil {
|
||||||
t.Fatalf("STARTTLS failed: %v", err)
|
t.Fatalf("STARTTLS failed: %v", err)
|
||||||
@@ -71,7 +71,7 @@ func TestSMTPConnection(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("TLS dial failed: %v", err)
|
t.Fatalf("TLS dial failed: %v", err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
client, err = smtp.NewClient(conn, host)
|
client, err = smtp.NewClient(conn, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -88,7 +88,7 @@ func TestSMTPConnection(t *testing.T) {
|
|||||||
t.Fatalf("STARTTLS failed: %v", err)
|
t.Fatalf("STARTTLS failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer func() { _ = client.Close() }()
|
||||||
|
|
||||||
auth := smtp.PlainAuth("", user, pass, host)
|
auth := smtp.PlainAuth("", user, pass, host)
|
||||||
if err := client.Auth(auth); err != nil {
|
if err := client.Auth(auth); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user