update seo
This commit is contained in:
parent
e2d8945b83
commit
7439412d34
130
TODO-SEO.md
130
TODO-SEO.md
@ -1,102 +1,62 @@
|
||||
# TODO SEO — myinfomate-landing
|
||||
|
||||
Liste des améliorations SEO à réaliser, classées par impact décroissant.
|
||||
---
|
||||
|
||||
## Ce qui est fait ✅
|
||||
|
||||
| # | Item |
|
||||
|---|------|
|
||||
| 1 | Routing multilingue + hreflang (fr/en/nl/de, sitemap, `x-default`) |
|
||||
| 2 | Schema.org FAQPage + SoftwareApplication sur les pages segment |
|
||||
| 3 | Schema.org Organization + WebSite + SoftwareApplication sur la home |
|
||||
| 4 | `llms.txt` — résumé produit pour les crawlers LLM (ChatGPT, Perplexity, Claude...) |
|
||||
| 5 | OG image 1200×630 générée dynamiquement via `opengraph-image.tsx`, locale-aware |
|
||||
| 6 | Titles + descriptions réécrits avec mots-clés ciblés (< 60 chars, ~155 chars, 4 langues) |
|
||||
| 7 | Alt texts descriptifs sur toutes les images hero |
|
||||
| 8 | H1 unique par page, `priority` sur l'image hero, font en `display=swap` |
|
||||
| 9 | robots.txt + sitemap.xml |
|
||||
|
||||
---
|
||||
|
||||
## 1. Multilingue avec hreflang (impact: ÉLEVÉ) ✅
|
||||
## À faire — code (peut être fait avec Claude)
|
||||
|
||||
Le site a 4 langues (FR/EN/NL/DE) mais tout est servi sur la même URL avec `lang="fr"` figé. Google n'indexe qu'une seule version → on rate les recherches NL et DE.
|
||||
### Moyen terme
|
||||
|
||||
- [x] Mettre en place un routing par langue : `/fr`, `/en`, `/nl`, `/de` (ou middleware Next)
|
||||
- [x] Ajouter `metadata.alternates.languages` dans `layout.tsx` et `[segment]/page.tsx`
|
||||
- [x] Rendre `<html lang>` dynamique selon la route
|
||||
- [x] Adapter le `sitemap.ts` pour générer les URLs de chaque langue
|
||||
- [x] Vérifier que les balises `<link rel="alternate" hreflang="x-default">` pointent vers la version par défaut
|
||||
- [x] **Refactor HomeClient en Server Component** — extrait en 4 composants client isolés (`NavBar`, `ModulesSection`, `ContactForm`, `Reveal`). Le reste de la page est rendu côté serveur. CTA buttons convertis en `<a href="#contact">` + CSS `scroll-behavior: smooth`.
|
||||
|
||||
- [x] **OG image par segment** — `src/app/[lang]/[segment]/opengraph-image.tsx` avec badge segment locale-aware (ex: "Musées & Patrimoine" / "Museums & Heritage")
|
||||
|
||||
---
|
||||
|
||||
## 2. Page d'accueil en Server Component (impact: MOYEN)
|
||||
## À faire — toi (hors code)
|
||||
|
||||
`src/app/page.tsx` est marqué `'use client'` → perte de perfs (FCP/LCP, signaux de ranking).
|
||||
### Actions rapides (< 1h chacune)
|
||||
|
||||
- [ ] Convertir `page.tsx` en Server Component
|
||||
- [ ] Extraire les parties interactives (menu langue, `Reveal`, etc.) dans des sous-composants client
|
||||
- [ ] Vérifier l'amélioration via Lighthouse avant/après
|
||||
- [ ] **Google Search Console** — soumettre `https://myinfomate.be/sitemap.xml`, vérifier qu'il n'y a pas d'erreurs d'indexation
|
||||
- [ ] **Bing Webmaster Tools** — même chose (souvent oublié, représente ~10% du trafic desktop)
|
||||
- [ ] **Lighthouse audit** — ouvrir Chrome DevTools sur myinfomate.be, onglet Lighthouse, mode Mobile → viser > 90. Les points rouges qui sortent sont à corriger en priorité.
|
||||
- [ ] **LinkedIn entreprise Unov** — si pas encore actif, créer la page et pointer vers myinfomate.be. Google valorise les signaux sociaux B2B.
|
||||
|
||||
---
|
||||
### Contenu long-tail (impact élevé, effort éditorial)
|
||||
|
||||
## 3. JSON-LD sur la home (impact: MOYEN)
|
||||
C'est le levier le plus fort à long terme. Chaque page = une requête supplémentaire capturée.
|
||||
|
||||
Schema.org est présent sur les segments mais pas sur la home.
|
||||
- [x] **Cas clients** — 3 cas clients créés en 4 langues : Musée de la Fraise (premier client, kiosk + mobile), Fort de Saint-Héribert (mobile offline + iBeacon), Visit Namur (bornes Office du Tourisme). Page listing `/[lang]/cas-clients` + détail `/[lang]/cas-clients/[slug]`, schema.org Article, lien depuis homepage (section dédiée + footer), sitemap mis à jour.
|
||||
- [ ] **Blog / ressources** — articles fondateurs à écrire :
|
||||
- "Audioguide vs application visiteur : que choisir en 2026 ?"
|
||||
- "RGPD et tracking visiteur dans un musée"
|
||||
- "ROI d'une application visiteur pour un site culturel"
|
||||
- "Comment digitaliser un parcours de visite"
|
||||
- "Kiosk tactile vs BYOD : avantages et limites"
|
||||
- [ ] **Pages comparatives** — très efficaces en B2B SaaS (les gens cherchent "alternative à Smartify") :
|
||||
- `/fr/comparatif/myinfomate-vs-smartify`
|
||||
- `/fr/comparatif/myinfomate-vs-stqry`
|
||||
- `/fr/comparatif/myinfomate-vs-orpheo`
|
||||
|
||||
- [ ] Ajouter un schéma `Organization` (Unov) avec `logo`, `url`, `sameAs` (LinkedIn, etc.)
|
||||
- [ ] Ajouter un schéma `WebSite`
|
||||
- [ ] Ajouter un schéma `SoftwareApplication` pour MyInfoMate (avec `aggregateRating` si avis disponibles)
|
||||
### Backlinks off-site (impact élevé, effort relationnel)
|
||||
|
||||
---
|
||||
|
||||
## 4. OG image dédiée (impact: MOYEN)
|
||||
|
||||
Actuellement `/myinfomate-logo.png` est utilisé comme image Open Graph → mauvais rendu sur LinkedIn / WhatsApp / Slack.
|
||||
|
||||
- [ ] Créer une vraie image 1200×630 (titre + visuel produit)
|
||||
- [ ] Idéalement, utiliser `opengraph-image.tsx` (Next 16) pour génération dynamique
|
||||
- [ ] Faire une variante par segment (musées, offices tourisme, etc.)
|
||||
|
||||
---
|
||||
|
||||
## 5. Title et meta description plus stratégiques (impact: MOYEN)
|
||||
|
||||
Le title actuel ("MyInfoMate | La technologie au service de l'expérience visiteur") fait ~63 chars et manque de mots-clés forts.
|
||||
|
||||
- [ ] Réécrire le title de la home avec mots-clés ciblés (ex: "MyInfoMate — Guide visiteur digital pour musées, sites & événements")
|
||||
- [ ] Réviser titles + descriptions de chaque segment pour viser des requêtes précises (ex: "application musée", "kiosk tactile musée", "guide visiteur tablette")
|
||||
- [ ] Garder titles < 60 chars et descriptions ~155 chars
|
||||
|
||||
---
|
||||
|
||||
## 6. Contenu long-tail (impact: ÉLEVÉ à long terme)
|
||||
|
||||
Aujourd'hui : 6 segments + 2 légales = peu de surface SEO.
|
||||
|
||||
- [ ] Créer une section `/cas-clients/[slug]` (un par client référent)
|
||||
- [ ] Créer une section `/blog/[slug]` ou `/ressources/[slug]` avec 3-5 articles fondateurs :
|
||||
- [ ] "Audioguide vs application visiteur : que choisir en 2026 ?"
|
||||
- [ ] "RGPD et tracking visiteur dans un musée"
|
||||
- [ ] "ROI d'une application visiteur pour un site culturel"
|
||||
- [ ] "Comment digitaliser un parcours de visite"
|
||||
- [ ] "Kiosk tactile vs BYOD : avantages et limites"
|
||||
- [ ] Créer des pages comparatives (très efficace B2B) :
|
||||
- [ ] `/comparatif/myinfomate-vs-smartify`
|
||||
- [ ] `/comparatif/myinfomate-vs-stqry`
|
||||
- [ ] `/comparatif/myinfomate-vs-orpheo`
|
||||
|
||||
---
|
||||
|
||||
## 7. Détails techniques (impact: FAIBLE à MOYEN)
|
||||
|
||||
- [ ] Vérifier que toutes les `<Image>` ont un `alt` descriptif (pas juste "logo")
|
||||
- [ ] S'assurer qu'il y a un H1 unique et explicite par page
|
||||
- [ ] Ajouter `priority` sur l'image hero (amélioration LCP)
|
||||
- [ ] Passer la font Material Symbols de `display=block` à `display=swap` (évite le blocage de rendu)
|
||||
- [ ] Vérifier le score PageSpeed/Lighthouse (cible: > 90 sur mobile)
|
||||
- [ ] Soumettre le sitemap dans Google Search Console et Bing Webmaster Tools
|
||||
- [ ] Mettre en place un suivi des positions (Search Console suffit pour démarrer)
|
||||
|
||||
---
|
||||
|
||||
## 8. Backlinks / off-site (impact: ÉLEVÉ, hors code)
|
||||
|
||||
Le SEO on-page sera bientôt solide ; le levier suivant est le netlinking.
|
||||
|
||||
- [ ] Référencement dans les annuaires culture/tech belges :
|
||||
- [ ] Digital Wallonia
|
||||
- [ ] AWEX
|
||||
- [ ] hub.brussels
|
||||
- [ ] Citations dans la presse spécialisée musées :
|
||||
- [ ] CLIC France
|
||||
- [ ] ICOM
|
||||
- [ ] Museumnext
|
||||
- [ ] Demander un lien retour depuis les sites des clients référents
|
||||
- [ ] Profil LinkedIn d'entreprise actif (publications régulières → trafic indirect)
|
||||
- [ ] **Digital Wallonia** — annuaire tech belge, référencement gratuit
|
||||
- [ ] **AWEX** — agence wallonne à l'exportation, pertinent si tu vises FR/NL/DE
|
||||
- [ ] **hub.brussels** — si tu vises Bruxelles
|
||||
- [ ] **CLIC France / Museumnext / ICOM** — presse spécialisée musées, demander une mention ou tribune
|
||||
- [ ] **Clients référents** — demander un lien retour sur leur site ("Propulsé par MyInfoMate" ou mention dans leur page outils)
|
||||
|
||||
85
public/llms.txt
Normal file
85
public/llms.txt
Normal file
@ -0,0 +1,85 @@
|
||||
# MyInfoMate
|
||||
|
||||
> MyInfoMate is a SaaS platform that digitalizes the visitor experience for cultural venues, tourism offices, parks, hotels, and events. It enables venue managers to create, manage and distribute interactive content on mobile apps, tablet kiosks, and web — with no coding required.
|
||||
|
||||
Developed by **Unov**, based in Namur, Wallonia (Belgium).
|
||||
Contact: contact@unov.be
|
||||
Website: https://myinfomate.be
|
||||
|
||||
## What is MyInfoMate?
|
||||
|
||||
MyInfoMate is a content management and distribution platform (CMS) for any venue that welcomes visitors. It provides:
|
||||
- A back-office web interface to create and manage interactive visitor journeys
|
||||
- A white-label native mobile app (iOS & Android) for visitors on their own devices (BYOD)
|
||||
- A tablet kiosk app for fixed installations
|
||||
- A web app accessible from any browser, no install needed
|
||||
|
||||
All deployments are managed from a single CMS. Content can be updated in real time from a browser.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Interactive Maps**: Real-time geolocation, clickable points of interest, route navigation
|
||||
- **Games & Quizzes**: Treasure hunts, knowledge quizzes, gamified trails, escape games with narrative and riddles
|
||||
- **Audio & Video**: Immersive audio guides, HD video, multimedia content
|
||||
- **Articles & PDF**: Descriptive sheets, brochures, digitized catalogs
|
||||
- **Agenda & Events**: Dynamic event calendar and scheduling
|
||||
- **Offline Access**: Automatic content download for areas without Wi-Fi or 4G
|
||||
- **Web & Social Content**: Embedded forms, social feeds, external links
|
||||
- **Statistics & Data**: Visitor behavior analytics, engagement tracking, content popularity
|
||||
- **Escape Game & Trails**: The only CMS on the market with a full escape game module built in
|
||||
- **AI Visitor Assistant**: Generative AI guide available 24/7, multilingual, scoped strictly to the venue's content
|
||||
- **Push Notifications**: Real-time alerts to visitors
|
||||
- **Automatic Translation**: Content auto-translated across languages
|
||||
- **White Label**: Full interface customization with venue's brand, logo, colors, and typography
|
||||
|
||||
## Deployment Modes
|
||||
|
||||
1. **Mobile App (BYOD)** — visitors use their own smartphone; white-label app published on App Store & Google Play
|
||||
2. **Tablet Kiosk** — robust pre-installed app on venue-managed tablet fleets
|
||||
3. **Web App** — accessible from any modern browser, no install required
|
||||
4. **VR & AR** (coming soon) — immersive experiences on Meta Quest and connected glasses
|
||||
|
||||
## AI Assistant
|
||||
|
||||
The MyInfoMate AI assistant is a generative AI guide integrated into the visitor app:
|
||||
- Answers visitor questions in their own language (French, English, Dutch, German, and more)
|
||||
- Knows the venue's content: points of interest, exhibitions, services
|
||||
- Maintains conversation context throughout the visit
|
||||
- Strictly scoped to the venue — no off-topic responses
|
||||
- Available 24/7
|
||||
|
||||
## Target Venues
|
||||
|
||||
- **Museums & Heritage**: digitize collections, create multimedia trails, multilingual smart guide
|
||||
- **Tourism Offices**: city/region maps, event agendas, 24/7 AI assistant
|
||||
- **Parks & Natural Sites**: trail markers, biodiversity content, offline quizzes and treasure hunts
|
||||
- **Hotels & Leisure**: interactive estate map, services, virtual concierge
|
||||
- **Events & Exhibitions**: festivals, trade shows, fairs — interactive maps, real-time agenda
|
||||
- **Education & Culture**: campuses, science centers, libraries — guided content per space
|
||||
|
||||
## Customer References
|
||||
|
||||
Real venues using MyInfoMate (Belgium):
|
||||
|
||||
- **Musée de la Fraise de Wépion** (2021) — first MyInfoMate customer. Started with a tablet kiosk solution (the original use case that drove the creation of MyInfoMate), then extended to a visitor mobile app covering the museum and the fruit garden trail. Demonstrates the no-code CMS autonomy for a small museum team. Case study: https://myinfomate.be/fr/cas-clients/musee-de-la-fraise
|
||||
|
||||
- **Fort de Saint-Héribert** (2023) — first mobile deployment of MyInfoMate, on a historic underground fort. 100% offline mode (no Wi-Fi possible in the galleries), iBeacon beacons for indoor location tracking, QR codes, audio guide, iOS + Android. Case study: https://myinfomate.be/fr/cas-clients/fort-de-saint-heribert
|
||||
|
||||
- **Visit Namur** (Tourist Office of Namur, 2024) — interactive tablet kiosks deployed at the tourism office. Interactive city map with geolocated points of interest, real-time events agenda, itinerary planning. Case study: https://myinfomate.be/fr/cas-clients/visit-namur
|
||||
|
||||
All case studies index: https://myinfomate.be/fr/cas-clients
|
||||
|
||||
## Pricing
|
||||
|
||||
Subscription-based (monthly, excl. VAT). Starting from **€39/month**.
|
||||
- No commitment for basic plans
|
||||
- Setup fee + 12-month commitment for native mobile app plans
|
||||
- Enterprise: custom quote for multi-site organizations or specific needs (custom development, enhanced SLA)
|
||||
- Multi-year contracts (2–3 years) available with preferential rates
|
||||
|
||||
## Company
|
||||
|
||||
**Unov** — software company based in Namur, Belgium.
|
||||
MyInfoMate is already deployed in Namur and across Wallonia.
|
||||
Email: contact@unov.be
|
||||
Website: https://myinfomate.be
|
||||
@ -1,313 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { resolveImage } from '@/data/stitch-images';
|
||||
import translations, { Language } from '@/data/translations';
|
||||
|
||||
const MODULE_META = [
|
||||
{ id: 'map', icon: 'map', theme: 'rose' },
|
||||
{ id: 'quiz', icon: 'sports_esports', theme: 'rose' },
|
||||
{ id: 'video', icon: 'videocam', theme: 'rose' },
|
||||
{ id: 'article', icon: 'article', theme: 'rose' },
|
||||
{ id: 'agenda', icon: 'calendar_month', theme: 'rose' },
|
||||
{ id: 'offline', icon: 'download_for_offline', theme: 'rose' },
|
||||
{ id: 'web', icon: 'public', theme: 'rose' },
|
||||
{ id: 'stats', icon: 'monitoring', theme: 'rose' },
|
||||
{ id: 'escapegame', icon: 'explore', theme: 'rose' },
|
||||
];
|
||||
|
||||
const LANGUAGES: { code: Language; label: string; name: string }[] = [
|
||||
{ code: 'fr', label: 'FR', name: 'Français' },
|
||||
{ code: 'en', label: 'EN', name: 'English' },
|
||||
{ code: 'nl', label: 'NL', name: 'Nederlands' },
|
||||
{ code: 'de', label: 'DE', name: 'Deutsch' },
|
||||
];
|
||||
|
||||
function Reveal({ children, delay = 0, className = '' }: { children: React.ReactNode; delay?: number; className?: string }) {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => { if (entry.isIntersecting) setVisible(true); },
|
||||
{ threshold: 0.12 }
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={className}
|
||||
style={{
|
||||
opacity: visible ? 1 : 0,
|
||||
transform: visible ? 'translateY(0)' : 'translateY(22px)',
|
||||
transition: `opacity 0.55s ease-out ${delay}ms, transform 0.55s ease-out ${delay}ms`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { getAllCasClients } from '@/data/cas-clients';
|
||||
import NavBar from '@/components/NavBar';
|
||||
import Reveal from '@/components/Reveal';
|
||||
import ModulesSection from '@/components/ModulesSection';
|
||||
import ContactForm from '@/components/ContactForm';
|
||||
|
||||
export default function Home({ lang }: { lang: Language }) {
|
||||
const router = useRouter();
|
||||
const setLang = (next: Language) => {
|
||||
if (next !== lang) router.push(`/${next}`);
|
||||
};
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
message: ''
|
||||
});
|
||||
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
const [activeModule, setActiveModule] = useState(0);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isLangOpen, setIsLangOpen] = useState(false);
|
||||
const langDropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [navVisible, setNavVisible] = useState(true);
|
||||
const lastScrollY = useRef(0);
|
||||
|
||||
const t = translations[lang];
|
||||
|
||||
const modules = MODULE_META.map((meta, i) => ({
|
||||
...meta,
|
||||
title: t.modules.items[i].title,
|
||||
description: t.modules.items[i].description,
|
||||
value: t.modules.items[i].value,
|
||||
}));
|
||||
|
||||
// Hide navbar on scroll down, show on scroll up
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const currentY = window.scrollY;
|
||||
setNavVisible(currentY < lastScrollY.current || currentY < 10);
|
||||
lastScrollY.current = currentY;
|
||||
};
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
// Lock body scroll when mobile menu is open
|
||||
useEffect(() => {
|
||||
if (isMenuOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
return () => { document.body.style.overflow = 'auto'; };
|
||||
}, [isMenuOpen]);
|
||||
|
||||
// Close lang dropdown on outside click
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (langDropdownRef.current && !langDropdownRef.current.contains(e.target as Node)) {
|
||||
setIsLangOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus('loading');
|
||||
|
||||
try {
|
||||
const response = await fetch("https://formspree.io/f/xbdaajlo", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setStatus('success');
|
||||
setFormData({ firstName: '', lastName: '', email: '', message: '' });
|
||||
} else {
|
||||
setStatus('error');
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const scrollToContact = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
document.getElementById('contact')?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
const casClients = getAllCasClients();
|
||||
|
||||
return (
|
||||
<main className="bg-slate-50 text-slate-900 selection:bg-primary/30 font-display">
|
||||
{/* Top Navigation */}
|
||||
<header className={`fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200 transition-transform duration-300 ${navVisible ? 'translate-y-0' : '-translate-y-full'}`}>
|
||||
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
|
||||
<a className="flex items-center gap-3 shrink-0" href="#">
|
||||
<Image
|
||||
src={resolveImage("{{DATA:IMAGE:IMAGE_14}}")}
|
||||
alt="MyInfoMate Logo"
|
||||
height={40}
|
||||
width={42}
|
||||
className="h-10 w-auto object-contain"
|
||||
/>
|
||||
<span className="text-xl font-extrabold tracking-tight text-slate-900 hidden lg:block">MyInfoMate</span>
|
||||
</a>
|
||||
<nav className="hidden lg:flex items-center gap-8">
|
||||
<a className="text-sm font-semibold text-slate-600 hover:text-primary transition-colors w-[80px] text-center" href="#deployment-modes">{t.nav.solution}</a>
|
||||
<a className="text-sm font-semibold text-slate-600 hover:text-primary transition-colors w-[128px] text-center" href="#features">{t.nav.features}</a>
|
||||
<a className="text-sm font-semibold text-slate-600 hover:text-primary transition-colors flex items-center gap-1" href="#ai-assistant">
|
||||
<span className="material-symbols-outlined text-primary" style={{fontSize:'15px'}}>auto_awesome</span>
|
||||
{t.nav.ai}
|
||||
</a>
|
||||
<a className="text-sm font-semibold text-slate-600 hover:text-primary transition-colors w-[88px] text-center" href="#audience">{t.nav.audience}</a>
|
||||
<a className="text-sm font-semibold text-slate-600 hover:text-primary transition-colors w-[64px] text-center" href="#pricing">{t.pricing.sectionLabel}</a>
|
||||
</nav>
|
||||
<div className="flex items-center gap-4">
|
||||
<a
|
||||
href="https://manager.myinfomate.be"
|
||||
className="hidden lg:block text-sm font-bold text-slate-900 hover:text-primary px-4 py-2 transition-colors"
|
||||
>
|
||||
{t.nav.login}
|
||||
</a>
|
||||
<button
|
||||
onClick={scrollToContact}
|
||||
className="bg-primary hover:brightness-110 text-slate-900 font-extrabold text-sm px-6 py-3 rounded-full shadow-lg shadow-primary/20 transition-all active:scale-95 hidden sm:block"
|
||||
>
|
||||
{t.nav.demo}
|
||||
</button>
|
||||
{/* Language selector - desktop */}
|
||||
<div ref={langDropdownRef} className="relative hidden lg:block">
|
||||
<button
|
||||
onClick={() => setIsLangOpen(!isLangOpen)}
|
||||
className="flex items-center gap-1.5 px-3 py-2 rounded-xl text-sm font-bold text-slate-500 hover:text-slate-900 hover:bg-slate-100 transition-all"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px] text-primary">language</span>
|
||||
<span>{lang.toUpperCase()}</span>
|
||||
<span className={`material-symbols-outlined text-[16px] transition-transform duration-200 ${isLangOpen ? 'rotate-180' : ''}`}>keyboard_arrow_down</span>
|
||||
</button>
|
||||
{isLangOpen && (
|
||||
<div className="absolute top-full right-0 mt-2 bg-white border border-slate-100 rounded-2xl shadow-xl overflow-hidden py-1.5 z-50">
|
||||
{LANGUAGES.map((l) => (
|
||||
<button
|
||||
key={l.code}
|
||||
onClick={() => { setLang(l.code); setIsLangOpen(false); }}
|
||||
className={`w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors ${lang === l.code ? 'text-primary bg-primary/5' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
|
||||
>
|
||||
<span className="text-sm font-black w-7 shrink-0">{l.label}</span>
|
||||
<span className="text-sm text-slate-400 whitespace-nowrap">{l.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
className="lg:hidden w-10 h-10 flex items-center justify-center text-slate-600 hover:text-primary transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-3xl">
|
||||
{isMenuOpen ? 'close' : 'menu'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
{/* Mobile Menu Backdrop */}
|
||||
<div
|
||||
className={`lg:hidden fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-[150] transition-opacity duration-300 ${isMenuOpen ? 'opacity-100 visible' : 'opacity-0 invisible pointer-events-none'}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Mobile Menu Sidebar */}
|
||||
<div className={`lg:hidden fixed inset-y-0 right-0 w-[300px] h-screen bg-white z-[200] shadow-2xl transition-transform duration-500 ease-in-out transform ${isMenuOpen ? 'translate-x-0' : 'translate-x-full'}`}>
|
||||
<div className="flex flex-col h-full bg-white">
|
||||
{/* Close Button Header */}
|
||||
<div className="flex justify-end p-6 border-b border-slate-50">
|
||||
<button
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
className="w-10 h-10 flex items-center justify-center text-slate-400 hover:text-slate-900 transition-colors"
|
||||
aria-label="Fermer le menu"
|
||||
>
|
||||
<span className="material-symbols-outlined text-3xl font-bold text-slate-900">close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 flex flex-col p-8 gap-8 overflow-y-auto scrollbar-hide">
|
||||
<div className="flex flex-col gap-6">
|
||||
{[
|
||||
{ label: t.mobileMenu.home, href: '#' },
|
||||
{ label: t.mobileMenu.solution, href: '#deployment-modes' },
|
||||
{ label: t.mobileMenu.features, href: '#features' },
|
||||
{ label: t.mobileMenu.ai, href: '#ai-assistant' },
|
||||
{ label: t.mobileMenu.audience, href: '#audience' },
|
||||
{ label: t.pricing.sectionLabel.toUpperCase(), href: '#pricing' },
|
||||
{ label: t.mobileMenu.contact, href: '#contact' }
|
||||
].map((item) => (
|
||||
<a
|
||||
key={item.label}
|
||||
className="text-sm font-bold tracking-[0.2em] text-slate-900 hover:text-primary transition-colors py-3 border-b border-slate-50 flex items-center justify-between"
|
||||
href={item.href}
|
||||
onClick={() => {
|
||||
if (item.href === '#') {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else if (item.href.startsWith('#')) {
|
||||
const el = document.querySelector(item.href);
|
||||
if (el) { el.scrollIntoView({ behavior: 'smooth' }); }
|
||||
}
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Language selector - mobile */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{LANGUAGES.map((l) => (
|
||||
<button
|
||||
key={l.code}
|
||||
onClick={() => setLang(l.code)}
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-xl border-2 transition-all text-left ${lang === l.code ? 'bg-primary/5 border-primary text-primary' : 'border-slate-100 text-slate-600 hover:border-slate-300'}`}
|
||||
>
|
||||
<span className="text-xs font-black w-6">{l.label}</span>
|
||||
<span className="text-sm font-medium">{l.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto py-8 flex flex-col gap-4">
|
||||
<a
|
||||
href="https://manager.myinfomate.be"
|
||||
className="w-full text-center py-4 text-xs font-black tracking-widest text-slate-800 border-2 border-slate-100 rounded-xl hover:border-primary transition-all"
|
||||
>
|
||||
{t.mobileMenu.login}
|
||||
</a>
|
||||
<button
|
||||
onClick={(e) => { scrollToContact(e); setIsMenuOpen(false); }}
|
||||
className="w-full py-4 bg-primary text-slate-900 font-black text-xs tracking-widest rounded-xl shadow-xl shadow-primary/20 active:scale-95 transition-all"
|
||||
>
|
||||
{t.mobileMenu.demo}
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<NavBar lang={lang} />
|
||||
|
||||
<div className="pt-20">
|
||||
{/* Hero Section */}
|
||||
@ -327,19 +33,17 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
{t.hero.subtitle}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-4">
|
||||
<button
|
||||
onClick={scrollToContact}
|
||||
className="w-full sm:w-auto px-8 py-4 bg-primary text-slate-900 font-extrabold rounded-full shadow-xl shadow-primary/30 flex items-center justify-center gap-2 group transition-all"
|
||||
<a
|
||||
href="#contact"
|
||||
className="inline-flex w-full sm:w-auto px-8 py-4 bg-primary text-slate-900 font-extrabold rounded-full shadow-xl shadow-primary/30 items-center justify-center gap-2 group transition-all"
|
||||
>
|
||||
{t.hero.cta}
|
||||
<span className="material-symbols-outlined transition-transform group-hover:translate-x-1">arrow_forward</span>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 relative w-full mt-12 lg:mt-0">
|
||||
{/* UI Mockup Group - Improved for Mobile */}
|
||||
<div className="relative w-full aspect-[4/3] sm:aspect-video lg:aspect-[4/3] max-w-xl mx-auto">
|
||||
{/* Interactive Map Tablet Preview */}
|
||||
<div className="absolute top-0 left-0 w-full h-full lg:h-[85%] bg-slate-900 rounded-2xl lg:rounded-3xl overflow-hidden shadow-2xl border-4 border-slate-800 z-10 transform lg:-rotate-2">
|
||||
<Image
|
||||
src={resolveImage("{{DATA:IMAGE:IMAGE_4}}")}
|
||||
@ -354,16 +58,12 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<p className="text-xs font-extrabold text-slate-900">{t.mockup.mapSubtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agenda Mobile Preview - Hidden or repositioned on very small screens */}
|
||||
<div className="hidden sm:block absolute -bottom-6 -left-6 lg:-bottom-10 lg:-left-10 w-1/3 aspect-[9/19] bg-white rounded-[1.5rem] lg:rounded-[2rem] shadow-2xl border-2 lg:border-4 border-slate-900 z-30 transform rotate-3 overflow-hidden">
|
||||
<Image src={resolveImage("{{DATA:IMAGE:IMAGE_5}}")} alt="App screenshot" fill className="object-cover object-top" sizes="20vw" />
|
||||
<Image src={resolveImage("{{DATA:IMAGE:IMAGE_5}}")} alt="MyInfoMate agenda module on mobile" fill className="object-cover object-top" sizes="20vw" />
|
||||
<div className="absolute top-1 left-1/2 -translate-x-1/2 w-6 lg:w-8 h-1 lg:h-2 bg-slate-900 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
{/* Tours Mobile Preview */}
|
||||
<div className="absolute -bottom-4 -right-4 lg:-bottom-6 lg:-right-6 w-1/3 lg:w-[35%] aspect-[9/19] bg-white rounded-[1.5rem] lg:rounded-[2rem] shadow-2xl border-2 lg:border-4 border-slate-900 z-20 transform -rotate-6 overflow-hidden">
|
||||
<Image src={resolveImage("{{DATA:IMAGE:IMAGE_7}}")} alt="App screenshot" fill className="object-cover object-top" sizes="20vw" />
|
||||
<Image src={resolveImage("{{DATA:IMAGE:IMAGE_7}}")} alt="MyInfoMate tours module on mobile" fill className="object-cover object-top" sizes="20vw" />
|
||||
<div className="absolute top-1 left-1/2 -translate-x-1/2 w-6 lg:w-8 h-1 lg:h-2 bg-slate-900 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -379,7 +79,6 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<h3 className="text-3xl lg:text-5xl font-extrabold text-slate-900 tracking-tight">{t.strategic.sectionTitle}</h3>
|
||||
</Reveal>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-16 gap-y-10">
|
||||
{/* Feature 1 */}
|
||||
<Reveal delay={0} className="flex items-start gap-5">
|
||||
<div className="shrink-0 w-12 h-12 rounded-2xl bg-primary/15 text-primary flex items-center justify-center">
|
||||
<span className="material-symbols-outlined text-2xl">sync</span>
|
||||
@ -389,7 +88,6 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<p className="text-slate-500 leading-relaxed text-sm">{t.strategic.feature1Desc}</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
{/* Feature 2 */}
|
||||
<Reveal delay={80} className="flex items-start gap-5">
|
||||
<div className="shrink-0 w-12 h-12 rounded-2xl bg-accent-violet/15 text-accent-violet flex items-center justify-center">
|
||||
<span className="material-symbols-outlined text-2xl">code_off</span>
|
||||
@ -399,7 +97,6 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<p className="text-slate-500 leading-relaxed text-sm">{t.strategic.feature2Desc}</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
{/* Feature 3 */}
|
||||
<Reveal delay={160} className="flex items-start gap-5">
|
||||
<div className="shrink-0 w-12 h-12 rounded-2xl bg-accent-orange/15 text-accent-orange flex items-center justify-center">
|
||||
<span className="material-symbols-outlined text-2xl">download_for_offline</span>
|
||||
@ -409,7 +106,6 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<p className="text-slate-500 leading-relaxed text-sm">{t.strategic.feature3Desc}</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
{/* Feature 4 */}
|
||||
<Reveal delay={240} className="flex items-start gap-5">
|
||||
<div className="shrink-0 w-12 h-12 rounded-2xl bg-primary/15 text-primary flex items-center justify-center">
|
||||
<span className="material-symbols-outlined text-2xl">dashboard_customize</span>
|
||||
@ -423,18 +119,15 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Deployment Modes Section */}
|
||||
{/* Deployment Modes */}
|
||||
<section className="py-24 bg-slate-50 border-y border-slate-100" id="deployment-modes">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<Reveal className="text-center mb-16">
|
||||
<h2 className="text-primary font-extrabold uppercase tracking-widest text-sm mb-4">{t.deployment.sectionLabel}</h2>
|
||||
<h3 className="text-3xl lg:text-5xl font-extrabold text-slate-900 leading-tight mb-6">{t.deployment.sectionTitle}</h3>
|
||||
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
|
||||
{t.deployment.sectionDesc}
|
||||
</p>
|
||||
<p className="text-lg text-slate-600 max-w-2xl mx-auto">{t.deployment.sectionDesc}</p>
|
||||
</Reveal>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{/* Mode 1: Mobile BYOD */}
|
||||
<Reveal delay={0}><div className="group p-8 rounded-[2.5rem] bg-white border border-slate-100 shadow-sm hover:shadow-xl transition-all duration-300">
|
||||
<div className="w-14 h-14 rounded-2xl bg-primary/10 text-primary flex items-center justify-center mb-6 group-hover:bg-primary group-hover:text-white transition-all">
|
||||
<span className="material-symbols-outlined text-3xl">smartphone</span>
|
||||
@ -442,7 +135,6 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<h4 className="text-xl font-extrabold text-slate-900 mb-4">{t.deployment.mode1Title}</h4>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">{t.deployment.mode1Desc}</p>
|
||||
</div></Reveal>
|
||||
{/* Mode 2: Kiosk Tablet */}
|
||||
<Reveal delay={80}><div className="group p-8 rounded-[2.5rem] bg-white border border-slate-100 shadow-sm hover:shadow-xl transition-all duration-300">
|
||||
<div className="w-14 h-14 rounded-2xl bg-accent-violet/10 text-accent-violet flex items-center justify-center mb-6 group-hover:bg-accent-violet group-hover:text-white transition-all">
|
||||
<span className="material-symbols-outlined text-3xl">tablet_android</span>
|
||||
@ -450,7 +142,6 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<h4 className="text-xl font-extrabold text-slate-900 mb-4">{t.deployment.mode2Title}</h4>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">{t.deployment.mode2Desc}</p>
|
||||
</div></Reveal>
|
||||
{/* Mode 3: Web App */}
|
||||
<Reveal delay={160}><div className="group p-8 rounded-[2.5rem] bg-white border border-slate-100 shadow-sm hover:shadow-xl transition-all duration-300">
|
||||
<div className="w-14 h-14 rounded-2xl bg-accent-orange/10 text-accent-orange flex items-center justify-center mb-6 group-hover:bg-accent-orange group-hover:text-white transition-all">
|
||||
<span className="material-symbols-outlined text-3xl">language</span>
|
||||
@ -458,9 +149,8 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<h4 className="text-xl font-extrabold text-slate-900 mb-4">{t.deployment.mode3Title}</h4>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">{t.deployment.mode3Desc}</p>
|
||||
</div></Reveal>
|
||||
{/* Mode 4: Future VR/AR */}
|
||||
<Reveal delay={240}><div className="group p-8 rounded-[2.5rem] bg-slate-900 text-white shadow-xl relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-3" title="Innovation à venir">
|
||||
<div className="absolute top-0 right-0 p-3">
|
||||
<span className="px-2 py-1 bg-primary text-slate-900 text-[10px] font-black uppercase rounded-md">{t.deployment.comingSoon}</span>
|
||||
</div>
|
||||
<div className="w-14 h-14 rounded-2xl bg-white/10 text-primary flex items-center justify-center mb-6">
|
||||
@ -473,178 +163,8 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Interactive Modules Section (Module Explorer) */}
|
||||
<section className="py-24 bg-white" id="features">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<Reveal className="text-center mb-16">
|
||||
<h2 className="text-rose-600 font-extrabold uppercase tracking-widest text-sm mb-4">{t.modules.sectionLabel}</h2>
|
||||
<h3 className="text-3xl lg:text-5xl font-extrabold text-slate-900 leading-tight mb-6">{t.modules.sectionTitle}</h3>
|
||||
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
|
||||
{t.modules.sectionDesc}
|
||||
</p>
|
||||
</Reveal>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-8 lg:gap-16 items-stretch">
|
||||
{/* Sidebar (Tabs) */}
|
||||
<Reveal delay={100} className="w-full lg:w-1/3 relative">
|
||||
<div className="lg:hidden absolute left-0 top-0 bottom-4 w-12 bg-gradient-to-r from-white to-transparent z-10 pointer-events-none"></div>
|
||||
<div className="lg:hidden absolute right-0 top-0 bottom-4 w-12 bg-gradient-to-l from-white to-transparent z-10 pointer-events-none"></div>
|
||||
<div className="flex lg:flex-col gap-3 overflow-x-auto pb-4 lg:pb-0 scrollbar-hide snap-x">
|
||||
{modules.map((module, index) => (
|
||||
<button
|
||||
key={module.id}
|
||||
onClick={() => setActiveModule(index)}
|
||||
onMouseEnter={() => setActiveModule(index)}
|
||||
className={`flex items-center gap-3 lg:gap-4 px-5 py-4 lg:px-6 lg:py-5 rounded-2xl border-2 transition-all text-left whitespace-nowrap lg:whitespace-normal snap-center flex-shrink-0 lg:flex-shrink-1 w-auto lg:w-full ${activeModule === index
|
||||
? 'bg-rose-50 border-rose-200 text-rose-600 shadow-sm'
|
||||
: 'bg-white border-slate-100 text-slate-500 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<span className={`material-symbols-outlined text-xl lg:text-2xl ${activeModule === index ? 'text-rose-600' : 'text-slate-400'}`}>
|
||||
{module.icon}
|
||||
</span>
|
||||
<span className="font-bold text-xs lg:text-base">{module.title}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Main Display */}
|
||||
<Reveal delay={200} className="flex-1 w-full lg:flex lg:flex-col">
|
||||
|
||||
{/* Mobile: single centered card */}
|
||||
<div className="lg:hidden bg-slate-50 rounded-[3rem] p-8 border border-slate-100 overflow-hidden">
|
||||
<div className="text-center mb-6">
|
||||
<div className="inline-flex p-3 rounded-2xl bg-white shadow-sm text-rose-600 mb-4 font-bold gap-2 items-center">
|
||||
<span className="material-symbols-outlined">{modules[activeModule].icon}</span>
|
||||
<span className="text-xs uppercase tracking-wider">{modules[activeModule].title}</span>
|
||||
</div>
|
||||
<h3 className="text-2xl font-black text-slate-900 mb-4 leading-tight">{modules[activeModule].title}</h3>
|
||||
<p className="text-slate-600 leading-relaxed mb-6">{modules[activeModule].description}</p>
|
||||
<div className="p-4 bg-rose-600/5 border border-rose-100 rounded-2xl">
|
||||
<p className="text-rose-600 font-bold text-sm flex items-center justify-center gap-2">
|
||||
<span className="material-symbols-outlined text-base">verified</span>
|
||||
{modules[activeModule].value}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<div className="relative w-[200px] h-[420px] bg-slate-900 rounded-[3rem] border-[8px] border-slate-800 shadow-2xl overflow-hidden">
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-24 h-5 bg-slate-800 rounded-b-2xl z-20"></div>
|
||||
<div className="absolute inset-[2px] bg-white rounded-[2.25rem] overflow-hidden">
|
||||
<div className="h-full flex flex-col pt-7">
|
||||
<header className="h-12 border-b border-slate-100 flex items-center px-3 bg-white/80 sticky top-0 z-10">
|
||||
<div className="w-7 h-7 rounded-lg bg-rose-100 flex items-center justify-center text-rose-600">
|
||||
<span className="material-symbols-outlined text-base">{modules[activeModule].icon}</span>
|
||||
</div>
|
||||
<span className="ml-2 font-bold text-xs text-slate-900 truncate">{modules[activeModule].title}</span>
|
||||
</header>
|
||||
<div className="p-3 flex-1 space-y-3">
|
||||
<div className="h-32 rounded-xl bg-slate-100 flex items-center justify-center relative overflow-hidden">
|
||||
<span className="material-symbols-outlined text-4xl text-slate-300">{modules[activeModule].icon}</span>
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-rose-500/10 to-transparent"></div>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-slate-100 rounded-full"></div>
|
||||
<div className="h-2 w-2/3 bg-slate-100 rounded-full"></div>
|
||||
<div className="h-14 w-full bg-slate-50 border border-slate-100 rounded-xl p-2 flex gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-rose-200 shrink-0"></div>
|
||||
<div className="space-y-1 flex-1">
|
||||
<div className="h-1.5 w-full bg-slate-200 rounded-full"></div>
|
||||
<div className="h-1.5 w-1/2 bg-slate-200 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="h-8 bg-slate-100 rounded-lg"></div>
|
||||
<div className="h-8 bg-rose-600 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop: original layout + enobase-style 3D phone stack */}
|
||||
<div className="hidden lg:flex flex-1 bg-slate-50 rounded-[3rem] p-10 border border-slate-100 gap-10 items-center">
|
||||
{/* Description */}
|
||||
<div className="flex-1 relative z-10 text-left">
|
||||
<div className="inline-flex p-3 rounded-2xl bg-white shadow-sm text-rose-600 mb-5 font-bold gap-2 items-center">
|
||||
<span className="material-symbols-outlined">{modules[activeModule].icon}</span>
|
||||
<span className="text-xs uppercase tracking-wider">{modules[activeModule].title}</span>
|
||||
</div>
|
||||
<h3 className="text-3xl font-black text-slate-900 mb-5 leading-tight">{modules[activeModule].title}</h3>
|
||||
<p className="text-slate-600 text-base leading-relaxed mb-6">{modules[activeModule].description}</p>
|
||||
<div className="p-5 bg-rose-600/5 border border-rose-100 rounded-2xl">
|
||||
<p className="text-rose-600 font-bold text-sm flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-base">verified</span>
|
||||
{t.modules.valueLabel} {modules[activeModule].value}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enobase-style 3D horizontal perspective phone stack */}
|
||||
<div className="flex-shrink-0 relative" style={{ width: '280px', height: '480px' }}>
|
||||
<div style={{ position: 'absolute', inset: 0, transform: 'perspective(900px) rotateX(6deg) rotateY(-15deg)' }}>
|
||||
{modules.map((module, index) => {
|
||||
const deckPos = (index - activeModule + modules.length) % modules.length;
|
||||
const isActive = deckPos === 0;
|
||||
return (
|
||||
<div
|
||||
key={module.id}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
transformOrigin: 'top left',
|
||||
transform: `translateX(${deckPos * 12}px) scale(${1 - deckPos * 0.06})`,
|
||||
zIndex: modules.length - deckPos,
|
||||
opacity: isActive ? 1 : Math.max(0.06, 0.55 - deckPos * 0.1),
|
||||
transition: 'transform 0.45s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.45s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
>
|
||||
<div className={`relative w-[240px] h-[480px] rounded-[3rem] border-[8px] overflow-hidden ${isActive ? 'bg-slate-900 border-slate-800 shadow-2xl shadow-rose-500/20' : 'bg-slate-800 border-slate-700 shadow-xl'}`}>
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-28 h-6 bg-slate-800 rounded-b-2xl z-20"></div>
|
||||
<div className="absolute inset-[2px] bg-white rounded-[2.25rem] overflow-hidden">
|
||||
<div className="h-full flex flex-col pt-8">
|
||||
<header className="h-14 border-b border-slate-100 flex items-center px-4 bg-white/80 backdrop-blur-sm sticky top-0 z-10">
|
||||
<div className="w-8 h-8 rounded-lg bg-rose-100 flex items-center justify-center text-rose-600">
|
||||
<span className="material-symbols-outlined text-lg">{module.icon}</span>
|
||||
</div>
|
||||
<span className="ml-3 font-bold text-xs text-slate-900 truncate">{module.title}</span>
|
||||
</header>
|
||||
<div className="p-4 flex-1 space-y-4">
|
||||
<div className="h-36 rounded-2xl bg-slate-100 flex items-center justify-center relative overflow-hidden">
|
||||
<span className="material-symbols-outlined text-5xl text-slate-300">{module.icon}</span>
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-rose-500/10 to-transparent"></div>
|
||||
</div>
|
||||
<div className="h-3 w-full bg-slate-100 rounded-full"></div>
|
||||
<div className="h-3 w-2/3 bg-slate-100 rounded-full"></div>
|
||||
<div className="h-20 w-full bg-slate-50 border border-slate-100 rounded-xl p-3 flex gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-rose-200 shrink-0"></div>
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="h-2 w-full bg-slate-200 rounded-full"></div>
|
||||
<div className="h-2 w-1/2 bg-slate-200 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="h-10 bg-slate-100 rounded-xl"></div>
|
||||
<div className="h-10 bg-rose-600 rounded-xl"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/* Module Explorer */}
|
||||
<ModulesSection lang={lang} />
|
||||
|
||||
{/* AI Assistant */}
|
||||
<section className="py-24 bg-slate-900 relative overflow-hidden" id="ai-assistant">
|
||||
@ -665,7 +185,6 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<p className="text-slate-400 text-lg max-w-2xl mx-auto">{t.ai.desc}</p>
|
||||
</Reveal>
|
||||
<div className="flex flex-col lg:flex-row items-center gap-12 lg:gap-20">
|
||||
{/* Chat mockup */}
|
||||
<Reveal delay={80} className="w-full lg:w-auto flex justify-center">
|
||||
<div className="w-full max-w-xs bg-slate-800 rounded-3xl border border-slate-700 shadow-2xl shadow-primary/10 overflow-hidden">
|
||||
<div className="flex items-center gap-3 p-4 border-b border-slate-700">
|
||||
@ -683,22 +202,16 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<div className="w-6 h-6 rounded-full bg-primary/20 flex items-center justify-center shrink-0">
|
||||
<span className="material-symbols-outlined text-primary" style={{ fontSize: '14px' }}>smart_toy</span>
|
||||
</div>
|
||||
<div className="bg-slate-700 text-slate-200 text-sm px-4 py-2.5 rounded-2xl rounded-bl-sm max-w-[85%] leading-relaxed">
|
||||
{t.ai.mockupMsg1}
|
||||
</div>
|
||||
<div className="bg-slate-700 text-slate-200 text-sm px-4 py-2.5 rounded-2xl rounded-bl-sm max-w-[85%] leading-relaxed">{t.ai.mockupMsg1}</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<div className="bg-primary text-slate-900 text-sm px-4 py-2.5 rounded-2xl rounded-br-sm max-w-[85%] font-semibold leading-relaxed">
|
||||
{t.ai.mockupMsg2}
|
||||
</div>
|
||||
<div className="bg-primary text-slate-900 text-sm px-4 py-2.5 rounded-2xl rounded-br-sm max-w-[85%] font-semibold leading-relaxed">{t.ai.mockupMsg2}</div>
|
||||
</div>
|
||||
<div className="flex gap-2 items-end">
|
||||
<div className="w-6 h-6 rounded-full bg-primary/20 flex items-center justify-center shrink-0">
|
||||
<span className="material-symbols-outlined text-primary" style={{ fontSize: '14px' }}>smart_toy</span>
|
||||
</div>
|
||||
<div className="bg-slate-700 text-slate-200 text-sm px-4 py-2.5 rounded-2xl rounded-bl-sm max-w-[85%] leading-relaxed">
|
||||
{t.ai.mockupMsg3}
|
||||
</div>
|
||||
<div className="bg-slate-700 text-slate-200 text-sm px-4 py-2.5 rounded-2xl rounded-bl-sm max-w-[85%] leading-relaxed">{t.ai.mockupMsg3}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-700">
|
||||
@ -711,7 +224,6 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
{/* Features grid */}
|
||||
<div className="flex-1 grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
<Reveal delay={0}><div className="bg-slate-800/60 border border-slate-700 rounded-2xl p-6 hover:border-primary/40 transition-colors group">
|
||||
<div className="w-11 h-11 bg-primary/10 rounded-xl flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
|
||||
@ -746,7 +258,7 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* White Label / Value Proposition */}
|
||||
{/* White Label */}
|
||||
<section className="py-24 bg-slate-50 border-y border-slate-100 relative overflow-hidden" id="whitelabel">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="flex flex-col lg:flex-row items-center gap-20">
|
||||
@ -898,7 +410,63 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Pricing Section */}
|
||||
{/* Cas Clients - Social Proof */}
|
||||
<section className="py-24 bg-slate-900 relative overflow-hidden" id="cas-clients">
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-20 left-1/4 w-96 h-96 bg-primary/10 rounded-full blur-3xl"></div>
|
||||
<div className="absolute bottom-20 right-1/4 w-80 h-80 bg-accent-violet/10 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
<div className="max-w-7xl mx-auto px-6 relative z-10">
|
||||
<Reveal className="text-center mb-16">
|
||||
<h2 className="text-primary font-extrabold uppercase tracking-widest text-sm mb-4">{t.casClients.sectionLabel}</h2>
|
||||
<h3 className="text-3xl lg:text-5xl font-extrabold text-white leading-tight mb-6">{t.casClients.title}</h3>
|
||||
<p className="text-slate-400 text-lg max-w-2xl mx-auto">{t.casClients.subtitle}</p>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{casClients.map((client, i) => {
|
||||
const ct = client.translations[lang];
|
||||
return (
|
||||
<Reveal key={client.slug} delay={i * 80}>
|
||||
<a
|
||||
href={`/${lang}/cas-clients/${client.slug}`}
|
||||
className="group flex flex-col h-full bg-slate-800/60 border border-slate-700 rounded-3xl p-8 hover:border-primary/40 hover:bg-slate-800/80 transition-all"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<span className="text-xs font-extrabold text-primary uppercase tracking-widest px-3 py-1 rounded-full bg-primary/10 border border-primary/20">
|
||||
{ct.badge}
|
||||
</span>
|
||||
<span className="text-xs text-slate-500 font-semibold">{client.year}</span>
|
||||
</div>
|
||||
<h4 className="text-2xl font-extrabold text-white mb-3 group-hover:text-primary transition-colors leading-tight">
|
||||
{client.client}
|
||||
</h4>
|
||||
<p className="text-slate-400 text-sm leading-relaxed mb-6 flex-1">
|
||||
{ct.description}
|
||||
</p>
|
||||
<span className="inline-flex items-center gap-1 text-xs font-extrabold text-primary uppercase tracking-widest group-hover:gap-2 transition-all">
|
||||
{t.casClients.readCase}
|
||||
<span className="material-symbols-outlined text-sm">arrow_forward</span>
|
||||
</span>
|
||||
</a>
|
||||
</Reveal>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Reveal delay={300} className="text-center mt-14">
|
||||
<a
|
||||
href={`/${lang}/cas-clients`}
|
||||
className="inline-flex items-center gap-2 text-primary font-bold hover:gap-3 transition-all"
|
||||
>
|
||||
{t.casClients.ctaAll}
|
||||
<span className="material-symbols-outlined">arrow_forward</span>
|
||||
</a>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Pricing */}
|
||||
<section className="py-24 bg-slate-50" id="pricing">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<Reveal>
|
||||
@ -908,10 +476,7 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<p className="text-slate-500 max-w-xl mx-auto">{t.pricing.sectionDesc}</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-6 items-stretch pt-6">
|
||||
|
||||
{/* Essentiel */}
|
||||
<Reveal delay={0}>
|
||||
<div className="flex flex-col h-full bg-white rounded-3xl border border-slate-200 p-8 shadow-sm hover:shadow-md transition-shadow relative">
|
||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2">
|
||||
@ -947,13 +512,11 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button onClick={scrollToContact} className="w-full py-3 rounded-2xl border-2 border-slate-200 text-slate-700 font-bold text-sm hover:border-primary hover:text-primary transition-all">
|
||||
<a href="#contact" className="block w-full py-3 rounded-2xl border-2 border-slate-200 text-slate-700 font-bold text-sm hover:border-primary hover:text-primary transition-all text-center">
|
||||
{t.pricing.ctaStart}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Pro */}
|
||||
<Reveal delay={60}>
|
||||
<div className="flex flex-col h-full bg-white rounded-3xl border-2 border-primary p-8 shadow-xl shadow-primary/10 relative">
|
||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2">
|
||||
@ -996,13 +559,11 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button onClick={scrollToContact} className="w-full py-3 rounded-2xl bg-primary text-slate-900 font-extrabold text-sm hover:brightness-110 transition-all shadow-lg shadow-primary/20">
|
||||
<a href="#contact" className="block w-full py-3 rounded-2xl bg-primary text-slate-900 font-extrabold text-sm hover:brightness-110 transition-all shadow-lg shadow-primary/20 text-center">
|
||||
{t.pricing.ctaStart}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Bundle */}
|
||||
<Reveal delay={120}>
|
||||
<div className="flex flex-col h-full bg-gradient-to-br from-slate-900 to-slate-800 rounded-3xl p-8 shadow-xl">
|
||||
<div className="mb-6">
|
||||
@ -1047,13 +608,11 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
{t.pricing.features.autoTranslation}
|
||||
</li>
|
||||
</ul>
|
||||
<button onClick={scrollToContact} className="w-full py-3 rounded-2xl bg-white/10 text-white font-bold text-sm hover:bg-white/20 transition-all border border-white/10">
|
||||
<a href="#contact" className="block w-full py-3 rounded-2xl bg-white/10 text-white font-bold text-sm hover:bg-white/20 transition-all border border-white/10 text-center">
|
||||
{t.pricing.ctaContact}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Enterprise */}
|
||||
<Reveal delay={180}>
|
||||
<div className="flex flex-col h-full bg-gradient-to-br from-violet-950 to-slate-900 rounded-3xl p-8 shadow-xl border border-violet-800/40 relative">
|
||||
<div className="mb-6">
|
||||
@ -1099,14 +658,12 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button onClick={scrollToContact} className="w-full py-3 rounded-2xl bg-violet-600 text-white font-bold text-sm hover:bg-violet-500 transition-all shadow-lg shadow-violet-900/40">
|
||||
<a href="#contact" className="block w-full py-3 rounded-2xl bg-violet-600 text-white font-bold text-sm hover:bg-violet-500 transition-all shadow-lg shadow-violet-900/40 text-center">
|
||||
{t.pricing.ctaContact}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
</div>
|
||||
|
||||
<Reveal delay={100}>
|
||||
<div className="mt-12 flex flex-col sm:flex-row items-center justify-between gap-6 bg-white rounded-2xl border border-slate-200 px-8 py-6">
|
||||
<div className="flex items-center gap-4">
|
||||
@ -1116,15 +673,14 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<p className="text-sm text-slate-500">{t.pricing.multiYearDesc}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={scrollToContact}
|
||||
<a
|
||||
href="#contact"
|
||||
className="shrink-0 px-6 py-3 rounded-full border-2 border-primary text-primary font-bold text-sm hover:bg-primary hover:text-slate-900 transition-all whitespace-nowrap"
|
||||
>
|
||||
{t.pricing.multiYearCta}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -1139,19 +695,17 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<h2 className="text-3xl lg:text-5xl font-extrabold text-white mb-8 relative z-10">
|
||||
{t.cta.titleBefore}<span className="text-primary">{t.cta.titleHighlight}</span>{t.cta.titleAfter}
|
||||
</h2>
|
||||
<p className="text-slate-400 text-lg mb-12 max-w-2xl mx-auto relative z-10">
|
||||
{t.cta.subtitle}
|
||||
</p>
|
||||
<p className="text-slate-400 text-lg mb-12 max-w-2xl mx-auto relative z-10">{t.cta.subtitle}</p>
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-6 relative z-10">
|
||||
<button
|
||||
onClick={scrollToContact}
|
||||
className="w-full sm:w-auto px-10 py-5 bg-primary text-slate-900 font-extrabold rounded-full shadow-2xl shadow-primary/40 hover:scale-105 transition-all"
|
||||
<a
|
||||
href="#contact"
|
||||
className="inline-flex w-full sm:w-auto px-10 py-5 bg-primary text-slate-900 font-extrabold rounded-full shadow-2xl shadow-primary/40 hover:scale-105 transition-all items-center justify-center"
|
||||
>
|
||||
{t.cta.button1}
|
||||
</button>
|
||||
</a>
|
||||
<a
|
||||
href="mailto:contact@unov.be"
|
||||
className="w-full sm:w-auto px-10 py-5 bg-white/10 text-white font-bold rounded-full hover:bg-white/20 transition-all border border-white/10 text-center"
|
||||
className="inline-flex w-full sm:w-auto px-10 py-5 bg-white/10 text-white font-bold rounded-full hover:bg-white/20 transition-all border border-white/10 items-center justify-center"
|
||||
>
|
||||
{t.cta.button2}
|
||||
</a>
|
||||
@ -1164,7 +718,6 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<section className="py-24 bg-white relative overflow-hidden" id="contact">
|
||||
<div className="absolute top-0 right-0 -z-10 w-64 h-64 bg-primary/5 rounded-full blur-3xl"></div>
|
||||
<div className="absolute bottom-0 left-0 -z-10 w-96 h-96 bg-accent-violet/5 rounded-full blur-3xl"></div>
|
||||
|
||||
<div className="max-w-4xl mx-auto px-6">
|
||||
<div className="bg-white rounded-[3rem] shadow-2xl border border-slate-100 p-10 lg:p-16 relative">
|
||||
<div className="text-center mb-12">
|
||||
@ -1172,94 +725,7 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<h3 className="text-3xl lg:text-4xl font-extrabold text-slate-900 mb-4">{t.contact.title}</h3>
|
||||
<p className="text-slate-600">{t.contact.subtitle}</p>
|
||||
</div>
|
||||
|
||||
{status === 'success' ? (
|
||||
<div className="text-center py-20">
|
||||
<div className="w-20 h-20 bg-primary/20 text-primary rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<span className="material-symbols-outlined text-5xl">task_alt</span>
|
||||
</div>
|
||||
<h3 className="text-3xl font-extrabold text-slate-900 mb-4">{t.contact.successTitle}</h3>
|
||||
<p className="text-slate-600 mb-8">{t.contact.successDesc}</p>
|
||||
<button
|
||||
onClick={() => setStatus('idle')}
|
||||
className="text-primary font-bold hover:underline"
|
||||
>
|
||||
{t.contact.successButton}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{status === 'error' && (
|
||||
<div className="p-4 bg-red-50 text-red-600 rounded-xl text-sm font-medium border border-red-100 flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-sm">error</span>
|
||||
{t.contact.errorMsg}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.contact.firstNameLabel}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="firstName"
|
||||
required
|
||||
value={formData.firstName}
|
||||
onChange={handleChange}
|
||||
className="w-full px-6 py-4 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all"
|
||||
placeholder={t.contact.firstNamePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.contact.lastNameLabel}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="lastName"
|
||||
required
|
||||
value={formData.lastName}
|
||||
onChange={handleChange}
|
||||
className="w-full px-6 py-4 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all"
|
||||
placeholder={t.contact.lastNamePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.contact.emailLabel}</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className="w-full px-6 py-4 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all"
|
||||
placeholder={t.contact.emailPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.contact.messageLabel}</label>
|
||||
<textarea
|
||||
name="message"
|
||||
rows={4}
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
className="w-full px-6 py-4 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all resize-none"
|
||||
placeholder={t.contact.messagePlaceholder}
|
||||
></textarea>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === 'loading'}
|
||||
className={`w-full py-5 bg-slate-900 text-white font-extrabold rounded-2xl shadow-xl hover:bg-slate-800 transition-all flex items-center justify-center gap-3 group ${status === 'loading' ? 'opacity-70 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{status === 'loading' ? (
|
||||
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></span>
|
||||
) : (
|
||||
<>
|
||||
{t.contact.submitButton}
|
||||
<span className="material-symbols-outlined group-hover:translate-x-1 transition-transform">send</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
<ContactForm lang={lang} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -1280,9 +746,7 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
/>
|
||||
<span className="text-lg font-extrabold tracking-tight text-slate-900">MyInfoMate</span>
|
||||
</div>
|
||||
<p className="text-slate-500 text-sm leading-relaxed mb-6">
|
||||
{t.footer.desc}
|
||||
</p>
|
||||
<p className="text-slate-500 text-sm leading-relaxed mb-6">{t.footer.desc}</p>
|
||||
<div className="flex gap-4">
|
||||
<a className="w-10 h-10 rounded-full bg-slate-200 flex items-center justify-center text-slate-600 hover:bg-primary hover:text-slate-900 transition-all" href="#">
|
||||
<span className="material-symbols-outlined">public</span>
|
||||
@ -1292,23 +756,23 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
<div>
|
||||
{/* <h4 className="font-bold text-slate-900 mb-6 uppercase text-xs tracking-widest">Produit</h4>
|
||||
<h4 className="font-bold text-slate-900 mb-6 uppercase text-xs tracking-widest">{t.footer.casClientsLink}</h4>
|
||||
<ul className="space-y-4">
|
||||
<li><a className="text-sm text-slate-500 hover:text-primary transition-colors" href="#">Fonctionnalités</a></li>
|
||||
<li><a className="text-sm text-slate-500 hover:text-primary transition-colors" href="#">Marque Blanche</a></li>
|
||||
<li><a className="text-sm text-slate-500 hover:text-primary transition-colors" href="#">CMS Centralisé</a></li>
|
||||
<li><a className="text-sm text-slate-500 hover:text-primary transition-colors" href="#">Tarifs</a></li>
|
||||
</ul> */}
|
||||
</div>
|
||||
<div>
|
||||
{/* <h4 className="font-bold text-slate-900 mb-6 uppercase text-xs tracking-widest">Entreprise</h4>
|
||||
<ul className="space-y-4">
|
||||
<li><a className="text-sm text-slate-500 hover:text-primary transition-colors" href="#">À propos</a></li>
|
||||
<li><a className="text-sm text-slate-500 hover:text-primary transition-colors" href="#">Blog</a></li>
|
||||
<li><a className="text-sm text-slate-500 hover:text-primary transition-colors" href="#">Partenaires</a></li>
|
||||
<li><a className="text-sm text-slate-500 hover:text-primary transition-colors" href="#">Contact</a></li>
|
||||
</ul> */}
|
||||
{casClients.map((c) => (
|
||||
<li key={c.slug}>
|
||||
<a className="text-sm text-slate-500 hover:text-primary transition-colors" href={`/${lang}/cas-clients/${c.slug}`}>
|
||||
{c.client}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<a className="text-sm text-primary font-bold hover:underline" href={`/${lang}/cas-clients`}>
|
||||
{t.casClients.ctaAll} →
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-slate-900 mb-6 uppercase text-xs tracking-widest">{t.footer.contactTitle}</h4>
|
||||
|
||||
143
src/app/[lang]/[segment]/opengraph-image.tsx
Normal file
143
src/app/[lang]/[segment]/opengraph-image.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { isLocale } from '@/i18n';
|
||||
import { getSegmentData } from '@/data/segments';
|
||||
|
||||
export const runtime = 'edge';
|
||||
export const size = { width: 1200, height: 630 };
|
||||
export const contentType = 'image/png';
|
||||
|
||||
export default function Image({ params }: { params: { lang: string; segment: string } }) {
|
||||
const lang = isLocale(params.lang) ? params.lang : 'fr';
|
||||
const data = getSegmentData(params.segment);
|
||||
const badge = data?.translations[lang].hero.badge ?? 'MyInfoMate';
|
||||
const title = data?.meta[lang].title ?? 'MyInfoMate';
|
||||
const description = data?.meta[lang].description ?? '';
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 60%, #0f172a 100%)',
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Cyan glow top-left */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-120px',
|
||||
left: '-80px',
|
||||
width: '600px',
|
||||
height: '600px',
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(13,242,223,0.18) 0%, transparent 65%)',
|
||||
}}
|
||||
/>
|
||||
{/* Purple glow bottom-right */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '-160px',
|
||||
right: '-80px',
|
||||
width: '700px',
|
||||
height: '700px',
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(139,92,246,0.14) 0%, transparent 65%)',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
padding: '0 80px',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{/* Segment badge */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
padding: '10px 24px',
|
||||
borderRadius: '999px',
|
||||
background: 'rgba(13,242,223,0.1)',
|
||||
border: '1px solid rgba(13,242,223,0.3)',
|
||||
marginBottom: '36px',
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '10px', height: '10px', borderRadius: '50%', background: '#0df2df' }} />
|
||||
<span
|
||||
style={{
|
||||
color: '#0df2df',
|
||||
fontSize: '18px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
{badge}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Product name */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: '72px',
|
||||
fontWeight: 900,
|
||||
color: '#ffffff',
|
||||
lineHeight: 1,
|
||||
marginBottom: '28px',
|
||||
letterSpacing: '-0.03em',
|
||||
}}
|
||||
>
|
||||
MyInfoMate
|
||||
</div>
|
||||
|
||||
{/* Page title */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: '26px',
|
||||
color: '#94a3b8',
|
||||
lineHeight: 1.4,
|
||||
maxWidth: '860px',
|
||||
marginBottom: '0px',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{title.replace(' | MyInfoMate', '').replace(' — MyInfoMate', '')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom URL */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '36px',
|
||||
color: '#475569',
|
||||
fontSize: '22px',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.02em',
|
||||
}}
|
||||
>
|
||||
myinfomate.be
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{ ...size }
|
||||
);
|
||||
}
|
||||
200
src/app/[lang]/cas-clients/[slug]/page.tsx
Normal file
200
src/app/[lang]/cas-clients/[slug]/page.tsx
Normal file
@ -0,0 +1,200 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { LOCALES, LOCALE_HTML_LANG, LOCALE_OG, DEFAULT_LOCALE, isLocale } from '@/i18n';
|
||||
import { getCasClientData, getAllCasClientSlugs } from '@/data/cas-clients';
|
||||
|
||||
const SITE_URL = 'https://myinfomate.be';
|
||||
|
||||
export function generateStaticParams() {
|
||||
const slugs = getAllCasClientSlugs();
|
||||
return LOCALES.flatMap((lang) => slugs.map((slug) => ({ lang, slug })));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ lang: string; slug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { lang, slug } = await params;
|
||||
if (!isLocale(lang)) return {};
|
||||
const data = getCasClientData(slug);
|
||||
if (!data) return {};
|
||||
const m = data.meta[lang];
|
||||
const languages = Object.fromEntries(
|
||||
LOCALES.map((l) => [LOCALE_HTML_LANG[l], `/${l}/cas-clients/${slug}`])
|
||||
);
|
||||
return {
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
openGraph: {
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
url: `${SITE_URL}/${lang}/cas-clients/${slug}`,
|
||||
siteName: 'MyInfoMate',
|
||||
locale: LOCALE_OG[lang],
|
||||
type: 'website',
|
||||
},
|
||||
alternates: {
|
||||
canonical: `/${lang}/cas-clients/${slug}`,
|
||||
languages: { ...languages, 'x-default': `/${DEFAULT_LOCALE}/cas-clients/${slug}` },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function CasClientPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ lang: string; slug: string }>;
|
||||
}) {
|
||||
const { lang, slug } = await params;
|
||||
if (!isLocale(lang)) notFound();
|
||||
const data = getCasClientData(slug);
|
||||
if (!data) notFound();
|
||||
|
||||
const t = data.translations[lang];
|
||||
|
||||
const caseStudySchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: t.headline,
|
||||
description: t.description,
|
||||
author: { '@type': 'Organization', name: 'Unov', url: SITE_URL },
|
||||
publisher: { '@type': 'Organization', name: 'MyInfoMate', url: SITE_URL },
|
||||
datePublished: `${data.year}-01-01`,
|
||||
url: `${SITE_URL}/${lang}/cas-clients/${slug}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(caseStudySchema) }} />
|
||||
|
||||
<main className="bg-slate-50 text-slate-900 min-h-screen font-display">
|
||||
{/* Nav */}
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200">
|
||||
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
|
||||
<a href={`/${lang}/cas-clients`} className="flex items-center gap-2 text-sm font-bold text-slate-600 hover:text-primary transition-colors">
|
||||
<span className="material-symbols-outlined text-base">arrow_back</span>
|
||||
{t.backLabel}
|
||||
</a>
|
||||
<a
|
||||
href={`/${lang}#contact`}
|
||||
className="bg-primary hover:brightness-110 text-slate-900 font-extrabold text-xs px-5 py-2.5 rounded-full shadow-md shadow-primary/20 transition-all hidden sm:block"
|
||||
>
|
||||
{t.cta.button1}
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="pt-16">
|
||||
{/* Hero */}
|
||||
<section className="py-20 lg:py-28 bg-white border-b border-slate-100 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 -z-10 w-1/2 h-full bg-gradient-to-l from-primary/5 to-transparent blur-3xl"></div>
|
||||
<div className="max-w-4xl mx-auto px-6">
|
||||
<div className="flex flex-wrap items-center gap-3 mb-6">
|
||||
<span className="text-xs font-extrabold text-primary uppercase tracking-widest px-3 py-1.5 rounded-full bg-primary/10 border border-primary/20">
|
||||
{t.badge}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400 font-semibold">{data.year} · {data.location}</span>
|
||||
</div>
|
||||
<p className="text-sm font-extrabold text-slate-400 uppercase tracking-widest mb-3">{data.client}</p>
|
||||
<h1 className="text-3xl lg:text-5xl font-extrabold text-slate-900 leading-[1.15] mb-6">
|
||||
{t.headline}
|
||||
</h1>
|
||||
<p className="text-lg text-slate-500 leading-relaxed max-w-2xl mb-10">{t.description}</p>
|
||||
|
||||
{/* Key stats */}
|
||||
<div className="flex flex-wrap gap-6">
|
||||
{t.results.items.map((r) => (
|
||||
<div key={r.label} className="flex flex-col">
|
||||
<span className="text-3xl font-extrabold text-slate-900">{r.value}</span>
|
||||
<span className="text-xs text-slate-400 font-semibold mt-0.5">{r.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Challenge */}
|
||||
<section className="py-16 lg:py-20 bg-slate-50">
|
||||
<div className="max-w-4xl mx-auto px-6">
|
||||
<p className="text-xs font-extrabold text-accent-violet uppercase tracking-widest mb-3">{t.challenge.label}</p>
|
||||
<h2 className="text-2xl lg:text-3xl font-extrabold text-slate-900 mb-10">{t.challenge.title}</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
{t.challenge.items.map((item, i) => (
|
||||
<div key={i} className="flex items-start gap-4 bg-white rounded-2xl p-6 border border-slate-100">
|
||||
<div className="w-8 h-8 rounded-xl bg-accent-violet/10 flex items-center justify-center shrink-0">
|
||||
<span className="material-symbols-outlined text-accent-violet text-base">priority_high</span>
|
||||
</div>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Solution */}
|
||||
<section className="py-16 lg:py-20 bg-white border-y border-slate-100">
|
||||
<div className="max-w-4xl mx-auto px-6">
|
||||
<p className="text-xs font-extrabold text-primary uppercase tracking-widest mb-3">{t.solution.label}</p>
|
||||
<h2 className="text-2xl lg:text-3xl font-extrabold text-slate-900 mb-6">{t.solution.title}</h2>
|
||||
<p className="text-slate-600 leading-relaxed text-lg mb-10 max-w-3xl">{t.solution.description}</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{t.solution.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="flex items-center gap-2 text-sm font-semibold text-slate-700 px-4 py-2.5 rounded-2xl bg-slate-100 border border-slate-200"
|
||||
>
|
||||
<span className="material-symbols-outlined text-primary text-base">check_circle</span>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Results */}
|
||||
<section className="py-16 lg:py-20 bg-slate-900 relative overflow-hidden">
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-10 left-1/4 w-80 h-80 bg-primary/10 rounded-full blur-3xl"></div>
|
||||
<div className="absolute bottom-10 right-1/4 w-60 h-60 bg-accent-violet/10 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
<div className="max-w-4xl mx-auto px-6 relative z-10">
|
||||
<p className="text-xs font-extrabold text-primary uppercase tracking-widest mb-3">{t.results.label}</p>
|
||||
<h2 className="text-2xl lg:text-3xl font-extrabold text-white mb-12">{t.results.title}</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-8">
|
||||
{t.results.items.map((r) => (
|
||||
<div key={r.label} className="bg-white/5 border border-white/10 rounded-2xl p-8 text-center">
|
||||
<div className="text-4xl font-extrabold text-primary mb-2">{r.value}</div>
|
||||
<div className="text-slate-400 text-sm">{r.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-16 lg:py-20 bg-white">
|
||||
<div className="max-w-2xl mx-auto px-6 text-center">
|
||||
<h2 className="text-2xl lg:text-3xl font-extrabold text-slate-900 mb-8">{t.cta.title}</h2>
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<a
|
||||
href={`/${lang}#contact`}
|
||||
className="inline-flex items-center gap-2 px-8 py-4 bg-primary text-slate-900 font-extrabold rounded-full shadow-xl shadow-primary/30 hover:brightness-110 transition-all"
|
||||
>
|
||||
{t.cta.button1}
|
||||
<span className="material-symbols-outlined">arrow_forward</span>
|
||||
</a>
|
||||
<a
|
||||
href={`/${lang}/${data.segmentSlug}`}
|
||||
className="inline-flex items-center gap-2 px-8 py-4 border-2 border-slate-200 text-slate-700 font-bold rounded-full hover:border-primary hover:text-primary transition-all"
|
||||
>
|
||||
{t.cta.button2}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
212
src/app/[lang]/cas-clients/page.tsx
Normal file
212
src/app/[lang]/cas-clients/page.tsx
Normal file
@ -0,0 +1,212 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { LOCALES, LOCALE_HTML_LANG, LOCALE_OG, DEFAULT_LOCALE, isLocale } from '@/i18n';
|
||||
import { getAllCasClients } from '@/data/cas-clients';
|
||||
|
||||
const SITE_URL = 'https://myinfomate.be';
|
||||
|
||||
const META_BY_LOCALE: Record<string, { title: string; description: string }> = {
|
||||
fr: {
|
||||
title: 'Cas clients MyInfoMate — Musées, offices de tourisme & patrimoine',
|
||||
description: 'Découvrez comment des lieux culturels belges utilisent MyInfoMate : Musée de la Fraise, Fort de Saint-Héribert, Visit Namur. App mobile, kiosk, offline.',
|
||||
},
|
||||
en: {
|
||||
title: 'MyInfoMate Case Studies — Museums, tourism offices & heritage',
|
||||
description: 'See how Belgian cultural venues use MyInfoMate: Musée de la Fraise, Fort de Saint-Héribert, Visit Namur. Mobile app, kiosk, offline.',
|
||||
},
|
||||
nl: {
|
||||
title: "MyInfoMate Klantcases — Musea, toerismekantoren & erfgoed",
|
||||
description: 'Ontdek hoe Belgische culturele locaties MyInfoMate gebruiken: Musée de la Fraise, Fort de Saint-Héribert, Visit Namur. Mobiele app, kiosk, offline.',
|
||||
},
|
||||
de: {
|
||||
title: 'MyInfoMate Referenzen — Museen, Tourismusbüros & Kulturerbe',
|
||||
description: 'Erfahren Sie, wie belgische Kulturstätten MyInfoMate nutzen: Musée de la Fraise, Fort de Saint-Héribert, Visit Namur. Mobile App, Kiosk, Offline.',
|
||||
},
|
||||
};
|
||||
|
||||
const HEADING_BY_LOCALE: Record<string, { h1: string; subtitle: string; badge: string }> = {
|
||||
fr: {
|
||||
badge: 'Cas clients',
|
||||
h1: 'Ils ont choisi MyInfoMate',
|
||||
subtitle: 'Musées, offices de tourisme, sites patrimoniaux — des lieux réels, des défis concrets, des résultats mesurables.',
|
||||
},
|
||||
en: {
|
||||
badge: 'Case studies',
|
||||
h1: 'They chose MyInfoMate',
|
||||
subtitle: 'Museums, tourism offices, heritage sites — real venues, concrete challenges, measurable results.',
|
||||
},
|
||||
nl: {
|
||||
badge: 'Klantcases',
|
||||
h1: 'Zij kozen voor MyInfoMate',
|
||||
subtitle: "Musea, toerismekantoren, erfgoedsites — echte locaties, concrete uitdagingen, meetbare resultaten.",
|
||||
},
|
||||
de: {
|
||||
badge: 'Referenzen',
|
||||
h1: 'Sie haben sich für MyInfoMate entschieden',
|
||||
subtitle: 'Museen, Tourismusbüros, Kulturstätten — echte Orte, konkrete Herausforderungen, messbare Ergebnisse.',
|
||||
},
|
||||
};
|
||||
|
||||
const BACK_BY_LOCALE: Record<string, string> = {
|
||||
fr: "Retour à l'accueil",
|
||||
en: 'Back to home',
|
||||
nl: 'Terug naar home',
|
||||
de: 'Zurück zur Startseite',
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return LOCALES.map((lang) => ({ lang }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ lang: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { lang } = await params;
|
||||
if (!isLocale(lang)) return {};
|
||||
const m = META_BY_LOCALE[lang];
|
||||
const languages = Object.fromEntries(LOCALES.map((l) => [LOCALE_HTML_LANG[l], `/${l}/cas-clients`]));
|
||||
return {
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
openGraph: {
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
url: `${SITE_URL}/${lang}/cas-clients`,
|
||||
siteName: 'MyInfoMate',
|
||||
locale: LOCALE_OG[lang],
|
||||
type: 'website',
|
||||
},
|
||||
alternates: {
|
||||
canonical: `/${lang}/cas-clients`,
|
||||
languages: { ...languages, 'x-default': `/${DEFAULT_LOCALE}/cas-clients` },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const SECTOR_LABEL_BY_LOCALE: Record<string, Record<string, string>> = {
|
||||
fr: { musees: 'Musée', 'parcs-naturels': 'Patrimoine', 'offices-tourisme': 'Office de Tourisme' },
|
||||
en: { musees: 'Museum', 'parcs-naturels': 'Heritage', 'offices-tourisme': 'Tourism Office' },
|
||||
nl: { musees: 'Museum', 'parcs-naturels': 'Erfgoed', 'offices-tourisme': 'Toeristenbureau' },
|
||||
de: { musees: 'Museum', 'parcs-naturels': 'Kulturerbe', 'offices-tourisme': 'Tourismusbüro' },
|
||||
};
|
||||
|
||||
export default async function CasClientsListPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ lang: string }>;
|
||||
}) {
|
||||
const { lang } = await params;
|
||||
if (!isLocale(lang)) notFound();
|
||||
|
||||
const clients = getAllCasClients();
|
||||
const h = HEADING_BY_LOCALE[lang];
|
||||
const back = BACK_BY_LOCALE[lang];
|
||||
const sectorLabels = SECTOR_LABEL_BY_LOCALE[lang];
|
||||
|
||||
return (
|
||||
<main className="bg-slate-50 text-slate-900 min-h-screen font-display">
|
||||
{/* Nav */}
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200">
|
||||
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center gap-4">
|
||||
<a href={`/${lang}`} className="flex items-center gap-2 text-sm font-bold text-slate-600 hover:text-primary transition-colors">
|
||||
<span className="material-symbols-outlined text-base">arrow_back</span>
|
||||
{back}
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="pt-16">
|
||||
{/* Hero */}
|
||||
<section className="py-20 lg:py-28 bg-white border-b border-slate-100">
|
||||
<div className="max-w-4xl mx-auto px-6 text-center">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 border border-primary/20 text-slate-700 text-xs font-bold uppercase tracking-wider mb-6">
|
||||
<span className="w-2 h-2 rounded-full bg-primary"></span>
|
||||
{h.badge}
|
||||
</div>
|
||||
<h1 className="text-4xl lg:text-6xl font-extrabold text-slate-900 leading-[1.1] mb-6">
|
||||
{h.h1}
|
||||
</h1>
|
||||
<p className="text-lg text-slate-500 max-w-2xl mx-auto">{h.subtitle}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Case studies grid */}
|
||||
<section className="py-20">
|
||||
<div className="max-w-5xl mx-auto px-6">
|
||||
<div className="flex flex-col gap-8">
|
||||
{clients.map((client) => {
|
||||
const t = client.translations[lang];
|
||||
const sector = sectorLabels[client.segmentSlug] ?? client.segmentSlug;
|
||||
return (
|
||||
<a
|
||||
key={client.slug}
|
||||
href={`/${lang}/cas-clients/${client.slug}`}
|
||||
className="group flex flex-col lg:flex-row gap-8 bg-white rounded-3xl border border-slate-100 p-8 lg:p-10 hover:border-primary/30 hover:shadow-xl hover:shadow-primary/5 transition-all"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<span className="text-xs font-extrabold text-primary uppercase tracking-widest px-3 py-1 rounded-full bg-primary/10 border border-primary/20">
|
||||
{sector}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400 font-semibold">{client.year} · {client.location}</span>
|
||||
</div>
|
||||
<h2 className="text-2xl font-extrabold text-slate-900 mb-3 group-hover:text-primary transition-colors">
|
||||
{client.client}
|
||||
</h2>
|
||||
<p className="text-slate-500 leading-relaxed mb-6">{t.description}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{t.solution.tags.slice(0, 4).map((tag) => (
|
||||
<span key={tag} className="text-xs font-semibold text-slate-500 px-3 py-1 rounded-full bg-slate-100">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex lg:flex-col items-center lg:items-end justify-between lg:justify-center gap-4 lg:w-40 shrink-0">
|
||||
<div className="hidden lg:flex flex-col gap-4 w-full">
|
||||
{t.results.items.map((r) => (
|
||||
<div key={r.label} className="text-right">
|
||||
<div className="text-2xl font-extrabold text-slate-900">{r.value}</div>
|
||||
<div className="text-xs text-slate-400">{r.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span className="flex items-center gap-1 text-xs font-extrabold text-primary uppercase tracking-widest group-hover:gap-2 transition-all whitespace-nowrap">
|
||||
Lire le cas
|
||||
<span className="material-symbols-outlined text-sm">arrow_forward</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer CTA */}
|
||||
<section className="py-16 bg-slate-900 text-center">
|
||||
<div className="max-w-2xl mx-auto px-6">
|
||||
<h2 className="text-2xl lg:text-3xl font-extrabold text-white mb-4">
|
||||
{lang === 'fr' ? 'Votre lieu sera le prochain ?' :
|
||||
lang === 'en' ? 'Will your venue be next?' :
|
||||
lang === 'nl' ? 'Is uw locatie de volgende?' :
|
||||
'Wird Ihr Ort der nächste sein?'}
|
||||
</h2>
|
||||
<a
|
||||
href={`/${lang}#contact`}
|
||||
className="inline-flex items-center gap-2 mt-6 px-8 py-4 bg-primary text-slate-900 font-extrabold rounded-full shadow-xl shadow-primary/30 hover:brightness-110 transition-all"
|
||||
>
|
||||
{lang === 'fr' ? 'Demander une démo gratuite' :
|
||||
lang === 'en' ? 'Request a free demo' :
|
||||
lang === 'nl' ? 'Gratis demo aanvragen' :
|
||||
'Kostenlose Demo anfordern'}
|
||||
<span className="material-symbols-outlined">arrow_forward</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
172
src/app/[lang]/opengraph-image.tsx
Normal file
172
src/app/[lang]/opengraph-image.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { isLocale } from '@/i18n';
|
||||
|
||||
export const runtime = 'edge';
|
||||
export const alt = 'MyInfoMate — Digital visitor app for museums, parks & venues';
|
||||
export const size = { width: 1200, height: 630 };
|
||||
export const contentType = 'image/png';
|
||||
|
||||
const TAGLINES: Record<string, string> = {
|
||||
fr: 'Guide visiteur digital pour musées, parcs & événements',
|
||||
en: 'Digital visitor guide for museums, parks & events',
|
||||
nl: 'Digitale bezoekersgids voor musea, parken & evenementen',
|
||||
de: 'Digitaler Besucherguide für Museen, Parks & Events',
|
||||
};
|
||||
|
||||
const FEATURES: Record<string, string[]> = {
|
||||
fr: ['Carte interactive', 'Audioguide', 'Quiz & Escape', 'Assistant IA'],
|
||||
en: ['Interactive map', 'Audio guide', 'Quiz & Escape', 'AI assistant'],
|
||||
nl: ['Interactieve kaart', 'Audiogids', 'Quiz & Escape', 'AI-assistent'],
|
||||
de: ['Interaktive Karte', 'Audioguide', 'Quiz & Escape', 'KI-Assistent'],
|
||||
};
|
||||
|
||||
export default function Image({ params }: { params: { lang: string } }) {
|
||||
const lang = isLocale(params.lang) ? params.lang : 'fr';
|
||||
const tagline = TAGLINES[lang];
|
||||
const features = FEATURES[lang];
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 60%, #0f172a 100%)',
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Cyan glow top-left */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-120px',
|
||||
left: '-80px',
|
||||
width: '600px',
|
||||
height: '600px',
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(13,242,223,0.18) 0%, transparent 65%)',
|
||||
}}
|
||||
/>
|
||||
{/* Purple glow bottom-right */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '-160px',
|
||||
right: '-80px',
|
||||
width: '700px',
|
||||
height: '700px',
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(139,92,246,0.14) 0%, transparent 65%)',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
padding: '0 80px',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{/* Badge */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
padding: '10px 24px',
|
||||
borderRadius: '999px',
|
||||
background: 'rgba(13,242,223,0.1)',
|
||||
border: '1px solid rgba(13,242,223,0.3)',
|
||||
marginBottom: '36px',
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '10px', height: '10px', borderRadius: '50%', background: '#0df2df' }} />
|
||||
<span
|
||||
style={{
|
||||
color: '#0df2df',
|
||||
fontSize: '18px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
SaaS · No-code · White Label
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Product name */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: '88px',
|
||||
fontWeight: 900,
|
||||
color: '#ffffff',
|
||||
lineHeight: 1,
|
||||
marginBottom: '28px',
|
||||
letterSpacing: '-0.03em',
|
||||
}}
|
||||
>
|
||||
MyInfoMate
|
||||
</div>
|
||||
|
||||
{/* Tagline */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: '30px',
|
||||
color: '#94a3b8',
|
||||
lineHeight: 1.4,
|
||||
maxWidth: '820px',
|
||||
marginBottom: '52px',
|
||||
}}
|
||||
>
|
||||
{tagline}
|
||||
</div>
|
||||
|
||||
{/* Feature pills */}
|
||||
<div style={{ display: 'flex', gap: '16px' }}>
|
||||
{features.map((f) => (
|
||||
<div
|
||||
key={f}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
borderRadius: '999px',
|
||||
background: 'rgba(255,255,255,0.07)',
|
||||
border: '1px solid rgba(255,255,255,0.12)',
|
||||
color: '#e2e8f0',
|
||||
fontSize: '20px',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{f}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom URL */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '36px',
|
||||
color: '#475569',
|
||||
fontSize: '22px',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.02em',
|
||||
}}
|
||||
>
|
||||
myinfomate.be
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{ ...size }
|
||||
);
|
||||
}
|
||||
@ -7,20 +7,20 @@ const SITE_URL = 'https://myinfomate.be';
|
||||
|
||||
const META_BY_LOCALE: Record<Locale, { title: string; description: string }> = {
|
||||
fr: {
|
||||
title: "MyInfoMate | La technologie au service de l'expérience visiteur",
|
||||
description: "La solution SaaS pour digitaliser l'expérience de vos visiteurs. Créativité et technologie au service de l'expérience visiteur.",
|
||||
title: "MyInfoMate — Application guide visiteur pour musées & lieux",
|
||||
description: "Créez votre app guide visiteur sans code. Cartes interactives, audioguide, quiz, escape game et assistant IA pour musées, parcs et offices de tourisme. Dès €39/mois.",
|
||||
},
|
||||
en: {
|
||||
title: "MyInfoMate | Technology serving the visitor experience",
|
||||
description: "The SaaS solution to digitalize your visitor experience. Creativity and technology at the service of the visitor experience.",
|
||||
title: "MyInfoMate — Digital Visitor App for Museums & Venues",
|
||||
description: "Build your visitor guide app with no code. Interactive maps, audio guides, quizzes, escape game and AI assistant for museums, parks and tourism offices. From €39/month.",
|
||||
},
|
||||
nl: {
|
||||
title: "MyInfoMate | Technologie ten dienste van de bezoekerservaring",
|
||||
description: "De SaaS-oplossing om uw bezoekerservaring te digitaliseren. Creativiteit en technologie ten dienste van de bezoekerservaring.",
|
||||
title: "MyInfoMate — Digitale bezoekersgids voor musea & locaties",
|
||||
description: "Maak uw bezoekersgids-app zonder code. Interactieve kaarten, audiogids, quiz, escape game en AI-assistent voor musea, parken en toerismekantoren. Vanaf €39/maand.",
|
||||
},
|
||||
de: {
|
||||
title: "MyInfoMate | Technologie im Dienste des Besuchererlebnisses",
|
||||
description: "Die SaaS-Lösung zur Digitalisierung Ihres Besuchererlebnisses. Kreativität und Technologie im Dienste des Besuchererlebnisses.",
|
||||
title: "MyInfoMate — Digitaler Besucherguide für Museen & Orte",
|
||||
description: "Erstellen Sie Ihre Besucher-App ohne Code. Interaktive Karten, Audioguide, Quiz, Escape Game und KI-Assistent für Museen, Parks und Tourismusbüros. Ab €39/Monat.",
|
||||
},
|
||||
};
|
||||
|
||||
@ -50,7 +50,6 @@ export async function generateMetadata({
|
||||
locale: LOCALE_OG[lang],
|
||||
alternateLocale: LOCALES.filter((l) => l !== lang).map((l) => LOCALE_OG[l]),
|
||||
type: 'website',
|
||||
images: [{ url: '/myinfomate-logo.png' }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
@ -67,5 +66,58 @@ export async function generateMetadata({
|
||||
export default async function HomePage({ params }: { params: Promise<{ lang: string }> }) {
|
||||
const { lang } = await params;
|
||||
if (!isLocale(lang)) notFound();
|
||||
return <HomeClient lang={lang} />;
|
||||
|
||||
const organizationSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
name: 'Unov',
|
||||
url: SITE_URL,
|
||||
logo: `${SITE_URL}/myinfomate-logo.png`,
|
||||
contactPoint: {
|
||||
'@type': 'ContactPoint',
|
||||
email: 'contact@unov.be',
|
||||
contactType: 'customer support',
|
||||
},
|
||||
};
|
||||
|
||||
const websiteSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
name: 'MyInfoMate',
|
||||
url: SITE_URL,
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: `${SITE_URL}/${lang}`,
|
||||
},
|
||||
};
|
||||
|
||||
const softwareSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'MyInfoMate',
|
||||
applicationCategory: 'BusinessApplication',
|
||||
operatingSystem: 'iOS, Android, Web',
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '39',
|
||||
priceCurrency: 'EUR',
|
||||
priceSpecification: {
|
||||
'@type': 'UnitPriceSpecification',
|
||||
price: '39',
|
||||
priceCurrency: 'EUR',
|
||||
unitText: 'MONTH',
|
||||
},
|
||||
},
|
||||
description: META_BY_LOCALE[lang as Locale].description,
|
||||
url: `${SITE_URL}/${lang}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema) }} />
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema) }} />
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(softwareSchema) }} />
|
||||
<HomeClient lang={lang} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,6 +8,9 @@
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
body {
|
||||
background-color: #f8fafc;
|
||||
color: #0f172a;
|
||||
|
||||
@ -15,20 +15,20 @@ const SITE_URL = "https://myinfomate.be";
|
||||
|
||||
const META_BY_LOCALE: Record<Locale, { title: string; description: string }> = {
|
||||
fr: {
|
||||
title: "MyInfoMate | La technologie au service de l'expérience visiteur",
|
||||
description: "La solution SaaS pour digitaliser l'expérience de vos visiteurs. Créativité et technologie au service de l'expérience visiteur.",
|
||||
title: "MyInfoMate — Application guide visiteur pour musées & lieux",
|
||||
description: "Créez votre app guide visiteur sans code. Cartes interactives, audioguide, quiz, escape game et assistant IA pour musées, parcs et offices de tourisme. Dès €39/mois.",
|
||||
},
|
||||
en: {
|
||||
title: "MyInfoMate | Technology serving the visitor experience",
|
||||
description: "The SaaS solution to digitalize your visitor experience. Creativity and technology at the service of the visitor experience.",
|
||||
title: "MyInfoMate — Digital Visitor App for Museums & Venues",
|
||||
description: "Build your visitor guide app with no code. Interactive maps, audio guides, quizzes, escape game and AI assistant for museums, parks and tourism offices. From €39/month.",
|
||||
},
|
||||
nl: {
|
||||
title: "MyInfoMate | Technologie ten dienste van de bezoekerservaring",
|
||||
description: "De SaaS-oplossing om uw bezoekerservaring te digitaliseren. Creativiteit en technologie ten dienste van de bezoekerservaring.",
|
||||
title: "MyInfoMate — Digitale bezoekersgids voor musea & locaties",
|
||||
description: "Maak uw bezoekersgids-app zonder code. Interactieve kaarten, audiogids, quiz, escape game en AI-assistent voor musea, parken en toerismekantoren. Vanaf €39/maand.",
|
||||
},
|
||||
de: {
|
||||
title: "MyInfoMate | Technologie im Dienste des Besuchererlebnisses",
|
||||
description: "Die SaaS-Lösung zur Digitalisierung Ihres Besuchererlebnisses. Kreativität und Technologie im Dienste des Besuchererlebnisses.",
|
||||
title: "MyInfoMate — Digitaler Besucherguide für Museen & Orte",
|
||||
description: "Erstellen Sie Ihre Besucher-App ohne Code. Interaktive Karten, Audioguide, Quiz, Escape Game und KI-Assistent für Museen, Parks und Tourismusbüros. Ab €39/Monat.",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { MetadataRoute } from "next";
|
||||
import { LOCALES, LOCALE_HTML_LANG, DEFAULT_LOCALE } from "@/i18n";
|
||||
import { getAllSegmentSlugs } from "@/data/segments";
|
||||
import { getAllCasClientSlugs } from "@/data/cas-clients";
|
||||
|
||||
const SITE_URL = "https://myinfomate.be";
|
||||
|
||||
@ -39,6 +40,28 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
||||
}
|
||||
}
|
||||
|
||||
for (const lang of LOCALES) {
|
||||
entries.push({
|
||||
url: `${SITE_URL}/${lang}/cas-clients`,
|
||||
lastModified: now,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.8,
|
||||
alternates: { languages: alternates((l) => `/${l}/cas-clients`) },
|
||||
});
|
||||
}
|
||||
|
||||
for (const slug of getAllCasClientSlugs()) {
|
||||
for (const lang of LOCALES) {
|
||||
entries.push({
|
||||
url: `${SITE_URL}/${lang}/cas-clients/${slug}`,
|
||||
lastModified: now,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.75,
|
||||
alternates: { languages: alternates((l) => `/${l}/cas-clients/${slug}`) },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
entries.push(
|
||||
{
|
||||
url: `${SITE_URL}/mentions-legales`,
|
||||
|
||||
124
src/components/ContactForm.tsx
Normal file
124
src/components/ContactForm.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import translations, { Language } from '@/data/translations';
|
||||
|
||||
export default function ContactForm({ lang }: { lang: Language }) {
|
||||
const t = translations[lang].contact;
|
||||
const [formData, setFormData] = useState({ firstName: '', lastName: '', email: '', message: '' });
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus('loading');
|
||||
try {
|
||||
const response = await fetch('https://formspree.io/f/xbdaajlo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
if (response.ok) {
|
||||
setStatus('success');
|
||||
setFormData({ firstName: '', lastName: '', email: '', message: '' });
|
||||
} else {
|
||||
setStatus('error');
|
||||
}
|
||||
} catch {
|
||||
setStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
if (status === 'success') {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<div className="w-20 h-20 bg-primary/20 text-primary rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<span className="material-symbols-outlined text-5xl">task_alt</span>
|
||||
</div>
|
||||
<h3 className="text-3xl font-extrabold text-slate-900 mb-4">{t.successTitle}</h3>
|
||||
<p className="text-slate-600 mb-8">{t.successDesc}</p>
|
||||
<button onClick={() => setStatus('idle')} className="text-primary font-bold hover:underline">
|
||||
{t.successButton}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{status === 'error' && (
|
||||
<div className="p-4 bg-red-50 text-red-600 rounded-xl text-sm font-medium border border-red-100 flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-sm">error</span>
|
||||
{t.errorMsg}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.firstNameLabel}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="firstName"
|
||||
required
|
||||
value={formData.firstName}
|
||||
onChange={handleChange}
|
||||
className="w-full px-6 py-4 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all"
|
||||
placeholder={t.firstNamePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.lastNameLabel}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="lastName"
|
||||
required
|
||||
value={formData.lastName}
|
||||
onChange={handleChange}
|
||||
className="w-full px-6 py-4 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all"
|
||||
placeholder={t.lastNamePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.emailLabel}</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className="w-full px-6 py-4 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all"
|
||||
placeholder={t.emailPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.messageLabel}</label>
|
||||
<textarea
|
||||
name="message"
|
||||
rows={4}
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
className="w-full px-6 py-4 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all resize-none"
|
||||
placeholder={t.messagePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === 'loading'}
|
||||
className={`w-full py-5 bg-slate-900 text-white font-extrabold rounded-2xl shadow-xl hover:bg-slate-800 transition-all flex items-center justify-center gap-3 group ${status === 'loading' ? 'opacity-70 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{status === 'loading' ? (
|
||||
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></span>
|
||||
) : (
|
||||
<>
|
||||
{t.submitButton}
|
||||
<span className="material-symbols-outlined group-hover:translate-x-1 transition-transform">send</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
197
src/components/ModulesSection.tsx
Normal file
197
src/components/ModulesSection.tsx
Normal file
@ -0,0 +1,197 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import translations, { Language } from '@/data/translations';
|
||||
import Reveal from '@/components/Reveal';
|
||||
|
||||
const MODULE_META = [
|
||||
{ id: 'map', icon: 'map' },
|
||||
{ id: 'quiz', icon: 'sports_esports' },
|
||||
{ id: 'video', icon: 'videocam' },
|
||||
{ id: 'article', icon: 'article' },
|
||||
{ id: 'agenda', icon: 'calendar_month' },
|
||||
{ id: 'offline', icon: 'download_for_offline' },
|
||||
{ id: 'web', icon: 'public' },
|
||||
{ id: 'stats', icon: 'monitoring' },
|
||||
{ id: 'escapegame', icon: 'explore' },
|
||||
];
|
||||
|
||||
export default function ModulesSection({ lang }: { lang: Language }) {
|
||||
const [activeModule, setActiveModule] = useState(0);
|
||||
const t = translations[lang];
|
||||
|
||||
const modules = MODULE_META.map((meta, i) => ({
|
||||
...meta,
|
||||
title: t.modules.items[i].title,
|
||||
description: t.modules.items[i].description,
|
||||
value: t.modules.items[i].value,
|
||||
}));
|
||||
|
||||
return (
|
||||
<section className="py-24 bg-white" id="features">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<Reveal className="text-center mb-16">
|
||||
<h2 className="text-rose-600 font-extrabold uppercase tracking-widest text-sm mb-4">{t.modules.sectionLabel}</h2>
|
||||
<h3 className="text-3xl lg:text-5xl font-extrabold text-slate-900 leading-tight mb-6">{t.modules.sectionTitle}</h3>
|
||||
<p className="text-lg text-slate-600 max-w-2xl mx-auto">{t.modules.sectionDesc}</p>
|
||||
</Reveal>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-8 lg:gap-16 items-stretch">
|
||||
{/* Sidebar (Tabs) */}
|
||||
<Reveal delay={100} className="w-full lg:w-1/3 relative">
|
||||
<div className="lg:hidden absolute left-0 top-0 bottom-4 w-12 bg-gradient-to-r from-white to-transparent z-10 pointer-events-none"></div>
|
||||
<div className="lg:hidden absolute right-0 top-0 bottom-4 w-12 bg-gradient-to-l from-white to-transparent z-10 pointer-events-none"></div>
|
||||
<div className="flex lg:flex-col gap-3 overflow-x-auto pb-4 lg:pb-0 scrollbar-hide snap-x">
|
||||
{modules.map((module, index) => (
|
||||
<button
|
||||
key={module.id}
|
||||
onClick={() => setActiveModule(index)}
|
||||
onMouseEnter={() => setActiveModule(index)}
|
||||
className={`flex items-center gap-3 lg:gap-4 px-5 py-4 lg:px-6 lg:py-5 rounded-2xl border-2 transition-all text-left whitespace-nowrap lg:whitespace-normal snap-center flex-shrink-0 lg:flex-shrink-1 w-auto lg:w-full ${activeModule === index
|
||||
? 'bg-rose-50 border-rose-200 text-rose-600 shadow-sm'
|
||||
: 'bg-white border-slate-100 text-slate-500 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<span className={`material-symbols-outlined text-xl lg:text-2xl ${activeModule === index ? 'text-rose-600' : 'text-slate-400'}`}>
|
||||
{module.icon}
|
||||
</span>
|
||||
<span className="font-bold text-xs lg:text-base">{module.title}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Main Display */}
|
||||
<Reveal delay={200} className="flex-1 w-full lg:flex lg:flex-col">
|
||||
{/* Mobile: single centered card */}
|
||||
<div className="lg:hidden bg-slate-50 rounded-[3rem] p-8 border border-slate-100 overflow-hidden">
|
||||
<div className="text-center mb-6">
|
||||
<div className="inline-flex p-3 rounded-2xl bg-white shadow-sm text-rose-600 mb-4 font-bold gap-2 items-center">
|
||||
<span className="material-symbols-outlined">{modules[activeModule].icon}</span>
|
||||
<span className="text-xs uppercase tracking-wider">{modules[activeModule].title}</span>
|
||||
</div>
|
||||
<h3 className="text-2xl font-black text-slate-900 mb-4 leading-tight">{modules[activeModule].title}</h3>
|
||||
<p className="text-slate-600 leading-relaxed mb-6">{modules[activeModule].description}</p>
|
||||
<div className="p-4 bg-rose-600/5 border border-rose-100 rounded-2xl">
|
||||
<p className="text-rose-600 font-bold text-sm flex items-center justify-center gap-2">
|
||||
<span className="material-symbols-outlined text-base">verified</span>
|
||||
{modules[activeModule].value}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<div className="relative w-[200px] h-[420px] bg-slate-900 rounded-[3rem] border-[8px] border-slate-800 shadow-2xl overflow-hidden">
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-24 h-5 bg-slate-800 rounded-b-2xl z-20"></div>
|
||||
<div className="absolute inset-[2px] bg-white rounded-[2.25rem] overflow-hidden">
|
||||
<div className="h-full flex flex-col pt-7">
|
||||
<header className="h-12 border-b border-slate-100 flex items-center px-3 bg-white/80 sticky top-0 z-10">
|
||||
<div className="w-7 h-7 rounded-lg bg-rose-100 flex items-center justify-center text-rose-600">
|
||||
<span className="material-symbols-outlined text-base">{modules[activeModule].icon}</span>
|
||||
</div>
|
||||
<span className="ml-2 font-bold text-xs text-slate-900 truncate">{modules[activeModule].title}</span>
|
||||
</header>
|
||||
<div className="p-3 flex-1 space-y-3">
|
||||
<div className="h-32 rounded-xl bg-slate-100 flex items-center justify-center relative overflow-hidden">
|
||||
<span className="material-symbols-outlined text-4xl text-slate-300">{modules[activeModule].icon}</span>
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-rose-500/10 to-transparent"></div>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-slate-100 rounded-full"></div>
|
||||
<div className="h-2 w-2/3 bg-slate-100 rounded-full"></div>
|
||||
<div className="h-14 w-full bg-slate-50 border border-slate-100 rounded-xl p-2 flex gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-rose-200 shrink-0"></div>
|
||||
<div className="space-y-1 flex-1">
|
||||
<div className="h-1.5 w-full bg-slate-200 rounded-full"></div>
|
||||
<div className="h-1.5 w-1/2 bg-slate-200 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="h-8 bg-slate-100 rounded-lg"></div>
|
||||
<div className="h-8 bg-rose-600 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop: 3D phone stack */}
|
||||
<div className="hidden lg:flex flex-1 bg-slate-50 rounded-[3rem] p-10 border border-slate-100 gap-10 items-center">
|
||||
<div className="flex-1 relative z-10 text-left">
|
||||
<div className="inline-flex p-3 rounded-2xl bg-white shadow-sm text-rose-600 mb-5 font-bold gap-2 items-center">
|
||||
<span className="material-symbols-outlined">{modules[activeModule].icon}</span>
|
||||
<span className="text-xs uppercase tracking-wider">{modules[activeModule].title}</span>
|
||||
</div>
|
||||
<h3 className="text-3xl font-black text-slate-900 mb-5 leading-tight">{modules[activeModule].title}</h3>
|
||||
<p className="text-slate-600 text-base leading-relaxed mb-6">{modules[activeModule].description}</p>
|
||||
<div className="p-5 bg-rose-600/5 border border-rose-100 rounded-2xl">
|
||||
<p className="text-rose-600 font-bold text-sm flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-base">verified</span>
|
||||
{t.modules.valueLabel} {modules[activeModule].value}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 relative" style={{ width: '280px', height: '480px' }}>
|
||||
<div style={{ position: 'absolute', inset: 0, transform: 'perspective(900px) rotateX(6deg) rotateY(-15deg)' }}>
|
||||
{modules.map((module, index) => {
|
||||
const deckPos = (index - activeModule + modules.length) % modules.length;
|
||||
const isActive = deckPos === 0;
|
||||
return (
|
||||
<div
|
||||
key={module.id}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
transformOrigin: 'top left',
|
||||
transform: `translateX(${deckPos * 12}px) scale(${1 - deckPos * 0.06})`,
|
||||
zIndex: modules.length - deckPos,
|
||||
opacity: isActive ? 1 : Math.max(0.06, 0.55 - deckPos * 0.1),
|
||||
transition: 'transform 0.45s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.45s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
>
|
||||
<div className={`relative w-[240px] h-[480px] rounded-[3rem] border-[8px] overflow-hidden ${isActive ? 'bg-slate-900 border-slate-800 shadow-2xl shadow-rose-500/20' : 'bg-slate-800 border-slate-700 shadow-xl'}`}>
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-28 h-6 bg-slate-800 rounded-b-2xl z-20"></div>
|
||||
<div className="absolute inset-[2px] bg-white rounded-[2.25rem] overflow-hidden">
|
||||
<div className="h-full flex flex-col pt-8">
|
||||
<header className="h-14 border-b border-slate-100 flex items-center px-4 bg-white/80 backdrop-blur-sm sticky top-0 z-10">
|
||||
<div className="w-8 h-8 rounded-lg bg-rose-100 flex items-center justify-center text-rose-600">
|
||||
<span className="material-symbols-outlined text-lg">{module.icon}</span>
|
||||
</div>
|
||||
<span className="ml-3 font-bold text-xs text-slate-900 truncate">{module.title}</span>
|
||||
</header>
|
||||
<div className="p-4 flex-1 space-y-4">
|
||||
<div className="h-36 rounded-2xl bg-slate-100 flex items-center justify-center relative overflow-hidden">
|
||||
<span className="material-symbols-outlined text-5xl text-slate-300">{module.icon}</span>
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-rose-500/10 to-transparent"></div>
|
||||
</div>
|
||||
<div className="h-3 w-full bg-slate-100 rounded-full"></div>
|
||||
<div className="h-3 w-2/3 bg-slate-100 rounded-full"></div>
|
||||
<div className="h-20 w-full bg-slate-50 border border-slate-100 rounded-xl p-3 flex gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-rose-200 shrink-0"></div>
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="h-2 w-full bg-slate-200 rounded-full"></div>
|
||||
<div className="h-2 w-1/2 bg-slate-200 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="h-10 bg-slate-100 rounded-xl"></div>
|
||||
<div className="h-10 bg-rose-600 rounded-xl"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
202
src/components/NavBar.tsx
Normal file
202
src/components/NavBar.tsx
Normal file
@ -0,0 +1,202 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { resolveImage } from '@/data/stitch-images';
|
||||
import translations, { Language } from '@/data/translations';
|
||||
|
||||
const LANGUAGES: { code: Language; label: string; name: string }[] = [
|
||||
{ code: 'fr', label: 'FR', name: 'Français' },
|
||||
{ code: 'en', label: 'EN', name: 'English' },
|
||||
{ code: 'nl', label: 'NL', name: 'Nederlands' },
|
||||
{ code: 'de', label: 'DE', name: 'Deutsch' },
|
||||
];
|
||||
|
||||
export default function NavBar({ lang }: { lang: Language }) {
|
||||
const router = useRouter();
|
||||
const t = translations[lang];
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isLangOpen, setIsLangOpen] = useState(false);
|
||||
const [navVisible, setNavVisible] = useState(true);
|
||||
const langDropdownRef = useRef<HTMLDivElement>(null);
|
||||
const lastScrollY = useRef(0);
|
||||
|
||||
const setLang = (next: Language) => {
|
||||
if (next !== lang) router.push(`/${next}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const currentY = window.scrollY;
|
||||
setNavVisible(currentY < lastScrollY.current || currentY < 10);
|
||||
lastScrollY.current = currentY;
|
||||
};
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = isMenuOpen ? 'hidden' : 'auto';
|
||||
return () => { document.body.style.overflow = 'auto'; };
|
||||
}, [isMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (langDropdownRef.current && !langDropdownRef.current.contains(e.target as Node)) {
|
||||
setIsLangOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className={`fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200 transition-transform duration-300 ${navVisible ? 'translate-y-0' : '-translate-y-full'}`}>
|
||||
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
|
||||
<a className="flex items-center gap-3 shrink-0" href="#">
|
||||
<Image
|
||||
src={resolveImage("{{DATA:IMAGE:IMAGE_14}}")}
|
||||
alt="MyInfoMate Logo"
|
||||
height={40}
|
||||
width={42}
|
||||
className="h-10 w-auto object-contain"
|
||||
/>
|
||||
<span className="text-xl font-extrabold tracking-tight text-slate-900 hidden lg:block">MyInfoMate</span>
|
||||
</a>
|
||||
<nav className="hidden lg:flex items-center gap-8">
|
||||
<a className="text-sm font-semibold text-slate-600 hover:text-primary transition-colors w-[80px] text-center" href="#deployment-modes">{t.nav.solution}</a>
|
||||
<a className="text-sm font-semibold text-slate-600 hover:text-primary transition-colors w-[128px] text-center" href="#features">{t.nav.features}</a>
|
||||
<a className="text-sm font-semibold text-slate-600 hover:text-primary transition-colors flex items-center gap-1" href="#ai-assistant">
|
||||
<span className="material-symbols-outlined text-primary" style={{ fontSize: '15px' }}>auto_awesome</span>
|
||||
{t.nav.ai}
|
||||
</a>
|
||||
<a className="text-sm font-semibold text-slate-600 hover:text-primary transition-colors w-[88px] text-center" href="#audience">{t.nav.audience}</a>
|
||||
<a className="text-sm font-semibold text-slate-600 hover:text-primary transition-colors w-[64px] text-center" href="#pricing">{t.pricing.sectionLabel}</a>
|
||||
</nav>
|
||||
<div className="flex items-center gap-4">
|
||||
<a
|
||||
href="https://manager.myinfomate.be"
|
||||
className="hidden lg:block text-sm font-bold text-slate-900 hover:text-primary px-4 py-2 transition-colors"
|
||||
>
|
||||
{t.nav.login}
|
||||
</a>
|
||||
<a
|
||||
href="#contact"
|
||||
className="bg-primary hover:brightness-110 text-slate-900 font-extrabold text-sm px-6 py-3 rounded-full shadow-lg shadow-primary/20 transition-all active:scale-95 hidden sm:block"
|
||||
>
|
||||
{t.nav.demo}
|
||||
</a>
|
||||
<div ref={langDropdownRef} className="relative hidden lg:block">
|
||||
<button
|
||||
onClick={() => setIsLangOpen(!isLangOpen)}
|
||||
className="flex items-center gap-1.5 px-3 py-2 rounded-xl text-sm font-bold text-slate-500 hover:text-slate-900 hover:bg-slate-100 transition-all"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px] text-primary">language</span>
|
||||
<span>{lang.toUpperCase()}</span>
|
||||
<span className={`material-symbols-outlined text-[16px] transition-transform duration-200 ${isLangOpen ? 'rotate-180' : ''}`}>keyboard_arrow_down</span>
|
||||
</button>
|
||||
{isLangOpen && (
|
||||
<div className="absolute top-full right-0 mt-2 bg-white border border-slate-100 rounded-2xl shadow-xl overflow-hidden py-1.5 z-50">
|
||||
{LANGUAGES.map((l) => (
|
||||
<button
|
||||
key={l.code}
|
||||
onClick={() => { setLang(l.code); setIsLangOpen(false); }}
|
||||
className={`w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors ${lang === l.code ? 'text-primary bg-primary/5' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
|
||||
>
|
||||
<span className="text-sm font-black w-7 shrink-0">{l.label}</span>
|
||||
<span className="text-sm text-slate-400 whitespace-nowrap">{l.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
className="lg:hidden w-10 h-10 flex items-center justify-center text-slate-600 hover:text-primary transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-3xl">
|
||||
{isMenuOpen ? 'close' : 'menu'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Mobile Menu Backdrop */}
|
||||
<div
|
||||
className={`lg:hidden fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-[150] transition-opacity duration-300 ${isMenuOpen ? 'opacity-100 visible' : 'opacity-0 invisible pointer-events-none'}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Mobile Menu Sidebar */}
|
||||
<div className={`lg:hidden fixed inset-y-0 right-0 w-[300px] h-screen bg-white z-[200] shadow-2xl transition-transform duration-500 ease-in-out transform ${isMenuOpen ? 'translate-x-0' : 'translate-x-full'}`}>
|
||||
<div className="flex flex-col h-full bg-white">
|
||||
<div className="flex justify-end p-6 border-b border-slate-50">
|
||||
<button
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
className="w-10 h-10 flex items-center justify-center text-slate-400 hover:text-slate-900 transition-colors"
|
||||
aria-label="Fermer le menu"
|
||||
>
|
||||
<span className="material-symbols-outlined text-3xl font-bold text-slate-900">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<nav className="flex-1 flex flex-col p-8 gap-8 overflow-y-auto scrollbar-hide">
|
||||
<div className="flex flex-col gap-6">
|
||||
{[
|
||||
{ label: t.mobileMenu.home, href: '#' },
|
||||
{ label: t.mobileMenu.solution, href: '#deployment-modes' },
|
||||
{ label: t.mobileMenu.features, href: '#features' },
|
||||
{ label: t.mobileMenu.ai, href: '#ai-assistant' },
|
||||
{ label: t.mobileMenu.audience, href: '#audience' },
|
||||
{ label: t.pricing.sectionLabel.toUpperCase(), href: '#pricing' },
|
||||
{ label: t.mobileMenu.contact, href: '#contact' },
|
||||
].map((item) => (
|
||||
<a
|
||||
key={item.label}
|
||||
className="text-sm font-bold tracking-[0.2em] text-slate-900 hover:text-primary transition-colors py-3 border-b border-slate-50 flex items-center justify-between"
|
||||
href={item.href}
|
||||
onClick={() => {
|
||||
if (item.href === '#') window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{LANGUAGES.map((l) => (
|
||||
<button
|
||||
key={l.code}
|
||||
onClick={() => setLang(l.code)}
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-xl border-2 transition-all text-left ${lang === l.code ? 'bg-primary/5 border-primary text-primary' : 'border-slate-100 text-slate-600 hover:border-slate-300'}`}
|
||||
>
|
||||
<span className="text-xs font-black w-6">{l.label}</span>
|
||||
<span className="text-sm font-medium">{l.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-auto py-8 flex flex-col gap-4">
|
||||
<a
|
||||
href="https://manager.myinfomate.be"
|
||||
className="w-full text-center py-4 text-xs font-black tracking-widest text-slate-800 border-2 border-slate-100 rounded-xl hover:border-primary transition-all"
|
||||
>
|
||||
{t.mobileMenu.login}
|
||||
</a>
|
||||
<a
|
||||
href="#contact"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
className="w-full py-4 bg-primary text-slate-900 font-black text-xs tracking-widest rounded-xl shadow-xl shadow-primary/20 active:scale-95 transition-all text-center"
|
||||
>
|
||||
{t.mobileMenu.demo}
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
41
src/components/Reveal.tsx
Normal file
41
src/components/Reveal.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default function Reveal({
|
||||
children,
|
||||
delay = 0,
|
||||
className = '',
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
delay?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => { if (entry.isIntersecting) setVisible(true); },
|
||||
{ threshold: 0.12 }
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={className}
|
||||
style={{
|
||||
opacity: visible ? 1 : 0,
|
||||
transform: visible ? 'translateY(0)' : 'translateY(22px)',
|
||||
transition: `opacity 0.55s ease-out ${delay}ms, transform 0.55s ease-out ${delay}ms`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
582
src/data/cas-clients.ts
Normal file
582
src/data/cas-clients.ts
Normal file
@ -0,0 +1,582 @@
|
||||
export type CasClientLanguage = 'fr' | 'en' | 'nl' | 'de';
|
||||
|
||||
export interface CasClientMeta {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface CasClientTranslation {
|
||||
backLabel: string;
|
||||
badge: string;
|
||||
headline: string;
|
||||
description: string;
|
||||
challenge: {
|
||||
label: string;
|
||||
title: string;
|
||||
items: string[];
|
||||
};
|
||||
solution: {
|
||||
label: string;
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
};
|
||||
results: {
|
||||
label: string;
|
||||
title: string;
|
||||
items: { value: string; label: string }[];
|
||||
};
|
||||
cta: {
|
||||
title: string;
|
||||
button1: string;
|
||||
button2: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CasClient {
|
||||
slug: string;
|
||||
client: string;
|
||||
year: number;
|
||||
location: string;
|
||||
segmentSlug: string;
|
||||
meta: Record<CasClientLanguage, CasClientMeta>;
|
||||
translations: Record<CasClientLanguage, CasClientTranslation>;
|
||||
}
|
||||
|
||||
// ─── Musée de la Fraise ──────────────────────────────────────────────────────
|
||||
|
||||
const museeDelaFraise: CasClient = {
|
||||
slug: 'musee-de-la-fraise',
|
||||
client: 'Musée de la Fraise de Wépion',
|
||||
year: 2021,
|
||||
location: 'Wépion, Belgique',
|
||||
segmentSlug: 'musees',
|
||||
meta: {
|
||||
fr: {
|
||||
title: 'Musée de la Fraise — Premier client MyInfoMate | Kiosk & App Mobile',
|
||||
description: 'Découvrez comment le Musée de la Fraise de Wépion a digitalisé son expérience visiteur avec MyInfoMate : kiosk tablette no-code dès 2021, puis extension mobile.',
|
||||
},
|
||||
en: {
|
||||
title: 'Musée de la Fraise — First MyInfoMate Client | Kiosk & Mobile App',
|
||||
description: 'See how the Musée de la Fraise de Wépion digitalized its visitor experience with MyInfoMate: no-code tablet kiosk from 2021, then mobile extension.',
|
||||
},
|
||||
nl: {
|
||||
title: 'Musée de la Fraise — Eerste MyInfoMate-klant | Kiosk & Mobiele App',
|
||||
description: 'Ontdek hoe het Musée de la Fraise de Wépion zijn bezoekerservaring digitaliseerde met MyInfoMate: no-code tablet kiosk vanaf 2021, daarna mobiele uitbreiding.',
|
||||
},
|
||||
de: {
|
||||
title: 'Musée de la Fraise — Erster MyInfoMate-Kunde | Kiosk & Mobile App',
|
||||
description: 'Erfahren Sie, wie das Musée de la Fraise de Wépion sein Besuchererlebnis mit MyInfoMate digitalisierte: No-Code-Tablet-Kiosk ab 2021, dann mobile Erweiterung.',
|
||||
},
|
||||
},
|
||||
translations: {
|
||||
fr: {
|
||||
backLabel: "Retour à l'accueil",
|
||||
badge: 'Musée & Culture',
|
||||
headline: 'De la vitrine au numérique : la naissance de MyInfoMate',
|
||||
description: "Premier client MyInfoMate — solution kiosk tablette avec CMS no-code dès novembre 2021, puis extension vers une application mobile pour le musée et le jardin de petits fruits.",
|
||||
challenge: {
|
||||
label: 'Le contexte',
|
||||
title: 'Un musée riche en histoire, pauvre en outils digitaux',
|
||||
items: [
|
||||
"Collections riches mais expérience visiteur uniquement papier et vitrines statiques",
|
||||
"Équipe du musée sans compétences techniques pour créer du contenu digital",
|
||||
"Budget limité : besoin d'une solution autonome, simple et pérenne",
|
||||
"Extension vers le jardin de petits fruits : un deuxième parcours à créer en outdoor",
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
label: 'La solution',
|
||||
title: 'Kiosk tablette no-code, puis application mobile',
|
||||
description: "Déploiement en mode kiosk tablette en novembre 2021 — le premier déploiement de MyInfoMate. L'équipe du musée gère le contenu en totale autonomie via le CMS no-code : textes, images, vidéos, sans aucune intervention externe. Extension progressive vers une application mobile visiteurs couvrant le musée et le jardin de petits fruits.",
|
||||
tags: ['Kiosk tablette', 'App mobile visiteurs', 'CMS no-code', 'Parcours outdoor', 'White label', 'Zéro développement'],
|
||||
},
|
||||
results: {
|
||||
label: 'Résultats',
|
||||
title: "Un musée autonome, une solution qui a évolué avec eux",
|
||||
items: [
|
||||
{ value: '2021', label: 'Premier déploiement' },
|
||||
{ value: 'Kiosk + Mobile', label: 'Double déploiement' },
|
||||
{ value: '100%', label: 'Autonomie éditoriale' },
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
title: 'Vous avez un musée ou un lieu culturel ?',
|
||||
button1: 'Demander une démo gratuite',
|
||||
button2: 'Voir la solution musées',
|
||||
},
|
||||
},
|
||||
en: {
|
||||
backLabel: 'Back to home',
|
||||
badge: 'Museum & Culture',
|
||||
headline: 'From display cases to digital: the birth of MyInfoMate',
|
||||
description: 'First MyInfoMate client — no-code tablet kiosk solution from November 2021, then extended to a mobile app for the museum and the fruit garden.',
|
||||
challenge: {
|
||||
label: 'Context',
|
||||
title: 'A history-rich museum, digitally underequipped',
|
||||
items: [
|
||||
'Rich collections but visitor experience limited to paper and static displays',
|
||||
'Museum team with no technical skills to create digital content',
|
||||
'Limited budget: need for an autonomous, simple and sustainable solution',
|
||||
'Extension to the fruit garden: a second outdoor trail to create',
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
label: 'Solution',
|
||||
title: 'No-code tablet kiosk, then mobile app',
|
||||
description: 'Tablet kiosk deployment in November 2021 — the very first MyInfoMate deployment. The museum team manages content autonomously via the no-code CMS: texts, images, videos, with no external intervention. Progressive extension to a visitor mobile app covering the museum and the fruit garden.',
|
||||
tags: ['Tablet kiosk', 'Visitor mobile app', 'No-code CMS', 'Outdoor trail', 'White label', 'Zero development'],
|
||||
},
|
||||
results: {
|
||||
label: 'Results',
|
||||
title: 'An autonomous museum, a solution that grew with them',
|
||||
items: [
|
||||
{ value: '2021', label: 'First deployment' },
|
||||
{ value: 'Kiosk + Mobile', label: 'Dual deployment' },
|
||||
{ value: '100%', label: 'Editorial autonomy' },
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
title: 'Do you manage a museum or cultural venue?',
|
||||
button1: 'Request a free demo',
|
||||
button2: 'See the museum solution',
|
||||
},
|
||||
},
|
||||
nl: {
|
||||
backLabel: 'Terug naar home',
|
||||
badge: 'Museum & Cultuur',
|
||||
headline: 'Van vitrine naar digitaal: de geboorte van MyInfoMate',
|
||||
description: 'Eerste MyInfoMate-klant — no-code tablet kiosk-oplossing vanaf november 2021, daarna uitgebreid met een mobiele app voor het museum en de fruittuin.',
|
||||
challenge: {
|
||||
label: 'Context',
|
||||
title: 'Een historisch rijk museum, digitaal onderbenut',
|
||||
items: [
|
||||
'Rijke collecties maar bezoekerservaring beperkt tot papier en statische vitrines',
|
||||
'Museumteam zonder technische kennis om digitale content te maken',
|
||||
'Beperkt budget: behoefte aan een autonome, eenvoudige en duurzame oplossing',
|
||||
'Uitbreiding naar de fruittuin: een tweede outdoor parcours',
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
label: 'Oplossing',
|
||||
title: 'No-code tablet kiosk, daarna mobiele app',
|
||||
description: 'Tablet kiosk-implementatie in november 2021 — de allereerste MyInfoMate-implementatie. Het museumteam beheert de inhoud volledig autonoom via het no-code CMS. Progressieve uitbreiding naar een bezoekers-app voor het museum en de fruittuin.',
|
||||
tags: ['Tablet kiosk', 'Bezoekers mobiele app', 'No-code CMS', 'Outdoor parcours', 'White label', 'Geen ontwikkeling'],
|
||||
},
|
||||
results: {
|
||||
label: 'Resultaten',
|
||||
title: 'Een autonoom museum, een oplossing die met hen meegroeit',
|
||||
items: [
|
||||
{ value: '2021', label: 'Eerste implementatie' },
|
||||
{ value: 'Kiosk + Mobiel', label: 'Dubbele implementatie' },
|
||||
{ value: '100%', label: 'Redactionele autonomie' },
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
title: 'Beheert u een museum of culturele locatie?',
|
||||
button1: 'Gratis demo aanvragen',
|
||||
button2: 'Bekijk de museum-oplossing',
|
||||
},
|
||||
},
|
||||
de: {
|
||||
backLabel: 'Zurück zur Startseite',
|
||||
badge: 'Museum & Kultur',
|
||||
headline: 'Von der Vitrine zur Digitalisierung: die Geburt von MyInfoMate',
|
||||
description: 'Erster MyInfoMate-Kunde — No-Code-Tablet-Kiosk-Lösung ab November 2021, dann Erweiterung auf eine mobile App für das Museum und den Obstgarten.',
|
||||
challenge: {
|
||||
label: 'Kontext',
|
||||
title: 'Ein geschichtsreiches Museum, digital unterausgestattet',
|
||||
items: [
|
||||
'Reiche Sammlungen, aber Besuchererlebnis auf Papier und statische Vitrinen beschränkt',
|
||||
'Museumsteam ohne technische Kenntnisse zur Erstellung digitaler Inhalte',
|
||||
'Begrenztes Budget: Bedarf an einer autonomen, einfachen und nachhaltigen Lösung',
|
||||
'Erweiterung auf den Obstgarten: ein zweiter Outdoor-Parcours',
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
label: 'Lösung',
|
||||
title: 'No-Code-Tablet-Kiosk, dann mobile App',
|
||||
description: 'Tablet-Kiosk-Einsatz im November 2021 — die allererste MyInfoMate-Implementierung. Das Museumsteam verwaltet Inhalte vollständig autonom über das No-Code-CMS. Schrittweise Erweiterung auf eine Besucher-App für Museum und Obstgarten.',
|
||||
tags: ['Tablet-Kiosk', 'Mobile Besucher-App', 'No-Code-CMS', 'Outdoor-Parcours', 'White Label', 'Null Entwicklung'],
|
||||
},
|
||||
results: {
|
||||
label: 'Ergebnisse',
|
||||
title: 'Ein autonomes Museum, eine Lösung die mit ihnen wächst',
|
||||
items: [
|
||||
{ value: '2021', label: 'Erste Implementierung' },
|
||||
{ value: 'Kiosk + Mobil', label: 'Doppelte Implementierung' },
|
||||
{ value: '100%', label: 'Redaktionelle Autonomie' },
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
title: 'Verwalten Sie ein Museum oder eine Kulturstätte?',
|
||||
button1: 'Kostenlose Demo anfordern',
|
||||
button2: 'Museum-Lösung entdecken',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Fort de Saint-Héribert ──────────────────────────────────────────────────
|
||||
|
||||
const fortDeSaintHeribert: CasClient = {
|
||||
slug: 'fort-de-saint-heribert',
|
||||
client: 'Fort de Saint-Héribert',
|
||||
year: 2023,
|
||||
location: 'Belgique',
|
||||
segmentSlug: 'parcs-naturels',
|
||||
meta: {
|
||||
fr: {
|
||||
title: 'Fort de Saint-Héribert — Application mobile offline | MyInfoMate',
|
||||
description: "Découvrez comment MyInfoMate a équipé le Fort de Saint-Héribert d'une app mobile 100% offline avec balises iBeacon et codes QR — sans Wi-Fi dans les galeries.",
|
||||
},
|
||||
en: {
|
||||
title: 'Fort de Saint-Héribert — Offline Mobile App | MyInfoMate',
|
||||
description: 'See how MyInfoMate equipped Fort de Saint-Héribert with a 100% offline mobile app using iBeacon beacons and QR codes — no Wi-Fi needed in the galleries.',
|
||||
},
|
||||
nl: {
|
||||
title: 'Fort de Saint-Héribert — Offline Mobiele App | MyInfoMate',
|
||||
description: 'Ontdek hoe MyInfoMate het Fort de Saint-Héribert uitrustte met een 100% offline mobiele app met iBeacon-bakens en QR-codes — geen Wi-Fi nodig.',
|
||||
},
|
||||
de: {
|
||||
title: 'Fort de Saint-Héribert — Offline Mobile App | MyInfoMate',
|
||||
description: 'Erfahren Sie, wie MyInfoMate Fort de Saint-Héribert mit einer 100% Offline-App mit iBeacon-Beacons und QR-Codes ausgestattet hat — kein WLAN nötig.',
|
||||
},
|
||||
},
|
||||
translations: {
|
||||
fr: {
|
||||
backLabel: "Retour à l'accueil",
|
||||
badge: 'Patrimoine & Tourisme',
|
||||
headline: 'Un fort historique souterrain, connecté sans réseau',
|
||||
description: "Application mobile iOS + Android en mode 100% offline sur un site patrimonial unique : balises iBeacon pour la localisation dans les galeries souterraines, codes QR et audioguide, sans Wi-Fi.",
|
||||
challenge: {
|
||||
label: 'Le contexte',
|
||||
title: 'Un site souterrain impossible à connecter',
|
||||
items: [
|
||||
"Galeries souterraines : impossible de déployer du Wi-Fi dans l'ensemble du fort",
|
||||
"Visiteurs équipés de smartphones, sans expérience digitale proposée jusqu'alors",
|
||||
"Contenu riche (audio, images, textes, historique) à diffuser en autonomie",
|
||||
"Localisation des visiteurs dans les galeries sans GPS",
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
label: 'La solution',
|
||||
title: 'Application mobile 100% offline avec iBeacon',
|
||||
description: "Déploiement d'une application mobile iOS + Android en mode offline complet. Le contenu est téléchargé à l'entrée du fort. Des balises iBeacon positionnées stratégiquement dans les galeries détectent la position du visiteur en temps réel et déclenchent l'affichage des contenus correspondants. Les codes QR permettent un accès alternatif instantané à chaque point d'intérêt.",
|
||||
tags: ['App mobile iOS & Android', 'Mode offline 100%', 'Balises iBeacon', 'Codes QR', 'Audioguide', 'Localisation intérieure'],
|
||||
},
|
||||
results: {
|
||||
label: 'Résultats',
|
||||
title: 'Une expérience immersive, partout dans le fort',
|
||||
items: [
|
||||
{ value: '100%', label: 'Disponibilité hors réseau' },
|
||||
{ value: '2023', label: 'Mise en production' },
|
||||
{ value: 'iOS + Android', label: 'Plateformes couvertes' },
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
title: 'Vous avez un site sans connexion fiable ?',
|
||||
button1: 'Demander une démo gratuite',
|
||||
button2: 'Voir la solution parcs & patrimoine',
|
||||
},
|
||||
},
|
||||
en: {
|
||||
backLabel: 'Back to home',
|
||||
badge: 'Heritage & Tourism',
|
||||
headline: 'A historic underground fort, connected without network',
|
||||
description: 'iOS + Android mobile app in 100% offline mode on a unique heritage site: iBeacon beacons for location in underground galleries, QR codes and audio guide — no Wi-Fi.',
|
||||
challenge: {
|
||||
label: 'Context',
|
||||
title: 'An underground site impossible to connect',
|
||||
items: [
|
||||
"Underground galleries: impossible to deploy Wi-Fi throughout the fort",
|
||||
"Visitors with smartphones, with no digital experience previously offered",
|
||||
"Rich content (audio, images, texts, history) to distribute autonomously",
|
||||
"Locating visitors in the galleries without GPS",
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
label: 'Solution',
|
||||
title: '100% offline mobile app with iBeacon',
|
||||
description: 'Deployment of an iOS + Android mobile app in full offline mode. Content is downloaded at the fort entrance. Strategically placed iBeacon beacons in the galleries detect visitor position in real time and trigger the display of corresponding content. QR codes provide instant alternative access to each point of interest.',
|
||||
tags: ['iOS & Android mobile app', '100% offline mode', 'iBeacon beacons', 'QR codes', 'Audio guide', 'Indoor location'],
|
||||
},
|
||||
results: {
|
||||
label: 'Results',
|
||||
title: 'An immersive experience, everywhere in the fort',
|
||||
items: [
|
||||
{ value: '100%', label: 'Offline availability' },
|
||||
{ value: '2023', label: 'Production launch' },
|
||||
{ value: 'iOS + Android', label: 'Platforms covered' },
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
title: 'Do you have a site without reliable connectivity?',
|
||||
button1: 'Request a free demo',
|
||||
button2: 'See the parks & heritage solution',
|
||||
},
|
||||
},
|
||||
nl: {
|
||||
backLabel: 'Terug naar home',
|
||||
badge: 'Erfgoed & Toerisme',
|
||||
headline: 'Een historisch ondergronds fort, verbonden zonder netwerk',
|
||||
description: 'iOS + Android mobiele app in 100% offline modus op een unieke erfgoedsite: iBeacon-bakens voor locatie in ondergrondse galerijen, QR-codes en audiogids — geen Wi-Fi.',
|
||||
challenge: {
|
||||
label: 'Context',
|
||||
title: 'Een ondergrondse site die onmogelijk te verbinden is',
|
||||
items: [
|
||||
'Ondergrondse galerijen: onmogelijk om Wi-Fi door het hele fort te installeren',
|
||||
'Bezoekers met smartphones, zonder eerder digitaal aanbod',
|
||||
'Rijke inhoud (audio, afbeeldingen, teksten, geschiedenis) autonoom verspreiden',
|
||||
'Bezoekers lokaliseren in de galerijen zonder GPS',
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
label: 'Oplossing',
|
||||
title: '100% offline mobiele app met iBeacon',
|
||||
description: 'Implementatie van een iOS + Android mobiele app in volledig offline modus. Inhoud wordt gedownload bij de ingang van het fort. Strategisch geplaatste iBeacon-bakens in de galerijen detecteren de positie van de bezoeker in realtime.',
|
||||
tags: ['iOS & Android mobiele app', '100% offline modus', 'iBeacon-bakens', 'QR-codes', 'Audiogids', 'Indoor locatie'],
|
||||
},
|
||||
results: {
|
||||
label: 'Resultaten',
|
||||
title: 'Een meeslepende ervaring, overal in het fort',
|
||||
items: [
|
||||
{ value: '100%', label: 'Offline beschikbaarheid' },
|
||||
{ value: '2023', label: 'Productielancering' },
|
||||
{ value: 'iOS + Android', label: 'Gedekte platformen' },
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
title: 'Heeft u een site zonder betrouwbare verbinding?',
|
||||
button1: 'Gratis demo aanvragen',
|
||||
button2: 'Bekijk de parken & erfgoed-oplossing',
|
||||
},
|
||||
},
|
||||
de: {
|
||||
backLabel: 'Zurück zur Startseite',
|
||||
badge: 'Kulturerbe & Tourismus',
|
||||
headline: 'Ein historisches unterirdisches Fort, ohne Netz verbunden',
|
||||
description: 'iOS + Android Mobile App im 100% Offline-Modus an einer einzigartigen Kulturstätte: iBeacon-Beacons zur Ortung in unterirdischen Galerien, QR-Codes und Audioguide — kein WLAN.',
|
||||
challenge: {
|
||||
label: 'Kontext',
|
||||
title: 'Eine unterirdische Stätte, die unmöglich zu verbinden ist',
|
||||
items: [
|
||||
'Unterirdische Galerien: unmöglich, überall im Fort WLAN einzurichten',
|
||||
'Besucher mit Smartphones, bisher ohne digitales Angebot',
|
||||
'Reichhaltige Inhalte (Audio, Bilder, Texte, Geschichte) autonom verteilen',
|
||||
'Besucher in den Galerien ohne GPS orten',
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
label: 'Lösung',
|
||||
title: '100% Offline Mobile App mit iBeacon',
|
||||
description: 'Einsatz einer iOS + Android Mobile App im vollständigen Offline-Modus. Inhalte werden am Fort-Eingang heruntergeladen. Strategisch platzierte iBeacon-Beacons in den Galerien erkennen die Besucherposition in Echtzeit.',
|
||||
tags: ['iOS & Android Mobile App', '100% Offline-Modus', 'iBeacon-Beacons', 'QR-Codes', 'Audioguide', 'Indoor-Ortung'],
|
||||
},
|
||||
results: {
|
||||
label: 'Ergebnisse',
|
||||
title: 'Ein immersives Erlebnis überall im Fort',
|
||||
items: [
|
||||
{ value: '100%', label: 'Offline-Verfügbarkeit' },
|
||||
{ value: '2023', label: 'Produktionsstart' },
|
||||
{ value: 'iOS + Android', label: 'Abgedeckte Plattformen' },
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
title: 'Haben Sie eine Stätte ohne zuverlässige Verbindung?',
|
||||
button1: 'Kostenlose Demo anfordern',
|
||||
button2: 'Parks & Kulturerbe-Lösung entdecken',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Visit Namur ─────────────────────────────────────────────────────────────
|
||||
|
||||
const visitNamur: CasClient = {
|
||||
slug: 'visit-namur',
|
||||
client: 'Visit Namur',
|
||||
year: 2024,
|
||||
location: 'Namur, Belgique',
|
||||
segmentSlug: 'offices-tourisme',
|
||||
meta: {
|
||||
fr: {
|
||||
title: 'Visit Namur — Borne tactile XXL touristique | MyInfoMate',
|
||||
description: "Découvrez comment l'Office du Tourisme de Namur a déployé une borne tactile interactive XXL MyInfoMate pour guider les visiteurs avec une carte, un agenda et des itinéraires.",
|
||||
},
|
||||
en: {
|
||||
title: 'Visit Namur — XXL Interactive Tourist Kiosk | MyInfoMate',
|
||||
description: "See how the Namur Tourist Office deployed a MyInfoMate XXL interactive tablet kiosk to guide visitors with a map, agenda and itineraries.",
|
||||
},
|
||||
nl: {
|
||||
title: 'Visit Namur — XXL Interactieve Toeristische Kiosk | MyInfoMate',
|
||||
description: "Ontdek hoe het Toeristenbureau van Namen een MyInfoMate XXL interactieve tablet-kiosk inzette om bezoekers te begeleiden met een kaart, agenda en routes.",
|
||||
},
|
||||
de: {
|
||||
title: 'Visit Namur — XXL Interaktiver Tourismus-Kiosk | MyInfoMate',
|
||||
description: "Erfahren Sie, wie das Tourismusbüro Namur einen XXL interaktiven MyInfoMate-Tablet-Kiosk einsetzte, um Besucher mit Karte, Agenda und Routen zu führen.",
|
||||
},
|
||||
},
|
||||
translations: {
|
||||
fr: {
|
||||
backLabel: "Retour à l'accueil",
|
||||
badge: 'Office de Tourisme',
|
||||
headline: 'Namur offre un accueil touristique digital et interactif',
|
||||
description: "Déploiement d'une borne tactile interactive XXL MyInfoMate dans l'Office du Tourisme de Namur : carte interactive de la ville, points d'intérêt géolocalisés, agenda des événements et planification d'itinéraires.",
|
||||
challenge: {
|
||||
label: 'Le contexte',
|
||||
title: "Un accueil touristique en attente de modernisation",
|
||||
items: [
|
||||
"Accueil basé sur supports papier, brochures et conseils oraux des agents",
|
||||
"Touristes cherchant une exploration autonome et interactive de la ville",
|
||||
"Besoin de mettre en avant les points d'intérêt, restaurants et événements de Namur",
|
||||
"Mise à jour du contenu sans dépendance à une agence externe",
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
label: 'La solution',
|
||||
title: 'Borne tactile XXL avec carte interactive et agenda',
|
||||
description: "Déploiement d'une grande tablette XXL en mode kiosk dans l'Office du Tourisme. La carte interactive de Namur présente les points d'intérêt géolocalisés, les itinéraires suggérés et l'agenda des événements en temps réel. L'équipe de l'office gère le contenu en autonomie via le CMS MyInfoMate, sans intervention externe.",
|
||||
tags: ['Kiosk tablette XXL', 'Carte interactive', "Points d'intérêt", 'Agenda événements', 'Planification itinéraires', 'CMS no-code'],
|
||||
},
|
||||
results: {
|
||||
label: 'Résultats',
|
||||
title: "Une expérience d'accueil moderne pour les touristes namurois",
|
||||
items: [
|
||||
{ value: 'Namur', label: 'Ville couverte' },
|
||||
{ value: '1 borne XXL', label: 'Mode de déploiement' },
|
||||
{ value: 'Temps réel', label: 'Mise à jour contenu' },
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
title: 'Vous gérez un office de tourisme ?',
|
||||
button1: 'Demander une démo gratuite',
|
||||
button2: 'Voir la solution offices de tourisme',
|
||||
},
|
||||
},
|
||||
en: {
|
||||
backLabel: 'Back to home',
|
||||
badge: 'Tourism Office',
|
||||
headline: 'Namur offers digital and interactive tourist reception',
|
||||
description: 'MyInfoMate XXL tablet kiosk deployment at the Namur Tourist Office: interactive city map, geolocated points of interest, events agenda and itinerary planning.',
|
||||
challenge: {
|
||||
label: 'Context',
|
||||
title: 'A tourist reception awaiting modernization',
|
||||
items: [
|
||||
'Reception based on paper materials, brochures and verbal advice from agents',
|
||||
'Tourists seeking autonomous and interactive city exploration',
|
||||
'Need to highlight Namur\'s points of interest, restaurants and events',
|
||||
'Content updates without dependency on an external agency',
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
label: 'Solution',
|
||||
title: 'XXL tablet kiosk with interactive map and agenda',
|
||||
description: 'Deployment of a large XXL tablet kiosk at the Tourist Office. The interactive map of Namur displays geolocated points of interest, suggested itineraries and real-time events agenda. The office team manages content autonomously via the MyInfoMate CMS.',
|
||||
tags: ['XXL tablet kiosk', 'Interactive map', 'Points of interest', 'Events agenda', 'Itinerary planning', 'No-code CMS'],
|
||||
},
|
||||
results: {
|
||||
label: 'Results',
|
||||
title: 'A modern welcome experience for Namur tourists',
|
||||
items: [
|
||||
{ value: 'Namur', label: 'City covered' },
|
||||
{ value: '1 XXL kiosk', label: 'Deployment mode' },
|
||||
{ value: 'Real-time', label: 'Content updates' },
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
title: 'Do you manage a tourism office?',
|
||||
button1: 'Request a free demo',
|
||||
button2: 'See the tourism office solution',
|
||||
},
|
||||
},
|
||||
nl: {
|
||||
backLabel: 'Terug naar home',
|
||||
badge: 'Toeristenbureau',
|
||||
headline: 'Namen biedt digitale en interactieve toeristische ontvangst',
|
||||
description: 'MyInfoMate XXL tablet kiosk-implementatie bij het Toeristenbureau van Namen: interactieve stadskaart, geogelokaliseerde bezienswaardigheden, evenementenagenda en routeplanning.',
|
||||
challenge: {
|
||||
label: 'Context',
|
||||
title: 'Een toeristische ontvangst die modernisering verwacht',
|
||||
items: [
|
||||
'Ontvangst gebaseerd op papieren materialen, brochures en mondeling advies',
|
||||
'Toeristen die op zoek zijn naar autonome en interactieve stadsverkenning',
|
||||
'Behoefte om de bezienswaardigheden, restaurants en evenementen van Namen te promoten',
|
||||
'Inhoud bijwerken zonder afhankelijkheid van een extern bureau',
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
label: 'Oplossing',
|
||||
title: 'XXL tablet-kiosk met interactieve kaart en agenda',
|
||||
description: 'Implementatie van een grote XXL tablet-kiosk bij het Toeristenbureau. De interactieve kaart van Namen toont geogelokaliseerde bezienswaardigheden, voorgestelde routes en realtime evenementenagenda. Het team beheert inhoud autonoom via MyInfoMate CMS.',
|
||||
tags: ['XXL tablet kiosk', 'Interactieve kaart', 'Bezienswaardigheden', 'Evenementenagenda', 'Routeplanning', 'No-code CMS'],
|
||||
},
|
||||
results: {
|
||||
label: 'Resultaten',
|
||||
title: 'Een moderne ontvangstervaring voor toeristen in Namen',
|
||||
items: [
|
||||
{ value: 'Namen', label: 'Stad gedekt' },
|
||||
{ value: '1 XXL kiosk', label: 'Implementatiemodus' },
|
||||
{ value: 'Realtime', label: 'Inhoud bijwerken' },
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
title: 'Beheert u een toeristenbureau?',
|
||||
button1: 'Gratis demo aanvragen',
|
||||
button2: 'Bekijk de toerisme-oplossing',
|
||||
},
|
||||
},
|
||||
de: {
|
||||
backLabel: 'Zurück zur Startseite',
|
||||
badge: 'Tourismusbüro',
|
||||
headline: 'Namur bietet digitalen und interaktiven Tourismusempfang',
|
||||
description: 'MyInfoMate XXL Tablet-Kiosk-Einsatz im Tourismusbüro Namur: interaktive Stadtkarte, geolokalisierte Sehenswürdigkeiten, Veranstaltungsagenda und Routenplanung.',
|
||||
challenge: {
|
||||
label: 'Kontext',
|
||||
title: 'Ein Tourismusempfang wartet auf Modernisierung',
|
||||
items: [
|
||||
'Empfang basierend auf Papiermaterial, Broschüren und mündlichen Ratschlägen',
|
||||
'Touristen suchen eine autonome und interaktive Stadterkundung',
|
||||
'Bedarf, Sehenswürdigkeiten, Restaurants und Veranstaltungen von Namur hervorzuheben',
|
||||
'Inhaltsaktualisierungen ohne Abhängigkeit von einer externen Agentur',
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
label: 'Lösung',
|
||||
title: 'XXL Tablet-Kiosk mit interaktiver Karte und Agenda',
|
||||
description: 'Einsatz eines großen XXL Tablet-Kiosks im Tourismusbüro. Die interaktive Karte von Namur zeigt geolokalisierte Sehenswürdigkeiten, vorgeschlagene Routen und Echtzeit-Veranstaltungsagenda. Das Team verwaltet Inhalte autonom über MyInfoMate CMS.',
|
||||
tags: ['XXL Tablet-Kiosk', 'Interaktive Karte', 'Sehenswürdigkeiten', 'Veranstaltungsagenda', 'Routenplanung', 'No-Code-CMS'],
|
||||
},
|
||||
results: {
|
||||
label: 'Ergebnisse',
|
||||
title: 'Ein modernes Empfangserlebnis für Namur-Touristen',
|
||||
items: [
|
||||
{ value: 'Namur', label: 'Abgedeckte Stadt' },
|
||||
{ value: '1 XXL-Kiosk', label: 'Implementierungsmodus' },
|
||||
{ value: 'Echtzeit', label: 'Inhaltsaktualisierungen' },
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
title: 'Verwalten Sie ein Tourismusbüro?',
|
||||
button1: 'Kostenlose Demo anfordern',
|
||||
button2: 'Tourismusbüro-Lösung entdecken',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Exports ───────────────────────────────────────────────────────────────<E29480><E29480>─
|
||||
|
||||
const casClients: CasClient[] = [museeDelaFraise, fortDeSaintHeribert, visitNamur];
|
||||
|
||||
export function getAllCasClientSlugs(): string[] {
|
||||
return casClients.map((c) => c.slug);
|
||||
}
|
||||
|
||||
export function getCasClientData(slug: string): CasClient | undefined {
|
||||
return casClients.find((c) => c.slug === slug);
|
||||
}
|
||||
|
||||
export function getAllCasClients(): CasClient[] {
|
||||
return casClients;
|
||||
}
|
||||
@ -243,9 +243,17 @@ const translations = {
|
||||
customDev: 'Développements sur mesure',
|
||||
},
|
||||
},
|
||||
casClients: {
|
||||
sectionLabel: 'Cas clients',
|
||||
title: 'Ils nous font confiance',
|
||||
subtitle: "Des musées, sites patrimoniaux et offices de tourisme ont choisi MyInfoMate pour digitaliser l'expérience de leurs visiteurs.",
|
||||
ctaAll: 'Voir tous les cas clients',
|
||||
readCase: 'Lire le cas',
|
||||
},
|
||||
footer: {
|
||||
desc: "La solution SaaS pour la digitalisation de vos lieux. Créativité et technologie au service de l'expérience visiteur.",
|
||||
contactTitle: 'Contact',
|
||||
casClientsLink: 'Cas clients',
|
||||
copyright: '© 2026 MyInfoMate. Tous droits réservés.',
|
||||
legal: 'Mentions Légales',
|
||||
privacy: 'Confidentialité',
|
||||
@ -494,9 +502,17 @@ const translations = {
|
||||
customDev: 'Custom development',
|
||||
},
|
||||
},
|
||||
casClients: {
|
||||
sectionLabel: 'Case studies',
|
||||
title: 'They trust us',
|
||||
subtitle: 'Museums, heritage sites and tourism offices chose MyInfoMate to digitalize their visitor experience.',
|
||||
ctaAll: 'View all case studies',
|
||||
readCase: 'Read case',
|
||||
},
|
||||
footer: {
|
||||
desc: 'The SaaS solution for digitalizing your venues. Creativity and technology at the service of visitor experience.',
|
||||
contactTitle: 'Contact',
|
||||
casClientsLink: 'Case studies',
|
||||
copyright: '© 2026 MyInfoMate. All rights reserved.',
|
||||
legal: 'Legal Notice',
|
||||
privacy: 'Privacy',
|
||||
@ -745,9 +761,17 @@ const translations = {
|
||||
customDev: 'Maatwerkontwikkeling',
|
||||
},
|
||||
},
|
||||
casClients: {
|
||||
sectionLabel: 'Klantcases',
|
||||
title: 'Zij vertrouwen ons',
|
||||
subtitle: 'Musea, erfgoedsites en toerismekantoren kozen voor MyInfoMate om hun bezoekerservaring te digitaliseren.',
|
||||
ctaAll: 'Bekijk alle klantcases',
|
||||
readCase: 'Lees case',
|
||||
},
|
||||
footer: {
|
||||
desc: 'De SaaS-oplossing voor de digitalisering van uw locaties. Creativiteit en technologie ten dienste van de bezoekerservaring.',
|
||||
contactTitle: 'Contact',
|
||||
casClientsLink: 'Klantcases',
|
||||
copyright: '© 2026 MyInfoMate. Alle rechten voorbehouden.',
|
||||
legal: 'Wettelijke vermeldingen',
|
||||
privacy: 'Privacy',
|
||||
@ -996,9 +1020,17 @@ const translations = {
|
||||
customDev: 'Individuelle Entwicklung',
|
||||
},
|
||||
},
|
||||
casClients: {
|
||||
sectionLabel: 'Referenzen',
|
||||
title: 'Sie vertrauen uns',
|
||||
subtitle: 'Museen, Kulturstätten und Tourismusbüros haben sich für MyInfoMate entschieden, um ihr Besuchererlebnis zu digitalisieren.',
|
||||
ctaAll: 'Alle Referenzen ansehen',
|
||||
readCase: 'Fall lesen',
|
||||
},
|
||||
footer: {
|
||||
desc: 'Die SaaS-Lösung für die Digitalisierung Ihrer Standorte. Kreativität und Technologie im Dienst des Besuchererlebnisses.',
|
||||
contactTitle: 'Kontakt',
|
||||
casClientsLink: 'Referenzen',
|
||||
copyright: '© 2026 MyInfoMate. Alle Rechte vorbehalten.',
|
||||
legal: 'Rechtliche Hinweise',
|
||||
privacy: 'Datenschutz',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user