Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
// - Git
"git.autofetch": true,

// - TypeScript
"typescript.updateImportsOnFileMove.enabled": "always",

// - Actions on save
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always"
},

// - Lint and format
"editor.formatOnSave": true,
"eslint.format.enable": true,
"eslint.validate": [
"javascript"
],
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "stylelint.vscode-stylelint"
},

// - Common
"editor.tabSize": 2,
}
61 changes: 59 additions & 2 deletions src/components/marketing/landing/visitor/featured/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,70 @@ const Featured: React.FC = () => {
/>
</div>

<div className="relative mt-12 rounded-sm overflow-hidden">
<div className="relative mt-12 rounded-sm">

<div className="absolute -top-20 left-20 z-[-1]">
<svg
Copy link

Copilot AI Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This SVG is purely decorative; add aria-hidden="true" to hide it from assistive technologies.

Copilot uses AI. Check for mistakes.
width="150"
height="150"
viewBox="0 0 171 171"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="rotate-[-15deg]"
>
<g filter="url(#filter0_d_2071_266)">
<path fillRule="evenodd" clipRule="evenodd" d="M55.2564 52.8785C44.3091 56.2504 38.168 67.8584 41.5399 78.8057L52.8784 115.618C56.2503 126.565 67.8583 132.706 78.8055 129.334L115.617 117.995C126.565 114.624 132.706 103.016 129.334 92.0684L117.995 55.2565C114.623 44.3092 103.016 38.1682 92.0683 41.54L55.2564 52.8785ZM93.7871 63.9959C90.8942 62.4654 87.3083 63.5699 85.7779 66.4628L69.9428 96.3946C68.4124 99.2875 69.5168 102.873 72.4097 104.404L77.0866 106.878C79.9795 108.409 83.5653 107.304 85.0958 104.411L100.931 74.4793C102.461 71.5864 101.357 68.0005 98.4639 66.4701L93.7871 63.9959Z" fill="#111111"/>
<path d="M90.5104 36.4805C104.252 32.2483 118.822 39.9572 123.055 53.6986L134.393 90.5105C138.626 104.252 130.917 118.823 117.175 123.055L80.3643 134.393C66.6227 138.626 52.0514 130.917 47.8189 117.175L36.4804 80.3635C32.2481 66.6221 39.957 52.0515 53.6985 47.819L90.5104 36.4805ZM91.3109 68.6752C91.0026 68.5124 90.62 68.63 90.4569 68.9382L74.6219 98.8701C74.4588 99.1783 74.5768 99.5607 74.8849 99.724L79.5626 102.198C79.871 102.361 80.2524 102.244 80.4156 101.935L96.2516 72.0031C96.4143 71.6947 96.2968 71.3122 95.9886 71.1491L91.3109 68.6752Z" stroke="#F7F7F7" strokeWidth="10.5882"/>
</g>
<defs>
<filter id="filter0_d_2071_266" x="0.0244141" y="0.0245361" width="170.825" height="170.825" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="15"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2071_266"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2071_266" result="shape"/>
</filter>
</defs>
</svg>
</div>

<div className="absolute -top-20 right-20 z-[-1]">
<svg
width="150"
height="150"
viewBox="0 0 174 174"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="rotate-[-15deg]"
>
<g filter="url(#filter0_d_2071_267)">
<path fillRule="evenodd" clipRule="evenodd" d="M84.9995 42.6393C74.4663 38.1378 62.2783 43.0274 57.7767 53.5605L42.6395 88.9801C38.1379 99.5133 43.0275 111.701 53.5607 116.203L88.9802 131.34C99.5134 135.842 111.701 130.952 116.203 120.419L131.34 84.9994C135.842 74.4662 130.952 62.2781 120.419 57.7765L84.9995 42.6393ZM107.219 76.0239C106 72.9864 102.55 71.5119 99.5123 72.7305L68.0846 85.3386C65.0471 86.5572 63.5726 90.0074 64.7912 93.0449L66.7612 97.9554C67.9798 100.993 71.43 102.467 74.4675 101.249L105.895 88.6407C108.933 87.4222 110.407 83.9719 109.189 80.9345L107.219 76.0239Z" fill="#2294FF"/>
<path d="M122.5 52.9087C135.721 58.5595 141.859 73.8586 136.208 87.0803L121.071 122.5C115.42 135.721 100.121 141.858 86.8993 136.208L51.4807 121.071C38.2589 115.421 32.121 100.121 37.7716 86.8992L52.9089 51.4797C58.5596 38.2581 73.8587 32.121 87.0804 37.7715L122.5 52.9087ZM102.305 77.9945C102.175 77.6711 101.807 77.5135 101.483 77.6434L70.0555 90.2515C69.7319 90.3814 69.5749 90.7494 69.7044 91.0732L71.6753 95.984C71.8052 96.3079 72.1722 96.4647 72.496 96.3348L103.925 83.727C104.248 83.5969 104.406 83.229 104.276 82.9054L102.305 77.9945Z" stroke="#F7F7F7" strokeWidth="10.5882"/>
</g>
<defs>
<filter id="filter0_d_2071_267" x="0.374512" y="0.37439" width="173.231" height="173.231" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="15"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2071_267"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2071_267" result="shape"/>
</filter>
</defs>
</svg>
</div>

<Image
src={"/static/images/onruntime-team.jpg"}
alt={"Équipe onRuntime Studio"}
width={1024}
height={510}
className="z-[-1] max-h-[510px] object-cover"
className="relative z-0 max-h-[510px] object-cover"
priority
sizes="100vw"
/>
Expand Down
124 changes: 124 additions & 0 deletions src/components/marketing/landing/visitor/reviews/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React from "react";
import Image from "next/image";

// Données extraites pour éviter la répétition
const reviewsData = [
{
id: 1,
text: "Composé de talents variés, allant de développeurs à des designers passionnés. Chacun apporte sa touche unique à nos projets. Ensemble, nous formons un collectif où l'innovation est au cœur de tout ce que nous entreprenons.",
author: "Cécile Pislor",
role: "CEO @ DroitAuCoeurCoaching",
avatar: "/favicon-32x32.png",
},
{
id: 2,
text: "Composé de talents variés, allant de développeurs à des designers passionnés. Chacun apporte sa touche unique à nos projets. Ensemble, nous formons un collectif où l'innovation est au cœur de tout ce que nous entreprenons.",
author: "Cécile Pislor",
role: "CEO @ DroitAuCoeurCoaching",
avatar: "/favicon-32x32.png",
},
{
id: 3,
text: "Composé de talents variés, allant de développeurs à des designers passionnés. Chacun apporte sa touche unique à nos projets. Ensemble, nous formons un collectif où l'innovation est au cœur de tout ce que nous entreprenons.",
author: "Cécile Pislor",
role: "CEO @ DroitAuCoeurCoaching",
avatar: "/favicon-32x32.png",
},
];

const Reviews: React.FC = () => {
return (
<section className="pt-[15em] py-16 px-4 relative min-h-screen">
<div className="max-w-7xl mx-auto relative">
{/* Titre sticky qui reste derrière les commentaires */}
<div className="sticky top-20 text-center z-0 mb-16">
<h2 className="text-black font-semibold text-4xl mb-4 pointer-events-none">
Ce que nos experts disent de nous.
</h2>
<p className="text-gray-600 text-lg pointer-events-none">
On ne les a pas du tout forcés à parler.
</p>
</div>

{/* Grille des avis qui défile devant le titre */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 relative z-10">
{reviewsData.map((review, index) => {
const getTransform = (index: number) => {
switch (index) {
case 0:
return "translate-y-0";
case 1:
return "translate-y-[30rem]";
case 2:
return "translate-y-32";
default:
return "translate-y-0";
}
};

return (
<article
key={review.id}
className={`bg-white rounded-lg p-6 shadow-sm hover:shadow-md transition-all duration-300 ${getTransform(index)} z-10 relative`}
>
<blockquote className="mb-6">
<p className="text-gray-700 italic leading-relaxed">
&ldquo;{review.text}&rdquo;
</p>
</blockquote>
<div className="flex items-center gap-4">
<Image
src={review.avatar}
alt={`Photo de ${review.author}`}
width={60}
height={60}
className="rounded-full"
/>
<div>
<h3 className="font-semibold text-lg text-gray-900">
{review.author}
</h3>
<p className="text-sm text-gray-600">{review.role}</p>
</div>
</div>
</article>
);
})}
</div>

{/* Spacer pour que les cartes aient assez d'espace ET pour révéler le titre à la fin */}
<div className="h-[40rem]"></div>
</div>

{/* Témoignage Lucas Bodin */}
<div className="max-w-7xl mx-auto px-4 py-16 gap-8">
<div className="flex justify-center items-center gap-16 max-w-5xl mx-auto">
<Image
src="/static/images/members/lucas-bodin.jpg"
alt="Photo Lucas"
width={150}
height={150}
className="rounded-lg rotate-[-15deg] shadow-lg bg-white px-2 pt-2 pb-6 transition-all duration-300 ease-in-out hover:rotate-0 hover:scale-105"
/>
<div className="flex-1">
<p className="text-2xl md:text-3xl font-medium text-black leading-relaxed mb-6">
&ldquo;onRuntime Studio c&rsquo;est bien plus qu&rsquo;un simple
studio de créateurs. C&rsquo;est un espace où l&rsquo;innovation,
la collaboration, et la passion se lient pour donner vie à des
projets uniques. Nous croyons au pouvoir de l&rsquo;intelligence
collective, où chaque créateur, apporte sa vision afin
qu&rsquo;ensemble, nous transformions des idées en
réalités.&rdquo;
</p>
<p className="text-lg text-gray-600">
Lucas Bodin, Head of Design @{" "}
<strong className="text-black">onRuntime Studio</strong>
</p>
</div>
</div>
</div>
</section>
);
};

export default Reviews;
118 changes: 118 additions & 0 deletions src/components/marketing/landing/visitor/services/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
'use client';

import React, { useEffect, useRef, useState } from 'react';
import Link from 'next/link';

const colorSets = [
{
text: 'text-black',
button: 'bg-black text-white',
},
{
text: 'text-[#652599]',
button: 'bg-[#652599] text-white',
},
{
text: 'text-[#23cd82]',
button: 'bg-[#23cd82] text-white',
},
{
text: 'text-[#ee2400]',
button: 'bg-[#ee2400] text-white',
},
];

const services = [
{
color: '#652599',
title: 'Design UX UI/Branding',
description:
'Moodboard, maquettage UI complet, prototypage et identité graphique faite maison.',
},
{
color: '#23cd82',
title: 'Développement Front & Back End',
description:
'Développement from-scratch côté front (web, app) et back (API, bots).',
},
{
color: '#ee2400',
title: 'Intégrations Web',
description:
'Création ou adaptation de projets avec Wordpress, Shopify, Webflow, etc.',
},
];

const Services: React.FC = () => {
const [colorIndex, setColorIndex] = useState(0);
const containerRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
const handleScroll = () => {
if (!containerRef.current) return;

const containerTop = containerRef.current.getBoundingClientRect().top + window.scrollY;
const scrollY = window.scrollY;
const earlyOffset = 25;
const relativeY = scrollY - containerTop + earlyOffset;

const sectionHeight = 300;
let index = Math.floor(relativeY / sectionHeight);
index = Math.max(0, Math.min(index, colorSets.length - 1));
Copy link

Copilot AI Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clamp uses colorSets.length but services.length is 3 while colorSets has 4 entries, leading to an extra color state with no corresponding service. Consider clamping against services.length - 1 or aligning the two arrays.

Suggested change
index = Math.max(0, Math.min(index, colorSets.length - 1));
index = Math.max(0, Math.min(index, services.length - 1));

Copilot uses AI. Check for mistakes.

setColorIndex(index);
};

window.addEventListener('scroll', handleScroll);
Copy link

Copilot AI Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throttling or debouncing the scroll handler would improve performance by reducing the number of state updates during rapid scrolling.

Copilot uses AI. Check for mistakes.
handleScroll();

return () => window.removeEventListener('scroll', handleScroll);
}, []);

const colors = colorSets[colorIndex];

return (
<div className="mt-8 flex justify-center gap-24 px-4" ref={containerRef}>
{/* Bloc gauche (texte + bouton) */}
<div className="flex flex-col gap-4 sticky top-24 self-start transition-all duration-500">
<h2
className={`font-semibold text-3xl transition-colors duration-1000 ease-out ${colors.text}`}
>
Un studio qui combine <br />
les savoirs-faire créatifs
</h2>
<p>
Nous sommes experts dans nos domaines : <br />
développement web, design d&apos;interface, audits ...
</p>
<Link href="/">
<button
className={`mt-8 px-6 py-2 rounded-md shadow hover:opacity-80 transition-all duration-500 ${colors.button}`}
>
En savoir plus
</button>
</Link>
</div>

{/* Bloc droite (services) */}
<div className="mt-44 w-[400px]">
<p className="mb-8">
Nos projets allient nos compétences afin qu’ils soient resplendissants sur le web.
</p>

{services.map((service, index) => (
<div key={index} style={{ minHeight: '300px' }}>
Comment on lines +103 to +104
Copy link

Copilot AI Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the array index as a key can cause rendering issues if items ever change order. Consider using a unique property (e.g., service.title) or adding an explicit id field.

Suggested change
{services.map((service, index) => (
<div key={index} style={{ minHeight: '300px' }}>
{services.map((service) => (
<div key={service.id} style={{ minHeight: '300px' }}>

Copilot uses AI. Check for mistakes.
<div
className="mb-4 p-6 rounded-xl h-[200px]"
style={{ backgroundColor: service.color }}
></div>
<h3 className="font-semibold text-2xl">{service.title}</h3>
<p className="mb-8">{service.description}</p>
</div>
))}
</div>
</div>
);
};

export default Services;
36 changes: 36 additions & 0 deletions src/components/marketing/landing/visitor/tonightpass/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from "react";
import Link from 'next/link';
import Image from 'next/image';

const Tonightpass: React.FC = () => {
return(
<div className="flex flex-col items-center h-auto min-h-screen bg-black pt-16">
<div className="sticky top-20 z-50">
<Link href="/projects/tonightpass">
<button className="bg-white text-black px-6 py-2 rounded-md shadow hover:bg-gray-100">
Notre success story 🏆
</button>
</Link>
</div>
<h2 className="text-white font-semibold text-center mt-8 text-4xl">Tonight Pass, <br /> la billetterie 2.0</h2>
<p className="text-white text-center mt-8">Réservez votre entrée, entrez rapidement, <br />restez en contact avec les gens.</p>
<strong className="text-white text-center mt-8">En quelques clics depuis votre canapé 🛋️</strong>
<Link href="/projects/tonightpass">
<button className="mt-8 bg-white text-black px-6 py-2 rounded-md shadow hover:bg-gray-100">
En savoir plus
</button>
</Link>
<div className="w-full max-w-screen-lg mt-8 mb-8">
<Image
src="/static/images/projects/tonightpass/showcase.jpg"
alt="Showcase Tonight Pass"
width={1000}
height={800}
className="w-full h-auto rounded-md"
/>
</div>
</div>
)
};

export default Tonightpass;
Loading