Portfolio Angular Data Science

Créez un portfolio Angular responsive avec Tailwind et des données TypeScript.

Introduction

30 Second Summary

Un parcours riche devient vite difficile à comprendre quand ses preuves restent dispersées. Une présentation claire relie votre expérience à ce que vous savez construire.

Dans ce projet, vous allez construire un portfolio monopage avec Angular pour présenter le profil professionnel de Steve Junior Dimale. Vous séparerez les données TypeScript de la présentation pour rendre les projets faciles à enrichir.

What You'll Build

Dans Microsoft Edge, vous verrez un portfolio sombre qui adapte sa grille de projets à chaque largeur d’écran.

À la fin de ce projet, vous aurez :

  • Un parcours professionnel complet accessible depuis une navigation sticky.
  • Une grille de projets extensible où chaque nouvel objet dans portfolio.data.ts produit une carte ProjectCard sans duplication du template.
  • Un design responsive qui conserve sa hiérarchie du téléphone au grand écran avec une identité sombre en glassmorphism.
  • Secret Mission: Ajoutez la directive appReveal pour révéler chaque section avec IntersectionObserver tout en respectant la réduction des mouvements.

Are there any prerequisites?

Des connaissances intermédiaires en Angular et TypeScript vous aideront à suivre l’architecture.

La préparation guidée couvre la vérification de Node.js, l’installation d’Angular CLI et la configuration de Tailwind CSS.

Before We Start

Cette première étape fixe le résultat professionnel que votre portfolio doit rendre visible à un recruteur. Cet objectif guidera chaque choix de contenu et de présentation.

Set Up Angular and Tailwind

An unverified Node.js runtime can stop Angular before your portfolio reaches its first render. A missing test browser creates the same problem at the final check.

In this step, you will prepare a standalone workspace with Angular CLI inside Visual Studio Code. You will add Tailwind CSS through the official integration. You will prove the setup works in Microsoft Edge.

In this step, get ready to:
  • Verify the Windows development environment.
  • Create the standalone Angular workspace.
  • Launch the Tailwind-enabled starter page.
Verify Node.js and Microsoft Edge

Node.js powers the Angular command-line tools. This project requires v22.22.3 or newer.

The integrated PowerShell terminal keeps every setup command inside your editor. Microsoft Edge provides the localhost check at the end of this step.

  • Press the Windows key to open Windows search.
  • Type Visual Studio Code into the search field.
  • Press Enter to open Visual Studio Code.
  • Press Ctrl+` to open the integrated terminal.
  • Check the installed Node.js version by running this command:
node --version

What Does This Command Do?

The command prints the active Node.js version. Angular can use this runtime when the value is v22.22.3 or newer.

✔️ I see the required version or higher

Your Node.js runtime meets Angular's requirement. That removes the first environment risk from your setup.

ⓧ I see an older version

Your installed version cannot support this Angular setup. Upgrade Node.js before creating the workspace.

  • Visit the official Node.js download page.
  • Choose a supported release at v22.22.3 or newer.
  • Download the Windows Installer (.msi).
  • Run the downloaded installer with its selected defaults.
  • Restart Visual Studio Code after the installation finishes.
  • Return to the version check above to confirm the new value.

ⓧ Command not found

PowerShell cannot find the Node.js executable. Install Node.js before continuing.

  • Visit the official Node.js download page.
  • Choose a supported release at v22.22.3 or newer.
  • Download the Windows Installer (.msi).
  • Run the downloaded installer with its selected defaults.
  • Restart Visual Studio Code after the installation finishes.
  • Return to the version check above to confirm the installed value.

Microsoft Edge gives you a consistent browser for checking the local Angular page. Confirm that Windows can launch it before you start the development server.

  • Press the Windows key to open Windows search.
  • Type Microsoft Edge into the search field.
  • Press Enter.

✔️ Microsoft Edge opens

Your test browser is ready. Keep its window available for the localhost check later in this step.

ⓧ Microsoft Edge is missing

Install Microsoft Edge from Microsoft's official download page. The installer makes the browser available through Windows search.

  • Visit the official Microsoft Edge download page in your current browser.
  • Download Microsoft Edge for Windows.
  • Run the downloaded installer.
  • Press the Windows key after installation.
  • Type Microsoft Edge into the search field.
  • Press Enter to confirm the browser opens.
Create the Angular workspace

Angular CLI generates the application scaffold from one command. The current official package version is 22.2.3.

The scaffold includes a standalone application. It also installs dependencies. It initializes a local Git repository.

  • Install Angular CLI globally by running this command:
npm install -g @angular/cli

What Does This Command Do?

The global installation makes Angular CLI available from any PowerShell location. The command installs the current 22.2.3 package.

The terminal returns to its prompt when the installation completes. Angular CLI is now available for workspace creation.

Angular CLI Installation Failed?

Check that the Node.js version meets the requirement above. Restart Visual Studio Code if PowerShell still uses the previous Node.js installation.

A network or permission problem can also interrupt the package download. Retry the installation after confirming your connection.

Help me troubleshoot an Angular CLI installation failure on Windows.

The Desktop gives the new workspace a predictable location. You can find the project without searching through your user folders.

  • Move the PowerShell terminal to your Desktop by running this command:
Set-Location -Path $HOME\Desktop

What Does This Command Do?

PowerShell uses $HOME for your Windows user folder. The command changes the terminal location to its Desktop subfolder.

The workspace wizard asks interactive setup questions. The selected defaults create the standalone application required for this project.

  • Prepare to press Enter whenever the wizard pauses.
  • Create the workspace by running this command:
ng new steve-dimale-portfolio

What Does This Command Create?

  • The steve-dimale-portfolio folder holds the Angular standalone workspace.
  • The generated package.json defines the Angular dependencies. It also defines the development scripts.
  • The src folder contains the starter application.
  • The installed packages live in node_modules.
  • The initialized Git repository tracks the workspace from its first generated state.

The scaffold can pause while it installs the generated dependencies. When the PowerShell prompt returns, you have a standalone Angular workspace with its local Git history ready.

Workspace Creation Stopped?

Make sure every wizard question has a selected answer. Press Enter to accept the displayed default.

If the package installation fails, confirm your internet connection. Remove the incomplete steve-dimale-portfolio folder before retrying the scaffold.

Help me diagnose why my Angular workspace scaffold did not finish.

  • Load the new workspace into Visual Studio Code by running these commands:
cd steve-dimale-portfolio
code .

What Do These Commands Do?

The first command moves PowerShell into the generated workspace. The second command opens that current folder in Visual Studio Code.

  • Press Ctrl+` in the workspace window to open its integrated terminal.

You will see steve-dimale-portfolio as the open folder in the Explorer sidebar. The PowerShell prompt ends with the same folder name.

Current Folder Did Not Open?

Confirm that the scaffold completed before entering the workspace folder. A missing folder means the previous command stopped early.

If PowerShell cannot find the Visual Studio Code command, use File followed by Open Folder inside Visual Studio Code. Select steve-dimale-portfolio from your Desktop.

Help me open the generated Angular workspace in Visual Studio Code.

Add Tailwind and start the server

Angular provides a purpose-built Tailwind integration. It installs the current 4.3.3 package. It also connects the generated stylesheet to the workspace.

  • Add Tailwind CSS from the workspace root by running this command:
ng add tailwindcss

What Does This Command Configure?

  • The integration adds Tailwind CSS to the workspace dependencies.
  • It updates the project configuration used for CSS processing.
  • It configures src/styles.css as the Tailwind entry stylesheet.

The PowerShell prompt returns when the integration finishes. Your package manifest now includes the generated Tailwind setup.

Tailwind Integration Failed?

Check that the PowerShell prompt ends with steve-dimale-portfolio. The integration must run from the workspace root.

Confirm that the Explorer sidebar contains angular.json before retrying. A missing file means the wrong folder is open.

Help me troubleshoot the Tailwind integration in my Angular workspace.

Before you start the development server, what do you expect Microsoft Edge to display when the workspace is healthy?

  • Start the Angular development server by running this command:
npm start

What Does This Command Start?

The generated start script compiles the Angular application. It serves the result at http://localhost:4200/.

The process keeps watching the project for saved changes. Leave this terminal running during the remaining build steps.

The terminal reports the localhost address when compilation succeeds. It also indicates that the server is watching for changes.

  • Switch back to the Microsoft Edge window from earlier.
  • Select the browser address bar.
  • Enter http://localhost:4200/.
  • Press Enter.

You will see the default Angular starter page. That is your first working render: Angular, Tailwind CSS, the development server, and Microsoft Edge now share one local workflow.

Starter Page Did Not Load?

Keep the integrated terminal open while testing the page. Closing the running process stops the localhost server.

Check the terminal for a compilation problem. If the server is healthy, confirm that the browser address uses port 4200.

Help me troubleshoot why the Angular starter page does not load in Microsoft Edge.

✔️ Awesome, I've got everything!

Your workspace is ready. Keep the integrated PowerShell terminal running from steve-dimale-portfolio while you build the portfolio.

ⓧ I'd like to double check the full code

This setup uses generated artifacts instead of hand-written application code. Check each workspace state before continuing.

  • Confirm Visual Studio Code has the steve-dimale-portfolio folder open.
  • Confirm the Explorer sidebar contains package.json with Angular 22 development tooling.
  • Confirm the Explorer sidebar contains angular.json plus the TypeScript configuration files.
  • Confirm the Explorer sidebar contains the src application scaffold.
  • Confirm src/styles.css is the Tailwind CSS entry stylesheet.
  • Confirm the scaffold output reported that the local Git repository was initialized.
  • Confirm the integrated PowerShell terminal is running the development server from the workspace root.
  • Confirm Microsoft Edge shows the default Angular starter page at http://localhost:4200/.

Your local foundation is working. Next, you will replace the generated page with Steve Junior Dimale's hero section and sticky navigation.

Build the Hero and Navigation

Your Angular workspace is running in Microsoft Edge. The starter page still says nothing about Steve Junior Dimale or the professional value behind this portfolio.

In this step, you will replace that starter page with a sticky navigation bar and a personalized hero. You will centralize the profile content in typed data so later sections can reuse the same source.

In this step, get ready to:
  • Create a typed profile and a reusable skills list.
  • Build standalone Header and Hero components.
  • Apply the dark glassmorphism theme and verify the customized hero.
Create the typed content layer

A TypeScript interface defines the shape that every profile object must follow. This catches missing profile fields before they reach the page.

  • Create src/app/models/portfolio.models.ts inside the steve-dimale-portfolio workspace using the Visual Studio Code file sidebar.
  • Define the profile structure by adding this code:
export interface Profile {
  name: string;
  role: string;
  location: string;
  email: string;
  phone: string;
  showPhone: boolean;
  summary: string;
  githubUrl: string;
  linkedinUrl: string;
}

What does this interface define?

  • The identity fields hold the name, role, location, and contact details shown across the portfolio.
  • The showPhone field controls whether the stored phone number appears publicly.
  • The URL fields provide typed customization points for the social links added later.
  • Save src/app/models/portfolio.models.ts.
  • Check the running terminal for a completed rebuild without a TypeScript error.

Seeing a TypeScript error?

Check that every field is inside the Profile braces. Confirm that each field name matches the code exactly.

If the error remains, help me compare my Profile interface with the required Angular project code.

The interface protects the data shape. A separate data file now holds the actual profile values so the components stay focused on presentation.

  • Create src/app/data/portfolio.data.ts using the Visual Studio Code file sidebar.
  • Centralize the profile and skills by adding this code:
import { Profile } from '../models/portfolio.models';

export const PROFILE: Profile = {
  name: 'Steve Junior Dimale',
  role: 'Data Scientist et Ingénieur logiciel',
  location: 'Québec, Canada',
  email: 'stevedimale6@gmail.com',
  phone: '(343) 208-0281',
  showPhone: false,
  summary:
    "Je combine l'analyse de données, la conception logicielle et une approche structurée de la résolution de problèmes pour transformer des besoins métier en produits fiables.",
  githubUrl: '',
  linkedinUrl: '',
};

export const SKILLS = [
  'Python',
  'SQL',
  'scikit-learn',
  'Pandas',
  'dbt',
  'DuckDB',
  'VB.NET',
  'DevOps',
  'DDD',
  'TDD',
] as const;

What does this data provide?

  • The PROFILE constant satisfies the Profile interface and becomes the shared source for identity details.
  • The SKILLS list uses as const to preserve its values as a readonly list.
  • The empty social URLs remain visible customization points without creating broken links.
  • Save src/app/data/portfolio.data.ts.
  • Check the running terminal for another completed rebuild without a type error.

Data file not compiling?

Check that portfolio.data.ts is inside src/app/data. Its import expects the model file at src/app/models/portfolio.models.ts.

For a stubborn import or typing problem, help me debug the PROFILE and SKILLS declarations in portfolio.data.ts.

Build the standalone layout components

A standalone component packages one part of the interface with its own selector and template. The Header handles navigation while the Hero turns the typed profile data into the first visible portfolio section.

  • Create the src/app/features/header folder using the Visual Studio Code file sidebar.
  • Create src/app/features/header/header.ts inside that folder.
  • Define the Header component by adding this code:
import { Component } from '@angular/core';

@Component({
  selector: 'app-header',
  standalone: true,
  templateUrl: './header.html',
})
export class Header {}

What does the Header class do?

  • The app-header selector gives the root layout a custom element for the navigation.
  • The templateUrl setting keeps the navigation markup in a nearby HTML file.
  • The standalone setting allows App to import the component directly.
  • Save src/app/features/header/header.ts.
  • Check that the running development server rebuilds without reporting a component error.
  • Create src/app/features/header/header.html beside header.ts.
  • Add the portfolio anchors by inserting this template:
<header class="site-header">
  <nav class="shell glass flex items-center justify-between gap-6 px-6 py-4 backdrop-blur-md" aria-label="Navigation principale">
    <a class="brand" href="#accueil" aria-label="Retour à l'accueil">SD.</a>

    <div class="desktop-nav">
      <a href="#profil">Profil</a>
      <a href="#experience">Expérience</a>
      <a href="#projets">Projets</a>
      <a href="#certifications">Certifications</a>
      <a href="#contact">Contact</a>
    </div>
  </nav>
</header>

How does the navigation work?

  • The brand points to #accueil so it returns visitors to the hero.
  • Each navigation link targets the identifier assigned to a portfolio section.
  • The aria-label values give assistive technology a useful name for the navigation and brand link.
  • Save src/app/features/header/header.html.
  • Check that the terminal completes another rebuild without a template error.

Header template not compiling?

Confirm that header.html sits beside header.ts. Check that every opening HTML tag has a matching closing tag.

If the template still fails, help me find the HTML mismatch in my Angular Header component.

  • Create the src/app/features/hero folder using the Visual Studio Code file sidebar.
  • Create src/app/features/hero/hero.ts inside that folder.
  • Connect the Hero component to the shared data by adding this code:
import { Component } from '@angular/core';
import { PROFILE, SKILLS } from '../../data/portfolio.data';

@Component({
  selector: 'app-hero',
  standalone: true,
  templateUrl: './hero.html',
})
export class Hero {
  protected readonly profile = PROFILE;
  protected readonly featuredSkills = SKILLS.slice(0, 5);
}

How does the Hero get its content?

  • The profile field exposes the shared PROFILE object to the template.
  • The featuredSkills field selects the first five skills for the compact hero card.
  • The protected readonly fields keep the template API focused and prevent accidental reassignment.
  • Save src/app/features/hero/hero.ts.
  • Check the running terminal for a completed rebuild without an import error.
  • Create src/app/features/hero/hero.html beside hero.ts.
  • Build the main hero message by adding this markup:
<section id="accueil" class="hero section">
  <div class="shell grid grid-cols-1 items-center gap-6 lg:grid-cols-2">
    <div>
      <p class="eyebrow">Data Science · Génie Logiciel</p>
      <h1>{{ profile.name }}</h1>
      <p class="hero-role">{{ profile.role }}</p>
      <p class="lead">
        Je transforme les données et les besoins métier en solutions logicielles utiles, mesurables et maintenables.
      </p>

      <div class="actions">
        <a class="button button-primary" [href]="'mailto:' + profile.email">Me contacter</a>
        <a class="button button-secondary" href="#projets">Voir mes projets</a>
      </div>
    </div>
  </div>
</section>

What does the main hero show?

  • The interpolation expressions read the name and role from profile.
  • The bound href builds the contact address from the profile email.
  • The project button points to #projets so it is ready for the project section added later.
  • Save src/app/features/hero/hero.html.
  • Check that the terminal rebuilds without an unknown property or interpolation error.
  • In src/app/features/hero/hero.html, locate the closing tag for the outer grid container.
  • Insert the professional summary card before that closing tag:
    <aside class="glass hero-card p-6 backdrop-blur-md" aria-label="Résumé professionnel">
      <span class="status-dot" aria-hidden="true"></span>
      <p>Basé au {{ profile.location }}</p>
      <h2>Analyse. Architecture. Impact.</h2>
      <p>
        Une approche bilingue et structurée, de l'exploration des données jusqu'à la livraison logicielle.
      </p>
      <div class="flex flex-wrap gap-2">
        @for (skill of featuredSkills; track skill) {
          <span class="tag">{{ skill }}</span>
        }
      </div>
    </aside>

How does the summary card stay data-driven?

  • The location comes from profile.location instead of duplicated template text.
  • The @for block renders one tag for every value in featuredSkills.
  • Tracking each skill by its value helps Angular associate rendered tags with their source entries.
  • Save src/app/features/hero/hero.html.
  • Check that the terminal completes the template compilation without an error.

Hero data not recognized?

Check that the template uses profile and featuredSkills with the same spelling as hero.ts.

For an @for or binding problem, help me debug the data bindings in my Angular Hero component.

The components now compile independently. The root App component must import them before their selectors can replace the starter page.

  • Replace the contents of src/app/app.ts with this root component:
import { Component } from '@angular/core';
import { Header } from './features/header/header';
import { Hero } from './features/hero/hero';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [Header, Hero],
  templateUrl: './app.html',
})
export class App {}

How does App compose the page?

  • The TypeScript imports make the Header and Hero classes available to the root component.
  • The component imports list allows their selectors inside app.html.
  • The root component remains a small layout layer without duplicating child component logic.
  • Save src/app/app.ts.
  • Check the terminal for a successful rebuild before changing the root template.
  • Replace the contents of src/app/app.html with this layout:
<app-header></app-header>

<main>
  <app-hero></app-hero>
</main>

What does the root template do?

  • The Header sits outside main because it provides navigation for the full page.
  • The Hero becomes the first primary content section inside main.
  • The previous starter markup disappears because the root template now contains only the portfolio layout.
  • Save src/app/app.html.
  • Switch back to Microsoft Edge to let the running development server refresh the page.

You should now see Steve Junior Dimale, the professional role, two action links, and the skill summary. The content is connected even though the final visual theme is still missing.

Still seeing the starter page?

Confirm that you edited src/app/app.html inside the running steve-dimale-portfolio workspace. Check the terminal for the first reported compilation error.

If the page remains unchanged, help me troubleshoot why my Angular App is not rendering Header and Hero.

Apply the dark glassmorphism theme

The component structure controls what appears. Tailwind CSS utilities handle the responsive grid while the global stylesheet creates the dark background, glass panels, buttons, and navigation behavior.

  • Replace the contents of src/styles.css with this base theme:
@import 'tailwindcss';

:root {
  color-scheme: dark;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: #050816;
  color: #e8eefc;
  font-synthesis: none;
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  background:
    radial-gradient(circle at 15% 10%, rgba(37, 99, 235, 0.22), transparent 34rem),
    radial-gradient(circle at 85% 35%, rgba(14, 165, 233, 0.14), transparent 30rem),
    linear-gradient(145deg, #03050d 0%, #071126 52%, #03050d 100%);
  color: #e8eefc;
}

What does the base theme change?

  • The Tailwind import keeps the utility classes in the component templates active.
  • The root rules establish the dark color scheme and the shared font stack.
  • The body gradients create the blue depth behind the portfolio content.
  • Save src/styles.css.
  • Switch back to Microsoft Edge to see the page change to a dark blue background.
  • Append the background texture and keyboard focus rules to src/styles.css using this code:
body::before {
  position: fixed;
  inset: 0;
  z-index: -1;
  content: '';
  background-image: linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
  background-size: 48px 48px;
  mask-image: linear-gradient(to bottom, black, transparent 85%);
}

a {
  color: inherit;
  text-decoration: none;
}

a:focus-visible {
  border-radius: 0.35rem;
  outline: 2px solid #38bdf8;
  outline-offset: 4px;
}

Why add focus styles?

  • The fixed pseudo-element adds a subtle grid behind the content without extra HTML.
  • The anchor reset lets buttons and navigation links inherit the portfolio palette.
  • The focus outline makes the currently selected link visible during keyboard navigation.
  • Save src/styles.css.
  • Press the Tab key in Microsoft Edge to move focus to a link.

You should see a bright blue outline around the focused navigation or hero link.

  • Append the global typography rules to src/styles.css using this code:
h1,
h2,
h3,
p {
  margin-top: 0;
}

h1 {
  max-width: 12ch;
  margin-bottom: 1rem;
  font-size: clamp(3rem, 9vw, 6.7rem);
  line-height: 0.92;
  letter-spacing: -0.065em;
}

h2 {
  font-size: clamp(1.8rem, 4vw, 3rem);
  line-height: 1.1;
}

h3 {
  font-size: 1.2rem;
}

p,
li {
  color: #aebbd3;
  line-height: 1.75;
}

How does the typography scale?

  • The responsive heading size grows with the viewport while preserving minimum and maximum values.
  • The tighter heading spacing gives the hero name a compact visual shape.
  • The lighter paragraph color separates supporting copy from headings.
  • Save src/styles.css.
  • Switch back to Microsoft Edge to see the hero name become larger than the supporting text.
  • Append the shared layout and sticky header rules to src/styles.css using this code:
.shell {
  width: min(1120px, calc(100% - 2rem));
  margin-inline: auto;
}

.section {
  padding-block: 5.5rem;
  scroll-margin-top: 6rem;
}

.glass {
  border: 1px solid rgba(148, 163, 184, 0.18);
  background: rgba(8, 15, 34, 0.68);
  box-shadow: 0 24px 80px rgba(0, 0, 0, 0.28);
}

.site-header {
  position: sticky;
  top: 1rem;
  z-index: 20;
}

.site-header nav {
  border-radius: 1.25rem;
}

What creates the glass header?

  • The shell class limits content width while preserving space at the viewport edges.
  • The translucent border, dark background, and shadow form the glass panel.
  • The sticky positioning keeps the navigation near the top while the page scrolls.
  • Save src/styles.css.
  • Scroll slightly in Microsoft Edge to confirm the navigation remains near the top edge.
  • Append the brand and desktop navigation styles to src/styles.css using this code:
.brand {
  color: #7dd3fc;
  font-size: 1.15rem;
  font-weight: 800;
  letter-spacing: -0.04em;
}

.desktop-nav {
  display: flex;
  gap: 1.25rem;
  color: #cbd5e1;
  font-size: 0.9rem;
}

.desktop-nav a:hover,
.project-links a:hover,
.social-links a:hover,
.footer a:hover {
  color: #7dd3fc;
}

How does the navigation signal interaction?

  • The brand uses the accent color and a heavier weight to anchor the navigation.
  • The navigation displays its links in a horizontal row with consistent spacing.
  • The hover color gives pointer users immediate feedback before they follow a link.
  • Save src/styles.css.
  • Move the pointer over a navigation link in Microsoft Edge to see it turn blue.
  • Append the hero sizing and text hierarchy rules to src/styles.css using this code:
.hero {
  display: grid;
  min-height: calc(100vh - 5rem);
  align-items: center;
}

.hero-role {
  margin-bottom: 1rem;
  color: #7dd3fc;
  font-size: clamp(1.2rem, 2vw, 1.6rem);
  font-weight: 700;
}

.lead {
  max-width: 62ch;
  font-size: 1.08rem;
}

.eyebrow {
  margin-bottom: 0.8rem;
  color: #38bdf8;
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

How does the hero establish hierarchy?

  • The hero fills most of the viewport so the identity becomes the first visual focus.
  • The role and eyebrow use the accent palette to frame the larger name.
  • The lead width keeps long text readable on wide screens.
  • Save src/styles.css.
  • Switch back to Microsoft Edge to confirm the hero occupies most of the visible page height.
  • Append the action layout and button foundation to src/styles.css using this code:
.actions,
.project-links,
.social-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.8rem;
  align-items: center;
}

.actions {
  margin-top: 2rem;
}

.button {
  display: inline-flex;
  min-height: 2.8rem;
  align-items: center;
  justify-content: center;
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 0.75rem 1.15rem;
  font-weight: 750;
  transition: transform 160ms ease, border-color 160ms ease, background 160ms ease;
}

What makes the links feel like buttons?

  • The action container keeps both links aligned while allowing them to wrap on narrow screens.
  • The button rules create a consistent clickable height and pill-shaped outline.
  • The transition prepares the buttons for a short hover movement.
  • Save src/styles.css.
  • Switch back to Microsoft Edge to see both hero links gain matching rounded shapes.
  • Append the button states and hero card styles to src/styles.css using this code:
.button:hover {
  transform: translateY(-2px);
}

.button-primary {
  background: #38bdf8;
  color: #03111d;
}

.button-secondary {
  border-color: rgba(148, 163, 184, 0.28);
  background: rgba(15, 23, 42, 0.65);
}

.hero-card {
  position: relative;
  overflow: hidden;
}

.hero-card::after {
  position: absolute;
  right: -5rem;
  bottom: -7rem;
  width: 14rem;
  height: 14rem;
  border-radius: 999px;
  background: rgba(56, 189, 248, 0.14);
  filter: blur(18px);
  content: '';
}

How does the hero card gain depth?

  • The primary button uses a solid accent color while the secondary button stays translucent.
  • The hover transform lifts either button by a small amount.
  • The hero card pseudo-element adds a blurred blue glow behind its content.
  • Save src/styles.css.
  • Move the pointer over Me contacter in Microsoft Edge to see the button lift slightly.
  • Append the status indicator and tag styles to src/styles.css using this code:
.status-dot {
  display: inline-block;
  width: 0.65rem;
  height: 0.65rem;
  margin-right: 0.5rem;
  border-radius: 999px;
  background: #34d399;
  box-shadow: 0 0 20px #34d399;
}

.tag,
.status-badge {
  display: inline-flex;
  border: 1px solid rgba(125, 211, 252, 0.18);
  border-radius: 999px;
  background: rgba(14, 165, 233, 0.08);
  color: #bae6fd;
  padding: 0.35rem 0.65rem;
  font-size: 0.78rem;
}

::selection {
  background: #38bdf8;
  color: #03111d;
}

What finishes the summary card?

  • The green status dot creates a compact visual indicator beside the location.
  • The tag style turns each featured skill into a bordered pill.
  • The selection rule keeps highlighted text consistent with the blue theme.
  • Save src/styles.css.
  • Switch back to Microsoft Edge to see five skill tags inside the glass summary card.
  • Append the narrow-screen navigation rules to src/styles.css using this code:
@media (max-width: 47.99rem) {
  .desktop-nav {
    max-width: 72%;
    gap: 0.75rem;
    overflow-x: auto;
    padding-bottom: 0.25rem;
  }

  .desktop-nav a {
    white-space: nowrap;
  }

  .section {
    padding-block: 4rem;
  }
}

How does the navigation handle small screens?

  • The navigation receives less horizontal space and tighter gaps on narrow screens.
  • Horizontal overflow keeps every anchor reachable without wrapping labels.
  • The reduced section padding preserves more room for content on a phone-sized viewport.
  • Save src/styles.css.
  • Narrow the Microsoft Edge window to confirm the navigation remains on one scrollable row.
  • Append the reduced-motion base rules to src/styles.css using this code:
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
  }
}

Why include reduced motion now?

  • The media query responds to a visitor's operating system motion preference.
  • Smooth scrolling becomes immediate when reduced motion is requested.
  • Transitions become effectively instant while the content and controls remain available.
  • Save src/styles.css.

Theme not appearing correctly?

Confirm that @import 'tailwindcss'; remains the first line of src/styles.css. Check the terminal for an unmatched brace or invalid stylesheet syntax.

If the layout stays unstyled, help me debug why my Angular global styles and Tailwind utilities are not applying.

Before the final refresh, which identity details and controls do you expect to see in the hero?

  • Return the Microsoft Edge window to a comfortable desktop width.
  • Refresh http://localhost:4200/ to run the final visual check.

You should see Steve Junior Dimale above the role Data Scientist et Ingénieur logiciel. You should also see the sticky navigation, the Québec location, five skill tags, and the Me contacter button.

That is the first portfolio view complete. Your identity and navigation now replace the generic Angular starter page.

✔️ Awesome, I've got everything!

Great. Double check that every edited file is saved and the development server remains running.

ⓧ I'd like to double check the full code

Compare each file below with the matching file in your Visual Studio Code workspace.

export interface Profile {
  name: string;
  role: string;
  location: string;
  email: string;
  phone: string;
  showPhone: boolean;
  summary: string;
  githubUrl: string;
  linkedinUrl: string;
}
import { Profile } from '../models/portfolio.models';

export const PROFILE: Profile = {
  name: 'Steve Junior Dimale',
  role: 'Data Scientist et Ingénieur logiciel',
  location: 'Québec, Canada',
  email: 'stevedimale6@gmail.com',
  phone: '(343) 208-0281',
  showPhone: false,
  summary:
    "Je combine l'analyse de données, la conception logicielle et une approche structurée de la résolution de problèmes pour transformer des besoins métier en produits fiables.",
  githubUrl: '',
  linkedinUrl: '',
};

export const SKILLS = [
  'Python',
  'SQL',
  'scikit-learn',
  'Pandas',
  'dbt',
  'DuckDB',
  'VB.NET',
  'DevOps',
  'DDD',
  'TDD',
] as const;
import { Component } from '@angular/core';

@Component({
  selector: 'app-header',
  standalone: true,
  templateUrl: './header.html',
})
export class Header {}
<header class="site-header">
  <nav class="shell glass flex items-center justify-between gap-6 px-6 py-4 backdrop-blur-md" aria-label="Navigation principale">
    <a class="brand" href="#accueil" aria-label="Retour à l'accueil">SD.</a>

    <div class="desktop-nav">
      <a href="#profil">Profil</a>
      <a href="#experience">Expérience</a>
      <a href="#projets">Projets</a>
      <a href="#certifications">Certifications</a>
      <a href="#contact">Contact</a>
    </div>
  </nav>
</header>
import { Component } from '@angular/core';
import { PROFILE, SKILLS } from '../../data/portfolio.data';

@Component({
  selector: 'app-hero',
  standalone: true,
  templateUrl: './hero.html',
})
export class Hero {
  protected readonly profile = PROFILE;
  protected readonly featuredSkills = SKILLS.slice(0, 5);
}
<section id="accueil" class="hero section">
  <div class="shell grid grid-cols-1 items-center gap-6 lg:grid-cols-2">
    <div>
      <p class="eyebrow">Data Science · Génie Logiciel</p>
      <h1>{{ profile.name }}</h1>
      <p class="hero-role">{{ profile.role }}</p>
      <p class="lead">
        Je transforme les données et les besoins métier en solutions logicielles utiles, mesurables et maintenables.
      </p>

      <div class="actions">
        <a class="button button-primary" [href]="'mailto:' + profile.email">Me contacter</a>
        <a class="button button-secondary" href="#projets">Voir mes projets</a>
      </div>
    </div>

    <aside class="glass hero-card p-6 backdrop-blur-md" aria-label="Résumé professionnel">
      <span class="status-dot" aria-hidden="true"></span>
      <p>Basé au {{ profile.location }}</p>
      <h2>Analyse. Architecture. Impact.</h2>
      <p>
        Une approche bilingue et structurée, de l'exploration des données jusqu'à la livraison logicielle.
      </p>
      <div class="flex flex-wrap gap-2">
        @for (skill of featuredSkills; track skill) {
          <span class="tag">{{ skill }}</span>
        }
      </div>
    </aside>
  </div>
</section>
import { Component } from '@angular/core';
import { Header } from './features/header/header';
import { Hero } from './features/hero/hero';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [Header, Hero],
  templateUrl: './app.html',
})
export class App {}
<app-header></app-header>

<main>
  <app-hero></app-hero>
</main>
@import 'tailwindcss';

:root {
  color-scheme: dark;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: #050816;
  color: #e8eefc;
  font-synthesis: none;
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  background:
    radial-gradient(circle at 15% 10%, rgba(37, 99, 235, 0.22), transparent 34rem),
    radial-gradient(circle at 85% 35%, rgba(14, 165, 233, 0.14), transparent 30rem),
    linear-gradient(145deg, #03050d 0%, #071126 52%, #03050d 100%);
  color: #e8eefc;
}

body::before {
  position: fixed;
  inset: 0;
  z-index: -1;
  content: '';
  background-image: linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
  background-size: 48px 48px;
  mask-image: linear-gradient(to bottom, black, transparent 85%);
}

a {
  color: inherit;
  text-decoration: none;
}

a:focus-visible {
  border-radius: 0.35rem;
  outline: 2px solid #38bdf8;
  outline-offset: 4px;
}

h1,
h2,
h3,
p {
  margin-top: 0;
}

h1 {
  max-width: 12ch;
  margin-bottom: 1rem;
  font-size: clamp(3rem, 9vw, 6.7rem);
  line-height: 0.92;
  letter-spacing: -0.065em;
}

h2 {
  font-size: clamp(1.8rem, 4vw, 3rem);
  line-height: 1.1;
}

h3 {
  font-size: 1.2rem;
}

p,
li {
  color: #aebbd3;
  line-height: 1.75;
}

.shell {
  width: min(1120px, calc(100% - 2rem));
  margin-inline: auto;
}

.section {
  padding-block: 5.5rem;
  scroll-margin-top: 6rem;
}

.glass {
  border: 1px solid rgba(148, 163, 184, 0.18);
  background: rgba(8, 15, 34, 0.68);
  box-shadow: 0 24px 80px rgba(0, 0, 0, 0.28);
}

.site-header {
  position: sticky;
  top: 1rem;
  z-index: 20;
}

.site-header nav {
  border-radius: 1.25rem;
}

.brand {
  color: #7dd3fc;
  font-size: 1.15rem;
  font-weight: 800;
  letter-spacing: -0.04em;
}

.desktop-nav {
  display: flex;
  gap: 1.25rem;
  color: #cbd5e1;
  font-size: 0.9rem;
}

.desktop-nav a:hover,
.project-links a:hover,
.social-links a:hover,
.footer a:hover {
  color: #7dd3fc;
}

.hero {
  display: grid;
  min-height: calc(100vh - 5rem);
  align-items: center;
}

.hero-role {
  margin-bottom: 1rem;
  color: #7dd3fc;
  font-size: clamp(1.2rem, 2vw, 1.6rem);
  font-weight: 700;
}

.lead {
  max-width: 62ch;
  font-size: 1.08rem;
}

.eyebrow {
  margin-bottom: 0.8rem;
  color: #38bdf8;
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.actions,
.project-links,
.social-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.8rem;
  align-items: center;
}

.actions {
  margin-top: 2rem;
}

.button {
  display: inline-flex;
  min-height: 2.8rem;
  align-items: center;
  justify-content: center;
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 0.75rem 1.15rem;
  font-weight: 750;
  transition: transform 160ms ease, border-color 160ms ease, background 160ms ease;
}

.button:hover {
  transform: translateY(-2px);
}

.button-primary {
  background: #38bdf8;
  color: #03111d;
}

.button-secondary {
  border-color: rgba(148, 163, 184, 0.28);
  background: rgba(15, 23, 42, 0.65);
}

.hero-card {
  position: relative;
  overflow: hidden;
}

.hero-card::after {
  position: absolute;
  right: -5rem;
  bottom: -7rem;
  width: 14rem;
  height: 14rem;
  border-radius: 999px;
  background: rgba(56, 189, 248, 0.14);
  filter: blur(18px);
  content: '';
}

.status-dot {
  display: inline-block;
  width: 0.65rem;
  height: 0.65rem;
  margin-right: 0.5rem;
  border-radius: 999px;
  background: #34d399;
  box-shadow: 0 0 20px #34d399;
}

.tag,
.status-badge {
  display: inline-flex;
  border: 1px solid rgba(125, 211, 252, 0.18);
  border-radius: 999px;
  background: rgba(14, 165, 233, 0.08);
  color: #bae6fd;
  padding: 0.35rem 0.65rem;
  font-size: 0.78rem;
}

::selection {
  background: #38bdf8;
  color: #03111d;
}

@media (max-width: 47.99rem) {
  .desktop-nav {
    max-width: 72%;
    gap: 0.75rem;
    overflow-x: auto;
    padding-bottom: 0.25rem;
  }

  .desktop-nav a {
    white-space: nowrap;
  }

  .section {
    padding-block: 4rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
  }
}

Your portfolio now opens with a typed identity, sticky navigation, responsive hero, and accessible dark theme. Next, you will add the profile and career sections that give those navigation links real destinations.

Add Career Content

Your hero now introduces Steve Junior Dimale. The portfolio still needs evidence of his education, professional experience, certifications, and project work.

This step turns the centralized TypeScript data into visible career sections. You will also write one project card by hand so you can see why repeating that markup would become difficult to maintain.

In this step, get ready to:
  • Model the education, experience, and certification data.
  • Render the profile, experience, and certification sections with @for.
  • Build one Olist project card directly in its template.
Centralize the career data

Typed interfaces define the shape of each career record before a component uses it. This gives Angular enough information to catch missing or misspelled fields during compilation.

  • Switch back to the src/app/models/portfolio.models.ts tab from the previous step.
  • Add these interfaces below the existing Profile interface:
export interface Education {
  id: string;
  degree: string;
  school: string;
  location: string;
  period: string;
}

export interface Experience {
  id: string;
  role: string;
  company: string;
  period: string;
  highlights: readonly string[];
}

export interface Certification {
  id: string;
  name: string;
  period: string;
  status: 'Obtenue' | 'En cours';
}

What do these interfaces define?

  • Education records contain the qualification, institution, location, and study period.
  • Experience records include a readonly list of career highlights.
  • Certification records restrict status to the two values shown by the portfolio.
  • Save src/app/models/portfolio.models.ts.

The development terminal recompiles the application. A successful compilation confirms that the new model declarations are valid.

Seeing a model error?

Check that each interface sits outside the closing brace of Profile. Confirm that the quotes around 'Obtenue' and 'En cours' are straight single quotes.

Ask for help with the model declarations.

The model layer now describes the records. The data file supplies the real CV content that each section reads.

  • Switch back to src/app/data/portfolio.data.ts.
  • Replace the current model import with this expanded import:
import {
  Certification,
  Education,
  Experience,
  Profile,
} from '../models/portfolio.models';

Why expand the import?

Each exported collection uses its matching interface. The compiler can now validate every education, experience, and certification object.

  • Save src/app/data/portfolio.data.ts.

The terminal recompiles without changing the browser yet. The successful build proves that the data file recognizes all four interfaces.

Seeing an import error?

Confirm that the import path remains ../models/portfolio.models. Check that each imported interface uses the same capitalization as its declaration.

Ask for help with the model import.

  • Add the languages and education records below the existing SKILLS collection:
export const LANGUAGES = ['Français', 'Anglais'] as const;

export const EDUCATION: readonly Education[] = [
  {
    id: 'la-cite-data',
    degree: 'DEC en Sciences de données appliquées',
    school: 'La Cité collégiale',
    location: 'Ottawa',
    period: '2026 - 2028',
  },
  {
    id: 'ihtm-software',
    degree: 'DEC en Génie logiciel',
    school: 'Institut de haute technologie et de management',
    location: 'Yaoundé',
    period: 'Année à préciser',
  },
  {
    id: 'uy1-sociology',
    degree: 'Baccalauréat en sociologie',
    school: 'Université de Yaoundé 1',
    location: 'Yaoundé',
    period: 'Année à préciser',
  },
];

How is the education data organized?

Each education record has a stable id for template tracking. The readonly array prevents components from accidentally replacing the CV entries.

  • Save src/app/data/portfolio.data.ts.

The development server recompiles successfully. This confirms that all three education objects satisfy the Education interface.

Seeing an education type error?

Compare every object against the five fields in Education. A missing comma or field causes the compilation to stop.

Ask for help with the education collection.

  • Add the professional experience records below EDUCATION by pasting this collection:
export const EXPERIENCES: readonly Experience[] = [
  {
    id: 'mastermind',
    role: 'Développeur logiciel VB.NET',
    company: 'Mastermind SARL',
    period: '2023 - 2025',
    highlights: [
      'Livraison en environnement Agile et DevOps.',
      'Contribution à une architecture microservices orientée DDD.',
      'Optimisation de requêtes SQL et pratique du TDD.',
    ],
  },
  {
    id: 'engepress',
    role: 'Data Scientist',
    company: 'Engepress SARL',
    period: 'Période à préciser',
    highlights: [
      'Création de modèles prédictifs avec Python et scikit-learn.',
      "Segmentation d'utilisateurs et automatisation de campagnes.",
      'Construction de pipelines dans un cadre Agile.',
    ],
  },
];

Why keep highlights nested?

Each job owns its own highlights collection. The experience template can loop over jobs first before looping over the achievements inside each job.

  • Save src/app/data/portfolio.data.ts.

The successful recompilation confirms that both jobs contain the required fields. It also validates each readonly highlights list.

Seeing an experience syntax error?

Check the apostrophe in the Engepress highlight. That string uses double quotes so the apostrophe in d'utilisateurs does not end the value.

Ask for help with the experience records.

  • Add the certification records below EXPERIENCES by pasting this collection:
export const CERTIFICATIONS: readonly Certification[] = [
  {
    id: 'azure-fundamentals',
    name: 'Microsoft Certified: Azure Fundamentals',
    period: '2025',
    status: 'Obtenue',
  },
  {
    id: 'azure-data-fundamentals',
    name: 'Microsoft Certified: Azure Data Fundamentals',
    period: '2026',
    status: 'Obtenue',
  },
  {
    id: 'power-bi-data-analyst',
    name: 'Microsoft Certified: Power BI Data Analyst Associate',
    period: 'En cours',
    status: 'En cours',
  },
];

How are certification states protected?

The Certification interface permits only Obtenue or En cours. A different status becomes a compilation error instead of inconsistent page content.

  • Save src/app/data/portfolio.data.ts.

The terminal reports a successful recompilation. Your career data layer is now typed and ready for presentation.

Seeing an invalid status?

Confirm that every status value exactly matches one of the two values declared by Certification.

Ask for help with the certification collection.

Render the career sections

The data now has a stable shape. Three standalone components can focus on presentation while the data file remains the single place to update the CV.

  • Create src/app/features/about/about.ts from the Explorer sidebar.
  • Paste the About component into the new file:
import { Component } from '@angular/core';
import {
  EDUCATION,
  LANGUAGES,
  PROFILE,
  SKILLS,
} from '../../data/portfolio.data';

@Component({
  selector: 'app-about',
  standalone: true,
  templateUrl: './about.html',
})
export class About {
  protected readonly profile = PROFILE;
  protected readonly skills = SKILLS;
  protected readonly languages = LANGUAGES;
  protected readonly education = EDUCATION;
}

What does the About component expose?

The component maps each centralized export to a protected readonly property. Its template can read those properties without owning a second copy of the data.

  • Create src/app/features/about/about.html from the Explorer sidebar.
  • Build the first valid version of the profile section by pasting this template:
<section id="profil" class="section">
  <div class="shell">
    <p class="eyebrow">Profil</p>
    <h2 class="section-title">Une double culture data et logiciel</h2>

    <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
      <article class="glass panel p-6 backdrop-blur-md">
        <h3>À propos</h3>
        <p>{{ profile.summary }}</p>
        <p>
          Langues:
          @for (language of languages; track language; let last = $last) {
            <span>{{ language }}{{ last ? '' : ' · ' }}</span>
          }
        </p>
        <div class="flex flex-wrap gap-2">
          @for (skill of skills; track skill) {
            <span class="tag">{{ skill }}</span>
          }
        </div>
      </article>
    </div>
  </div>
</section>

How does the profile panel work?

The first @for loop prints the language names with separators. The second loop turns every centralized skill into a tag.

  • Save both About files.

The terminal recompiles the standalone component and its template. This proves that the profile bindings can read the centralized values.

  • Insert this education grid immediately after the closing </article> in about.html.
      <div class="grid grid-cols-1 gap-6">
        @for (item of education; track item.id) {
          <article class="glass panel p-6 backdrop-blur-md">
            <p class="card-meta">{{ item.period }}</p>
            <h3>{{ item.degree }}</h3>
            <p>{{ item.school }} · {{ item.location }}</p>
          </article>
        }
      </div>

Why track each education record?

Tracking item.id gives every education card a stable identity. Angular can update a changed record without rebuilding unrelated cards.

  • Save src/app/features/about/about.html.

The terminal recompiles with both profile columns present. The About section is complete even though the root layout does not render it yet.

Seeing an About template error?

Confirm that the education grid sits inside the outer responsive grid. Check that every opening <div> has a matching closing tag.

Ask for help with the About template.

  • Create src/app/features/experience/experience.ts from the Explorer sidebar.
  • Paste the Experience component into the new file:
import { Component } from '@angular/core';
import { EXPERIENCES } from '../../data/portfolio.data';

@Component({
  selector: 'app-experience',
  standalone: true,
  templateUrl: './experience.html',
})
export class Experience {
  protected readonly experiences = EXPERIENCES;
}

What does this component own?

The component exposes the centralized EXPERIENCES collection. It contains no duplicate career content.

  • Create src/app/features/experience/experience.html from the Explorer sidebar.
  • Paste the professional timeline into the new file:
<section id="experience" class="section">
  <div class="shell">
    <p class="eyebrow">Expérience</p>
    <h2 class="section-title">Des données aux systèmes métier</h2>

    <div class="timeline">
      @for (item of experiences; track item.id) {
        <article class="glass timeline-card p-6 backdrop-blur-md">
          <p class="card-meta">{{ item.period }}</p>
          <h3>{{ item.role }}</h3>
          <p class="company">{{ item.company }}</p>
          <ul>
            @for (highlight of item.highlights; track highlight) {
              <li>{{ highlight }}</li>
            }
          </ul>
        </article>
      }
    </div>
  </div>
</section>

How do the nested loops work?

The outer loop creates one timeline card per job. The inner loop creates the achievement list belonging to that job.

  • Save both Experience files.

The terminal recompiles both nested loops. A successful result confirms that every experience exposes its readonly highlights.

Seeing a nested loop error?

Check that the inner @for block remains inside the outer block. Confirm that the inner expression reads item.highlights.

Ask for help with the Experience template.

  • Create src/app/features/certifications/certifications.ts from the Explorer sidebar.
  • Paste the Certifications component into the new file:
import { Component } from '@angular/core';
import { CERTIFICATIONS } from '../../data/portfolio.data';

@Component({
  selector: 'app-certifications',
  standalone: true,
  templateUrl: './certifications.html',
})
export class Certifications {
  protected readonly certifications = CERTIFICATIONS;
}

What feeds this component?

The component reads the typed CERTIFICATIONS array. The template decides how each record appears.

  • Create src/app/features/certifications/certifications.html from the Explorer sidebar.
  • Paste the certification grid into the new file:
<section id="certifications" class="section">
  <div class="shell">
    <p class="eyebrow">Certifications</p>
    <h2 class="section-title">Validation continue des compétences</h2>

    <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
      @for (certification of certifications; track certification.id) {
        <article class="glass panel p-6 backdrop-blur-md">
          <p class="card-meta">{{ certification.period }}</p>
          <h3>{{ certification.name }}</h3>
          <span class="status-badge">{{ certification.status }}</span>
        </article>
      }
    </div>
  </div>
</section>

How does the grid respond?

The grid starts with one column. Tailwind changes it to two columns at the medium breakpoint before using three columns at the large breakpoint.

  • Save both Certifications files.

The terminal recompiles the responsive grid. The status badge now receives one of the two values allowed by the model.

Certification cards not compiling?

Confirm that the loop variable is named certification throughout the block. Check that the component property uses the plural name certifications.

Ask for help with the certification grid.

The new sections use shared glass panels and a timeline. Their CSS keeps the hierarchy consistent with the hero while preserving the dark theme.

  • Switch back to src/styles.css.
  • Add these career typography styles below the existing hero styles:
.section-title {
  max-width: 18ch;
  margin-bottom: 2rem;
}

.card-meta,
.company {
  color: #7dd3fc;
  font-size: 0.86rem;
  font-weight: 700;
}

What do these styles emphasize?

The section title stays readable at large sizes. Dates and company names use the accent color to make each card easier to scan.

  • Save src/styles.css.

The development server accepts the new selectors. Their effect becomes visible as soon as the root layout renders the career components.

  • Add the timeline styles below the career typography rules:
.timeline {
  display: grid;
  gap: 1.25rem;
  border-left: 1px solid rgba(125, 211, 252, 0.24);
  padding-left: 1.25rem;
}

.timeline-card {
  position: relative;
}

.timeline-card::before {
  position: absolute;
  top: 2rem;
  left: -1.68rem;
  width: 0.8rem;
  height: 0.8rem;
  border: 3px solid #071126;
  border-radius: 999px;
  background: #38bdf8;
  content: '';
}

How is the timeline drawn?

The container supplies the vertical line. Each card adds one circular marker through its ::before pseudo-element.

  • Save src/styles.css.

The terminal recompiles the stylesheet successfully. The timeline styles are ready for the Experience component.

  • Replace src/app/app.ts with this intermediate root component:
import { Component } from '@angular/core';
import { About } from './features/about/about';
import { Certifications } from './features/certifications/certifications';
import { Experience } from './features/experience/experience';
import { Header } from './features/header/header';
import { Hero } from './features/hero/hero';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [Header, Hero, About, Experience, Certifications],
  templateUrl: './app.html',
})
export class App {}

Why import the components here?

A standalone root component lists every component used by its template. These imports make the new selectors available to app.html.

  • Replace src/app/app.html with this intermediate layout:
<app-header></app-header>

<main>
  <app-hero></app-hero>
  <app-about></app-about>
  <app-experience></app-experience>
  <app-certifications></app-certifications>
</main>

What changes in the page flow?

The root layout keeps the header outside the main content. The profile, experience, and certification sections now follow the hero in page order.

  • Save src/app/app.ts.
  • Save src/app/app.html.
  • Switch back to Microsoft Edge.

The live page now shows the profile, three education cards, two experience cards, and three certification cards. The Header links for Profil, Expérience, and Certifications move to real sections.

Career sections missing?

Check the PowerShell terminal for the first compilation error. Confirm that every component appears in the imports array before its selector appears in app.html.

Ask for help with the root layout.

Add the first project card by hand

The career story needs a concrete project example. You will place the first Olist card directly in its template so the cost of repeating presentation markup becomes visible.

  • Create src/app/features/projects/projects.ts from the Explorer sidebar.
  • Paste the Projects component into the new file:
import { Component } from '@angular/core';

@Component({
  selector: 'app-projects',
  standalone: true,
  templateUrl: './projects.html',
})
export class Projects {}

Why is this component empty?

The first card stores its content directly in the template. The component does not expose project data yet.

  • Create src/app/features/projects/projects.html from the Explorer sidebar.
  • Paste the complete Olist card into the new file:
<section id="projets" class="section">
  <div class="shell">
    <p class="eyebrow">Projets</p>
    <h2 class="section-title">Pipelines, prédiction et apprentissage</h2>

    <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
      <article class="project-card glass transition hover:-translate-y-1 motion-reduce:transition-none motion-reduce:hover:transform-none">
        <img src="/images/olist.svg" alt="Illustration abstraite d'un pipeline de données e-commerce" />

        <div class="flex h-full flex-col gap-6 p-6">
          <div>
            <h3>Pipeline d'ingestion et de transformation E-Commerce</h3>
            <p>Pipeline analytique basé sur le jeu de données Olist pour ingérer, transformer et interroger des données e-commerce reproductibles.</p>
          </div>

          <div class="flex flex-wrap gap-2">
            <span class="tag">Python</span>
            <span class="tag">dbt</span>
            <span class="tag">DuckDB</span>
            <span class="tag">SQL</span>
            <span class="tag">Git</span>
            <span class="tag">GitHub</span>
          </div>

          <div class="project-links">
            <span>Liens à personnaliser</span>
          </div>
        </div>
      </article>
    </div>
  </div>
</section>

What is hard-coded here?

The title, description, technology tags, image path, and link reminder all live inside the template. A second project would need another copy of the same article structure.

  • Save both Projects files.

The terminal compiles the component and its static template. The root layout still needs to place the section on the page.

  • Add the project card styles below the timeline rules in src/styles.css.
.project-card {
  height: 100%;
  overflow: hidden;
}

.project-card:hover {
  border-color: rgba(125, 211, 252, 0.48);
}

.project-card img {
  width: 100%;
  aspect-ratio: 16 / 9;
  border-bottom: 1px solid rgba(148, 163, 184, 0.14);
  object-fit: cover;
}

.project-links {
  margin-top: auto;
  color: #7dd3fc;
  font-size: 0.9rem;
  font-weight: 700;
}

What shapes the project card?

The image keeps a consistent widescreen ratio. The links area stays at the bottom while the card border becomes brighter on hover.

  • Save src/styles.css.

The server recompiles the project styles. They become visible after the root layout renders Projects.

  • Replace src/app/app.ts with the final root component for this step:
import { Component } from '@angular/core';
import { About } from './features/about/about';
import { Certifications } from './features/certifications/certifications';
import { Experience } from './features/experience/experience';
import { Header } from './features/header/header';
import { Hero } from './features/hero/hero';
import { Projects } from './features/projects/projects';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [Header, Hero, About, Experience, Projects, Certifications],
  templateUrl: './app.html',
})
export class App {}

What changed in the root component?

The Projects import makes <app-projects> available to the root template. Its position in the imports list matches the page flow.

  • Replace src/app/app.html with the final layout for this step:
<app-header></app-header>

<main>
  <app-hero></app-hero>
  <app-about></app-about>
  <app-experience></app-experience>
  <app-projects></app-projects>
  <app-certifications></app-certifications>
</main>

Where does the project section appear?

The project section follows professional experience. Certifications close the visible career story for this step.

  • Save src/app/app.ts.
  • Save src/app/app.html.

Before you refresh the page, how many project cards do you expect the current template to produce?

  • Switch back to Microsoft Edge.
  • Refresh http://localhost:4200/.
  • Select Profil in the sticky navigation.
  • Select Expérience in the sticky navigation.
  • Select Projets in the sticky navigation.
  • Select Certifications in the sticky navigation.

You will see centralized profile, education, experience, and certification content. The Projects section shows exactly one Olist card because the template contains exactly one article.

You have reached the duplication limit

Adding Titanic and the learning platform with the current approach would require two more copies of the card markup. Every copy would repeat the image, description, tag, and link structure.

Leave projects.html with one card. The next step replaces this duplication-prone structure with typed project data and a reusable presentation component.

Only seeing the hero?

Read the first error in the PowerShell terminal. Confirm that About, Experience, Projects, and Certifications are imported by App.

Ask for help with the complete page.

✔️ Awesome, I've got everything!

Great. Save every open file and keep the PowerShell development server running.

ⓧ I'd like to double check the full code

Compare each modified file with the references below. These versions represent the complete project state at the end of this step.

export interface Profile {
  name: string;
  role: string;
  location: string;
  email: string;
  phone: string;
  showPhone: boolean;
  summary: string;
  githubUrl: string;
  linkedinUrl: string;
}

export interface Education {
  id: string;
  degree: string;
  school: string;
  location: string;
  period: string;
}

export interface Experience {
  id: string;
  role: string;
  company: string;
  period: string;
  highlights: readonly string[];
}

export interface Certification {
  id: string;
  name: string;
  period: string;
  status: 'Obtenue' | 'En cours';
}

What should the model file contain?

The file contains the original profile shape plus the three career record interfaces introduced in this step.

import {
  Certification,
  Education,
  Experience,
  Profile,
} from '../models/portfolio.models';

export const PROFILE: Profile = {
  name: 'Steve Junior Dimale',
  role: 'Data Scientist et Ingénieur logiciel',
  location: 'Québec, Canada',
  email: 'stevedimale6@gmail.com',
  phone: '(343) 208-0281',
  showPhone: false,
  summary:
    "Je combine l'analyse de données, la conception logicielle et une approche structurée de la résolution de problèmes pour transformer des besoins métier en produits fiables.",
  githubUrl: '',
  linkedinUrl: '',
};

export const SKILLS = [
  'Python',
  'SQL',
  'scikit-learn',
  'Pandas',
  'dbt',
  'DuckDB',
  'VB.NET',
  'DevOps',
  'DDD',
  'TDD',
] as const;

export const LANGUAGES = ['Français', 'Anglais'] as const;

export const EDUCATION: readonly Education[] = [
  {
    id: 'la-cite-data',
    degree: 'DEC en Sciences de données appliquées',
    school: 'La Cité collégiale',
    location: 'Ottawa',
    period: '2026 - 2028',
  },
  {
    id: 'ihtm-software',
    degree: 'DEC en Génie logiciel',
    school: 'Institut de haute technologie et de management',
    location: 'Yaoundé',
    period: 'Année à préciser',
  },
  {
    id: 'uy1-sociology',
    degree: 'Baccalauréat en sociologie',
    school: 'Université de Yaoundé 1',
    location: 'Yaoundé',
    period: 'Année à préciser',
  },
];

export const EXPERIENCES: readonly Experience[] = [
  {
    id: 'mastermind',
    role: 'Développeur logiciel VB.NET',
    company: 'Mastermind SARL',
    period: '2023 - 2025',
    highlights: [
      'Livraison en environnement Agile et DevOps.',
      'Contribution à une architecture microservices orientée DDD.',
      'Optimisation de requêtes SQL et pratique du TDD.',
    ],
  },
  {
    id: 'engepress',
    role: 'Data Scientist',
    company: 'Engepress SARL',
    period: 'Période à préciser',
    highlights: [
      'Création de modèles prédictifs avec Python et scikit-learn.',
      "Segmentation d'utilisateurs et automatisation de campagnes.",
      'Construction de pipelines dans un cadre Agile.',
    ],
  },
];

export const CERTIFICATIONS: readonly Certification[] = [
  {
    id: 'azure-fundamentals',
    name: 'Microsoft Certified: Azure Fundamentals',
    period: '2025',
    status: 'Obtenue',
  },
  {
    id: 'azure-data-fundamentals',
    name: 'Microsoft Certified: Azure Data Fundamentals',
    period: '2026',
    status: 'Obtenue',
  },
  {
    id: 'power-bi-data-analyst',
    name: 'Microsoft Certified: Power BI Data Analyst Associate',
    period: 'En cours',
    status: 'En cours',
  },
];

What should the data file contain?

The file retains the profile and skill exports. It adds the languages, education, experience, and certification collections used by the new sections.

import { Component } from '@angular/core';
import {
  EDUCATION,
  LANGUAGES,
  PROFILE,
  SKILLS,
} from '../../data/portfolio.data';

@Component({
  selector: 'app-about',
  standalone: true,
  templateUrl: './about.html',
})
export class About {
  protected readonly profile = PROFILE;
  protected readonly skills = SKILLS;
  protected readonly languages = LANGUAGES;
  protected readonly education = EDUCATION;
}

What should the About component expose?

The component exposes the profile, skills, languages, and education collections to its template.

<section id="profil" class="section">
  <div class="shell">
    <p class="eyebrow">Profil</p>
    <h2 class="section-title">Une double culture data et logiciel</h2>

    <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
      <article class="glass panel p-6 backdrop-blur-md">
        <h3>À propos</h3>
        <p>{{ profile.summary }}</p>
        <p>
          Langues:
          @for (language of languages; track language; let last = $last) {
            <span>{{ language }}{{ last ? '' : ' · ' }}</span>
          }
        </p>
        <div class="flex flex-wrap gap-2">
          @for (skill of skills; track skill) {
            <span class="tag">{{ skill }}</span>
          }
        </div>
      </article>

      <div class="grid grid-cols-1 gap-6">
        @for (item of education; track item.id) {
          <article class="glass panel p-6 backdrop-blur-md">
            <p class="card-meta">{{ item.period }}</p>
            <h3>{{ item.degree }}</h3>
            <p>{{ item.school }} · {{ item.location }}</p>
          </article>
        }
      </div>
    </div>
  </div>
</section>

What should the About template render?

The section renders the profile summary, language names, skill tags, and three education cards.

import { Component } from '@angular/core';
import { EXPERIENCES } from '../../data/portfolio.data';

@Component({
  selector: 'app-experience',
  standalone: true,
  templateUrl: './experience.html',
})
export class Experience {
  protected readonly experiences = EXPERIENCES;
}

What should the Experience component expose?

The component exposes the centralized experience collection to its timeline template.

<section id="experience" class="section">
  <div class="shell">
    <p class="eyebrow">Expérience</p>
    <h2 class="section-title">Des données aux systèmes métier</h2>

    <div class="timeline">
      @for (item of experiences; track item.id) {
        <article class="glass timeline-card p-6 backdrop-blur-md">
          <p class="card-meta">{{ item.period }}</p>
          <h3>{{ item.role }}</h3>
          <p class="company">{{ item.company }}</p>
          <ul>
            @for (highlight of item.highlights; track highlight) {
              <li>{{ highlight }}</li>
            }
          </ul>
        </article>
      }
    </div>
  </div>
</section>

What should the Experience template render?

The template creates one timeline card per job. It renders each job's achievements as a nested list.

import { Component } from '@angular/core';
import { CERTIFICATIONS } from '../../data/portfolio.data';

@Component({
  selector: 'app-certifications',
  standalone: true,
  templateUrl: './certifications.html',
})
export class Certifications {
  protected readonly certifications = CERTIFICATIONS;
}

What should the Certifications component expose?

The component exposes the centralized certification collection to its responsive grid.

<section id="certifications" class="section">
  <div class="shell">
    <p class="eyebrow">Certifications</p>
    <h2 class="section-title">Validation continue des compétences</h2>

    <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
      @for (certification of certifications; track certification.id) {
        <article class="glass panel p-6 backdrop-blur-md">
          <p class="card-meta">{{ certification.period }}</p>
          <h3>{{ certification.name }}</h3>
          <span class="status-badge">{{ certification.status }}</span>
        </article>
      }
    </div>
  </div>
</section>

What should the Certifications template render?

The template renders three tracked cards with a period, credential name, and status badge.

import { Component } from '@angular/core';

@Component({
  selector: 'app-projects',
  standalone: true,
  templateUrl: './projects.html',
})
export class Projects {}

What should the Projects component contain?

The component has no data property because this temporary version stores the Olist content directly in its template.

<section id="projets" class="section">
  <div class="shell">
    <p class="eyebrow">Projets</p>
    <h2 class="section-title">Pipelines, prédiction et apprentissage</h2>

    <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
      <article class="project-card glass transition hover:-translate-y-1 motion-reduce:transition-none motion-reduce:hover:transform-none">
        <img src="/images/olist.svg" alt="Illustration abstraite d'un pipeline de données e-commerce" />

        <div class="flex h-full flex-col gap-6 p-6">
          <div>
            <h3>Pipeline d'ingestion et de transformation E-Commerce</h3>
            <p>Pipeline analytique basé sur le jeu de données Olist pour ingérer, transformer et interroger des données e-commerce reproductibles.</p>
          </div>

          <div class="flex flex-wrap gap-2">
            <span class="tag">Python</span>
            <span class="tag">dbt</span>
            <span class="tag">DuckDB</span>
            <span class="tag">SQL</span>
            <span class="tag">Git</span>
            <span class="tag">GitHub</span>
          </div>

          <div class="project-links">
            <span>Liens à personnaliser</span>
          </div>
        </div>
      </article>
    </div>
  </div>
</section>

What should the Projects template contain?

The template contains one complete Olist article. Its repeated presentation structure remains visible for the next refactoring step.

import { Component } from '@angular/core';
import { About } from './features/about/about';
import { Certifications } from './features/certifications/certifications';
import { Experience } from './features/experience/experience';
import { Header } from './features/header/header';
import { Hero } from './features/hero/hero';
import { Projects } from './features/projects/projects';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [Header, Hero, About, Experience, Projects, Certifications],
  templateUrl: './app.html',
})
export class App {}

What should the root component import?

The root imports the Header and Hero from the previous step. It also imports all four sections added here.

<app-header></app-header>

<main>
  <app-hero></app-hero>
  <app-about></app-about>
  <app-experience></app-experience>
  <app-projects></app-projects>
  <app-certifications></app-certifications>
</main>

What should the root layout render?

The layout renders the sticky header before the main content. The five content sections follow in navigation order.

Your portfolio now proves the career story and exposes the cost of hard-coded project markup. Next, you will turn that single card into a typed project system that grows from data.

Create Data-Driven Project Cards

Votre portfolio présente maintenant le parcours professionnel complet. La section Projets révèle cependant une limite concrète puisque chaque nouveau projet exigerait une copie de la carte Olist.

Vous allez transformer cette carte en système piloté par des données avec TypeScript et un composant Angular réutilisable. Une nouvelle entrée dans le tableau suffira ensuite à produire une nouvelle carte.

In this step, get ready to:
  • Définir un modèle typé pour les projets du portfolio.
  • Ajouter trois projets avec leurs illustrations locales.
  • Remplacer la carte codée en dur par un composant réutilisable.
Définir le modèle et les données des projets

L’interface Project fixe la structure que chaque projet doit respecter. Le tableau typé devient ensuite la source unique utilisée par l’interface.

  • Dans l’Explorateur de Visual Studio Code, revenez au fichier src/app/models/portfolio.models.ts.
  • Repérez l’interface Experience.
  • Ajoutez l’interface du projet sous Experience avec ce code:
export interface Project {
  id: string;
  title: string;
  description: string;
  stack: readonly string[];
  imageUrl: string;
  imageAlt: string;
  repositoryUrl?: string;
  demoUrl?: string;
}

Que définit cette interface ?

  • Les champs id, title et description identifient le projet présenté par la carte.
  • Le tableau stack reste en lecture seule afin de protéger la liste des technologies.
  • Les champs repositoryUrl et demoUrl sont optionnels afin qu’une carte fonctionne même sans lien public.
  • Enregistrez src/app/models/portfolio.models.ts.
  • Revenez au terminal du serveur de développement pour confirmer que la modification se compile sans erreur.

Le modèle accepte désormais une fiche projet complète. Cette compilation confirme que le nouveau contrat TypeScript s’intègre au modèle existant.

Le modèle ne se compile pas ?

Vérifiez que Project se trouve après la fermeture de Experience. Chaque interface doit posséder ses propres accolades.

Comparez les deux propriétés optionnelles avec le code fourni. Le point d’interrogation doit précéder les deux-points.

aidez-moi à corriger l’interface Project dans portfolio.models.ts

Les données des projets restent séparées du rendu visuel. Cette séparation vous permet de corriger un titre ou d’ajouter une réalisation sans toucher au HTML.

  • Dans l’Explorateur de Visual Studio Code, revenez au fichier src/app/data/portfolio.data.ts.
  • Remplacez le bloc d’importation placé en haut du fichier par celui-ci:
import {
  Certification,
  Education,
  Experience,
  Profile,
  Project,
} from '../models/portfolio.models';

Pourquoi importer Project ?

L’import relie le tableau de données au contrat Project. TypeScript peut ainsi signaler une propriété manquante ou une valeur placée dans le mauvais champ.

Quelles données ajouter ?

  • Le projet Olist utilise l’identifiant olist-pipeline et l’image locale /images/olist.svg.
  • Le projet Titanic utilise l’identifiant titanic-prediction et l’image locale /images/titanic.svg.
  • La plateforme d’apprentissage utilise l’identifiant learning-platform et la technologie officielle GDScript.
  • Ajoutez le tableau PROJECTS entre EXPERIENCES et CERTIFICATIONS en utilisant le fichier complet ci-dessous comme référence.
  • Enregistrez src/app/data/portfolio.data.ts.
  • Revenez au terminal du serveur de développement pour confirmer que les trois objets respectent l’interface Project.

Le terminal termine sa reconstruction sans erreur. Vos trois fiches projet sont maintenant validées par TypeScript.

✔️ Awesome, I've got everything!

Parfait. Vérifiez que src/app/models/portfolio.models.ts et src/app/data/portfolio.data.ts sont enregistrés.

ⓧ I'd like to double check the full code

Comparez votre fichier de modèles avec cette version complète.

export interface Profile {
  name: string;
  role: string;
  location: string;
  email: string;
  phone: string;
  showPhone: boolean;
  summary: string;
  githubUrl: string;
  linkedinUrl: string;
}

export interface Education {
  id: string;
  degree: string;
  school: string;
  location: string;
  period: string;
}

export interface Experience {
  id: string;
  role: string;
  company: string;
  period: string;
  highlights: readonly string[];
}

export interface Project {
  id: string;
  title: string;
  description: string;
  stack: readonly string[];
  imageUrl: string;
  imageAlt: string;
  repositoryUrl?: string;
  demoUrl?: string;
}

export interface Certification {
  id: string;
  name: string;
  period: string;
  status: 'Obtenue' | 'En cours';
}

Ce fichier réunit les contrats utilisés par les données centralisées du portfolio.

Comparez ensuite votre fichier de données avec cette version complète.

import {
  Certification,
  Education,
  Experience,
  Profile,
  Project,
} from '../models/portfolio.models';

export const PROFILE: Profile = {
  name: 'Steve Junior Dimale',
  role: 'Data Scientist et Ingénieur logiciel',
  location: 'Québec, Canada',
  email: 'stevedimale6@gmail.com',
  phone: '(343) 208-0281',
  showPhone: false,
  summary:
    "Je combine l'analyse de données, la conception logicielle et une approche structurée de la résolution de problèmes pour transformer des besoins métier en produits fiables.",
  githubUrl: '',
  linkedinUrl: '',
};

export const SKILLS = [
  'Python',
  'SQL',
  'scikit-learn',
  'Pandas',
  'dbt',
  'DuckDB',
  'VB.NET',
  'DevOps',
  'DDD',
  'TDD',
] as const;

export const LANGUAGES = ['Français', 'Anglais'] as const;

export const EDUCATION: readonly Education[] = [
  {
    id: 'la-cite-data',
    degree: 'DEC en Sciences de données appliquées',
    school: 'La Cité collégiale',
    location: 'Ottawa',
    period: '2026 - 2028',
  },
  {
    id: 'ihtm-software',
    degree: 'DEC en Génie logiciel',
    school: 'Institut de haute technologie et de management',
    location: 'Yaoundé',
    period: 'Année à préciser',
  },
  {
    id: 'uy1-sociology',
    degree: 'Baccalauréat en sociologie',
    school: 'Université de Yaoundé 1',
    location: 'Yaoundé',
    period: 'Année à préciser',
  },
];

export const EXPERIENCES: readonly Experience[] = [
  {
    id: 'mastermind',
    role: 'Développeur logiciel VB.NET',
    company: 'Mastermind SARL',
    period: '2023 - 2025',
    highlights: [
      'Livraison en environnement Agile et DevOps.',
      'Contribution à une architecture microservices orientée DDD.',
      'Optimisation de requêtes SQL et pratique du TDD.',
    ],
  },
  {
    id: 'engepress',
    role: 'Data Scientist',
    company: 'Engepress SARL',
    period: 'Période à préciser',
    highlights: [
      'Création de modèles prédictifs avec Python et scikit-learn.',
      "Segmentation d'utilisateurs et automatisation de campagnes.",
      'Construction de pipelines dans un cadre Agile.',
    ],
  },
];

export const PROJECTS: readonly Project[] = [
  {
    id: 'olist-pipeline',
    title: "Pipeline d'ingestion et de transformation E-Commerce",
    description:
      'Pipeline analytique basé sur le jeu de données Olist pour ingérer, transformer et interroger des données e-commerce reproductibles.',
    stack: ['Python', 'dbt', 'DuckDB', 'SQL', 'Git', 'GitHub'],
    imageUrl: '/images/olist.svg',
    imageAlt: "Illustration abstraite d'un pipeline de données e-commerce",
  },
  {
    id: 'titanic-prediction',
    title: 'Pipeline d’analyse prédictive de survie',
    description:
      'Exploration, préparation et modélisation des données Titanic afin de comparer des variables et prédire la survie des passagers.',
    stack: ['Python', 'scikit-learn', 'Pandas', 'Seaborn', 'Google Colab'],
    imageUrl: '/images/titanic.svg',
    imageAlt: "Illustration abstraite d'un modèle prédictif Titanic",
  },
  {
    id: 'learning-platform',
    title: "Plateforme d'apprentissage interactive",
    description:
      "Expérience d'apprentissage interactive organisée autour de défis progressifs, de retours visuels et d'une navigation ludique.",
    stack: ['GDScript'],
    imageUrl: '/images/learning.svg',
    imageAlt: "Illustration abstraite d'une plateforme d'apprentissage",
  },
];

export const CERTIFICATIONS: readonly Certification[] = [
  {
    id: 'azure-fundamentals',
    name: 'Microsoft Certified: Azure Fundamentals',
    period: '2025',
    status: 'Obtenue',
  },
  {
    id: 'azure-data-fundamentals',
    name: 'Microsoft Certified: Azure Data Fundamentals',
    period: '2026',
    status: 'Obtenue',
  },
  {
    id: 'power-bi-data-analyst',
    name: 'Microsoft Certified: Power BI Data Analyst Associate',
    period: 'En cours',
    status: 'En cours',
  },
];

Le tableau PROJECTS contient exactement les trois objets que la grille affichera.

Les données ne se compilent pas ?

Vérifiez que Project figure dans le bloc d’importation. Vérifiez ensuite que chaque objet contient les six champs obligatoires.

La technologie du troisième projet doit être écrite exactement GDScript.

aidez-moi à corriger le tableau PROJECTS

Ajouter les illustrations locales

Les fichiers SVG restent nets sur un téléphone comme sur un grand écran. Le dossier public les rend disponibles depuis des chemins qui commencent par /images/.

  • Dans l’Explorateur de Visual Studio Code, faites un clic droit sur le dossier public.
  • Sélectionnez New Folder.
  • Nommez le dossier images.

Vous voyez maintenant public/images dans l’Explorateur. Ce dossier accueillera les trois visuels utilisés par les données.

  • Faites un clic droit sur public/images.
  • Sélectionnez New File.
  • Nommez le fichier olist.svg.

Le fichier public/images/olist.svg apparaît dans l’Explorateur.

  • Ajoutez l’illustration du pipeline e-commerce avec ce code:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450" role="img" aria-labelledby="title desc">
  <title id="title">Pipeline e-commerce</title>
  <desc id="desc">Blocs de données reliés par un flux lumineux</desc>
  <defs>
    <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#071126"/><stop offset="1" stop-color="#0f3b5f"/></linearGradient>
    <linearGradient id="flow" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#38bdf8"/><stop offset="1" stop-color="#34d399"/></linearGradient>
  </defs>
  <rect width="800" height="450" fill="url(#bg)"/><circle cx="120" cy="90" r="130" fill="#2563eb" opacity=".18"/>
  <path d="M190 225h420" stroke="url(#flow)" stroke-width="14" stroke-linecap="round" opacity=".8"/><path d="m580 195 45 30-45 30" fill="none" stroke="#34d399" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/>
  <g fill="#0b1f38" stroke="#7dd3fc" stroke-width="4"><rect x="95" y="145" width="170" height="160" rx="24"/><rect x="315" y="125" width="170" height="200" rx="24"/><rect x="535" y="145" width="170" height="160" rx="24"/></g>
  <g fill="#bae6fd"><rect x="130" y="185" width="100" height="14" rx="7"/><rect x="130" y="220" width="75" height="14" rx="7" opacity=".65"/><rect x="350" y="175" width="100" height="14" rx="7"/><rect x="350" y="210" width="75" height="14" rx="7" opacity=".65"/><rect x="350" y="245" width="90" height="14" rx="7" opacity=".4"/><rect x="570" y="185" width="100" height="14" rx="7"/><rect x="570" y="220" width="75" height="14" rx="7" opacity=".65"/></g>
</svg>

Que montre ce visuel ?

Les trois blocs représentent les étapes d’un pipeline e-commerce. Le flux lumineux relie l’ingestion à la transformation des données.

  • Enregistrez public/images/olist.svg.
  • Dans Microsoft Edge, ouvrez un nouvel onglet.
  • Saisissez http://localhost:4200/images/olist.svg dans la barre d’adresse.

Vous voyez trois blocs reliés par un flux lumineux. Le serveur peut donc servir le premier visuel local.

Le visuel Olist ne s’affiche pas ?

Vérifiez que le fichier se trouve dans public/images et que son nom est exactement olist.svg.

aidez-moi à afficher olist.svg depuis le dossier public

  • Dans l’Explorateur de Visual Studio Code, créez le fichier public/images/titanic.svg avec New File.

Le deuxième fichier apparaît à côté de olist.svg.

  • Ajoutez l’illustration de l’analyse prédictive avec ce code:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450" role="img" aria-labelledby="title desc">
  <title id="title">Analyse prédictive Titanic</title><desc id="desc">Graphique prédictif au-dessus de vagues stylisées</desc>
  <defs><linearGradient id="sea" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#0c4a6e"/><stop offset="1" stop-color="#020617"/></linearGradient></defs>
  <rect width="800" height="450" fill="#071126"/><circle cx="650" cy="90" r="150" fill="#2563eb" opacity=".18"/><rect x="100" y="75" width="600" height="260" rx="28" fill="#081a32" stroke="#7dd3fc" stroke-width="4"/>
  <path d="M150 280 260 215l95 25 105-115 90 70 100-85" fill="none" stroke="#38bdf8" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/>
  <g fill="#34d399"><circle cx="150" cy="280" r="12"/><circle cx="260" cy="215" r="12"/><circle cx="355" cy="240" r="12"/><circle cx="460" cy="125" r="12"/><circle cx="550" cy="195" r="12"/><circle cx="650" cy="110" r="12"/></g>
  <path d="M0 345c90-45 160 45 250 0s160 45 250 0 160 45 300 0v105H0z" fill="url(#sea)"/><path d="M0 365c90-45 160 45 250 0s160 45 250 0 160 45 300 0" fill="none" stroke="#7dd3fc" stroke-width="5" opacity=".7"/>
</svg>

Que montre ce visuel ?

La courbe représente une analyse prédictive. Les vagues relient l’illustration au contexte du projet Titanic.

  • Enregistrez public/images/titanic.svg.
  • Dans Microsoft Edge, remplacez le chemin de l’adresse par /images/titanic.svg.

Vous voyez une courbe prédictive au-dessus de vagues stylisées. Le deuxième chemin d’image fonctionne.

Le visuel Titanic reste vide ?

Vérifiez que la première balise commence par <svg et que la dernière balise est </svg>.

aidez-moi à corriger titanic.svg

  • Dans l’Explorateur de Visual Studio Code, créez le fichier public/images/learning.svg avec New File.

Les trois fichiers d’illustration sont maintenant regroupés dans public/images.

  • Ajoutez l’illustration de la plateforme d’apprentissage avec ce code:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450" role="img" aria-labelledby="title desc">
  <title id="title">Plateforme d'apprentissage interactive</title><desc id="desc">Parcours de cartes reliées représentant des défis progressifs</desc>
  <defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#071126"/><stop offset="1" stop-color="#172554"/></linearGradient></defs>
  <rect width="800" height="450" fill="url(#bg)"/><path d="M120 330c120-250 250 160 390-90 55-98 100-100 170-100" fill="none" stroke="#38bdf8" stroke-width="10" stroke-dasharray="18 18" stroke-linecap="round"/>
  <g stroke="#bae6fd" stroke-width="4"><rect x="70" y="270" width="150" height="100" rx="22" fill="#0b1f38"/><rect x="325" y="190" width="150" height="100" rx="22" fill="#0b1f38"/><rect x="580" y="80" width="150" height="100" rx="22" fill="#0b1f38"/></g>
  <g fill="#34d399"><circle cx="145" cy="320" r="18"/><circle cx="400" cy="240" r="18"/><circle cx="655" cy="130" r="18"/></g>
  <g fill="#bae6fd" opacity=".7"><rect x="105" y="345" width="80" height="8" rx="4"/><rect x="360" y="265" width="80" height="8" rx="4"/><rect x="615" y="155" width="80" height="8" rx="4"/></g>
</svg>

Que montre ce visuel ?

Les cartes reliées représentent une progression entre plusieurs défis. Le chemin visuel soutient le thème de l’apprentissage interactif.

  • Enregistrez public/images/learning.svg.
  • Dans Microsoft Edge, remplacez le chemin de l’adresse par /images/learning.svg.

Vous voyez trois cartes reliées par un parcours. Les trois illustrations locales sont prêtes pour la grille.

Le dernier visuel ne s’affiche pas ?

Vérifiez que le fichier s’appelle exactement learning.svg et qu’il se trouve au même niveau que les deux autres images.

aidez-moi à afficher learning.svg

✔️ Awesome, I've got everything!

Les trois illustrations s’affichent directement dans Microsoft Edge depuis leurs chemins locaux.

ⓧ I'd like to double check the full code

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450" role="img" aria-labelledby="title desc">
  <title id="title">Pipeline e-commerce</title>
  <desc id="desc">Blocs de données reliés par un flux lumineux</desc>
  <defs>
    <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#071126"/><stop offset="1" stop-color="#0f3b5f"/></linearGradient>
    <linearGradient id="flow" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#38bdf8"/><stop offset="1" stop-color="#34d399"/></linearGradient>
  </defs>
  <rect width="800" height="450" fill="url(#bg)"/><circle cx="120" cy="90" r="130" fill="#2563eb" opacity=".18"/>
  <path d="M190 225h420" stroke="url(#flow)" stroke-width="14" stroke-linecap="round" opacity=".8"/><path d="m580 195 45 30-45 30" fill="none" stroke="#34d399" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/>
  <g fill="#0b1f38" stroke="#7dd3fc" stroke-width="4"><rect x="95" y="145" width="170" height="160" rx="24"/><rect x="315" y="125" width="170" height="200" rx="24"/><rect x="535" y="145" width="170" height="160" rx="24"/></g>
  <g fill="#bae6fd"><rect x="130" y="185" width="100" height="14" rx="7"/><rect x="130" y="220" width="75" height="14" rx="7" opacity=".65"/><rect x="350" y="175" width="100" height="14" rx="7"/><rect x="350" y="210" width="75" height="14" rx="7" opacity=".65"/><rect x="350" y="245" width="90" height="14" rx="7" opacity=".4"/><rect x="570" y="185" width="100" height="14" rx="7"/><rect x="570" y="220" width="75" height="14" rx="7" opacity=".65"/></g>
</svg>

Cette version complète correspond au visuel du pipeline Olist.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450" role="img" aria-labelledby="title desc">
  <title id="title">Analyse prédictive Titanic</title><desc id="desc">Graphique prédictif au-dessus de vagues stylisées</desc>
  <defs><linearGradient id="sea" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#0c4a6e"/><stop offset="1" stop-color="#020617"/></linearGradient></defs>
  <rect width="800" height="450" fill="#071126"/><circle cx="650" cy="90" r="150" fill="#2563eb" opacity=".18"/><rect x="100" y="75" width="600" height="260" rx="28" fill="#081a32" stroke="#7dd3fc" stroke-width="4"/>
  <path d="M150 280 260 215l95 25 105-115 90 70 100-85" fill="none" stroke="#38bdf8" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/>
  <g fill="#34d399"><circle cx="150" cy="280" r="12"/><circle cx="260" cy="215" r="12"/><circle cx="355" cy="240" r="12"/><circle cx="460" cy="125" r="12"/><circle cx="550" cy="195" r="12"/><circle cx="650" cy="110" r="12"/></g>
  <path d="M0 345c90-45 160 45 250 0s160 45 250 0 160 45 300 0v105H0z" fill="url(#sea)"/><path d="M0 365c90-45 160 45 250 0s160 45 250 0 160 45 300 0" fill="none" stroke="#7dd3fc" stroke-width="5" opacity=".7"/>
</svg>

Cette version complète correspond au visuel de l’analyse Titanic.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450" role="img" aria-labelledby="title desc">
  <title id="title">Plateforme d'apprentissage interactive</title><desc id="desc">Parcours de cartes reliées représentant des défis progressifs</desc>
  <defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#071126"/><stop offset="1" stop-color="#172554"/></linearGradient></defs>
  <rect width="800" height="450" fill="url(#bg)"/><path d="M120 330c120-250 250 160 390-90 55-98 100-100 170-100" fill="none" stroke="#38bdf8" stroke-width="10" stroke-dasharray="18 18" stroke-linecap="round"/>
  <g stroke="#bae6fd" stroke-width="4"><rect x="70" y="270" width="150" height="100" rx="22" fill="#0b1f38"/><rect x="325" y="190" width="150" height="100" rx="22" fill="#0b1f38"/><rect x="580" y="80" width="150" height="100" rx="22" fill="#0b1f38"/></g>
  <g fill="#34d399"><circle cx="145" cy="320" r="18"/><circle cx="400" cy="240" r="18"/><circle cx="655" cy="130" r="18"/></g>
  <g fill="#bae6fd" opacity=".7"><rect x="105" y="345" width="80" height="8" rx="4"/><rect x="360" y="265" width="80" height="8" rx="4"/><rect x="615" y="155" width="80" height="8" rx="4"/></g>
</svg>

Cette version complète correspond au visuel de la plateforme d’apprentissage.

Créer ProjectCard et générer la grille

Le composant ProjectCard reçoit un seul projet grâce à un signal input requis. Son template peut alors afficher n’importe quel objet conforme à l’interface Project.

  • Dans l’Explorateur de Visual Studio Code, faites un clic droit sur src/app/features.
  • Sélectionnez New Folder.
  • Nommez le dossier project-card.

Vous voyez maintenant src/app/features/project-card à côté des autres fonctionnalités.

  • Dans ce dossier, créez le fichier project-card.ts avec New File.

Le fichier TypeScript du composant apparaît dans le nouveau dossier.

  • Dans le même dossier, créez le fichier project-card.html avec New File.

Le composant possède maintenant ses deux fichiers associés.

  • Dans project-card.ts, définissez le composant et son entrée requise avec ce code:
import { Component, input } from '@angular/core';
import { Project } from '../../models/portfolio.models';

@Component({
  selector: 'app-project-card',
  standalone: true,
  templateUrl: './project-card.html',
})
export class ProjectCard {
  readonly project = input.required<Project>();
}

Comment fonctionne l’entrée requise ?

L’appel input.required<Project>() exige un objet Project pour chaque instance du composant. Le template lit ensuite cette valeur en appelant project().

  • Enregistrez src/app/features/project-card/project-card.ts.
  • Revenez au terminal du serveur de développement pour confirmer que le composant se compile.

Le composant est reconnu sans erreur. Son template peut maintenant traduire les données en carte visuelle.

ProjectCard ne se compile pas ?

Vérifiez l’import de input depuis @angular/core et le chemin vers portfolio.models.

aidez-moi à corriger project-card.ts

  • Dans project-card.html, ajoutez le rendu réutilisable avec ce code:
<article class="project-card glass transition hover:-translate-y-1 motion-reduce:transition-none motion-reduce:hover:transform-none">
  <img [src]="project().imageUrl" [alt]="project().imageAlt" />

  <div class="flex h-full flex-col gap-6 p-6">
    <div>
      <h3>{{ project().title }}</h3>
      <p>{{ project().description }}</p>
    </div>

    <div class="flex flex-wrap gap-2">
      @for (technology of project().stack; track technology) {
        <span class="tag">{{ technology }}</span>
      }
    </div>

    <div class="project-links">
      @if (project().repositoryUrl) {
        <a [href]="project().repositoryUrl" target="_blank" rel="noreferrer">Code source</a>
      }
      @if (project().demoUrl) {
        <a [href]="project().demoUrl" target="_blank" rel="noreferrer">Démonstration</a>
      }
      @if (!project().repositoryUrl && !project().demoUrl) {
        <span>Liens à personnaliser</span>
      }
    </div>
  </div>
</article>

Que rend cette carte ?

  • L’image utilise imageUrl pour sa source et imageAlt pour sa description accessible.
  • Le bloc @for transforme chaque technologie de stack en étiquette.
  • Les blocs conditionnels affichent les liens disponibles ou le rappel Liens à personnaliser.
  • Enregistrez src/app/features/project-card/project-card.html.
  • Revenez au terminal du serveur de développement pour confirmer que le template se compile.

La carte réutilisable est prête. Il reste à transmettre les trois objets depuis le composant parent.

Le template signale une propriété inconnue ?

Vérifiez que chaque accès commence par project(). Un signal se lit avec des parenthèses.

aidez-moi à corriger project-card.html

  • Dans l’Explorateur de Visual Studio Code, revenez à src/app/features/projects/projects.ts.
  • Remplacez son contenu par ce code:
import { Component } from '@angular/core';
import { PROJECTS } from '../../data/portfolio.data';
import { ProjectCard } from '../project-card/project-card';

@Component({
  selector: 'app-projects',
  standalone: true,
  imports: [ProjectCard],
  templateUrl: './projects.html',
})
export class Projects {
  protected readonly projects = PROJECTS;
}

Comment le parent prépare la grille ?

Le composant parent importe PROJECTS comme source de données. Il importe aussi ProjectCard afin que son sélecteur soit disponible dans le template.

  • Enregistrez src/app/features/projects/projects.ts.
  • Revenez au terminal du serveur de développement pour confirmer que les nouveaux imports se compilent.

Le parent connaît désormais les données et le composant de présentation. La carte codée en dur peut être remplacée.

ProjectCard reste inconnu ?

Vérifiez que ProjectCard figure dans la propriété imports du décorateur du composant.

aidez-moi à importer ProjectCard dans Projects

Avant la dernière modification, combien de cartes pensez-vous que le navigateur affichera à partir des trois objets de PROJECTS ?

  • Dans l’Explorateur de Visual Studio Code, revenez à src/app/features/projects/projects.html.
  • Remplacez la carte Olist codée en dur par ce template:
<section id="projets" class="section">
  <div class="shell">
    <p class="eyebrow">Projets</p>
    <h2 class="section-title">Pipelines, prédiction et apprentissage</h2>

    <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
      @for (project of projects; track project.id) {
        <app-project-card [project]="project"></app-project-card>
      }
    </div>
  </div>
</section>

Comment la grille devient-elle extensible ?

Le bloc @for (project of projects; track project.id) crée une instance de ProjectCard pour chaque objet. L’identifiant unique aide Angular à suivre chaque carte lorsque le tableau change.

Les classes de grille commencent par une colonne. Elles passent ensuite à deux colonnes puis à trois selon la largeur disponible.

  • Enregistrez src/app/features/projects/projects.html.
  • Revenez à l’onglet du portfolio dans Microsoft Edge.
  • Actualisez http://localhost:4200/.
  • Descendez jusqu’à la section Projets.

Vous voyez trois cartes distinctes avec leurs images, leurs descriptions et leurs technologies. La duplication du HTML a disparu.

Vous voyez moins de trois cartes ?

Vérifiez que PROJECTS contient trois objets. Vérifiez ensuite que la boucle utilise bien la propriété projects du composant parent.

Si une image manque, ouvrez son chemin directement dans Microsoft Edge afin de distinguer un problème de fichier d’un problème de carte.

aidez-moi à afficher les trois ProjectCard

Le dernier test prouve que la grille dépend uniquement des données. Avant de modifier le tableau, combien de cartes attendez-vous après l’ajout temporaire d’un quatrième objet ?

  • Dans src/app/data/portfolio.data.ts, dupliquez temporairement un objet du tableau PROJECTS.
  • Remplacez l’identifiant de la copie par une valeur unique.
  • Enregistrez src/app/data/portfolio.data.ts.
  • Revenez à la section Projets dans Microsoft Edge.

Vous voyez une quatrième carte sans aucune modification de projects.html. Voilà la preuve que la présentation est maintenant pilotée par les données.

  • Supprimez l’objet temporaire de PROJECTS.
  • Enregistrez src/app/data/portfolio.data.ts.
  • Revenez à Microsoft Edge pour confirmer que la grille contient de nouveau trois cartes.

Bien joué. Votre portfolio peut maintenant grandir avec un nouvel objet au lieu d’un nouveau bloc HTML.

✔️ Awesome, I've got everything!

Vérifiez que les quatre fichiers du système de cartes sont enregistrés. Microsoft Edge doit afficher exactement trois cartes.

ⓧ I'd like to double check the full code

import { Component, input } from '@angular/core';
import { Project } from '../../models/portfolio.models';

@Component({
  selector: 'app-project-card',
  standalone: true,
  templateUrl: './project-card.html',
})
export class ProjectCard {
  readonly project = input.required<Project>();
}

Ce fichier définit le composant réutilisable et son entrée requise.

<article class="project-card glass transition hover:-translate-y-1 motion-reduce:transition-none motion-reduce:hover:transform-none">
  <img [src]="project().imageUrl" [alt]="project().imageAlt" />

  <div class="flex h-full flex-col gap-6 p-6">
    <div>
      <h3>{{ project().title }}</h3>
      <p>{{ project().description }}</p>
    </div>

    <div class="flex flex-wrap gap-2">
      @for (technology of project().stack; track technology) {
        <span class="tag">{{ technology }}</span>
      }
    </div>

    <div class="project-links">
      @if (project().repositoryUrl) {
        <a [href]="project().repositoryUrl" target="_blank" rel="noreferrer">Code source</a>
      }
      @if (project().demoUrl) {
        <a [href]="project().demoUrl" target="_blank" rel="noreferrer">Démonstration</a>
      }
      @if (!project().repositoryUrl && !project().demoUrl) {
        <span>Liens à personnaliser</span>
      }
    </div>
  </div>
</article>

Ce template affiche le visuel, le contenu, les technologies et les liens de chaque projet.

import { Component } from '@angular/core';
import { PROJECTS } from '../../data/portfolio.data';
import { ProjectCard } from '../project-card/project-card';

@Component({
  selector: 'app-projects',
  standalone: true,
  imports: [ProjectCard],
  templateUrl: './projects.html',
})
export class Projects {
  protected readonly projects = PROJECTS;
}

Ce composant parent expose le tableau et rend ProjectCard disponible dans son template.

<section id="projets" class="section">
  <div class="shell">
    <p class="eyebrow">Projets</p>
    <h2 class="section-title">Pipelines, prédiction et apprentissage</h2>

    <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
      @for (project of projects; track project.id) {
        <app-project-card [project]="project"></app-project-card>
      }
    </div>
  </div>
</section>

Ce template génère toute la grille à partir du tableau projects.

Votre grille de projets est désormais extensible et entièrement alimentée par TypeScript. Ensuite, vous finaliserez les contacts, l’accessibilité responsive et la compilation de production.

Polish the Portfolio and Build

Your typed project grid now turns three data objects into reusable cards. The completed portfolio is already visible in Microsoft Edge.

The page still needs a contact path that handles unavailable social links safely. Its responsive interactions also need a focused browser check.

You will finish the layout in Visual Studio Code. A production build will then validate the Angular TypeScript and templates.

In this step, get ready to:
  • Add a contact footer powered by the shared profile data.
  • Test the responsive layout and accessible interaction states.
  • Compile the completed portfolio for production.
Add the contact footer

The footer uses the existing PROFILE object as its single contact source. Conditional content keeps the phone private while empty social URLs produce customization reminders.

  • Expand src/app/features in the Explorer sidebar.
  • Create a folder named footer with the folder control at the top of the sidebar.

You should see footer beside the other feature folders.

  • Create footer.html inside src/app/features/footer with the file control at the top of the sidebar.
  • Build the contact template by adding this code:
<footer id="contact" class="footer section">
  <div class="shell glass flex flex-col gap-6 p-6 backdrop-blur-md md:flex-row md:items-center md:justify-between">
    <div>
      <p class="eyebrow">Contact</p>
      <h2>Construisons quelque chose d'utile.</h2>
      <a [href]="'mailto:' + profile.email">{{ profile.email }}</a>
      @if (profile.showPhone) {
        <p>{{ profile.phone }}</p>
      }
    </div>

    <div class="social-links">
      @if (profile.githubUrl) {
        <a [href]="profile.githubUrl" target="_blank" rel="noreferrer">GitHub</a>
      } @else {
        <span>GitHub à personnaliser</span>
      }

      @if (profile.linkedinUrl) {
        <a [href]="profile.linkedinUrl" target="_blank" rel="noreferrer">LinkedIn</a>
      } @else {
        <span>LinkedIn à personnaliser</span>
      }
    </div>
  </div>

  <p class="copyright">© {{ year }} {{ profile.name }}</p>
</footer>

How Does the Footer Handle Contact Data?

  • The email link reads its address from profile.email.
  • The phone appears only when profile.showPhone contains true.
  • Empty GitHub and LinkedIn URLs display customization reminders.
  • The copyright line combines the current year with the profile name.
  • Save src/app/features/footer/footer.html.
  • Confirm footer.html appears inside the footer folder in the Explorer sidebar.

Cannot Find the Footer Template?

  • Check that footer.html sits directly inside src/app/features/footer.
  • Move the file into the footer folder if it appears beside that folder.

Help me place the footer template correctly.

The template needs a component class that provides profile and year. The shared profile object keeps the footer consistent with the hero.

  • Create footer.ts inside src/app/features/footer with the file control at the top of the sidebar.
  • Define the standalone footer component by adding this code:
import { Component } from '@angular/core';
import { PROFILE } from '../../data/portfolio.data';

@Component({
  selector: 'app-footer',
  standalone: true,
  templateUrl: './footer.html',
})
export class Footer {
  protected readonly profile = PROFILE;
  protected readonly year = new Date().getFullYear();
}

What Does the Footer Component Provide?

  • The profile property exposes the centralized contact data to the template.
  • The year property reads the current year when the component loads.
  • The app-footer selector provides a reusable element for the root layout.
  • Save src/app/features/footer/footer.ts.
  • Return to the development server terminal from earlier.

The server should process the new component without reporting a TypeScript or template problem.

Seeing a Footer Compilation Problem?

  • Confirm the component class is named Footer.
  • Confirm templateUrl points to ./footer.html.
  • Confirm both footer files sit inside src/app/features/footer.

Help me fix the footer component error.

Standalone components list their template dependencies directly. Registering Footer makes its selector available to the root template.

  • Select src/app/app.ts in the Explorer sidebar.
  • Register the footer by replacing the file with this code:
import { Component } from '@angular/core';
import { About } from './features/about/about';
import { Certifications } from './features/certifications/certifications';
import { Experience } from './features/experience/experience';
import { Footer } from './features/footer/footer';
import { Header } from './features/header/header';
import { Hero } from './features/hero/hero';
import { Projects } from './features/projects/projects';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [Header, Hero, About, Experience, Projects, Certifications, Footer],
  templateUrl: './app.html',
})
export class App {}

Why Register the Footer Here?

The root component now recognizes app-footer as a template dependency. Every existing feature remains registered in the same order.

  • Save src/app/app.ts.
  • Return to the development server terminal.

The application should rebuild without an unresolved import problem.

Footer Import Not Resolving?

  • Confirm footer.ts exports a class named Footer.
  • Confirm the import path ends with ./features/footer/footer.
  • Confirm Footer appears in the root imports array.

Help me resolve the Footer import.

The root template controls the vertical page order. The footer belongs after the main content so the contact section closes the portfolio.

  • Select src/app/app.html in the Explorer sidebar.
  • Connect the footer by replacing the file with this code:
<app-header></app-header>

<main>
  <app-hero></app-hero>
  <app-about></app-about>
  <app-experience></app-experience>
  <app-projects></app-projects>
  <app-certifications></app-certifications>
</main>

<app-footer></app-footer>

How Is the Root Layout Organized?

  • The header stays outside main so its sticky positioning remains independent.
  • The portfolio sections preserve their existing order inside main.
  • The footer closes the layout after the certifications section.

Before you refresh, where do you expect the new contact section to appear in the page flow?

  • Save src/app/app.html.
  • Switch back to Microsoft Edge.
  • Refresh http://localhost:4200/.

You should see the contact footer beneath the certifications. It shows the email address plus customization reminders for GitHub and LinkedIn.

The phone remains hidden because showPhone contains false.

Footer Missing From the Page?

  • Confirm app-footer appears after the closing main tag in src/app/app.html.
  • Confirm Footer appears in the imports array in src/app/app.ts.
  • Confirm both updated files are saved.

Help me find why the footer is missing.

Your contact path is now live. Visitors can reach the supplied email address while unfinished social links remain clearly identified.

✔️ Awesome, I've got everything!

Great. Double-check that all four footer and root-layout files are saved.

ⓧ I'd like to double check the full code

Compare each complete file with the matching file in your workspace.

<footer id="contact" class="footer section">
  <div class="shell glass flex flex-col gap-6 p-6 backdrop-blur-md md:flex-row md:items-center md:justify-between">
    <div>
      <p class="eyebrow">Contact</p>
      <h2>Construisons quelque chose d'utile.</h2>
      <a [href]="'mailto:' + profile.email">{{ profile.email }}</a>
      @if (profile.showPhone) {
        <p>{{ profile.phone }}</p>
      }
    </div>

    <div class="social-links">
      @if (profile.githubUrl) {
        <a [href]="profile.githubUrl" target="_blank" rel="noreferrer">GitHub</a>
      } @else {
        <span>GitHub à personnaliser</span>
      }

      @if (profile.linkedinUrl) {
        <a [href]="profile.linkedinUrl" target="_blank" rel="noreferrer">LinkedIn</a>
      } @else {
        <span>LinkedIn à personnaliser</span>
      }
    </div>
  </div>

  <p class="copyright">© {{ year }} {{ profile.name }}</p>
</footer>

Footer Template Check

This template contains the email action plus conditional phone and social-link states. It ends with the current year and profile name.

import { Component } from '@angular/core';
import { PROFILE } from '../../data/portfolio.data';

@Component({
  selector: 'app-footer',
  standalone: true,
  templateUrl: './footer.html',
})
export class Footer {
  protected readonly profile = PROFILE;
  protected readonly year = new Date().getFullYear();
}

Footer Component Check

This component exposes the centralized profile data and the current year. Its template path points directly to footer.html.

import { Component } from '@angular/core';
import { About } from './features/about/about';
import { Certifications } from './features/certifications/certifications';
import { Experience } from './features/experience/experience';
import { Footer } from './features/footer/footer';
import { Header } from './features/header/header';
import { Hero } from './features/hero/hero';
import { Projects } from './features/projects/projects';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [Header, Hero, About, Experience, Projects, Certifications, Footer],
  templateUrl: './app.html',
})
export class App {}

Root Component Check

The root component imports all seven standalone features. Footer appears as the final entry in the imports array.

<app-header></app-header>

<main>
  <app-hero></app-hero>
  <app-about></app-about>
  <app-experience></app-experience>
  <app-projects></app-projects>
  <app-certifications></app-certifications>
</main>

<app-footer></app-footer>

Root Template Check

The root template preserves every existing section. app-footer appears after main.

Test the finished presentation

The existing src/styles.css combines the dark glassmorphism theme with Tailwind CSS responsive utilities. This check confirms those rules work across the finished page.

  • Select src/styles.css in the Explorer sidebar.
  • Locate the a:focus-visible rule.
  • Locate the @media (max-width: 47.99rem) block.
  • Locate the @media (prefers-reduced-motion: reduce) block.

These rules provide visible keyboard focus and horizontally scrollable mobile navigation. The reduced-motion block removes smooth movement when that preference is active.

  • Switch back to Microsoft Edge.
  • Narrow the browser window until the project grid uses one column.

You should see one project card per row. Every card keeps its image and text within the viewport.

  • Scroll the navigation horizontally at the top of the narrow page.

You should be able to reach every navigation link without the labels wrapping into the page content.

  • Expand the browser window to a wide desktop size.

You should see the project grid expand to three columns. The footer should arrange its contact and social content across the wider row.

  • Hover over one project card.

The card should move upward slightly. Its translucent border should become brighter.

  • Press Tab repeatedly to move through the links.

You should see a blue outline around the focused link. The outline should remain visible against the dark background.

  • Select each link in the header navigation.

Each link should move you to its matching section. The sticky navigation should remain available near the top of the window.

  • Select Contact in the header navigation.

You should arrive at the footer. It displays stevedimale6@gmail.com plus the two customization reminders.

Your system may ask which mail application should handle the email link. This prompt depends on your local application settings.

  • Select stevedimale6@gmail.com in the footer.

You should see a new message addressed to stevedimale6@gmail.com in the selected mail application.

  • Return to src/app/data/portfolio.data.ts from earlier.
  • Confirm showPhone remains false.
  • Confirm githubUrl remains an empty string.
  • Confirm linkedinUrl remains an empty string.
  • Review each value containing À préciser.
  • Keep each unverified value as its current customization reminder.

The footer state now matches the shared data. The phone stays private while missing social destinations remain visible as follow-up work.

Build the production version

The development server confirms that the portfolio works during editing. The production build performs a final compiler check.

A second PowerShell terminal lets the current preview keep running. The build can use the same workspace files independently.

  • Create a second PowerShell terminal with the plus control in the terminal panel.

The new prompt should point to the steve-dimale-portfolio workspace.

Before you run the build, do you expect the compiler to accept every TypeScript file and template?

  • Validate the production application by running this command:
ng build

What Does the Production Build Check?

  • Angular compiles the application TypeScript.
  • Angular validates the component templates.
  • The build writes the production artifacts under dist/steve-dimale-portfolio.

The terminal should finish without compilation errors. That clears the final technical gate for the completed responsive portfolio.

Production Build Failing?

  • Start with the first file path reported by the compiler.
  • Compare that file with the full-code reference above when the error points to the footer or root layout.
  • Save the corrected file before running the build again.

Help me diagnose the production build failure.

Secret mission

Révéler les sections au défilement

Donnez un rythme visuel à votre portfolio lorsque le visiteur descend dans la page. Vous allez créer une directive Angular qui révèle chaque section une seule fois tout en respectant la préférence de réduction des mouvements.

Clean Up Your Resources

Clean Up Your Resources

Votre portfolio est entièrement local. Choisissez de conserver le workspace, d’arrêter le serveur pour y revenir, ou de supprimer le dossier sans laisser de service cloud actif ni de coût continu.

Resources you used:

  • Le dossier steve-dimale-portfolio, qui contient les sources Angular, les SVG locaux, le dépôt Git et node_modules.
  • Le serveur de développement local du portfolio, s’il est encore actif.

Keep everything running

Aucune action n’est nécessaire si vous continuez à développer votre portfolio.

  • Continuez à personnaliser les informations du portfolio dans src/app/data/portfolio.data.ts.
  • Conservez le dossier steve-dimale-portfolio avec son dépôt Git local et ses dépendances installées.

Pause - I'll come back to this later

Cette option arrête le serveur local pour libérer les ressources de votre ordinateur. Tous les fichiers du portfolio restent disponibles.

  • Revenez dans le terminal intégré de Visual Studio Code utilisé pour le serveur local.
  • Appuyez sur Ctrl+C pour arrêter le serveur.

C’est fait. Le serveur local est arrêté.

Votre code reste intact dans le dossier steve-dimale-portfolio.

  • À votre retour, revenez dans le terminal intégré depuis le dossier steve-dimale-portfolio.
  • Relancez le serveur de développement en exécutant cette commande:
npm start

Ce que fait cette commande

Cette commande relance le serveur de développement du portfolio. Vous pourrez de nouveau tester la page dans Microsoft Edge à l’adresse http://localhost:4200/.

Le serveur ne redémarre pas ?

Vérifiez que le terminal se trouve dans le dossier steve-dimale-portfolio.

Si le problème continue, aidez-moi à diagnostiquer le redémarrage du serveur Angular.

Delete - I don't want to use this again

Cette option supprime définitivement le workspace local et son dépôt Git. L’option Pause conserve tout votre travail si vous souhaitez encore y revenir.

  • Revenez dans le terminal intégré de Visual Studio Code utilisé pour le serveur local.
  • Appuyez sur Ctrl+C si le serveur fonctionne encore.
  • Dans l’Explorateur Windows, localisez le dossier steve-dimale-portfolio que vous avez créé.
  • Supprimez le dossier steve-dimale-portfolio depuis son menu contextuel.
  • Confirmez la suppression si Windows demande une confirmation.

Vous ne devez plus voir le dossier steve-dimale-portfolio dans l’Explorateur Windows. Le code source, les SVG locaux, le dépôt Git et les dépendances du projet sont désormais supprimés.

Nice Work!

Nice Work!

Bravo. Vous avez construit un portfolio Angular standalone qui présente le profil de Steve Junior Dimale dans une interface responsive prête à être personnalisée.

Vous avez appris à :

  • Structurer une application standalone avec des composants dédiés au hero, au profil, aux expériences, aux projets, aux certifications et aux contacts. La navigation sticky relie chaque section du portfolio.
  • Séparer les modèles et les données avec des interfaces TypeScript typées. La grille de projets utilise input.required<Project>() dans un composant réutilisable. Le contrôle de flux @for transforme chaque objet de PROJECTS en carte.
  • Créer une identité visuelle accessible avec Tailwind CSS, le glassmorphism et des visuels SVG locaux. Le portfolio adapte sa grille aux écrans étroits et larges. Sa compilation de production confirme que les données et les templates sont valides.
  • Secret Mission : Ajouté la directive appReveal avec IntersectionObserver pour révéler chaque section une seule fois. Les styles de réduction des mouvements gardent le contenu immédiatement visible lorsque l’animation est désactivée.

Prêt à tester vos connaissances ?