mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-12 06:40:57 +04:30
Initial commit: Meshkee dashboards monorepo.
Includes business, customer, and super-admin apps with shared packages and production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import { ApiError, getAccessToken, isAbortError } from '../lib/api'
|
||||
import { fetchCurrentUser, login as loginRequest, logout as logoutRequest } from '../services/authService'
|
||||
import type { AuthUser } from '../types/auth'
|
||||
|
||||
interface AuthContextValue {
|
||||
user: AuthUser | null
|
||||
isLoading: boolean
|
||||
login: (cellNumber: string, password: string) => Promise<void>
|
||||
logout: () => void
|
||||
setUser: (user: AuthUser) => void
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null)
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<AuthUser | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const logout = useCallback(() => {
|
||||
logoutRequest()
|
||||
setUser(null)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
|
||||
async function init() {
|
||||
if (!getAccessToken()) {
|
||||
if (!controller.signal.aborted) setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { user: currentUser } = await fetchCurrentUser(controller.signal)
|
||||
if (controller.signal.aborted) return
|
||||
if (currentUser.dashboard !== 'super_admin') {
|
||||
logout()
|
||||
return
|
||||
}
|
||||
setUser(currentUser)
|
||||
} catch (err) {
|
||||
if (isAbortError(err) || controller.signal.aborted) return
|
||||
logout()
|
||||
} finally {
|
||||
if (!controller.signal.aborted) setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
void init()
|
||||
|
||||
return () => {
|
||||
controller.abort()
|
||||
}
|
||||
}, [logout])
|
||||
|
||||
const login = useCallback(
|
||||
async (cellNumber: string, password: string) => {
|
||||
const data = await loginRequest(cellNumber, password)
|
||||
|
||||
if (data.user.dashboard !== 'super_admin') {
|
||||
logoutRequest()
|
||||
throw new ApiError(
|
||||
'This account does not have super admin access. Use the correct dashboard for your role.',
|
||||
403,
|
||||
)
|
||||
}
|
||||
|
||||
setUser(data.user)
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ user, isLoading, login, logout, setUser }),
|
||||
[user, isLoading, login, logout],
|
||||
)
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext)
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within AuthProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import styles from '../components/Toast.module.css'
|
||||
|
||||
export type ToastVariant = 'success' | 'error' | 'info'
|
||||
|
||||
interface ToastItem {
|
||||
id: number
|
||||
message: string
|
||||
variant: ToastVariant
|
||||
}
|
||||
|
||||
interface ToastContextValue {
|
||||
showToast: (message: string, variant?: ToastVariant) => void
|
||||
}
|
||||
|
||||
const ToastContext = createContext<ToastContextValue | null>(null)
|
||||
|
||||
const TOAST_DURATION_MS = 3200
|
||||
const ANIMATION_MS = 200
|
||||
|
||||
export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
const [toast, setToast] = useState<ToastItem | null>(null)
|
||||
const [closing, setClosing] = useState(false)
|
||||
const idRef = useRef(0)
|
||||
const dismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const clearTimers = useCallback(() => {
|
||||
if (dismissTimerRef.current) {
|
||||
clearTimeout(dismissTimerRef.current)
|
||||
dismissTimerRef.current = null
|
||||
}
|
||||
if (closeTimerRef.current) {
|
||||
clearTimeout(closeTimerRef.current)
|
||||
closeTimerRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
const dismissToast = useCallback(() => {
|
||||
setClosing(true)
|
||||
closeTimerRef.current = setTimeout(() => {
|
||||
setToast(null)
|
||||
setClosing(false)
|
||||
}, ANIMATION_MS)
|
||||
}, [])
|
||||
|
||||
const showToast = useCallback(
|
||||
(message: string, variant: ToastVariant = 'info') => {
|
||||
clearTimers()
|
||||
idRef.current += 1
|
||||
setClosing(false)
|
||||
setToast({ id: idRef.current, message, variant })
|
||||
|
||||
dismissTimerRef.current = setTimeout(() => {
|
||||
dismissToast()
|
||||
}, TOAST_DURATION_MS)
|
||||
},
|
||||
[clearTimers, dismissToast],
|
||||
)
|
||||
|
||||
useEffect(() => clearTimers, [clearTimers])
|
||||
|
||||
const value = useMemo(() => ({ showToast }), [showToast])
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={value}>
|
||||
{children}
|
||||
<div className={styles.container} aria-live="polite" aria-atomic="true">
|
||||
{toast && (
|
||||
<div
|
||||
key={toast.id}
|
||||
className={`${styles.toast} ${styles[toast.variant]} ${closing ? styles.toastOut : styles.toastIn}`}
|
||||
role="status"
|
||||
>
|
||||
{toast.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ToastContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useToast() {
|
||||
const context = useContext(ToastContext)
|
||||
if (!context) {
|
||||
throw new Error('useToast must be used within ToastProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
Reference in New Issue
Block a user