diff --git a/.github/workflows/neon-branching.yml b/.github/workflows/neon-branching.yml new file mode 100644 index 0000000..3f26530 --- /dev/null +++ b/.github/workflows/neon-branching.yml @@ -0,0 +1,67 @@ +name: Neon Branch on PR + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + create-db-branch: + runs-on: ubuntu-latest + + steps: + - name: Create Neon Branch + id: create-branch + run: | + echo "🚀 Création branche DB pour PR #${{ github.event.number }}" + + RESPONSE=$(curl -s -X POST \ + "https://console.neon.tech/api/v2/projects/${{ secrets.NEON_PROJECT_ID }}/branches" \ + -H "Authorization: Bearer ${{ secrets.NEON_API_KEY }}" \ + -H "Content-Type: application/json" \ + -d '{ + "branch": { + "name": "pr-${{ github.event.number }}", + "parent_id": "main" + } + }') + + echo "$RESPONSE" | jq '.' + + DB_URL=$(echo $RESPONSE | jq -r '.connection_uris[0].connection_uri') + + if [ "$DB_URL" = "null" ] || [ -z "$DB_URL" ]; then + echo "❌ Erreur création branche" + exit 1 + fi + + echo "NEW_DB_URL=$DB_URL" >> $GITHUB_ENV + echo "✅ Branche créée: pr-${{ github.event.number }}" + + - name: Comment PR with DB URL + uses: thollander/actions-comment-pull-request@v2 + with: + message: | + ## 🗄️ Base de données éphémère créée ! + + **Branche Neon** : `pr-${{ github.event.number }}` + + **Connection String** : + ``` + ${{ env.NEW_DB_URL }} + ``` + + ### 🧪 Pour tester localement : + + 1. Copier l'URL ci-dessus + 2. Dans ton `.env` local : + ```bash + DATABASE_URL="" + ``` + 3. Lancer ton app : + ```bash + npm run dev + ``` + + ⚠️ Cette branche sera **automatiquement supprimée** à la fermeture de la PR. + + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/neon-cleanup.yml b/.github/workflows/neon-cleanup.yml new file mode 100644 index 0000000..9837fe3 --- /dev/null +++ b/.github/workflows/neon-cleanup.yml @@ -0,0 +1,48 @@ +name: Neon Cleanup on PR Close + +on: + pull_request: + types: [closed] + +jobs: + delete-db-branch: + runs-on: ubuntu-latest + + steps: + - name: Delete Neon Branch + run: | + BRANCH_NAME="pr-${{ github.event.number }}" + + echo "🗑️ Suppression branche DB: $BRANCH_NAME" + + # Récupérer liste des branches + BRANCHES=$(curl -s \ + "https://console.neon.tech/api/v2/projects/${{ secrets.NEON_PROJECT_ID }}/branches" \ + -H "Authorization: Bearer ${{ secrets.NEON_API_KEY }}") + + # Trouver l'ID de la branche + BRANCH_ID=$(echo $BRANCHES | jq -r ".branches[] | select(.name==\"$BRANCH_NAME\") | .id") + + if [ -z "$BRANCH_ID" ] || [ "$BRANCH_ID" = "null" ]; then + echo "⚠️ Branche $BRANCH_NAME introuvable (déjà supprimée ?)" + exit 0 + fi + + # Supprimer la branche + DELETE_RESPONSE=$(curl -s -X DELETE \ + "https://console.neon.tech/api/v2/projects/${{ secrets.NEON_PROJECT_ID }}/branches/$BRANCH_ID" \ + -H "Authorization: Bearer ${{ secrets.NEON_API_KEY }}") + + echo "✅ Branche $BRANCH_NAME supprimée" + + - name: Comment PR + uses: thollander/actions-comment-pull-request@v2 + with: + message: | + ## 🗑️ Branche DB nettoyée + + La branche Neon `pr-${{ github.event.number }}` a été supprimée. + + Quota disponible restauré ✅ + + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/package-lock.json b/package-lock.json index 2bba6ff..e8b0863 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "react-dom": "^19.2.0", "react-router-dom": "^7.13.0", "sonner": "^2.0.7", + "zod": "^4.3.6", "zustand": "^5.0.11" }, "devDependencies": { @@ -3905,7 +3906,6 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 3e3de10..566807e 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "react-dom": "^19.2.0", "react-router-dom": "^7.13.0", "sonner": "^2.0.7", + "zod": "^4.3.6", "zustand": "^5.0.11" }, "devDependencies": { diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index a597731..13fe4b9 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -1,13 +1,15 @@ import { useState, useEffect } from 'react'; import { toast } from 'sonner'; import { useNavigate } from 'react-router-dom'; +import { userStorySchema } from '../schemas/userstory.schema'; import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors, - closestCorners + closestCorners, + useDroppable } from '@dnd-kit/core'; import { SortableContext, @@ -29,6 +31,8 @@ const COLUMNS = [ { id: 'DONE', label: 'Done', color: 'text-green-400' }, ]; +const PRIORITY_ORDER = { High: 0, Medium: 1, Low: 2 }; + const PRIORITY_COLORS = { High: 'text-red-400 bg-red-500/10', Medium: 'text-yellow-400 bg-yellow-500/10', @@ -71,6 +75,7 @@ function StoryCard({ story, onEdit, onDelete, isDragging }) { } function KanbanColumn({ column, stories, onEdit, onDelete, activeId }) { + const { setNodeRef } = useDroppable({ id: column.id }); return (
@@ -78,7 +83,7 @@ function KanbanColumn({ column, stories, onEdit, onDelete, activeId }) { {stories.length}
s.id)} strategy={verticalListSortingStrategy}> -
+
{stories.map(story => ( { e.preventDefault(); + setFormErrors({}); + + const result = userStorySchema.safeParse(formData); + if (!result.success) { + const errs = {}; + result.error.issues.forEach(err => { if (err.path[0]) errs[err.path[0]] = err.message; }); + setFormErrors(errs); + return; + } + const payload = { title: `En tant que ${formData.asA}, je veux ${formData.iWant}`, description: `Afin de ${formData.soThat}`, @@ -194,19 +211,21 @@ export default function Dashboard() { // Trouver la colonne cible (over peut être une story ou une colonne) const targetStory = stories.find(s => s.id === over.id); - const targetStatus = targetStory ? targetStory.status : over.id; + const isColumn = COLUMNS.find(c => c.id === over.id); + const targetStatus = isColumn ? over.id : (targetStory ? targetStory.status : over.id); if (!COLUMNS.find(c => c.id === targetStatus)) return; if (draggedStory.status === targetStatus) return; // Update optimiste + const previousStories = stories; setStories(prev => prev.map(s => s.id === active.id ? { ...s, status: targetStatus } : s)); try { await api.updateStoryStatus(token, active.id, targetStatus, 0); } catch (err) { + setStories(previousStories); toast.error('Erreur lors du déplacement'); - loadStories(); } }; @@ -244,7 +263,10 @@ export default function Dashboard() { s.status === col.id).sort((a, b) => a.position - b.position)} + stories={stories.filter(s => s.status === col.id).sort((a, b) => { + const pd = PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]; + return pd !== 0 ? pd : b.id - a.id; + })} onEdit={handleEdit} onDelete={handleDelete} activeId={activeId} @@ -275,14 +297,17 @@ export default function Dashboard() {
setFormData({...formData, asA: e.target.value})} className="glass-input w-full" placeholder="utilisateur, admin..." required /> + {formErrors.asA &&

{formErrors.asA}

}