diff --git a/index.html b/index.html index 7690c63..9694cc5 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,8 @@ -
+
+
diff --git a/src/App.tsx b/src/App.tsx index c964f59..46d4d53 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,13 @@ import InfiniteCanvas from "./components/InfinateCanvas" +import { ToastContextProvider } from "./components/Toast" import { useThemeDetector } from "./utils/useThemeDetector" export function App() { useThemeDetector() - return + return ( + + + + ) } diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx new file mode 100644 index 0000000..2779928 --- /dev/null +++ b/src/components/Toast.tsx @@ -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([]) + + 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 ( + + {children} + document.getElementById("toast-root")!}> + {toasts.map((toast, i) => ( + { + if (state === "exited") { + setToasts((prev) => prev.filter((t) => t.ts !== toast.ts)) + } + }} + element={(state) => } + /> + ))} + + + ) +} + +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 ( +
+ {toast.message} +
+ ) + } +) diff --git a/src/components/cardSelector/ImageCardButton.tsx b/src/components/cardSelector/ImageCardButton.tsx index 4958b1b..d5491ef 100644 --- a/src/components/cardSelector/ImageCardButton.tsx +++ b/src/components/cardSelector/ImageCardButton.tsx @@ -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 } }) - 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 } diff --git a/src/signals/images.ts b/src/signals/images.ts index 381b2aa..3fe2f8c 100644 --- a/src/signals/images.ts +++ b/src/signals/images.ts @@ -18,6 +18,7 @@ function addImage(data: Omit) { images.value[newCard.id] = newCard images.notify() focusedItem.value = newCard.id + return newCard.id } function removeImage(id: ImageCardType["id"]) { diff --git a/src/styles.css b/src/styles.css index 2cad54e..ef9368e 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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; +} diff --git a/tailwind.config.js b/tailwind.config.js index ee39c6c..f653b80 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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: [], -}; +}