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