feat: add social links to footer and optional company logo toggle

**Social Links in Footer (Page 2):**
- Replace address/phone with LinkedIn, GitHub, and Behance links
- Maintain email@ link
- All links are clickable and open in new tabs
- Footer displays social media profiles prominently

**Company Logo Toggle Feature:**
- Add "Show logos" toggle switch in top action bar
- Toggle displays company logos (48x48px) to the left of each experience item
- LinkedIn-style layout when logos are shown
- Logos hidden by default, optional display via toggle
- Graceful fallback: missing logos don't break layout (onerror handler)
- Logos directory created at static/images/logos/ with README

**Technical Implementation:**
- New CSS file: logo-toggle.css for toggle switch and logo layout
- JavaScript: toggleLogos() function for show/hide functionality
- Template updates: experience items now support flex layout with logos
- Action bar grid updated to accommodate 4 columns
- Logo display uses CSS class `.show-logos` on `.cv-paper`
- Print CSS: logos hidden in PDF exports by default

**User Experience:**
- Clean toggle switch UI with smooth animations
- Mobile responsive design
- Accessibility: proper ARIA labels for toggle
- Optional feature that doesn't clutter default view
- Professional LinkedIn-style appearance when enabled

Logos can be added to static/images/logos/ directory using filenames
from the companyLogo field in CV JSON data.
This commit is contained in:
juanatsap
2025-11-05 12:15:43 +00:00
parent 38bf09196e
commit 2c372eee49
30 changed files with 4306 additions and 42 deletions
+89
View File
@@ -0,0 +1,89 @@
package pdf
import (
"context"
"fmt"
"io"
"time"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp"
)
// Generator handles PDF generation using headless Chrome
type Generator struct {
timeout time.Duration
}
// NewGenerator creates a new PDF generator with the specified timeout
func NewGenerator(timeout time.Duration) *Generator {
if timeout == 0 {
timeout = 30 * time.Second
}
return &Generator{
timeout: timeout,
}
}
// GenerateFromURL generates a PDF from a given URL
func (g *Generator) GenerateFromURL(ctx context.Context, url string) ([]byte, error) {
// Create context with timeout
ctx, cancel := context.WithTimeout(ctx, g.timeout)
defer cancel()
// Create chromedp context
allocCtx, allocCancel := chromedp.NewContext(ctx)
defer allocCancel()
// Buffer to store PDF
var pdfBuffer []byte
// Run chromedp tasks
err := chromedp.Run(allocCtx,
// Navigate to URL
chromedp.Navigate(url),
// Wait for page to be ready
chromedp.WaitReady("body"),
// Small delay to ensure all content is loaded
chromedp.Sleep(500*time.Millisecond),
// Generate PDF with print-optimized settings
chromedp.ActionFunc(func(ctx context.Context) error {
var err error
pdfBuffer, _, err = page.PrintToPDF().
WithPrintBackground(true).
WithPreferCSSPageSize(true).
WithMarginTop(0).
WithMarginBottom(0).
WithMarginLeft(0).
WithMarginRight(0).
WithPaperWidth(8.27). // A4 width in inches
WithPaperHeight(11.69). // A4 height in inches
Do(ctx)
return err
}),
)
if err != nil {
return nil, fmt.Errorf("chromedp execution failed: %w", err)
}
if len(pdfBuffer) == 0 {
return nil, fmt.Errorf("generated PDF is empty")
}
return pdfBuffer, nil
}
// StreamFromURL generates a PDF and writes it to the provided writer
func (g *Generator) StreamFromURL(ctx context.Context, url string, w io.Writer) error {
pdfData, err := g.GenerateFromURL(ctx, url)
if err != nil {
return err
}
_, err = w.Write(pdfData)
return err
}