Toasts and stuff
This commit is contained in:
parent
e979a4dcb1
commit
d1888d2c6c
|
|
@ -0,0 +1,55 @@
|
|||
import { useMemo } from 'preact/hooks'
|
||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||
import { AppContextProvider } from './contexts/AppContext'
|
||||
import { ConfirmModalsContextProvider } from './contexts/ConfirmModalsContext'
|
||||
import { useToasts } from './contexts/ToastsContext/ToastsContext'
|
||||
import { Router } from './pages/Router'
|
||||
import { RequestError } from './api/request'
|
||||
|
||||
export const App = () => {
|
||||
const toasts = useToasts()
|
||||
|
||||
const queryClient = useMemo(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
mutations: {
|
||||
onError: async (error) => {
|
||||
if (error instanceof RequestError) {
|
||||
const responseText = await error.response.text()
|
||||
|
||||
try {
|
||||
const response = JSON.parse(responseText)
|
||||
|
||||
toasts.push({
|
||||
message: response.error ?? responseText,
|
||||
variant: 'error',
|
||||
})
|
||||
} catch (e) {
|
||||
toasts.push({ message: responseText, variant: 'error' })
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
toasts.push({ message: String(error), variant: 'error' })
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[toasts.push]
|
||||
)
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppContextProvider>
|
||||
<ConfirmModalsContextProvider>
|
||||
<Router />
|
||||
</ConfirmModalsContextProvider>
|
||||
</AppContextProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,24 +1,10 @@
|
|||
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||
import { AppContextProvider } from './contexts/AppContext'
|
||||
import { ConfirmModalsContextProvider } from './contexts/ConfirmModalsContext'
|
||||
import { Router } from './pages/Router'
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
import { App } from './App'
|
||||
import { ToastsContextProvider } from './contexts/ToastsContext/ToastsContext'
|
||||
|
||||
export const Root = () => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppContextProvider>
|
||||
<ConfirmModalsContextProvider>
|
||||
<Router />
|
||||
</ConfirmModalsContextProvider>
|
||||
</AppContextProvider>
|
||||
</QueryClientProvider>
|
||||
<ToastsContextProvider>
|
||||
<App />
|
||||
</ToastsContextProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
.toasts {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
|
||||
.toast {
|
||||
background-color: var(--toast-bg-color);
|
||||
color: var(--toast-fg-color);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
max-width: 90%;
|
||||
width: 25rem;
|
||||
box-shadow: var(--box-shadow);
|
||||
margin-top: 1rem;
|
||||
animation-name: toastIn;
|
||||
animation-duration: 200ms;
|
||||
animation-iteration-count: 1;
|
||||
|
||||
&.variant-error {
|
||||
background-color: var(--toast-error-bg-color);
|
||||
color: var(--toast-error-fg-color);
|
||||
}
|
||||
|
||||
&.out {
|
||||
animation-name: toastOut;
|
||||
animation-duration: 200ms;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toastIn {
|
||||
from {
|
||||
transform: translate(0, 100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(0, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toastOut {
|
||||
from {
|
||||
transform: translate(0, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(0, -100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ a {
|
|||
@import 'components/data-table';
|
||||
@import 'components/section-title';
|
||||
@import 'components/empty';
|
||||
@import 'components/toasts';
|
||||
|
||||
@import 'pages/login-page';
|
||||
@import 'pages/sensors-page';
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@
|
|||
--input-disabled-fg-color: #000;
|
||||
--input-disabled-bg-color: #fff;
|
||||
--input-hint-color: #444;
|
||||
--toast-bg-color: #eee;
|
||||
--toast-fg-color: #666;
|
||||
--toast-error-bg-color: rgb(153, 14, 14);
|
||||
--toast-error-fg-color: #fff;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import { ComponentChildren, createContext } from 'preact'
|
||||
import { useContext, useMemo, useRef } from 'preact/hooks'
|
||||
import { Toasts } from './components/Toasts'
|
||||
import { ToastConfig, ToastsContextType } from './types'
|
||||
|
||||
type Props = {
|
||||
children: ComponentChildren
|
||||
}
|
||||
|
||||
const ToastsContext = createContext<ToastsContextType | null>(null)
|
||||
|
||||
export const ToastsContextProvider = ({ children }: Props) => {
|
||||
const pushRef = useRef<ToastsContextType['push'] | null>(null)
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ push: (config: ToastConfig) => pushRef.current?.(config) }),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<ToastsContext.Provider value={value}>
|
||||
{children}
|
||||
<Toasts pushRef={pushRef} />
|
||||
</ToastsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useToasts = () => {
|
||||
const context = useContext(ToastsContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useToasts must be used within a ToastsContextProvider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { cn } from '@/utils/cn'
|
||||
import { ToastConfig } from '../types'
|
||||
|
||||
type Props = {
|
||||
config: ToastConfig
|
||||
fadingOut?: boolean
|
||||
}
|
||||
|
||||
export const Toast = ({ config, fadingOut }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'toast',
|
||||
config.variant && `variant-${config.variant}`,
|
||||
fadingOut && 'out'
|
||||
)}
|
||||
>
|
||||
{config.message}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { useState, useCallback, MutableRef } from 'preact/hooks'
|
||||
import { ToastConfigWithUniqueId, ToastConfig } from '../types'
|
||||
import { Toast } from './Toast'
|
||||
|
||||
type Props = {
|
||||
pushRef: MutableRef<((message: ToastConfig) => void) | null>
|
||||
}
|
||||
|
||||
let ID_COUNTER = 1
|
||||
|
||||
export const Toasts = ({ pushRef }: Props) => {
|
||||
const [toasts, setToasts] = useState<ToastConfigWithUniqueId[]>([])
|
||||
const [fading, setFading] = useState<Record<number, boolean | undefined>>({})
|
||||
|
||||
const push = useCallback((config: ToastConfig) => {
|
||||
const item = { ...config, id: ID_COUNTER++ }
|
||||
const timeout = config.timeoutInMs ?? 5000
|
||||
|
||||
setToasts((prev) => [...prev, item])
|
||||
|
||||
setTimeout(() => {
|
||||
setFading((prev) => ({ ...prev, [item.id]: true }))
|
||||
}, Math.max(0, timeout - 500))
|
||||
|
||||
setTimeout(() => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== item.id))
|
||||
|
||||
setFading((prev) => {
|
||||
const result = { ...prev }
|
||||
delete result[item.id]
|
||||
|
||||
return result
|
||||
})
|
||||
}, timeout)
|
||||
}, [])
|
||||
|
||||
pushRef.current = push
|
||||
|
||||
return (
|
||||
<div className="toasts">
|
||||
{toasts.map((t) => (
|
||||
<Toast key={t.id} config={t} fadingOut={fading[t.id]} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { ComponentChildren } from 'preact'
|
||||
|
||||
export type ToastConfig = {
|
||||
message: ComponentChildren
|
||||
timeoutInMs?: number
|
||||
variant?: 'success' | 'info' | 'error'
|
||||
}
|
||||
|
||||
export type ToastConfigWithUniqueId = ToastConfig & {
|
||||
id: number
|
||||
}
|
||||
|
||||
export type ToastsContextType = {
|
||||
push: (config: ToastConfig) => void
|
||||
}
|
||||
|
|
@ -270,6 +270,7 @@ export const useForm = <TValue extends BaseValue = BaseValue>({
|
|||
) => {
|
||||
return {
|
||||
name,
|
||||
id: name,
|
||||
ref: handleInputRef,
|
||||
onChange: (e: Event) => handleInputChange(e, options),
|
||||
value: `${internalRef.current.value[name] ?? ''}`,
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ func PostMQTTBrokerPublish(s *app.Server) gin.HandlerFunc {
|
|||
}
|
||||
|
||||
if err := s.Services.MQTTBrokers.PublishTopic(brokerId, body.Topic, body.Message, qos, retain); err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue