Added screenshots

This commit is contained in:
Martin Ziegler
2025-11-28 10:23:59 +01:00
parent 8a85147713
commit 09677861ba
94 changed files with 25918 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => {
const stored = localStorage.getItem('theme') as Theme | null;
if (stored) return stored;
// Check system preference
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
return 'light';
}
return 'dark';
});
useEffect(() => {
const root = document.documentElement;
if (theme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setThemeState((prev) => (prev === 'dark' ? 'light' : 'dark'));
};
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
+81
View File
@@ -0,0 +1,81 @@
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
import { CheckCircle, XCircle, AlertCircle, Info, X } from 'lucide-react';
type ToastType = 'success' | 'error' | 'warning' | 'info';
interface Toast {
id: string;
message: string;
type: ToastType;
}
interface ToastContextType {
showToast: (message: string, type?: ToastType) => void;
}
const ToastContext = createContext<ToastContextType | undefined>(undefined);
export function useToast() {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
}
const icons = {
success: <CheckCircle className="w-5 h-5 text-green-400" />,
error: <XCircle className="w-5 h-5 text-red-400" />,
warning: <AlertCircle className="w-5 h-5 text-yellow-400" />,
info: <Info className="w-5 h-5 text-blue-400" />,
};
const bgColors = {
success: 'bg-green-500/10 border-green-500/30',
error: 'bg-red-500/10 border-red-500/30',
warning: 'bg-yellow-500/10 border-yellow-500/30',
info: 'bg-blue-500/10 border-blue-500/30',
};
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const showToast = useCallback((message: string, type: ToastType = 'success') => {
const id = Math.random().toString(36).substr(2, 9);
setToasts((prev) => [...prev, { id, message, type }]);
// Auto-dismiss after 3 seconds
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 3000);
}, []);
const dismissToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
return (
<ToastContext.Provider value={{ showToast }}>
{children}
{/* Toast Container */}
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
{toasts.map((toast) => (
<div
key={toast.id}
className={`flex items-center gap-3 px-4 py-3 rounded-lg border shadow-lg backdrop-blur-sm animate-slide-in ${bgColors[toast.type]}`}
>
{icons[toast.type]}
<span className="text-white text-sm">{toast.message}</span>
<button
onClick={() => dismissToast(toast.id)}
className="ml-2 text-bambu-gray hover:text-white transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
))}
</div>
</ToastContext.Provider>
);
}