feat(undo-toast): Toast mit Undo-Action für deletes [tsc:fail]
This commit is contained in:
parent
d03cb2491b
commit
419fcb2732
@ -7,6 +7,7 @@
|
||||
"time-rounding-rules",
|
||||
"user-avatars",
|
||||
"app-version-display",
|
||||
"api-client-phase12"
|
||||
"api-client-phase12",
|
||||
"router-phase12"
|
||||
]
|
||||
}
|
||||
5
.phase13-state.json
Normal file
5
.phase13-state.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"completed_features": [],
|
||||
"current_feature": "undo-toast",
|
||||
"started_at": "2026-05-23T06:42:42.473991"
|
||||
}
|
||||
@ -1554,3 +1554,27 @@ src/index.ts(27,25): error TS2769: No overload matches this call.
|
||||
Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error.
|
||||
Argument of type 'Promise<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
||||
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTy
|
||||
- `06:41:11` **INFO** Committed feature router-phase12
|
||||
- `06:41:11` **INFO** Pushed: rc=0
|
||||
|
||||
## Phase-12 Run beendet (2026-05-23 06:41:11)
|
||||
|
||||
- `06:41:11` **INFO** OK: 0, Attempted: 6, Total: 6
|
||||
|
||||
## 🚀 Phase-13 Codegen-Run gestartet (2026-05-23 06:42:42)
|
||||
|
||||
|
||||
## Phase-3 Feature: undo-toast (2026-05-23 06:42:42)
|
||||
|
||||
- `06:42:42` **INFO** Description: Toast mit Undo-Action für deletes
|
||||
- `06:42:42` **INFO** Generating apps/web/src/components/Toast.tsx (ERWEITERT — behalte alle bestehenden Methoden (success/error/info). Fü…)
|
||||
- `06:43:06` **INFO** wrote 2823 chars in 24.3s (attempt 1)
|
||||
- `06:43:06` **INFO** Running tsc --noEmit on api…
|
||||
- `06:43:08` **WARN** tsc errors:
|
||||
src/index.ts(27,25): error TS2769: No overload matches this call.
|
||||
Overload 1 of 3, '(plugin: FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error.
|
||||
Argument of type 'Promise<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
||||
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTypeProvider>, opts: { ...; }, done: (err?: Error | undefined) => void): void'.
|
||||
Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error.
|
||||
Argument of type 'Promise<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
||||
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTy
|
||||
|
||||
@ -6,12 +6,14 @@ interface Toast {
|
||||
id: string;
|
||||
message: string;
|
||||
type: ToastType;
|
||||
onUndo?: () => void;
|
||||
}
|
||||
|
||||
interface ToastContextType {
|
||||
success: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
info: (message: string) => void;
|
||||
undoable: (options: { message: string; onUndo: () => void }) => void;
|
||||
}
|
||||
|
||||
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
||||
@ -19,21 +21,28 @@ 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 removeToast = useCallback((id: string) => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||
}, []);
|
||||
|
||||
const addToast = useCallback((message: string, type: ToastType, onUndo?: () => void) => {
|
||||
const id = Math.random().toString(36).substring(2, 9);
|
||||
setToasts((prev) => [...prev, { id, message, type, onUndo }]);
|
||||
|
||||
const duration = onUndo ? 8000 : 4000;
|
||||
setTimeout(() => {
|
||||
removeToast(id);
|
||||
}, duration);
|
||||
}, [removeToast]);
|
||||
|
||||
const success = (message: string) => addToast(message, 'success');
|
||||
const error = (message: string) => addToast(message, 'error');
|
||||
const info = (message: string) => addToast(message, 'info');
|
||||
const undoable = ({ message, onUndo }: { message: string; onUndo: () => void }) =>
|
||||
addToast(message, 'info', onUndo);
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ success, error, info }}>
|
||||
<ToastContext.Provider value={{ success, error, info, undoable }}>
|
||||
{children}
|
||||
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 w-full max-w-sm">
|
||||
{toasts.map((toast) => (
|
||||
@ -41,12 +50,24 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||
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
|
||||
flex items-center justify-between gap-4
|
||||
${toast.type === 'success' ? 'bg-emerald-500' : ''}
|
||||
${toast.type === 'error' ? 'bg-red-500' : ''}
|
||||
${toast.type === 'info' ? 'bg-slate-700' : ''}
|
||||
`}
|
||||
>
|
||||
{toast.message}
|
||||
<span>{toast.message}</span>
|
||||
{toast.onUndo && (
|
||||
<button
|
||||
onClick={() => {
|
||||
toast.onUndo();
|
||||
removeToast(toast.id);
|
||||
}}
|
||||
className="text-xs font-bold uppercase tracking-wider bg-white/20 hover:bg-white/30 px-2 py-1 rounded transition-colors"
|
||||
>
|
||||
Rückgängig
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
152
scripts/phase13_features.py
Normal file
152
scripts/phase13_features.py
Normal file
@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase-13: undo-toast, breadcrumb, in-app-changelog, ARIA, kpi-comparison."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from phase2_features import Feature, FileGen, ROOT, log, log_section # noqa: E402
|
||||
from phase3_features import run_feature_v2 # noqa: E402
|
||||
|
||||
PHASE13_STATE = ROOT / ".phase13-state.json"
|
||||
|
||||
FEATURES: list[Feature] = [
|
||||
Feature(
|
||||
name="undo-toast",
|
||||
description="Toast mit Undo-Action für deletes",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/web/src/components/Toast.tsx",
|
||||
purpose=(
|
||||
"ERWEITERT — behalte alle bestehenden Methoden (success/error/info). Füge eine: "
|
||||
"undoable({message, onUndo}) — zeigt Toast mit message + 'Rückgängig'-Button, "
|
||||
"wenn Klick: ruft onUndo, schließt Toast. Auto-dismiss nach 8s (länger als normal)."
|
||||
),
|
||||
refs=["apps/web/src/components/Toast.tsx"],
|
||||
),
|
||||
],
|
||||
),
|
||||
Feature(
|
||||
name="breadcrumb-navigation",
|
||||
description="Breadcrumb-Komponente, top der Detail-Pages",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/web/src/components/Breadcrumb.tsx",
|
||||
purpose=(
|
||||
"Breadcrumb-Component. Props: items: [{label, to?}] (last item ohne to = current page). "
|
||||
"Render: Home > Customers > Customer-Name als Text-Links + chevron. "
|
||||
"Tailwind text-sm text-gray-500 with hover-darker."
|
||||
),
|
||||
),
|
||||
FileGen(
|
||||
path="apps/web/src/pages/CustomerDetail.tsx",
|
||||
purpose=(
|
||||
"ERWEITERT — behalte alles. Füge <Breadcrumb items={[{label:'Dashboard',to:'/'},{label:'Customers',to:'/customers'},{label:customer.name}]} /> "
|
||||
"ganz oben."
|
||||
),
|
||||
refs=["apps/web/src/pages/CustomerDetail.tsx"],
|
||||
),
|
||||
FileGen(
|
||||
path="apps/web/src/pages/ProjectDetail.tsx",
|
||||
purpose=(
|
||||
"ERWEITERT — behalte alles. Füge analog Breadcrumb mit Projects-Pfad."
|
||||
),
|
||||
refs=["apps/web/src/pages/ProjectDetail.tsx"],
|
||||
),
|
||||
],
|
||||
),
|
||||
Feature(
|
||||
name="in-app-changelog",
|
||||
description="Changelog-Modal mit Versions-History",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/web/src/components/ChangelogModal.tsx",
|
||||
purpose=(
|
||||
"Changelog-Modal. Inline-Array CHANGELOG = [{version, date, changes: [strings]}] mit "
|
||||
"5 fake Einträgen (z.B. 0.0.1 Initial release, 0.0.2 added Projects, etc). "
|
||||
"Trigger via prop {open, onClose}. Tailwind fixed inset-0 + centered card mit Versionsliste."
|
||||
),
|
||||
),
|
||||
FileGen(
|
||||
path="apps/web/src/components/VersionBadge.tsx",
|
||||
purpose=(
|
||||
"ERWEITERT — behalte VersionBadge. Klick öffnet jetzt <ChangelogModal /> (useState open). "
|
||||
"Statt new-tab-Link."
|
||||
),
|
||||
refs=["apps/web/src/components/VersionBadge.tsx"],
|
||||
),
|
||||
],
|
||||
),
|
||||
Feature(
|
||||
name="aria-improvements",
|
||||
description="Accessibility-Improvements (ARIA labels) in Nav + Forms",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/web/src/components/Nav.tsx",
|
||||
purpose=(
|
||||
"ERWEITERT — füge aria-label zu allen icon-only-Buttons (Theme-Toggle, Lang-Toggle, Logout, CommandPalette-trigger). "
|
||||
"role='navigation' aufs nav-element. Active-Link mit aria-current='page'. "
|
||||
"Behalte alles, nur a11y polish."
|
||||
),
|
||||
refs=["apps/web/src/components/Nav.tsx"],
|
||||
),
|
||||
],
|
||||
),
|
||||
Feature(
|
||||
name="kpi-comparison",
|
||||
description="Dashboard KPI-Karten mit Vergleich zur Vorwoche",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/web/src/pages/Dashboard.tsx",
|
||||
purpose=(
|
||||
"ERWEITERT — behalte alles (Stats-Cards, Chart, ActivityFeed, Export-Button). "
|
||||
"Pro Stats-Karte: zusätzliche Mini-Zeile 'vs. Vorwoche: +12%' (grün) oder '-5%' (rot). "
|
||||
"Berechnung: fetch time-entries letzte 14 Tage, split current/previous week, calc delta."
|
||||
),
|
||||
refs=["apps/web/src/pages/Dashboard.tsx"],
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def load_state() -> dict:
|
||||
if PHASE13_STATE.exists():
|
||||
return json.loads(PHASE13_STATE.read_text())
|
||||
return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()}
|
||||
|
||||
|
||||
def save_state(state: dict) -> None:
|
||||
PHASE13_STATE.write_text(json.dumps(state, indent=2))
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
log_section("🚀 Phase-13 Codegen-Run gestartet")
|
||||
state = load_state()
|
||||
for feature in FEATURES:
|
||||
if feature.name in state.get("completed_features", []):
|
||||
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.name} crashed: {e}", level="ERROR")
|
||||
state.setdefault("attempted_features", []).append(feature.name); save_state(state)
|
||||
|
||||
log_section("Phase-13 Run beendet")
|
||||
log(f"OK: {len(state.get('completed_features', []))}, Attempted: {len(state.get('attempted_features', []))}, Total: {len(FEATURES)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(asyncio.run(main()))
|
||||
Loading…
Reference in New Issue
Block a user