diff --git a/content/public/portfolio-content.json b/content/public/portfolio-content.json index 67aa54c..391eabd 100644 --- a/content/public/portfolio-content.json +++ b/content/public/portfolio-content.json @@ -17,9 +17,9 @@ "profile": { "introduction": "I work across Linux infrastructure and full-stack delivery, pairing practical operations with software built to be understood, maintained, and improved over time.", "screeningFacts": [ - { "label": "Education", "value": "Bachelor's degree in Business Management IT, Romanian-American University, 2023–2026" }, + { "label": "Education", "value": "Bachelor's degree in Business Management IT, Romanian-American University, 2023-2026" }, { "label": "Languages", "value": "Romanian (native), English (C2), French (A1)" }, - { "label": "Certification", "value": "PCEP — Certified Entry-Level Python Programmer, 2022" } + { "label": "Certification", "value": "PCEP - Certified Entry-Level Python Programmer, 2022" } ] }, "experience": [ @@ -27,7 +27,7 @@ "id": "mihai-razus-pfa", "role": "Full-Stack Developer & DevOps collaborator", "organization": "Mihai Razus PFA", - "dates": "Aug 2025 – Present", + "dates": "Aug 2025 - Present", "highlights": [ "Contribute to full-stack delivery across application and deployment work.", "Support practical DevOps collaboration alongside software development." @@ -37,7 +37,7 @@ "id": "camera-consultantilor-fiscali", "role": "Green Innovation Intern", "organization": "Camera Consultanților Fiscali", - "dates": "Jun 2025 – Aug 2025", + "dates": "Jun 2025 - Aug 2025", "highlights": [ "Contributed to green innovation work in an internship setting.", "Applied structured problem-solving and digital skills to assigned work." @@ -47,7 +47,7 @@ "id": "isense-solutions", "role": "Data Science Intern", "organization": "iSense Solutions", - "dates": "Jun 2025 – Aug 2025", + "dates": "Jun 2025 - Aug 2025", "highlights": [ "Contributed to data science work in an internship setting.", "Applied analytical and programming skills to assigned tasks." diff --git a/scripts/verify-artifacts.mjs b/scripts/verify-artifacts.mjs index ab6562d..7a22bb6 100644 --- a/scripts/verify-artifacts.mjs +++ b/scripts/verify-artifacts.mjs @@ -213,10 +213,19 @@ const assertSemanticHtml = (html, inputs, contract) => { } for (const section of sections) { const label = inputs.content.navigation.find((item) => item.id === section.id)?.label; + if (section.id === 'start') { + const heroHeadings = [...section.querySelectorAll(':scope h1')] + .filter((node) => isVisibleSemanticNode(node, appRoot)); + if (heroHeadings.length !== 1 || heroHeadings[0].id !== 'start-heading' || + heroHeadings[0].textContent !== inputs.content.identity.name) { + throw new Error('Missing visible hero heading: start'); + } + continue; + } const headings = [...section.querySelectorAll(':scope > h2')] .filter((node) => isVisibleSemanticNode(node, appRoot)); if (label === undefined || headings.length !== 1 || headings[0].id !== `${section.id}-heading` || - headings[0].textContent !== label.toUpperCase()) { + headings[0].textContent !== label) { throw new Error(`Missing visible section heading: ${section.id}`); } } @@ -235,7 +244,7 @@ const assertSemanticHtml = (html, inputs, contract) => { } const mailLinks = [...appRoot.querySelectorAll(`a[href="mailto:${inputs.content.contact.email}"]`)] .filter((node) => isVisibleSemanticNode(node, appRoot)); - if (mailLinks.length !== 1 || mailLinks[0].textContent !== inputs.content.contact.email) { + if (mailLinks.length !== 1 || mailLinks[0].textContent !== 'Start a conversation') { throw new Error('Missing visible semantic mailto contact link.'); } diff --git a/src/components/AppShell.vue b/src/components/AppShell.vue index 91fbf41..c20595e 100644 --- a/src/components/AppShell.vue +++ b/src/components/AppShell.vue @@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted, shallowRef, watch } from 'vue'; import ContactDestination from './ContactDestination.vue'; import DriveControls from './DriveControls.vue'; import ExperienceTimeline from './ExperienceTimeline.vue'; +import HeroJourney from './HeroJourney.vue'; import InfrastructureDiagram from './InfrastructureDiagram.vue'; import JourneyHud from './JourneyHud.vue'; import PortfolioSection from './PortfolioSection.vue'; @@ -16,7 +17,7 @@ import { useAudioEngine } from '@/composables/useAudioEngine'; import { useJourneySections } from '@/composables/useJourneySections'; import { useKeyboardNavigation } from '@/composables/useKeyboardNavigation'; import { useMotionPreferences } from '@/composables/useMotionPreferences'; -import type { PortfolioContent, SectionId } from '@/content/types'; +import type { PortfolioContent, SectionId, SectionLayout } from '@/content/types'; import type { SceneHostState } from '@/composables/useSceneController'; import type { MotionPolicy } from '@/journey/motion-policy'; @@ -29,6 +30,17 @@ const canonicalSectionIds = [ 'start', 'profile', 'experience', 'infrastructure', 'selected-builds', 'additional-work', 'toolkit', 'contact', ] as const satisfies readonly SectionId[]; +const sectionLayouts = { + start: 'hero', + profile: 'right-rail', + experience: 'left-timeline', + infrastructure: 'full-diagram', + 'selected-builds': 'project-stage', + 'additional-work': 'compact-rail', + toolkit: 'toolkit-field', + contact: 'destination', +} as const satisfies Readonly>; + const navigationById = computed(() => new Map(props.content.navigation.map((item) => [item.id, item]))); const sectionLabel = (id: SectionId): string => { @@ -174,9 +186,6 @@ onUnmounted(() => removePageListeners()); :section-label="activeSectionLabel" />
- -

{{ content.identity.valueProposition }}

-
+ - +

{{ content.profile.introduction }}

-
+