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>
7
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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/" && \
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Ce dépôt contient le contenu du Technology Radar AJR, publié sous : https://www.coeurbox.syoul.fr
|
||||
|
||||
Le projet est basé sur le framework [aoe_technology_radar](https://github.com/AOEpeople/aoe_technology_radar).
|
||||
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/`.
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
"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
@@ -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
@@ -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
|
||||
22
radar-app/.github/workflows/semanticore.yml
vendored
Normal 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
@@ -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
@@ -0,0 +1 @@
|
||||
npx --no-install commitlint --edit "$1"
|
||||
1
radar-app/.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
npx lint-staged
|
||||
8
radar-app/.npmignore
Normal file
@@ -0,0 +1,8 @@
|
||||
*.tgz
|
||||
.idea
|
||||
.github
|
||||
/.next/
|
||||
/out/
|
||||
/techradar/
|
||||
/data/about.json
|
||||
/data/data.json
|
||||
3
radar-app/.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
out
|
||||
CHANGELOG.md
|
||||
11
radar-app/.prettierrc
Normal 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
@@ -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
@@ -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"));
|
||||
}
|
||||
26
radar-app/commitlint.config.js
Normal 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",
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
147
radar-app/data/config.default.json
Normal 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
|
||||
}
|
||||
}
|
||||
3
radar-app/data/config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"basePath": "/techradar"
|
||||
}
|
||||
BIN
radar-app/docs/assets/screenshot-techradar.png
Normal file
|
After Width: | Height: | Size: 283 KiB |
16
radar-app/next.config.js
Normal 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;
|
||||
10011
radar-app/package-lock.json
generated
Normal file
47
radar-app/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"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": "15.2.4",
|
||||
"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": "15.2.4",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
17
radar-app/postcss.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
"postcss-nested",
|
||||
[
|
||||
"postcss-preset-env",
|
||||
{
|
||||
autoprefixer: {
|
||||
flexbox: "no-2009",
|
||||
},
|
||||
stage: 3,
|
||||
features: {
|
||||
"custom-properties": false,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
BIN
radar-app/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
radar-app/public/logo.svg
Normal 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
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["config:base", ":semanticCommitTypeAll(chore)"]
|
||||
}
|
||||
271
radar-app/scripts/buildData.ts
Normal 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();
|
||||
108
radar-app/scripts/errorHandler.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
95
radar-app/scripts/positioner.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
40
radar-app/src/app/sitemap.ts
Normal 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,
|
||||
];
|
||||
}
|
||||
50
radar-app/src/components/Badge/Badge.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
105
radar-app/src/components/Badge/Badge.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
8
radar-app/src/components/Filter/Filter.module.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.filter {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
25
radar-app/src/components/Filter/Filter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
29
radar-app/src/components/Filter/QueryFilter.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
39
radar-app/src/components/Filter/QueryFilter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
8
radar-app/src/components/Filter/RingFilter.module.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.filter {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
43
radar-app/src/components/Filter/RingFilter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
54
radar-app/src/components/Footer/Footer.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
20
radar-app/src/components/Footer/Footer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
119
radar-app/src/components/ItemDetail/ItemDetail.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
81
radar-app/src/components/ItemDetail/ItemDetail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
76
radar-app/src/components/ItemList/ItemList.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
radar-app/src/components/ItemList/ItemList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
24
radar-app/src/components/Layout/Layout.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
39
radar-app/src/components/Layout/Layout.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
54
radar-app/src/components/Logo/Logo.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
radar-app/src/components/Logo/Logo.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
20
radar-app/src/components/Navigation/Navigation.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
37
radar-app/src/components/Navigation/Navigation.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
26
radar-app/src/components/QuadrantLink/QuadrantLink.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
35
radar-app/src/components/QuadrantList/QuadrantList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
58
radar-app/src/components/Radar/Blip.tsx
Normal 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} />;
|
||||
}
|
||||
9
radar-app/src/components/Radar/Chart.module.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.ringLabels {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.ringLabels {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
163
radar-app/src/components/Radar/Chart.tsx
Normal 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);
|
||||
37
radar-app/src/components/Radar/Label.module.css
Normal 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;
|
||||
}
|
||||
36
radar-app/src/components/Radar/Label.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
35
radar-app/src/components/Radar/Legend.module.css
Normal 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%);
|
||||
}
|
||||
}
|
||||
36
radar-app/src/components/Radar/Legend.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
74
radar-app/src/components/Radar/Radar.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
111
radar-app/src/components/Radar/Radar.tsx
Normal 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;
|
||||
38
radar-app/src/components/RingList/RingList.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
27
radar-app/src/components/RingList/RingList.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import styles from "./RingList.module.css";
|
||||
|
||||
import { RingBadge } from "@/components/Badge/Badge";
|
||||
import { ItemList, ItemListProps } from "@/components/ItemList/ItemList";
|
||||
import { groupItemsByRing } from "@/lib/data";
|
||||
import { Item } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface RingListProps {
|
||||
items: Item[];
|
||||
size?: ItemListProps["size"];
|
||||
}
|
||||
export function RingList({ items, size }: RingListProps) {
|
||||
const rings = groupItemsByRing(items);
|
||||
return (
|
||||
<ul className={cn(styles.rings, { [styles.isSmall]: size == "small" })}>
|
||||
{Object.entries(rings).map(([ring, items]) => {
|
||||
return (
|
||||
<li key={ring} className={styles.ring}>
|
||||
<RingBadge className={styles.badge} ring={ring} />
|
||||
<ItemList className={styles.list} items={items} size={size} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
29
radar-app/src/components/SocialLinks/SocialLinks.module.css
Normal file
@@ -0,0 +1,29 @@
|
||||
.links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: var(--background);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: block;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--foreground);
|
||||
padding: 6px;
|
||||
border-radius: 50%;
|
||||
|
||||
&:hover {
|
||||
background: var(--background);
|
||||
.icon {
|
||||
fill: var(--foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
radar-app/src/components/SocialLinks/SocialLinks.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import styles from "./SocialLinks.module.css";
|
||||
|
||||
import {
|
||||
SocialFacebook,
|
||||
SocialGithub,
|
||||
SocialGitlab,
|
||||
SocialInstagram,
|
||||
SocialLinkedin,
|
||||
SocialX,
|
||||
SocialXing,
|
||||
SocialYoutube,
|
||||
} from "@/components/Icons";
|
||||
import { getSocialLinks } from "@/lib/data";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SocialLinksProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function getIcon(name: string) {
|
||||
switch (name.toLowerCase()) {
|
||||
case "facebook":
|
||||
return SocialFacebook;
|
||||
case "github":
|
||||
return SocialGithub;
|
||||
case "gitlab":
|
||||
return SocialGitlab;
|
||||
case "instagram":
|
||||
return SocialInstagram;
|
||||
case "linkedin":
|
||||
return SocialLinkedin;
|
||||
case "x":
|
||||
return SocialX;
|
||||
case "xing":
|
||||
return SocialXing;
|
||||
case "youtube":
|
||||
return SocialYoutube;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function SocialLinks({ className }: SocialLinksProps) {
|
||||
const links = getSocialLinks();
|
||||
return (
|
||||
<ul className={cn(styles.links, className)}>
|
||||
{links.map((link, i) => {
|
||||
const Icon = getIcon(link.icon);
|
||||
return (
|
||||
Icon && (
|
||||
<li key={i}>
|
||||
<a
|
||||
href={link.href}
|
||||
className={styles.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon className={styles.icon} />
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
43
radar-app/src/components/Tags/Tags.module.css
Normal file
@@ -0,0 +1,43 @@
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: -2px 6px 0 -5px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 6px 15px 5px;
|
||||
margin: 6px;
|
||||
text-transform: uppercase;
|
||||
border: 1px solid var(--tag);
|
||||
border-radius: 13px;
|
||||
background: var(--tag);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: all 150ms ease-in-out;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.active {
|
||||
background: var(--foreground);
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
&.active {
|
||||
.icon {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
text-align: center;
|
||||
margin: 0 auto 60px;
|
||||
max-width: 600px;
|
||||
}
|
||||
47
radar-app/src/components/Tags/Tags.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import Link, { LinkProps } from "next/link";
|
||||
import { ComponentPropsWithoutRef } from "react";
|
||||
|
||||
import styles from "./Tags.module.css";
|
||||
|
||||
import IconRemove from "@/components/Icons/Close";
|
||||
import IconTag from "@/components/Icons/Tag";
|
||||
import { getLabel } from "@/lib/data";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type TagProps = {
|
||||
tag: string;
|
||||
isActive?: boolean;
|
||||
} & Omit<LinkProps, "href"> &
|
||||
ComponentPropsWithoutRef<"a">;
|
||||
|
||||
export function Tag({ tag, isActive, className, ...props }: TagProps) {
|
||||
const Icon = isActive ? IconRemove : IconTag;
|
||||
return (
|
||||
<Link
|
||||
{...props}
|
||||
className={cn(styles.tag, className, isActive && styles.active)}
|
||||
href={isActive ? "/" : `/?tag=${encodeURIComponent(tag)}`}
|
||||
>
|
||||
<Icon className={cn(styles.icon)} />
|
||||
<span className={styles.label}>{tag}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
interface TagsProps {
|
||||
tags: string[];
|
||||
activeTag?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Tags({ tags, activeTag, className }: TagsProps) {
|
||||
const label = getLabel("filterByTag");
|
||||
return (
|
||||
<div className={cn(styles.tags, className)}>
|
||||
{!!label && <h3>{label}</h3>}
|
||||
{tags.map((tag) => (
|
||||
<Tag key={tag} tag={tag} isActive={activeTag == tag} scroll={false} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
radar-app/src/icons/attention.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="M12.8455 5.21137C12.4531 4.59003 11.5469 4.59003 11.1545 5.21137L2.78316 18.466C2.36261 19.1319 2.84109 20 3.62865 20H20.3713C21.1589 20 21.6374 19.1319 21.2168 18.466L12.8455 5.21137ZM9.46353 4.14338C10.6408 2.27935 13.3592 2.27936 14.5365 4.14339L22.9078 17.398C24.1695 19.3956 22.734 22 20.3713 22H3.62865C1.26598 22 -0.169465 19.3956 1.09218 17.398L9.46353 4.14338ZM13 17C13 17.5523 12.5523 18 12 18C11.4477 18 11 17.5523 11 17C11 16.4477 11.4477 16 12 16C12.5523 16 13 16.4477 13 17ZM10.6941 10.1644L11.4178 14.5068C11.4652 14.7914 11.7115 15 12 15C12.2885 15 12.5348 14.7914 12.5822 14.5068L13.3059 10.1644C13.4075 9.55487 12.9375 9 12.3195 9H11.6805C11.0625 9 10.5925 9.55487 10.6941 10.1644Z" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 874 B |
1
radar-app/src/icons/back.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421" viewBox="0 0 30 21"><path d="M4.012 11.427h24.704a1.134 1.134 0 0 0 0-2.267H3.729l7.224-7.225A1.133 1.133 0 1 0 9.35.332L.332 9.35a1.134 1.134 0 0 0-.106 1.481c.099.182.245.335.423.439l8.701 8.701a1.133 1.133 0 1 0 1.603-1.603l-6.941-6.941Z" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 420 B |
1
radar-app/src/icons/blip_changed.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="12" height="12" x="2" y="2" rx="3" transform="rotate(-45 8 8)"/></svg>
|
||||
|
After Width: | Height: | Size: 143 B |
1
radar-app/src/icons/blip_default.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6"/></svg>
|
||||
|
After Width: | Height: | Size: 95 B |
1
radar-app/src/icons/blip_new.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"><path d="m.247 10.212 5.02-8.697a2 2 0 0 1 3.465 0l5.021 8.697a2 2 0 0 1-1.732 3H1.98a2 2 0 0 1-1.732-3z"/></svg>
|
||||
|
After Width: | Height: | Size: 153 B |
1
radar-app/src/icons/close.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><path d="m18.7,1.4l-7.68,7.73L3.35,1.41c-.54-.54-1.4-.54-1.89,0-.54.54-.54,1.41,0,1.89l7.67,7.73-7.73,7.67c-.54.54-.54,1.41,0,1.89.54.54,1.41.54,1.89,0l7.73-7.68,7.68,7.68c.54.54,1.41.54,1.89,0,.49-.54.54-1.41,0-1.89l-7.68-7.68,7.68-7.67c.54-.54.54-1.4,0-1.89-.48-.54-1.35-.54-1.89-.06Z" fill-rule="evenodd" stroke-width="0"/></svg>
|
||||
|
After Width: | Height: | Size: 437 B |
6
radar-app/src/icons/edit.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" ?>
|
||||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m18.988 2.012 3 3L19.701 7.3l-3-3zM8 16h3l7.287-7.287-3-3L8 13z"/>
|
||||
<path
|
||||
d="M19 19H8.158c-.026 0-.053.01-.079.01-.033 0-.066-.009-.1-.01H5V5h6.847l2-2H5c-1.103 0-2 .896-2 2v14c0 1.104.897 2 2 2h14a2 2 0 0 0 2-2v-8.668l-2 2V19z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 360 B |
1
radar-app/src/icons/filter.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><path d="m20.31,2.31c-.3-.56-.76-.83-1.4-.83H3.01c-.83,0-1.4.5-1.5,1.3,0,.07.03.2.07.23,2.1,2.4,4.19,4.72,6.32,7.09.1.1.13.2.13.37v6.55c0,.97.47,1.7,1.33,2.13.6.3,1.23.5,1.83.76.5.2.97.4,1.46.56.5.17.97-.07,1.13-.5.07-.17.1-.4.1-.6v-8.88c0-.13.03-.23.13-.33.76-.86,1.56-1.73,2.33-2.59,1.33-1.5,2.7-2.99,4.02-4.49.03-.03.1-.13.1-.2-.07-.2-.07-.4-.17-.56h0Zm-2,.86c-1.66,1.83-3.29,3.66-4.96,5.49-.03.03-.43.43-.93.9v9.21c-.07-.03-.13-.03-.17-.07-.7-.27-1.4-.56-2.1-.83-.43-.17-.63-.5-.63-.97v-7.35c-.47-.47-.86-.83-.9-.86-1.73-1.86-3.39-3.76-5.09-5.62-.03-.03-.07-.07-.1-.13h15.07c-.1.1-.13.17-.2.23h0Z" stroke-width="0"/></svg>
|
||||
|
After Width: | Height: | Size: 731 B |
1
radar-app/src/icons/overview.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><path d="m5.67,1c-1.45,0-2.67,1.22-2.67,2.67v15.11c0,1.23,1,2.22,2.22,2.22h13.11c.35,0,.67-.32.67-.67V5.67c0-.39-.29-.67-.67-.67-.64,0-1.33-.69-1.33-1.33s.69-1.33,1.33-1.33c.35,0,.67-.31.67-.67s-.31-.67-.67-.67c0,0-12.67,0-12.67,0Zm0,1.33h10.37c-.23.39-.37.85-.37,1.33s.14.94.37,1.33H5.67c-.64,0-1.33-.69-1.33-1.33s.69-1.33,1.33-1.33Zm-1.33,3.63c.39.23.85.37,1.33.37h12v13.33H5.22c-.48,0-.88-.37-.89-.86,0-.01,0-.02,0-.03V5.97h0Zm2.44,2.81c-.37.01-.65.32-.64.69.01.35.29.63.64.64h8.44c.37-.01.65-.32.64-.69-.01-.35-.29-.63-.64-.64H6.78Zm0,3.56c-.37,0-.67.3-.67.67,0,.37.3.67.67.67h8.44c.37,0,.67-.3.67-.67,0-.37-.3-.67-.67-.67H6.78Zm0,3.56c-.37.01-.65.32-.64.69.01.35.29.63.64.64h8.44c.37-.01.65-.32.64-.69-.01-.35-.29-.63-.64-.64H6.78Z" fill-rule="evenodd" stroke-width="0"/></svg>
|
||||
|
After Width: | Height: | Size: 887 B |
1
radar-app/src/icons/pie.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><path d="m9.71.38h-.64v2.61C4.22,3.32.38,7.36.38,12.29s4.19,9.33,9.33,9.33,8.97-3.84,9.3-8.69h2.61v-.64C21.62,5.72,16.28.38,9.71.38Zm0,19.95c-4.44,0-8.05-3.61-8.05-8.05s3.26-7.69,7.4-8.02v8.66h8.66c-.33,4.14-3.8,7.4-8.02,7.4h0Zm9.33-8.69h-8.69V1.69c5.34.32,9.63,4.61,9.95,9.96,0,0-1.27,0-1.27,0Z" fill-rule="evenodd" stroke-width="0"/></svg>
|
||||
|
After Width: | Height: | Size: 446 B |
1
radar-app/src/icons/question.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><path d="m11,22C4.9,22,0,17,0,11S4.9,0,11,0h0c6,0,11,4.9,11,11s-5,11-11,11Zm0-20.5h0C5.8,1.5,1.5,5.7,1.5,11s4.2,9.5,9.5,9.5,9.5-4.2,9.5-9.5S16.2,1.5,11,1.5" stroke-width="0"/><path d="m11,16.65c-.7,0-1.2-.5-1.2-1.2h0c0-.7.6-1.2,1.2-1.2s1.2.6,1.2,1.2-.5,1.2-1.2,1.2h0Zm2.8-6.9c-.1.2-.3.4-.4.5l-.4.4c-.2.1-.3.3-.4.4l-.3.3c-.2.3-.3.6-.3,1v.7h-1.8v-1c0-.3,0-.6.2-.9.2-.3.4-.6.7-.8l1.1-1.1c.2-.3.4-.6.4-1s-.1-.7-.4-.9c-.3-.2-.6-.4-1-.4s-.7.1-1,.4-.4.6-.5,1h-2c.1-.8.5-1.6,1.1-2.2.7-.5,1.5-.8,2.3-.8s1.6.2,2.3.8c.6.5.9,1.2.9,2.1,0,.5-.2,1-.5,1.5h0Z" fill-rule="evenodd" stroke-width="0"/></svg>
|
||||
|
After Width: | Height: | Size: 693 B |
1
radar-app/src/icons/search.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><path d="m20.6,18.31l-4.01-4.01c2.44-3.37,2.15-8.11-.89-11.14-1.61-1.62-3.81-2.53-6.09-2.52-2.28,0-4.48.91-6.09,2.52-3.36,3.36-3.36,8.82,0,12.18,0,0,0,0,0,0,1.61,1.62,3.81,2.53,6.09,2.52,1.78,0,3.55-.55,5.05-1.64l4.01,4.01c.26.26.61.4.96.4.75,0,1.36-.61,1.36-1.36,0-.36-.14-.71-.4-.96h0Zm-15.73-4.32c-2.62-2.62-2.62-6.87,0-9.49,1.26-1.26,2.96-1.97,4.74-1.97,1.79,0,3.48.7,4.74,1.97,2.62,2.62,2.62,6.87,0,9.49-1.26,1.26-2.96,1.97-4.74,1.97-1.78,0-3.49-.7-4.74-1.97Z" fill-rule="evenodd" stroke-width="0"/></svg>
|
||||
|
After Width: | Height: | Size: 615 B |
4
radar-app/src/icons/social-facebook.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 320 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M279.14 288l14.22-92.66h-88.91v-60.13c0-25.35 12.42-50.06 52.24-50.06h40.42V6.26S260.43 0 225.36 0c-73.22 0-121.08 44.38-121.08 124.72v70.62H22.89V288h81.39v224h100.17V288z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 261 B |
4
radar-app/src/icons/social-github.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 496 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
7
radar-app/src/icons/social-gitlab.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512">
|
||||
<g id="b">
|
||||
<path
|
||||
d="M487.75,200.06l-.7-1.78L419.55,22.09c-1.37-3.45-3.81-6.38-6.95-8.37-6.44-4-14.69-3.55-20.66,1.11-2.88,2.34-4.98,5.52-5.99,9.09l-45.58,139.46h-184.58L110.2,23.93c-.99-3.59-3.09-6.78-5.99-9.12-5.97-4.66-14.22-5.11-20.66-1.11-3.13,1.99-5.56,4.92-6.95,8.37L8.96,198.18l-.67,1.78c-19.96,52.17-3.01,111.25,41.58,144.89l.23.18.62.44,102.84,77.01,50.88,38.51,30.99,23.4c7.45,5.66,17.76,5.66,25.21,0l30.99-23.4,50.88-38.51,103.46-77.48.26-.21c44.49-33.64,61.41-92.62,41.53-144.73Z" stroke-width="0"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 646 B |
4
radar-app/src/icons/social-instagram.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1010 B |
4
radar-app/src/icons/social-linkedin.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M100.28 448H7.4V148.9h92.88zM53.79 108.1C24.09 108.1 0 83.5 0 53.8a53.79 53.79 0 0 1 107.58 0c0 29.7-24.1 54.3-53.79 54.3zM447.9 448h-92.68V302.4c0-34.7-.7-79.2-48.29-79.2-48.29 0-55.69 37.7-55.69 76.7V448h-92.78V148.9h89.08v40.8h1.3c12.4-23.5 42.69-48.3 87.88-48.3 94 0 111.28 61.9 111.28 142.3V448z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 389 B |
4
radar-app/src/icons/social-x.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 238 B |
4
radar-app/src/icons/social-xing.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 384 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M162.7 210c-1.8 3.3-25.2 44.4-70.1 123.5-4.9 8.3-10.8 12.5-17.7 12.5H9.8c-7.7 0-12.1-7.5-8.5-14.4l69-121.3c.2 0 .2-.1 0-.3l-43.9-75.6c-4.3-7.8.3-14.1 8.5-14.1H100c7.3 0 13.3 4.1 18 12.2l44.7 77.5zM382.6 46.1l-144 253v.3L330.2 466c3.9 7.1.2 14.1-8.5 14.1h-65.2c-7.6 0-13.6-4-18-12.2l-92.4-168.5c3.3-5.8 51.5-90.8 144.8-255.2 4.6-8.1 10.4-12.2 17.5-12.2h65.7c8 0 12.3 6.7 8.5 14.1z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 468 B |