feat: add Carbone tags help - view in-app modal and download as .docx
This commit is contained in:
@@ -473,6 +473,128 @@ router.get('/master-template', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/carbone/help-docx - Download tags reference as .docx
|
||||
router.get('/help-docx', (req, res) => {
|
||||
const helpPath = path.join(TEMPLATES_DIR, 'HELP_TAGS_REFERENCE.docx');
|
||||
if (fs.existsSync(helpPath)) {
|
||||
res.download(helpPath, 'Carbone_Tags_Reference.docx');
|
||||
} else {
|
||||
res.status(404).json({ error: 'Help file not found' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/carbone/help - Get tags reference as JSON (for in-app viewing)
|
||||
router.get('/help', (req, res) => {
|
||||
res.json({
|
||||
sections: [
|
||||
{
|
||||
title: '1. Personal Details',
|
||||
type: 'simple',
|
||||
tags: [
|
||||
{ tag: '{d.fullName}', desc: 'Full name (first + last)' },
|
||||
{ tag: '{d.firstName}', desc: 'First name only' },
|
||||
{ tag: '{d.lastName}', desc: 'Last name only' },
|
||||
{ tag: '{d.email}', desc: 'Email address' },
|
||||
{ tag: '{d.phone}', desc: 'Phone number' },
|
||||
{ tag: '{d.address}', desc: 'Physical address' },
|
||||
{ tag: '{d.linkedin}', desc: 'LinkedIn profile URL' },
|
||||
{ tag: '{d.github}', desc: 'GitHub profile URL' },
|
||||
{ tag: '{d.website}', desc: 'Personal website' },
|
||||
{ tag: '{d.summary}', desc: 'Professional summary paragraph' },
|
||||
{ tag: '{d.generationDate}', desc: 'Date the CV was generated' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '2. Work Experience (Loop)',
|
||||
type: 'loop',
|
||||
note: 'Carbone auto-detects loops from [i]. Section repeats for each job.',
|
||||
tags: [
|
||||
{ tag: '{d.experience[i].position}', desc: 'Job title' },
|
||||
{ tag: '{d.experience[i].company}', desc: 'Company name' },
|
||||
{ tag: '{d.experience[i].location}', desc: 'Work location' },
|
||||
{ tag: '{d.experience[i].startDate}', desc: 'Start date (Mon YYYY)' },
|
||||
{ tag: '{d.experience[i].endDate}', desc: 'End date or "Present"' },
|
||||
{ tag: '{d.experience[i].duration}', desc: 'Calculated duration (e.g., 4.5 years)' },
|
||||
{ tag: '{d.experience[i].description}', desc: 'Job description' },
|
||||
{ tag: '{d.experience[i].skillsUsed}', desc: 'Comma-separated skills used' },
|
||||
{ tag: '{d.experience[i].achievements[j]}', desc: 'Achievement bullet (sub-loop)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '3. Skills',
|
||||
type: 'loop',
|
||||
tags: [
|
||||
{ tag: '{d.skills[i].name}', desc: 'Skill name' },
|
||||
{ tag: '{d.skills[i].category}', desc: 'Skill category' },
|
||||
{ tag: '{d.skills[i].proficiency}', desc: 'Proficiency level' },
|
||||
{ tag: '{d.skills[i].years}', desc: 'Years of experience (dynamic)' },
|
||||
{ tag: '{d.skillsByCategory[i].category}', desc: 'Category name (grouped)' },
|
||||
{ tag: '{d.skillsByCategory[i].skills[j].name}', desc: 'Skill in category (grouped)' },
|
||||
{ tag: '{d.skillsByCategory[i].skills[j].years}', desc: 'Years in category (grouped)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '4. Education (Loop)',
|
||||
type: 'loop',
|
||||
tags: [
|
||||
{ tag: '{d.education[i].degree}', desc: 'Degree name' },
|
||||
{ tag: '{d.education[i].field}', desc: 'Field of study' },
|
||||
{ tag: '{d.education[i].institution}', desc: 'University/institution' },
|
||||
{ tag: '{d.education[i].startDate}', desc: 'Start date' },
|
||||
{ tag: '{d.education[i].endDate}', desc: 'End date' },
|
||||
{ tag: '{d.education[i].grade}', desc: 'Grade/result' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '5. Certifications (Loop)',
|
||||
type: 'loop',
|
||||
tags: [
|
||||
{ tag: '{d.certifications[i].name}', desc: 'Certification name' },
|
||||
{ tag: '{d.certifications[i].issuer}', desc: 'Issuing organization' },
|
||||
{ tag: '{d.certifications[i].date}', desc: 'Issue date' },
|
||||
{ tag: '{d.certifications[i].expiryDate}', desc: 'Expiry date (if any)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '6. Conditionals (Show/Hide)',
|
||||
type: 'simple',
|
||||
note: 'Show content only when a field has a value (or is empty).',
|
||||
tags: [
|
||||
{ tag: '{d.field:showBegin}...{d.field:showEnd}', desc: 'Show if NOT empty' },
|
||||
{ tag: '{d.field:hideBegin}...{d.field:hideEnd}', desc: 'Show if empty' },
|
||||
{ tag: '{d.field:ifEM(replacement text)}', desc: 'Show text if field is empty' },
|
||||
{ tag: '{d.field:ifNEM(replacement text)}', desc: 'Show text if field is not empty' },
|
||||
{ tag: '{d.field:ifEQ(value):showBegin}...{d.field:showEnd}', desc: 'Show if equals value' },
|
||||
{ tag: '{d.field:ifContain(text):showBegin}...{d.field:showEnd}', desc: 'Show if contains text' },
|
||||
{ tag: '{d.field:ifGT(5):showBegin}...{d.field:showEnd}', desc: 'Show if greater than value' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '7. Formatting',
|
||||
type: 'simple',
|
||||
tags: [
|
||||
{ tag: '{d.field:formatD(YYYY-MM-DD)}', desc: 'Format date: 2022-01-01' },
|
||||
{ tag: '{d.field:formatD(MMM YYYY)}', desc: 'Format date: Jan 2022' },
|
||||
{ tag: '{d.field:formatN(0.0)}', desc: 'Format number with 1 decimal' },
|
||||
{ tag: '{d.field:upperCase}', desc: 'Convert to UPPERCASE' },
|
||||
{ tag: '{d.field:lowerCase}', desc: 'Convert to lowercase' },
|
||||
{ tag: '{d.field:ucWords}', desc: 'Capitalize Each Word' },
|
||||
{ tag: '{d.field:substr(0, 100)}', desc: 'First 100 characters' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '8. Array Operations',
|
||||
type: 'simple',
|
||||
tags: [
|
||||
{ tag: '{d.skills:arrayJoin(, )}', desc: 'Join all items with separator' },
|
||||
{ tag: '{d.skills:arrayMap(name):arrayJoin(, )}', desc: 'Extract field and join' },
|
||||
{ tag: '{d.experience:count}', desc: 'Count items in array' }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
// POST /api/carbone/templates/upload - Upload a custom .docx template
|
||||
router.post('/templates/upload', upload.single('template'), (req, res) => {
|
||||
if (!req.file) {
|
||||
|
||||
BIN
renderer/carbone-templates/HELP_TAGS_REFERENCE.docx
Normal file
BIN
renderer/carbone-templates/HELP_TAGS_REFERENCE.docx
Normal file
Binary file not shown.
364
renderer/carbone-templates/HELP_TAGS_REFERENCE.md
Normal file
364
renderer/carbone-templates/HELP_TAGS_REFERENCE.md
Normal file
@@ -0,0 +1,364 @@
|
||||
# Carbone Template Tags Reference
|
||||
|
||||
This document lists all available Carbone tags you can use in your CV templates (.docx files).
|
||||
You can use these tags when editing your template in Microsoft Word or LibreOffice.
|
||||
|
||||
---
|
||||
|
||||
## 1. Personal Details (Simple Text Tags)
|
||||
|
||||
These tags are replaced with the candidate's personal information.
|
||||
|
||||
| Tag | Description | Example Output |
|
||||
|-----|-------------|----------------|
|
||||
| `{d.fullName}` | Full name (first + last) | John Smith |
|
||||
| `{d.firstName}` | First name only | John |
|
||||
| `{d.lastName}` | Last name only | Smith |
|
||||
| `{d.email}` | Email address | john.smith@email.com |
|
||||
| `{d.phone}` | Phone number | +1-555-123-4567 |
|
||||
| `{d.address}` | Physical address | Cape Town, South Africa |
|
||||
| `{d.linkedin}` | LinkedIn profile URL | linkedin.com/in/johnsmith |
|
||||
| `{d.github}` | GitHub profile URL | github.com/jsmith |
|
||||
| `{d.website}` | Personal website | www.johnsmith.com |
|
||||
| `{d.summary}` | Professional summary paragraph | Experienced software engineer... |
|
||||
| `{d.generationDate}` | Date the CV was generated | July 16, 2026 |
|
||||
|
||||
---
|
||||
|
||||
## 2. Work Experience (Loop Tags)
|
||||
|
||||
Loop tags repeat a section of your document for each item in an array.
|
||||
Carbone automatically detects loops when you use `[i]` in the tag name.
|
||||
|
||||
### Basic Loop Fields
|
||||
|
||||
| Tag | Description | Example Output |
|
||||
|-----|-------------|----------------|
|
||||
| `{d.experience[i].position}` | Job title | Senior Software Engineer |
|
||||
| `{d.experience[i].company}` | Company name | TechCorp International |
|
||||
| `{d.experience[i].location}` | Work location | Cape Town |
|
||||
| `{d.experience[i].startDate}` | Start date (formatted) | Jan 2022 |
|
||||
| `{d.experience[i].endDate}` | End date or "Present" | Present |
|
||||
| `{d.experience[i].duration}` | Calculated duration | 4.5 years |
|
||||
| `{d.experience[i].description}` | Job description | Led a team building microservices |
|
||||
| `{d.experience[i].skillsUsed}` | Comma-separated skills used | C#, Azure, Docker |
|
||||
|
||||
### Achievements Sub-Loop
|
||||
|
||||
Inside the experience section, you can loop through achievements:
|
||||
|
||||
| Tag | Description |
|
||||
|-----|-------------|
|
||||
| `{d.experience[i].achievements[j]}` | Individual achievement bullet point |
|
||||
|
||||
### How to Set Up a Loop in Word
|
||||
|
||||
In your Word document, simply write the tags in the section you want repeated.
|
||||
Carbone auto-detects the loop boundaries based on the document structure
|
||||
(paragraphs, table rows, or list items).
|
||||
|
||||
Example layout in Word:
|
||||
```
|
||||
{d.experience[i].position} at {d.experience[i].company}
|
||||
{d.experience[i].startDate} - {d.experience[i].endDate} ({d.experience[i].duration})
|
||||
{d.experience[i].description}
|
||||
|
||||
• {d.experience[i].achievements[j]}
|
||||
|
||||
Skills used: {d.experience[i].skillsUsed}
|
||||
```
|
||||
|
||||
For each job entry in the candidate's data, Carbone will duplicate this block
|
||||
and fill in the values.
|
||||
|
||||
---
|
||||
|
||||
## 3. Skills (Loop Tags)
|
||||
|
||||
### Flat Skills List
|
||||
|
||||
| Tag | Description | Example Output |
|
||||
|-----|-------------|----------------|
|
||||
| `{d.skills[i].name}` | Skill name | C# / .NET |
|
||||
| `{d.skills[i].category}` | Skill category | Programming |
|
||||
| `{d.skills[i].proficiency}` | Proficiency level | Expert |
|
||||
| `{d.skills[i].years}` | Years of experience (dynamic) | 8.5 |
|
||||
| `{d.skills[i].startYear}` | Year started | 2018 |
|
||||
|
||||
### Skills Grouped by Category
|
||||
|
||||
Use this when you want skills organized under category headings:
|
||||
|
||||
| Tag | Description | Example Output |
|
||||
|-----|-------------|----------------|
|
||||
| `{d.skillsByCategory[i].category}` | Category name | Programming |
|
||||
| `{d.skillsByCategory[i].skills[j].name}` | Skill name within category | C# / .NET |
|
||||
| `{d.skillsByCategory[i].skills[j].years}` | Years of experience | 8.5 |
|
||||
| `{d.skillsByCategory[i].skills[j].proficiency}` | Proficiency level | Expert |
|
||||
|
||||
Example layout in Word:
|
||||
```
|
||||
{d.skillsByCategory[i].category}:
|
||||
• {d.skillsByCategory[i].skills[j].name} ({d.skillsByCategory[i].skills[j].years}y)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Education (Loop Tags)
|
||||
|
||||
| Tag | Description | Example Output |
|
||||
|-----|-------------|----------------|
|
||||
| `{d.education[i].degree}` | Degree name | BSc Computer Science |
|
||||
| `{d.education[i].field}` | Field of study | Computer Science |
|
||||
| `{d.education[i].institution}` | University/institution | University of Cape Town |
|
||||
| `{d.education[i].startDate}` | Start date | Jan 2014 |
|
||||
| `{d.education[i].endDate}` | End date | Dec 2017 |
|
||||
| `{d.education[i].grade}` | Grade/result | First Class Honours |
|
||||
|
||||
---
|
||||
|
||||
## 5. Certifications (Loop Tags)
|
||||
|
||||
| Tag | Description | Example Output |
|
||||
|-----|-------------|----------------|
|
||||
| `{d.certifications[i].name}` | Certification name | Azure Developer Associate |
|
||||
| `{d.certifications[i].issuer}` | Issuing organization | Microsoft |
|
||||
| `{d.certifications[i].date}` | Issue date | Jun 2021 |
|
||||
| `{d.certifications[i].expiryDate}` | Expiry date (if any) | Jun 2024 |
|
||||
|
||||
---
|
||||
|
||||
## 6. Conditional Tags (Show/Hide Content)
|
||||
|
||||
Conditionals let you show or hide content based on whether a field has a value.
|
||||
|
||||
### Show if NOT Empty: `:showBegin` / `:showEnd`
|
||||
|
||||
Only shows the content between the markers if the field has a value:
|
||||
|
||||
```
|
||||
{d.linkedin:showBegin}LinkedIn: {d.linkedin}{d.linkedin:showEnd}
|
||||
```
|
||||
|
||||
If the candidate has no LinkedIn, the entire "LinkedIn: ..." text is hidden.
|
||||
|
||||
### Hide if NOT Empty: `:hideBegin` / `:hideEnd`
|
||||
|
||||
Shows content only if the field IS empty (opposite of showBegin):
|
||||
|
||||
```
|
||||
{d.website:hideBegin}No website provided{d.website:hideEnd}
|
||||
```
|
||||
|
||||
### If Empty: `:ifEM`
|
||||
|
||||
Shows the replacement text if the field is empty:
|
||||
|
||||
```
|
||||
{d.address:ifEM(Address not provided)}
|
||||
```
|
||||
|
||||
### If Not Empty: `:ifNEM`
|
||||
|
||||
Shows the replacement text if the field has a value:
|
||||
|
||||
```
|
||||
{d.email:ifNEM(Email verified)}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Formatting Tags
|
||||
|
||||
### Date Formatting: `:formatD`
|
||||
|
||||
Format dates inside your template:
|
||||
|
||||
```
|
||||
{d.experience[i].startDate:formatD(YYYY-MM-DD)} -> 2022-01-01
|
||||
{d.experience[i].startDate:formatD(MMM YYYY)} -> Jan 2022
|
||||
{d.experience[i].startDate:formatD(DD/MM/YYYY)} -> 01/01/2022
|
||||
```
|
||||
|
||||
Note: Our system already pre-formats dates as "Mon YYYY", so you typically
|
||||
don't need this formatter unless you want a different format.
|
||||
|
||||
### Number Formatting: `:formatN`
|
||||
|
||||
```
|
||||
{d.skills[i].years:formatN(0.0)} -> 8.5
|
||||
{d.skills[i].years:formatN(0)} -> 9
|
||||
```
|
||||
|
||||
### Case Conversion
|
||||
|
||||
```
|
||||
{d.fullName:upperCase} -> JOHN SMITH
|
||||
{d.fullName:lowerCase} -> john smith
|
||||
{d.fullName:ucFirst} -> John smith
|
||||
{d.fullName:ucWords} -> John Smith
|
||||
```
|
||||
|
||||
### Text Operations
|
||||
|
||||
```
|
||||
{d.summary:substr(0, 100)} -> First 100 characters of summary
|
||||
{d.summary:slice(0, 50)} -> First 50 characters
|
||||
{d.email:padl(30)} -> Padded to 30 chars on the left
|
||||
{d.email:padr(30)} -> Padded to 30 chars on the right
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Comparison Conditionals
|
||||
|
||||
These conditionals compare a field to a specific value:
|
||||
|
||||
### If Equal: `:ifEQ`
|
||||
|
||||
```
|
||||
{d.skills[i].proficiency:ifEQ(Expert):showBegin}
|
||||
This person is an expert!
|
||||
{d.skills[i].proficiency:showEnd}
|
||||
```
|
||||
|
||||
### If Not Equal: `:ifNE`
|
||||
|
||||
```
|
||||
{d.experience[i].endDate:ifNE():showBegin}
|
||||
(Former position)
|
||||
{d.experience[i].endDate:showEnd}
|
||||
```
|
||||
|
||||
### If Greater Than: `:ifGT`
|
||||
|
||||
```
|
||||
{d.skills[i].years:ifGT(5):showBegin}Senior level{d.skills[i].years:showEnd}
|
||||
```
|
||||
|
||||
### If Greater Than or Equal: `:ifGTE`
|
||||
|
||||
```
|
||||
{d.skills[i].years:ifGTE(3):showBegin}Experienced{d.skills[i].years:showEnd}
|
||||
```
|
||||
|
||||
### If Less Than: `:ifLT`
|
||||
|
||||
```
|
||||
{d.skills[i].years:ifLT(2):showBegin}Junior level{d.skills[i].years:showEnd}
|
||||
```
|
||||
|
||||
### If Contains: `:ifContain`
|
||||
|
||||
```
|
||||
{d.skills[i].name:ifContain(Azure):showBegin}Cloud certified{d.skills[i].name:showEnd}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Array Operations
|
||||
|
||||
### Array Join: `:arrayJoin`
|
||||
|
||||
Join all items in an array with a separator:
|
||||
|
||||
```
|
||||
{d.skills:arrayJoin(, )} -> C#, Azure, React, Docker
|
||||
```
|
||||
|
||||
### Array Map: `:arrayMap`
|
||||
|
||||
Extract a specific field from all array items:
|
||||
|
||||
```
|
||||
{d.skills:arrayMap(name):arrayJoin(, )} -> C#, Azure, React, Docker
|
||||
```
|
||||
|
||||
### Count: `:count`
|
||||
|
||||
Count items in an array:
|
||||
|
||||
```
|
||||
{d.experience:count} -> 3
|
||||
{d.skills:count} -> 7
|
||||
```
|
||||
|
||||
### Length: `:len`
|
||||
|
||||
String length:
|
||||
|
||||
```
|
||||
{d.summary:len} -> 145
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Complete Example Template
|
||||
|
||||
Here is how a complete section of your Word document might look:
|
||||
|
||||
```
|
||||
{d.fullName}
|
||||
{d.email} | {d.phone} | {d.address}
|
||||
{d.linkedin:showBegin}LinkedIn: {d.linkedin} | {d.linkedin:showEnd}
|
||||
{d.github:showBegin}GitHub: {d.github}{d.github:showEnd}
|
||||
|
||||
PROFESSIONAL SUMMARY
|
||||
{d.summary}
|
||||
|
||||
WORK EXPERIENCE
|
||||
{d.experience[i].position} at {d.experience[i].company}
|
||||
{d.experience[i].startDate} - {d.experience[i].endDate} ({d.experience[i].duration})
|
||||
{d.experience[i].location:showBegin}Location: {d.experience[i].location}{d.experience[i].location:showEnd}
|
||||
{d.experience[i].description}
|
||||
|
||||
Achievements:
|
||||
• {d.experience[i].achievements[j]}
|
||||
|
||||
Skills used: {d.experience[i].skillsUsed}
|
||||
|
||||
SKILLS
|
||||
{d.skillsByCategory[i].category}:
|
||||
• {d.skillsByCategory[i].skills[j].name} ({d.skillsByCategory[i].skills[j].years}y)
|
||||
{d.skillsByCategory[i].skills[j].proficiency:showBegin} - {d.skillsByCategory[i].skills[j].proficiency}{d.skillsByCategory[i].skills[j].proficiency:showEnd}
|
||||
|
||||
EDUCATION
|
||||
{d.education[i].degree} in {d.education[i].field}
|
||||
{d.education[i].institution}
|
||||
{d.education[i].startDate} - {d.education[i].endDate}
|
||||
{d.education[i].grade:showBegin}Grade: {d.education[i].grade}{d.education[i].grade:showEnd}
|
||||
|
||||
CERTIFICATIONS
|
||||
• {d.certifications[i].name} - {d.certifications[i].issuer} ({d.certifications[i].date})
|
||||
|
||||
Generated on {d.generationDate}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Card
|
||||
|
||||
| Syntax | Meaning |
|
||||
|--------|---------|
|
||||
| `{d.field}` | Simple text replacement |
|
||||
| `{d.array[i].field}` | Loop - repeats for each array item |
|
||||
| `{d.array[i].subarray[j].field}` | Nested loop |
|
||||
| `:showBegin` / `:showEnd` | Show content if field is not empty |
|
||||
| `:hideBegin` / `:hideEnd` | Show content if field is empty |
|
||||
| `:ifEM(text)` | Show text if field is empty |
|
||||
| `:ifNEM(text)` | Show text if field is not empty |
|
||||
| `:ifEQ(value)` | Show if field equals value |
|
||||
| `:ifNE(value)` | Show if field does not equal value |
|
||||
| `:ifGT(value)` | Show if field is greater than value |
|
||||
| `:ifGTE(value)` | Show if field is greater than or equal |
|
||||
| `:ifLT(value)` | Show if field is less than value |
|
||||
| `:ifLTE(value)` | Show if field is less than or equal |
|
||||
| `:ifContain(text)` | Show if field contains text |
|
||||
| `:formatD(YYYY-MM-DD)` | Format date |
|
||||
| `:formatN(0.0)` | Format number |
|
||||
| `:upperCase` / `:lowerCase` | Case conversion |
|
||||
| `:ucFirst` / `:ucWords` | Capitalize first letter / each word |
|
||||
| `:arrayJoin(, )` | Join array with separator |
|
||||
| `:count` | Count array items |
|
||||
| `:substr(0, 100)` | Substring |
|
||||
| `:padl(30)` / `:padr(30)` | Pad string to length |
|
||||
@@ -150,3 +150,37 @@ async function deleteCarboneTemplate(templateId) {
|
||||
toast('Delete failed: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// TAGS HELP - View in app
|
||||
// ============================================================
|
||||
async function viewTagsHelp() {
|
||||
try {
|
||||
const resp = await fetch(CARBONE_API + '/help');
|
||||
const data = await resp.json();
|
||||
|
||||
let html = '<div style="max-height:600px;overflow-y:auto">';
|
||||
for (const section of data.sections) {
|
||||
html += `<h3 style="margin-top:20px;color:var(--accent)">${section.title}</h3>`;
|
||||
if (section.note) html += `<p class="text-muted text-sm" style="margin-bottom:8px">${section.note}</p>`;
|
||||
html += '<table><thead><tr><th>Tag</th><th>Description</th></tr></thead><tbody>';
|
||||
for (const t of section.tags) {
|
||||
html += `<tr><td style="font-family:monospace;color:var(--accent);white-space:nowrap">${escapeHtml(t.tag)}</td><td>${escapeHtml(t.desc)}</td></tr>`;
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
}
|
||||
html += '</div>';
|
||||
html += '<div class="mt-16"><button class="btn btn-outline" onclick="downloadHelpDocx()">Download as .docx</button></div>';
|
||||
|
||||
showModal(html, 'Carbone Tags Reference');
|
||||
} catch (e) {
|
||||
toast('Failed to load help: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// TAGS HELP - Download .docx
|
||||
// ============================================================
|
||||
function downloadHelpDocx() {
|
||||
window.open(CARBONE_API + '/help-docx', '_blank');
|
||||
}
|
||||
@@ -73,6 +73,8 @@
|
||||
4. Generate CVs by merging candidate data with your template -> PDF
|
||||
</div>
|
||||
<button class="btn mt-16" onclick="downloadMasterTemplate()">Download Master Template (.docx)</button>
|
||||
<button class="btn btn-outline mt-16" style="margin-left:8px" onclick="viewTagsHelp()">View Tags Help</button>
|
||||
<button class="btn btn-outline mt-16" style="margin-left:8px" onclick="downloadHelpDocx()">Download Help (.docx)</button>
|
||||
</div>
|
||||
<div class="card mb-16">
|
||||
<div class="card-header"><span class="card-title">Upload Custom Template</span></div>
|
||||
|
||||
Reference in New Issue
Block a user