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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Deploy GitHub Pages

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Package site
run: |
mkdir -p _site
cp index.html 404.html styles.css app.js favicon.svg .nojekyll _site/
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: _site
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
Empty file added .nojekyll
Empty file.
12 changes: 12 additions & 0 deletions 404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta http-equiv="refresh" content="0; url=./" />
<title>Page introuvable</title>
<link rel="icon" href="./favicon.svg" type="image/svg+xml" />
</head>
<body>
<p><a href="./">Retour au guide</a></p>
</body>
</html>
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Est-ce vibecodé ?

Petit guide statique pour reconnaître le code écrit à l’instinct (souvent avec une IA) : **huit signes**, des **exemples concrets**, un **détecteur noté sur 100**.

Aucun build. Ouvre `index.html` en local, ou publie le dépôt sur GitHub Pages.

## Lancer en local

Ouvre le fichier, ou sers le dossier :

```bash
python3 -m http.server 8080
```

Puis va sur `http://localhost:8080`.

## Publier sur GitHub Pages

### Option A — depuis la branche (la plus simple)

1. Fusionne ce travail dans `main`.
2. Dépôt → **Settings** → **Pages**.
3. **Source** : *Deploy from a branch*.
4. Branch `main`, dossier `/ (root)`, **Save**.
5. Le site apparaît sur `https://<user>.github.io/guide_vibecode/` après quelques minutes.

### Option B — GitHub Actions

1. Settings → **Pages** → **Source** : *GitHub Actions*.
2. Le workflow `.github/workflows/pages.yml` publie la racine à chaque push sur `main`.

Le fichier `.nojekyll` empêche GitHub de traiter le site avec Jekyll.

## Contenu

| Fichier | Rôle |
| --- | --- |
| `index.html` | Guide + détecteur |
| `styles.css` | Mise en page |
| `app.js` | Score et verdict |
| `404.html` | Retour à l’accueil |

Licence MIT.
188 changes: 188 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
const SIGNS = [
{
id: "comments",
title: "Le tutoriel dans le code",
hint: "Chaque ligne a son commentaire d’évidence.",
},
{
id: "names",
title: "Les noms jetables",
hint: "data, item, result, obj, temp, value…",
},
{
id: "catchall",
title: "L’erreur de décoration",
hint: "try/catch qui loggue, avale, ou dit « handle error ».",
},
{
id: "overkill",
title: "L’usine à gaz",
hint: "Cinq couches pour un if, un hook, et trois helpers vides.",
},
{
id: "docs",
title: "La doc qui répète le code",
hint: "JSDoc d’un add(a, b) plus long que la fonction.",
},
{
id: "assistant",
title: "Les traces de l’assistant",
hint: "« Sure! », TODO: implement, api.example.com, emojis de célébration.",
},
{
id: "patchwork",
title: "Le patchwork de styles",
hint: "camelCase + snake_case, tabs + espaces, 3 façons de faire un fetch.",
},
{
id: "leftovers",
title: "Les restes de chantier",
hint: "console.log, imports morts, fichiers Untitled, README de rêve.",
},
];

const LEVELS = [
{ value: 0, label: "Absent" },
{ value: 1, label: "Léger" },
{ value: 2, label: "Présent" },
{ value: 3, label: "Criant" },
];

const VERDICTS = [
{
min: 0,
stamp: "Artisan",
title: "Code avec une âme",
text: "Peu de tics d’assistant. Soit quelqu’un a écrit ça, soit quelqu’un a eu le courage de relire.",
},
{
min: 18,
stamp: "Vibe light",
title: "Un Copilot a soufflé",
text: "Quelques formules toutes faites, mais le fond tient. Une passe de nettoyage et ça redevient du code.",
},
{
min: 38,
stamp: "Vibecodé",
title: "On entend encore le « Sure! »",
text: "Les signes s’accumulent : commentaires-tutoriel, noms génériques, politesse inutile. Diagnostic assez net.",
},
{
min: 62,
stamp: "Vibe max",
title: "Plus de vibe que de métier",
text: "Ça compile peut-être. Ça raconte surtout une session de génération. À reprendre plutôt qu’à patcher.",
},
{
min: 82,
stamp: "Spécimen",
title: "Laboratoire, rayon IA",
text: "Félicitations : c’est un cas d’école. Gardez-le pour le guide. Ne le mettez pas en prod.",
},
];

const state = Object.fromEntries(SIGNS.map((sign) => [sign.id, 0]));

function maxScore() {
return SIGNS.length * 3;
}

function rawScore() {
return Object.values(state).reduce((sum, value) => sum + value, 0);
}

function percent() {
return Math.round((rawScore() / maxScore()) * 100);
}

function verdictFor(score) {
return [...VERDICTS].reverse().find((item) => score >= item.min) ?? VERDICTS[0];
}

function renderChecks() {
const root = document.querySelector("#checks");
root.innerHTML = SIGNS.map(
(sign) => `
<div class="check">
<h4>${sign.title}</h4>
<p>${sign.hint}</p>
<div class="levels" role="radiogroup" aria-label="${sign.title}">
${LEVELS.map(
(level) => `
<label>
<input type="radio" name="${sign.id}" value="${level.value}" ${
level.value === 0 ? "checked" : ""
} />
${level.label}
</label>
`
).join("")}
</div>
</div>
`
).join("");

root.addEventListener("change", (event) => {
const input = event.target;
if (!(input instanceof HTMLInputElement)) return;
state[input.name] = Number(input.value);
renderMeter();
});
}

function renderMeter() {
const score = percent();
const verdict = verdictFor(score);
const meter = document.querySelector(".meter-ring");
meter.style.setProperty("--score", String(score));
document.querySelector("#score-value").textContent = String(score);
document.querySelector("#stamp").textContent = verdict.stamp;
document.querySelector("#verdict-title").textContent = verdict.title;
document.querySelector("#verdict-text").textContent = verdict.text;
}

function copyResult() {
const score = percent();
const verdict = verdictFor(score);
const active = SIGNS.filter((sign) => state[sign.id] >= 2)
.map((sign) => sign.title)
.join(", ");
const text = [
`Est-ce vibecodé ? ${score}/100 — ${verdict.stamp}`,
verdict.title,
active ? `Signes nets : ${active}` : "Aucun signe criant coché.",
window.location.href.split("#")[0],
].join("\n");

const button = document.querySelector("#copy");
const done = () => {
button.textContent = "Copié";
window.setTimeout(() => {
button.textContent = "Copier le verdict";
}, 1600);
};

if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(text).then(done).catch(() => {
window.prompt("Copier le verdict :", text);
});
return;
}
window.prompt("Copier le verdict :", text);
}

function resetScore() {
SIGNS.forEach((sign) => {
state[sign.id] = 0;
const input = document.querySelector(`input[name="${sign.id}"][value="0"]`);
if (input) input.checked = true;
});
renderMeter();
}

document.addEventListener("DOMContentLoaded", () => {
renderChecks();
renderMeter();
document.querySelector("#copy").addEventListener("click", copyResult);
document.querySelector("#reset").addEventListener("click", resetScore);
});
5 changes: 5 additions & 0 deletions favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading