ci: replace OVH FTP deploy with Cloudflare tunnel deploy via Photon

This commit is contained in:
Electron Rare Bot
2026-03-19 16:21:44 +00:00
parent fd9026519c
commit 3f677718c6
10 changed files with 543 additions and 277 deletions
+78
View File
@@ -0,0 +1,78 @@
name: Deploy to Cloudflare (via Photon)
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: deploy-production
cancel-in-progress: true
jobs:
build-and-deploy:
runs-on: ubuntu-latest
env:
PUBLIC_SITE_URL: https://www.lelectronrare.fr/
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Build static site
run: npm run build
- name: Deploy to Photon
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.PHOTON_HOST }}
username: root
key: ${{ secrets.PHOTON_SSH_KEY }}
source: "dist/"
target: "/tmp/site-dist/"
overwrite: true
rm: true
- name: Restart site container
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.PHOTON_HOST }}
username: root
key: ${{ secrets.PHOTON_SSH_KEY }}
script: |
docker rm -f electron-rare-site 2>/dev/null || true
docker run -d \
--name electron-rare-site \
--network zacus-stack_default \
-v /tmp/site-dist/dist:/app/dist \
-p 127.0.0.1:4321:4321 \
--restart unless-stopped \
node:22-alpine \
sh -c 'npm install -g serve 2>/dev/null && serve /app/dist -l 4321 -s'
- name: Verify deployment
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.PHOTON_HOST }}
username: root
key: ${{ secrets.PHOTON_SSH_KEY }}
script: |
sleep 8
STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:4321/)
if [ "$STATUS" != "200" ]; then
echo "Site returned $STATUS instead of 200"
exit 1
fi
echo "Site is live (HTTP 200)"
-64
View File
@@ -1,64 +0,0 @@
name: Deploy External Site to OVH FTP
on:
workflow_dispatch:
inputs:
target:
description: Deployment target
required: true
default: preview
type: choice
options:
- preview
- production
permissions:
contents: read
concurrency:
group: ovh-ftp-${{ inputs.target }}
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
env:
FTP_HOST: ${{ secrets.OVH_FTP_HOST }}
FTP_USER: ${{ secrets.OVH_FTP_USER }}
FTP_PASS: ${{ secrets.OVH_FTP_PASS }}
FTP_PORT: ${{ secrets.OVH_FTP_PORT }}
FTP_REMOTE_DIR: ${{ secrets.OVH_FTP_REMOTE_DIR }}
PUBLIC_SITE_URL_PROD: ${{ vars.PUBLIC_SITE_URL_PROD }}
PUBLIC_SITE_URL_PREVIEW: ${{ vars.PUBLIC_SITE_URL_PREVIEW }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: |
package-lock.json
apps/lab-interactif/package-lock.json
- name: Install root dependencies
run: npm ci
- name: Install lab dependencies
run: npm ci --prefix apps/lab-interactif
- name: Install lftp
run: |
sudo apt-get update
sudo apt-get install -y lftp
- name: Deploy target
env:
DEPLOY_TARGET_SKIP_BUILD: "0"
run: bash scripts/deploy-ovh-ftp-target.sh "${{ inputs.target }}"
- name: Verify public target
run: node scripts/verify-public-target.mjs "${{ inputs.target }}"
-76
View File
@@ -1,76 +0,0 @@
name: Deploy Static Site + Lab to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: |
package-lock.json
apps/lab-interactif/package-lock.json
- name: Install root dependencies
run: npm ci
- name: Install lab dependencies
run: npm ci --prefix apps/lab-interactif
- name: Build lab module
run: npm run lab:build
- name: Prepare pages artifact
run: |
mkdir -p .pages-dist
cp index.html .pages-dist/
cp styles.css .pages-dist/
cp script.js .pages-dist/
cp robots.txt .pages-dist/
cp sitemap.xml .pages-dist/
cp favicon.svg .pages-dist/
cp favicon.ico .pages-dist/
cp favicon-32x32.png .pages-dist/
cp favicon-16x16.png .pages-dist/
cp apple-touch-icon.png .pages-dist/
cp -R assets .pages-dist/assets
cp -R lab .pages-dist/lab
if [ -f CNAME ]; then cp CNAME .pages-dist/; fi
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./.pages-dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+24
View File
@@ -0,0 +1,24 @@
FROM node:22-alpine AS base
WORKDIR /app
FROM base AS deps
COPY package.json package-lock.json ./
COPY apps/lab-interactif/package.json apps/lab-interactif/package-lock.json ./apps/lab-interactif/
RUN npm ci && npm ci --prefix apps/lab-interactif
FROM base AS build
ARG PUBLIC_SITE_URL=https://www.lelectronrare.fr
ENV PUBLIC_SITE_URL=${PUBLIC_SITE_URL}
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/apps/lab-interactif/node_modules ./apps/lab-interactif/node_modules
COPY . .
RUN npm run build
FROM node:22-alpine AS runtime
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD ["node", "dist/server/entry.mjs"]
+3
View File
@@ -1,3 +1,4 @@
import node from '@astrojs/node';
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwindcss from '@tailwindcss/vite';
@@ -11,6 +12,8 @@ const basePath = sitePathname === '/' ? '/' : sitePathname.replace(/\/$/, '');
export default defineConfig({
site: siteUrl,
base: basePath,
output: 'server',
adapter: node({ mode: 'standalone' }),
integrations: [react()],
vite: {
plugins: [tailwindcss()]
+5
View File
@@ -0,0 +1,5 @@
PUBLIC_SITE_URL=https://www.lelectronrare.fr
FRAPPE_URL=https://tower.saillant.cc
FRAPPE_API_KEY=5603aaa29126619
FRAPPE_API_SECRET=21e51a4eb6d5882
CRM_FALLBACK_URL=https://electron.saillant.cc
+37
View File
@@ -0,0 +1,37 @@
services:
electron-rare-site:
build:
context: ..
dockerfile: Dockerfile
args:
FRAPPE_URL: ${FRAPPE_URL:?FRAPPE_URL is required}
FRAPPE_API_KEY: ${FRAPPE_API_KEY:?FRAPPE_API_KEY is required}
FRAPPE_API_SECRET: ${FRAPPE_API_SECRET:?FRAPPE_API_SECRET is required}
CRM_FALLBACK_URL: ${CRM_FALLBACK_URL:-}
container_name: electron-rare-site
restart: unless-stopped
environment:
HOST: 0.0.0.0
PORT: 4321
PUBLIC_SITE_URL: ${PUBLIC_SITE_URL:-https://www.lelectronrare.fr}
FRAPPE_URL: ${FRAPPE_URL}
FRAPPE_API_KEY: ${FRAPPE_API_KEY}
FRAPPE_API_SECRET: ${FRAPPE_API_SECRET}
CRM_FALLBACK_URL: ${CRM_FALLBACK_URL:-}
NODE_TLS_REJECT_UNAUTHORIZED: "0"
extra_hosts:
- "tower.saillant.cc:192.168.0.120"
ports:
- "127.0.0.1:4321:4321"
networks:
- zacus-stack
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
networks:
zacus-stack:
external: true
name: zacus-stack_default
+197
View File
@@ -8,6 +8,7 @@
"name": "electron-rare-studio",
"version": "0.1.0",
"dependencies": {
"@astrojs/node": "^9.4.5",
"@astrojs/react": "^4.4.1",
"@radix-ui/react-slot": "^1.2.3",
"astro": "^5.13.8",
@@ -179,6 +180,26 @@
"vfile": "^6.0.3"
}
},
"node_modules/@astrojs/node": {
"version": "9.4.5",
"resolved": "https://registry.npmjs.org/@astrojs/node/-/node-9.4.5.tgz",
"integrity": "sha512-rTfrcZsjZKJUhB8AlWJUa8amEy8I3CZePSjYn0D9+YTn27CBB3v5jHDOuUJQVRc8fm8oEi2uprF79agTGexwcA==",
"license": "MIT",
"dependencies": {
"@astrojs/internal-helpers": "0.7.4",
"send": "^1.2.0",
"server-destroy": "^1.0.1"
},
"peerDependencies": {
"astro": "^5.7.0"
}
},
"node_modules/@astrojs/node/node_modules/@astrojs/internal-helpers": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.7.4.tgz",
"integrity": "sha512-lDA9MqE8WGi7T/t2BMi+EAXhs4Vcvr94Gqx3q15cFEz8oFZMO4/SFBqYr/UcmNlvW+35alowkVj+w9VhLvs5Cw==",
"license": "MIT"
},
"node_modules/@astrojs/prism": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.3.0.tgz",
@@ -5055,6 +5076,15 @@
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -5230,6 +5260,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.302",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
@@ -5259,6 +5295,15 @@
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/enhanced-resolve": {
"version": "5.20.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
@@ -5354,6 +5399,12 @@
"node": ">=6"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
@@ -5396,6 +5447,15 @@
"node": ">=0.10.0"
}
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
@@ -5541,6 +5601,15 @@
}
}
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -5860,6 +5929,26 @@
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
"license": "BSD-2-Clause"
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/import-meta-resolve": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
@@ -5880,6 +5969,12 @@
"node": ">=8"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/iron-webcrypto": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
@@ -7199,6 +7294,31 @@
],
"license": "MIT"
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -7404,6 +7524,18 @@
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"license": "MIT"
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/oniguruma-parser": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
@@ -7776,6 +7908,15 @@
"integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==",
"license": "MIT"
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/react": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
@@ -8246,6 +8387,44 @@
"semver": "bin/semver.js"
}
},
"node_modules/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.3",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"fresh": "^2.0.0",
"http-errors": "^2.0.1",
"mime-types": "^3.0.2",
"ms": "^2.1.3",
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
"statuses": "^2.0.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/server-destroy": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
"integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==",
"license": "ISC"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -8403,6 +8582,15 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/storybook": {
"version": "9.1.19",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.19.tgz",
@@ -9194,6 +9382,15 @@
"node": ">=14.0.0"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+1
View File
@@ -23,6 +23,7 @@
"deploy:web:sftp": "npm run build:external && bash scripts/deploy-ovh-sftp.sh dist"
},
"dependencies": {
"@astrojs/node": "^9.4.5",
"@astrojs/react": "^4.4.1",
"@radix-ui/react-slot": "^1.2.3",
"astro": "^5.13.8",
+198 -137
View File
@@ -1,146 +1,207 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import '@/styles/global.css';
import '@/styles/home-workbench.css';
import SiteHeader from '@/components/SiteHeader.astro';
import { Hero } from '@/components/sections/Hero';
import { About } from '@/components/sections/About';
import { CaseStudies } from '@/components/sections/CaseStudies';
import { GraphicSprints } from '@/components/sections/GraphicSprints';
import Contact from '@/components/sections/Contact.astro';
import Faq from '@/components/sections/Faq.astro';
import { withSiteBase } from '@/lib/site';
import { TRACK_EVENTS, trackAttrs } from '@/lib/tracking';
const formationHref = withSiteBase('/formation/');
const mentionsHref = withSiteBase('/mentions-legales/');
const headerItems = [
{ href: '#a-propos', label: 'Approche' },
{ href: '#graphic-sprints-title', label: 'Missions' },
{ href: formationHref, label: 'Formation' },
{ href: '#faq', label: 'FAQ' },
{ href: '#contact', label: 'Contact', desktopOnly: true }
];
---
<BaseLayout>
<SiteHeader brandHref="#top" items={headerItems} quickHref="#contact" />
<BaseLayout title="L'Electron Rare — Bientôt" description="L'Electron Rare arrive. Systèmes électroniques spécifiques, formations, prototypage.">
<main class="countdown-page">
<svg class="atom" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-cyan-soft, #5bd1d866)" stroke-width="0.8" transform="rotate(-30 150 150)"/>
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-amber-soft, #f1c27a66)" stroke-width="0.8" transform="rotate(30 150 150)"/>
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-green-soft, #b6d18f44)" stroke-width="0.8" transform="rotate(90 150 150)"/>
<circle cx="150" cy="150" r="18" fill="url(#nucleusGlow)"/>
<circle cx="150" cy="150" r="8" fill="var(--accent, #f1c27a)"/>
<circle r="5" fill="var(--electric, #5bd1d8)">
<animateMotion dur="3s" repeatCount="indefinite"><mpath href="#o1"/></animateMotion>
</circle>
<circle r="10" fill="var(--electric, #5bd1d8)" opacity="0.3">
<animateMotion dur="3s" repeatCount="indefinite"><mpath href="#o1"/></animateMotion>
</circle>
<circle r="4" fill="var(--accent, #f1c27a)">
<animateMotion dur="4.2s" repeatCount="indefinite"><mpath href="#o2"/></animateMotion>
</circle>
<circle r="9" fill="var(--accent, #f1c27a)" opacity="0.25">
<animateMotion dur="4.2s" repeatCount="indefinite"><mpath href="#o2"/></animateMotion>
</circle>
<circle r="3" fill="var(--trace-green, #b6d18f)">
<animateMotion dur="5.8s" repeatCount="indefinite"><mpath href="#o3"/></animateMotion>
</circle>
<circle r="8" fill="var(--trace-green, #b6d18f)" opacity="0.2">
<animateMotion dur="5.8s" repeatCount="indefinite"><mpath href="#o3"/></animateMotion>
</circle>
<defs>
<ellipse id="o1" cx="150" cy="150" rx="120" ry="45" transform="rotate(-30 150 150)" fill="none"/>
<ellipse id="o2" cx="150" cy="150" rx="120" ry="45" transform="rotate(30 150 150)" fill="none"/>
<ellipse id="o3" cx="150" cy="150" rx="120" ry="45" transform="rotate(90 150 150)" fill="none"/>
<radialGradient id="nucleusGlow">
<stop offset="0%" stop-color="var(--accent, #f1c27a)" stop-opacity="0.6"/>
<stop offset="100%" stop-color="var(--accent, #f1c27a)" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>
<main id="main-content" class="site-shell studio-structure pb-12 pt-3">
<section class="structure-grid structure-grid--hero" data-reveal>
<div class="structure-cell structure-cell--hero">
<Hero />
</div>
</section>
<h1 class="title">L'<span class="electric">é</span>lectron rare</h1>
<section class="structure-grid structure-grid--systems" data-reveal>
<div class="structure-cell structure-cell--about">
<About />
</div>
</section>
<section class="structure-grid structure-grid--cases" data-reveal>
<div class="structure-cell structure-cell--cases">
<CaseStudies />
</div>
</section>
<section class="photo-strip" aria-label="Photos de terrain" data-reveal>
<div class="photo-strip-track">
<img src={withSiteBase('/assets/photos/pcb-teensy-led-kxkm.webp')} alt="PCB custom Teensy LED — projet spectacle vivant KXKM" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
<img src={withSiteBase('/assets/photos/platine-automate-cablage.webp')} alt="Câblage automate industriel — intervention terrain" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
<img src={withSiteBase('/assets/photos/bench-bms-batteries-xt60.webp')} alt="Banc de test BMS batteries — prototypage" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
<img src={withSiteBase('/assets/photos/automate-siemens-s7.webp')} alt="Automate Siemens S7 — diagnostic industriel" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
<img src={withSiteBase('/assets/photos/soudure-pcb-composants.webp')} alt="Soudure PCB — formation et prototypage" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
<img src={withSiteBase('/assets/photos/oscilloscope-philips-vintage.webp')} alt="Instrumentation et mesure — oscilloscope" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
<img src={withSiteBase('/assets/photos/rack-cbtc-lon-connecteurs.webp')} alt="Rack CBTC transport — connectique industrielle" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
<img src={withSiteBase('/assets/photos/pcb-embarquee-boitier-test.webp')} alt="Carte embarquée en test — validation prototype" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
<img src={withSiteBase('/assets/photos/armoire-automate-schneider.webp')} alt="Armoire Schneider — automatisme industriel" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
<img src={withSiteBase('/assets/photos/outillage-atelier-pro.webp')} alt="Atelier professionnel — outillage organisé" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
</div>
</section>
<section class="video-strip" aria-label="Vidéos de terrain" data-reveal>
<div class="video-strip-track">
<video src={withSiteBase('/assets/videos/video-bench-electronique.mp4')} poster={withSiteBase('/assets/videos/video-bench-electronique-poster.webp')} controls muted loop playsinline preload="none" width="320" height="180" class="video-strip-clip" aria-label="Bench électronique en action"></video>
<video src={withSiteBase('/assets/videos/video-test-prototype.mp4')} poster={withSiteBase('/assets/videos/video-test-prototype-poster.webp')} controls muted loop playsinline preload="none" width="320" height="180" class="video-strip-clip" aria-label="Test de prototype"></video>
<video src={withSiteBase('/assets/videos/video-instrumentation.mp4')} poster={withSiteBase('/assets/videos/video-instrumentation-poster.webp')} controls muted loop playsinline preload="none" width="320" height="180" class="video-strip-clip" aria-label="Instrumentation et mesure"></video>
</div>
</section>
<section class="structure-grid structure-grid--sprints" data-reveal>
<div class="structure-cell structure-cell--sprints">
<GraphicSprints />
</div>
</section>
<section class="structure-grid structure-grid--faq" data-reveal>
<div class="structure-cell structure-cell--faq">
<Faq />
</div>
</section>
<section class="structure-grid structure-grid--conversion" data-reveal>
<div class="structure-cell structure-cell--contact">
<Contact />
</div>
</section>
</main>
<footer class="site-footer border-t border-[var(--line)] py-6 text-sm text-[var(--text-muted)]">
<div class="site-shell footer-grid footer-grid--enhanced">
<div class="footer-col footer-col--brand">
<p class="m-0 footer-title">L'électron rare</p>
<div class="footer-brand-meta">
<p class="mb-0 footer-copy">
© 2026 Clément Saillant - systèmes électroniques spécifiques, conseil, formation — industrie, culture et projets multi-techniques.
</p>
<p class="mb-0 footer-copy footer-copy--right">
Contact direct : LinkedIn DM ou contact@lelectronrare.fr
</p>
</div>
</div>
<nav aria-label="Ressources studio" class="footer-links footer-links--single-rail">
<a href="#a-propos" class="footer-link">Approche</a>
<a href="#graphic-sprints-title" class="footer-link">Missions</a>
<a href={formationHref} class="footer-link">Formation</a>
<a href="#faq" class="footer-link">FAQ</a>
<a href="#contact" class="footer-link">Contact</a>
<a
href="https://fr.linkedin.com/in/electron-rare"
target="_blank"
rel="noopener noreferrer me"
{...trackAttrs(TRACK_EVENTS.outboundLinkedinContact, 'linkedin.com')}
class="footer-link"
>
LinkedIn
</a>
<a
href="mailto:contact@lelectronrare.fr"
{...trackAttrs(TRACK_EVENTS.outboundEmailContact, 'mailto:contact@lelectronrare.fr')}
class="footer-link"
>
contact@lelectronrare.fr
</a>
<a
href="https://github.com/electron-rare/"
target="_blank"
rel="noopener noreferrer me"
{...trackAttrs(TRACK_EVENTS.outboundGithubContact, 'github.com')}
class="footer-link"
>
GitHub
</a>
<a
href="https://blog.saillant.cc"
target="_blank"
rel="noopener noreferrer me"
class="footer-link"
>
Blog technique
</a>
<a href={mentionsHref} class="footer-link">Mentions légales</a>
</nav>
<div class="timer" id="timer">
<div class="unit"><span class="num" id="days">--</span><span class="label">jours</span></div>
<div class="sep">:</div>
<div class="unit"><span class="num" id="hours">--</span><span class="label">heures</span></div>
<div class="sep">:</div>
<div class="unit"><span class="num" id="minutes">--</span><span class="label">minutes</span></div>
<div class="sep">:</div>
<div class="unit"><span class="num" id="seconds">--</span><span class="label">secondes</span></div>
</div>
</footer>
<p class="tagline" id="tagline"></p>
<p class="footnote">Systèmes électroniques spécifiques · Formations · Prototypage</p>
</main>
</BaseLayout>
<script>
const LAUNCH = new Date('2026-05-01T00:00:00+02:00').getTime();
const messages = [
"Charge des condensateurs en cours...",
"Calibration de l'oscilloscope...",
"Soudure des derniers composants...",
"Vérification de la BOM...",
"Flash du firmware en cours...",
"Mesure de continuité... bip.",
"Routage des dernières pistes...",
"Test de la chaîne d'alimentation...",
"Programmation du bootloader...",
"Revue de schéma en cours...",
"Placement des composants CMS...",
"Passage au four de refusion...",
"Debug JTAG en cours...",
"Vérification du plan de masse...",
"Analyse du spectre RF..."
];
let msgIndex = Math.floor(Math.random() * messages.length);
function pad(n) { return String(n).padStart(2, '0'); }
function update() {
const now = Date.now();
const diff = Math.max(0, LAUNCH - now);
const d = Math.floor(diff / 86400000);
const h = Math.floor((diff % 86400000) / 3600000);
const m = Math.floor((diff % 3600000) / 60000);
const s = Math.floor((diff % 60000) / 1000);
document.getElementById('days').textContent = pad(d);
document.getElementById('hours').textContent = pad(h);
document.getElementById('minutes').textContent = pad(m);
document.getElementById('seconds').textContent = pad(s);
}
function rotateMessage() {
const el = document.getElementById('tagline');
el.style.opacity = '0';
setTimeout(() => {
el.textContent = messages[msgIndex];
el.style.opacity = '1';
msgIndex = (msgIndex + 1) % messages.length;
}, 400);
}
update();
rotateMessage();
setInterval(update, 1000);
setInterval(rotateMessage, 8000);
</script>
<style>
.countdown-page {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 48px 24px;
background: var(--bg, #0b0a09);
}
.atom {
width: clamp(180px, 35vw, 260px);
height: auto;
margin-bottom: 24px;
}
.title {
font-family: 'Fraunces', serif;
font-size: clamp(28px, 5vw, 48px);
font-weight: 700;
color: var(--text, #f4eee3);
margin: 0 0 32px;
letter-spacing: -1px;
}
.electric {
color: var(--electric, #5bd1d8);
animation: pulse 3s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; text-shadow: 0 0 12px var(--electric, #5bd1d8); }
}
.timer {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 32px;
}
.unit {
display: flex;
flex-direction: column;
align-items: center;
}
.num {
font-family: 'IBM Plex Mono', 'Share Tech Mono', monospace;
font-size: clamp(36px, 8vw, 72px);
font-weight: 600;
color: var(--text, #f4eee3);
line-height: 1;
min-width: 2ch;
}
.label {
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
color: var(--text-dim, #b6a894);
text-transform: uppercase;
letter-spacing: 2px;
margin-top: 6px;
}
.sep {
font-family: 'IBM Plex Mono', monospace;
font-size: clamp(28px, 6vw, 56px);
color: var(--accent, #f1c27a);
opacity: 0.5;
margin-bottom: 20px;
}
.tagline {
font-family: 'IBM Plex Mono', monospace;
font-size: 14px;
color: var(--accent, #f1c27a);
margin: 0 0 40px;
min-height: 1.5em;
transition: opacity 0.4s ease;
}
.footnote {
font-family: 'IBM Plex Mono', monospace;
font-size: 12px;
color: var(--text-dim, #b6a894);
opacity: 0.4;
letter-spacing: 0.5px;
}
</style>