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>
<body>
<div id="root" />
<div id="toast-root"></div>
<div id="root"></div>
<script type="module" src="/src/main.ts" defer></script>
</body>
</html>

View File

@ -1,8 +1,13 @@
import InfiniteCanvas from "./components/InfinateCanvas"
import { ToastContextProvider } from "./components/Toast"
import { useThemeDetector } from "./utils/useThemeDetector"
export function App() {
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 images from "../../signals/images"
import { updateLocalStorage } from "../../utils/localStorage"
import { useToast } from "../Toast"
import { Tooltip } from "./Tooltip"
import { defaultClassName } from "./utils"
export function ImageCardButton() {
const toast = useToast()
function _handleClick(mouseEvent: MouseEvent) {
const input = document.createElement('input')
input.type = 'file'
@ -30,7 +32,7 @@ export function ImageCardButton() {
if (typeof content == 'string') img = content?.split(':')[1]
if (!img) return
ImagesSignal.default.addImage({
const imgId = ImagesSignal.default.addImage({
type: "image",
title: "New Image",
contents: content as string,
@ -43,7 +45,16 @@ export function ImageCardButton() {
h: normalizedH
}
})
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
}

View File

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

View File

@ -35,3 +35,29 @@ body {
margin: 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: {
extend: {
colors: {
primary: "rgb(64 37 121)",
"primary-light": "rgb(195 177 232)",
info: "#4f46e5",
success: "#267d46",
warning: "#a46319",
error: "#963030",
},
},
},
plugins: [],
};
}