aboutsummaryrefslogtreecommitdiff
path: root/web/src/context
diff options
context:
space:
mode:
authorzwlucas <lucas.fariamo08@gmail.com>2026-07-25 21:23:29 +0000
committerzwlucas <lucas.fariamo08@gmail.com>2026-07-25 21:23:29 +0000
commitef55d9bb24430610b60c1b77821307cd72c08c55 (patch)
treee6935a810b34370b0a4c821e55f6397aac50c832 /web/src/context
downloadhelpdesk-ef55d9bb24430610b60c1b77821307cd72c08c55.tar.gz
helpdesk-ef55d9bb24430610b60c1b77821307cd72c08c55.zip
feat: upload of the projectHEADmaster
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'web/src/context')
-rw-r--r--web/src/context/AuthContext.tsx79
-rw-r--r--web/src/context/Toast.module.css75
-rw-r--r--web/src/context/ToastContext.tsx71
3 files changed, 225 insertions, 0 deletions
diff --git a/web/src/context/AuthContext.tsx b/web/src/context/AuthContext.tsx
new file mode 100644
index 0000000..f86a940
--- /dev/null
+++ b/web/src/context/AuthContext.tsx
@@ -0,0 +1,79 @@
+/**
+ * AuthContext.tsx
+ *
+ * Contexto global de autenticação.
+ * Persiste o JWT no localStorage e expõe o estado do usuário logado.
+ */
+
+import {
+ createContext,
+ useContext,
+ useState,
+ useCallback,
+ type ReactNode,
+} from 'react';
+import type { UserResponse } from '@/lib/api';
+
+// ─── Tipos ────────────────────────────────────────────────────────────────────
+
+interface AuthState {
+ user: UserResponse | null;
+ token: string | null;
+ isAuthenticated: boolean;
+ login: (token: string, user: UserResponse) => void;
+ logout: () => void;
+}
+
+// ─── Chave no localStorage ────────────────────────────────────────────────────
+
+const TOKEN_KEY = 'helpdesk:token';
+const USER_KEY = 'helpdesk:user';
+
+// ─── Context ──────────────────────────────────────────────────────────────────
+
+const AuthContext = createContext<AuthState | null>(null);
+
+// ─── Provider ─────────────────────────────────────────────────────────────────
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ // Inicializa com dados persistidos (restaura sessão ao recarregar)
+ const [token, setToken] = useState<string | null>(
+ () => localStorage.getItem(TOKEN_KEY),
+ );
+ const [user, setUser] = useState<UserResponse | null>(() => {
+ const stored = localStorage.getItem(USER_KEY);
+ return stored ? (JSON.parse(stored) as UserResponse) : null;
+ });
+
+ const login = useCallback((newToken: string, newUser: UserResponse) => {
+ localStorage.setItem(TOKEN_KEY, newToken);
+ localStorage.setItem(USER_KEY, JSON.stringify(newUser));
+ setToken(newToken);
+ setUser(newUser);
+ }, []);
+
+ const logout = useCallback(() => {
+ localStorage.removeItem(TOKEN_KEY);
+ localStorage.removeItem(USER_KEY);
+ setToken(null);
+ setUser(null);
+ }, []);
+
+ return (
+ <AuthContext.Provider
+ value={{ user, token, isAuthenticated: Boolean(token), login, logout }}
+ >
+ {children}
+ </AuthContext.Provider>
+ );
+}
+
+// ─── Hook ─────────────────────────────────────────────────────────────────────
+
+export function useAuth(): AuthState {
+ const ctx = useContext(AuthContext);
+ if (!ctx) {
+ throw new Error('useAuth deve ser usado dentro de <AuthProvider>');
+ }
+ return ctx;
+}
diff --git a/web/src/context/Toast.module.css b/web/src/context/Toast.module.css
new file mode 100644
index 0000000..bb56336
--- /dev/null
+++ b/web/src/context/Toast.module.css
@@ -0,0 +1,75 @@
+.toastContainer {
+ position: fixed;
+ bottom: var(--space-6);
+ right: var(--space-6);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+ z-index: 9999;
+ pointer-events: none; /* Deixa cliques passarem pelo container vazio */
+}
+
+.toast {
+ pointer-events: auto; /* Reativa cliques dentro do toast */
+ min-width: 300px;
+ background: var(--color-surface);
+ color: var(--color-text-primary);
+ border: 1px solid var(--color-border);
+ padding: var(--space-3) var(--space-4);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-lg);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-3);
+ font-size: var(--text-sm);
+ font-weight: 500;
+ backdrop-filter: blur(8px);
+
+ animation: slideIn var(--transition-normal) ease forwards;
+}
+
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateX(100%) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0) scale(1);
+ }
+}
+
+.info {
+ border-left: 4px solid var(--color-accent);
+}
+
+.success {
+ border-left: 4px solid var(--color-priority-low);
+}
+
+.error {
+ border-left: 4px solid var(--color-priority-critical);
+}
+
+.closeBtn {
+ background: none;
+ border: none;
+ color: var(--color-text-muted);
+ font-size: var(--text-lg);
+ cursor: pointer;
+ padding: 0;
+ line-height: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ border-radius: var(--radius-sm);
+ transition: background var(--transition-fast), color var(--transition-fast);
+}
+
+.closeBtn:hover {
+ background: var(--color-bg);
+ color: var(--color-text-primary);
+}
diff --git a/web/src/context/ToastContext.tsx b/web/src/context/ToastContext.tsx
new file mode 100644
index 0000000..87b3894
--- /dev/null
+++ b/web/src/context/ToastContext.tsx
@@ -0,0 +1,71 @@
+/**
+ * ToastContext.tsx
+ * Sistema minimalista de Toast Notifications.
+ */
+
+import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
+import { createPortal } from 'react-dom';
+import styles from './Toast.module.css';
+
+type ToastType = 'info' | 'success' | 'error';
+
+interface ToastData {
+ id: string;
+ message: string;
+ type: ToastType;
+}
+
+interface ToastContextValue {
+ addToast: (message: string, type?: ToastType) => void;
+}
+
+const ToastContext = createContext<ToastContextValue | null>(null);
+
+export function ToastProvider({ children }: { children: ReactNode }) {
+ const [toasts, setToasts] = useState<ToastData[]>([]);
+
+ const addToast = useCallback((message: string, type: ToastType = 'info') => {
+ const id = Math.random().toString(36).substring(2, 9);
+ setToasts((prev) => [...prev, { id, message, type }]);
+
+ // Remove automaticamente após 4 segundos
+ setTimeout(() => {
+ setToasts((prev) => prev.filter((t) => t.id !== id));
+ }, 4000);
+ }, []);
+
+ const removeToast = (id: string) => {
+ setToasts((prev) => prev.filter((t) => t.id !== id));
+ };
+
+ return (
+ <ToastContext.Provider value={{ addToast }}>
+ {children}
+
+ {/* Portal renderiza os toasts no final do <body> */}
+ {createPortal(
+ <div className={styles.toastContainer}>
+ {toasts.map((toast) => (
+ <div key={toast.id} className={`${styles.toast} ${styles[toast.type]}`}>
+ <span>{toast.message}</span>
+ <button
+ className={styles.closeBtn}
+ onClick={() => removeToast(toast.id)}
+ aria-label="Fechar"
+ >
+ &times;
+ </button>
+ </div>
+ ))}
+ </div>,
+ document.body
+ )}
+ </ToastContext.Provider>
+ );
+}
+
+export function useToast() {
+ const ctx = useContext(ToastContext);
+ if (!ctx) throw new Error('useToast deve ser usado dentro do ToastProvider');
+ return ctx;
+}