Skip to content

Commit 3f6f0c4

Browse files
authored
Merge pull request #7 from kydoCode/feature/zod-frontend
Feature/zod frontend
2 parents 3bbba78 + aff250e commit 3f6f0c4

11 files changed

Lines changed: 219 additions & 29 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: Neon Branch on PR
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
7+
jobs:
8+
create-db-branch:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Create Neon Branch
13+
id: create-branch
14+
run: |
15+
echo "🚀 Création branche DB pour PR #${{ github.event.number }}"
16+
17+
RESPONSE=$(curl -s -X POST \
18+
"https://console.neon.tech/api/v2/projects/${{ secrets.NEON_PROJECT_ID }}/branches" \
19+
-H "Authorization: Bearer ${{ secrets.NEON_API_KEY }}" \
20+
-H "Content-Type: application/json" \
21+
-d '{
22+
"branch": {
23+
"name": "pr-${{ github.event.number }}",
24+
"parent_id": "main"
25+
}
26+
}')
27+
28+
echo "$RESPONSE" | jq '.'
29+
30+
DB_URL=$(echo $RESPONSE | jq -r '.connection_uris[0].connection_uri')
31+
32+
if [ "$DB_URL" = "null" ] || [ -z "$DB_URL" ]; then
33+
echo "❌ Erreur création branche"
34+
exit 1
35+
fi
36+
37+
echo "NEW_DB_URL=$DB_URL" >> $GITHUB_ENV
38+
echo "✅ Branche créée: pr-${{ github.event.number }}"
39+
40+
- name: Comment PR with DB URL
41+
uses: thollander/actions-comment-pull-request@v2
42+
with:
43+
message: |
44+
## 🗄️ Base de données éphémère créée !
45+
46+
**Branche Neon** : `pr-${{ github.event.number }}`
47+
48+
**Connection String** :
49+
```
50+
${{ env.NEW_DB_URL }}
51+
```
52+
53+
### 🧪 Pour tester localement :
54+
55+
1. Copier l'URL ci-dessus
56+
2. Dans ton `.env` local :
57+
```bash
58+
DATABASE_URL="<URL_CI_DESSUS>"
59+
```
60+
3. Lancer ton app :
61+
```bash
62+
npm run dev
63+
```
64+
65+
⚠️ Cette branche sera **automatiquement supprimée** à la fermeture de la PR.
66+
67+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/neon-cleanup.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Neon Cleanup on PR Close
2+
3+
on:
4+
pull_request:
5+
types: [closed]
6+
7+
jobs:
8+
delete-db-branch:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Delete Neon Branch
13+
run: |
14+
BRANCH_NAME="pr-${{ github.event.number }}"
15+
16+
echo "🗑️ Suppression branche DB: $BRANCH_NAME"
17+
18+
# Récupérer liste des branches
19+
BRANCHES=$(curl -s \
20+
"https://console.neon.tech/api/v2/projects/${{ secrets.NEON_PROJECT_ID }}/branches" \
21+
-H "Authorization: Bearer ${{ secrets.NEON_API_KEY }}")
22+
23+
# Trouver l'ID de la branche
24+
BRANCH_ID=$(echo $BRANCHES | jq -r ".branches[] | select(.name==\"$BRANCH_NAME\") | .id")
25+
26+
if [ -z "$BRANCH_ID" ] || [ "$BRANCH_ID" = "null" ]; then
27+
echo "⚠️ Branche $BRANCH_NAME introuvable (déjà supprimée ?)"
28+
exit 0
29+
fi
30+
31+
# Supprimer la branche
32+
DELETE_RESPONSE=$(curl -s -X DELETE \
33+
"https://console.neon.tech/api/v2/projects/${{ secrets.NEON_PROJECT_ID }}/branches/$BRANCH_ID" \
34+
-H "Authorization: Bearer ${{ secrets.NEON_API_KEY }}")
35+
36+
echo "✅ Branche $BRANCH_NAME supprimée"
37+
38+
- name: Comment PR
39+
uses: thollander/actions-comment-pull-request@v2
40+
with:
41+
message: |
42+
## 🗑️ Branche DB nettoyée
43+
44+
La branche Neon `pr-${{ github.event.number }}` a été supprimée.
45+
46+
Quota disponible restauré ✅
47+
48+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"react-dom": "^19.2.0",
1818
"react-router-dom": "^7.13.0",
1919
"sonner": "^2.0.7",
20+
"zod": "^4.3.6",
2021
"zustand": "^5.0.11"
2122
},
2223
"devDependencies": {

src/pages/Dashboard.jsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { useState, useEffect } from 'react';
22
import { toast } from 'sonner';
33
import { useNavigate } from 'react-router-dom';
4+
import { userStorySchema } from '../schemas/userstory.schema';
45
import {
56
DndContext,
67
DragOverlay,
78
PointerSensor,
89
useSensor,
910
useSensors,
10-
closestCorners
11+
closestCorners,
12+
useDroppable
1113
} from '@dnd-kit/core';
1214
import {
1315
SortableContext,
@@ -29,6 +31,8 @@ const COLUMNS = [
2931
{ id: 'DONE', label: 'Done', color: 'text-green-400' },
3032
];
3133

34+
const PRIORITY_ORDER = { High: 0, Medium: 1, Low: 2 };
35+
3236
const PRIORITY_COLORS = {
3337
High: 'text-red-400 bg-red-500/10',
3438
Medium: 'text-yellow-400 bg-yellow-500/10',
@@ -71,14 +75,15 @@ function StoryCard({ story, onEdit, onDelete, isDragging }) {
7175
}
7276

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

@@ -133,10 +139,21 @@ export default function Dashboard() {
133139
setShowModal(false);
134140
setEditId(null);
135141
setFormData(EMPTY_FORM);
142+
setFormErrors({});
136143
};
137144

138145
const handleSubmit = async (e) => {
139146
e.preventDefault();
147+
setFormErrors({});
148+
149+
const result = userStorySchema.safeParse(formData);
150+
if (!result.success) {
151+
const errs = {};
152+
result.error.issues.forEach(err => { if (err.path[0]) errs[err.path[0]] = err.message; });
153+
setFormErrors(errs);
154+
return;
155+
}
156+
140157
const payload = {
141158
title: `En tant que ${formData.asA}, je veux ${formData.iWant}`,
142159
description: `Afin de ${formData.soThat}`,
@@ -194,19 +211,21 @@ export default function Dashboard() {
194211

195212
// Trouver la colonne cible (over peut être une story ou une colonne)
196213
const targetStory = stories.find(s => s.id === over.id);
197-
const targetStatus = targetStory ? targetStory.status : over.id;
214+
const isColumn = COLUMNS.find(c => c.id === over.id);
215+
const targetStatus = isColumn ? over.id : (targetStory ? targetStory.status : over.id);
198216

199217
if (!COLUMNS.find(c => c.id === targetStatus)) return;
200218
if (draggedStory.status === targetStatus) return;
201219

202220
// Update optimiste
221+
const previousStories = stories;
203222
setStories(prev => prev.map(s => s.id === active.id ? { ...s, status: targetStatus } : s));
204223

205224
try {
206225
await api.updateStoryStatus(token, active.id, targetStatus, 0);
207226
} catch (err) {
227+
setStories(previousStories);
208228
toast.error('Erreur lors du déplacement');
209-
loadStories();
210229
}
211230
};
212231

@@ -244,7 +263,10 @@ export default function Dashboard() {
244263
<KanbanColumn
245264
key={col.id}
246265
column={col}
247-
stories={stories.filter(s => s.status === col.id).sort((a, b) => a.position - b.position)}
266+
stories={stories.filter(s => s.status === col.id).sort((a, b) => {
267+
const pd = PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
268+
return pd !== 0 ? pd : b.id - a.id;
269+
})}
248270
onEdit={handleEdit}
249271
onDelete={handleDelete}
250272
activeId={activeId}
@@ -275,14 +297,17 @@ export default function Dashboard() {
275297
<div>
276298
<label className="block text-sm font-medium mb-2">En tant que</label>
277299
<input type="text" value={formData.asA} onChange={(e) => setFormData({...formData, asA: e.target.value})} className="glass-input w-full" placeholder="utilisateur, admin..." required />
300+
{formErrors.asA && <p className="text-red-400 text-xs mt-1">{formErrors.asA}</p>}
278301
</div>
279302
<div>
280303
<label className="block text-sm font-medium mb-2">Je veux</label>
281304
<textarea value={formData.iWant} onChange={(e) => setFormData({...formData, iWant: e.target.value})} className="glass-input w-full min-h-[80px]" rows="3" required />
305+
{formErrors.iWant && <p className="text-red-400 text-xs mt-1">{formErrors.iWant}</p>}
282306
</div>
283307
<div>
284308
<label className="block text-sm font-medium mb-2">Afin de</label>
285309
<textarea value={formData.soThat} onChange={(e) => setFormData({...formData, soThat: e.target.value})} className="glass-input w-full min-h-[80px]" rows="3" required />
310+
{formErrors.soThat && <p className="text-red-400 text-xs mt-1">{formErrors.soThat}</p>}
286311
</div>
287312

288313
<div className="grid grid-cols-2 gap-4">

src/pages/Login.jsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,37 @@ import { useState } from 'react';
22
import { useNavigate, Link } from 'react-router-dom';
33
import useAuthStore from '../store/authStore';
44
import api from '../services/api';
5+
import { loginSchema } from '../schemas/auth.schema';
56
import Header from '../components/Header';
67
import Footer from '../components/Footer';
78

89
export default function Login() {
910
const [email, setEmail] = useState('');
1011
const [password, setPassword] = useState('');
11-
const [error, setError] = useState('');
12+
const [errors, setErrors] = useState({});
1213
const [loading, setLoading] = useState(false);
1314
const navigate = useNavigate();
1415
const setAuth = useAuthStore(state => state.setAuth);
1516

1617
const handleSubmit = async (e) => {
1718
e.preventDefault();
18-
setError('');
19-
setLoading(true);
19+
setErrors({});
20+
21+
const result = loginSchema.safeParse({ email, password });
22+
if (!result.success) {
23+
const fieldErrors = {};
24+
result.error.issues.forEach(err => { fieldErrors[err.path[0]] = err.message; });
25+
setErrors(fieldErrors);
26+
return;
27+
}
2028

29+
setLoading(true);
2130
try {
2231
const data = await api.login(email, password);
2332
setAuth(data.user, data.token);
2433
navigate('/dashboard');
2534
} catch (err) {
26-
setError('Email ou mot de passe incorrect');
35+
setErrors({ global: 'Email ou mot de passe incorrect' });
2736
} finally {
2837
setLoading(false);
2938
}
@@ -52,6 +61,7 @@ export default function Login() {
5261
className="glass-input w-full"
5362
required
5463
/>
64+
{errors.email && <p className="text-red-400 text-xs mt-1">{errors.email}</p>}
5565
</div>
5666

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

68-
{error && (
79+
{errors.global && (
6980
<div className="bg-red-500/20 border border-red-500/50 rounded-lg p-3 text-sm">
70-
{error}
81+
{errors.global}
7182
</div>
7283
)}
7384

src/pages/Profile.jsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState } from 'react';
22
import { useNavigate } from 'react-router-dom';
33
import useAuthStore from '../store/authStore';
44
import api from '../services/api';
5+
import { changePasswordSchema } from '../schemas/profile.schema';
56
import Header from '../components/Header';
67
import Footer from '../components/Footer';
78

@@ -26,13 +27,9 @@ export default function Profile() {
2627
setError('');
2728
setSuccess('');
2829

29-
if (newPassword !== confirmPassword) {
30-
setError('Les mots de passe ne correspondent pas');
31-
return;
32-
}
33-
34-
if (newPassword.length < 6) {
35-
setError('Le mot de passe doit contenir au moins 6 caractères');
30+
const result = changePasswordSchema.safeParse({ oldPassword, newPassword, confirmPassword });
31+
if (!result.success) {
32+
setError(result.error.issues[0].message);
3633
return;
3734
}
3835

@@ -43,10 +40,7 @@ export default function Profile() {
4340
setOldPassword('');
4441
setNewPassword('');
4542
setConfirmPassword('');
46-
setTimeout(() => {
47-
setShowPasswordForm(false);
48-
setSuccess('');
49-
}, 2000);
43+
setTimeout(() => { setShowPasswordForm(false); setSuccess(''); }, 2000);
5044
} catch (err) {
5145
setError('Erreur lors du changement de mot de passe');
5246
} finally {

0 commit comments

Comments
 (0)