Initial commit

This commit is contained in:
root
2026-01-26 08:34:48 +01:00
commit 387314581f
216 changed files with 81204 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
'use client';
import { createContext, useContext, useState, useCallback, ReactNode } from 'react';
import { useAccentColor } from './AccentColorContext';
type ToastType = 'success' | 'error' | 'info';
interface Toast {
id: string;
message: string;
type: ToastType;
}
interface ToastContextType {
toasts: Toast[];
showToast: (message: string, type?: ToastType) => void;
removeToast: (id: string) => void;
}
const ToastContext = createContext<ToastContextType | null>(null);
// Check if a color is light (needs dark text)
function isLightColor(hex: string): boolean {
const color = hex.replace('#', '');
const r = parseInt(color.slice(0, 2), 16);
const g = parseInt(color.slice(2, 4), 16);
const b = parseInt(color.slice(4, 6), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.6;
}
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const showToast = useCallback((message: string, type: ToastType = 'info') => {
const id = Math.random().toString(36).slice(2);
setToasts(prev => [...prev, { id, message, type }]);
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id));
}, 3500);
}, []);
const removeToast = useCallback((id: string) => {
setToasts(prev => prev.filter(t => t.id !== id));
}, []);
return (
<ToastContext.Provider value={{ toasts, showToast, removeToast }}>
{children}
<ToastContainer toasts={toasts} removeToast={removeToast} />
</ToastContext.Provider>
);
}
export function useToast() {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
}
function ToastContainer({ toasts, removeToast }: { toasts: Toast[]; removeToast: (id: string) => void }) {
const { accentColor } = useAccentColor();
const needsDarkText = isLightColor(accentColor);
if (toasts.length === 0) return null;
return (
<div style={{
position: 'fixed',
bottom: '24px',
right: '24px',
zIndex: 9999,
display: 'flex',
flexDirection: 'column',
gap: '8px',
pointerEvents: 'none',
}}>
{toasts.map(toast => (
<div
key={toast.id}
onClick={() => removeToast(toast.id)}
style={{
pointerEvents: 'auto',
cursor: 'pointer',
padding: '12px 16px',
borderRadius: '8px',
background: toast.type === 'error'
? 'var(--danger)'
: toast.type === 'success'
? 'var(--accent)'
: 'var(--background-secondary)',
color: toast.type === 'error'
? '#fff'
: toast.type === 'success'
? (needsDarkText ? '#000' : '#fff')
: 'var(--foreground)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
fontSize: '14px',
fontWeight: 500,
maxWidth: '320px',
animation: 'toastSlideIn 0.2s ease-out',
border: '1px solid var(--border)',
}}
>
{toast.message}
</div>
))}
<style jsx global>{`
@keyframes toastSlideIn {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
`}</style>
</div>
);
}