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
67 changes: 67 additions & 0 deletions .github/workflows/neon-branching.yml
Original file line number Diff line number Diff line change
@@ -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="<URL_CI_DESSUS>"
```
3. Lancer ton app :
```bash
npm run dev
```

⚠️ Cette branche sera **automatiquement supprimée** à la fermeture de la PR.

GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 changes: 48 additions & 0 deletions .github/workflows/neon-cleanup.yml
Original file line number Diff line number Diff line change
@@ -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 }}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
35 changes: 30 additions & 5 deletions src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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',
Expand Down Expand Up @@ -71,14 +75,15 @@ function StoryCard({ story, onEdit, onDelete, isDragging }) {
}

function KanbanColumn({ column, stories, onEdit, onDelete, activeId }) {
const { setNodeRef } = useDroppable({ id: column.id });
return (
<div className="flex flex-col">
<div className="glass-card p-3 mb-3">
<h2 className={`text-sm font-bold ${column.color}`}>{column.label}</h2>
<span className="text-xs text-white/40">{stories.length}</span>
</div>
<SortableContext items={stories.map(s => s.id)} strategy={verticalListSortingStrategy}>
<div className="space-y-3 min-h-[60px]">
<div ref={setNodeRef} className="space-y-3 min-h-[60px]">
{stories.map(story => (
<StoryCard
key={story.id}
Expand All @@ -99,6 +104,7 @@ export default function Dashboard() {
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [formData, setFormData] = useState(EMPTY_FORM);
const [formErrors, setFormErrors] = useState({});
const [editId, setEditId] = useState(null);
const [activeId, setActiveId] = useState(null);

Expand Down Expand Up @@ -133,10 +139,21 @@ export default function Dashboard() {
setShowModal(false);
setEditId(null);
setFormData(EMPTY_FORM);
setFormErrors({});
};

const handleSubmit = async (e) => {
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}`,
Expand Down Expand Up @@ -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();
}
};

Expand Down Expand Up @@ -244,7 +263,10 @@ export default function Dashboard() {
<KanbanColumn
key={col.id}
column={col}
stories={stories.filter(s => 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}
Expand Down Expand Up @@ -275,14 +297,17 @@ export default function Dashboard() {
<div>
<label className="block text-sm font-medium mb-2">En tant que</label>
<input type="text" value={formData.asA} onChange={(e) => setFormData({...formData, asA: e.target.value})} className="glass-input w-full" placeholder="utilisateur, admin..." required />
{formErrors.asA && <p className="text-red-400 text-xs mt-1">{formErrors.asA}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-2">Je veux</label>
<textarea value={formData.iWant} onChange={(e) => setFormData({...formData, iWant: e.target.value})} className="glass-input w-full min-h-[80px]" rows="3" required />
{formErrors.iWant && <p className="text-red-400 text-xs mt-1">{formErrors.iWant}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-2">Afin de</label>
<textarea value={formData.soThat} onChange={(e) => setFormData({...formData, soThat: e.target.value})} className="glass-input w-full min-h-[80px]" rows="3" required />
{formErrors.soThat && <p className="text-red-400 text-xs mt-1">{formErrors.soThat}</p>}
</div>

<div className="grid grid-cols-2 gap-4">
Expand Down
23 changes: 17 additions & 6 deletions src/pages/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,37 @@ import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import useAuthStore from '../store/authStore';
import api from '../services/api';
import { loginSchema } from '../schemas/auth.schema';
import Header from '../components/Header';
import Footer from '../components/Footer';

export default function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [errors, setErrors] = useState({});
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const setAuth = useAuthStore(state => state.setAuth);

const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
setErrors({});

const result = loginSchema.safeParse({ email, password });
if (!result.success) {
const fieldErrors = {};
result.error.issues.forEach(err => { fieldErrors[err.path[0]] = err.message; });
setErrors(fieldErrors);
return;
}

setLoading(true);
try {
const data = await api.login(email, password);
setAuth(data.user, data.token);
navigate('/dashboard');
} catch (err) {
setError('Email ou mot de passe incorrect');
setErrors({ global: 'Email ou mot de passe incorrect' });
} finally {
setLoading(false);
}
Expand Down Expand Up @@ -52,6 +61,7 @@ export default function Login() {
className="glass-input w-full"
required
/>
{errors.email && <p className="text-red-400 text-xs mt-1">{errors.email}</p>}
</div>

<div>
Expand All @@ -63,11 +73,12 @@ export default function Login() {
className="glass-input w-full"
required
/>
{errors.password && <p className="text-red-400 text-xs mt-1">{errors.password}</p>}
</div>

{error && (
{errors.global && (
<div className="bg-red-500/20 border border-red-500/50 rounded-lg p-3 text-sm">
{error}
{errors.global}
</div>
)}

Expand Down
16 changes: 5 additions & 11 deletions src/pages/Profile.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import useAuthStore from '../store/authStore';
import api from '../services/api';
import { changePasswordSchema } from '../schemas/profile.schema';
import Header from '../components/Header';
import Footer from '../components/Footer';

Expand All @@ -26,13 +27,9 @@ export default function Profile() {
setError('');
setSuccess('');

if (newPassword !== confirmPassword) {
setError('Les mots de passe ne correspondent pas');
return;
}

if (newPassword.length < 6) {
setError('Le mot de passe doit contenir au moins 6 caractères');
const result = changePasswordSchema.safeParse({ oldPassword, newPassword, confirmPassword });
if (!result.success) {
setError(result.error.issues[0].message);
return;
}

Expand All @@ -43,10 +40,7 @@ export default function Profile() {
setOldPassword('');
setNewPassword('');
setConfirmPassword('');
setTimeout(() => {
setShowPasswordForm(false);
setSuccess('');
}, 2000);
setTimeout(() => { setShowPasswordForm(false); setSuccess(''); }, 2000);
} catch (err) {
setError('Erreur lors du changement de mot de passe');
} finally {
Expand Down
Loading
Loading