From 1eaed831fde4abb01caff9ac737abc80463e3c5d Mon Sep 17 00:00:00 2001 From: Caio Kinzel Filho Date: Sun, 14 Dec 2025 19:41:19 +1000 Subject: [PATCH] feat(xp): add terminal-style ui for experience timeline Replace the traditional card-based timeline with a terminal-themed presentation to show career entries in a command-output style. This introduces a .terminal container, header (window controls and title), prompt/output lines, ASCII timeline connectors, compact formatted fields (role, period, tags, highlights, tech stack, URL), and a show-more interaction that reveals remaining entries. Also update computed properties (totalCompanies, remainingCount), utilities (formatting, duration, tag classes), and various CSS styles to support the terminal UI and responsive behavior. The change was needed to provide an alternative terminal UI option requested in the prompt, offering a compact, readable, and themed way to browse experiences while keeping existing data (highlights, technologies, websites) available in a CLI-like layout. --- src/pages/ExperiencePage.vue | 762 ++++++++++++++++++++++------------- 1 file changed, 483 insertions(+), 279 deletions(-) diff --git a/src/pages/ExperiencePage.vue b/src/pages/ExperiencePage.vue index a3982e9..6f86719 100644 --- a/src/pages/ExperiencePage.vue +++ b/src/pages/ExperiencePage.vue @@ -10,86 +10,180 @@
-
-
-
-
-
- +
+ +
+
+ + + +
+
caio@career ~ % career --list --verbose
+
+ + +
+ +
+ $ + career --list --verbose +
+ + +
+
+ Career Summary +
+
+ ══════════════════════════════════════════════════════════ +
+
+ Total Experience: + {{ yearsOfExperience }} years +
+
+ Companies: + {{ totalCompanies }} +
+
+ Current Status: + ● EMPLOYED +
+
+ + +
+ +
+ + + ──── + {{ + !experience.endDate ? "◉" : "○" + }} + ── +
+ + +
+ [{{ String(index + 1).padStart(2, "0") }}] + {{ experience.company }} + (current) +
+ + +
+
+ + Role: + {{ experience.position }} + via {{ getViaName(experience.via) }}
-
- +
+ + Period: + {{ formatDateRange(experience) }} + ({{ calculateDuration(experience) }})
-
-
-

+
+ + Tags: + + [{{ tag }}] + +
+
+ +
+ + +
+ + + {{ highlight }} +
+ +
+ +
+ + +
+ + Stack: + {{ + parseTechnologies(experience.technologies).join(", ") + }} +
+ + +
+ + URL: {{ experience.website }} - {{ experience.company }} - - {{ experience.company }} -

- - {{ experience.position }} - • via {{ getViaName(experience.via) }} - -
-
-
- {{ formatDateRange(experience) }} - {{ calculateDuration(experience) }}
- + + +
+ +
+
+
+ + +
+
+ +
+
+ ... {{ remainingCount }} more entries hidden ... +
+
+
+ $ + +
+
+ + +
@@ -102,38 +196,11 @@ import { experiencesConfig, type Experience } from "../domain/experience/data"; import BadgeGroup from "../components/molecules/BadgeGroup.vue"; - // Dynamically import all logos from assets/logos - const logoModules = import.meta.glob("../assets/logos/*.jpeg", { - eager: true, - import: "default", - }); - - // Build a map from slug to logo URL - const companyLogos: Record = {}; - for (const path in logoModules) { - const slug = path.replace("../assets/logos/", "").replace(".jpeg", ""); - companyLogos[slug] = logoModules[path] as string; - } - - function getCompanyLogo(experience: Experience): string | undefined { - return companyLogos[experience.slug]; - } - - // Via company logos and names mapping - const viaLogos: Record = { - toptal: companyLogos["toptal"], - tw: companyLogos["thoughtworks"], - }; - const viaNames: Record = { toptal: "Toptal", tw: "ThoughtWorks", }; - function getViaLogo(via: string): string | undefined { - return viaLogos[via]; - } - function getViaName(via: string): string { return viaNames[via] || via; } @@ -150,6 +217,15 @@ return currentYear - startYear; }); + const totalCompanies = computed(() => { + const companies = new Set(experiencesConfig.map((exp) => exp.company)); + return companies.size; + }); + + const remainingCount = computed(() => { + return experiencesConfig.length - INITIAL_VISIBLE_COUNT; + }); + const visibleExperiences = computed(() => { if (showAll.value) { return experiencesConfig; @@ -172,7 +248,9 @@ } }); - return Array.from(techSet).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + return Array.from(techSet).sort((a, b) => + a.toLowerCase().localeCompare(b.toLowerCase()) + ); }); function formatDateRange(experience: Experience): string { @@ -180,37 +258,37 @@ const startStr = formatMonth(startDate); if (!experience.endDate) { - return `${startStr} - present`; + return `${startStr} → present`; } const endDate = new Date(experience.endDate); const endStr = formatMonth(endDate); - return `${startStr} - ${endStr}`; + return `${startStr} → ${endStr}`; } function formatMonth(date: Date): string { const months = [ - "jan", - "feb", - "mar", - "apr", - "may", - "jun", - "jul", - "aug", - "sep", - "oct", - "nov", - "dec", + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", ]; - return `${months[date.getMonth()]}/${date.getFullYear()}`; + return `${months[date.getMonth()]} ${date.getFullYear()}`; } function calculateDuration(experience: Experience): string { const startDate = new Date(experience.startDate); const endDate = experience.endDate ? new Date(experience.endDate) : new Date(); - let months = + const months = (endDate.getFullYear() - startDate.getFullYear()) * 12 + (endDate.getMonth() - startDate.getMonth()); @@ -219,18 +297,14 @@ let duration = ""; if (years > 0) { - duration += `${years} ${years === 1 ? "year" : "years"}`; + duration += `${years}y`; } if (remainingMonths > 0) { - if (duration) duration += ", "; - duration += `${remainingMonths} ${remainingMonths === 1 ? "month" : "months"}`; + if (duration) duration += " "; + duration += `${remainingMonths}m`; } if (!duration) { - duration = "< 1 month"; - } - - if (!experience.endDate) { - duration += " (and counting)"; + duration = "<1m"; } return duration; @@ -239,6 +313,18 @@ function parseTechnologies(technologies: string): string[] { return technologies.split(",").map((tech) => tech.trim()); } + + function getTagClass(tag: string): string { + const classes: Record = { + remote: "tag-remote", + "on-site": "tag-onsite", + fintech: "tag-fintech", + startup: "tag-startup", + contract: "tag-contract", + enterprise: "tag-enterprise", + }; + return classes[tag] || ""; + }