generated from Klectr/KTemplate
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:
parent
3fd6aef52c
commit
1071743290
@ -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>
|
||||||
|
@ -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
112
src/components/Toast.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
updateLocalStorage("images", images.images.value)
|
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
|
||||||
}
|
}
|
||||||
|
@ -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"]) {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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: [],
|
||||||
};
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user