fix(images): adds a toaster and handles failed image inserts

The image insert will throw a dom exception if the image size exceeds the alotted storage provided
by local storage. This just lets the user know somethng fufuckedd up and atleast removeds the image
after inserting (inknow, dont tell me) This iwill be solved in the future with IDB
This commit is contained in:
Triston Armstrong 2024-10-10 03:35:35 -04:00
parent 3fd6aef52c
commit 1071743290
Signed by: tristonarmstrong
GPG Key ID: A23B48AE45EB6EFE
7 changed files with 165 additions and 7 deletions

View File

@ -13,7 +13,8 @@
</head> </head>
<body> <body>
<div id="root" /> <div id="toast-root"></div>
<div id="root"></div>
<script type="module" src="/src/main.ts" defer></script> <script type="module" src="/src/main.ts" defer></script>
</body> </body>
</html> </html>

View File

@ -1,8 +1,13 @@
import InfiniteCanvas from "./components/InfinateCanvas" import InfiniteCanvas from "./components/InfinateCanvas"
import { ToastContextProvider } from "./components/Toast"
import { useThemeDetector } from "./utils/useThemeDetector" import { useThemeDetector } from "./utils/useThemeDetector"
export function App() { export function App() {
useThemeDetector() useThemeDetector()
return <InfiniteCanvas /> return (
<ToastContextProvider>
<InfiniteCanvas />
</ToastContextProvider>
)
} }

112
src/components/Toast.tsx Normal file
View File

@ -0,0 +1,112 @@
import {
createContext,
memo,
Portal,
Transition,
TransitionState,
useContext,
useEffect,
useState,
} from "kaioken"
type Toast = {
ts: number
type: "info" | "success" | "warning" | "error"
message: string
expires: number
expired?: boolean
}
const defaultDuration = 3000
const ToastContext = createContext<{
showToast: (type: Toast["type"], message: string) => void
}>(null as any)
export const useToast = () => useContext(ToastContext)
export const ToastContextProvider: Kaioken.FC = ({ children }) => {
const [toasts, setToasts] = useState<Toast[]>([])
useEffect(() => {
const interval = setInterval(() => {
setToasts((prev) =>
prev.map((toast) => {
if (toast.expired) {
return toast
}
return {
...toast,
expired: Date.now() > toast.expires,
}
})
)
}, 500)
return () => clearInterval(interval)
}, [])
const showToast = (
type: Toast["type"],
message: string,
duration?: number
) => {
const ts = Date.now()
const toast = {
ts,
type,
message,
expires: ts + (duration ?? defaultDuration),
}
setToasts((prev) => [...prev, toast])
}
return (
<ToastContext.Provider value={{ showToast }}>
{children}
<Portal container={() => document.getElementById("toast-root")!}>
{toasts.map((toast, i) => (
<Transition
key={toast.ts}
in={!toast.expired}
duration={{
in: 50,
out: 300,
}}
onTransitionEnd={(state) => {
if (state === "exited") {
setToasts((prev) => prev.filter((t) => t.ts !== toast.ts))
}
}}
element={(state) => <ToastItem toast={toast} state={state} i={i} />}
/>
))}
</Portal>
</ToastContext.Provider>
)
}
const ToastItem = memo(
({
toast,
state,
i,
}: {
toast: Toast
state: TransitionState
i: number
}) => {
if (state == "exited") return null
const translateX = state === "entered" ? 0 : 250
const translateY = i * 70
return (
<div
style={{
transform: `translate(${translateX}px, ${translateY}px)`,
}}
className={`toast toast-${toast.type}`}
>
<span className="toast-message">{toast.message}</span>
</div>
)
}
)

View File

@ -1,10 +1,12 @@
import { ImagesSignal } from "../../signals" import { ImagesSignal } from "../../signals"
import images from "../../signals/images" import images from "../../signals/images"
import { updateLocalStorage } from "../../utils/localStorage" import { updateLocalStorage } from "../../utils/localStorage"
import { useToast } from "../Toast"
import { Tooltip } from "./Tooltip" import { Tooltip } from "./Tooltip"
import { defaultClassName } from "./utils" import { defaultClassName } from "./utils"
export function ImageCardButton() { export function ImageCardButton() {
const toast = useToast()
function _handleClick(mouseEvent: MouseEvent) { function _handleClick(mouseEvent: MouseEvent) {
const input = document.createElement('input') const input = document.createElement('input')
input.type = 'file' input.type = 'file'
@ -30,7 +32,7 @@ export function ImageCardButton() {
if (typeof content == 'string') img = content?.split(':')[1] if (typeof content == 'string') img = content?.split(':')[1]
if (!img) return if (!img) return
ImagesSignal.default.addImage({ const imgId = ImagesSignal.default.addImage({
type: "image", type: "image",
title: "New Image", title: "New Image",
contents: content as string, contents: content as string,
@ -43,7 +45,16 @@ export function ImageCardButton() {
h: normalizedH h: normalizedH
} }
}) })
updateLocalStorage("images", images.images.value)
try {
updateLocalStorage("images", images.images.value)
} catch (e: unknown) {
if (e instanceof DOMException) {
if (e.name !== 'QuotaExceededError') return
toast.showToast("error", "Could not add such a girthy image!")
ImagesSignal.default.removeImage(imgId)
}
}
} }
image.src = readerEvent.target?.result as string image.src = readerEvent.target?.result as string
} }

View File

@ -18,6 +18,7 @@ function addImage(data: Omit<ImageCardType, "id">) {
images.value[newCard.id] = newCard images.value[newCard.id] = newCard
images.notify() images.notify()
focusedItem.value = newCard.id focusedItem.value = newCard.id
return newCard.id
} }
function removeImage(id: ImageCardType["id"]) { function removeImage(id: ImageCardType["id"]) {

View File

@ -35,3 +35,29 @@ body {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.toast {
@apply flex flex-col items-center justify-center;
@apply fixed right-4 top-4 w-52 px-4;
@apply transition-all duration-300 rounded z-50;
}
.toast-message {
color: #e5e5e5;
}
.toast.toast-info {
@apply bg-info;
}
.toast.toast-success {
@apply bg-success;
}
.toast.toast-warning {
@apply bg-warning;
}
.toast.toast-error {
@apply bg-error;
}

View File

@ -4,10 +4,12 @@ export default {
theme: { theme: {
extend: { extend: {
colors: { colors: {
primary: "rgb(64 37 121)", info: "#4f46e5",
"primary-light": "rgb(195 177 232)", success: "#267d46",
warning: "#a46319",
error: "#963030",
}, },
}, },
}, },
plugins: [], plugins: [],
}; }