Was die Menschen, für die ich baue, über die Zusammenarbeit sagen.
”
„Herr Agossou entwickelte für uns nicht lediglich eine gewöhnliche Website auf Grundlage einer gekauften Standardvorlage. Vielmehr programmierte und gestaltete er eine vollständig individuelle, technisch anspruchsvolle und exakt auf unsere betrieblichen Anforderungen zugeschnittene Weblösung.“
/KOMPETENZEN
Websites & Plattformen
Ich baue Websites und Anwendungen mit Next.js, React, TypeScript und Tailwind CSS: schnell, gut auffindbar und bei Bedarf als PWA installierbar.
CMS & Back-End
Ich pflege Inhalte mit Strapi, Sanity oder Supabase und baue REST-APIs, Mitgliederbereiche mit Authentifizierung, automatische E-Mails und PDF-Dokumente.
GET /api/projects
200 OK · 42 ms
{
"title": "Zielscheibe",
"status": "live",
"cms": "Strapi 5"
}
Identität & UI/UX
Ich entwerfe UI/UX-Mockups und visuelle Identitäten, bis hin zum Styleguide.
AaStyle guide
Mehrsprachig & barrierefrei
Ich baue Websites in bis zu 7 Sprachen, auch Arabisch von rechts nach links, gestaltet für jedes Publikum und jeden Bildschirm.
ENHello
FRBonjour
DEHallo
ARمرحبا
Infrastruktur & Deployment
Ich arbeite mit Docker, Linux und Continuous Integration, schütze Formulare (Spam-Schutz, Sicherheits-Header) und deploye auf Vercel, Netlify oder Railway.
❯ docker compose up -d
✔ web · running
✔ db · healthy
✔ proxy · running
❯ git push (deploy)
Werkstatt
/WERKSTATT
Code und Design, die ich von Anfang bis Ende selbst gebaut habe. Jedes Stück zeigt den echten Code und daneben, was er erzeugt.
Laserbeni
Der 3D-Zonenwähler
Auf der Preisseite tippt die Kundin eine Zone eines 3D-Körpers an und sieht ihren Preis. Der Ausgangskörper kommt aus einer freien Bibliothek: Alles, was ihn nutzbar macht, habe ich mir ausgedacht und geschrieben.
Three.js
GLSL
Raycasting
24 Zonen
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 * Die 3D-Szene: lädt den Körper (ein Netz, dessen Punkte jeweils ihre Zone tragen), stellt ihn auf27 * einen Sockel und verarbeitet Maus und Finger (siehe `pointer.ts`): Ziehen dreht den Körper,28 * Antippen wählt eine Zone, Überfahren leuchtet sie auf.29 */30exportfunctioncreateStage(container:HTMLElement,{ model, onPick, onFail }:Handlers):Stage{31const renderer =newWebGLRenderer({ antialias:true, alpha:true});32 renderer.setPixelRatio(Math.min(devicePixelRatio,2));33const canvas = renderer.domElement;34 canvas.style.cssText ="display:block;width:100%;height:100%;touch-action:pan-y;cursor:grab";35 container.appendChild(canvas);3637const scene =newScene();38const camera =newPerspectiveCamera(28,1,0.1,50);39const figure =newGroup();40const sun =newDirectionalLight(0xffe2c4,2);41 sun.position.set(1,2,2);42 scene.add(figure,newAmbientLight(0xffffff,1.4), sun);4344const material =newShaderMaterial({45 vertexShader:VERTEX,46 fragmentShader:FRAGMENT,47 uniforms:{48 uBase:{ value:newColor("#e8ddd0")},49 uGold:{ value:newColor("#c9a064")},50 uGoldLight:{ value:newColor("#e6c48f")},51 uGoldDeep:{ value:newColor("#8a6a38")},52 uTime:{ value:0},53},54});5556let ctx:PickContext|null=null;57let around:Uint32Array[]=[];58let selected:BufferAttribute;59let hovered:BufferAttribute;60let current:ReadonlySet<Zone>=newSet();61let disposed =false;6263newGLTFLoader().load(64 model,65(gltf)=>{66if(disposed)return;67const mesh = gltf.scene.children[0]asMesh;68const geometry = mesh.geometry;69 geometry.computeBoundingBox();70const box = geometry.boundingBox!;71const height = box.max.y - box.min.y;7273const zone = geometry.getAttribute("_zone");74const where = geometry.getAttribute("position");75// Punkte ohne Zone (der Kopf) erhalten ihre aus ihrer Lage76const zoneOf =Float32Array.from({ length: zone.count },(_, i)=> zone.getX(i)||headZone(where.getX(i), where.getY(i), where.getZ(i)));77 around =neighboursOf(geometry.getIndex()!.array, zone.count);78 selected =newBufferAttribute(newFloat32Array(zone.count),1);79 hovered =newBufferAttribute(newFloat32Array(zone.count),1);80 geometry.setAttribute("aSel", selected);81 geometry.setAttribute("aHover", hovered);8283const body =newMesh(geometry, material);84 body.position.z =-(box.min.z + box.max.z)/2;85 figure.add(body);8687 scene.add(...buildPodium(box.min.y, height));8889const tan =Math.tan((camera.fov *Math.PI)/360);90 camera.userData ={ distance:(0.66* height)/ tan, lookY: box.min.y +0.5* height };91 ctx ={ mesh: body, camera, canvas, zoneOf };92show(current);93resize();94},95undefined,96 onFail,97);9899functionshow(selection:ReadonlySet<Zone>){100 current = selection;101if(ctx)paint(selected, ctx.zoneOf, around, selection,4);102}103104functionresize(){105const{ clientWidth: w, clientHeight: h }= container;106if(!w ||!h)return;107 renderer.setSize(w, h,false);108 camera.aspect = w / h;109const{ distance, lookY }= camera.userData as{ distance?: number; lookY?: number };110// der ganze Körper muss in die verfügbare Höhe UND Breite passen111if(distance && lookY !==undefined){112 camera.position.set(0, lookY, distance /Math.min(1, camera.aspect *1.25));113 camera.lookAt(0, lookY,0);114}115 camera.updateProjectionMatrix();116}117const observer =newResizeObserver(resize);118 observer.observe(container);119120const spin =createSpin(figure);121attachPointer(canvas, figure,()=> ctx,{122 onPick,123 onTouch: spin.touch,124 onHover:(zone)=> ctx &&paint(hovered, ctx.zoneOf, around,newSet(zone ?[zone]:[]),3),125});126127let frame =0;128let previous =0;129const loop =(time: number)=>{130 spin.tick(previous ?(time - previous)/1000:0, time);131 previous = time;132 material.uniforms.uTime.value = time /1000;133 renderer.render(scene, camera);134 frame =requestAnimationFrame(loop);135};136 frame =requestAnimationFrame(loop);137138return{139 show,140dispose(){141 disposed =true;142cancelAnimationFrame(frame);143 observer.disconnect();144 renderer.dispose();145 canvas.remove();146},147};148}149
Vorschau
Der gezeigte Code gehört zu dieser Demo. Auf der Laserbeni-Seite kann der Wähler mehr: Rücken, Gesicht, Pakete und Buchung.
Profil
/PROFIL
Full-Stack-Entwickler mit solidem Hintergrund in Netzwerk- und Telekommunikationstechnik. Ich entwerfe und liefere mehrsprachige Webplattformen (Next.js, React, TypeScript, Node.js), von der Datenbank bis zur Oberfläche, mit Tests, automatisiertem Deployment und Sicherheit von Anfang an.
Frontend
Next.js
React
TypeScript
Tailwind CSS
Three.js
Framer Motion
Responsive
UX-/UI-Design
Backend & Daten
Node.js
REST-API
Strapi
Supabase
Sanity
PostgreSQL
Authentifizierung & Mitgliederbereiche
Mehrsprachig, bis zu 7 Sprachen
Qualität & Auslieferung
Automatisierte Tests
GitHub Actions (CI/CD)
Docker
Cloudflare Pages
Vercel
Netlify
PWA
Sicherheit & Zuverlässigkeit
Serverseitige Validierung
Anti-Spam (Turnstile)
Sicherheits-Header
Linux
SSH · Tailscale
Netzwerke und Telekommunikation
KI-gestützte Entwicklung
Geprüfter und getesteter Code
Agentisches Coding
KI-native Workflows
LLM-Integration (Groq, OpenAI)
Sprachen & Vermittlung
Deutsch (C1)
Französisch (Muttersprache)
Englisch (B1)
Pädagogik
Erstellung von Lernmaterial
Homelab
Ein persönlicher Cluster (ZimaBoard, Raspberry Pi), auf dem ich meine eigenen Dienste mit Docker betreibe, um das, was ich online stelle, echt zu testen.
Eine Stelle, ein Projekt? Sprechen wir darüber.
Schreib mir oder plane einen Austausch: Wir sprechen bei einem (virtuellen) Kaffee darüber.
❯ write --to castoleBerlin --:--
Hallo, hier ist Castole! Erzähl mir, was du vorhast: Ich lese jede Nachricht selbst.