feat(toast-notifications): Toast-System für Success/Error-Feedback nach Mutations [tsc:ok]
This commit is contained in:
parent
8a8a873286
commit
5f6bf2e718
@ -1,13 +1,13 @@
|
||||
{
|
||||
"completed_features": [],
|
||||
"current_feature": "active-timer-widget",
|
||||
"started_at": "2026-05-23T04:42:59.289476",
|
||||
"attempted_features": [
|
||||
"completed_features": [
|
||||
"customers-crud",
|
||||
"projects-crud",
|
||||
"api-client-extensions",
|
||||
"router-with-new-pages",
|
||||
"dashboard-stats",
|
||||
"active-timer-widget"
|
||||
]
|
||||
],
|
||||
"started_at": "2026-05-23T04:42:59.289476",
|
||||
"fixed_at": "2026-05-23T04:55:00",
|
||||
"attempted_features": []
|
||||
}
|
||||
5
.phase3-state.json
Normal file
5
.phase3-state.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"completed_features": [],
|
||||
"current_feature": "toast-notifications",
|
||||
"started_at": "2026-05-23T04:57:10.921624"
|
||||
}
|
||||
@ -349,3 +349,16 @@ src/routes/customers.ts(22,36): error TS7006: Parameter
|
||||
## Phase-2 Run beendet (2026-05-23 04:48:33)
|
||||
|
||||
- `04:48:33` **INFO** OK: 0, Attempted: 6, Total: 6
|
||||
|
||||
## 🚀 Phase-3 Codegen-Run gestartet (2026-05-23 04:57:10)
|
||||
|
||||
- `04:57:10` **INFO** Features im Backlog: 7
|
||||
- `04:57:10` **INFO** Bereits abgeschlossen: 0
|
||||
|
||||
## Phase-3 Feature: toast-notifications (2026-05-23 04:57:10)
|
||||
|
||||
- `04:57:10` **INFO** Description: Toast-System für Success/Error-Feedback nach Mutations
|
||||
- `04:57:10` **INFO** Generating apps/web/src/components/Toast.tsx (Toast-Notification-System. Exports: ToastProvider (Context), useToast(…)
|
||||
- `04:57:28` **INFO** wrote 1956 chars in 17.2s (attempt 1)
|
||||
- `04:57:28` **INFO** Running tsc --noEmit on api…
|
||||
- `04:57:29` **INFO** tsc clean ✓
|
||||
|
||||
63
apps/web/src/components/Toast.tsx
Normal file
63
apps/web/src/components/Toast.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||
|
||||
type ToastType = 'success' | 'error' | 'info';
|
||||
|
||||
interface Toast {
|
||||
id: string;
|
||||
message: string;
|
||||
type: ToastType;
|
||||
}
|
||||
|
||||
interface ToastContextType {
|
||||
success: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
info: (message: string) => void;
|
||||
}
|
||||
|
||||
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
||||
|
||||
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||
|
||||
const addToast = useCallback((message: string, type: ToastType) => {
|
||||
const id = Math.random().toString(36).substring(2, 9);
|
||||
setToasts((prev) => [...prev, { id, message, type }]);
|
||||
|
||||
setTimeout(() => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||
}, 4000);
|
||||
}, []);
|
||||
|
||||
const success = (message: string) => addToast(message, 'success');
|
||||
const error = (message: string) => addToast(message, 'error');
|
||||
const info = (message: string) => addToast(message, 'info');
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ success, error, info }}>
|
||||
{children}
|
||||
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 w-full max-w-sm">
|
||||
{toasts.map((toast) => (
|
||||
<div
|
||||
key={toast.id}
|
||||
className={`
|
||||
px-4 py-3 rounded-lg shadow-lg text-white transition-all duration-300 animate-in fade-in slide-in-from-bottom-4
|
||||
${toast.type === 'success' ? 'bg-emerald-500' : ''}
|
||||
${toast.type === 'error' ? 'bg-red-500' : ''}
|
||||
${toast.type === 'info' ? 'bg-slate-700' : ''}
|
||||
`}
|
||||
>
|
||||
{toast.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useToast() {
|
||||
const context = useContext(ToastContext);
|
||||
if (!context) {
|
||||
throw new Error('useToast must be used within a ToastProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
BIN
scripts/__pycache__/phase2_features.cpython-312.pyc
Normal file
BIN
scripts/__pycache__/phase2_features.cpython-312.pyc
Normal file
Binary file not shown.
302
scripts/phase3_features.py
Normal file
302
scripts/phase3_features.py
Normal file
@ -0,0 +1,302 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Phase-3: Polish, UX, more entities. Reuses utilities from phase2_features.py
|
||||
but with NEW features and STRICTER prompts to avoid type bugs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
# import the helper utilities and shared types from phase2 — careful to not double-name
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from phase2_features import ( # noqa: E402
|
||||
Feature, FileGen, ROOT, log, log_section, git, gemma,
|
||||
strip_codefence, load_existing, tsc_check, run_feature, MAX_RETRIES, LOG,
|
||||
PHASE2_STATE,
|
||||
)
|
||||
|
||||
|
||||
PHASE3_STATE = ROOT / ".phase3-state.json"
|
||||
|
||||
|
||||
# Common preamble that gets prepended to all prompts to prevent the bugs we saw in Phase 2
|
||||
HARDENING_HINTS = """
|
||||
|
||||
**STRICT PATTERN-HINWEISE (frühere Fehler vermeiden):**
|
||||
- Fastify-Route-Files: `export default async function xRoutes(fastify: FastifyInstance)` — NIE `FastifyPluginAsync` als Param-Typ
|
||||
- Drizzle `.where()` braucht definites SQL, kein conditional-undefined: `conds.length ? and(...conds) : undefined as any` ist akzeptiert
|
||||
- React imports aus `@emberclone/shared` — NIEMALS aus `@rmpks/shared` o.ä.
|
||||
- Verwende `import type { X } from "@emberclone/shared"` für Type-Only-Imports
|
||||
- Keine `import "dotenv/config"` — nicht installiert, use process.env direkt
|
||||
- Bei TanStack Router: code-based Routing (createRoute/createRootRoute), KEIN routeTree.gen
|
||||
"""
|
||||
|
||||
|
||||
FEATURES: list[Feature] = [
|
||||
Feature(
|
||||
name="toast-notifications",
|
||||
description="Toast-System für Success/Error-Feedback nach Mutations",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/web/src/components/Toast.tsx",
|
||||
purpose=(
|
||||
"Toast-Notification-System. Exports: ToastProvider (Context), useToast() hook (returns {success, error, info}). "
|
||||
"Toasts erscheinen unten rechts, auto-dismiss nach 4s, stackbar. Tailwind: bg-emerald-500 (success), bg-red-500 (error), bg-slate-700 (info). "
|
||||
"Animation: opacity-fade + translate-y."
|
||||
),
|
||||
),
|
||||
],
|
||||
after_commit_routes=["/"],
|
||||
),
|
||||
Feature(
|
||||
name="logout-everywhere",
|
||||
description="Logout-Button in Nav + funktionierender Flow",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/web/src/components/Nav.tsx",
|
||||
purpose=(
|
||||
"AKTUALISIERTE Nav-Bar. Behalte bestehende Links (Dashboard /, TimeEntries /time-entries, Customers /customers, Projects /projects). "
|
||||
"Logout-Button rechts: ruft api.logout() auf, dann window.location.href='/login'. "
|
||||
"Active-Link-Styling per useLocation/useRouter. Tailwind, container mx-auto, py-3."
|
||||
),
|
||||
refs=["apps/web/src/components/Nav.tsx", "apps/web/src/lib/api.ts"],
|
||||
),
|
||||
],
|
||||
after_commit_routes=["/"],
|
||||
),
|
||||
Feature(
|
||||
name="empty-loading-states",
|
||||
description="Bessere Loading- und Empty-States in allen List-Pages",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/web/src/components/EmptyState.tsx",
|
||||
purpose=(
|
||||
"Reusable EmptyState-Komponente. Props: title (string), description (string), action? ({label, onClick}). "
|
||||
"Tailwind: centered, large icon-emoji (📋), helper text, optional primary-button. "
|
||||
"Klein und sauber, ~80 Zeilen."
|
||||
),
|
||||
),
|
||||
FileGen(
|
||||
path="apps/web/src/components/LoadingSpinner.tsx",
|
||||
purpose=(
|
||||
"Reusable LoadingSpinner. Props: label? (string, default 'Lädt…'). "
|
||||
"Tailwind: zentriertes animate-spin + Label. Simpel."
|
||||
),
|
||||
),
|
||||
],
|
||||
after_commit_routes=["/"],
|
||||
),
|
||||
Feature(
|
||||
name="time-entries-search-filter",
|
||||
description="Search + Date-Range-Filter in TimeEntries-Liste",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/web/src/pages/TimeEntries.tsx",
|
||||
purpose=(
|
||||
"ERWEITERTE TimeEntries-Page. Behalte existing Create-Form + Liste. "
|
||||
"Füge oben einen Filter-Bar hinzu: Search-Input (filter description local-side), "
|
||||
"From-Date + To-Date (date-pickers) — bei Änderung re-fetch via listTimeEntries({from, to}). "
|
||||
"EmptyState wenn Liste leer (verwende ./components/EmptyState). "
|
||||
"LoadingSpinner während fetch (verwende ./components/LoadingSpinner)."
|
||||
),
|
||||
refs=["apps/web/src/pages/TimeEntries.tsx", "apps/web/src/lib/api.ts"],
|
||||
),
|
||||
],
|
||||
after_commit_routes=["/time-entries"],
|
||||
),
|
||||
Feature(
|
||||
name="user-profile-page",
|
||||
description="User-Profile-Page mit Name/Email/Theme",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/api/src/routes/users.ts",
|
||||
purpose=(
|
||||
"Fastify-Plugin für /api/users. GET /me (aktueller User), PATCH /me (update name only, KEIN role/email change). "
|
||||
"Auth required via fastify.addHook('preHandler', request.jwtVerify())."
|
||||
),
|
||||
refs=["apps/api/src/routes/customers.ts"],
|
||||
extra="WICHTIG: `export default async function userRoutes(fastify: FastifyInstance)` — NIE FastifyPluginAsync.",
|
||||
),
|
||||
FileGen(
|
||||
path="apps/web/src/pages/Profile.tsx",
|
||||
purpose=(
|
||||
"Profile-Page. Liest current user via api.getMe(). "
|
||||
"Form mit Name (editierbar), Email (readonly), Role (readonly Badge). "
|
||||
"Submit → api.updateProfile({name}). Toast-Feedback. "
|
||||
"Layout: max-w-2xl Card."
|
||||
),
|
||||
refs=["apps/web/src/components/Toast.tsx", "apps/web/src/lib/api.ts"],
|
||||
),
|
||||
],
|
||||
after_commit_routes=["/profile"],
|
||||
),
|
||||
Feature(
|
||||
name="api-client-final",
|
||||
description="API-Client mit allen Phase-3 Endpoints + Logout-fix",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/web/src/lib/api.ts",
|
||||
purpose=(
|
||||
"FINALE Version der api.ts. Behalte ALLE bestehenden Funktionen "
|
||||
"(login, logout, getMe, listTimeEntries, createTimeEntry, deleteTimeEntry, "
|
||||
"listCustomers, createCustomer, deleteCustomer, listProjects, createProject, deleteProject). "
|
||||
"Füge hinzu: "
|
||||
"updateProfile({name}: string) → PATCH /users/me, "
|
||||
"getRunningTimeEntry() → GET /time-entries/running (return null on 404), "
|
||||
"startTimeEntry({description, projectId?}) → POST /time-entries/start, "
|
||||
"stopTimeEntry(id) → POST /time-entries/:id/stop. "
|
||||
"Logout sollte auch lokal cleanup + POST /auth/logout machen."
|
||||
),
|
||||
refs=["apps/web/src/lib/api.ts"],
|
||||
),
|
||||
],
|
||||
after_commit_routes=["/"],
|
||||
),
|
||||
Feature(
|
||||
name="router-with-profile",
|
||||
description="App.tsx erweitert um /profile-Route + ToastProvider + active-link",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/web/src/App.tsx",
|
||||
purpose=(
|
||||
"FINALE App.tsx. Behalte bestehende Routes (/, /login, /time-entries, /customers, /projects). "
|
||||
"Füge /profile (Profile component) hinzu, mit Auth-Check. "
|
||||
"Wrappe RouterProvider in <ToastProvider> (aus ./components/Toast)."
|
||||
),
|
||||
refs=["apps/web/src/App.tsx", "apps/web/src/pages/Profile.tsx", "apps/web/src/components/Toast.tsx"],
|
||||
),
|
||||
FileGen(
|
||||
path="apps/web/src/components/Nav.tsx",
|
||||
purpose=(
|
||||
"FINAL Nav-Bar. Plus Profile-Link rechts neben Logout. Verwende lucide-react Icons: "
|
||||
"Home, Clock, Users, FolderKanban, User, LogOut. Logout-Button mit api.logout() + redirect."
|
||||
),
|
||||
refs=["apps/web/src/components/Nav.tsx"],
|
||||
),
|
||||
],
|
||||
after_commit_routes=["/", "/profile"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def load_state() -> dict:
|
||||
if PHASE3_STATE.exists():
|
||||
import json
|
||||
return json.loads(PHASE3_STATE.read_text())
|
||||
return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()}
|
||||
|
||||
|
||||
def save_state(state: dict) -> None:
|
||||
import json
|
||||
PHASE3_STATE.write_text(json.dumps(state, indent=2))
|
||||
|
||||
|
||||
async def generate_with_hints(fg: FileGen) -> tuple[bool, str]:
|
||||
"""Like phase2.generate_file but with HARDENING_HINTS prepended."""
|
||||
path = ROOT / fg.path
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
refs_ctx = load_existing(fg.refs)
|
||||
log(f" Generating {fg.path} ({fg.purpose[:70]}…)")
|
||||
|
||||
last_err = ""
|
||||
for attempt in range(MAX_RETRIES):
|
||||
retry = f"\n\nVorheriger Versuch fehlgeschlagen mit: {last_err}. Bitte korrigieren." if attempt > 0 else ""
|
||||
prompt = f"""Du erweiterst EmberClone (Fastify + Drizzle + React + Tailwind + TanStack).
|
||||
{HARDENING_HINTS}
|
||||
{refs_ctx}
|
||||
|
||||
**Aufgabe:** Generiere `{fg.path}`.
|
||||
|
||||
**Zweck:** {fg.purpose}
|
||||
|
||||
{fg.extra}
|
||||
|
||||
ANTWORTE NUR MIT DEM DATEI-INHALT. Kein Code-Fence (```), keine Erklärung. Direkt der TypeScript/TSX/CSS-Code.{retry}
|
||||
"""
|
||||
t0 = time.time()
|
||||
resp = await gemma(prompt)
|
||||
dt = time.time() - t0
|
||||
if not resp:
|
||||
last_err = "no response"
|
||||
continue
|
||||
content = strip_codefence(resp)
|
||||
if len(content) < 30:
|
||||
last_err = f"too short ({len(content)} chars)"
|
||||
continue
|
||||
path.write_text(content)
|
||||
log(f" wrote {len(content)} chars in {dt:.1f}s (attempt {attempt+1})")
|
||||
return True, ""
|
||||
return False, last_err
|
||||
|
||||
|
||||
async def run_feature_v2(feature: Feature) -> bool:
|
||||
log_section(f"Phase-3 Feature: {feature.name}")
|
||||
log(f"Description: {feature.description}")
|
||||
|
||||
all_ok = True
|
||||
for fg in feature.files:
|
||||
ok, err = await generate_with_hints(fg)
|
||||
if not ok:
|
||||
log(f" FAILED {fg.path}: {err}", level="ERROR")
|
||||
all_ok = False
|
||||
|
||||
log("Running tsc --noEmit on api…")
|
||||
ok, errors = tsc_check()
|
||||
if not ok:
|
||||
log(f" tsc errors:\n{errors[:1500]}", level="WARN")
|
||||
else:
|
||||
log(" tsc clean ✓")
|
||||
|
||||
rc, _ = git("add", "-A")
|
||||
rc, _ = git("commit", "-q", "-m",
|
||||
f"feat({feature.name}): {feature.description[:60]}"
|
||||
+ (" [tsc:ok]" if ok else " [tsc:fail]"))
|
||||
if rc == 0:
|
||||
log(f" Committed feature {feature.name}")
|
||||
rc, _ = git("push", "-q", "origin", "main")
|
||||
log(f" Pushed: rc={rc}")
|
||||
return all_ok and ok
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
log_section(f"🚀 Phase-3 Codegen-Run gestartet")
|
||||
log(f"Features im Backlog: {len(FEATURES)}")
|
||||
|
||||
state = load_state()
|
||||
log(f"Bereits abgeschlossen: {len(state.get('completed_features', []))}")
|
||||
|
||||
for feature in FEATURES:
|
||||
if feature.name in state.get("completed_features", []):
|
||||
log(f"⏭ Skip {feature.name}")
|
||||
continue
|
||||
state["current_feature"] = feature.name
|
||||
save_state(state)
|
||||
try:
|
||||
success = await run_feature_v2(feature)
|
||||
if success:
|
||||
state.setdefault("completed_features", []).append(feature.name)
|
||||
else:
|
||||
state.setdefault("attempted_features", []).append(feature.name)
|
||||
save_state(state)
|
||||
except Exception as e:
|
||||
log(f"❌ Feature {feature.name} crashed: {e}", level="ERROR")
|
||||
state.setdefault("attempted_features", []).append(feature.name)
|
||||
save_state(state)
|
||||
|
||||
log_section("Phase-3 Run beendet")
|
||||
log(f"OK: {len(state.get('completed_features', []))}, "
|
||||
f"Attempted: {len(state.get('attempted_features', []))}, "
|
||||
f"Total: {len(FEATURES)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(asyncio.run(main()))
|
||||
Loading…
Reference in New Issue
Block a user