24 Commits

Author SHA1 Message Date
syoul
9fb80ef59f fix: sbom-generate - inclure node_modules pour SBOM complet
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Retirer l'exclusion des node_modules permet a Syft de scanner
les paquets reellement installes (transitifs inclus).
Seuls les artefacts de build sont exclus (build/, .next/, out/).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 20:42:49 +01:00
syoul
53ceb29bbe fix: sbom-generate - scanner le repertoire source au lieu de l'image Docker
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Les vars CI (CI_REPO_OWNER etc.) ne sont pas injectees dans les steps
avec volumes: sans environment: (bug Woodpecker next). Supprimer le
docker socket et scanner dir:. evite le probleme et donne un SBOM
complet des dependances npm declarees.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 19:20:47 +01:00
syoul
7862bb11b7 fix: sbom-generate - calculer le nom image depuis les vars CI
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Remplace la lecture de .env.deploy par un recalcul direct
depuis CI_REPO_OWNER/CI_REPO_NAME/CI_COMMIT_BRANCH (meme
logique que write-env), evitant la dependance sur le fichier.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 19:13:00 +01:00
syoul
42b3d28505 feat: integrer SBOM dans la pipeline CI
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- sbom-generate: Syft scanne l'image Docker buildee (radar-business)
- sbom-scan: Trivy CVE depuis le SBOM (cache /home/syoul/trivy-cache)
- sbom-publish: envoi vers Dependency-Track (dtrack.syoul.fr)
Nouveau secret requis: dependency_track_token

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 19:04:20 +01:00
syoul
658be24b7c fix: corriger les deux erreurs de build CI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- scripts/build-radar.js: bash → sh (Alpine n'a pas bash)
- radar-app/package.json: ajouter postcss comme dépendance directe
- radar-app/package-lock.json: régénéré avec postcss ^8.5.3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:51:49 +01:00
syoul
a2fbbe5d44 fix: restaurer cd .. après radar-app (shell partagé dans Woodpecker)
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Les commandes d'un step partagent le même shell dans Woodpecker.
cd radar-app persiste aux commandes suivantes — cd .. est nécessaire.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:43:50 +01:00
syoul
e1447aca28 fix: resynchroniser radar-app/package-lock.json + sous-shell Woodpecker
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- radar-app/package-lock.json : lock file désynchronisé (Next.js 15 -> 16), ajout au suivi git
- .woodpecker.yml : supprimer && cd .. inutile (chaque commande = sous-shell séparé dans Woodpecker next)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:41:04 +01:00
syoul
4755b392a3 fix: resynchroniser package-lock.json avec package.json
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
npm ci échouait en CI : lock file désynchronisé après renommage du package.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:38:14 +01:00
syoul
445a540d54 ci: trigger pipeline Woodpecker
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:37:08 +01:00
syoul
1dd4b4d1d1 ci: trigger pipeline Woodpecker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:35:08 +01:00
syoul
7bb7484ec8 ci: corrections conformité prestashop
- Ajout step validate (docker compose config avec placeholder)
- Remplacement export \$(cat .env.deploy | xargs) → --env-file .env.deploy
- SERVICE_3000_TAGS : urlprefix-DOMAIN/ → urlprefix-DOMAIN/* (match tous les paths)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:33:40 +01:00
syoul
940834d993 refactor: COMPOSE_PROJECT_NAME dynamique depuis les vars CI
docker-compose.business.yml:
- Ajout name: ${COMPOSE_PROJECT_NAME:-ajr-techradardev-main}
- container_name et SERVICE_3000_NAME utilisent COMPOSE_PROJECT_NAME

.woodpecker.yml:
- write-env génère COMPOSE_PROJECT_NAME=owner-repo-branch (même convention que prestashop)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:31:05 +01:00
syoul
fa0aa808ac feat: intégration Fabio/Consul/Registrator + TLS acme.sh
docker-compose.business.yml:
- Suppression version: obsolete et ports: (Fabio gère le routing)
- Ajout labels SERVICE_3000_* pour Registrator/Consul/Fabio
- Ajout LETSENCRYPT_HOST pour sonic-acme-1
- Ajout réseau sonic (externe, partagé avec la stack)

.woodpecker.yml:
- Ajout write-env (RADAR_DOMAIN depuis secret, séparé car from_secret + volumes incompatibles)
- Ajout TLS acme.sh dans deploy (idempotent, exit 2 = skip)
- Healthcheck sur https://RADAR_DOMAIN (100s max)

Secret Woodpecker à créer : radar_domain

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:29:39 +01:00
syoul
002764ea9a ci: aligner pipeline sur le modèle de référence (fabio/consul/registrator)
- Suppression de deploy-rsync (hors stack)
- Renommage deploy-docker → deploy
- Ajout healthcheck (curl localhost:3006, max 100s)
- Remplacement notify Telegram/vars Drone → notify-failure avec vars CI
  ($CI_BUILD_NUMBER, $CI_COMMIT_SHA, $CI_COMMIT_BRANCH)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:26:52 +01:00
syoul
b4d7f7e10f docs: ajouter section "Où éditer le contenu" dans le README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:21:03 +01:00
syoul
029fbe5d26 docs: clarifier structure du projet dans le README
- Distinguer clairement radar-business/ (source) vs radar/ (temporaire)
- Indiquer data/ comme dossier de build versionné (suite déplacement docs/data/)
- Corriger branche principale (stand-alone → main)
- Simplifier section Documentation (docs/ non versionnée)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:18:41 +01:00
syoul
fe16d01be7 refactor: déplacer docs/data/ → data/ pour séparer données de build et docs
docs/ est entièrement ignoré par .gitignore, mais docs/data/team/*.md
est nécessaire au build (generate-team-visualization-data.js).
Déplacement vers data/ à la racine pour que ces fichiers soient
versionnés et disponibles lors du déploiement depuis le dépôt.

- Nouveau dossier data/ versionné (profils équipe, technologies)
- docs/ entièrement ignoré (documentation humaine uniquement)
- Mise à jour des 4 références dans les scripts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:14:56 +01:00
syoul
7c4204c689 ci: correction syntaxe Woodpecker (.woodpecker.yml)
- Suppression des headers Drone CI (kind/type/name)
- Déplacement du trigger when: au niveau global
- Volumes inline (host:container) au lieu des named volumes
- Suppression de la section volumes: Drone en bas de fichier
- Correction $$ → $ pour les variables dans les commandes
- Chaînage des cd avec && pour les commandes radar-app
- Image docker:27-cli cohérente avec le modèle de référence

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:03:33 +01:00
syoul
27685ee250 docs: mise à jour du README et ajout de la configuration CI Woodpecker
- Réécriture complète du README pour le projet stand-alone Techradar Laplank
- Ajout de .woodpecker.yml pour la pipeline CI/CD
- Mise à jour du .gitignore pour exclure les dossiers docs/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 17:34:12 +01:00
syoul
236b8fe037 refactor: renommer le package de 'aoe-techradar' à 'techradar-laplank'
- Mise à jour du nom dans package.json
- Mise à jour du nom dans package-lock.json
- Le nom reflète mieux l'identité du projet Laplank

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 18:52:59 +01:00
syoul
e9fd40f27d fix: résolution des marqueurs de conflit dans radar-app/package.json
- Conservation de Next.js 16.1.6 et eslint-config-next 16.1.6

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 18:37:40 +01:00
syoul
6c304d461c merge: synchroniser stand-alone avec main (refactorisation stand-alone + migration Next.js 16)
- Merge de main dans stand-alone
- Résolution du conflit dans radar-app/package.json (conservation de Next.js 16.1.6)
- Intégration de la refactorisation stand-alone complète
- Migration Next.js 15.2.4 -> 16.1.6

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 18:36:03 +01:00
syoul
acb475d5d0 feat: migration Next.js 15.2.4 -> 16.1.6
- Mise à jour de Next.js vers 16.1.6 (dernière version stable)
- Mise à jour de eslint-config-next vers 16.1.6
- Ajout documentation migration-nextjs-16.md

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 18:35:55 +01:00
syoul
9d8ae3d32a refactor: projet stand-alone sans dépendance aoe_technology_radar
- Intégration du code source du framework dans radar-app/ (vendoring)
- Suppression de la dépendance npm aoe_technology_radar
- Création de scripts build-radar.js et serve-radar.js pour remplacer le CLI techradar
- Adaptation de tous les scripts et Docker pour utiliser radar-app/ au lieu de .techradar
- Refactorisation complète de Dockerfile.business
- Mise à jour de la documentation (architecture, déploiement, développement)
- Mise à jour de .gitignore pour ignorer les artefacts de build de radar-app/
- Ajout de postcss dans les dépendances Docker pour le build Next.js

Le projet est maintenant complètement indépendant du package externe.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 18:11:40 +01:00
152 changed files with 17546 additions and 195 deletions

12
.gitignore vendored
View File

@@ -13,6 +13,13 @@ src/rd.json
radar.backup
config.json.backup
# radar-app build artifacts (le code source doit être versionné)
radar-app/node_modules
radar-app/out
radar-app/build
radar-app/.next
radar-app/.turbo
# Fichiers temporaires générés par serve-business.sh
radar/*.md
!radar-business/**/*.md
@@ -20,3 +27,8 @@ public/inline-strategie.js
public/strategie-content-raw.txt
public/strategie-content.js
public/strategie-inline.html
# Docs
docs/
docs-sbom/
docs-syoul/

158
.woodpecker.yml Normal file
View File

@@ -0,0 +1,158 @@
when:
- branch:
- main
- stand-alone
event: push
steps:
# Etape 0 : Validation syntaxique du docker-compose
# Les vars CI (CI_REPO_OWNER, CI_COMMIT_BRANCH) sont injectees automatiquement par Woodpecker
- name: validate
image: docker:27-cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
RADAR_DOMAIN: validate.example.com
commands:
- |
export COMPOSE_PROJECT_NAME=$(printf '%s-%s-%s' "$CI_REPO_OWNER" "$CI_REPO_NAME" "$CI_COMMIT_BRANCH" | tr 'A-Z/' 'a-z-')
docker compose -f docker-compose.business.yml config --quiet
- echo "docker-compose.business.yml valide"
# Etape 1 : Build de l'application statique
- name: build
image: node:20-alpine
environment:
NODE_ENV: production
HUSKY: 0
HUSKY_SKIP_INSTALL: 1
commands:
- apk add --no-cache git python3
- npm ci --legacy-peer-deps
- cd radar-app && npm ci --legacy-peer-deps --include=dev && npm run build:icons && cd ..
- node scripts/generate-team-visualization-data.js
- npm run build
- ls -la build/ | head -10
# Etape 2a : Ecriture du .env depuis les secrets
# NOTE: from_secret et volumes: incompatibles dans le meme step (bug Woodpecker next)
- name: write-env
image: alpine:3.20
environment:
RADAR_DOMAIN:
from_secret: radar_domain
commands:
- env | grep -E "^(RADAR_DOMAIN)=" > .env.deploy
# COMPOSE_PROJECT_NAME : convention user-project-branch, genere depuis les vars CI
- OWNER=$(echo "$CI_REPO_OWNER" | tr 'A-Z' 'a-z') && REPO=$(echo "$CI_REPO_NAME" | tr 'A-Z' 'a-z') && BRANCH=$(echo "$CI_COMMIT_BRANCH" | tr 'A-Z/' 'a-z-') && echo "COMPOSE_PROJECT_NAME=$OWNER-$REPO-$BRANCH" >> .env.deploy
- echo "Fichier .env.deploy cree ($(wc -c < .env.deploy) octets)"
# Etape 2b : Deploiement sur sonic via Docker socket
# NOTE: from_secret et volumes: incompatibles — pas de secrets ici
- name: deploy
image: docker:27-cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- docker compose --env-file .env.deploy -f docker-compose.business.yml build --no-cache
- docker compose --env-file .env.deploy -f docker-compose.business.yml up -d --remove-orphans
- docker compose --env-file .env.deploy -f docker-compose.business.yml ps
- |
DOMAIN=$(grep '^RADAR_DOMAIN=' .env.deploy | cut -d= -f2)
# --- Certificat TLS (acme.sh via sonic-acme-1) ---
# acme.sh est idempotent : skip si cert valide, renouvelle si proche expiration
# exit 0 = emis/renouvele, exit 2 = skip (domaine inchange), autres = erreur
ACME_EXIT=0
docker exec sonic-acme-1 /app/acme.sh \
--home /etc/acme.sh \
--issue -d "$DOMAIN" \
--webroot /usr/share/nginx/html \
--server letsencrypt \
--accountemail support+acme@asycn.io || ACME_EXIT=$?
if [ "$ACME_EXIT" -ne 0 ] && [ "$ACME_EXIT" -ne 2 ]; then
echo "ERREUR: acme.sh a echoue (exit $ACME_EXIT)"
exit 1
fi
docker exec sonic-acme-1 cp /etc/acme.sh/$DOMAIN/fullchain.cer /host/certs/$DOMAIN-cert.pem
docker exec sonic-acme-1 cp /etc/acme.sh/$DOMAIN/$DOMAIN.key /host/certs/$DOMAIN-key.pem
echo "Cert TLS: /host/certs/$DOMAIN-cert.pem OK (acme exit $ACME_EXIT)"
# Etape 3a : Generation SBOM (Syft) — inventaire des dependances npm du workspace
# Scan du repertoire source (pas de docker socket = pas de bug volumes/CI-vars)
- name: sbom-generate
image: alpine:3.20
commands:
- apk add --no-cache curl
- curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin latest
- mkdir -p .reports
- syft dir:. --exclude './build' --exclude './radar-app/.next' --exclude './radar-app/out' -o cyclonedx-json --file .reports/sbom-radar.cyclonedx.json
- echo "SBOM genere $(wc -c < .reports/sbom-radar.cyclonedx.json) octets"
# Etape 3b : Scan CVE (Trivy) depuis le SBOM Syft
# Cache /home/syoul/trivy-cache evite ~200Mo de telechargement des DB CVE a chaque build
# Prerequis sur sonic : mkdir -p /home/syoul/trivy-cache
- name: sbom-scan
image: aquasec/trivy:latest
volumes:
- /home/syoul/trivy-cache:/root/.cache/trivy
commands:
- trivy sbom --format json --output .reports/trivy-radar.json .reports/sbom-radar.cyclonedx.json
- echo "Scan CVE termine"
# Etape 3c : Publication SBOM vers Dependency-Track (dtrack.syoul.fr)
# NOTE: from_secret et volumes: incompatibles — pas de volumes ici
- name: sbom-publish
image: alpine/curl:latest
environment:
DTRACK_TOKEN:
from_secret: dependency_track_token
commands:
- |
VERSION=$(date +%Y-%m-%d)-$(echo "$CI_COMMIT_SHA" | cut -c1-8)
HTTP=$(curl -s -o /tmp/dtrack-response.txt -w "%{http_code}" -X POST "https://dtrack.syoul.fr/api/v1/bom" \
-H "X-Api-Key: $DTRACK_TOKEN" \
-F "autoCreate=true" \
-F "projectName=techradardev-app" \
-F "projectVersion=$VERSION" \
-F "bom=@.reports/sbom-radar.cyclonedx.json")
echo "HTTP $HTTP : $(cat /tmp/dtrack-response.txt)"
[ "$HTTP" -ge 200 ] && [ "$HTTP" -lt 300 ] || exit 1
# Etape 4 : Healthcheck post-deploiement
- name: healthcheck
image: alpine:3.20
commands:
- apk add --no-cache --quiet curl
- |
DOMAIN=$(grep '^RADAR_DOMAIN=' .env.deploy | cut -d= -f2)
if [ -z "$DOMAIN" ]; then
echo "ERREUR: RADAR_DOMAIN non defini dans .env.deploy"
exit 1
fi
TARGET="https://$DOMAIN"
echo "Healthcheck sur $TARGET (max 100s)..."
MAX=20
i=0
until [ $i -ge $MAX ]; do
CODE=$(curl -sSo /dev/null -w "%{http_code}" "$TARGET" 2>/dev/null)
echo "Tentative $((i+1))/$MAX - HTTP $CODE"
if [ "$CODE" = "200" ] || [ "$CODE" = "301" ] || [ "$CODE" = "302" ]; then
echo "Radar repond sur $TARGET"
exit 0
fi
i=$((i+1))
sleep 5
done
echo "ERREUR: Radar ne repond pas apres 100 secondes"
exit 1
# Notification en cas d'echec
- name: notify-failure
image: alpine:3.20
commands:
- 'echo "ECHEC pipeline #$CI_BUILD_NUMBER sur commit $CI_COMMIT_SHA"'
- 'echo "Branche: $CI_COMMIT_BRANCH"'
when:
- status: failure

View File

@@ -29,30 +29,22 @@ ENV NODE_ENV=production
# Installation des dépendances système
RUN apk add --no-cache git python3
# Copie des fichiers de dépendances
# Copie des fichiers de dépendances racine
COPY package.json package-lock.json* ./
# Installation des dépendances Node
RUN npm install --legacy-peer-deps --ignore-scripts cytoscape cytoscape-cose-bilkent echarts-for-react
# Installation des dépendances Node racine (pour scripts: generate-team-visualization-data, etc.)
RUN npm install --legacy-peer-deps --ignore-scripts
# Patch du package aoe_technology_radar pour inclure gray-matter dans les dépendances runtime
RUN node -e "const fs=require('fs');const pkgPath='./node_modules/aoe_technology_radar/package.json';const pkg=JSON.parse(fs.readFileSync(pkgPath,'utf8'));pkg.dependencies=pkg.dependencies||{};pkg.dependencies['gray-matter']='^4.0.3';pkg.dependencies['postcss']='^8.4.47';pkg.scripts=pkg.scripts||{};pkg.scripts.prepare='';fs.writeFileSync(pkgPath,JSON.stringify(pkg,null,2));"
# Copie du reste du projet
# Copie du reste du projet (inclut radar-app/)
COPY . .
RUN chmod +x scripts/start-business.sh
# Préparer .techradar une fois pour toutes (évite les réinstallations au runtime)
# Le script techradar.js crée automatiquement .techradar lors de l'exécution
# Création manuelle de .techradar en copiant depuis node_modules
RUN mkdir -p .techradar && \
cp -r node_modules/aoe_technology_radar/* .techradar/
# Créer le fichier hash pour éviter la recréation (calculé séparément pour éviter les problèmes d'échappement)
RUN node -e "const crypto=require('crypto');const fs=require('fs');const hash=crypto.createHash('sha256').update(fs.readFileSync('package.json')).digest('hex');fs.writeFileSync('.techradar/hash',hash);"
RUN node -e "const fs=require('fs');const p='.techradar/package.json';if(!fs.existsSync(p)){console.error('.techradar/package.json not found');process.exit(1);}const pkg=JSON.parse(fs.readFileSync(p,'utf8'));pkg.scripts=pkg.scripts||{};pkg.scripts.prepare='';fs.writeFileSync(p,JSON.stringify(pkg,null,2));"
# Installer les dépendances dans .techradar (y compris devDependencies pour tsx nécessaire à build:data)
RUN cd .techradar && npm install --legacy-peer-deps --include=dev cytoscape cytoscape-cose-bilkent echarts-for-react
RUN cd .techradar && npm run build:icons
# Installer les dépendances dans radar-app (Next.js et dépendances du framework)
# Désactiver le script prepare (husky) pour éviter les erreurs
RUN cd radar-app && \
node -e "const fs=require('fs');const p='package.json';const pkg=JSON.parse(fs.readFileSync(p,'utf8'));pkg.scripts=pkg.scripts||{};pkg.scripts.prepare='';fs.writeFileSync(p,JSON.stringify(pkg,null,2));" && \
npm install --legacy-peer-deps --include=dev cytoscape cytoscape-cose-bilkent echarts-for-react postcss && \
npm run build:icons
# --- CONFIGURATION BUSINESS ---
# Application de la logique Business (remplacement de la config et des données)
@@ -70,40 +62,40 @@ RUN echo "🔄 Régénération des données de visualisation équipe..." && \
ls -la public/team-visualization-data.json && \
head -20 public/team-visualization-data.json
# Copier les fichiers nécessaires dans .techradar avant le build (comme le fait techradar.js)
RUN rm -rf .techradar/data/radar && \
mkdir -p .techradar/data/radar/2025-01-15 && \
cp -r radar-business/2025-01-15/* .techradar/data/radar/2025-01-15/ && \
# Supprimer toute release de démo (2017-03-01, 2024-03-01, etc.) éventuellement recopiée depuis le package
find .techradar/data/radar -mindepth 1 -maxdepth 1 ! -name '2025-01-15' -exec rm -rf {} + && \
cp radar-business/config-business.json .techradar/data/config.json && \
rm -rf .techradar/public && mkdir -p .techradar/public && \
cp -r public/* .techradar/public/ && \
cp public/_team-content .techradar/public/_team-content 2>/dev/null || true && \
cp public/team-visualization-data.json .techradar/public/team-visualization-data.json 2>/dev/null || true && \
cp about.md .techradar/data/about.md 2>/dev/null || echo "about.md not found, skipping" && \
cp custom.css .techradar/src/styles/custom.css 2>/dev/null || echo "custom.css not found, skipping" && \
# Copier les fichiers nécessaires dans radar-app avant le build
RUN rm -rf radar-app/data/radar && \
mkdir -p radar-app/data/radar/2025-01-15 && \
cp -r radar-business/2025-01-15/* radar-app/data/radar/2025-01-15/ && \
# Supprimer toute release de démo (2017-03-01, 2024-03-01, etc.) éventuellement présentes
find radar-app/data/radar -mindepth 1 -maxdepth 1 ! -name '2025-01-15' -exec rm -rf {} + && \
cp radar-business/config-business.json radar-app/data/config.json && \
rm -rf radar-app/public && mkdir -p radar-app/public && \
cp -r public/* radar-app/public/ && \
cp public/_team-content radar-app/public/_team-content 2>/dev/null || true && \
cp public/team-visualization-data.json radar-app/public/team-visualization-data.json 2>/dev/null || true && \
cp about.md radar-app/data/about.md 2>/dev/null || echo "about.md not found, skipping" && \
cp custom.css radar-app/src/styles/custom.css 2>/dev/null || echo "custom.css not found, skipping" && \
echo "Fichiers public copiés" && \
echo "📁 Vérification des fichiers team dans .techradar/public/:" && \
ls -la .techradar/public/ | grep -E "(team\.html|team-visualization)" && echo "✅ Fichiers team trouvés" || (echo "⚠️ Fichiers team non trouvés dans .techradar/public/" && echo "📁 Contenu de public/ source:" && ls -la public/ | head -10) && \
echo "📁 Vérification des fichiers team dans radar-app/public/:" && \
ls -la radar-app/public/ | grep -E "(team\.html|team-visualization)" && echo "✅ Fichiers team trouvés" || (echo "⚠️ Fichiers team non trouvés dans radar-app/public/" && echo "📁 Contenu de public/ source:" && ls -la public/ | head -10) && \
echo "📁 Vérification que _team-content existe dans public/ source:" && \
test -f public/_team-content && echo "✅ public/_team-content existe" || echo "❌ public/_team-content n'existe pas"
# Diagnostic : compter les fichiers markdown copiés dans .techradar/data/radar
RUN echo "📊 Comptage des fichiers .md dans .techradar/data/radar" && \
find .techradar/data/radar -name "*.md" | wc -l && \
find .techradar/data/radar -name "*.md" | head -10
# Diagnostic : compter les fichiers markdown copiés dans radar-app/data/radar
RUN echo "📊 Comptage des fichiers .md dans radar-app/data/radar" && \
find radar-app/data/radar -name "*.md" | wc -l && \
find radar-app/data/radar -name "*.md" | head -10
# Créer la page Next.js /team ET un fichier HTML statique /team/index.html
# La page Next.js pour le routing, le HTML statique pour garantir l'affichage
RUN mkdir -p .techradar/src/pages
COPY docker/team-page.tsx .techradar/src/pages/team.tsx
RUN mkdir -p radar-app/src/pages
COPY docker/team-page.tsx radar-app/src/pages/team.tsx
# Modifier _document.tsx pour charger team-block-script.js en premier (avant le rendu)
COPY docker/patch_document.py /tmp/patch_document.py
RUN python3 /tmp/patch_document.py && \
echo "📄 _document.tsx apres modification:" && \
cat .techradar/src/pages/_document.tsx
cat radar-app/src/pages/_document.tsx
# Script Python pour ajouter le lien Équipe dans Navigation.tsx (supprime TOUS les doublons)
COPY docker/add_team_link.py /tmp/add_team_link.py
@@ -124,7 +116,7 @@ RUN /tmp/add_team_link.sh
# Builder l'application en mode production pour éviter Fast Refresh
# Utiliser WORKDIR pour changer de répertoire de manière fiable
WORKDIR /app/.techradar
WORKDIR /app/radar-app
RUN npm run build:data
RUN npm run build
# S'assurer que _team-content.html et team-visualization-data.json sont copiés dans out/
@@ -153,9 +145,9 @@ RUN if [ -d "out" ]; then \
if [ -d "public/team" ]; then \
mkdir -p out/team && \
cp -rv public/team/* out/team/ && echo "✅ /team/index.html copié dans out/team/"; \
elif [ -d "/app/.techradar/public/team" ]; then \
elif [ -d "/app/radar-app/public/team" ]; then \
mkdir -p out/team && \
cp -rv /app/.techradar/public/team/* out/team/ && echo "✅ /team/index.html copié depuis /app/.techradar/public/team/"; \
cp -rv /app/radar-app/public/team/* out/team/ && echo "✅ /team/index.html copié depuis /app/radar-app/public/team/"; \
fi && \
echo "🔍 VÉRIFICATION: _team-content dans out/:" && \
ls -la out/_team-content 2>/dev/null || echo "❌ _team-content absent de out/" && \

225
Readme.md
View File

@@ -1,52 +1,177 @@
# AJR Technology Radar - Content
# Techradar Laplank
Ce dépôt contient le contenu du Technology Radar AJR, publié sous : https://www.coeurbox.syoul.fr
**Techradar Laplank** est un Technology Radar interactif pour suivre l'évolution des technologies de l'écosystème Laplank/Duniter/Ğ1. Le projet est basé sur le framework [aoe_technology_radar](https://github.com/AOEpeople/aoe_technology_radar), dont le code source est vendu dans le répertoire `radar-app/`.
Le projet est basé sur le framework [aoe_technology_radar](https://github.com/AOEpeople/aoe_technology_radar).
## 🎯 Vue d'ensemble
## Vue d'ensemble
**Techradar Laplank** est un projet **stand-alone** qui présente :
- **38 technologies** organisées par quadrants business et anneaux d'adoption
- **12 membres d'équipe** avec leurs compétences et projets
- **Visualisations interactives** : graphe réseau, matrice de congestion, équipe de genèse
- **Pages de stratégie** : Technique, Business, DataViz Expert
Le projet contient deux radars distincts :
### Technologies utilisées
1. **Radar Technique Principal** : Radar standard des technologies utilisées par AJR
2. **Radar Technologique Laplank** : Tech radar classique pour suivre l'évolution des technologies de l'écosystème Laplank/Duniter/Ğ1 avec historique par release
- **Next.js** : 16.1.6 (avec Turbopack)
- **React** : 19
- **TypeScript** : 5
- **Cytoscape.js** : Visualisation du graphe réseau
- **ECharts** : Matrice de congestion
- **Node.js** : 20+
## Radar Technologique Laplank
## 🚀 Démarrage rapide
Le Radar Technologique Laplank est un tech radar classique accessible via le port **3006** avec une **protection par mot de passe** (`laplank-radar`).
### Prérequis
### Fonctionnalités
- Node.js 20 ou supérieur
- npm ou yarn
- Docker (pour le déploiement)
- **Historique par release** : Suivi de l'évolution des technologies au fil du temps avec organisation par date
- **Pages de stratégie dynamiques** : Accès à trois pages de stratégie depuis le header :
- Stratégie Technique
- Business
- DataViz Expert
- **Protection par mot de passe** : Accès restreint via un système d'authentification client-side
- **Quadrants business** : Classification selon l'impact business (Différenciantes, Commodité, Risque, Émergentes)
- **Anneaux classiques** : Hold, Assess, Trial, Adopt
### Installation
```bash
git clone gitea@git.open.us.org:AJR/TechradarDev.git
cd TechradarDev
npm install
```
### Développement local
**Mode développement Next.js** (avec hot-reload) :
```bash
npm run serve-dev
```
Le serveur démarre sur http://localhost:3000
**Mode production local** (serveur statique) :
```bash
npm install
npm run serve-business
```
Le serveur démarre sur http://localhost:3006
### Déploiement
**Build de production** :
```bash
npm run build
```
Les fichiers statiques sont générés dans le répertoire `build/`
Le Radar Technologique Laplank est déployé via Docker et Portainer :
## 🐳 Déploiement Docker
Le radar est déployé via Docker Compose :
**Commande de déploiement :**
```bash
docker compose -f docker-compose.business.yml up -d --build
```
Cette commande :
- Build l'image Docker sans cache (`--build`)
- Démarre le conteneur en mode détaché (`-d`)
- Le radar sera accessible sur http://localhost:3006
**Arrêter le déploiement :**
```bash
docker compose -f docker-compose.business.yml down
```
**Configuration Docker :**
- **Dockerfile** : `Dockerfile.business`
- **Docker Compose** : `docker-compose.business.yml`
- **Port** : 3006 (mappé depuis le port 3000 du conteneur)
- **Base path** : `/` (racine)
- **Conteneur** : `laplank-radar-technolologique`
## Content Guidelines
## 📊 Fonctionnalités
Les nouveaux blips doivent être tagués. Les tags suivants sont établis :
### Radar Technologique
- **Quadrants business** :
- Technologies Différenciantes
- Technologies Commodité
- Technologies Risque
- Technologies Émergentes
- **Anneaux d'adoption** :
- **Adopt** : Technologies adoptées et utilisées en production
- **Trial** : Technologies en phase d'essai
- **Assess** : Technologies à évaluer
- **Hold** : Technologies à éviter ou remplacer
- **38 technologies** suivies dans la release 2025-01-15
### Page Équipe (`/team`)
Visualisations interactives de l'équipe :
1. **Graphe réseau** : Relations entre membres et technologies (Cytoscape.js)
2. **Matrice de congestion** : Technologies core vs disponibilité de l'équipe (ECharts)
3. **Équipe de genèse** : Membres fondateurs et leurs compétences
4. **Profils cliquables** : Cartes de profil détaillées pour chaque membre
**12 membres** de l'équipe avec leurs compétences, projets et disponibilités.
### Pages de stratégie
Accès à trois pages de stratégie depuis le header :
- **Stratégie Technique** : Vision technique et roadmap
- **Business** : Impact business et métriques
- **DataViz Expert** : Stratégie de visualisation de données
**Protection par mot de passe** : `laplank-radar`
## 📁 Structure du projet
```
TechradarDev/
├── radar-business/ # ✏️ SOURCE DE VÉRITÉ — éditer ici
│ ├── 2025-01-15/ # Fichiers .md des technologies (38 blips)
│ ├── config-business.json # Configuration du radar (quadrants, rings)
│ └── FORMAT-BLIP.md # Format des métadonnées
├── data/ # ✏️ Données de build versionnées
│ └── team/ # Profils des 12 membres (*.md)
├── radar-app/ # ⚙️ Framework Next.js (ne pas éditer)
│ ├── src/ # Code source Next.js
│ └── data/ # Copie générée au build (ne pas éditer)
├── radar/ # ⚠️ Dossier temporaire (généré par serve-business.sh)
│ └── ... # Ne jamais éditer ici — écrasé à chaque lancement
├── scripts/
│ ├── build-radar.js # Script de build stand-alone
│ ├── serve-radar.js # Script de serve
│ ├── generate-team-visualization-data.js
│ └── ...
├── public/ # Fichiers statiques
│ ├── team-block-script.js
│ └── team-visualization-data.json # Généré par generate-team-visualization-data.js
├── Dockerfile.business
└── docker-compose.business.yml
```
## 🛠️ Scripts disponibles
| Script | Description |
|--------|-------------|
| `npm run build` | Build de production (génère `build/`) |
| `npm run serve` | Serve les fichiers statiques depuis `build/` |
| `npm run serve-dev` | Mode développement Next.js (hot-reload) |
| `npm run serve-business` | Serve le radar business en local (port 3006) |
| `npm run extract-tech` | Extraction des technologies depuis la doc |
| `npm run analyze-business` | Analyse des métriques business |
## ✏️ Où éditer le contenu
| Quoi | Dossier |
|---|---|
| Ajouter / modifier une technologie | `radar-business/2025-01-15/` |
| Ajouter / modifier un membre d'équipe | `data/team/` |
> ⚠️ Ne jamais éditer dans `radar/` (temporaire, écrasé au lancement) ni dans `radar-app/data/` (copie générée au build).
## 📝 Content Guidelines
### Tags disponibles
Les blips doivent être tagués avec les tags suivants :
* architecture
* security
@@ -59,35 +184,39 @@ Les nouveaux blips doivent être tagués. Les tags suivants sont établis :
* ux/ui
* documentation
Exemple d'utilisation :
```md
Exemple :
```markdown
tags: [devops, security]
```
## Development
### Format des blips
### Build le radar principal
```
npm install
npm run serve
```
Voir `radar-business/FORMAT-BLIP.md` pour le format complet des métadonnées business.
Puis ouvrir : http://localhost:3000/techradar
## 🔐 Sécurité
### Build avec fichiers statiques
```
npm install
npm run build
```
- **Protection par mot de passe** : Les pages de stratégie sont protégées par le mot de passe `laplank-radar`
- **Authentification client-side** : Système d'authentification JavaScript côté client
## Documentation
## 📚 Documentation
La documentation complète est disponible dans le dossier `docs/` :
La documentation technique est disponible dans `docs/app/` (non versionnée, locale uniquement) :
- [Architecture](./docs/app/architecture.md)
- [Configuration](./docs/app/configuration.md)
- [Développement](./docs/app/developpement.md)
- [Déploiement](./docs/app/deploiement.md)
- [Contribution](./docs/app/contribution.md)
- [Guide Radar Technologique Laplank](./docs/app/guide-radar-business.md)
- Architecture, Configuration, Développement, Déploiement
- Guide Radar Business, Guide Page Équipe, Migration Next.js 16
## 🔄 État du projet
- **Branche principale** : `main`
- **Version** : 4.3.0
- **Statut** : Projet stand-alone, indépendant du package externe `aoe_technology_radar`
- **Framework** : Code vendu dans `radar-app/` (basé sur aoe_technology_radar)
## 📞 Support
- **Dépôt Git** : https://git.open.us.org/AJR/TechradarDev
- **Radar en ligne** : http://localhost:3006 (après déploiement)
## 📄 Licence
MIT

View File

@@ -1,26 +1,34 @@
version: '3.8'
# Convention de nommage : user-project-branch (ex: ajr-techradardev-main)
# Permet plusieurs instances en parallele (prod/test) sans collision
name: ${COMPOSE_PROJECT_NAME:-ajr-techradardev-main}
services:
radar-business:
container_name: laplank-radar-technolologique
container_name: ${COMPOSE_PROJECT_NAME:-ajr-techradardev-main}-app
build:
context: .
dockerfile: Dockerfile.business
pull: true # Force le pull de l'image de base pour éviter le cache
pull: true
args:
BUILD_DATE: "${BUILD_DATE:-$(date +%s)}"
BUILD_VERSION: "${BUILD_VERSION:-dev}"
CACHE_BUST: "${CACHE_BUST:-$(date +%s%N)}" # Nanosecondes pour garantir l'unicité et forcer l'invalidation
# Note: no_cache n'est pas supporté dans docker-compose
# Pour forcer le rebuild sans cache dans Portainer, utilisez l'option "Rebuild" avec "No cache" dans l'interface
# Si vous utilisez une image pré-bâtie, décommentez image et commentez build
# image: votre-registre/laplank-radar-business:latest
CACHE_BUST: "${CACHE_BUST:-$(date +%s%N)}"
restart: unless-stopped
ports:
- "3006:3000" # Mappe le port 3006 de l'hôte vers le port 3000 du conteneur
environment:
- NODE_ENV=production
# Optionnel : Persistance des logs si nécessaire
# volumes:
# - ./logs:/app/logs
labels:
# Registrator lit l'IP du conteneur depuis le reseau "sonic" (-useIpFromNetwork sonic)
# et enregistre le service dans Consul avec le tag urlprefix- -> Fabio route vers ce service
- SERVICE_3000_NAME=${COMPOSE_PROJECT_NAME:-ajr-techradardev-main}-app-3000
- SERVICE_3000_TAGS=urlprefix-${RADAR_DOMAIN}/*
- SERVICE_3000_CHECK_TCP=true
# sonic-acme-1 (acme-companion) emet le cert TLS et le copie dans /host/certs/
# Fabio le detecte automatiquement par SNI pour HTTPS
- LETSENCRYPT_HOST=${RADAR_DOMAIN}
networks:
- sonic
networks:
sonic:
# Reseau externe existant sur le serveur (partage avec Registrator/Consul/Fabio)
external: true

View File

@@ -3,7 +3,7 @@ import sys
import re
import os
f = ".techradar/src/components/Navigation/Navigation.tsx"
f = "radar-app/src/components/Navigation/Navigation.tsx"
try:
# Vérifier que le fichier existe

View File

@@ -3,14 +3,14 @@ set -e
echo "🔧 Modification de Navigation.tsx pour le lien Équipe..."
NAV_FILE=".techradar/src/components/Navigation/Navigation.tsx"
NAV_FILE="radar-app/src/components/Navigation/Navigation.tsx"
# Vérifier que le fichier existe
if [ ! -f "$NAV_FILE" ]; then
echo "❌ Fichier $NAV_FILE introuvable"
echo "📁 Répertoire actuel: $(pwd)"
echo "📁 Contenu de .techradar/src/components/:"
ls -la .techradar/src/components/ 2>/dev/null || echo "Répertoire non trouvé"
echo "📁 Contenu de radar-app/src/components/:"
ls -la radar-app/src/components/ 2>/dev/null || echo "Répertoire non trouvé"
exit 1
fi

View File

@@ -3,7 +3,7 @@
import sys
doc_path = ".techradar/src/pages/_document.tsx"
doc_path = "radar-app/src/pages/_document.tsx"
try:
with open(doc_path, "r") as f:

View File

@@ -31,7 +31,7 @@ Données métier et contenu utilisé par l'application pour générer le radar :
## Vue d'ensemble
Le Technology Radar AJR est une application web interactive qui présente les technologies, outils, méthodes et plateformes utilisées et évaluées par AJR. Il est basé sur le framework [aoe_technology_radar](https://github.com/AOEpeople/aoe_technology_radar).
Le Technology Radar AJR est une application web interactive qui présente les technologies, outils, méthodes et plateformes utilisées et évaluées par AJR. Il est basé sur le framework [aoe_technology_radar](https://github.com/AOEpeople/aoe_technology_radar), dont le code source est vendu dans le répertoire `radar-app/`.
Le radar est organisé en quatre quadrants et quatre anneaux (rings) pour classifier chaque technologie selon son niveau d'adoption et sa catégorie.
@@ -39,7 +39,7 @@ Le radar est organisé en quatre quadrants et quatre anneaux (rings) pour classi
- **Radar en ligne** : https://www.coeurbox.syoul.fr
- **Dépôt Git** : https://git.open.us.org/AJR/TechradarDev
- **Framework source** : https://github.com/AOEpeople/aoe_technology_radar
- **Framework source** : https://github.com/AOEpeople/aoe_technology_radar (code vendu dans `radar-app/`)
## Démarrage rapide

View File

@@ -4,7 +4,7 @@ Bienvenue dans la documentation du projet AJR Technology Radar (CoeurBox).
## Vue d'ensemble
Le Technology Radar AJR est une application web interactive qui présente les technologies, outils, méthodes et plateformes utilisées et évaluées par AJR. Il est basé sur le framework [aoe_technology_radar](https://github.com/AOEpeople/aoe_technology_radar).
Le Technology Radar AJR est une application web interactive qui présente les technologies, outils, méthodes et plateformes utilisées et évaluées par AJR. Il est basé sur le framework [aoe_technology_radar](https://github.com/AOEpeople/aoe_technology_radar), dont le code source est vendu dans le répertoire `radar-app/`.
Le radar est organisé en quatre quadrants et quatre anneaux (rings) pour classifier chaque technologie selon son niveau d'adoption et sa catégorie.
@@ -38,7 +38,7 @@ Les données utilisées par l'application sont dans le dossier [`../data/`](../d
- **Radar en ligne** : https://www.coeurbox.syoul.fr
- **Radar Technologique Laplank** : http://laplank.techradar.syoul.fr:3006
- **Dépôt Git** : https://git.open.us.org/AJR/TechradarDev
- **Framework source** : https://github.com/AOEpeople/aoe_technology_radar
- **Framework source** : https://github.com/AOEpeople/aoe_technology_radar (code vendu dans `radar-app/`)
## Démarrage rapide

View File

@@ -2,7 +2,7 @@
## Vue d'ensemble
Le projet AJR Technology Radar est une application web statique construite avec le framework `aoe_technology_radar` (basé sur Next.js). L'application génère un site web interactif à partir de fichiers Markdown organisés par dates de release.
Le projet AJR Technology Radar est une application web statique construite avec le framework `aoe_technology_radar` (basé sur Next.js), dont le code source est vendu dans le répertoire `radar-app/`. L'application génère un site web interactif à partir de fichiers Markdown organisés par dates de release.
## Structure des répertoires
@@ -42,7 +42,7 @@ TechradarDev/
│ ├── app/ # Documentation technique de l'application
│ └── data/ # Données métier et contenu
│ └── team/ # Profils individuels des membres de l'équipe
├── .techradar/ # Framework aoe_technology_radar (généré pendant le build)
├── radar-app/ # Framework aoe_technology_radar (code vendu dans le repo)
│ ├── src/ # Code source Next.js du framework
│ │ ├── pages/ # Pages Next.js (routes)
│ │ │ └── team.tsx # Page /team générée par Dockerfile
@@ -79,15 +79,17 @@ Le projet utilise le framework **aoe_technology_radar** qui est basé sur :
### Processus de build
1. **Installation des dépendances** : `npm install` installe `aoe_technology_radar` depuis GitHub
2. **Préparation du framework** : Copie de `node_modules/aoe_technology_radar` vers `.techradar/`
3. **Configuration** : Copie de `radar-business/config-business.json` vers `.techradar/data/config.json`
4. **Données** : Copie des blips depuis `radar-business/2025-01-15/` vers `.techradar/data/radar/2025-01-15/`
5. **Modifications personnalisées** :
- Création de `.techradar/src/pages/team.tsx` (page Next.js pour `/team`)
- Modification de `.techradar/src/components/Navigation/Navigation.tsx` (ajout du lien Équipe)
6. **Build Next.js** : `npm run build:data` puis `npm run build` génère les fichiers statiques
7. **Output** : Fichiers statiques dans `.techradar/out/` servis par un serveur statique
1. **Injection des données** : Le script `scripts/build-radar.js` copie :
- `radar-business/config-business.json``radar-app/data/config.json`
- `radar-business/2025-01-15/``radar-app/data/radar/2025-01-15/`
- `public/*``radar-app/public/`
- Génère `team-visualization-data.json` et le copie dans `radar-app/public/`
2. **Modifications personnalisées** :
- Création de `radar-app/src/pages/team.tsx` (page Next.js pour `/team`)
- Modification de `radar-app/src/components/Navigation/Navigation.tsx` (ajout du lien Équipe)
- Modification de `radar-app/src/pages/_document.tsx` (chargement du script team-block-script.js)
3. **Build Next.js** : `cd radar-app && npm run build:data && npm run build` génère les fichiers statiques
4. **Output** : Fichiers statiques dans `radar-app/out/` copiés vers `build/` à la racine
### Modifications personnalisées
@@ -96,7 +98,7 @@ Le projet apporte plusieurs modifications au framework de base :
#### 1. Page Equipe (`/team`)
- **Script principal** : `public/team-block-script.js` (injection du contenu et visualisations)
- **Route Next.js** : `.techradar/src/pages/team.tsx` (page vide, le script remplace le contenu)
- **Route Next.js** : `radar-app/src/pages/team.tsx` (page vide, le script remplace le contenu)
- **Chargement** : Le script est charge via `_document.tsx` avec `strategy="beforeInteractive"`
- **Bibliotheques** : Cytoscape.js et ECharts charges depuis CDN
- **Donnees** : `public/team-visualization-data.json` genere par `scripts/generate-team-visualization-data.js`
@@ -104,7 +106,7 @@ Le projet apporte plusieurs modifications au framework de base :
#### 2. Navigation modifiée
- **Fichier modifié** : `.techradar/src/components/Navigation/Navigation.tsx`
- **Fichier modifié** : `radar-app/src/components/Navigation/Navigation.tsx`
- **Modification** : Ajout du lien "👥 Équipe" vers `/team`
- **Méthode** : Script Python dans `Dockerfile.business` qui :
- Supprime tous les liens Équipe existants (évite les doublons)
@@ -160,7 +162,7 @@ Description de la technologie en Markdown.
## Dépendances principales
- **aoe_technology_radar** : Framework principal (dépendance GitHub)
- **radar-app/** : Framework principal (code vendu dans le repo, basé sur aoe_technology_radar)
- **Node.js** : Runtime JavaScript (version 20+)
- **npm** : Gestionnaire de paquets
- **gray-matter** : Parsing YAML front matter

View File

@@ -101,30 +101,32 @@ Le `Dockerfile.business` effectue les opérations suivantes :
- Git et Python3 pour les scripts
- Variables d'environnement pour désactiver Husky
2. **Préparation du framework** :
- Copie de `node_modules/aoe_technology_radar` vers `.techradar/`
- Patch du package pour inclure `gray-matter` et `postcss`
2. **Installation des dépendances** :
- Installation des dépendances racine (pour scripts: generate-team-visualization-data, etc.)
- Installation des dépendances dans `radar-app/` (Next.js et dépendances du framework)
- Désactivation du script `prepare` (husky) dans `radar-app/package.json`
3. **Configuration des données** :
- Purge des données de démo : `rm -rf .techradar/data/radar/*`
- Copie des blips business : `radar-business/2025-01-15/*``.techradar/data/radar/2025-01-15/`
- Copie de la config : `radar-business/config-business.json``.techradar/data/config.json`
- Purge des données de démo : `rm -rf radar-app/data/radar/*`
- Copie des blips business : `radar-business/2025-01-15/*``radar-app/data/radar/2025-01-15/`
- Copie de la config : `radar-business/config-business.json``radar-app/data/config.json`
- Copie des fichiers publics : `public/*``radar-app/public/`
- Génération et copie de `team-visualization-data.json` dans `radar-app/public/`
4. **Modifications personnalisees** :
- Creation de `.techradar/src/pages/team.tsx` (page Next.js vide pour `/team`)
- Modification de `.techradar/src/pages/_document.tsx` via script Python :
- Creation de `radar-app/src/pages/team.tsx` (page Next.js vide pour `/team`)
- Modification de `radar-app/src/pages/_document.tsx` via script Python :
- Ajout du chargement de `team-block-script.js` avec `strategy="beforeInteractive"`
- Modification de `.techradar/src/components/Navigation/Navigation.tsx` via script Python :
- Modification de `radar-app/src/components/Navigation/Navigation.tsx` via script Python :
- Suppression de tous les liens Equipe existants (evite les doublons)
- Ajout d'un seul lien "Equipe" apres le lien "Vue d'ensemble"
5. **Build Next.js** :
- `npm run build:data` : Génère les données du radar
- `npm run build` : Build de l'application Next.js
- `cd radar-app && npm run build:data` : Génère les données du radar
- `cd radar-app && npm run build` : Build de l'application Next.js
6. **Copie des fichiers publics** :
- Copie de `public/team-block-script.js` et `public/team-visualization-data.json` vers `.techradar/public/`
- Les fichiers sont ensuite copies dans `out/` apres le build
6. **Post-build** :
- Copie des fichiers additionnels (`_team-content`, `team-visualization-data.json`, `team/`) depuis `radar-app/public/` vers `radar-app/out/`
7. **Demarrage** :
- Execution de `scripts/start-business.sh` qui :
@@ -138,7 +140,7 @@ Le `Dockerfile.business` effectue les opérations suivantes :
Le script `docker/add_team_link.py` :
1. **Verifie l'existence du fichier** : `.techradar/src/components/Navigation/Navigation.tsx`
1. **Verifie l'existence du fichier** : `radar-app/src/components/Navigation/Navigation.tsx`
2. **Supprime tous les liens Equipe existants** : Evite les doublons meme si le script s'execute plusieurs fois
3. **Ajoute un seul lien Equipe** : Apres le lien "Vue d'ensemble"
4. **Verifie le resultat** : S'assure qu'il n'y a qu'un seul lien apres l'operation

View File

@@ -22,7 +22,7 @@ cd TechradarDev
npm install
```
Cette commande installe le framework `aoe_technology_radar` depuis GitHub.
Cette commande installe les dépendances racine (pour les scripts utilitaires). Le framework Next.js est déjà présent dans `radar-app/` (code vendu dans le repo).
## Développement local
@@ -58,7 +58,7 @@ Pour générer les fichiers statiques :
npm run build
```
Les fichiers générés sont créés dans le répertoire `build/` (généré par le framework).
Les fichiers générés sont créés dans le répertoire `build/` (copiés depuis `radar-app/out/`).
## Structure des fichiers radar

View File

@@ -21,10 +21,10 @@ Cette approche evite les conflits SSR tout en permettant des visualisations inte
### Fichiers impliques
- **Script principal** : `public/team-block-script.js` (injection du contenu et visualisations)
- **Page Next.js** : `.techradar/src/pages/team.tsx` (page vide, le script remplace le contenu)
- **Page Next.js** : `radar-app/src/pages/team.tsx` (page vide, le script remplace le contenu)
- **Donnees JSON** : `public/team-visualization-data.json` (genere par `scripts/generate-team-visualization-data.js`)
- **Navigation** : `.techradar/src/components/Navigation/Navigation.tsx` (modifiee par script Python)
- **Document modifie** : `.techradar/src/pages/_document.tsx` (modifie pour charger le script)
- **Navigation** : `radar-app/src/components/Navigation/Navigation.tsx` (modifiee par script Python)
- **Document modifie** : `radar-app/src/pages/_document.tsx` (modifie pour charger le script)
## Acces
@@ -184,8 +184,8 @@ Description du membre de l'equipe.
### Dans le Dockerfile
1. **Copie des fichiers publics** : `public/team-block-script.js` et `public/team-visualization-data.json` vers `.techradar/public/`
2. **Creation de la page Next.js** : Genere `.techradar/src/pages/team.tsx` (page vide)
1. **Copie des fichiers publics** : `public/team-block-script.js` et `public/team-visualization-data.json` vers `radar-app/public/`
2. **Creation de la page Next.js** : Genere `radar-app/src/pages/team.tsx` (page vide)
3. **Modification de _document.tsx** : Ajoute le chargement de `team-block-script.js` avec `strategy="beforeInteractive"`
4. **Modification de Navigation** : Ajoute le lien "Equipe" dans `Navigation.tsx` via script Python
5. **Build Next.js** : Genere les fichiers statiques dans `out/`
@@ -332,7 +332,7 @@ Parametres disponibles :
**Solutions** :
1. Verifier les logs Docker lors du build
2. Verifier que le fichier `.techradar/src/components/Navigation/Navigation.tsx` existe
2. Verifier que le fichier `radar-app/src/components/Navigation/Navigation.tsx` existe
3. Rebuild avec `--no-cache` pour forcer l'execution du script
### La page `/team` affiche le radar au lieu des visualisations
@@ -386,7 +386,7 @@ docker compose -f docker-compose.business.yml up -d
## Fichiers associes
- **Script principal** : `public/team-block-script.js` (injection et visualisations)
- **Page Next.js** : `docker/team-page.tsx` (page vide copiee vers `.techradar/src/pages/team.tsx`)
- **Page Next.js** : `docker/team-page.tsx` (page vide copiee vers `radar-app/src/pages/team.tsx`)
- **Donnees JSON** : `public/team-visualization-data.json` (genere)
- **Script de generation** : `scripts/generate-team-visualization-data.js`
- **Profils equipe** : `docs/data/team/*.md` (fichiers Markdown avec metadonnees YAML)

View File

@@ -0,0 +1,89 @@
# Migration vers Next.js 16.1.6
## Résumé des changements
Migration de Next.js de la version **15.2.4** vers **16.1.6** (dernière version stable).
## Modifications apportées
### Dépendances mises à jour
- **next** : `15.2.4``16.1.6`
- **eslint-config-next** : `15.2.4``16.1.6`
### Dépendances conservées (compatibles)
- **react** : `^19` (déjà à jour, compatible avec Next.js 16)
- **react-dom** : `^19` (déjà à jour, compatible avec Next.js 16)
- **@types/react** : `^19` (compatible)
- **@types/react-dom** : `^19` (compatible)
- **typescript** : `^5` (compatible, minimum requis 5.1.0)
## Changements dans Next.js 16
### Exigences système
- **Node.js** : Minimum 20.9.0 (déjà utilisé dans Dockerfile avec Node 20)
- **TypeScript** : Minimum 5.1.0 (déjà satisfait avec TypeScript 5)
- **Browsers** : Chrome 111+, Edge 111+, Firefox 111+, Safari 16.4+
### Turbopack par défaut
- Turbopack est maintenant stable et utilisé par défaut pour `next dev` et `next build`
- Plus besoin du flag `--turbopack` dans les scripts
- Si vous utilisez une configuration webpack personnalisée, les builds échoueront par défaut
- Solution : migrer vers Turbopack ou utiliser `next build --webpack` pour désactiver
### Configuration actuelle
Le fichier `radar-app/next.config.js` actuel est compatible avec Next.js 16 :
```javascript
const nextConfig = {
basePath,
output: "export",
trailingSlash: true,
reactStrictMode: true,
experimental: {
scrollRestoration: true,
},
};
```
**Note** : L'option `experimental.scrollRestoration` pourrait être dépréciée dans Next.js 16, mais ne devrait pas causer d'erreur.
## Tests à effectuer
1. ✅ Build Docker : Vérifier que le build fonctionne avec la nouvelle version
2. ✅ Serveur de développement : Tester `npm run serve-dev`
3. ✅ Build de production : Tester `npm run build`
4. ✅ Page d'accueil : Vérifier le rendu
5. ✅ Page team : Vérifier les visualisations
6. ✅ Navigation : Vérifier tous les liens
## Migration automatique (optionnel)
Si des problèmes surviennent, vous pouvez utiliser le codemod officiel :
```bash
cd radar-app
npx @next/codemod@canary upgrade latest
```
Ce codemod gère automatiquement :
- Suppression de `experimental_ppr` Route Segment Config
- Suppression du préfixe `unstable_` des APIs stabilisées
- Migration de la convention `middleware` dépréciée vers `proxy`
- Migration de `next lint` vers ESLint CLI
- Mise à jour de `next.config.js` pour la nouvelle configuration Turbopack
## Notes importantes
- Le projet utilise déjà React 19, qui est compatible avec Next.js 16
- Le Dockerfile utilise Node.js 20, ce qui satisfait l'exigence minimale
- Aucun changement de code source n'est nécessaire pour cette migration
- Les dépendances optionnelles (cytoscape, echarts-for-react) restent inchangées
## Date de migration
Migration effectuée le : 2026-02-25

View File

@@ -23,7 +23,7 @@
**Vérification** :
```bash
# Dans le conteneur
grep -c 'href="/team"' .techradar/src/components/Navigation/Navigation.tsx
grep -c 'href="/team"' radar-app/src/components/Navigation/Navigation.tsx
# Doit retourner 1 (un seul lien)
```
@@ -38,14 +38,14 @@ grep -c 'href="/team"' .techradar/src/components/Navigation/Navigation.tsx
**Solutions** :
1. Vérifier les logs Docker lors du build pour voir si le script Python s'est exécuté
2. Vérifier que le fichier `.techradar/src/components/Navigation/Navigation.tsx` existe
2. Vérifier que le fichier `radar-app/src/components/Navigation/Navigation.tsx` existe
3. Vérifier que le script Python a bien trouvé l'emplacement pour insérer le lien
4. Rebuild avec `--no-cache` pour forcer l'exécution
**Vérification** :
```bash
# Dans le conteneur
grep 'href="/team"' .techradar/src/components/Navigation/Navigation.tsx
grep 'href="/team"' radar-app/src/components/Navigation/Navigation.tsx
# Doit retourner le lien
```
@@ -66,10 +66,10 @@ grep 'href="/team"' .techradar/src/components/Navigation/Navigation.tsx
3. Vérifier dans les logs Docker que les données ont été copiées :
```bash
# Dans le conteneur
find .techradar/data/radar -name "*.md" | wc -l
find radar-app/data/radar -name "*.md" | wc -l
# Doit retourner ~38 fichiers
```
4. Vérifier que `config-business.json` a été copié vers `.techradar/data/config.json`
4. Vérifier que `config-business.json` a été copié vers `radar-app/data/config.json`
**Migration des rings** :
```bash
@@ -110,8 +110,8 @@ find . -name "*.md" -exec sed -i 's/^ring: support$/ring: adopt/' {} \;
**Verification** :
```bash
# Dans le conteneur
ls -l .techradar/public/team-block-script.js
grep "team-block-script" .techradar/src/pages/_document.tsx
ls -l radar-app/public/team-block-script.js
grep "team-block-script" radar-app/src/pages/_document.tsx
```
#### Page `/team` retourne 404
@@ -123,14 +123,14 @@ grep "team-block-script" .techradar/src/pages/_document.tsx
- Le serveur utilise `--single` qui redirige vers index.html
**Solutions** :
1. Verifier que le Dockerfile a bien cree `.techradar/src/pages/team.tsx`
1. Verifier que le Dockerfile a bien cree `radar-app/src/pages/team.tsx`
2. Verifier que `scripts/start-business.sh` ne contient pas l'option `--single`
3. Verifier les logs du build Docker
**Verification** :
```bash
# Dans le conteneur
ls -l .techradar/src/pages/team.tsx
ls -l radar-app/src/pages/team.tsx
ls -l out/team/index.html
```
@@ -244,20 +244,20 @@ Dans Portainer, cocher l'option "No cache" lors du rebuild de la stack.
docker exec -it <container-name> /bin/sh
# Verifier les fichiers de la page equipe
ls -la .techradar/src/pages/team.tsx
ls -la .techradar/public/team-block-script.js
ls -la radar-app/src/pages/team.tsx
ls -la radar-app/public/team-block-script.js
ls -la out/team-block-script.js
ls -la out/team-visualization-data.json
# Verifier les modifications
grep "team-block-script" .techradar/src/pages/_document.tsx
ls -la .techradar/src/components/Navigation/Navigation.tsx
grep "team-block-script" radar-app/src/pages/_document.tsx
ls -la radar-app/src/components/Navigation/Navigation.tsx
# Compter les blips
find .techradar/data/radar -name "*.md" | wc -l
find radar-app/data/radar -name "*.md" | wc -l
# Verifier la config
head -60 .techradar/data/config.json
head -60 radar-app/data/config.json
```
### Vérifier les logs
@@ -287,10 +287,10 @@ grep -h "^ring:" *.md | sort | uniq -c
```bash
# Compter les liens Équipe
grep -c 'href="/team"' .techradar/src/components/Navigation/Navigation.tsx
grep -c 'href="/team"' radar-app/src/components/Navigation/Navigation.tsx
# Voir le contexte autour du lien
grep -A 3 -B 3 'href="/team"' .techradar/src/components/Navigation/Navigation.tsx
grep -A 3 -B 3 'href="/team"' radar-app/src/components/Navigation/Navigation.tsx
```
## Obtenir de l'aide

609
export/team.md Normal file
View File

@@ -0,0 +1,609 @@
# Profils de l'équipe
Ce document contient tous les profils des membres de l'équipe fusionnés.
---
## 1000i100
---
name: "1000i100"
fullName: "1000i100"
role: "DevOps & Développeur Web"
availability: 50
seniorityLevel: expert
yearsExperience: 10
joinDate: "2018-01"
interests: ["Serverless", "CI/CD", "Docker", "Photographie", "CNV", "Modèles économiques"]
skills:
- name: "Serverless"
level: expert
years: 5
lastUsed: "2024-12"
- name: "GitLab"
level: expert
years: 6
lastUsed: "2024-12"
- name: "CI/CD"
level: expert
years: 6
lastUsed: "2024-12"
- name: "Docker"
level: expert
years: 7
lastUsed: "2024-12"
- name: "web"
level: expert
years: 10
lastUsed: "2024-12"
softSkills:
- "Polyvalence"
- "Photographie"
- "Soutien psychologique"
- "CNV (Communication Non Violente)"
projects:
- "Outils serverless"
- "Pipeline GitLab CI/CD"
---
Développeur d'outils serverless, et plombier des pipeline Gitlab (CI/CD avec Docker). Enfin une monnaie mécaniquement redistributive ! Avec un soupçon de revenu de base, une bonne dose d'auto-gestion et une communauté adorable ! Informaticien couteau suisse à dominante développeur web, photographe à ses heures, soutien psy informel, amateur de CNV et de modèles économiques expérimental et éthique !
---
## aya
---
name: "aya"
fullName: "aya"
role: "Administrateur Système & Infrastructure Distribuée"
availability: 50
seniorityLevel: expert
yearsExperience: 23
joinDate: "2021-01"
interests: ["Logiciels libres", "Infrastructure distribuée", "Stockage distribué", "IPFS", "ThreeFold"]
skills:
- name: "Linux"
level: expert
years: 23
lastUsed: "2024-12"
- name: "glusterfs"
level: intermediate
years: 5
lastUsed: "2023-06"
- name: "cephfs"
level: intermediate
years: 4
lastUsed: "2023-06"
- name: "ipfs"
level: intermediate
years: 3
lastUsed: "2024-12"
- name: "infrastructure"
level: expert
years: 15
lastUsed: "2024-12"
- name: "systèmes distribués"
level: expert
years: 10
lastUsed: "2024-12"
- name: "ThreeFold"
level: intermediate
years: 3
lastUsed: "2024-12"
softSkills:
- "Vulgarisation"
- "Autonomie"
- "Recherche"
projects:
- "Infrastructure d'hébergement distribué"
---
Je participe à la vulgarisation des logiciels libres depuis ma première installation de linux debian potato en 2001.
J'ai découvert la monnaie libre à travers mes recherches concernant les systèmes de fichiers. Travaillant principalement sur des infrastructures d'hébergement distribué, j'ai utilisé différents systèmes de réplication de fichiers comme glusterfs, cephfs, pour en arriver à ipfs. C'est en cherchant une alternative à filecoin, la crypto proposée par ipfs pour mettre en commun son espace de stockage, que je découvre la monnaie libre, on est en 2021.
Je rejoins Axiom-Team pour participer à la vulgarisation de la monnaie libre.
---
## boris
---
name: "boris"
fullName: "boris"
role: "UX/UI Designer & Développeur Full Stack"
availability: 40
seniorityLevel: intermediate
yearsExperience: 8
joinDate: "2018-01"
interests: ["UX/UI", "LLM", "Langues étrangères", "Médecine traditionnelle chinoise", "Feng Shui", "Tao", "Musique"]
skills:
- name: "UX"
level: intermediate
years: 5
lastUsed: "2024-12"
- name: "UI"
level: intermediate
years: 5
lastUsed: "2024-12"
- name: "Figma"
level: intermediate
years: 4
lastUsed: "2024-12"
- name: "LLM"
level: intermediate
years: 2
lastUsed: "2024-12"
- name: "JavaScript"
level: intermediate
years: 6
lastUsed: "2024-12"
- name: "TypeScript"
level: intermediate
years: 4
lastUsed: "2024-12"
- name: "APIs"
level: intermediate
years: 5
lastUsed: "2024-12"
- name: "Vis.js"
level: intermediate
years: 3
lastUsed: "2024-11"
softSkills:
- "Polyvalence"
- "Créativité"
- "Curiosité"
- "Multiculturalisme"
projects:
- "UX/UI de Ğecko (Figma)"
- "App de médecine chinoise basée sur LLM"
- "Site monnaie-libre.fr"
- "Duniter | Accueil"
- "cesium.app"
- "Ğ1Quest (vue radar des annonces Ğchange)"
- "Ğrocéliande (skin Ğchange style Amazon)"
- "g1.business (routes commerciales)"
- "Ğ1Gate (flux de monnaie en treemap)"
- "H2G2 (guide du terraformeur terrien)"
- "Ğ1 KDE Notifier"
- "Simulateur RSA / Prime d'activité"
- "Cerveau externe (Vis.js pour impros rap)"
- "NoBS Troll-Emploi (moteur de recherche d'emploi)"
---
Il est assez dispersé, "jack of all trade, master of none". Ces derniers temps, il passe beaucoup de temps à faire de la génération de musiques rigolotes (ou autre) avec les LLM et Suno. Il aime les langues étrangères (l'anglais surtout), la médecine traditionnelle chinoise, le Feng Shui (le tao en général). Il est communiste. Il a bossé sur l'UX/UI de Ğecko (via Figma). Grâce à Cursor, il développe une app de médecine chinoise basée sur les LLM. Dans la Ğ1, il a essayé de contribuer à l'onboarding (il a refait le site monnaie-libre.fr, Duniter | Accueil, et fait le site cesium.app). Il a aussi fait des clients Ğchange : Ğ1Quest (une projection des annonces Ğchange, notamment en "vue radar"), Ğrocéliande (un genre de skin pour Ğchange calqué sur l'interface d'Amazon, et qui ne prend que les annonces avec "envoi possible" dans la description), g1.business (qui permet de repérer les "routes commerciale", de faire correspondre pour un produit l'offre d'un endroit et la demande à un endroit distant, et qui projette sur une carte les moyens de productions disponibles à la location en Ğ1). Il a aussi fait Ğ1Gate (qui permet de suivre les flux de monnaie en vue "treemap"), H2G2 "le guide du terraformeur terrien" (une vue à la recette MineCraft de choses qu'on peut produire "dans la vraie vie"), Ğ1 KDE Notifier (Un petit outil pour être notifié de mouvements sur un portefeuille Ğ1), un Simulateur RSA / Prime d'activité (Un simulateur RSA/prime d'activité plus très à jour au niveau des données, mais qui permet de se rendre compte à quel point le fonctionnement de la prime d'activité est complètement stupide, et incite à éviter de travailler de façon trop importante trop ponctuellement, si on ne veut pas risquer de perdre de l'argent en allant se casser le cul au boulot), Cerveau externe (Un truc fait avec Vis.js, pour projeter des mots, colorés suivant la rime, regroupés autour des consonnes, et liés s'ils appartiennent à un même thème. Dans l'idée de faire des impros de rap avec. Proto sans réelle interface utilisateur utilisable par les moldus. Faire F5 pour raffraîchir et ainsi avoir un autre graphe de mots.), NoBS Troll-Emploi (Un moteur de recherche d'emploi basé sur l'API Pôle-Emploi et qui permet d'avoir plus de filtres : mots-clefs à exclure, pas de tutoiement, pas de "digital", etc… Idéal pour les gens qui, certes, acceptent d'être exploités lorsqu'ils développent du logiciel, mais veulent diminuer au maximum la quantité de bullshit dans leur job).
---
## elois
---
name: "elois"
fullName: "Eloïs"
role: "Développeur Blockchain"
availability: 25
seniorityLevel: expert
yearsExperience: 5
joinDate: "2019-01"
interests: ["Blockchain", "Rust", "Migration", "Cryptographie"]
skills:
- name: "Rust"
level: expert
years: 5
lastUsed: "2024-12"
- name: "blockchain"
level: expert
years: 5
lastUsed: "2024-12"
- name: "Substrate"
level: expert
years: 4
lastUsed: "2024-12"
- name: "migration"
level: expert
years: 3
lastUsed: "2024-11"
softSkills:
- "Autodidactie"
- "Recherche"
- "Architecture"
projects:
- "Rustification de Duniter v1"
- "Duniter v2S"
---
A appris les technologies blockchain en autodidact, travaillé sur la "rustification" (passage en Rust) de Duniter v1, puis bossé chez MoonPay.
---
## fred
---
name: "fred"
fullName: "Fred"
role: "Développeur & Architecte Systèmes Décentralisés"
availability: 40
seniorityLevel: expert
yearsExperience: 20
joinDate: "2014-01"
interests: ["IPFS", "Secure ScuttleButt", "Nostr", "TiddlyWiki", "ThreeFold", "Systèmes décentralisés"]
skills:
- name: "IPFS"
level: expert
years: 6
lastUsed: "2024-12"
- name: "Secure ScuttleButt"
level: expert
years: 5
lastUsed: "2024-11"
- name: "Nostr"
level: expert
years: 3
lastUsed: "2024-12"
- name: "TiddlyWiki"
level: expert
years: 8
lastUsed: "2024-12"
- name: "développement"
level: expert
years: 20
lastUsed: "2024-12"
- name: "ThreeFold"
level: intermediate
years: 2
lastUsed: "2024-12"
softSkills:
- "Architecture"
- "Entrepreneuriat"
- "Innovation"
projects:
- "Astroport (système d'information combinant Ğ1, IPFS et Nostr)"
- "G1SMS (système de paiement par SMS en Ğ1)"
- "G1billet (paper wallet pour la Ğ1)"
- "Linkeo (entreprise)"
---
A monté une boite (Linkeo) qui a bouffé une partie du marché de PagesJaunes début/milieu des années 2000. Très intéressé (et sachant) sur IPFS, Secure ScuttleButt, Nostr et TiddlyWiki. Il développe Astroport, un système d'information qui combine la Ğ1, IPFS et Nostr. Par le passé, il a aussi créé G1SMS (système de paiement par SMS en Ğ1) et G1billet (paper wallet pour la Ğ1).
---
## hugo
---
name: "hugo"
fullName: "Hugo Trentesaux"
role: "Financement & Gestion"
availability: 20
seniorityLevel: intermediate
yearsExperience: 5
joinDate: "2017-01"
interests: ["Financement", "Gestion", "Rédaction", "Administration"]
skills:
- name: "financement"
level: intermediate
years: 5
lastUsed: "2024-12"
- name: "rédaction"
level: intermediate
years: 5
lastUsed: "2024-12"
- name: "gestion"
level: intermediate
years: 5
lastUsed: "2024-12"
softSkills:
- "Rédaction"
- "Administration"
- "Gestion de projet"
projects:
- "Dossier de financement Ğecko (ADEME)"
---
Je m'intéresse à la Ğ1 depuis 2017 et pense que l'association Axiom Team constitue une base juridique utile car nécessaire pour de nombreuses interactions avec le monde €.
J'ai travaillé sur le dossier de financement de Ǧecko auprès de l'ADEME avec succès. À l'avenir, je compte participer au fonctionnement d'Axiom Team, et à la partie rédactionnelle des dossiers de financement.
---
## manuTopik
---
name: "manuTopik"
fullName: "ManUtopiK"
role: "Développeur Web Full Stack"
availability: 40
seniorityLevel: expert
yearsExperience: 12
joinDate: "2014-01"
interests: ["Web", "Alternatives", "Monnaie libre", "Solarpunk", "Intelligence collective"]
skills:
- name: "VueJS"
level: expert
years: 8
lastUsed: "2024-12"
- name: "Nuxt.js"
level: expert
years: 6
lastUsed: "2024-11"
- name: "JavaScript"
level: expert
years: 12
lastUsed: "2024-12"
- name: "TypeScript"
level: intermediate
years: 4
lastUsed: "2024-12"
- name: "CMS"
level: expert
years: 5
lastUsed: "2024-12"
- name: "web"
level: expert
years: 12
lastUsed: "2024-12"
softSkills:
- "Communication"
- "Vulgarisation"
- "Créativité"
projects:
- "monnaie-libre.fr"
- "carte.monnaie-libre.fr"
- "Doc silkaj"
- "WotWizard-UI"
- "g1lib"
- "Duniter UI (nuxt - abandonné)"
- "Extension web g1Compagnon (en cours)"
- "Interface web pour g1Billet (en cours)"
---
Diplomé dans le domaine des énergies renouvelables, mon côté "web enthousiaste" m'a finalement amené à faire du développement web depuis + de 12 ans.
Passionné par tout ce qui est "alternatif" et qui rend libre, j'ai découvert le concept de la monnaie libre en 2014. L'économie actuelle est à mes yeux le principal facteur du bordel que l'on a mis sur cette planète depuis des générations. J'espère en un monde un peu plus libre, auto gouverné en intelligence collective, et avec du #solarpunk comme horizon. Profitons des crises pour tout changer !
À fond sur VueJS ; il a créé un CMS basé sur VueJS.
## Contributions
- Développement et rédaction du site monnaie-libre.fr (Dépôt du site, de l'api)
- Développement de la carte.monnaie-libre.fr (Dépôt)
- Doc silkaj
- WotWizard-UI
- g1lib
- Duniter UI avec nuxt (Abandonné)
## En cours
- Extension web g1Compagnon
- Interface web pour g1Billet
---
## poka
---
name: "poka"
fullName: "Poka"
role: "Développeur Full Stack & Administrateur Système"
availability: 50
seniorityLevel: expert
yearsExperience: 8
joinDate: "2016-01"
interests: ["Mobile", "Infrastructure", "Automatisation", "Blockchain"]
skills:
- name: "Flutter"
level: expert
years: 4
lastUsed: "2024-12"
- name: "Dart"
level: expert
years: 4
lastUsed: "2024-12"
- name: "Python"
level: intermediate
years: 5
lastUsed: "2024-11"
- name: "bash"
level: expert
years: 8
lastUsed: "2024-12"
- name: "ProxMox"
level: expert
years: 6
lastUsed: "2024-12"
- name: "infrastructure"
level: expert
years: 8
lastUsed: "2024-12"
softSkills:
- "Autonomie"
- "Pédagogie"
- "Maintenance système"
projects:
- "Ğecko"
- "Ğ1-stats"
- "jaklis"
- "py-g1-migrator"
- "Infrastructure Axiom-Team"
---
Je suis contributeur actif sur le projet Duniter depuis 2016 aux RML7 de Laval.
Je code Ğecko en Flutter/Dart. Je maintiens aussi l'infra Axiom-Team, soit 2 serveurs ProxMox.
J'ai aussi codé Ğ1-stats en bash. Et jaklis en python. J'ai aussi codé py-g1-migrator
---
## syoul
---
name: "syoul"
fullName: "Syoul"
role: "Etudiant IPSSI - Alternance Admin Infrastructure Securisee chez AJR"
availability: 50
seniorityLevel: beginner
yearsExperience: 1
joinDate: "2024-06"
interests: ["Autohebergement", "Proxmox", "Docker", "Infrastructure", "Securite"]
skills:
- name: "Proxmox"
level: beginner
years: 3
lastUsed: "2024-12"
- name: "Docker"
level: beginner
years: 1
lastUsed: "2024-12"
- name: "Linux"
level: beginner
years: 1
lastUsed: "2024-12"
- name: "autohebergement"
level: beginner
years: 3
lastUsed: "2024-12"
softSkills:
- "Apprentissage"
- "Curiosite"
- "Autonomie"
projects:
- "Autohebergement personnel (Proxmox + Docker)"
- "Alternance AJR - Administration Infrastructure"
---
Etudiant a l'IPSSI depuis 6 mois, en alternance Administrateur Infrastructure Securisee chez AJR.
Gere son infrastructure personnelle avec Proxmox et Docker pour l'autohebergement de services.
---
## tuxmain
---
name: "tuxmain"
fullName: "tuxmain"
role: "Étudiant Math & Cryptographie"
availability: 20
seniorityLevel: beginner
yearsExperience: 3
joinDate: "2022-01"
interests: ["Mathématiques", "Cryptographie", "Chiffrage", "Électronique", "Minetest"]
skills:
- name: "cryptographie"
level: intermediate
years: 3
lastUsed: "2024-12"
- name: "chiffrage"
level: intermediate
years: 3
lastUsed: "2024-12"
- name: "math"
level: expert
years: 5
lastUsed: "2024-12"
- name: "électronique"
level: beginner
years: 2
lastUsed: "2024-11"
softSkills:
- "Recherche"
- "Analyse"
- "Bidouille"
projects:
- "Administration serveur Minetest"
- "Bidouille électronique"
---
Étudiant en math. Bien compétent sur la cryptographie, le chiffrage, les conversions de clef d'une base en une autre. Administrateur de serveur Minetest. Il bidouille aussi de l'électronique.
---
## vivien
---
name: "vivien"
fullName: "Vivien"
role: "Développeur"
availability: 40
seniorityLevel: beginner
yearsExperience: 2
joinDate: "2023-01"
interests: ["Cesium", "Godot", "Jeux", "Cartes Magic"]
skills:
- name: "Cesium"
level: beginner
years: 2
lastUsed: "2024-12"
- name: "Godot"
level: beginner
years: 2
lastUsed: "2024-11"
softSkills:
- "Apprentissage"
- "Curiosité"
projects:
- "Contribution à Cesium"
- "Développement en Godot"
---
Se forme pour contribuer à certains logiciels de la Ğ1 (Cesium). Développe aussi en Godot. Passionné de jeux (cartes Magic notamment).
---
## yvv
---
name: "yvv"
fullName: "Yvv"
role: "Gestion & Mobilisation"
availability: 70
seniorityLevel: senior
yearsExperience: 10
joinDate: "2015-01"
interests: ["Gestion", "Mobilisation", "Économie du don", "Wiki", "Médiathèque"]
skills:
- name: "gestion"
level: expert
years: 10
lastUsed: "2024-12"
- name: "médiathèque"
level: intermediate
years: 3
lastUsed: "2024-11"
- name: "wiki"
level: intermediate
years: 5
lastUsed: "2024-12"
softSkills:
- "Gestion"
- "Organisation"
- "Mobilisation"
- "Communication"
projects:
- "Tuyauterie autogestion des dons (UNL)"
- "WishBounty v2"
- "FAQs version wiki"
- "Médiathèque (nocodb)"
- "Librodrome"
- "Conserverie éphémère mobile"
---
Vieux bouc dans le CA, je tire ma révérence en tant que secrétaire. Focus sur ce qui m'intéresse le plus, nouvelle forme de mobilisation.
## Pour mission UNL
- Aboutir la tuyauterie autogestion des dons.
- L'élargir pour une v2 sur … un goût de paradis, le WishBounty.
## Pour mission fédération - services aux monnaie-libristes
- Bosser sur une FAQs version wiki, si un mediawiki ou autre voit le jour.
- Bosser sur une médiathèque, si un nocodb ou autre voit le jour.
## Pour ML
- Diffuser mon bouquin "une économie du don - enfin concevable" et m'en servir de support pour mener des ateliers éco et "passer la seconde".
- Lancer un événement structurant, le Librodrome.
- Lancer une expérience de production collective monnaie-libriste, probablement une conserverie éphémère mobile.

445
export/technologies-team.md Normal file
View File

@@ -0,0 +1,445 @@
# Technologies et Compétences - Écosystème Duniter/Ğ1
Ce document liste les technologies et compétences identifiées dans l'écosystème Duniter/Ğ1 basé sur l'analyse de https://git.duniter.org/
## Technologies de Développement
### Langages de Programmation
#### Rust
- **Utilisation** : Développement du nœud Duniter v2S (basé sur Substrate)
- **Projets** :
- `Duniter v2S` : Nœud blockchain principal
- `Ğcli-v2s` : Interface en ligne de commande Rust
- `homebrew-duniter-gcli` : Package Homebrew pour Ğcli
- **Compétences requises** : Rust avancé, développement blockchain, Substrate framework
#### Python
- **Utilisation** : Clients en ligne de commande et outils
- **Projets** :
- `silkaj` : Client CLI Python pour la monnaie Ğ1
- `Tikka` : Client riche pour la monnaie Ğ1
- **Compétences requises** : Python, développement CLI, APIs REST
#### JavaScript/TypeScript
- **Utilisation** : Clients web, extensions navigateur, sites web
- **Projets** :
- `Ğ1Companion` : Extension web pour navigateurs
- Clients web divers
- **Compétences requises** : JavaScript/TypeScript, développement d'extensions navigateur, Web APIs
### Frameworks et Bibliothèques
#### Substrate Framework
- **Utilisation** : Framework blockchain pour Duniter v2S
- **Description** : Framework Rust pour construire des blockchains personnalisées
- **Compétences requises** : Blockchain, Rust, Substrate, consensus algorithms
#### Nuxt.js
- **Utilisation** : Framework Vue.js pour sites web
- **Projets** :
- `monnaie-libre-fr` : Site web avec Nuxt + nuxt-content
- **Compétences requises** : Vue.js, Nuxt.js, SSR, JAMstack
#### NetlifyCMS
- **Utilisation** : CMS headless basé sur Git
- **Projets** :
- `monnaie-libre-fr` : CMS pour le site web
- **Compétences requises** : Git-based CMS, JAMstack, workflows Git
#### WordUp CMS
- **Utilisation** : CMS pour sites web
- **Projets** :
- `axiom-team-fr` : Site de production avec WordUp
- **Compétences requises** : CMS management, intégration d'APIs
### Outils et Bibliothèques Spécialisées
#### Squid (Indexer)
- **Utilisation** : Indexation de données blockchain
- **Projets** :
- `duniter-squid` : Indexer basé sur Squid pour Duniter v2S
- **Compétences requises** : Indexation de données, GraphQL, blockchain data processing
#### g1-papi
- **Utilisation** : Bibliothèque API pour Ğ1
- **Type** : Bibliothèque partagée
- **Compétences requises** : API design, développement de bibliothèques
### Clients et Interfaces
#### Clients CLI (Command Line Interface)
- **Rust CLI** : `Ğcli-v2s` - Interface avancée pour utilisateurs experts
- **Python CLI** : `silkaj`, `Tikka` - Clients en ligne de commande
- **Compétences requises** : Développement CLI, UX en ligne de commande, parsing d'arguments
#### Extensions Navigateur
- **Ğ1Companion** : Extension web pour navigateurs
- **Compétences requises** : Web Extensions API, Chrome/Firefox extensions, JavaScript
#### Clients Graphiques
- **Ğecko** : Client avec interface graphique
- **Cesium-grp/cesium2s** : Client Cesium pour Duniter v2s
- **Compétences requises** : Développement d'interfaces graphiques, frameworks UI
### Intégrations et APIs
#### Intégrations Externes
- **HelloAsso** : Intégration pour dons
- **Paheko** : Intégration comptable
- **ĞWishBounty** : Application pour automatiser les flux de dons
- **Compétences requises** : Intégration d'APIs tierces, webhooks, synchronisation de données
#### APIs Internes
- **api-axiom-team-fr** : API pour le site Axiom
- **Compétences requises** : REST APIs, GraphQL, documentation d'API
## Technologies d'Authentification et d'Identité
### Authentification et Autorisation
#### Microsoft Entra (concurrents)
- **Utilisation** : Solution d'identité et d'accès cloud de Microsoft
- **Description** : Plateforme d'identité en tant que service (IDaaS) qui fournit l'authentification unique (SSO), la gestion des identités et l'accès conditionnel. Alternative aux solutions d'authentification traditionnelles.
- **Compétences requises** : Gestion d'identité cloud, SSO, intégration d'identité, sécurité des accès
#### AUTHZ et AUTHN
- **Utilisation** : Concepts fondamentaux de sécurité
- **Description** :
- **AUTHN (Authentication)** : Vérification de l'identité d'un utilisateur (qui êtes-vous ?)
- **AUTHZ (Authorization)** : Vérification des permissions d'accès (que pouvez-vous faire ?)
- **Compétences requises** : Principes de sécurité, gestion des identités, contrôle d'accès, modèles de permissions
#### Better Auth
- **Utilisation** : Bibliothèque d'authentification moderne
- **Description** : Solution d'authentification open-source offrant une API simple et flexible pour gérer l'authentification dans les applications web. Supporte OAuth, email/password, et autres méthodes d'authentification.
- **Compétences requises** : Développement web, authentification, OAuth, sécurité des applications
### Identité Décentralisée
#### DID et UCAN
- **Utilisation** : Identifiants décentralisés et système d'autorisation
- **Description** :
- **DID (Decentralized Identifiers)** : Identifiants uniques décentralisés qui permettent aux utilisateurs de contrôler leur identité sans dépendre d'une autorité centrale
- **UCAN (User Controlled Authorization Networks)** : Système d'autorisation basé sur des capacités (capabilities) où les utilisateurs contrôlent leurs propres permissions
- **Compétences requises** : Identité décentralisée, Web3, cryptographie, systèmes d'autorisation basés sur les capacités
#### VC (Verifiable Credentials)
- **Utilisation** : Credentials vérifiables pour l'identité numérique
- **Description** : Standard W3C pour les credentials numériques qui peuvent être vérifiés cryptographiquement. Permet de créer des identités numériques portables et vérifiables sans dépendre d'une autorité centrale.
- **Compétences requises** : Standards W3C, identité numérique, cryptographie, vérification de credentials, blockchain (optionnel)
### Protocoles d'Authentification
#### OpenID Connect
- **Utilisation** : Protocole d'authentification et d'autorisation
- **Description** : Couche d'identité construite sur OAuth 2.0 qui permet aux clients de vérifier l'identité d'un utilisateur basée sur l'authentification effectuée par un serveur d'autorisation. Standard de l'industrie pour l'authentification fédérée.
- **Compétences requises** : OAuth 2.0, protocoles d'authentification, intégration SSO, sécurité web
#### SPIFFE
- **Utilisation** : Identité sécurisée pour les workloads en production
- **Description** : SPIFFE (Secure Production Identity Framework For Everyone) fournit un cadre pour identifier et authentifier les workloads dans des environnements hétérogènes et distribués. Utilise des identités basées sur des certificats X.509 ou JWT.
- **Compétences requises** : Sécurité des microservices, identité des workloads, mTLS, infrastructure distribuée, Kubernetes, service mesh
## Technologies d'Infrastructure Décentralisée
### ThreeFold
#### Zero OS
- **Utilisation** : Système d'exploitation autonome pour infrastructure décentralisée
- **Description** : Système d'exploitation efficace et sécurisé qui s'exécute directement sur le matériel, permettant un cloud autonome
- **Compétences requises** : Administration système bare metal, cloud décentralisé, Zero OS
#### ThreeFold Grid
- **Utilisation** : Infrastructure Internet décentralisée globale
- **Description** : Plateforme opérationnelle d'infrastructure Internet décentralisée déployée localement, scalable globalement, possédée et alimentée par les utilisateurs
- **Compétences requises** : Infrastructure décentralisée, cloud computing, réseaux distribués
#### 3Node
- **Utilisation** : Nœuds physiques de l'infrastructure ThreeFold
- **Description** : Serveurs informatiques dédiés à 100% au réseau, fournissant capacité de calcul, stockage et réseau
- **Compétences requises** : Administration de serveurs, déploiement de nœuds, maintenance hardware
#### ThreeFold Compute
- **Utilisation** : Capacité de calcul bare metal
- **Description** : Peut exécuter toute charge de travail Web2, Web3 ou IA à la périphérie d'Internet, avec plus de scalabilité et de fiabilité
- **Compétences requises** : Virtualisation, conteneurisation, Kubernetes, edge computing
#### ThreeFold Data Storage
- **Utilisation** : Système de stockage de données inviolable
- **Description** : Les données ne peuvent pas être compromises et restent toujours privées, possédées par vous. Système scalable jusqu'au niveau planétaire, au moins 10x plus efficace et plusieurs ordres de grandeur plus sécurisé et fiable
- **Compétences requises** : Stockage distribué, réplication de données, sécurité des données
#### ThreeFold Network (Mycelium)
- **Utilisation** : Réseau overlay chiffré de bout en bout
- **Description** : Réseau toujours à la recherche du chemin le plus court possible entre les participants. Adresse Internet logique liée de manière sécurisée à une clé privée. Scalabilité illimitée et optimisations de performance
- **Compétences requises** : Réseaux overlay, chiffrement de bout en bout, routage réseau
#### ThreeFold Blockchain
- **Utilisation** : Blockchain pour la vérification et l'enregistrement de la capacité
- **Description** : Vérifie, enregistre et sécurise la capacité des nœuds sur la blockchain ThreeFold
- **Compétences requises** : Blockchain, consensus, cryptographie
#### ThreeFold Cloud
- **Utilisation** : Cloud open-source décentralisé
- **Description** : Déploiement de machines virtuelles, conteneurs, clusters Kubernetes, web gateways et plus sur un cloud open source décentralisé best-effort
- **Compétences requises** : Cloud computing, Kubernetes, déploiement d'applications, administration système
#### AIBox
- **Utilisation** : Solution de calcul IA auto-hébergée alimentée par ThreeFold
- **Description** : Solution de calcul IA dédiée fonctionnant sur l'infrastructure ThreeFold
- **Compétences requises** : Intelligence artificielle, machine learning, infrastructure IA
#### 3Phone
- **Utilisation** : Appareils sécurisés de la famille 3Phone
- **Description** : Premiers appareils sécurisés conçus pour fonctionner de manière transparente avec le ThreeFold Grid
- **Compétences requises** : Développement mobile, sécurité des appareils, intégration réseau
#### 3Router
- **Utilisation** : Routeurs intelligents pour connexions optimisées
- **Description** : Routeurs intelligents garantissant des connexions de chemin le plus court entre nœuds et téléphones avec chiffrement de bout en bout
- **Compétences requises** : Routage réseau, optimisation de réseau, sécurité réseau
## Technologies d'Infrastructure et Déploiement
### Conteneurisation
- **Docker** : Conteneurisation des applications
- **Compétences requises** : Docker, Docker Compose, orchestration de conteneurs
### Déploiement Web
- **Netlify** : Déploiement JAMstack (mentionné pour monnaie-libre-fr)
- **Compétences requises** : CI/CD, déploiement continu, Netlify
### Gestion de Code Source
- **Git** : Système de contrôle de version
- **Forge Git** : git.duniter.org (forge Git auto-hébergée)
- **Compétences requises** : Git avancé, workflows Git, gestion de forge
### Package Management
- **Homebrew** : Gestion de paquets pour macOS
- **npm/yarn** : Gestion de paquets JavaScript
- **pip/poetry** : Gestion de paquets Python
- **Cargo** : Gestion de paquets Rust
- **Compétences requises** : Gestion de dépendances, gestion de versions, publication de paquets
## Compétences d'Administration Système
### Administration Linux/Unix
- **Systèmes d'exploitation** : Linux (Debian, Ubuntu, etc.)
- **Compétences requises** :
- Administration système Linux
- Gestion des utilisateurs et permissions
- Configuration réseau
- Monitoring système
- Gestion des logs
- Sécurisation des serveurs
### Administration Blockchain
- **Gestion de nœuds** : Administration de nœuds Duniter
- **Compétences requises** :
- Configuration de nœuds blockchain
- Gestion de la synchronisation
- Monitoring de la blockchain
- Gestion des clés cryptographiques
- Maintenance des nœuds
### Bases de Données
- **PostgreSQL** : Base de données relationnelle utilisée dans les projets
- **Compétences requises** :
- Administration PostgreSQL
- Optimisation de requêtes
- Sauvegarde et restauration
- Réplication
- Performance tuning
- SQL avancé
### Réseau et Sécurité
- **Réseau** :
- Configuration de pare-feu
- Gestion des ports et services
- Load balancing
- CDN configuration
- DNS, DHCP, VPN, SD-WAN
- Configuration réseau avancée
- **Sécurité** :
- SSL/TLS configuration
- Gestion des certificats
- Sécurisation des APIs
- Protection contre les attaques
- Audit de sécurité
- Chiffrement des communications et données
- Surveillance et détection d'intrusions
- Prévention des cyberattaques
### Monitoring et Observabilité
- **Monitoring** :
- Monitoring des applications
- Monitoring des nœuds blockchain
- Alerting
- Métriques et dashboards
- **Logs** :
- Centralisation des logs
- Analyse de logs
- Rotation des logs
### CI/CD et Automatisation
- **Intégration Continue** :
- Configuration de pipelines CI/CD
- Tests automatisés
- Build automatisé
- Déploiement automatisé
- **Outils** :
- GitHub Actions, GitLab CI, Drone CI
- Scripts d'automatisation
- Configuration de workflows
### Automatisation et Scripting
- **Scripts** :
- Bash scripting avancé
- Python scripting pour automatisation
- Automatisation de tâches d'administration
- Scripts de déploiement
- Automatisation des environnements pour cohérence
- **Compétences requises** : Scripting, automatisation, amélioration de la cohérence des environnements
### Infrastructure Cloud/On-Premise
- **Cloud** :
- Déploiement sur cloud (si applicable)
- Gestion de ressources cloud
- Auto-scaling
- Cloud décentralisé (ThreeFold Grid)
- **On-Premise** :
- Gestion de serveurs physiques
- Virtualisation (VMware, Hyper-V, KVM)
- Gestion de l'infrastructure
- Provisioning de serveurs
- Infrastructure décentralisée (3Nodes)
### Gestion de Configuration
- **Configuration Management** :
- Ansible, Puppet, Chef
- Infrastructure as Code
- Configuration de serveurs
- **Versioning** :
- Versioning de la configuration
- Gestion des environnements (dev, staging, prod)
### Sauvegarde et Récupération
- **Sauvegarde** :
- Stratégies de sauvegarde
- Sauvegarde des bases de données
- Sauvegarde de la configuration
- Sauvegarde de la blockchain
- **Récupération** :
- Plans de reprise après sinistre
- Tests de restauration
- RTO/RPO
## Compétences DevOps
### Container Orchestration
- **Kubernetes** : Orchestration de conteneurs (mentionné comme compétence requise)
- **Docker Swarm** : Alternative à Kubernetes
- **Compétences requises** : Orchestration, scaling, service mesh, gestion de clusters
### Infrastructure as Code
- **Terraform** : Provisioning d'infrastructure
- **CloudFormation** : Si AWS
- **Compétences requises** : IaC, provisioning automatisé
### Secrets Management
- **Gestion des secrets** : Vault, AWS Secrets Manager
- **Compétences requises** : Sécurité des secrets, rotation
## Compétences Spécialisées Blockchain
### Cryptographie
- **Cryptographie appliquée** :
- Signatures cryptographiques
- Hashing
- Clés publiques/privées
- Certificats
- **Compétences requises** : Cryptographie, sécurité
### Consensus et Réseau
- **Protocoles de consensus** : Compréhension des mécanismes de consensus
- **Réseau P2P** : Gestion de réseaux pair-à-pair
- **Compétences requises** : Blockchain, réseaux distribués
## Résumé des Compétences par Catégorie
### Développement
- Rust (avancé)
- Python
- JavaScript/TypeScript
- Vue.js / Nuxt.js
- Substrate Framework
- Développement CLI
- Extensions navigateur
- APIs REST/GraphQL
### Blockchain
- Développement blockchain
- Substrate
- Consensus algorithms
- Cryptographie
- Réseaux P2P
### Web
- Frameworks web modernes
- JAMstack
- CMS headless
- Intégrations d'APIs
### Infrastructure
- Administration Linux
- Docker/Conteneurisation
- CI/CD
- Monitoring
- Sécurité
- Bases de données
- Réseau
- Infrastructure décentralisée (ThreeFold Grid)
- Edge computing
- Cloud décentralisé
- Zero OS
- Stockage distribué
### DevOps
- Automatisation
- Infrastructure as Code
- Gestion de configuration
- Orchestration
## Compétences Transversales
### Communication et Collaboration
- Travail en équipe avec développeurs et parties prenantes
- Communication efficace
- Documentation technique
- Partage de connaissances
### Veille Technologique
- Suivi des évolutions technologiques
- Meilleures pratiques du secteur
- Évaluation de nouvelles technologies
- Adaptation aux changements
## Notes
Cette liste est basée sur l'analyse des projets visibles sur https://git.duniter.org/ et les informations disponibles sur l'écosystème Duniter/Ğ1. Certaines technologies peuvent être utilisées mais non explicitement mentionnées dans les descriptions de projets.
### Sources
- https://git.duniter.org/ - Dépôt principal des projets Duniter
- https://www.threefold.io/ - Infrastructure Internet décentralisée ThreeFold
- Documentation technique des projets individuels
- Analyse des technologies blockchain et monnaies libres
- Analyse des infrastructures décentralisées
### Pour une analyse complète, il serait recommandé de :
1. Examiner le code source des projets principaux
2. Analyser les fichiers de configuration (package.json, Cargo.toml, requirements.txt, Dockerfile)
3. Examiner les fichiers de déploiement (docker-compose.yml, scripts CI/CD)
4. Consulter la documentation technique de chaque projet
5. Analyser les dépendances et bibliothèques utilisées

1133
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,18 @@
{
"name": "aoe-techradar",
"name": "techradar-laplank",
"private": true,
"version": "4.3.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "techradar build",
"serve": "techradar serve",
"build": "node scripts/build-radar.js",
"serve": "node scripts/serve-radar.js build",
"serve-dev": "node scripts/serve-radar.js dev",
"serve-business": "./scripts/serve-business.sh",
"extract-tech": "node scripts/extract-technologies.js",
"analyze-business": "node scripts/analyze-business-metrics.js"
},
"dependencies": {
"aoe_technology_radar": "github:AOEpeople/aoe_technology_radar#main",
"glob": "^10.3.10",
"gray-matter": "^4.0.3"
},

6
radar-app/.eslintrc.json Normal file
View File

@@ -0,0 +1,6 @@
{
"extends": "next/core-web-vitals",
"rules": {
"@next/next/no-img-element": "off"
}
}

27
radar-app/.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Test AOE Technology Radar
on:
pull_request:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
- uses: actions/cache@v4
with:
path: |
~/.npm
**/node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm
- run: npm ci
- run: npm run build:data
- run: npm run build
- run: if [ -n "$(git status --porcelain)" ]; then echo 'workspace is dirty after rebuilding' ; git status ; git diff ; exit 1 ; fi

View File

@@ -0,0 +1,22 @@
name: Semanticore
on:
push:
branches:
- main
jobs:
semanticore:
runs-on: ubuntu-latest
name: Semanticore
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.*
- name: Semanticore
run: go run github.com/aoepeople/semanticore@v0 -npm-update-version package.json
env:
SEMANTICORE_TOKEN: ${{secrets.GITHUB_TOKEN}}

42
radar-app/.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
/techradar/
# generated
/src/components/Icons/
/data/about.json
/data/data.json
# misc
.DS_Store
.idea/
*.pem
*.tgz
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

1
radar-app/.husky/commit-msg Executable file
View File

@@ -0,0 +1 @@
npx --no-install commitlint --edit "$1"

View File

@@ -0,0 +1 @@
npx lint-staged

8
radar-app/.npmignore Normal file
View File

@@ -0,0 +1,8 @@
*.tgz
.idea
.github
/.next/
/out/
/techradar/
/data/about.json
/data/data.json

View File

@@ -0,0 +1,3 @@
node_modules
out
CHANGELOG.md

11
radar-app/.prettierrc Normal file
View File

@@ -0,0 +1,11 @@
{
"printWidth": 80,
"tabWidth": 2,
"trailingComma": "all",
"singleQuote": false,
"semi": true,
"importOrder": ["^[./]", "^@(.*)$"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": ["@trivago/prettier-plugin-sort-imports"]
}

204
radar-app/LICENSE Normal file
View File

@@ -0,0 +1,204 @@
The license applies to the generator code and not the articles in the "radar" folder. (Read also the README.md in this folder)
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

167
radar-app/bin/techradar.js Normal file
View File

@@ -0,0 +1,167 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const crypto = require("crypto");
const CWD = process.cwd();
const BUILDER_DIR = path.join(CWD, ".techradar");
const SOURCE_DIR = path.join(CWD, "node_modules", "aoe_technology_radar");
const HASH_FILE = path.join(BUILDER_DIR, "hash");
const PARAMETER = process.argv[2]; // "build" or "serve"
const FLAGS = process.argv.slice(3).join(" ");
function info(message) {
console.log(`\x1b[32m${message}\x1b[0m`);
}
function warn(message) {
console.log(`\x1b[33mWarning: ${message}\x1b[0m`);
}
function error(message) {
console.error(`Error: ${message}`);
process.exit(1);
}
function bootstrap() {
if (!fs.existsSync(path.join(CWD, "radar"))) {
warn(
"Could not find radar directory. Created a bootstrap radar directory in your current working directory. Feel free to customize it.",
);
fs.cpSync(path.join(SOURCE_DIR, "data", "radar"), path.join(CWD, "radar"), {
recursive: true,
});
}
if (!fs.existsSync(path.join(CWD, "public"))) {
warn(
"Could not find public directory. Created a public radar directory in your current working directory.",
);
fs.cpSync(path.join(SOURCE_DIR, "public"), path.join(CWD, "public"), {
recursive: true,
});
}
if (!fs.existsSync(path.join(CWD, "config.json"))) {
warn(
"Could not find a config.json. Created a bootstrap config.json in your current working directory. Customize it to your needs.",
);
fs.copyFileSync(
path.join(SOURCE_DIR, "data", "config.default.json"),
path.join(CWD, "config.json"),
);
}
if (!fs.existsSync(path.join(CWD, "about.md"))) {
warn(
"Could not find a about.md. Created a bootstrap about.md in your current working directory. Customize it to your needs.",
);
fs.copyFileSync(
path.join(SOURCE_DIR, "data", "about.md"),
path.join(CWD, "about.md"),
);
}
if (!fs.existsSync(path.join(CWD, "custom.css"))) {
warn("Created a bootstrap custom.css in your current working directory.");
fs.copyFileSync(
path.join(SOURCE_DIR, "src", "styles", "custom.css"),
path.join(CWD, "custom.css"),
);
}
}
// Calculate current hash of package.json
function calculateHash(file) {
const fileBuffer = fs.readFileSync(file);
const hashSum = crypto.createHash("sha256");
hashSum.update(fileBuffer);
return hashSum.digest("hex");
}
const CURRENT_HASH = calculateHash(path.join(CWD, "package.json"));
// Check if builder dir needs to be recreated
let RECREATE_DIR = false;
if (
!fs.existsSync(BUILDER_DIR) ||
!fs.existsSync(HASH_FILE) ||
fs.readFileSync(HASH_FILE, "utf8") !== CURRENT_HASH
) {
RECREATE_DIR = true;
}
if (RECREATE_DIR) {
// Remove existing builder dir if it exists
if (fs.existsSync(BUILDER_DIR)) {
fs.rmSync(BUILDER_DIR, { recursive: true });
}
// Copy source dir to builder dir
try {
fs.cpSync(SOURCE_DIR, BUILDER_DIR, { recursive: true });
fs.writeFileSync(HASH_FILE, CURRENT_HASH);
} catch (e) {
error(`Could not copy ${SOURCE_DIR} to ${BUILDER_DIR}`);
}
try {
process.chdir(BUILDER_DIR);
info("Installing npm packages");
execSync("npm install", { stdio: "inherit" });
} catch (e) {
error("Could not install npm packages");
}
}
bootstrap();
try {
if (fs.existsSync(path.join(BUILDER_DIR, "data", "radar"))) {
fs.rmSync(path.join(BUILDER_DIR, "data", "radar"), { recursive: true });
}
fs.cpSync(path.join(CWD, "radar"), path.join(BUILDER_DIR, "data", "radar"), {
recursive: true,
});
fs.cpSync(path.join(CWD, "public"), path.join(BUILDER_DIR, "public"), {
recursive: true,
});
fs.copyFileSync(
path.join(CWD, "about.md"),
path.join(BUILDER_DIR, "data", "about.md"),
);
fs.copyFileSync(
path.join(CWD, "custom.css"),
path.join(BUILDER_DIR, "src", "styles", "custom.css"),
);
fs.copyFileSync(
path.join(CWD, "config.json"),
path.join(BUILDER_DIR, "data", "config.json"),
);
process.chdir(BUILDER_DIR);
} catch (e) {
error(e.message);
}
info("Building data");
execSync(`npm run build:data -- ${FLAGS}`, {
stdio: "inherit",
});
if (PARAMETER === "serve") {
info("Starting techradar");
execSync("npm run dev", { stdio: "inherit" });
}
if (PARAMETER === "build") {
info("Building techradar");
execSync("npm run build", { stdio: "inherit" });
if (fs.existsSync(path.join(CWD, "build"))) {
fs.rmSync(path.join(CWD, "build"), { recursive: true });
}
info(`Copying techradar to ${path.join(CWD, "build")}`);
fs.renameSync(path.join(BUILDER_DIR, "out"), path.join(CWD, "build"));
}

View File

@@ -0,0 +1,26 @@
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
[
"feat",
"sec",
"fix",
"bug",
"test",
"refactor",
"rework",
"ops",
"ci",
"cd",
"build",
"doc",
"perf",
"chore",
"update",
],
],
},
};

View File

@@ -0,0 +1,147 @@
{
"basePath": "/techradar",
"baseUrl": "",
"editUrl": "https://github.dev/AOEpeople/techradar/blob/main/radar/{release}/{id}.md",
"logoFile": "logo.svg",
"jsFile": "",
"toggles": {
"showSearch": false,
"showChart": true,
"showTagFilter": true,
"showQuadrantList": true,
"showEmptyRings": false
},
"sections": ["radar", "tags", "list"],
"colors": {
"foreground": "#fcf2e6",
"background": "#113521",
"highlight": "#d4a373",
"content": "#fff",
"text": "#575757",
"link": "#bc6c25",
"border": "rgba(255, 255, 255, 0.1)",
"tag": "rgba(255, 255, 255, 0.1)"
},
"quadrants": [
{
"id": "languages-and-frameworks",
"title": "Languages & Frameworks",
"description": "A selection of programming languages, alongside essential frameworks for building a variety of custom software.",
"color": "#a3b18a"
},
{
"id": "methods-and-patterns",
"title": "Methods & Patterns",
"description": "Key software development methods and design patterns, covering everything from continuous integration and testing to architecture.",
"color": "#588157"
},
{
"id": "platforms-and-operations",
"title": "Platforms & Operations",
"description": "Technologies and tools for software and infrastructure operations, including platforms and services for managing and scaling applications.",
"color": "#3f633e"
},
{
"id": "tools",
"title": "Tools",
"description": "A range of software tools, from simple productivity enhancers to comprehensive project solutions, catering to various project needs.",
"color": "#40713f"
}
],
"rings": [
{
"id": "adopt",
"title": "Adopt",
"description": "",
"color": "#588157",
"radius": 0.5,
"strokeWidth": 5
},
{
"id": "trial",
"title": "Trial",
"description": "",
"color": "#457b9d",
"radius": 0.69,
"strokeWidth": 3
},
{
"id": "assess",
"title": "Assess",
"description": "",
"color": "#bc6c25",
"radius": 0.85,
"strokeWidth": 2
},
{
"id": "hold",
"title": "Hold",
"description": "",
"color": "#d62828",
"radius": 1,
"strokeWidth": 0.75
}
],
"flags": {
"new": {
"color": "#f1235a",
"title": "New",
"titleShort": "N",
"description": "New in this version"
},
"changed": {
"color": "#40a7d1",
"title": "Changed",
"titleShort": "C",
"description": "Recently changed"
},
"default": {
"description": "Unchanged"
}
},
"chart": {
"size": 800,
"blipSize": 12
},
"social": [
{
"href": "https://twitter.com/aoepeople",
"icon": "x"
},
{
"href": "https://www.linkedin.com/company/aoe",
"icon": "linkedIn"
},
{
"href": "https://www.xing.com/company/aoe",
"icon": "xing"
},
{
"href": "https://github.com/aoepeople",
"icon": "github"
}
],
"imprint": "https://www.aoe.com/en/imprint.html",
"labels": {
"title": "Technology Radar",
"imprint": "Legal Information",
"quadrant": "Quadrant",
"quadrantOverview": "Quadrant Overview",
"zoomIn": "Zoom in",
"filterByTag": "Filter by Tag",
"footer": "The technology radar is a project by AOE GmbH. Feel free to build your own radar based on the open source project.",
"notUpdated": "This item was not updated in last three versions of the Radar. Should it have appeared in one of the more recent editions, there is a good chance it remains pertinent. However, if the item dates back further, its relevance may have diminished and our current evaluation could vary. Regrettably, our capacity to consistently revisit items from past Radar editions is limited.",
"notFound": "404 - Page not found",
"pageAbout": "How to use AOE Technology Radar?",
"pageOverview": "Technologies Overview",
"pageSearch": "Search",
"searchPlaceholder": "What are you looking for?",
"metaDescription": ""
},
"fuzzySearch": {
"threshold": 0.4,
"distance": 600,
"ignoreLocation": false,
"includeScore": true
}
}

View File

@@ -0,0 +1,3 @@
{
"basePath": "/techradar"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

16
radar-app/next.config.js Normal file
View File

@@ -0,0 +1,16 @@
const config = require("./data/config.json");
const basePath =
config.basePath && config.basePath !== "/" ? config.basePath : "";
/** @type {import("next").NextConfig} */
const nextConfig = {
basePath,
output: "export",
trailingSlash: true,
reactStrictMode: true,
experimental: {
scrollRestoration: true,
},
};
module.exports = nextConfig;

10393
radar-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
radar-app/package.json Normal file
View File

@@ -0,0 +1,48 @@
{
"name": "aoe_technology_radar",
"version": "4.7.0-rc.1",
"bin": {
"techradar": "bin/techradar.js"
},
"scripts": {
"dev": "next dev",
"build:icons": "npx @svgr/cli --typescript --no-dimensions --no-prettier --out-dir src/components/Icons -- src/icons",
"build:data": "tsx scripts/buildData.ts",
"build": "next build",
"start": "next start",
"lint": "next lint",
"fix": "prettier . --write",
"prepare": "husky",
"postinstall": "npm run build:icons"
},
"devDependencies": {
"@commitlint/cli": "^19.8.0",
"@commitlint/config-conventional": "^19.8.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
"clsx": "^2.1.1",
"eslint": "^9.23.0",
"eslint-config-next": "16.1.6",
"fuse.js": "^7.1.0",
"gray-matter": "^4.0.3",
"highlight.js": "^11.11.1",
"husky": "^9.1.7",
"lint-staged": "^15.5.0",
"marked": "^15.0.7",
"marked-highlight": "^2.2.1",
"next": "16.1.6",
"postcss": "^8.5.3",
"postcss-nested": "^7.0.2",
"postcss-preset-env": "^10.1.5",
"prettier": "^3.5.3",
"react": "^19",
"react-dom": "^19",
"tsx": "^4.19.3",
"typescript": "^5"
},
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
}
}

View File

@@ -0,0 +1,17 @@
module.exports = {
plugins: [
"postcss-nested",
[
"postcss-preset-env",
{
autoprefixer: {
flexbox: "no-2009",
},
stage: 3,
features: {
"custom-properties": false,
},
},
],
],
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 149.6 64.73"><path d="M25.87,0h-1.77v7.24C10.65,8.15,0,19.35,0,33.02s11.62,25.87,25.87,25.87,24.87-10.65,25.79-24.1h7.24v-1.77C58.89,14.81,44.09,0,25.87,0ZM25.87,55.32c-12.31,0-22.32-10.01-22.32-22.32S12.59,11.67,24.07,10.76v24.01h24.01c-.92,11.48-10.54,20.52-22.24,20.52h0l.03.03ZM51.74,31.22h-24.1V3.63c14.81.89,26.7,12.78,27.59,27.62h-3.52l.03-.03Z" fill="#fff" fill-rule="evenodd" stroke-width="0"/><path d="M78.09,10.41h-6.09v16.5h-1.5V10.41h-6.08v-1.27h13.67v1.27Z" fill="#fff" stroke-width="0"/><path d="M90.78,18.39h-8.31v7.25h9.56v1.27h-11.06V9.14h11v1.27h-9.5v6.71h8.31v1.27Z" fill="#fff" stroke-width="0"/><path d="M107.77,21.37c-.2,1.87-.87,3.3-2.01,4.3-1.13.99-2.65,1.49-4.53,1.49-1.32,0-2.48-.33-3.5-.99-1.01-.66-1.8-1.59-2.35-2.8-.55-1.21-.83-2.59-.84-4.14v-2.31c0-1.58.28-2.98.83-4.2.55-1.22,1.35-2.16,2.39-2.83,1.04-.66,2.23-1,3.58-1,1.9,0,3.41.51,4.51,1.54,1.1,1.03,1.74,2.45,1.92,4.26h-1.51c-.38-3.02-2.01-4.53-4.92-4.53-1.61,0-2.9.6-3.85,1.81s-1.43,2.87-1.43,5v2.17c0,2.05.46,3.69,1.4,4.91.93,1.22,2.19,1.83,3.78,1.83s2.75-.38,3.55-1.13c.8-.75,1.29-1.88,1.48-3.39h1.51Z" fill="#fff" stroke-width="0"/><path d="M124.59,26.91h-1.51v-8.52h-10.16v8.52h-1.5V9.14h1.5v7.98h10.16v-7.98h1.51v17.77Z" fill="#fff" stroke-width="0"/><path d="M71.73,41.62h-2.32v6.29h-4.28v-17.77h6.99c2.11,0,3.76.47,4.94,1.4,1.19.93,1.78,2.26,1.78,3.96,0,1.24-.25,2.26-.75,3.07-.5.81-1.28,1.47-2.35,1.98l3.71,7.18v.18h-4.59l-3.14-6.29ZM69.41,38.33h2.71c.81,0,1.43-.21,1.84-.64.41-.43.62-1.03.62-1.79s-.21-1.37-.62-1.81-1.03-.65-1.83-.65h-2.71v4.9Z" fill="#fff" stroke-width="0"/><path d="M91.53,44.59h-5.87l-1.03,3.32h-4.58l6.52-17.77h4.03l6.57,17.77h-4.6l-1.04-3.32ZM86.69,41.28h3.82l-1.92-6.16-1.9,6.16Z" fill="#fff" stroke-width="0"/><path d="M98.47,47.91v-17.77h5.73c1.57,0,2.98.36,4.24,1.07,1.25.71,2.23,1.72,2.94,3.01s1.06,2.75,1.07,4.36v.82c0,1.63-.34,3.09-1.03,4.38-.69,1.29-1.66,2.3-2.91,3.03-1.25.73-2.64,1.1-4.18,1.1h-5.85ZM102.75,33.44v11.17h1.49c1.23,0,2.17-.44,2.83-1.31.66-.87.99-2.17.99-3.9v-.77c0-1.72-.33-3.01-.99-3.88-.66-.87-1.62-1.31-2.88-1.31h-1.44Z" fill="#fff" stroke-width="0"/><path d="M124.47,44.59h-5.87l-1.03,3.32h-4.58l6.52-17.77h4.03l6.57,17.77h-4.6l-1.04-3.32ZM119.62,41.28h3.82l-1.92-6.16-1.9,6.16Z" fill="#fff" stroke-width="0"/><path d="M138.01,41.62h-2.32v6.29h-4.28v-17.77h7c2.11,0,3.75.47,4.94,1.4,1.19.93,1.78,2.26,1.78,3.96,0,1.24-.25,2.26-.75,3.07-.5.81-1.28,1.47-2.35,1.98l3.71,7.18v.18h-4.59l-3.14-6.29ZM135.69,38.33h2.71c.81,0,1.43-.21,1.84-.64.41-.43.62-1.03.62-1.79s-.21-1.37-.62-1.81c-.41-.44-1.03-.65-1.83-.65h-2.71v4.9Z" fill="#fff" stroke-width="0"/></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

3
radar-app/renovate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": ["config:base", ":semanticCommitTypeAll(chore)"]
}

View File

@@ -0,0 +1,271 @@
import fs from "fs";
import matter from "gray-matter";
import hljs from "highlight.js";
import { Marked } from "marked";
import { markedHighlight } from "marked-highlight";
import path from "path";
import nextConfig from "../next.config.js";
import config from "../src/lib/config";
import ErrorHandler, { ErrorType, TextColor } from "./errorHandler.js";
import Positioner from "./positioner";
import { Flag, Item } from "@/lib/types";
const {
rings,
chart: { size },
} = config;
const ringIds = rings.map((r) => r.id);
const quadrants = config.quadrants.map((q, i) => ({ ...q, position: i + 1 }));
const quadrantIds = quadrants.map((q) => q.id);
const tags = (config as { tags?: string[] }).tags || [];
const positioner = new Positioner(size, quadrants, rings);
const errorHandler = new ErrorHandler(quadrants, rings);
const marked = new Marked(
markedHighlight({
langPrefix: "hljs language-",
highlight(code, lang, info) {
const language = hljs.getLanguage(lang) ? lang : "plaintext";
return hljs.highlight(code, { language }).value;
},
}),
);
function dataPath(...paths: string[]): string {
return path.resolve("data", ...paths);
}
function convertToHtml(markdown: string): string {
// replace deprecated internal links with .html extension
markdown = markdown.replace(/(]\(\/[^)]+)\.html/g, "$1/");
if (nextConfig.basePath) {
markdown = markdown.replace(/]\(\//g, `](${nextConfig.basePath}/`);
}
let html = marked.parse(markdown.trim()) as string;
html = html.replace(
/a href="http/g,
'a target="_blank" rel="noopener noreferrer" href="http',
);
return html;
}
function readMarkdownFile(filePath: string) {
const id = path.basename(filePath, ".md");
const fileContent = fs.readFileSync(filePath, "utf8");
try {
const { data, content } = matter(fileContent);
const body = convertToHtml(content);
return { id, data, body };
} catch (error) {
console.error(`Failed parsing ${filePath}: ${error}`);
process.exit(1);
}
}
// Function to recursively read Markdown files and parse them
async function parseDirectory(dirPath: string): Promise<Item[]> {
const items: Record<string, Item> = {};
async function readDir(dirPath: string) {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
await readDir(fullPath);
} else if (entry.isFile() && entry.name.endsWith(".md")) {
const releaseDate = path.basename(path.dirname(fullPath));
const { id, data, body } = readMarkdownFile(fullPath);
if (!items[id]) {
items[id] = {
id,
release: releaseDate,
title: data.title || id,
ring: data.ring,
quadrant: data.quadrant,
body,
featured: data.featured !== false,
flag: Flag.Default,
tags: data.tags || [],
revisions: [],
position: [0, 0],
};
} else {
items[id].release = releaseDate;
items[id].body = body || items[id].body;
items[id].title = data.title || items[id].title;
items[id].ring = data.ring || items[id].ring;
items[id].quadrant = data.quadrant || items[id].quadrant;
items[id].tags = data.tags || items[id].tags;
items[id].featured =
typeof data.featured === "boolean"
? data.featured
: items[id].featured;
}
items[id].revisions!.push({
release: releaseDate,
ring: data.ring,
body,
});
}
}
}
await readDir(dirPath);
return Object.values(items).sort((a, b) => a.title.localeCompare(b.title));
}
function getUniqueReleases(items: Item[]): string[] {
const releases = new Set<string>();
for (const item of items) {
for (const revision of item.revisions || []) {
releases.add(revision.release);
}
}
return Array.from(releases).sort();
}
function getUniqueTags(items: Item[]): string[] {
const tags = new Set<string>();
for (const item of items) {
for (const tag of item.tags || []) {
tags.add(tag);
}
}
return Array.from(tags).sort();
}
function getFlag(item: Item, allReleases: string[]): Flag {
// return default flag if this is the first edition of the radar
if (allReleases.length === 1) {
return Flag.Default;
}
const latestRelease = allReleases[allReleases.length - 1];
const revisions = item.revisions || [];
const isInLatestRelease =
revisions.length > 0 &&
revisions[revisions.length - 1].release === latestRelease;
if (revisions.length == 1 && isInLatestRelease) {
return Flag.New;
} else if (revisions.length > 1 && isInLatestRelease) {
return Flag.Changed;
}
return Flag.Default;
}
function postProcessItems(items: Item[]): {
releases: string[];
tags: string[];
items: Item[];
} {
const filteredItems = items.filter((item) => {
// check if the items' quadrant and ring are valid
if (!item.quadrant || !item.ring) {
errorHandler.processBuildErrors(ErrorType.NoQuadrant, item.id);
return false;
}
if (!quadrantIds.includes(item.quadrant)) {
errorHandler.processBuildErrors(
ErrorType.InvalidQuadrant,
item.id,
item.quadrant,
);
return false;
}
if (!ringIds.includes(item.ring)) {
errorHandler.processBuildErrors(
ErrorType.InvalidRing,
item.id,
item.ring,
);
return false;
}
// check if config has a key `tags` and if it is an array
if (Array.isArray(tags) && tags.length) {
// if tags are specified, only keep items that have at least one of the tags
return item.tags?.some((tag) => tags.includes(tag));
}
return true;
});
errorHandler.checkForBuildErrors();
const releases = getUniqueReleases(filteredItems);
const uniqueTags = getUniqueTags(filteredItems);
const processedItems = filteredItems.map((item) => {
const processedItem = {
...item,
position: positioner.getNextPosition(item.quadrant, item.ring),
flag: getFlag(item, releases),
// only keep revision which ring or body is different
revisions: item.revisions
?.filter((revision, index, revisions) => {
const { ring, body } = revision;
return (
ring !== item.ring ||
(body != "" &&
body != item.body &&
body !== revisions[index - 1]?.body)
);
})
.reverse(),
};
// unset revisions if there are none
if (!processedItem.revisions?.length) {
delete processedItem.revisions;
}
// unset tags if there are none
if (!processedItem.tags?.length) {
delete processedItem.tags;
}
return processedItem;
});
return { releases, tags: uniqueTags, items: processedItems };
}
async function main() {
// Parse the data and write radar data to JSON file
const items = await parseDirectory(dataPath("radar"));
const data = postProcessItems(items);
if (data.items.length === 0) {
errorHandler.processBuildErrors(ErrorType.NoRadarItems);
}
errorHandler.checkForBuildErrors(true);
const json = JSON.stringify(data, null, 2);
fs.writeFileSync(dataPath("data.json"), json);
// write about data to JSON file
const about = readMarkdownFile(dataPath("about.md"));
fs.writeFileSync(dataPath("about.json"), JSON.stringify(about, null, 2));
console.log(
" Data written to data/data.json and data/about.json\n\n" +
errorHandler.colorizeBackground(
" Build was successfull ",
TextColor.Green,
),
);
}
main();

View File

@@ -0,0 +1,108 @@
import { Quadrant, Ring } from "@/lib/types";
export enum ErrorType {
NoQuadrant = "Item {0} has no quadrant or ring",
InvalidQuadrant = "Item {0} has invalid quadrant {1}\n\tvalid quadrants are: {2}",
InvalidRing = "Item {0} has invalid ring {1}\n\tvalid rings are: {2}",
NoRadarItems = "No valid radar items found. Please check the markdown files in the `radar` directory.",
}
export enum TextColor {
Default = 0,
Black,
Red = 31,
Green = 32,
Yellow = 33,
Blue = 34,
Mangenta = 35,
Cyan = 36,
White = 37,
}
export default class ErrorHandler {
private buildErrors: string[] = [];
private quadrants: Quadrant[];
private rings: Ring[];
private isStrict: boolean;
private supportsColor: boolean;
constructor(quadrants: Quadrant[], rings: Ring[]) {
this.isStrict = process.argv.slice(2).includes("--strict");
this.supportsColor = process.stdout.isTTY && process.env.TERM !== "dumb";
this.quadrants = quadrants;
this.rings = rings;
console.log(` Build is${this.isStrict ? "" : " not"} in strict mode\n`);
}
public processBuildErrors(errorType: ErrorType, ...args: string[]) {
const errorHint = this.getErrorHint(errorType);
const errorMsg = this.formatString(
errorType.toString(),
errorHint ? [...args, errorHint] : args,
);
this.buildErrors.push(errorMsg);
}
public checkForBuildErrors(exitOnErr: boolean = false) {
if (this.buildErrors.length > 0) {
console.warn(
this.colorizeBackground(
`There ${this.buildErrors.length > 1 ? "are" : "is"} ${this.buildErrors.length} error${this.buildErrors.length > 1 ? "s" : ""} in your data build`,
TextColor.Red,
) +
"\n\n" +
this.buildErrors
.map((error, index) => `${index + 1}. ${error}`)
.join("\n") +
"\n",
);
if (this.isStrict || exitOnErr) {
process.exit(1);
}
this.buildErrors = [];
}
}
private getErrorHint(errorType: ErrorType) {
switch (errorType) {
case ErrorType.InvalidQuadrant:
return this.quadrants.map((quadrant) => quadrant.id).join(", ");
case ErrorType.InvalidRing:
return this.rings.map((ring) => ring.id).join(", ");
default:
break;
}
}
public colorizeBackground(str: string, backgroundColor: TextColor) {
if (this.supportsColor) {
return `\x1b[${backgroundColor + 10}m${str}\x1b[${TextColor.Default}m`;
}
return str;
}
private formatString(msg: string, inserts: string[]) {
return inserts.reduce(
(acc, cur, index) =>
acc.replaceAll(
`{${index}}`,
this.colorizeString(
cur,
index === 2 ? TextColor.Green : TextColor.Red,
),
),
msg,
);
}
private colorizeString(str: string, textColor: TextColor) {
if (this.supportsColor) {
return `\x1b[${textColor}m${str}\x1b[${TextColor.Default}m`;
}
return str;
}
}

View File

@@ -0,0 +1,95 @@
import { Quadrant, Ring } from "@/lib/types";
type Position = [x: number, y: number];
type RingDimension = [innerRadius: number, outerRadius: number];
// Corresponding to positions 1, 2, 3, and 4 respectively
const startAngles = [270, 0, 180, 90];
export default class Positioner {
private readonly centerRadius: number;
private readonly minDistance: number = 20;
private readonly paddingRing: number = 15;
private readonly paddingAngle: number = 10;
private positions: Record<string, Position[]> = {};
private ringDimensions: Record<string, RingDimension> = {};
private quadrantAngles: Record<string, number> = {};
constructor(size: number, quadrants: Quadrant[], rings: Ring[]) {
this.centerRadius = size / 2;
quadrants.forEach((quadrant) => {
this.quadrantAngles[quadrant.id] = startAngles[quadrant.position - 1];
});
rings.forEach((ring, index) => {
const innerRadius =
(rings[index - 1]?.radius ?? 0) * this.centerRadius + this.paddingRing;
const outerRadius =
(ring.radius ?? 1) * this.centerRadius - this.paddingRing;
this.ringDimensions[ring.id] = [innerRadius, outerRadius];
});
}
static getDistance(a: Position, b: Position): number {
const [x1, y1] = a;
const [x2, y2] = b;
return Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
}
private isOverlapping(position: Position, positions: Position[]): boolean {
return positions.some(
(p) => Positioner.getDistance(position, p) < this.minDistance,
);
}
private getXYPosition(
quadrantId: string,
ringId: string,
radiusFraction: number,
angleFraction: number,
): Position {
const [innerRadius, outerRadius] = this.ringDimensions[ringId];
const ringWidth = outerRadius - innerRadius;
const absoluteRadius = innerRadius + radiusFraction * ringWidth;
const startAngle = this.quadrantAngles[quadrantId] + this.paddingAngle;
const endAngle = startAngle + 90 - 2 * this.paddingAngle;
const absoluteAngle = startAngle + (endAngle - startAngle) * angleFraction;
const angleInRadians = ((absoluteAngle - 90) * Math.PI) / 180;
return [
Math.round(this.centerRadius + absoluteRadius * Math.cos(angleInRadians)),
Math.round(this.centerRadius + absoluteRadius * Math.sin(angleInRadians)),
];
}
public getNextPosition(quadrantId: string, ringId: string): Position {
this.positions[quadrantId] ??= [];
let tries = 0;
let position: Position;
do {
position = this.getXYPosition(
quadrantId,
ringId,
Math.sqrt(Math.random()),
Math.random(),
);
tries++;
} while (
this.isOverlapping(position, this.positions[quadrantId]) &&
tries < 150
);
if (tries >= 150) {
console.warn(
`Could not find a non-overlapping position for ${quadrantId} in ring ${ringId}`,
);
}
this.positions[quadrantId].push(position);
return position;
}
}

View File

@@ -0,0 +1,40 @@
import { MetadataRoute } from "next";
import { getAbsoluteUrl, getItems, getQuadrants } from "@/lib/data";
export const dynamic = "force-static";
export const revalidate = 60;
export default function sitemap(): MetadataRoute.Sitemap {
const quadrants = getQuadrants().map((quadrant) => ({
url: getAbsoluteUrl(`/${quadrant.id}/`),
lastModified: new Date(),
priority: 0.8,
}));
const items = getItems().map((item) => ({
url: getAbsoluteUrl(`/${item.quadrant}/${item.id}/`),
lastModified: new Date(),
priority: 0.5,
}));
return [
{
url: getAbsoluteUrl(),
lastModified: new Date(),
priority: 1,
},
{
url: getAbsoluteUrl("/overview/"),
lastModified: new Date(),
priority: 0.9,
},
{
url: getAbsoluteUrl("/help-and-about-tech-radar/"),
lastModified: new Date(),
priority: 0.9,
},
...quadrants,
...items,
];
}

View File

@@ -0,0 +1,50 @@
.badge {
position: relative;
display: inline-block;
vertical-align: middle;
padding: 6px 15px;
text-transform: uppercase;
border: 1px solid transparent;
border-radius: 13px;
font-size: 12px;
line-height: 1;
overflow: hidden;
text-decoration: none;
}
.size-small {
padding: 4px 8px;
font-size: 9px;
}
.size-large {
padding: 7px 20px;
font-size: 14px;
border-radius: 15px;
}
.colored {
color: var(--foreground);
background-color: var(--badge);
}
.selectable {
cursor: pointer;
&:not(.selected) {
color: var(--foreground);
border: 1px solid var(--foreground);
background: transparent;
}
&:not(.colored) {
color: var(--foreground);
border: 1px solid var(--foreground);
background: transparent;
&.selected {
color: var(--background);
background: var(--foreground);
}
}
}

View File

@@ -0,0 +1,105 @@
import {
CSSProperties,
ComponentPropsWithoutRef,
ReactNode,
useMemo,
} from "react";
import styles from "./Badge.module.css";
import { getFlag, getRing } from "@/lib/data";
import { formatRelease } from "@/lib/format";
import { Flag } from "@/lib/types";
import { cn } from "@/lib/utils";
interface BadgeProps extends ComponentPropsWithoutRef<"span"> {
children?: ReactNode;
color?: string;
selectable?: boolean;
selected?: boolean;
size?: "small" | "medium" | "large";
}
export function Badge({
children,
color,
size = "medium",
selectable,
selected,
...props
}: BadgeProps) {
const style = useMemo(
() => (color ? ({ "--badge": color } as CSSProperties) : undefined),
[color],
);
const Component = props.onClick ? "button" : "span";
return (
<Component
{...props}
style={style}
className={cn(
props.className,
styles.badge,
styles[`size-${size}`],
color && styles.colored,
selectable && styles.selectable,
selected && styles.selected,
)}
>
{children}
</Component>
);
}
interface RingBadgeProps extends Omit<BadgeProps, "color" | "children"> {
ring: string;
release?: string;
}
export function RingBadge({
ring: ringName,
release,
...props
}: RingBadgeProps) {
const ring = getRing(ringName);
if (!ring) return null;
const label = release
? `${ring.title} | ${formatRelease(release)}`
: ring.title;
return (
<Badge color={ring.color} {...props}>
{label}
</Badge>
);
}
// Type guard to check if flag has the required attributes
function hasRequiredFlagAttributes(flag: any): flag is {
color: string;
title: string;
titleShort: string;
} {
return "color" in flag && "title" in flag && "titleShort" in flag;
}
interface FlagBadgeProps
extends Omit<BadgeProps, "color" | "children" | "size"> {
flag: Flag;
short?: boolean;
}
export function FlagBadge({ flag: flagName, short, ...props }: FlagBadgeProps) {
if (flagName === Flag.Default) return null;
const flag = getFlag(flagName);
if (!flag || !hasRequiredFlagAttributes(flag)) return null;
return (
<Badge color={flag.color} size="small" {...props}>
{short ? flag.titleShort : flag.title}
</Badge>
);
}

View File

@@ -0,0 +1,8 @@
.filter {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
margin-bottom: 20px;
gap: 20px;
}

View File

@@ -0,0 +1,25 @@
import styles from "./Filter.module.css";
import { QueryFilter } from "@/components/Filter/QueryFilter";
import { RingFilter } from "@/components/Filter/RingFilter";
interface FilterProps {
query?: string;
onQueryChange: (query: string) => void;
ring?: string;
onRingChange: (ring: string) => void;
}
export function Filter({
query,
onQueryChange,
ring,
onRingChange,
}: FilterProps) {
return (
<div className={styles.filter}>
<QueryFilter value={query} onChange={onQueryChange} />
<RingFilter value={ring} onChange={onRingChange} />
</div>
);
}

View File

@@ -0,0 +1,29 @@
.filter {
flex: 1 1 100%;
position: relative;
}
.input {
padding-right: 50px;
}
.button {
position: absolute;
top: 50%;
right: 16px;
width: 20px;
height: 20px;
margin: -10px 0 0;
background: transparent;
border: none;
}
.icon {
fill: var(--highlight);
}
@media (min-width: 768px) {
.filter {
flex: 1 1 auto;
}
}

View File

@@ -0,0 +1,39 @@
import { ChangeEvent, useEffect, useState } from "react";
import Search from "../Icons/Search";
import styles from "./QueryFilter.module.css";
import { getLabel } from "@/lib/data";
interface QueryFilterProps {
value?: string;
onChange: (value: string) => void;
}
export function QueryFilter({ value, onChange }: QueryFilterProps) {
const [val, setVal] = useState(value);
const _onChange = (e: ChangeEvent<HTMLInputElement>) => {
setVal(e.target.value);
onChange(e.target.value);
};
useEffect(() => {
setVal(value);
}, [value]);
return (
<div className={styles.filter}>
<input
className={styles.input}
id="search"
type="search"
placeholder={getLabel("searchPlaceholder")}
value={val}
onChange={_onChange}
/>
<button className={styles.button} type="submit">
<Search className={styles.icon} />
</button>
</div>
);
}

View File

@@ -0,0 +1,8 @@
.filter {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 20px;
}

View File

@@ -0,0 +1,43 @@
import styles from "./RingFilter.module.css";
import { Badge, RingBadge } from "@/components/Badge/Badge";
import { getRings } from "@/lib/data";
import { cn } from "@/lib/utils";
interface RingFilterProps {
value?: string;
onChange: (value: string) => void;
className?: string;
}
export function RingFilter({ value, onChange, className }: RingFilterProps) {
const rings = getRings();
return (
<ul className={cn(styles.filter, className)}>
<li>
<Badge
size="large"
selectable
selected={!value}
onClick={() => {
onChange("");
}}
>
All
</Badge>
</li>
{rings.map((ring) => (
<li key={ring.id}>
<RingBadge
ring={ring.id}
size="large"
selectable
selected={value === ring.id}
onClick={() => onChange(ring.id)}
/>
</li>
))}
</ul>
);
}

View File

@@ -0,0 +1,54 @@
.branding {
padding: 20px 0;
border-top: 1px solid var(--border);
}
.logo {
display: block;
margin: 0 auto 20px;
width: 150px;
}
.description {
font-size: 12px;
margin: 0 0 30px;
}
.imprint {
opacity: 0.7;
display: block;
font-size: 12px;
text-decoration: underline;
text-align: center;
&:hover {
opacity: 1;
}
}
@media (min-width: 768px) {
.branding {
display: flex;
justify-content: center;
align-items: center;
}
.logo {
margin: 0;
}
.description {
margin: 0 50px 0;
}
.imprint {
text-align: right;
}
}
@media (min-width: 768px) and (max-width: 1023px) {
.socialLinks {
flex-wrap: wrap;
min-width: 150px;
}
}

View File

@@ -0,0 +1,20 @@
import styles from "./Footer.module.css";
import { SocialLinks } from "@/components/SocialLinks/SocialLinks";
import { getAppName, getImprintUrl, getLabel, getLogoUrl } from "@/lib/data";
export function Footer() {
const logoUrl = getLogoUrl();
return (
<div className={styles.footer}>
<div className={styles.branding}>
<img src={logoUrl} className={styles.logo} alt={getAppName()} />
<p className={styles.description}>{getLabel("footer")}</p>
<SocialLinks className={styles.socialLinks} />
</div>
<a href={getImprintUrl()} className={styles.imprint} target="_blank">
{getLabel("imprint")}
</a>
</div>
);
}

View File

@@ -0,0 +1,119 @@
.header {
display: flex;
flex-wrap: wrap;
align-items: center;
margin: 0 0 20px;
}
.title {
margin: 0 30px 0 0;
}
.editLink {
position: absolute;
top: 10px;
right: 10px;
display: block;
width: 20px;
height: 20px;
opacity: 0;
transition: opacity 0.2s;
}
.revision {
padding: 30px 0 15px 35px;
margin-left: 20px;
border-left: 1px solid var(--border);
&:hover {
.editLink {
opacity: 1;
}
}
}
.release {
display: block;
text-align: center;
text-transform: uppercase;
font-size: 12px;
line-height: 1.2;
width: 50px;
height: 50px;
padding: 10px 0;
border-radius: 50%;
border: 1px solid var(--border);
background: var(--background);
float: left;
margin: -15px 0 0 -60px;
}
.notMaintainedIcon {
fill: currentColor;
width: 24px;
height: 24px;
margin: 8px auto;
}
.ring {
float: left;
margin: -45px 0 0 0;
}
.content {
position: relative;
background: var(--content);
color: var(--text);
border-radius: 6px;
padding: 30px 15px;
}
.content a {
color: var(--link);
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
@media (min-width: 768px) {
.revision {
padding: 30px 0 15px 50px;
margin-left: 38px;
}
.release {
font-size: 18px;
width: 75px;
height: 75px;
padding: 15px 0;
margin: -15px 0 0 -90px;
}
.ring {
margin-left: -15px;
}
.content {
padding: 30px;
}
}
/* special styles for revisions without content */
.revision.noContent {
.content {
background: none;
}
.ring {
margin-top: -20px;
}
}
.revision.hint {
.content {
font-size: 14px;
background: var(--border);
color: var(--foreground);
}
}

View File

@@ -0,0 +1,81 @@
import styles from "./ItemDetail.module.css";
import { RingBadge } from "@/components/Badge/Badge";
import { Attention, Edit } from "@/components/Icons";
import { Tag } from "@/components/Tags/Tags";
import { getEditUrl, getLabel, getReleases } from "@/lib/data";
import { Item } from "@/lib/types";
import { cn } from "@/lib/utils";
const latestReleases = getReleases().slice(-3);
function isNotMaintained(release: string) {
return !latestReleases.includes(release);
}
interface ItemProps {
item: Item;
}
export function ItemDetail({ item }: ItemProps) {
const notMaintainedText = getLabel("notUpdated");
return (
<>
<div className={styles.header}>
<h1 className={styles.title}>{item.title}</h1>
{item.tags?.map((tag) => <Tag key={tag} tag={tag} />)}
</div>
<div className={styles.revisions}>
{notMaintainedText && isNotMaintained(item.release) && (
<div className={cn(styles.revision, styles.hint)}>
<span className={styles.release}>
<Attention className={styles.notMaintainedIcon} />
</span>
<div className={styles.content}>{notMaintainedText}</div>
</div>
)}
<Revision
id={item.id}
release={item.release}
ring={item.ring}
body={item.body}
/>
{item.revisions?.map((revision, index) => (
<Revision key={index} id={item.id} {...revision} />
))}
</div>
</>
);
}
interface RevisionProps {
id: string;
release: string;
ring: string;
body?: string;
}
function Revision({ id, release, ring, body }: RevisionProps) {
const date = new Date(release);
const editLink = getEditUrl({ id, release });
const formattedDate = date.toLocaleDateString("en-US", {
month: "short",
year: "numeric",
});
return (
<div className={cn(styles.revision, !body && styles.noContent)}>
<time dateTime={release} className={styles.release}>
{formattedDate}
</time>
<div className={styles.content}>
<RingBadge className={styles.ring} ring={ring} size="large" />
{body ? <div dangerouslySetInnerHTML={{ __html: body }} /> : null}
{editLink && (
<a href={editLink} target="_blank" className={styles.editLink}>
<Edit />
</a>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,76 @@
.list {
list-style: none;
margin: 0;
padding: 0;
}
.item {
+ .item {
border-top: 1px solid var(--border);
}
}
.flag {
display: inline;
flex: 0 0 auto;
align-self: baseline;
margin-left: 8px;
}
.ring {
flex: 0 0 auto;
margin-left: 16px;
align-self: baseline;
}
.quadrant {
font-size: 14px;
opacity: 0.7;
}
.info {
flex-basis: 100%;
}
.link {
display: block;
padding: 10px;
border-radius: 6px;
&.isFadedOut {
opacity: 0.65;
}
&:hover,
&.isActive {
background: var(--foreground);
color: var(--background);
opacity: 1;
}
}
.isSmall {
font-size: 14px;
.link {
padding: 8px;
}
}
.isLarge {
.link {
display: flex;
flex-wrap: wrap;
}
.quadrant {
margin-left: auto;
}
@media (min-width: 768px) {
.info {
flex-basis: auto;
margin-left: auto;
}
}
}

View File

@@ -0,0 +1,67 @@
import Link from "next/link";
import styles from "./ItemList.module.css";
import { FlagBadge, RingBadge } from "@/components/Badge/Badge";
import { getQuadrant } from "@/lib/data";
import { Item } from "@/lib/types";
import { cn } from "@/lib/utils";
export interface ItemListProps {
items: Item[];
activeId?: string;
size?: "small" | "default" | "large";
hideRing?: boolean;
className?: string;
}
export function ItemList({
items,
activeId,
size = "default",
hideRing = false,
className,
}: ItemListProps) {
return (
<ul
className={cn(styles.list, className, {
[styles.isSmall]: size === "small",
[styles.isLarge]: size === "large",
})}
>
{items.map((item) => (
<li className={styles.item} key={item.id}>
<Link
className={cn(styles.link, {
[styles.isFadedOut]: !item.featured,
[styles.isActive]: item.id === activeId,
})}
href={`/${item.quadrant}/${item.id}`}
>
<span className={styles.title}>{item.title}</span>
<FlagBadge
className={styles.flag}
flag={item.flag}
short={size == "small"}
/>
{size === "large" && (
<div className={styles.info}>
<span className={styles.quadrant}>
{getQuadrant(item.quadrant)?.title}
</span>
{!hideRing && (
<RingBadge
className={styles.ring}
ring={item.ring}
size="small"
/>
)}
</div>
)}
</Link>
</li>
))}
</ul>
);
}

View File

@@ -0,0 +1,24 @@
.layout {
min-height: 100vh;
}
.container {
max-width: var(--max-width);
margin: 0 auto;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.layout.default {
.content {
max-width: var(--max-width);
min-height: calc(100vh - 250px);
margin: 0 auto;
padding: 20px;
}
}

View File

@@ -0,0 +1,39 @@
import { Roboto } from "next/font/google";
import { FC, ReactNode } from "react";
import styles from "./Layout.module.css";
import { Footer } from "@/components/Footer/Footer";
import { Logo } from "@/components/Logo/Logo";
import { Navigation } from "@/components/Navigation/Navigation";
import { cn } from "@/lib/utils";
const font = Roboto({ weight: ["400", "700"], subsets: ["latin"] });
export type LayoutClass = "default" | "full";
interface LayoutProps {
children: ReactNode;
layoutClass?: LayoutClass;
}
export const Layout: FC<LayoutProps> = ({
children,
layoutClass = "default",
}) => {
return (
<div
id="layout"
className={cn(styles.layout, font.className, styles[layoutClass])}
>
<header className={cn(styles.container, styles.header)}>
<Logo />
<Navigation />
</header>
<main className={cn(styles.content)}>{children}</main>
<footer className={cn(styles.container, styles.footer)}>
<Footer />
</footer>
</div>
);
};

View File

@@ -0,0 +1,54 @@
.logo {
position: relative;
display: flex;
justify-content: flex-start;
align-items: center;
min-height: 60px;
gap: 16px;
transition: padding-left 200ms ease-in-out;
&:before {
content: "";
display: block;
position: absolute;
left: 0;
width: 22px;
height: 22px;
background: url("../../icons/back.svg") no-repeat 50% 50%;
opacity: 0;
transition: opacity 200ms ease-in-out;
}
}
.src {
width: 150px;
transition: width 200ms ease-in-out;
}
.subline {
position: relative;
top: -2px;
font-size: 18px;
opacity: 0;
transition: opacity 200ms ease-in-out;
@media (max-width: 1024px) {
display: none;
}
}
.logo.small {
.subline {
opacity: 0.8;
}
.src {
width: 75px;
}
&:hover {
padding-left: 30px;
&:before {
opacity: 1;
}
}
}

View File

@@ -0,0 +1,22 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import styles from "./Logo.module.css";
import { getAppName, getLogoUrl } from "@/lib/data";
import { cn } from "@/lib/utils";
export function Logo() {
const pathname = usePathname();
const appName = getAppName();
const logoUrl = getLogoUrl();
return (
<Link href="/" className={cn(styles.logo, pathname != "/" && styles.small)}>
<img src={logoUrl} className={cn(styles.src)} alt={appName} />
<span className={styles.subline}>{appName}</span>
</Link>
);
}

View File

@@ -0,0 +1,20 @@
.list {
list-style: none;
display: flex;
gap: 16px;
font-size: 14px;
}
.icon {
display: inline-block;
vertical-align: middle;
width: 22px;
margin: 0 6px 0 0;
fill: var(--highlight);
}
@media (max-width: 900px) {
.label {
display: none;
}
}

View File

@@ -0,0 +1,37 @@
import Link from "next/link";
import styles from "./Navigation.module.css";
import IconOverview from "@/components/Icons/Overview";
import IconQuestion from "@/components/Icons/Question";
import IconSearch from "@/components/Icons/Search";
import { getLabel, getToggle } from "@/lib/data";
export function Navigation() {
return (
<nav className={styles.nav}>
<ul className={styles.list}>
<li className={styles.item}>
<Link href="/help-and-about-tech-radar">
<IconQuestion className={styles.icon} />
<span className={styles.label}>{getLabel("pageAbout")}</span>
</Link>
</li>
<li className={styles.item}>
<Link href="/overview">
<IconOverview className={styles.icon} />
<span className={styles.label}>{getLabel("pageOverview")}</span>
</Link>
</li>
{getToggle("showSearch") && (
<li className={styles.item}>
<Link href="/overview">
<IconSearch className={styles.icon} />
<span className={styles.label}>{getLabel("pageSearch")}</span>
</Link>
</li>
)}
</ul>
</nav>
);
}

View File

@@ -0,0 +1,20 @@
.link {
text-transform: uppercase;
font-size: 12px;
white-space: nowrap;
}
.icon {
fill: var(--highlight);
display: inline-block;
vertical-align: middle;
margin: -2px 6px 0 0;
width: 16px;
height: 16px;
}
.link:hover {
.label {
text-decoration: underline;
}
}

View File

@@ -0,0 +1,26 @@
import Link from "next/link";
import styles from "./QuadrantLink.module.css";
import Pie from "@/components/Icons/Pie";
import { getLabel } from "@/lib/data";
import { Quadrant } from "@/lib/types";
import { cn } from "@/lib/utils";
interface QuadrantLinkProps {
quadrant: Quadrant;
label?: string;
className?: string;
}
export function QuadrantLink({
quadrant,
label = getLabel("zoomIn"),
className,
}: QuadrantLinkProps) {
return (
<Link className={cn(styles.link, className)} href={`/${quadrant.id}`}>
<Pie className={styles.icon} />
<span className={styles.label}>{label}</span>
</Link>
);
}

View File

@@ -0,0 +1,38 @@
.quadrants {
--cols: 1;
--gap: 60px;
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: var(--gap);
}
.quadrant {
margin-bottom: 20px;
flex: 1 0
calc(100% / var(--cols) - var(--gap) / var(--cols) * (var(--cols) - 1));
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
margin-bottom: 20px;
border-bottom: 1px solid var(--border);
}
.title {
margin: 0;
}
.link {
}
@media (min-width: 1220px) {
.quadrants {
--cols: 2;
}
}

View File

@@ -0,0 +1,35 @@
import Link from "next/link";
import styles from "./QuadrantList.module.css";
import { QuadrantLink } from "@/components/QuadrantLink/QuadrantLink";
import { RingList } from "@/components/RingList/RingList";
import { getQuadrant, groupItemsByQuadrant } from "@/lib/data";
import { Item } from "@/lib/types";
interface RingListProps {
items: Item[];
}
export function QuadrantList({ items }: RingListProps) {
const quadrants = groupItemsByQuadrant(items);
return (
<ul className={styles.quadrants}>
{Object.entries(quadrants).map(([quadrantId, items]) => {
const quadrant = getQuadrant(quadrantId);
if (!quadrant) return null;
return (
<li key={quadrantId} className={styles.quadrant}>
<div className={styles.header}>
<h3 className={styles.title}>
<Link href={`/${quadrant.id}`}>{quadrant.title}</Link>
</h3>
<QuadrantLink quadrant={quadrant} />
</div>
<RingList items={items} size="small" />
</li>
);
})}
</ul>
);
}

View File

@@ -0,0 +1,58 @@
import React from "react";
import { getChartConfig } from "@/lib/data";
import { Flag } from "@/lib/types";
const { blipSize } = getChartConfig();
const halfBlipSize = blipSize / 2;
interface BlipProps {
color: string;
x: number;
y: number;
}
export function Blip({ flag, color, x, y }: BlipProps & { flag: Flag }) {
switch (flag) {
case Flag.New:
return <BlipNew x={x} y={y} color={color} />;
case Flag.Changed:
return <BlipChanged x={x} y={y} color={color} />;
default:
return <BlipDefault x={x} y={y} color={color} />;
}
}
function BlipNew({ x, y, color }: BlipProps) {
x = Math.round(x - halfBlipSize);
y = Math.round(y - halfBlipSize);
return (
<path
stroke="none"
fill={color}
d="M5.7679491924311 2.1387840678323a2 2 0 0 1 3.4641016151378 0l5.0358983848622 8.7224318643355a2 2 0 0 1 -1.7320508075689 3l-10.071796769724 0a2 2 0 0 1 -1.7320508075689 -3"
transform={`translate(${x},${y})`}
/>
);
}
function BlipChanged({ x, y, color }: BlipProps) {
x = Math.round(x - halfBlipSize);
y = Math.round(y - halfBlipSize);
return (
<rect
transform={`rotate(-45 ${x} ${y})`}
x={x}
y={y}
width={blipSize}
height={blipSize}
rx="3"
stroke="none"
fill={color}
/>
);
}
function BlipDefault({ x, y, color }: BlipProps) {
return <circle cx={x} cy={y} r={halfBlipSize} stroke="none" fill={color} />;
}

View File

@@ -0,0 +1,9 @@
.ringLabels {
text-transform: uppercase;
}
@media (max-width: 767px) {
.ringLabels {
display: none;
}
}

View File

@@ -0,0 +1,163 @@
import Link from "next/link";
import React, { FC, Fragment, memo } from "react";
import styles from "./Chart.module.css";
import { Blip } from "@/components/Radar/Blip";
import { Item, Quadrant, Ring } from "@/lib/types";
export interface ChartProps {
size?: number;
quadrants: Quadrant[];
rings: Ring[];
items: Item[];
className?: string;
}
const _Chart: FC<ChartProps> = ({
size = 800,
quadrants = [],
rings = [],
items = [],
className,
}) => {
const viewBoxSize = size;
const center = size / 2;
const startAngles = [270, 0, 180, 90]; // Corresponding to positions 1, 2, 3, and 4 respectively
// Helper function to convert polar coordinates to cartesian
const polarToCartesian = (
radius: number,
angleInDegrees: number,
): { x: number; y: number } => {
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
return {
x: Math.round(center + radius * Math.cos(angleInRadians)),
y: Math.round(center + radius * Math.sin(angleInRadians)),
};
};
// Function to generate the path for a ring segment
const describeArc = (radiusPercentage: number, position: number): string => {
// Define the start and end angles based on the quadrant position
const startAngle = startAngles[position - 1];
const endAngle = startAngle + 90;
const radius = radiusPercentage * center; // Convert percentage to actual radius
const start = polarToCartesian(radius, endAngle);
const end = polarToCartesian(radius, startAngle);
// prettier-ignore
return [
"M", start.x, start.y,
"A", radius, radius, 0, 0, 0, end.x, end.y,
].join(" ");
};
const renderGlow = (position: number, color: string) => {
const gradientId = `glow-${position}`;
const cx = position === 1 || position === 3 ? 1 : 0;
const cy = position === 1 || position === 2 ? 1 : 0;
const x = position === 1 || position === 3 ? 0 : center;
const y = position === 1 || position === 2 ? 0 : center;
return (
<>
<defs>
<radialGradient id={gradientId} x={0} y={0} r={1} cx={cx} cy={cy}>
<stop offset="0%" stopColor={color} stopOpacity={0.5}></stop>
<stop offset="100%" stopColor={color} stopOpacity={0}></stop>
</radialGradient>
</defs>
<rect
width={center}
height={center}
x={x}
y={y}
fill={`url(#${gradientId})`}
/>
</>
);
};
// Function to place items inside their rings and quadrants
const renderItem = (item: Item) => {
const ring = rings.find((r) => r.id === item.ring);
const quadrant = quadrants.find((q) => q.id === item.quadrant);
if (!ring || !quadrant) return null; // If no ring or quadrant, don't render item
const [x, y] = item.position;
return (
<Link
key={item.id}
href={`/${item.quadrant}/${item.id}`}
data-tooltip={item.title}
data-tooltip-color={quadrant.color}
tabIndex={-1}
>
<Blip flag={item.flag} color={quadrant.color} x={x} y={y} />
</Link>
);
};
const renderRingLabels = () => {
return rings.map((ring, index) => {
const outerRadius = ring.radius || 1;
const innerRadius = rings[index - 1]?.radius || 0;
const position = ((outerRadius + innerRadius) / 2) * center;
return (
<Fragment key={ring.id}>
<text
x={center + position}
y={center}
textAnchor="middle"
dominantBaseline="middle"
fontSize="12"
>
{ring.title}
</text>
<text
x={center - position}
y={center}
textAnchor="middle"
dominantBaseline="middle"
fontSize="12"
>
{ring.title}
</text>
</Fragment>
);
});
};
return (
<svg
className={className}
width={viewBoxSize}
height={viewBoxSize}
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
>
{quadrants.map((quadrant) => (
<g key={quadrant.id} data-quadrant={quadrant.id}>
{renderGlow(quadrant.position, quadrant.color)}
{rings.map((ring) => (
<path
key={`${ring.id}-${quadrant.id}`}
data-key={`${ring.id}-${quadrant.id}`}
d={describeArc(ring.radius || 0.5, quadrant.position)}
fill="none"
stroke={quadrant.color}
strokeWidth={ring.strokeWidth || 2}
/>
))}
</g>
))}
<g className={styles.items}>{items.map((item) => renderItem(item))}</g>
<g className={styles.ringLabels}>{renderRingLabels()}</g>
</svg>
);
};
export const Chart = memo(_Chart);

View File

@@ -0,0 +1,37 @@
.label {
width: 240px;
min-height: 210px;
position: absolute;
top: 0;
left: 0;
}
.header {
display: flex;
justify-content: space-between;
padding: 10px 0;
margin: 0 0 15px;
border-bottom: 2px solid var(--quadrant-color);
text-transform: uppercase;
font-size: 12px;
}
.title {
margin: 0 0 10px;
}
.description {
font-size: 14px;
}
.position-2,
.position-4 {
left: auto;
right: 0;
}
.position-3,
.position-4 {
top: auto;
bottom: 0;
}

View File

@@ -0,0 +1,36 @@
import Link from "next/link";
import { CSSProperties, useMemo } from "react";
import styles from "./Label.module.css";
import { QuadrantLink } from "@/components/QuadrantLink/QuadrantLink";
import { getLabel } from "@/lib/data";
import { Quadrant } from "@/lib/types";
import { cn } from "@/lib/utils";
interface LabelProps {
quadrant: Quadrant;
}
export function Label({ quadrant }: LabelProps) {
const style = useMemo(
() => ({ "--quadrant-color": quadrant.color }) as CSSProperties,
[quadrant.color],
);
return (
<div
className={cn(styles.label, styles[`position-${quadrant.position}`])}
style={style}
>
<div className={styles.header}>
<span>
{getLabel("quadrant")} {quadrant.position}
</span>
<QuadrantLink quadrant={quadrant} />
</div>
<h3 className={styles.title}>{quadrant.title}</h3>
<p className={styles.description}>{quadrant.description}</p>
</div>
);
}

View File

@@ -0,0 +1,35 @@
.legend {
list-style: none;
padding: 0;
margin: 0;
font-size: 14px;
display: none;
}
.icon {
display: inline-block;
vertical-align: middle;
width: 16px;
height: 16px;
margin: -2px 8px 0 0;
}
@media (min-width: 768px) {
.legend {
display: block;
position: absolute;
left: 50%;
bottom: 50px;
transform: translateX(-50%);
}
}
@media (min-width: 1200px) {
.legend {
bottom: auto;
left: auto;
right: 0;
top: 50%;
transform: translateY(-50%);
}
}

View File

@@ -0,0 +1,36 @@
import { ComponentPropsWithoutRef } from "react";
import styles from "./Legend.module.css";
import BlipChanged from "@/components/Icons/BlipChanged";
import BlipDefault from "@/components/Icons/BlipDefault";
import BlipNew from "@/components/Icons/BlipNew";
import { getFlags } from "@/lib/data";
import { Flag } from "@/lib/types";
function Icon({
flag,
...props
}: { flag: Flag } & ComponentPropsWithoutRef<"svg">) {
switch (flag) {
case Flag.New:
return <BlipNew {...props} />;
case Flag.Changed:
return <BlipChanged {...props} />;
case Flag.Default:
return <BlipDefault {...props} />;
}
}
export function Legend() {
return (
<ul className={styles.legend}>
{Object.entries(getFlags()).map(([key, flag]) => (
<li key={key}>
<Icon flag={key as Flag} className={styles.icon} />
<span className={styles.label}>{flag.description}</span>
</li>
))}
</ul>
);
}

View File

@@ -0,0 +1,74 @@
.radar {
padding: 0 15px 30px;
position: relative;
transition: padding 200ms ease-in-out;
}
.chart {
display: block;
max-width: 100%;
height: auto;
margin: 0 auto;
fill: currentColor;
}
.tooltip {
background-color: var(--tooltip, var(--background));
color: var(--foreground);
font-size: 14px;
padding: 4px 8px;
height: fit-content;
width: fit-content;
border-radius: 6px;
position: absolute;
text-align: center;
opacity: 0;
transform: translate(-50%, -90%) scale(0.7);
transform-origin: 50% 100%;
transition:
all 100ms ease-in-out,
left 0ms,
top 0ms;
box-shadow:
0 4px 14px 0 rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(0, 0, 0, 0.05);
pointer-events: none;
z-index: 1;
&:before {
content: "";
display: block;
position: absolute;
z-index: 2;
bottom: -1px;
left: 50%;
margin-left: -8px;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid var(--tooltip, var(--background));
transition: bottom 100ms ease-in-out;
}
&.isShown {
opacity: 1;
transform: translate(-50%, -130%) scale(1);
&:before {
bottom: -7px;
}
}
}
@media (max-width: 767px) {
.labels {
display: none;
}
}
@media (min-width: 768px) and (max-width: 1200px) {
.radar {
padding: 150px 15px;
}
}

View File

@@ -0,0 +1,111 @@
import React, {
CSSProperties,
FC,
MouseEvent,
useMemo,
useRef,
useState,
} from "react";
import styles from "./Radar.module.css";
import { Chart } from "@/components/Radar/Chart";
import { Label } from "@/components/Radar/Label";
import { Legend } from "@/components/Radar/Legend";
import { Item, Quadrant, Ring } from "@/lib/types";
import { cn } from "@/lib/utils";
export interface RadarProps {
size?: number;
quadrants: Quadrant[];
rings: Ring[];
items: Item[];
}
export const Radar: FC<RadarProps> = ({
size = 800,
quadrants = [],
rings = [],
items = [],
}) => {
const radarRef = useRef<HTMLDivElement>(null);
const [tooltip, setTooltip] = useState({
show: false,
text: "",
color: "",
x: 0,
y: 0,
});
const tooltipStyle = useMemo(
() =>
({
left: tooltip.x,
top: tooltip.y,
...(tooltip.color ? { "--tooltip": tooltip.color } : undefined),
}) as CSSProperties,
[tooltip],
);
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
const link =
e.target instanceof Element && e.target.closest("a[data-tooltip]");
if (link) {
const text = link.getAttribute("data-tooltip") || "";
const color = link.getAttribute("data-tooltip-color") || "";
const linkRect = link.getBoundingClientRect();
const radarRect = radarRef.current!.getBoundingClientRect();
// Adjusting tooltip position to be relative to the radar container
const x = linkRect.left - radarRect.left + linkRect.width / 2;
const y = linkRect.top - radarRect.top;
setTooltip({
text,
color,
show: !!text,
x,
y,
});
} else {
if (tooltip.show) {
setTooltip({ ...tooltip, show: false });
}
}
};
const handleMouseLeave = () => {
setTooltip({ ...tooltip, show: false });
};
return (
<div
ref={radarRef}
className={styles.radar}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<Chart
className={styles.chart}
size={size}
quadrants={quadrants}
rings={rings}
items={items}
/>
<div className={styles.labels}>
{quadrants.map((quadrant) => (
<Label key={quadrant.id} quadrant={quadrant} />
))}
</div>
<Legend />
<span
className={cn(styles.tooltip, tooltip.show && styles.isShown)}
style={tooltipStyle}
>
{tooltip.text}
</span>
</div>
);
};
export default Radar;

View File

@@ -0,0 +1,38 @@
.rings {
--cols: 1;
--gap: 30px;
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: var(--gap);
}
.ring {
margin-bottom: 20px;
flex: 1 0
calc(100% / var(--cols) - var(--gap) / var(--cols) * (var(--cols) - 1));
}
.badge {
margin-bottom: 20px;
}
@media (min-width: 480px) {
.rings {
--cols: 2;
}
}
@media (min-width: 768px) {
.rings.isSmall {
--cols: 4;
}
}
@media (min-width: 1024px) {
.rings {
--cols: 4;
}
}

Some files were not shown because too many files have changed in this diff Show More