Ce que disent de notre collaboration les personnes pour qui je construis.
”
« Monsieur Agossou n'a pas simplement développé pour nous un site web ordinaire à partir d'un modèle standard acheté. Il a au contraire programmé et conçu une solution web entièrement individuelle, techniquement exigeante et taillée exactement pour les besoins de notre entreprise. »
/COMPÉTENCES
Sites & plateformes web
Je conçois des sites et des applications avec Next.js, React, TypeScript et Tailwind CSS : rapides, bien référencés, installables en PWA quand c'est utile.
CMS & back-end
Je gère les contenus avec Strapi, Sanity ou Supabase, et je construis des API REST, des espaces membres avec authentification, des e-mails automatiques et des documents PDF.
GET /api/projects
200 OK · 42 ms
{
"title": "Zielscheibe",
"status": "live",
"cms": "Strapi 5"
}
Identité & UI/UX
Je dessine des maquettes UI/UX et des identités visuelles, jusqu'au guide graphique.
AaStyle guide
Multilingue & accessible
Je réalise des sites jusqu'à 7 langues, dont l'arabe de droite à gauche, pensés pour tous les publics et tous les écrans.
ENHello
FRBonjour
DEHallo
ARمرحبا
Infrastructure & mise en ligne
J'utilise Docker, Linux et l'intégration continue, je protège les formulaires (anti-spam, en-têtes de sécurité) et je déploie sur Vercel, Netlify ou Railway.
❯ docker compose up -d
✔ web · running
✔ db · healthy
✔ proxy · running
❯ git push (deploy)
Atelier
/ATELIER
Du code et du design que j'ai construits de bout en bout. Chaque pièce montre le vrai code, et ce qu'il produit juste à côté.
Laserbeni
Le sélecteur de zones en 3D
Sur la page des tarifs, la cliente touche une zone d'un corps en 3D et voit son prix. Le corps de départ vient d'une bibliothèque libre : tout ce qui le rend utilisable, je l'ai pensé et écrit.
Three.js
GLSL
Raycasting
24 zones
Code
features/atelier/zone-demo/stage.ts
1import{2AmbientLight,3BufferAttribute,4Color,5DirectionalLight,6Group,7Mesh,8PerspectiveCamera,9Scene,10ShaderMaterial,11WebGLRenderer,12}from"three";13import{GLTFLoader}from"three/examples/jsm/loaders/GLTFLoader.js";14import{ neighboursOf }from"./mesh";15import{ paint,typePickContext}from"./pick";16import{ attachPointer }from"./pointer";17import{ buildPodium }from"./podium";18import{ createSpin }from"./spin";19import{FRAGMENT,VERTEX}from"./shaders";20import{ headZone,typeZone}from"./zones";2122exporttypeStage={ show:(selection:ReadonlySet<Zone>)=>void; dispose:()=>void};23typeHandlers={ model: string; onPick:(zone:Zone)=>void; onFail:()=>void};2425/**26 * La scène 3D : charge le corps (un maillage dont chaque point porte sa zone de prix), le pose sur un socle et gère la27 * souris et le doigt (voir `pointer.ts`) : glisser fait tourner le corps, toucher choisit une zone, le survol l'allume.28 */29exportfunctioncreateStage(container:HTMLElement,{ model, onPick, onFail }:Handlers):Stage{30const renderer =newWebGLRenderer({ antialias:true, alpha:true});31 renderer.setPixelRatio(Math.min(devicePixelRatio,2));32const canvas = renderer.domElement;33 canvas.style.cssText ="display:block;width:100%;height:100%;touch-action:pan-y;cursor:grab";34 container.appendChild(canvas);3536const scene =newScene();37const camera =newPerspectiveCamera(28,1,0.1,50);38const figure =newGroup();39const sun =newDirectionalLight(0xffe2c4,2);40 sun.position.set(1,2,2);41 scene.add(figure,newAmbientLight(0xffffff,1.4), sun);4243const material =newShaderMaterial({44 vertexShader:VERTEX,45 fragmentShader:FRAGMENT,46 uniforms:{47 uBase:{ value:newColor("#e8ddd0")},48 uGold:{ value:newColor("#c9a064")},49 uGoldLight:{ value:newColor("#e6c48f")},50 uGoldDeep:{ value:newColor("#8a6a38")},51 uTime:{ value:0},52},53});5455let ctx:PickContext|null=null;56let around:Uint32Array[]=[];57let selected:BufferAttribute;58let hovered:BufferAttribute;59let current:ReadonlySet<Zone>=newSet();60let disposed =false;6162newGLTFLoader().load(63 model,64(gltf)=>{65if(disposed)return;66const mesh = gltf.scene.children[0]asMesh;67const geometry = mesh.geometry;68 geometry.computeBoundingBox();69const box = geometry.boundingBox!;70const height = box.max.y - box.min.y;7172const zone = geometry.getAttribute("_zone");73const where = geometry.getAttribute("position");74// les points sans zone (la tête) reçoivent la leur d'après leur position75const zoneOf =Float32Array.from({ length: zone.count },(_, i)=> zone.getX(i)||headZone(where.getX(i), where.getY(i), where.getZ(i)));76 around =neighboursOf(geometry.getIndex()!.array, zone.count);77 selected =newBufferAttribute(newFloat32Array(zone.count),1);78 hovered =newBufferAttribute(newFloat32Array(zone.count),1);79 geometry.setAttribute("aSel", selected);80 geometry.setAttribute("aHover", hovered);8182const body =newMesh(geometry, material);83 body.position.z =-(box.min.z + box.max.z)/2;84 figure.add(body);8586 scene.add(...buildPodium(box.min.y, height));8788const tan =Math.tan((camera.fov *Math.PI)/360);89 camera.userData ={ distance:(0.66* height)/ tan, lookY: box.min.y +0.5* height };90 ctx ={ mesh: body, camera, canvas, zoneOf };91show(current);92resize();93},94undefined,95 onFail,96);9798functionshow(selection:ReadonlySet<Zone>){99 current = selection;100if(ctx)paint(selected, ctx.zoneOf, around, selection,4);101}102103functionresize(){104const{ clientWidth: w, clientHeight: h }= container;105if(!w ||!h)return;106 renderer.setSize(w, h,false);107 camera.aspect = w / h;108const{ distance, lookY }= camera.userData as{ distance?: number; lookY?: number };109// le corps entier doit tenir dans la hauteur ET dans la largeur disponibles110if(distance && lookY !==undefined){111 camera.position.set(0, lookY, distance /Math.min(1, camera.aspect *1.25));112 camera.lookAt(0, lookY,0);113}114 camera.updateProjectionMatrix();115}116const observer =newResizeObserver(resize);117 observer.observe(container);118119const spin =createSpin(figure);120attachPointer(canvas, figure,()=> ctx,{121 onPick,122 onTouch: spin.touch,123 onHover:(zone)=> ctx &&paint(hovered, ctx.zoneOf, around,newSet(zone ?[zone]:[]),3),124});125126let frame =0;127let previous =0;128const loop =(time: number)=>{129 spin.tick(previous ?(time - previous)/1000:0, time);130 previous = time;131 material.uniforms.uTime.value = time /1000;132 renderer.render(scene, camera);133 frame =requestAnimationFrame(loop);134};135 frame =requestAnimationFrame(loop);136137return{138 show,139dispose(){140 disposed =true;141cancelAnimationFrame(frame);142 observer.disconnect();143 renderer.dispose();144 canvas.remove();145},146};147}148
Aperçu
Le code affiché est celui de cette démonstration. Sur le site de Laserbeni, le sélecteur va plus loin : dos, visage, forfaits et réservation.
Profil
/PROFIL
Développeur full-stack avec une base solide en réseaux et télécommunications. Je conçois et livre des plateformes web multilingues (Next.js, React, TypeScript, Node.js), de la base de données à l'interface, avec des tests, un déploiement automatisé et la sécurité pensés dès le départ.
Front-end
Next.js
React
TypeScript
Tailwind CSS
Three.js
Framer Motion
Responsive
UX / UI design
Back-end & données
Node.js
API REST
Strapi
Supabase
Sanity
PostgreSQL
Authentification & espaces membres
Multilingue, jusqu'à 7 langues
Qualité & livraison
Tests automatisés
GitHub Actions (CI/CD)
Docker
Cloudflare Pages
Vercel
Netlify
PWA
Sécurité & fiabilité
Validation côté serveur
Anti-spam (Turnstile)
En-têtes de sécurité
Linux
SSH · Tailscale
Réseaux et télécoms
Développement assisté par IA
Code relu et testé
Codage agentique
Workflows natifs IA
Intégration de LLM (Groq, OpenAI)
Langues & transmission
Allemand (C1)
Français (natif)
Anglais (B1)
Pédagogie
Création de supports
Homelab
Un cluster personnel (ZimaBoard, Raspberry Pi) où j'héberge mes propres services avec Docker, pour tester en vrai ce que je mets en ligne.
Un poste, un projet ? Parlons-en.
Écris-moi ou planifie un échange : on en parle autour d'un café (virtuel).
❯ write --to castoleBerlin --:--
Salut, c'est Castole ! Dis-moi ce que tu as en tête : je lis chaque message moi-même.