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",
|
"time-rounding-rules",
|
||||||
"user-avatars",
|
"user-avatars",
|
||||||
"app-version-display",
|
"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.
|
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>'.
|
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
|
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;
|
id: string;
|
||||||
message: string;
|
message: string;
|
||||||
type: ToastType;
|
type: ToastType;
|
||||||
|
onUndo?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToastContextType {
|
interface ToastContextType {
|
||||||
success: (message: string) => void;
|
success: (message: string) => void;
|
||||||
error: (message: string) => void;
|
error: (message: string) => void;
|
||||||
info: (message: string) => void;
|
info: (message: string) => void;
|
||||||
|
undoable: (options: { message: string; onUndo: () => void }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
||||||
@ -19,21 +21,28 @@ const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
|||||||
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||||
|
|
||||||
const addToast = useCallback((message: string, type: ToastType) => {
|
const removeToast = useCallback((id: string) => {
|
||||||
const id = Math.random().toString(36).substring(2, 9);
|
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||||
setToasts((prev) => [...prev, { id, message, type }]);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
||||||
}, 4000);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
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 success = (message: string) => addToast(message, 'success');
|
||||||
const error = (message: string) => addToast(message, 'error');
|
const error = (message: string) => addToast(message, 'error');
|
||||||
const info = (message: string) => addToast(message, 'info');
|
const info = (message: string) => addToast(message, 'info');
|
||||||
|
const undoable = ({ message, onUndo }: { message: string; onUndo: () => void }) =>
|
||||||
|
addToast(message, 'info', onUndo);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastContext.Provider value={{ success, error, info }}>
|
<ToastContext.Provider value={{ success, error, info, undoable }}>
|
||||||
{children}
|
{children}
|
||||||
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 w-full max-w-sm">
|
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 w-full max-w-sm">
|
||||||
{toasts.map((toast) => (
|
{toasts.map((toast) => (
|
||||||
@ -41,12 +50,24 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|||||||
key={toast.id}
|
key={toast.id}
|
||||||
className={`
|
className={`
|
||||||
px-4 py-3 rounded-lg shadow-lg text-white transition-all duration-300 animate-in fade-in slide-in-from-bottom-4
|
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 === 'success' ? 'bg-emerald-500' : ''}
|
||||||
${toast.type === 'error' ? 'bg-red-500' : ''}
|
${toast.type === 'error' ? 'bg-red-500' : ''}
|
||||||
${toast.type === 'info' ? 'bg-slate-700' : ''}
|
${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>
|
||||||
))}
|
))}
|
||||||
</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