SPB Git

spb/hilmacorp

Public

chore(retrait cluster): synchro copie du nœud M3U96a + README « URL live hors fonction » (2026-09-04)

Simon-Pierre Boucher committed (Sep 4, 2026) Browse files

46 changed files +5,101 −0

added .gitignore +15 −0
@@ -0,0 +1,15 @@
1 +# Author: Simon-Pierre Boucher
2 +# Contact: contact@spboucher.ai
3 +# Project: Hilmacorp.ai — Web Platform
4 +# File: .gitignore
5 +# Description: Files and directories excluded from version control
6 +
7 +/node_modules
8 +/.next
9 +/out
10 +.env
11 +.env.local
12 +.DS_Store
13 +*.tsbuildinfo
14 +next-env.d.ts
15 +/data
added CLAUDE.md +124 −0
@@ -0,0 +1,124 @@
1 +# CLAUDE.md — Plateforme Web Hilmacorp.ai
2 +
3 +## Objectif du projet
4 +
5 +Construire le site web officiel de **Hilmacorp.ai**, une entreprise d'intelligence artificielle fondée par **Simon-Pierre Boucher** et **Massamba Mbaye**. Le site doit être moderne, professionnel, rapide et bilingue (français / anglais), et présenter l'entreprise, ses services, ses fondateurs et sa vision.
6 +
7 +---
8 +
9 +## ⚠️ RÈGLE OBLIGATOIRE — En-tête de chaque fichier
10 +
11 +**Chaque fichier créé dans ce projet (code, config, style, composant, script, etc.) DOIT commencer par l'en-tête suivant**, adapté à la syntaxe de commentaire du langage :
12 +
13 +```
14 +/**
15 + * Author: Simon-Pierre Boucher
16 + * Contact: contact@spboucher.ai
17 + * Project: Hilmacorp.ai — Web Platform
18 + * File: <chemin/nom-du-fichier>
19 + * Description: <courte description du rôle du fichier>
20 + */
21 +```
22 +
23 +Exemples selon le type de fichier :
24 +
25 +- **JS / TS / TSX / CSS** : bloc `/** ... */`
26 +- **HTML** : `<!-- Author: Simon-Pierre Boucher | Contact: contact@spboucher.ai | ... -->`
27 +- **Python / Shell / YAML** : lignes commençant par `#`
28 +- **Markdown** : bloc de commentaire HTML `<!-- ... -->` en tête de fichier
29 +- **JSON** : exception — pas de commentaire possible; ajouter si pertinent un champ `"_author": "Simon-Pierre Boucher <contact@spboucher.ai>"` quand le schéma le permet (ex. `package.json` → champ `author`).
30 +
31 +**Ne jamais créer ou modifier un fichier sans vérifier la présence de cet en-tête.**
32 +
33 +---
34 +
35 +## Stack technique recommandé
36 +
37 +- **Framework** : Next.js 14+ (App Router) avec TypeScript
38 +- **Styles** : Tailwind CSS
39 +- **Composants** : composants React réutilisables, design system léger
40 +- **i18n** : support français / anglais (next-intl ou équivalent)
41 +- **Déploiement cible** : Vercel ou infrastructure privée (le code doit rester portable)
42 +- **SEO** : métadonnées complètes, Open Graph, sitemap, robots.txt
43 +- **Formulaire de contact** : API route avec validation, envoi vers `contact@spboucher.ai`
44 +
45 +---
46 +
47 +## Structure du site (pages)
48 +
49 +1. **Accueil (`/`)**
50 + - Hero : « Hilmacorp.ai builds intelligent systems for organizations that want to work, analyze, decide, and innovate differently. »
51 + - Présentation courte de l'entreprise
52 + - Aperçu des services principaux avec liens
53 + - Appel à l'action (contact / démonstration)
54 +
55 +2. **À propos (`/about`)**
56 + - Mission : rendre l'IA avancée utile, accessible et opérationnelle pour les organisations de toutes tailles
57 + - Approche (processus itératif en 8 étapes : identifier le problème → évaluer données/infrastructure → concevoir l'architecture → prototype → tests → mesure de la valeur → déploiement contrôlé → amélioration continue)
58 + - Vision de l'entreprise
59 +
60 +3. **Fondateurs (`/founders`)**
61 + - **Simon-Pierre Boucher** : recherche en IA, grands modèles de langage (LLM), agents IA, analyse de données, modélisation financière, développement logiciel, systèmes IA avancés, modèles open-weight, infrastructure locale, systèmes multimodaux, workflows agentiques.
62 + - **Massamba Mbaye** : leadership, développement stratégique, opérations d'affaires, partenariats, déploiement commercial de technologies innovantes, lien entre capacités techniques et besoins organisationnels.
63 +
64 +4. **Services (`/services`)** — une page index + sous-pages ou sections pour :
65 + - Agents IA et automatisation intelligente
66 + - Grands modèles de langage (approche indépendante des fournisseurs, routage multi-modèles)
67 + - IA privée et locale (déploiements open-weight, Apple Silicon, GPU, architectures hybrides)
68 + - RAG — Retrieval-Augmented Generation (citations, contrôles d'accès, journaux d'audit)
69 + - Analyse de données et recherche (finance, immobilier, veille concurrentielle, prévision…)
70 + - IA multimodale (texte, images, PDF, tableaux, audio, vidéo, données structurées)
71 + - Stratégie et consultation IA (évaluations d'opportunités, feuilles de route, POC, formation)
72 + - Développement logiciel sur mesure (plateformes, APIs, tableaux de bord, intégrations)
73 +
74 +5. **Sécurité et déploiement responsable (`/security`)**
75 + - Contrôles d'accès par rôles, authentification sécurisée, chiffrement, hébergement privé de modèles, workflows d'approbation humaine, monitoring, journalisation, validation des sorties.
76 +
77 +6. **Industries (`/industries`)**
78 + - Services financiers, immobilier, éducation, recherche académique, services professionnels, administration publique, technologie, consultation, assurance, juridique et conformité, médias, commerce de détail.
79 +
80 +7. **Contact (`/contact`)**
81 + - Formulaire (nom, organisation, courriel, message)
82 + - Courriel de contact affiché : **contact@spboucher.ai**
83 +
84 +---
85 +
86 +## Directives de design
87 +
88 +- Esthétique sobre, technologique et crédible : fond sombre ou clair épuré, typographie soignée, beaucoup d'espace blanc.
89 +- Éviter les templates génériques; le design doit paraître intentionnel et distinctif.
90 +- Palette suggérée : tons neutres profonds + un accent (bleu profond ou vert technologique).
91 +- Responsive obligatoire (mobile-first).
92 +- Accessibilité : contrastes AA minimum, navigation clavier, balises sémantiques.
93 +- Performance : images optimisées, chargement paresseux, score Lighthouse ≥ 90.
94 +
95 +---
96 +
97 +## Contenu
98 +
99 +- Tout le contenu textuel provient du document de présentation Hilmacorp.ai (mission, services, approche, vision). Reprendre ce contenu fidèlement, en versions française et anglaise.
100 +- Ton : professionnel, clair, orienté résultats. Pas de jargon marketing vide.
101 +
102 +---
103 +
104 +## Conventions de développement
105 +
106 +- TypeScript strict activé.
107 +- Composants dans `components/`, pages dans `app/`, contenus i18n dans `messages/` ou `locales/`.
108 +- Un composant = un fichier. Nommage PascalCase pour les composants.
109 +- Commits atomiques avec messages clairs.
110 +- Tests de base pour les API routes (formulaire de contact).
111 +- Aucune clé API ou secret dans le code — utiliser les variables d'environnement (`.env.example` fourni).
112 +
113 +---
114 +
115 +## Checklist avant livraison
116 +
117 +- [ ] En-tête **Simon-Pierre Boucher / contact@spboucher.ai** présent dans chaque fichier
118 +- [ ] Site bilingue FR/EN fonctionnel
119 +- [ ] Toutes les pages listées ci-dessus créées
120 +- [ ] Formulaire de contact fonctionnel avec validation
121 +- [ ] SEO et métadonnées configurés
122 +- [ ] Responsive vérifié (mobile, tablette, desktop)
123 +- [ ] Lighthouse ≥ 90 (performance, accessibilité, SEO)
124 +- [ ] `README.md` avec instructions d'installation et de déploiement
added README.md +72 −0
@@ -0,0 +1,72 @@
1 +> ⚠️ **URL live hors fonction pour le moment.** L'application a été retirée du cluster MacLustr le 2026-09-04 (processus arrêtés, copie du nœud supprimée). Ce dépôt spbgit est désormais la seule source de vérité du projet. Ancienne URL : https://www.hilmacorp.ai
2 +
3 +<!--
4 +Author: Simon-Pierre Boucher
5 +Contact: contact@spboucher.ai
6 +Project: Hilmacorp.ai — Web Platform
7 +File: README.md
8 +Description: Installation, development, testing, and deployment instructions
9 +-->
10 +
11 +# Hilmacorp.ai — Web Platform
12 +
13 +Official bilingual (FR/EN) corporate website of **Hilmacorp.ai**, an artificial-intelligence
14 +company founded by **Simon-Pierre Boucher** and **Massamba Mbaye**.
15 +
16 +## Stack
17 +
18 +- **Next.js 15** (App Router) + **TypeScript** (strict)
19 +- **Tailwind CSS v4**
20 +- Custom lightweight i18n (`/fr` and `/en` locale segments, dictionaries in `messages/`)
21 +- Contact form API route with server-side validation (`/api/contact`)
22 +- Full SEO: per-page metadata, Open Graph, `sitemap.xml`, `robots.txt`
23 +
24 +## Project layout
25 +
26 +```
27 +app/
28 + [locale]/ Pages (home, about, founders, services, security, industries, contact)
29 + api/contact/ Contact form API route
30 + robots.ts sitemap.ts SEO metadata routes
31 +components/ Reusable React components (PascalCase, one component per file)
32 +lib/ i18n loader + contact validation (pure, tested)
33 +messages/ fr.json / en.json content dictionaries
34 +tests/ node:test unit tests for the API validation
35 +```
36 +
37 +## Installation
38 +
39 +```bash
40 +npm install
41 +cp .env.example .env # then fill in values as needed
42 +```
43 +
44 +## Development
45 +
46 +```bash
47 +npm run dev # http://localhost:3000 → redirects to /fr or /en
48 +```
49 +
50 +## Tests
51 +
52 +```bash
53 +npm test # node --test (requires Node ≥ 22 for native TS type stripping)
54 +```
55 +
56 +## Production build & start
57 +
58 +```bash
59 +npm run build
60 +npm run start -- -p 8070
61 +```
62 +
63 +## Environment variables
64 +
65 +See `.env.example`. No secrets are committed. If `RESEND_API_KEY` is unset, contact
66 +messages are validated and appended to `data/contact-messages.jsonl` on the server.
67 +
68 +## Deployment
69 +
70 +The site is portable: Vercel, any Node host, or private infrastructure.
71 +Current production deployment: MacLustr node **M3U96a**, PM2 (`hilmacorp` + `hilmacorp-ngrok`),
72 +exposed at **https://www.hilmacorp.ai** via ngrok on port 8070.
added app/[locale]/[...rest]/page.tsx +13 −0
@@ -0,0 +1,13 @@
1 +/**
2 + * Author: Simon-Pierre Boucher
3 + * Contact: contact@spboucher.ai
4 + * Project: Hilmacorp.ai — Web Platform
5 + * File: app/[locale]/[...rest]/page.tsx
6 + * Description: Catch-all route delegating unknown paths to the locale-aware 404 page
7 + */
8 +
9 +import { notFound } from "next/navigation";
10 +
11 +export default function CatchAll() {
12 + notFound();
13 +}
added app/[locale]/about/page.tsx +104 −0
@@ -0,0 +1,104 @@
1 +/**
2 + * Author: Simon-Pierre Boucher
3 + * Contact: contact@spboucher.ai
4 + * Project: Hilmacorp.ai — Web Platform
5 + * File: app/[locale]/about/page.tsx
6 + * Description: About page — mission, iterative eight-step approach, principles, and company vision
7 + */
8 +
9 +import type { Metadata } from "next";
10 +import Container from "@/components/Container";
11 +import SectionHeading from "@/components/SectionHeading";
12 +import { getMessages, isLocale, type Locale } from "@/lib/i18n";
13 +
14 +export async function generateMetadata({
15 + params,
16 +}: {
17 + params: Promise<{ locale: string }>;
18 +}): Promise<Metadata> {
19 + const { locale } = await params;
20 + if (!isLocale(locale)) return {};
21 + const { meta } = getMessages(locale);
22 + return { title: { absolute: meta.about.title }, description: meta.about.description };
23 +}
24 +
25 +export default async function AboutPage({
26 + params,
27 +}: {
28 + params: Promise<{ locale: string }>;
29 +}) {
30 + const { locale } = await params;
31 + const { about } = getMessages(locale as Locale);
32 +
33 + return (
34 + <>
35 + <section className="relative overflow-hidden border-b border-line">
36 + <div className="grid-backdrop absolute inset-0" aria-hidden="true" />
37 + <Container className="relative py-20 sm:py-28">
38 + <SectionHeading as="h1" title={about.title} lead={about.lead} />
39 + </Container>
40 + </section>
41 +
42 + {/* Mission */}
43 + <section className="py-20 sm:py-24">
44 + <Container className="grid gap-10 lg:grid-cols-[1fr_2fr]">
45 + <h2 className="font-display text-2xl font-semibold tracking-tight text-accent">
46 + {about.missionTitle}
47 + </h2>
48 + <p className="text-lg leading-relaxed text-ink">{about.missionBody}</p>
49 + </Container>
50 + </section>
51 +
52 + {/* Approach — 8 steps */}
53 + <section className="border-t border-line bg-paper-2 py-20 sm:py-24">
54 + <Container>
55 + <SectionHeading title={about.approachTitle} lead={about.approachLead} />
56 + <ol className="mt-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
57 + {about.steps.map((step, index) => (
58 + <li
59 + key={step.title}
60 + className="rounded-2xl border border-line bg-card p-6"
61 + >
62 + <p className="font-mono text-sm text-accent">
63 + {String(index + 1).padStart(2, "0")}
64 + </p>
65 + <h3 className="mt-3 text-base font-semibold text-ink">{step.title}</h3>
66 + <p className="mt-2 text-sm leading-relaxed text-ink-faint">{step.body}</p>
67 + </li>
68 + ))}
69 + </ol>
70 + </Container>
71 + </section>
72 +
73 + {/* Principles */}
74 + <section className="py-20 sm:py-24">
75 + <Container>
76 + <SectionHeading title={about.principlesTitle} lead={about.principlesLead} />
77 + <ul className="mt-12 grid gap-x-12 gap-y-10 sm:grid-cols-2">
78 + {about.principles.map((principle) => (
79 + <li key={principle.title} className="border-l-2 border-accent pl-6">
80 + <h3 className="font-display text-xl font-semibold text-ink">
81 + {principle.title}
82 + </h3>
83 + <p className="mt-2 leading-relaxed text-ink-soft">{principle.body}</p>
84 + </li>
85 + ))}
86 + </ul>
87 + </Container>
88 + </section>
89 +
90 + {/* Vision */}
91 + <section className="border-t border-line bg-ink py-20 sm:py-24">
92 + <Container className="grid gap-10 lg:grid-cols-[1fr_2fr]">
93 + <h2 className="font-display text-2xl font-semibold tracking-tight text-paper">
94 + {about.visionTitle}
95 + </h2>
96 + <div className="space-y-5">
97 + <p className="text-lg leading-relaxed text-paper/80">{about.visionBody1}</p>
98 + <p className="text-lg leading-relaxed text-paper/80">{about.visionBody2}</p>
99 + </div>
100 + </Container>
101 + </section>
102 + </>
103 + );
104 +}
added app/[locale]/apps/page.tsx +168 −0
@@ -0,0 +1,168 @@
1 +/**
2 + * Author: Simon-Pierre Boucher
3 + * Contact: contact@spboucher.ai
4 + * Project: Hilmacorp.ai — Web Platform
5 + * File: app/[locale]/apps/page.tsx
6 + * Description: Apps page — web platforms (AI Risk Index, LLM Index) and the Zyquo family
7 + * of native macOS applications with direct DMG downloads
8 + */
9 +
10 +import type { Metadata } from "next";
11 +import Container from "@/components/Container";
12 +import SectionHeading from "@/components/SectionHeading";
13 +import { getMessages, isLocale, type Locale } from "@/lib/i18n";
14 +
15 +export async function generateMetadata({
16 + params,
17 +}: {
18 + params: Promise<{ locale: string }>;
19 +}): Promise<Metadata> {
20 + const { locale } = await params;
21 + if (!isLocale(locale)) return {};
22 + const { meta } = getMessages(locale);
23 + return { title: { absolute: meta.apps.title }, description: meta.apps.description };
24 +}
25 +
26 +function CheckIcon() {
27 + return (
28 + <svg
29 + width="16"
30 + height="16"
31 + viewBox="0 0 16 16"
32 + fill="none"
33 + aria-hidden="true"
34 + className="mt-0.5 shrink-0"
35 + >
36 + <path
37 + d="M3 8.5l3.2 3L13 4.5"
38 + stroke="#0b6b4a"
39 + strokeWidth="1.8"
40 + strokeLinecap="round"
41 + strokeLinejoin="round"
42 + />
43 + </svg>
44 + );
45 +}
46 +
47 +export default async function AppsPage({
48 + params,
49 +}: {
50 + params: Promise<{ locale: string }>;
51 +}) {
52 + const { locale } = await params;
53 + const { apps } = getMessages(locale as Locale);
54 +
55 + return (
56 + <>
57 + <section className="relative overflow-hidden border-b border-line">
58 + <div className="grid-backdrop absolute inset-0" aria-hidden="true" />
59 + <Container className="relative py-20 sm:py-28">
60 + <SectionHeading as="h1" title={apps.title} lead={apps.lead} />
61 + </Container>
62 + </section>
63 +
64 + {/* Web platforms */}
65 + <section className="py-16 sm:py-20">
66 + <Container>
67 + <SectionHeading title={apps.webTitle} lead={apps.webLead} />
68 + <div className="mt-10 grid gap-6 lg:grid-cols-2">
69 + {apps.web.map((app) => (
70 + <article
71 + key={app.id}
72 + className="flex flex-col rounded-3xl border border-line bg-card p-8 shadow-sm sm:p-10"
73 + >
74 + <p className="font-mono text-xs uppercase tracking-[0.2em] text-ink-faint">
75 + {app.urlLabel}
76 + </p>
77 + <h3 className="mt-3 font-display text-2xl font-semibold tracking-tight text-ink">
78 + {app.name}
79 + </h3>
80 + <p className="mt-2 font-medium text-accent">{app.tagline}</p>
81 + <p className="mt-4 leading-relaxed text-ink-soft">{app.description}</p>
82 + <ul className="mt-6 grid gap-2.5">
83 + {app.points.map((point) => (
84 + <li key={point} className="flex items-start gap-2.5 text-sm text-ink">
85 + <CheckIcon />
86 + {point}
87 + </li>
88 + ))}
89 + </ul>
90 + <div className="mt-8 flex-1" />
91 + <a
92 + href={app.url}
93 + target="_blank"
94 + rel="noopener noreferrer"
95 + className="inline-flex w-fit items-center gap-2 rounded-full bg-ink px-6 py-2.5 text-sm font-medium text-paper transition-colors hover:bg-accent-strong"
96 + >
97 + {apps.visit}
98 + <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
99 + <path
100 + d="M4 10L10 4M10 4H5.5M10 4v4.5"
101 + stroke="currentColor"
102 + strokeWidth="1.6"
103 + strokeLinecap="round"
104 + strokeLinejoin="round"
105 + />
106 + </svg>
107 + </a>
108 + </article>
109 + ))}
110 + </div>
111 + </Container>
112 + </section>
113 +
114 + {/* macOS apps */}
115 + <section className="border-t border-line bg-paper-2 py-16 sm:py-20">
116 + <Container>
117 + <SectionHeading title={apps.macTitle} lead={apps.macLead} />
118 + <div className="mt-10 grid gap-6 lg:grid-cols-2">
119 + {apps.mac.map((app) => (
120 + <article
121 + key={app.id}
122 + className="flex flex-col rounded-3xl border border-line bg-card p-8 shadow-sm sm:p-10"
123 + >
124 + <p className="font-mono text-xs uppercase tracking-[0.2em] text-ink-faint">
125 + {app.requires}
126 + </p>
127 + <h3 className="mt-3 font-display text-2xl font-semibold tracking-tight text-ink">
128 + {app.name}
129 + </h3>
130 + <p className="mt-2 font-medium text-accent">{app.tagline}</p>
131 + <p className="mt-4 leading-relaxed text-ink-soft">{app.description}</p>
132 + <ul className="mt-6 grid gap-2.5">
133 + {app.points.map((point) => (
134 + <li key={point} className="flex items-start gap-2.5 text-sm text-ink">
135 + <CheckIcon />
136 + {point}
137 + </li>
138 + ))}
139 + </ul>
140 + <div className="mt-8 flex-1" />
141 + <div className="flex flex-wrap items-center gap-4">
142 + <a
143 + href={`/downloads/${app.file}`}
144 + download
145 + className="inline-flex items-center gap-2 rounded-full bg-ink px-6 py-2.5 text-sm font-medium text-paper transition-colors hover:bg-accent-strong"
146 + >
147 + <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
148 + <path
149 + d="M7 1.5v8m0 0L3.8 6.3M7 9.5l3.2-3.2M2 12.5h10"
150 + stroke="currentColor"
151 + strokeWidth="1.6"
152 + strokeLinecap="round"
153 + strokeLinejoin="round"
154 + />
155 + </svg>
156 + {apps.download}
157 + </a>
158 + <span className="font-mono text-xs text-ink-faint">{app.size}</span>
159 + </div>
160 + </article>
161 + ))}
162 + </div>
163 + <p className="mt-10 text-sm text-ink-faint">{apps.macNote}</p>
164 + </Container>
165 + </section>
166 + </>
167 + );
168 +}
added app/[locale]/contact/page.tsx +78 −0
@@ -0,0 +1,78 @@
1 +/**
2 + * Author: Simon-Pierre Boucher
3 + * Contact: contact@spboucher.ai
4 + * Project: Hilmacorp.ai — Web Platform
5 + * File: app/[locale]/contact/page.tsx
6 + * Description: Contact page — practical information cards and validated contact form
7 + */
8 +
9 +import type { Metadata } from "next";
10 +import Container from "@/components/Container";
11 +import SectionHeading from "@/components/SectionHeading";
12 +import ContactForm from "@/components/ContactForm";
13 +import { getMessages, isLocale, type Locale } from "@/lib/i18n";
14 +
15 +export async function generateMetadata({
16 + params,
17 +}: {
18 + params: Promise<{ locale: string }>;
19 +}): Promise<Metadata> {
20 + const { locale } = await params;
21 + if (!isLocale(locale)) return {};
22 + const { meta } = getMessages(locale);
23 + return { title: { absolute: meta.contact.title }, description: meta.contact.description };
24 +}
25 +
26 +export default async function ContactPage({
27 + params,
28 +}: {
29 + params: Promise<{ locale: string }>;
30 +}) {
31 + const { locale } = await params;
32 + const { contact } = getMessages(locale as Locale);
33 +
34 + const infoCards = [
35 + {
36 + title: contact.infoEmailTitle,
37 + body: (
38 + <a
39 + href="mailto:contact@spboucher.ai"
40 + className="font-medium text-accent hover:text-accent-strong"
41 + >
42 + contact@spboucher.ai
43 + </a>
44 + ),
45 + },
46 + { title: contact.infoResponseTitle, body: contact.infoResponse },
47 + { title: contact.infoLanguagesTitle, body: contact.infoLanguages },
48 + ];
49 +
50 + return (
51 + <>
52 + <section className="relative overflow-hidden border-b border-line">
53 + <div className="grid-backdrop absolute inset-0" aria-hidden="true" />
54 + <Container className="relative py-20 sm:py-28">
55 + <SectionHeading as="h1" title={contact.title} lead={contact.lead} />
56 + </Container>
57 + </section>
58 +
59 + <section className="py-16 sm:py-20">
60 + <Container className="grid gap-12 lg:grid-cols-[1fr_2fr]">
61 + <div className="flex flex-col gap-4">
62 + {infoCards.map((card) => (
63 + <div key={card.title} className="rounded-2xl border border-line bg-card p-6">
64 + <h2 className="font-mono text-xs uppercase tracking-[0.25em] text-ink-faint">
65 + {card.title}
66 + </h2>
67 + <p className="mt-2.5 text-sm leading-relaxed text-ink-soft">{card.body}</p>
68 + </div>
69 + ))}
70 + </div>
71 + <div className="rounded-3xl border border-line bg-card p-8 shadow-sm sm:p-10">
72 + <ContactForm strings={contact.form} />
73 + </div>
74 + </Container>
75 + </section>
76 + </>
77 + );
78 +}
added app/[locale]/founders/page.tsx +104 −0
@@ -0,0 +1,104 @@
1 +/**
2 + * Author: Simon-Pierre Boucher
3 + * Contact: contact@spboucher.ai
4 + * Project: Hilmacorp.ai — Web Platform
5 + * File: app/[locale]/founders/page.tsx
6 + * Description: Founders page — profiles of Simon-Pierre Boucher and Massamba Mbaye
7 + */
8 +
9 +import type { Metadata } from "next";
10 +import Container from "@/components/Container";
11 +import SectionHeading from "@/components/SectionHeading";
12 +import { getMessages, isLocale, type Locale } from "@/lib/i18n";
13 +
14 +export async function generateMetadata({
15 + params,
16 +}: {
17 + params: Promise<{ locale: string }>;
18 +}): Promise<Metadata> {
19 + const { locale } = await params;
20 + if (!isLocale(locale)) return {};
21 + const { meta } = getMessages(locale);
22 + return { title: { absolute: meta.founders.title }, description: meta.founders.description };
23 +}
24 +
25 +function FounderCard({
26 + name,
27 + role,
28 + bio,
29 + tags,
30 + initials,
31 +}: {
32 + name: string;
33 + role: string;
34 + bio: string;
35 + tags: string[];
36 + initials: string;
37 +}) {
38 + return (
39 + <article className="rounded-3xl border border-line bg-card p-8 shadow-sm sm:p-10">
40 + <div className="flex items-center gap-5">
41 + <span
42 + aria-hidden="true"
43 + className="flex h-16 w-16 shrink-0 items-center justify-center rounded-2xl bg-ink font-mono text-lg font-semibold text-paper"
44 + >
45 + {initials}
46 + </span>
47 + <div>
48 + <h2 className="font-display text-2xl font-semibold text-ink">{name}</h2>
49 + <p className="mt-1 text-sm font-medium text-accent">{role}</p>
50 + </div>
51 + </div>
52 + <p className="mt-6 leading-relaxed text-ink-soft">{bio}</p>
53 + <ul className="mt-6 flex flex-wrap gap-2">
54 + {tags.map((tag) => (
55 + <li
56 + key={tag}
57 + className="rounded-full bg-accent-soft px-3 py-1 text-xs font-medium text-accent-strong"
58 + >
59 + {tag}
60 + </li>
61 + ))}
62 + </ul>
63 + </article>
64 + );
65 +}
66 +
67 +export default async function FoundersPage({
68 + params,
69 +}: {
70 + params: Promise<{ locale: string }>;
71 +}) {
72 + const { locale } = await params;
73 + const { founders } = getMessages(locale as Locale);
74 +
75 + return (
76 + <>
77 + <section className="relative overflow-hidden border-b border-line">
78 + <div className="grid-backdrop absolute inset-0" aria-hidden="true" />
79 + <Container className="relative py-20 sm:py-28">
80 + <SectionHeading as="h1" title={founders.title} lead={founders.lead} />
81 + </Container>
82 + </section>
83 +
84 + <section className="py-20 sm:py-24">
85 + <Container className="grid gap-8 lg:grid-cols-2">
86 + <FounderCard
87 + name={founders.spb.name}
88 + role={founders.spb.role}
89 + bio={founders.spb.bio}
90 + tags={founders.spb.tags}
91 + initials="SPB"
92 + />
93 + <FounderCard
94 + name={founders.mm.name}
95 + role={founders.mm.role}
96 + bio={founders.mm.bio}
97 + tags={founders.mm.tags}
98 + initials="MM"
99 + />
100 + </Container>
101 + </section>
102 + </>
103 + );
104 +}
added app/[locale]/industries/page.tsx +61 −0
@@ -0,0 +1,61 @@
1 +/**
2 + * Author: Simon-Pierre Boucher
3 + * Contact: contact@spboucher.ai
4 + * Project: Hilmacorp.ai — Web Platform
5 + * File: app/[locale]/industries/page.tsx
6 + * Description: Industries page — the twelve sectors served by Hilmacorp.ai
7 + */
8 +
9 +import type { Metadata } from "next";
10 +import Container from "@/components/Container";
11 +import SectionHeading from "@/components/SectionHeading";
12 +import { getMessages, isLocale, type Locale } from "@/lib/i18n";
13 +
14 +export async function generateMetadata({
15 + params,
16 +}: {
17 + params: Promise<{ locale: string }>;
18 +}): Promise<Metadata> {
19 + const { locale } = await params;
20 + if (!isLocale(locale)) return {};
21 + const { meta } = getMessages(locale);
22 + return { title: { absolute: meta.industries.title }, description: meta.industries.description };
23 +}
24 +
25 +export default async function IndustriesPage({
26 + params,
27 +}: {
28 + params: Promise<{ locale: string }>;
29 +}) {
30 + const { locale } = await params;
31 + const { industries } = getMessages(locale as Locale);
32 +
33 + return (
34 + <>
35 + <section className="relative overflow-hidden border-b border-line">
36 + <div className="grid-backdrop absolute inset-0" aria-hidden="true" />
37 + <Container className="relative py-20 sm:py-28">
38 + <SectionHeading as="h1" title={industries.title} lead={industries.lead} />
39 + </Container>
40 + </section>
41 +
42 + <section className="py-16 sm:py-20">
43 + <Container>
44 + <ul className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
45 + {industries.items.map((industry) => (
46 + <li
47 + key={industry.title}
48 + className="rounded-2xl border border-line bg-card p-7 shadow-sm transition-colors hover:border-accent"
49 + >
50 + <h2 className="font-display text-lg font-semibold text-ink">
51 + {industry.title}
52 + </h2>
53 + <p className="mt-2 text-sm leading-relaxed text-ink-faint">{industry.body}</p>
54 + </li>
55 + ))}
56 + </ul>
57 + </Container>
58 + </section>
59 + </>
60 + );
61 +}
added app/[locale]/layout.tsx +84 −0
@@ -0,0 +1,84 @@
1 +/**
2 + * Author: Simon-Pierre Boucher
3 + * Contact: contact@spboucher.ai
4 + * Project: Hilmacorp.ai — Web Platform
5 + * File: app/[locale]/layout.tsx
6 + * Description: Locale-scoped root layout — HTML shell, fonts, global metadata, header and footer
7 + */
8 +
9 +import type { Metadata } from "next";
10 +import { Inter, Fraunces } from "next/font/google";
11 +import { notFound } from "next/navigation";
12 +import Header from "@/components/Header";
13 +import Footer from "@/components/Footer";
14 +import CookieConsent from "@/components/CookieConsent";
15 +import { getMessages, isLocale, locales, type Locale } from "@/lib/i18n";
16 +import "@/app/globals.css";
17 +
18 +const inter = Inter({ subsets: ["latin"], variable: "--font-inter", display: "swap" });
19 +const fraunces = Fraunces({ subsets: ["latin"], variable: "--font-fraunces", display: "swap" });
20 +
21 +const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://www.hilmacorp.ai";
22 +
23 +export function generateStaticParams() {
24 + return locales.map((locale) => ({ locale }));
25 +}
26 +
27 +export async function generateMetadata({
28 + params,
29 +}: {
30 + params: Promise<{ locale: string }>;
31 +}): Promise<Metadata> {
32 + const { locale } = await params;
33 + if (!isLocale(locale)) return {};
34 + const messages = getMessages(locale);
35 + return {
36 + metadataBase: new URL(SITE_URL),
37 + title: {
38 + default: messages.meta.home.title,
39 + template: `%s — ${messages.meta.siteName}`,
40 + },
41 + description: messages.meta.home.description,
42 + alternates: {
43 + languages: { fr: "/fr", en: "/en" },
44 + },
45 + openGraph: {
46 + type: "website",
47 + siteName: messages.meta.siteName,
48 + locale: locale === "fr" ? "fr_CA" : "en_US",
49 + url: `${SITE_URL}/${locale}`,
50 + title: messages.meta.home.title,
51 + description: messages.meta.home.description,
52 + },
53 + twitter: {
54 + card: "summary",
55 + title: messages.meta.home.title,
56 + description: messages.meta.home.description,
57 + },
58 + robots: { index: true, follow: true },
59 + };
60 +}
61 +
62 +export default async function LocaleLayout({
63 + children,
64 + params,
65 +}: {
66 + children: React.ReactNode;
67 + params: Promise<{ locale: string }>;
68 +}) {
69 + const { locale } = await params;
70 + if (!isLocale(locale)) notFound();
71 + const typedLocale = locale as Locale;
72 + const messages = getMessages(typedLocale);
73 +
74 + return (
75 + <html lang={typedLocale} className={`${inter.variable} ${fraunces.variable}`}>
76 + <body className="flex min-h-screen flex-col font-sans">
77 + <Header locale={typedLocale} nav={messages.nav} />
78 + <main className="flex-1">{children}</main>
79 + <Footer locale={typedLocale} messages={messages} />
80 + <CookieConsent locale={typedLocale} strings={messages.cookies} />
81 + </body>
82 + </html>
83 + );
84 +}
added app/[locale]/not-found.tsx +38 −0
@@ -0,0 +1,38 @@
1 +/**
2 + * Author: Simon-Pierre Boucher
3 + * Contact: contact@spboucher.ai
4 + * Project: Hilmacorp.ai — Web Platform
5 + * File: app/[locale]/not-found.tsx
6 + * Description: Locale-aware 404 page (light theme)
7 + */
8 +
9 +import Link from "next/link";
10 +import Container from "@/components/Container";
11 +
12 +export default function NotFound() {
13 + return (
14 + <Container className="py-32 text-center">
15 + <p className="font-mono text-sm uppercase tracking-[0.3em] text-accent">404</p>
16 + <h1 className="mt-4 font-display text-3xl font-semibold text-ink">
17 + Page not found / Page introuvable
18 + </h1>
19 + <p className="mt-4 text-ink-soft">
20 + The page you are looking for does not exist. / La page que vous cherchez n&apos;existe pas.
21 + </p>
22 + <div className="mt-8 flex justify-center gap-4">
23 + <Link
24 + href="/en"
25 + className="rounded-full border border-line-strong px-5 py-2 text-sm text-ink hover:border-accent hover:text-accent"
26 + >
27 + English home
28 + </Link>
29 + <Link
30 + href="/fr"
31 + className="rounded-full border border-line-strong px-5 py-2 text-sm text-ink hover:border-accent hover:text-accent"
32 + >
33 + Accueil français
34 + </Link>
35 + </div>
36 + </Container>
37 + );
38 +}
added app/[locale]/page.tsx +203 −0
@@ -0,0 +1,203 @@
1 +/**
2 + * Author: Simon-Pierre Boucher
3 + * Contact: contact@spboucher.ai
4 + * Project: Hilmacorp.ai — Web Platform
5 + * File: app/[locale]/page.tsx
6 + * Description: Home page — hero, introduction, commitments, services overview, method strip, call to action
7 + */
8 +
9 +import type { Metadata } from "next";
10 +import Link from "next/link";
11 +import Container from "@/components/Container";
12 +import SectionHeading from "@/components/SectionHeading";
13 +import { getMessages, isLocale, type Locale } from "@/lib/i18n";
14 +
15 +export async function generateMetadata({
16 + params,
17 +}: {
18 + params: Promise<{ locale: string }>;
19 +}): Promise<Metadata> {
20 + const { locale } = await params;
21 + if (!isLocale(locale)) return {};
22 + const { meta } = getMessages(locale);
23 + return { title: { absolute: meta.home.title }, description: meta.home.description };
24 +}
25 +
26 +export default async function HomePage({
27 + params,
28 +}: {
29 + params: Promise<{ locale: string }>;
30 +}) {
31 + const { locale } = await params;
32 + const typedLocale = locale as Locale;
33 + const messages = getMessages(typedLocale);
34 + const { home, services } = messages;
35 +
36 + return (
37 + <>
38 + {/* Hero */}
39 + <section className="relative overflow-hidden border-b border-line">
40 + <div className="grid-backdrop absolute inset-0" aria-hidden="true" />
41 + <Container className="relative py-24 sm:py-36">
42 + <p className="mb-5 font-mono text-xs uppercase tracking-[0.3em] text-accent">
43 + {home.heroKicker}
44 + </p>
45 + <h1 className="max-w-4xl font-display text-4xl font-semibold leading-[1.08] tracking-tight text-ink sm:text-6xl">
46 + {home.heroTitle}
47 + </h1>
48 + <p className="mt-8 max-w-2xl text-lg leading-relaxed text-ink-soft">
49 + {home.heroLead}
50 + </p>
51 + <div className="mt-10 flex flex-wrap gap-4">
52 + <Link
53 + href={`/${typedLocale}/contact`}
54 + className="rounded-full bg-ink px-7 py-3 text-sm font-medium text-paper transition-colors hover:bg-accent-strong"
55 + >
56 + {home.heroCtaPrimary}
57 + </Link>
58 + <Link
59 + href={`/${typedLocale}/services`}
60 + className="rounded-full border border-line-strong px-7 py-3 text-sm font-medium text-ink transition-colors hover:border-accent hover:text-accent"
61 + >
62 + {home.heroCtaSecondary}
63 + </Link>
64 + </div>
65 + </Container>
66 + </section>
67 +
68 + {/* Stats band */}
69 + <section className="border-b border-line bg-paper-2">
70 + <Container>
71 + <dl className="grid grid-cols-1 divide-y divide-line sm:grid-cols-3 sm:divide-x sm:divide-y-0">
72 + {[
73 + { value: "8", label: home.statsProjects },
74 + { value: "2", label: home.statsLanguages },
75 + { value: "8", label: home.statsSteps },
76 + ].map((stat) => (
77 + <div key={stat.label} className="px-2 py-9 text-center">
78 + <dd className="font-display text-4xl font-semibold text-accent">
79 + {stat.value}
80 + </dd>
81 + <dt className="mt-1 text-sm text-ink-faint">{stat.label}</dt>
82 + </div>
83 + ))}
84 + </dl>
85 + </Container>
86 + </section>
87 +
88 + {/* Introduction */}
89 + <section className="py-20 sm:py-28">
90 + <Container>
91 + <SectionHeading title={home.introTitle} />
92 + <div className="mt-8 grid gap-8 lg:grid-cols-2">
93 + <p className="leading-relaxed text-ink-soft">{home.introBody1}</p>
94 + <p className="leading-relaxed text-ink-soft">{home.introBody2}</p>
95 + </div>
96 + <div className="mt-10 flex flex-wrap gap-6">
97 + <Link
98 + href={`/${typedLocale}/about`}
99 + className="text-sm font-medium text-accent hover:text-accent-strong"
100 + >
101 + {home.introLinkAbout} →
102 + </Link>
103 + <Link
104 + href={`/${typedLocale}/founders`}
105 + className="text-sm font-medium text-accent hover:text-accent-strong"
106 + >
107 + {home.introLinkFounders} →
108 + </Link>
109 + </div>
110 + </Container>
111 + </section>
112 +
113 + {/* Commitments */}
114 + <section className="border-t border-line bg-paper-2 py-20 sm:py-28">
115 + <Container>
116 + <SectionHeading title={home.whyTitle} lead={home.whyLead} />
117 + <ul className="mt-12 grid gap-4 sm:grid-cols-2">
118 + {home.why.map((item, index) => (
119 + <li
120 + key={item.title}
121 + className="rounded-2xl border border-line bg-card p-8"
122 + >
123 + <p className="font-mono text-sm text-accent">
124 + {String(index + 1).padStart(2, "0")}
125 + </p>
126 + <h3 className="mt-3 font-display text-xl font-semibold text-ink">
127 + {item.title}
128 + </h3>
129 + <p className="mt-3 leading-relaxed text-ink-soft">{item.body}</p>
130 + </li>
131 + ))}
132 + </ul>
133 + </Container>
134 + </section>
135 +
136 + {/* Services overview */}
137 + <section className="py-20 sm:py-28">
138 + <Container>
139 + <SectionHeading title={home.servicesTitle} lead={home.servicesLead} />
140 + <ul className="mt-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
141 + {services.items.map((service, index) => (
142 + <li key={service.id}>
143 + <Link
144 + href={`/${typedLocale}/services#${service.id}`}
145 + className="group block h-full rounded-2xl border border-line bg-card p-6 transition-colors hover:border-accent"
146 + >
147 + <p className="font-mono text-xs text-ink-faint">
148 + {String(index + 1).padStart(2, "0")}
149 + </p>
150 + <h3 className="mt-3 text-base font-semibold text-ink group-hover:text-accent">
151 + {service.title}
152 + </h3>
153 + </Link>
154 + </li>
155 + ))}
156 + </ul>
157 + <div className="mt-10">
158 + <Link
159 + href={`/${typedLocale}/services`}
160 + className="text-sm font-medium text-accent hover:text-accent-strong"
161 + >
162 + {home.servicesCtaAll} →
163 + </Link>
164 + </div>
165 + </Container>
166 + </section>
167 +
168 + {/* Method strip */}
169 + <section className="border-t border-line bg-ink py-16 sm:py-20">
170 + <Container className="flex flex-col items-start gap-6 sm:flex-row sm:items-center sm:justify-between">
171 + <div className="max-w-2xl">
172 + <h2 className="font-display text-2xl font-semibold tracking-tight text-paper sm:text-3xl">
173 + {home.processTitle}
174 + </h2>
175 + <p className="mt-3 leading-relaxed text-paper/70">{home.processBody}</p>
176 + </div>
177 + <Link
178 + href={`/${typedLocale}/about`}
179 + className="shrink-0 rounded-full border border-paper/30 px-6 py-2.5 text-sm font-medium text-paper transition-colors hover:border-paper hover:bg-paper hover:text-ink"
180 + >
181 + {home.processLink}
182 + </Link>
183 + </Container>
184 + </section>
185 +
186 + {/* Call to action */}
187 + <section className="py-20 sm:py-28">
188 + <Container className="text-center">
189 + <h2 className="font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
190 + {home.ctaTitle}
191 + </h2>
192 + <p className="mx-auto mt-5 max-w-2xl leading-relaxed text-ink-soft">{home.ctaBody}</p>
193 + <Link
194 + href={`/${typedLocale}/contact`}
195 + className="mt-9 inline-block rounded-full bg-ink px-8 py-3.5 text-sm font-medium text-paper transition-colors hover:bg-accent-strong"
196 + >
197 + {home.ctaButton}
198 + </Link>
199 + </Container>
200 + </section>
201 + </>
202 + );
203 +}
added app/[locale]/privacy/page.tsx +77 −0
@@ -0,0 +1,77 @@
1 +/**
2 + * Author: Simon-Pierre Boucher
3 + * Contact: contact@spboucher.ai
4 + * Project: Hilmacorp.ai — Web Platform
5 + * File: app/[locale]/privacy/page.tsx
6 + * Description: Privacy Policy page — data practices plus the cookie inventory and preferences access
7 + */
8 +
9 +import type { Metadata } from "next";
10 +import LegalPage from "@/components/LegalPage";
11 +import ManageCookiesButton from "@/components/ManageCookiesButton";
12 +import { getMessages, isLocale, type Locale } from "@/lib/i18n";
13 +
14 +export async function generateMetadata({
15 + params,
16 +}: {
17 + params: Promise<{ locale: string }>;
18 +}): Promise<Metadata> {
19 + const { locale } = await params;
20 + if (!isLocale(locale)) return {};
21 + const { meta } = getMessages(locale);
22 + return { title: { absolute: meta.privacy.title }, description: meta.privacy.description };
23 +}
24 +
25 +export default async function PrivacyPage({
26 + params,
27 +}: {
28 + params: Promise<{ locale: string }>;
29 +}) {
30 + const { locale } = await params;
31 + const { privacy } = getMessages(locale as Locale);
32 + const cookies = privacy.cookiesSection;
33 +
34 + return (
35 + <LegalPage
36 + title={privacy.title}
37 + lead={privacy.lead}
38 + lastUpdated={privacy.lastUpdated}
39 + sections={privacy.sections}
40 + >
41 + <section>
42 + <h2 className="font-display text-xl font-semibold text-ink">{cookies.title}</h2>
43 + <p className="mt-3 leading-relaxed text-ink-soft">{cookies.intro}</p>
44 + <div className="mt-6 overflow-x-auto rounded-2xl border border-line">
45 + <table className="w-full border-collapse bg-card text-left text-sm">
46 + <thead>
47 + <tr className="border-b border-line bg-paper-2">
48 + <th scope="col" className="px-5 py-3 font-semibold text-ink">
49 + {cookies.tableName}
50 + </th>
51 + <th scope="col" className="px-5 py-3 font-semibold text-ink">
52 + {cookies.tablePurpose}
53 + </th>
54 + <th scope="col" className="px-5 py-3 font-semibold text-ink">
55 + {cookies.tableDuration}
56 + </th>
57 + </tr>
58 + </thead>
59 + <tbody>
60 + {cookies.rows.map((row) => (
61 + <tr key={row.name} className="border-b border-line last:border-b-0">
62 + <td className="px-5 py-3 font-mono text-xs text-ink">{row.name}</td>
63 + <td className="px-5 py-3 text-ink-soft">{row.purpose}</td>
64 + <td className="px-5 py-3 text-ink-soft">{row.duration}</td>
65 + </tr>
66 + ))}
67 + </tbody>
68 + </table>
69 + </div>
70 + <ManageCookiesButton
71 + label={cookies.manage}
72 + className="mt-6 rounded-full border border-line-strong px-6 py-2.5 text-sm font-medium text-ink transition-colors hover:border-accent hover:text-accent"
73 + />
74 + </section>
75 + </LegalPage>
76 + );
77 +}
added app/[locale]/security/page.tsx +79 −0
@@ -0,0 +1,79 @@
1 +/**
2 + * Author: Simon-Pierre Boucher
3 + * Contact: contact@spboucher.ai
4 + * Project: Hilmacorp.ai — Web Platform
5 + * File: app/[locale]/security/page.tsx
6 + * Description: Security & responsible deployment page — governance controls and practices
7 + */
8 +
9 +import type { Metadata } from "next";
10 +import Container from "@/components/Container";
11 +import SectionHeading from "@/components/SectionHeading";
12 +import { getMessages, isLocale, type Locale } from "@/lib/i18n";
13 +
14 +export async function generateMetadata({
15 + params,
16 +}: {
17 + params: Promise<{ locale: string }>;
18 +}): Promise<Metadata> {
19 + const { locale } = await params;
20 + if (!isLocale(locale)) return {};
21 + const { meta } = getMessages(locale);
22 + return { title: { absolute: meta.security.title }, description: meta.security.description };
23 +}
24 +
25 +export default async function SecurityPage({
26 + params,
27 +}: {
28 + params: Promise<{ locale: string }>;
29 +}) {
30 + const { locale } = await params;
31 + const { security } = getMessages(locale as Locale);
32 +
33 + return (
34 + <>
35 + <section className="relative overflow-hidden border-b border-line">
36 + <div className="grid-backdrop absolute inset-0" aria-hidden="true" />
37 + <Container className="relative py-20 sm:py-28">
38 + <SectionHeading as="h1" title={security.title} lead={security.lead} />
39 + </Container>
40 + </section>
41 +
42 + <section className="py-16 sm:py-20">
43 + <Container>
44 + <ul className="grid gap-4 sm:grid-cols-2">
45 + {security.items.map((item) => (
46 + <li
47 + key={item.title}
48 + className="rounded-2xl border border-line bg-card p-7 shadow-sm"
49 + >
50 + <div className="flex items-start gap-4">
51 + <span
52 + aria-hidden="true"
53 + className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-accent-soft"
54 + >
55 + <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
56 + <path
57 + d="M8 1.5l5 2v4c0 3.2-2.1 5.6-5 7-2.9-1.4-5-3.8-5-7v-4l5-2z"
58 + stroke="#0b6b4a"
59 + strokeWidth="1.5"
60 + strokeLinejoin="round"
61 + />
62 + </svg>
63 + </span>
64 + <div>
65 + <h2 className="text-base font-semibold text-ink">{item.title}</h2>
66 + <p className="mt-2 text-sm leading-relaxed text-ink-faint">{item.body}</p>
67 + </div>
68 + </div>
69 + </li>
70 + ))}
71 + </ul>
72 + <p className="mx-auto mt-14 max-w-3xl border-l-2 border-accent pl-6 font-display text-xl leading-relaxed text-ink">
73 + {security.closing}
74 + </p>
75 + </Container>
76 + </section>
77 + </>
78 + );
79 +}
added app/[locale]/services/page.tsx +119 −0
@@ -0,0 +1,119 @@
1 +/**
2 + * Author: Simon-Pierre Boucher
3 + * Contact: contact@spboucher.ai
4 + * Project: Hilmacorp.ai — Web Platform
5 + * File: app/[locale]/services/page.tsx
6 + * Description: Services page — the eight practice areas as anchored sections, plus engagement model
7 + */
8 +
9 +import type { Metadata } from "next";
10 +import Link from "next/link";
11 +import Container from "@/components/Container";
12 +import SectionHeading from "@/components/SectionHeading";
13 +import { getMessages, isLocale, type Locale } from "@/lib/i18n";
14 +
15 +export async function generateMetadata({
16 + params,
17 +}: {
18 + params: Promise<{ locale: string }>;
19 +}): Promise<Metadata> {
20 + const { locale } = await params;
21 + if (!isLocale(locale)) return {};
22 + const { meta } = getMessages(locale);
23 + return { title: { absolute: meta.services.title }, description: meta.services.description };
24 +}
25 +
26 +export default async function ServicesPage({
27 + params,
28 +}: {
29 + params: Promise<{ locale: string }>;
30 +}) {
31 + const { locale } = await params;
32 + const typedLocale = locale as Locale;
33 + const { services, nav } = getMessages(typedLocale);
34 +
35 + return (
36 + <>
37 + <section className="relative overflow-hidden border-b border-line">
38 + <div className="grid-backdrop absolute inset-0" aria-hidden="true" />
39 + <Container className="relative py-20 sm:py-28">
40 + <SectionHeading as="h1" title={services.title} lead={services.lead} />
41 + <nav aria-label="Services index" className="mt-10 flex flex-wrap gap-2">
42 + {services.items.map((service) => (
43 + <a
44 + key={service.id}
45 + href={`#${service.id}`}
46 + className="rounded-full border border-line-strong bg-card px-4 py-1.5 text-sm text-ink-soft transition-colors hover:border-accent hover:text-accent"
47 + >
48 + {service.title}
49 + </a>
50 + ))}
51 + </nav>
52 + </Container>
53 + </section>
54 +
55 + <section className="py-16 sm:py-20">
56 + <Container>
57 + <div className="grid gap-6 lg:grid-cols-2">
58 + {services.items.map((service, index) => (
59 + <article
60 + key={service.id}
61 + id={service.id}
62 + className="scroll-mt-24 rounded-3xl border border-line bg-card p-8 shadow-sm sm:p-10"
63 + >
64 + <p className="font-mono text-sm text-accent">
65 + {String(index + 1).padStart(2, "0")}
66 + </p>
67 + <h2 className="mt-3 font-display text-2xl font-semibold tracking-tight text-ink">
68 + {service.title}
69 + </h2>
70 + <p className="mt-4 leading-relaxed text-ink-soft">{service.description}</p>
71 + <ul className="mt-6 grid gap-2.5">
72 + {service.points.map((point) => (
73 + <li key={point} className="flex items-start gap-2.5 text-sm text-ink">
74 + <svg
75 + width="16"
76 + height="16"
77 + viewBox="0 0 16 16"
78 + fill="none"
79 + aria-hidden="true"
80 + className="mt-0.5 shrink-0"
81 + >
82 + <path
83 + d="M3 8.5l3.2 3L13 4.5"
84 + stroke="#0b6b4a"
85 + strokeWidth="1.8"
86 + strokeLinecap="round"
87 + strokeLinejoin="round"
88 + />
89 + </svg>
90 + {point}
91 + </li>
92 + ))}
93 + </ul>
94 + </article>
95 + ))}
96 + </div>
97 + </Container>
98 + </section>
99 +
100 + {/* Engagement model */}
101 + <section className="border-t border-line bg-ink py-16 sm:py-20">
102 + <Container className="text-center">
103 + <h2 className="font-display text-2xl font-semibold tracking-tight text-paper sm:text-3xl">
104 + {services.engagementTitle}
105 + </h2>
106 + <p className="mx-auto mt-4 max-w-3xl leading-relaxed text-paper/70">
107 + {services.engagementBody}
108 + </p>
109 + <Link
110 + href={`/${typedLocale}/contact`}
111 + className="mt-8 inline-block rounded-full bg-paper px-8 py-3.5 text-sm font-medium text-ink transition-colors hover:bg-accent-soft"
112 + >
113 + {nav.cta}
114 + </Link>
115 + </Container>
116 + </section>
117 + </>
118 + );
119 +}
added app/[locale]/terms/page.tsx +40 −0
@@ -0,0 +1,40 @@
1 +/**
2 + * Author: Simon-Pierre Boucher
3 + * Contact: contact@spboucher.ai
4 + * Project: Hilmacorp.ai — Web Platform
5 + * File: app/[locale]/terms/page.tsx
6 + * Description: Terms of Service page (bilingual legal document)
7 + */
8 +
9 +import type { Metadata } from "next";
10 +import LegalPage from "@/components/LegalPage";
11 +import { getMessages, isLocale, type Locale } from "@/lib/i18n";
12 +
13 +export async function generateMetadata({
14 + params,
15 +}: {
16 + params: Promise<{ locale: string }>;
17 +}): Promise<Metadata> {
18 + const { locale } = await params;
19 + if (!isLocale(locale)) return {};
20 + const { meta } = getMessages(locale);
21 + return { title: { absolute: meta.terms.title }, description: meta.terms.description };
22 +}
23 +
24 +export default async function TermsPage({
25 + params,
26 +}: {
27 + params: Promise<{ locale: string }>;
28 +}) {
29 + const { locale } = await params;
30 + const { terms } = getMessages(locale as Locale);
31 +
32 + return (
33 + <LegalPage
34 + title={terms.title}
35 + lead={terms.lead}
36 + lastUpdated={terms.lastUpdated}
37 + sections={terms.sections}
38 + />
39 + );
40 +}
added app/api/contact/route.ts +77 −0
@@ -0,0 +1,77 @@
1 +/**
2 + * Author: Simon-Pierre Boucher
3 + * Contact: contact@spboucher.ai
4 + * Project: Hilmacorp.ai — Web Platform
5 + * File: app/api/contact/route.ts
6 + * Description: Contact form API route — validates input, then delivers to contact@spboucher.ai
7 + * via Resend when RESEND_API_KEY is set, otherwise persists to data/contact-messages.jsonl
8 + */
9 +
10 +import { NextResponse } from "next/server";
11 +import { appendFile, mkdir } from "node:fs/promises";
12 +import path from "node:path";
13 +import { validateContact } from "@/lib/validate";
14 +
15 +export const runtime = "nodejs";
16 +
17 +const CONTACT_TO = process.env.CONTACT_TO ?? "contact@spboucher.ai";
18 +
19 +async function deliverByEmail(data: {
20 + name: string;
21 + organization: string;
22 + email: string;
23 + message: string;
24 +}): Promise<boolean> {
25 + const apiKey = process.env.RESEND_API_KEY;
26 + if (!apiKey) return false;
27 + const response = await fetch("https://api.resend.com/emails", {
28 + method: "POST",
29 + headers: {
30 + Authorization: `Bearer ${apiKey}`,
31 + "Content-Type": "application/json",
32 + },
33 + body: JSON.stringify({
34 + from: process.env.CONTACT_FROM ?? "Hilmacorp.ai <onboarding@resend.dev>",
35 + to: [CONTACT_TO],
36 + reply_to: data.email,
37 + subject: `[hilmacorp.ai] Contact — ${data.name}`,
38 + text: `Name: ${data.name}\nOrganization: ${data.organization || "—"}\nEmail: ${data.email}\n\n${data.message}`,
39 + }),
40 + });
41 + return response.ok;
42 +}
43 +
44 +async function persistLocally(data: object): Promise<void> {
45 + const dir = path.join(process.cwd(), "data");
46 + await mkdir(dir, { recursive: true });
47 + const record = { receivedAt: new Date().toISOString(), to: CONTACT_TO, ...data };
48 + await appendFile(path.join(dir, "contact-messages.jsonl"), JSON.stringify(record) + "\n", "utf8");
49 +}
50 +
51 +export async function POST(request: Request) {
52 + let body: unknown;
53 + try {
54 + body = await request.json();
55 + } catch {
56 + return NextResponse.json({ ok: false, errors: { body: "invalid_json" } }, { status: 400 });
57 + }
58 +
59 + const result = validateContact(body);
60 + if (!result.ok || !result.data) {
61 + return NextResponse.json({ ok: false, errors: result.errors }, { status: 422 });
62 + }
63 +
64 + try {
65 + const emailed = await deliverByEmail(result.data);
66 + if (!emailed) await persistLocally(result.data);
67 + return NextResponse.json({ ok: true });
68 + } catch (error) {
69 + console.error("[contact] delivery failed:", error);
70 + try {
71 + await persistLocally(result.data);
72 + return NextResponse.json({ ok: true });
73 + } catch {
74 + return NextResponse.json({ ok: false, errors: { server: "delivery_failed" } }, { status: 500 });
75 + }
76 + }
77 +}
added app/globals.css +59 −0
@@ -0,0 +1,59 @@
1 +/**
2 + * Author: Simon-Pierre Boucher
3 + * Contact: contact@spboucher.ai
4 + * Project: Hilmacorp.ai — Web Platform
5 + * File: app/globals.css
6 + * Description: Global styles — Tailwind v4 entry point, light editorial design tokens, base rules
7 + */
8 +
9 +@import "tailwindcss";
10 +
11 +@theme {
12 + /* Light editorial palette: warm paper base, deep green-black ink, deep emerald accent */
13 + --color-paper: #faf9f6;
14 + --color-paper-2: #f2f0ea;
15 + --color-card: #ffffff;
16 + --color-line: #e7e3da;
17 + --color-line-strong: #d3cec1;
18 + --color-ink: #191f1c;
19 + --color-ink-soft: #414c46;
20 + --color-ink-faint: #6d7871;
21 + --color-accent: #0b6b4a;
22 + --color-accent-strong: #095c40;
23 + --color-accent-soft: #e6f2ec;
24 +
25 + --font-sans: var(--font-inter), ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
26 + --font-display: var(--font-fraunces), ui-serif, Georgia, serif;
27 + --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
28 +}
29 +
30 +html {
31 + scroll-behavior: smooth;
32 +}
33 +
34 +body {
35 + background-color: var(--color-paper);
36 + color: var(--color-ink);
37 + -webkit-font-smoothing: antialiased;
38 + text-rendering: optimizeLegibility;
39 +}
40 +
41 +::selection {
42 + background-color: var(--color-accent);
43 + color: var(--color-paper);
44 +}
45 +
46 +/* Subtle engineering-grid backdrop used by hero sections (light variant) */
47 +.grid-backdrop {
48 + background-image:
49 + linear-gradient(to right, rgb(25 31 28 / 0.045) 1px, transparent 1px),
50 + linear-gradient(to bottom, rgb(25 31 28 / 0.045) 1px, transparent 1px);
51 + background-size: 44px 44px;
52 + mask-image: radial-gradient(ellipse 90% 75% at 50% 0%, black 35%, transparent 100%);
53 +}
54 +
55 +/* Focus visibility for keyboard navigation (accessibility) */
56 +:focus-visible {
57 + outline: 2px solid var(--color-accent);
58 + outline-offset: 2px;
59 +}
added app/icon.svg +5 −0
Large diff (5 lines) is not loaded by default to keep the page fast.Load diff
added app/robots.ts +18 −0
Large diff (18 lines) is not loaded by default to keep the page fast.Load diff
added app/sitemap.ts +28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added components/ContactForm.tsx +154 −0
Large diff (154 lines) is not loaded by default to keep the page fast.Load diff
added components/Container.tsx +23 −0
Large diff (23 lines) is not loaded by default to keep the page fast.Load diff
added components/CookieConsent.tsx +97 −0
Large diff (97 lines) is not loaded by default to keep the page fast.Load diff
added components/Footer.tsx +106 −0
Large diff (106 lines) is not loaded by default to keep the page fast.Load diff
added components/Header.tsx +138 −0
Large diff (138 lines) is not loaded by default to keep the page fast.Load diff
added components/LegalPage.tsx +64 −0
Large diff (64 lines) is not loaded by default to keep the page fast.Load diff
added components/LocaleSwitcher.tsx +37 −0
Large diff (37 lines) is not loaded by default to keep the page fast.Load diff
added components/Logo.tsx +28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added components/ManageCookiesButton.tsx +29 −0
Large diff (29 lines) is not loaded by default to keep the page fast.Load diff
added components/SectionHeading.tsx +41 −0
Large diff (41 lines) is not loaded by default to keep the page fast.Load diff
added lib/i18n.ts +30 −0
Large diff (30 lines) is not loaded by default to keep the page fast.Load diff
added lib/validate.ts +47 −0
Large diff (47 lines) is not loaded by default to keep the page fast.Load diff
added messages/en.json +394 −0
Large diff (394 lines) is not loaded by default to keep the page fast.Load diff
added messages/fr.json +394 −0
Large diff (394 lines) is not loaded by default to keep the page fast.Load diff
added middleware.ts +41 −0
Large diff (41 lines) is not loaded by default to keep the page fast.Load diff
added next.config.mjs +15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added package-lock.json +1735 −0
Large diff (1,735 lines) is not loaded by default to keep the page fast.Load diff
added package.json +27 −0
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
added postcss.config.mjs +15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added public/downloads/ZyquoAgent.dmg +0 −0

Binary file not shown.

added public/downloads/ZyquoCloud.dmg +0 −0

Binary file not shown.

added public/downloads/ZyquoLocal.dmg +0 −0

Binary file not shown.

added public/downloads/ZyquoRouter.dmg +0 −0

Binary file not shown.

added tests/contact-validation.test.mjs +94 −0
Large diff (94 lines) is not loaded by default to keep the page fast.Load diff
added tsconfig.json +26 −0
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
Menu

Theme