diff --git a/assets/icons/3c73ee8caf56fcc08bd11d595dca167d.png b/assets/icons/3c73ee8caf56fcc08bd11d595dca167d.png new file mode 100644 index 0000000..ad6d0c6 Binary files /dev/null and b/assets/icons/3c73ee8caf56fcc08bd11d595dca167d.png differ diff --git a/assets/icons/IdeaDrawnNewLogo_transparent.png b/assets/icons/IdeaDrawnNewLogo_transparent.png new file mode 100644 index 0000000..6d9a791 Binary files /dev/null and b/assets/icons/IdeaDrawnNewLogo_transparent.png differ diff --git a/assets/icons/check.svg b/assets/icons/check.svg new file mode 100644 index 0000000..ee7888a --- /dev/null +++ b/assets/icons/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/delete.svg b/assets/icons/delete.svg new file mode 100644 index 0000000..0de353f --- /dev/null +++ b/assets/icons/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/down.svg b/assets/icons/down.svg new file mode 100644 index 0000000..003a983 --- /dev/null +++ b/assets/icons/down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/exclamation.png b/assets/icons/exclamation.png new file mode 100644 index 0000000..327868e Binary files /dev/null and b/assets/icons/exclamation.png differ diff --git a/assets/icons/icons_151918.png b/assets/icons/icons_151918.png new file mode 100644 index 0000000..5881f9d Binary files /dev/null and b/assets/icons/icons_151918.png differ diff --git a/assets/icons/icons_19028.png b/assets/icons/icons_19028.png new file mode 100644 index 0000000..f21ed64 Binary files /dev/null and b/assets/icons/icons_19028.png differ diff --git a/assets/icons/icons_417061.png b/assets/icons/icons_417061.png new file mode 100644 index 0000000..7cafcf7 Binary files /dev/null and b/assets/icons/icons_417061.png differ diff --git a/assets/icons/icons_515682.png b/assets/icons/icons_515682.png new file mode 100644 index 0000000..4236cc3 Binary files /dev/null and b/assets/icons/icons_515682.png differ diff --git a/assets/icons/image.png b/assets/icons/image.png new file mode 100644 index 0000000..01c7bf6 Binary files /dev/null and b/assets/icons/image.png differ diff --git a/assets/icons/search_35dp_E8EAED_FILL0_wght700_GRAD200_opsz40.svg b/assets/icons/search_35dp_E8EAED_FILL0_wght700_GRAD200_opsz40.svg new file mode 100644 index 0000000..4b645a8 --- /dev/null +++ b/assets/icons/search_35dp_E8EAED_FILL0_wght700_GRAD200_opsz40.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/Canvas/Canvas.tsx b/components/Canvas/Canvas.tsx index 45e27ec..88da7d9 100644 --- a/components/Canvas/Canvas.tsx +++ b/components/Canvas/Canvas.tsx @@ -6,12 +6,12 @@ import useIndexed from "../../state/hooks/useIndexed"; import useStoreSubscription from "../../state/hooks/useStoreSubscription"; import useLayerReferences from "../../state/hooks/useLayerReferences"; import useStore from "../../state/hooks/useStore"; -import * as Utils from "../../utils"; +import * as Utils from "../../lib/utils"; import clsx from "clsx"; // Types import type { FC, MouseEvent } from "react"; -import type { Layer, Coordinates } from "../../types"; +import type { Layer, Coordinates, CanvasFile } from "../../types"; // Styles import "./Canvas.styles.css"; @@ -24,6 +24,7 @@ type DBLayer = { image: Blob; name: string; position: number; + id: string; }; const Canvas: FC = ({ isGrabbing }) => { @@ -59,8 +60,8 @@ const Canvas: FC = ({ isGrabbing }) => { })) ); - const { references, add, remove } = useLayerReferences(); - const { get } = useIndexed(); + const { references, add, getActiveLayer } = useLayerReferences(); + const { get, remove } = useIndexed(); const isDrawing = useRef(false); const isMovingElement = useStoreSubscription((state) => state.elementMoving); @@ -238,9 +239,62 @@ const Canvas: FC = ({ isGrabbing }) => { // TODO: Improve this implementation of updating the layers from the storage. useEffect(() => { async function updateLayers() { - const entries = await get<[string, DBLayer][]>("layers"); + const urlParams = new URLSearchParams(window.location.search); + const fileId = urlParams.get("f"); + // The open query parameter is used to determine if the file + // was created from opening a file from the local computer. + const open = urlParams.get("open"); + + if (!fileId) { + // Simply do nothing. We want to redirect if there is no file. + // This is handled in the Page component for the editor route. + return; + } + + const entries = await get("layers", fileId); if (!entries) { + // We're in a new file. We have one layer by default, + // so we'll render the opened file with that layer. + + if (!open) { + return; + } + + const file = await get("files", fileId); + + if (!file) { + console.error( + "Tried to get file from temporary storage, there was no file." + ); + } else { + const ref = getActiveLayer(); + const canvasWidth = Number(ref.style.width.replace("px", "")); + const canvasHeight = Number(ref.style.height.replace("px", "")); + const ctx = ref.getContext("2d"); + const img = new Image(); + + img.width = canvasWidth; + img.height = canvasHeight; + + img.onload = () => { + ctx!.drawImage(img, 0, 0, canvasWidth, canvasHeight); + URL.revokeObjectURL(img.src); + + // A custom event to notify that the image of the layer + // displayed in the layer list needs to be updated. + + const ev = new CustomEvent("imageupdate", { + detail: { + layer: ref + } + }); + + document.dispatchEvent(ev); + }; + + img.src = URL.createObjectURL(file.file); + } return; } @@ -248,18 +302,18 @@ const Canvas: FC = ({ isGrabbing }) => { updateLayerContents(newEntries); } - function updateLayerState(entries: [string, DBLayer][]) { - return new Promise<[string, DBLayer][]>((resolve) => { + function updateLayerState(entries: DBLayer[]) { + return new Promise((resolve) => { const newLayers: Layer[] = []; - const sorted = entries.sort((a, b) => b[1].position - a[1].position); // Sort by position, where the highest position is the top layer. + const sorted = entries.sort((a, b) => b.position - a.position); // Sort by position, where the highest position is the top layer. sorted.forEach((entry, i) => { - const [layerId, layer] = entry; + const { name, id } = entry; newLayers.push({ - name: layer.name, - id: layerId, + name: name, + id: id, active: i === 0, hidden: false }); @@ -275,10 +329,10 @@ const Canvas: FC = ({ isGrabbing }) => { }); } - function updateLayerContents(entries: [string, DBLayer][]) { + function updateLayerContents(entries: DBLayer[]) { entries.forEach((entry) => { - const [, layer] = entry; - const canvas = references.current[layer.position]; + const { position, image } = entry; + const canvas = references.current[position]; if (!canvas) return; @@ -300,12 +354,12 @@ const Canvas: FC = ({ isGrabbing }) => { document.dispatchEvent(ev); }; - img.src = URL.createObjectURL(layer.image); + img.src = URL.createObjectURL(image); }); } updateLayers(); - }, [setLayers, get, references]); + }, [setLayers, get, references, getActiveLayer, remove]); useEffect(() => { const refs = references.current; @@ -350,9 +404,9 @@ const Canvas: FC = ({ isGrabbing }) => { transform }} ref={(element) => { - if (element !== null) { - add(element, i); - } + if (element !== null) { + add(element, i); + } }} id={layer.id} width={width * dpi} diff --git a/components/CanvasPane/CanvasPane.tsx b/components/CanvasPane/CanvasPane.tsx index aeb616e..35af2d8 100644 --- a/components/CanvasPane/CanvasPane.tsx +++ b/components/CanvasPane/CanvasPane.tsx @@ -20,14 +20,17 @@ import type { Coordinates } from "../../types"; // Styles import "./CanvasPane.styles.css"; +import ScaleIndicator from "../ScaleIndicator/ScaleIndicator"; const MemoizedShapeElement = memo(ShapeElement); const MemoizedCanvas = memo(Canvas); const MemoizedDrawingToolbar = memo(DrawingToolbar); +const MemoizedScaleIndicator = memo(ScaleIndicator); const CanvasPane: FC = () => { const { mode, + scale, changeX, changeY, increaseScale, @@ -40,6 +43,7 @@ const CanvasPane: FC = () => { } = useStore( useShallow((state) => ({ mode: state.mode, + scale: state.scale, changeX: state.changeX, changeY: state.changeY, increaseScale: state.increaseScale, @@ -340,6 +344,8 @@ const CanvasPane: FC = () => { > + + ); }; diff --git a/components/CanvasPointerMarker/CanvasPointerMarker.tsx b/components/CanvasPointerMarker/CanvasPointerMarker.tsx index 5885a6f..36b0592 100644 --- a/components/CanvasPointerMarker/CanvasPointerMarker.tsx +++ b/components/CanvasPointerMarker/CanvasPointerMarker.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useRef } from "react"; import useStore from "../../state/hooks/useStore"; import useStoreSubscription from "../../state/hooks/useStoreSubscription"; import { useShallow } from "zustand/react/shallow"; -import * as Utils from "../../utils"; +import * as Utils from "../../lib/utils"; // Types import type { FC, RefObject } from "react"; @@ -51,8 +51,7 @@ const CanvasPointerMarker: FC = ({ setIsVisible(false); } - const { x, y, left, top, width, height } = - canvasSpace.getBoundingClientRect(); + const { left, top, width, height } = canvasSpace.getBoundingClientRect(); let newX; let newY; diff --git a/components/CanvasPointerSelection/CanvasPointerSelection.tsx b/components/CanvasPointerSelection/CanvasPointerSelection.tsx index cb96043..afb0daf 100644 --- a/components/CanvasPointerSelection/CanvasPointerSelection.tsx +++ b/components/CanvasPointerSelection/CanvasPointerSelection.tsx @@ -1,6 +1,6 @@ // Lib import { useRef, useEffect, useState } from "react"; -import * as UTILS from "../../utils"; +import * as UTILS from "../../lib/utils"; import useLayerReferences from "../../state/hooks/useLayerReferences"; import useStoreSubscription from "../../state/hooks/useStoreSubscription"; import useStore from "../../state/hooks/useStore"; diff --git a/components/Dropdown/Dropdown.styles.css b/components/Dropdown/Dropdown.styles.css new file mode 100644 index 0000000..79f93cf --- /dev/null +++ b/components/Dropdown/Dropdown.styles.css @@ -0,0 +1,53 @@ +:root { + --description: lightgray; +} + +.desc { + color: var(--description); +} + +.currentFilter { + display: inline-flex; + align-items: center; + justify-content: center; + background: none; + border: none; + cursor: pointer; + padding: 0 10px; +} +.dropdownFilter { + opacity: 0; + display: flex; + align-items: flex-start; + justify-content: center; + flex-direction: column; + z-index: 888; + width: 10rem; + background: rgba(0, 0, 0, 0.9); + border: solid 1px rgba(255, 255, 255, 0.2); + border-radius: 3px; + padding-block: 10px; + padding-inline: 1rem; + padding-inline-end: 0; + position: absolute; + transition: all 0.3s ease; + pointer-events: none; +} + +.filter-item { + width: fit-content; + background: none; + border: none; + padding-block: 5px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} + +.active { + opacity: 1; + transform: translateY(10px); + pointer-events: all; +} diff --git a/components/Dropdown/Dropdown.tsx b/components/Dropdown/Dropdown.tsx new file mode 100644 index 0000000..1ada09f --- /dev/null +++ b/components/Dropdown/Dropdown.tsx @@ -0,0 +1,84 @@ +// Lib +import check from "../../assets/icons/check.svg"; +import down from "../../assets/icons/down.svg"; +import { useState } from "react"; + +// Types +import type { FC, MouseEvent } from "react"; +import { Option } from "../../types"; + +// Styles +import "./Dropdown.styles.css"; + +type DropdownProps = { + description: string; + options: Option[]; + onSelect?: (option: Option) => void; +}; + +const Dropdown: FC = ({ + description = "Default", + options = [], + onSelect +}) => { + const [selected, setSelected] = useState(options[0]); + const [open, setOpen] = useState(false); + + const handleSelect = (e: MouseEvent) => { + const option = options.find( + (option) => option.value === e.currentTarget.id + ); + if (option) { + setSelected(option); + onSelect && onSelect(option); + } + setOpen(false); + }; + + const handleToggle = () => setOpen(!open); + + return ( + + {description}: + + {selected.label} + + + + {options.map((option) => ( + + {selected.value === option.value && ( + + )} + {option.label} + + ))} + + + ); +}; + +export default Dropdown; diff --git a/components/ExportCanvasButton/ExportCanvasButton.tsx b/components/ExportCanvasButton/ExportCanvasButton.tsx index f3ea3e7..3ad23e8 100644 --- a/components/ExportCanvasButton/ExportCanvasButton.tsx +++ b/components/ExportCanvasButton/ExportCanvasButton.tsx @@ -1,7 +1,7 @@ // Lib import { useRef } from "react"; import useLayerReferences from "../../state/hooks/useLayerReferences"; -import * as UTILS from "../../utils"; +import * as UTILS from "../../lib/utils"; // Types import type { FC } from "react"; @@ -15,7 +15,7 @@ const ExportCanvasButton: FC = () => { const elements = document.getElementsByClassName("element"); const blob = await UTILS.generateCanvasImage( - references.current, + references.current, elements, 1, true @@ -52,4 +52,4 @@ const ExportCanvasButton: FC = () => { ); }; -export default ExportCanvasButton; \ No newline at end of file +export default ExportCanvasButton; diff --git a/components/FileCard/FileCard.styles.css b/components/FileCard/FileCard.styles.css new file mode 100644 index 0000000..54b0e9c --- /dev/null +++ b/components/FileCard/FileCard.styles.css @@ -0,0 +1,60 @@ +.file-card { + border-radius: 6px; + border: none; + display: flex; + height: fit-content; + width: 230px; + /* background-color: #232222; */ + display: flex; + flex-direction: column; + background: none; + border-radius: 6px; +} + +.file-preview { + background-color: white; + width: 100%; + height: 125px; + border-radius: 6px; + object-fit: cover; +} + +.file-text { + margin: 12px; + margin-inline: 5px; +} + +.file-title { + text-decoration: none; + width: 100%; + font-weight: 500; + color: white; + white-space: nowrap; + overflow-x: hidden; + text-overflow: ellipsis; + font-size: 1rem; +} + +.interactive-file-options { + display: flex; + justify-content: space-between; + width: 100%; + overflow: hidden; + align-items: center; +} + +.interactive-file-options > button { + border: none; + cursor: pointer; + background: none; + border-radius: 6px; + font-size: large; +} + +.interactive-file-options > button:hover { + background: rgba(255, 255, 255, 0.1); +} + +.file-date { + color: rgba(255, 255, 255, 0.6); +} diff --git a/components/FileCard/FileCard.tsx b/components/FileCard/FileCard.tsx new file mode 100644 index 0000000..c953c93 --- /dev/null +++ b/components/FileCard/FileCard.tsx @@ -0,0 +1,157 @@ +// Lib +import { useRef, useState } from "react"; +import useIndexed from "../../state/hooks/useIndexed"; + +// Types +import type { FC, MouseEvent } from "react"; +import type { CanvasFile } from "../../types"; + +// Styles +import "./FileCard.styles.css"; + +// Components +import { Menu, MenuItem } from "@mui/material"; +import KebabMenu from "../icons/KebabMenu/KebabMenu"; + +type ShownFile = { + id: string; + file: CanvasFile; +}; + +type FileCardProps = { + id: string; + file: CanvasFile; + setCanvases: React.Dispatch>; +}; + +const FileCard: FC = ({ file, id, setCanvases }) => { + const fileURL = useRef(URL.createObjectURL(file.file)); + const [menuAnchorEl, setMenuAnchorEl] = useState( + null + ); + const { set } = useIndexed(); + const date = new Date(file.file.lastModified ?? 0); + const stringDate = date.toLocaleDateString(); + const name = file.file.name ?? "Untitled"; + const menuOpen = Boolean(menuAnchorEl); + + const handleMenuOpen = (e: MouseEvent) => { + setMenuAnchorEl(e.currentTarget as HTMLButtonElement); + }; + + const handleMenuClose = () => { + setMenuAnchorEl(null); + }; + + const onImageLoad = () => { + URL.revokeObjectURL(fileURL.current); + }; + + const handleAction = async () => { + handleMenuClose(); + const question = file.archived + ? "Are you sure you want to restore this file?" + : "Are you sure you want to archive this file? You can always restore it later."; + const confirmArchive = window.confirm(question); + + if (confirmArchive) { + // Toggle the archive status of the file. + const newFile = { + file: file.file, + archived: !file.archived, + archivedDate: file.archived ? null : Date.now() + }; + await set("files", id, newFile); + + // Update the canvases to reflect the change. + + setCanvases((canvases) => { + const index = canvases.findIndex((canvas) => canvas.id === id); + const newCanvases = [...canvases]; + newCanvases[index] = { + id, + file: newFile + }; + return newCanvases; + }); + } + }; + + const imageToDisplay = file.archived ? ( + + ) : ( + + + + ); + + const nameToDisplay = file.archived ? ( + {name} + ) : ( + + {name} + + ); + + return ( + + {imageToDisplay} + + + {nameToDisplay} + + + + + + + Last Updated: {stringDate} + + + + + + {file.archived ? "Restore" : "Archive"} File + + + + ); +}; + +export default FileCard; diff --git a/components/FileTemplatePreview/FileTemplatePreview.styles.css b/components/FileTemplatePreview/FileTemplatePreview.styles.css new file mode 100644 index 0000000..cc9cc79 --- /dev/null +++ b/components/FileTemplatePreview/FileTemplatePreview.styles.css @@ -0,0 +1,5 @@ +.file-template-preview { + border: 1px solid #ccc; + border-radius: 5px; + margin: 25px; +} diff --git a/components/FileTemplatePreview/FileTemplatePreview.tsx b/components/FileTemplatePreview/FileTemplatePreview.tsx new file mode 100644 index 0000000..1cd93cd --- /dev/null +++ b/components/FileTemplatePreview/FileTemplatePreview.tsx @@ -0,0 +1,38 @@ +// Styles +import "./FileTemplatePreview.styles.css"; + +// Types +import type { CSSProperties, FC } from "react"; + +type FileTemplatePreviewProps = { + width: number; + height: number; +}; + +const FileTemplatePreview: FC = ({ + width, + height +}) => { + const styles: CSSProperties = { + width, + height + }; + + return ( + + + + Template Preview + + + {width} x {height} + + + + ); +}; + +export default FileTemplatePreview; diff --git a/components/IndexedDBProvider/IndexedDBProvider.tsx b/components/IndexedDBProvider/IndexedDBProvider.tsx index 9febaed..b855e31 100644 --- a/components/IndexedDBProvider/IndexedDBProvider.tsx +++ b/components/IndexedDBProvider/IndexedDBProvider.tsx @@ -4,7 +4,7 @@ import { useEffect, createContext, useRef, useCallback, useMemo } from "react"; // Types import type { FC, PropsWithChildren } from "react"; -const STORES = ["layers", "elements"]; +const STORES = ["layers", "elements", "files"]; const VERSION = 1; // Bump this up when the schema changes. type IndexedUtils = { @@ -23,7 +23,7 @@ type IndexedUtils = { * @param value The value to set. * @returns A promise that resolves when the data is set. */ - set: (store: string, key: string, value: unknown) => Promise; + set: (store: string, key: string, value: T) => Promise; /** * Remove data from the database. @@ -112,7 +112,7 @@ export const IndexedDBProvider: FC = ({ children }) => { ); const set = useCallback( - async (store: string, key: string, value: unknown) => { + async (store: string, key: string | undefined, value: T) => { const db = database.current ?? (await openDatabase()); return new Promise((resolve, reject) => { diff --git a/components/LayerInfo/LayerInfo.styles.css b/components/LayerInfo/LayerInfo.styles.css index 7522ada..d40254f 100644 --- a/components/LayerInfo/LayerInfo.styles.css +++ b/components/LayerInfo/LayerInfo.styles.css @@ -18,7 +18,6 @@ .layer-info-name { color: white; font-size: 1em; - font-weight: bold; margin: 0 10px; white-space: nowrap; overflow: hidden; diff --git a/components/LayerPreview/LayerPreview.tsx b/components/LayerPreview/LayerPreview.tsx index 400781f..b5695bb 100644 --- a/components/LayerPreview/LayerPreview.tsx +++ b/components/LayerPreview/LayerPreview.tsx @@ -1,6 +1,6 @@ // Lib import { useState, useEffect } from "react"; -import * as Utils from "../../utils"; +import * as Utils from "../../lib/utils"; // Styles import "./LayerPreview.styles.css"; diff --git a/components/LeftToolbar/LeftToolbar.styles.css b/components/LeftToolbar/LeftToolbar.styles.css index f1cebc3..afafae0 100644 --- a/components/LeftToolbar/LeftToolbar.styles.css +++ b/components/LeftToolbar/LeftToolbar.styles.css @@ -1,8 +1,7 @@ #left-toolbar-container { display: flex; flex-direction: column; - width: 3vw; - min-width: 50px; + width: 50px; height: 100%; overflow: auto; background-color: rgb(36, 36, 36); @@ -12,7 +11,7 @@ .toolbar-option { width: 100%; - min-height: 4vh; + min-height: 3.5vh; border: 1px solid rgb(36, 36, 36); padding: 0.1em; font-size: 1.6em; diff --git a/components/Main/Main.styles.css b/components/Main/Main.styles.css index 1968c4e..e97ab7f 100644 --- a/components/Main/Main.styles.css +++ b/components/Main/Main.styles.css @@ -5,7 +5,7 @@ Be sure to change this value if the navbar height changes. Likewise, if this value changes, be sure to change the height of the navbar. */ - height: calc(100vh - 4rem); + height: calc(100vh - 3rem); } /* #right-side-pane { diff --git a/components/Main/Main.tsx b/components/Main/Main.tsx index 50f7858..61d26f5 100644 --- a/components/Main/Main.tsx +++ b/components/Main/Main.tsx @@ -21,7 +21,15 @@ const Main: FC = () => { useEffect(() => { async function getElements() { - const elements = await get("elements", "items"); + const urlParams = new URLSearchParams(window.location.search); + const fileId = urlParams.get("f"); + + if (!fileId) { + // Do nothing. + return; + } + + const elements = await get("elements", fileId); setElements(elements ?? []); } diff --git a/components/Navbar/Navbar.styles.css b/components/Navbar/Navbar.styles.css index feee7e4..5fd10a5 100644 --- a/components/Navbar/Navbar.styles.css +++ b/components/Navbar/Navbar.styles.css @@ -2,13 +2,15 @@ display: flex; justify-content: space-between; align-items: center; - padding: 0.5rem; - height: 4rem; + padding: 0.2rem; + min-height: 3rem; + height: 3rem; border-bottom: 1px solid #d1836a; width: 100%; } -#navbar-links-section { +#navbar-links-section, +#navbar-buttons-section { display: flex; flex-direction: row; align-items: center; @@ -24,15 +26,14 @@ #navbar-logo { width: 3rem; height: 3rem; - margin-right: 1rem; - border-radius: 50%; + margin-right: 0.5rem; } #navbar-logo:hover { cursor: pointer; /* Add a light shadow to the logo */ - box-shadow: 0 0 5px rgba(226, 226, 226, 0.1); + /* box-shadow: 0 0 5px rgba(226, 226, 226, 0.1); */ } #navbar-links { @@ -42,7 +43,7 @@ #navbar-links > button { margin-right: 1rem; - font-size: 1.2em; + font-size: 1.1em; font-weight: 500; color: #fdfdfd; border-bottom: 2px solid transparent; @@ -67,7 +68,7 @@ border-radius: 8px; border: 1px solid white; color: white; - padding: 0.6em 1.2em; + padding: 0.2em 0.8em; margin: 0 0.2rem; font-size: 1em; font-weight: 500; diff --git a/components/Navbar/Navbar.tsx b/components/Navbar/Navbar.tsx index 9f24ace..fe583e0 100644 --- a/components/Navbar/Navbar.tsx +++ b/components/Navbar/Navbar.tsx @@ -1,10 +1,10 @@ // Lib -import logo from "../../assets/icons/IdeaDrawnNewLogo_invert.png"; -import { Snackbar } from "@mui/material"; +import logo from "../../assets/icons/IdeaDrawnNewLogo_transparent.png"; import { useState } from "react"; +import useIndexed from "../../state/hooks/useIndexed"; // Types -import type { FC } from "react"; +import type { FC, MouseEvent } from "react"; // Styles import "./Navbar.styles.css"; @@ -12,9 +12,24 @@ import "./Navbar.styles.css"; // Components import ExportCanvasButton from "../ExportCanvasButton/ExportCanvasButton"; import SaveCanvasButton from "../SaveCanvasButton/SaveCanvasButton"; +import { Menu, MenuItem, Snackbar, Fade } from "@mui/material"; +import { CanvasFile } from "../../types"; const Navbar: FC = () => { const [snackbarOpen, setSnackbarOpen] = useState(false); + const [menuAnchorEl, setMenuAnchorEl] = useState( + null + ); + const { set, get } = useIndexed(); + const menuOpen = Boolean(menuAnchorEl); + + const handleMenuOpen = (e: MouseEvent) => { + setMenuAnchorEl(e.currentTarget as HTMLButtonElement); + }; + + const handleMenuClose = () => { + setMenuAnchorEl(null); + }; const openSnackbar = () => { setSnackbarOpen(true); @@ -24,23 +39,101 @@ const Navbar: FC = () => { setSnackbarOpen(false); }; + const menuTabs = ["File", "Edit", "View", "Filter", "Admin"]; + + type MenuOptions = { + [key: string]: { + text: string; + action: () => void; + }[]; + }; + + const menuOptions: MenuOptions = { + File: [ + { + text: "Rename File", + action: async () => { + const urlParams = new URLSearchParams(window.location.search); + const fileID = urlParams.get("f"); + + if (!fileID) { + throw new Error("No file ID found in URL."); + } + + const file = await get("files", fileID); + + if (!file) { + throw new Error("File not found."); + } + + const newName = window.prompt("Enter new file name", file.file.name); + + if (!newName) { + return; + } + + if (newName.trim().length === 0) { + return; + } + + await set("files", fileID, new File([file.file.name], newName)); + } + } + ] + }; + return ( - + - + - File - Edit - View - Filter - Admin + {menuTabs.map((tab) => ( + + {tab} + + ))} + + + {menuAnchorEl && + menuOptions[menuAnchorEl.name].map((option) => ( + { + option.action(); + handleMenuClose(); + }} + > + {option.text} + + ))} + diff --git a/components/SaveCanvasButton/SaveCanvasButton.tsx b/components/SaveCanvasButton/SaveCanvasButton.tsx index 2468084..8daa6d4 100644 --- a/components/SaveCanvasButton/SaveCanvasButton.tsx +++ b/components/SaveCanvasButton/SaveCanvasButton.tsx @@ -3,10 +3,11 @@ import { useState, useRef, useEffect, useCallback } from "react"; import useIndexed from "../../state/hooks/useIndexed"; import useLayerReferences from "../../state/hooks/useLayerReferences"; import useStoreSubscription from "../../state/hooks/useStoreSubscription"; +import { generateCanvasImage } from "../../lib/utils"; // Types import type { FC } from "react"; -import type { CanvasElement } from "../../types"; +import type { CanvasElement, CanvasFile } from "../../types"; // Icons import FloppyDisk from "../icons/FloppyDisk/FloppyDisk"; @@ -19,30 +20,40 @@ const SaveCanvasButton: FC = () => { const timeout = useRef | undefined>(undefined); const elements = useStoreSubscription((state) => state.elements); const { references, remove } = useLayerReferences(); - const { set } = useIndexed(); + const { set, get } = useIndexed(); const saveCanvas = useCallback(async () => { if (!references.current.length) throw new Error( "Cannot export canvas: no references found. This is a bug." ); - // const elements = Array.from(document.getElementsByClassName("element")); - references.current.forEach((canvas, index) => { + const urlParams = new URLSearchParams(window.location.search); + const fileId = urlParams.get("f"); + + if (!fileId) { + throw new Error("No file ID found in the URL. This is a bug."); + } + + const canvasAsJSON = references.current.map((canvas, index) => { if (canvas === null) { remove(index); return; } - canvas.toBlob(async (blob) => { - if (!blob) { - throw new Error( - `Failed to save canvas with id: ${canvas.id} and name: ${canvas.getAttribute("data-name")}.` - ); - } - - await set("layers", canvas.id, { - name: canvas.getAttribute("data-name"), - image: blob, - position: index + + return new Promise((resolve) => { + canvas.toBlob(async (blob) => { + if (!blob) { + throw new Error( + `Failed to save canvas with id: ${canvas.id} and name: ${canvas.getAttribute("data-name")}.` + ); + } + + resolve({ + name: canvas.getAttribute("data-name"), + image: blob, + position: index, + id: canvas.id + }); }); }); }); @@ -56,7 +67,30 @@ const SaveCanvasButton: FC = () => { } ); - await set("elements", "items", allUnfocused); + const layers = await Promise.all(canvasAsJSON); + const elementsHTML = document.getElementsByClassName("elements"); + + const fullImage = await generateCanvasImage( + references.current, + elementsHTML, + 0.8 + ); + + const oldFile = await get("files", fileId); + + if (!oldFile) { + throw new Error("No file found with the given id."); + } + + await set("files", fileId, { + ...oldFile, + file: new File([fullImage], oldFile.file.name, { + type: "image/png", + lastModified: Date.now() + }) + }); + await set("layers", fileId, layers); + await set("elements", fileId, allUnfocused); // Update the UI to indicate that the canvas has been saved setSaved(true); @@ -64,7 +98,7 @@ const SaveCanvasButton: FC = () => { timeout.current = setTimeout(() => { setSaved(false); }, 1000); - }, [references, set, remove, elements]); + }, [references, set, remove, elements, get]); useEffect(() => { function handleKeyboardSave(e: KeyboardEvent) { diff --git a/components/ScaleIndicator/ScaleIndicator.tsx b/components/ScaleIndicator/ScaleIndicator.tsx new file mode 100644 index 0000000..dd0d7ab --- /dev/null +++ b/components/ScaleIndicator/ScaleIndicator.tsx @@ -0,0 +1,47 @@ +// Lib +import { useRef, useState, useEffect } from "react"; + +// Types +import { FC } from "react"; + +type ScaleIndicatorProps = { + scale: number; +}; + +const ScaleIndicator: FC = ({ scale }) => { + const timeoutRef = useRef>(null); + const [visible, setVisible] = useState(false); + + useEffect(() => { + setVisible(true); + timeoutRef.current = setTimeout(() => setVisible(false), 2000); + + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + }, [scale]); + + return ( + + {scale.toFixed(1)}x + + ); +}; + +export default ScaleIndicator; diff --git a/components/ShapeOption/ShapeOption.tsx b/components/ShapeOption/ShapeOption.tsx index 28cdca6..79bc1a0 100644 --- a/components/ShapeOption/ShapeOption.tsx +++ b/components/ShapeOption/ShapeOption.tsx @@ -1,6 +1,6 @@ // Lib import useLayerReferences from "../../state/hooks/useLayerReferences"; -import * as Utils from "../../utils"; +import * as Utils from "../../lib/utils"; // Types import type { Shape } from "../../types"; @@ -15,7 +15,7 @@ const ShapeOption: FC<{ icon: string; name: Shape }> = ({ icon, name }) => { const { getActiveLayer } = useLayerReferences(); const handleShapeChange = () => { - const activeLayer = getActiveLayer(); + const activeLayer = getActiveLayer(); if (!activeLayer) { throw new Error("No active layer found. Cannot create element."); diff --git a/components/ToolbarButton/ToolbarButton.tsx b/components/ToolbarButton/ToolbarButton.tsx index 5d4df36..49318aa 100644 --- a/components/ToolbarButton/ToolbarButton.tsx +++ b/components/ToolbarButton/ToolbarButton.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect } from "react"; import useStore from "../../state/hooks/useStore"; import { useShallow } from "zustand/react/shallow"; -import * as UTILS from "../../utils"; +import * as UTILS from "../../lib/utils"; // Types import type { Mode, ToolbarMode } from "../../types"; diff --git a/components/icons/KebabMenu/KebabMenu.tsx b/components/icons/KebabMenu/KebabMenu.tsx new file mode 100644 index 0000000..c7f5c13 --- /dev/null +++ b/components/icons/KebabMenu/KebabMenu.tsx @@ -0,0 +1,36 @@ +const KebabMenu = () => ( + + + + + +); + +export default KebabMenu; diff --git a/utils.ts b/lib/utils.ts similarity index 98% rename from utils.ts rename to lib/utils.ts index e7df460..446ab49 100644 --- a/utils.ts +++ b/lib/utils.ts @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from "uuid"; -import type { Layer, Coordinates, CanvasElementType } from "./types"; +import type { Layer, Coordinates, CanvasElementType } from "../types"; type CapitalizeOptions = { titleCase: boolean; @@ -230,7 +230,7 @@ async function generateCanvasImage( const referenceLayer = layers[0]; const { width, height } = referenceLayer; const dpi = Number(referenceLayer.getAttribute("data-dpi")); - const scale = Number(referenceLayer.getAttribute("data-scale")); + // const scale = Number(referenceLayer.getAttribute("data-scale")); if (!dpi) { throw new Error("Failed to get DPI from canvas when attempting to export."); diff --git a/pages/index/index.page.css b/pages/editor/index.page.css similarity index 100% rename from pages/index/index.page.css rename to pages/editor/index.page.css diff --git a/pages/editor/index.page.tsx b/pages/editor/index.page.tsx new file mode 100644 index 0000000..fbd04dd --- /dev/null +++ b/pages/editor/index.page.tsx @@ -0,0 +1,65 @@ +"use client"; + +// Lib +import { useEffect } from "react"; +import { navigateTo } from "../../lib/utils"; + +// Components +import Navbar from "../../components/Navbar/Navbar"; +import Main from "../../components/Main/Main"; + +// The tags +// eslint-disable-next-line +export const documentProps = { + title: "IdeaDrawn", // + desc: "A drawing canvas editor on the browser" // +}; + +function Page() { + useEffect(() => { + async function checkStoragePersistency() { + if (!navigator.storage || !navigator.storage.persist) return; + + const isPersisted = await navigator.storage.persisted(); + + if (!isPersisted) { + const isPersisting = await navigator.storage.persist(); + + if (isPersisting) { + console.log("Storage is now persisted."); + } else { + console.error("Storage was not persisted."); + } + } else { + console.log("Storage is already persisted."); + } + } + + function getFileID() { + const urlParams = new URLSearchParams(window.location.search); + const fileId = urlParams.get("f"); // Represents the file's ID. + + if (!fileId) { + // Redirect to the dashboard page. + navigateTo("/home"); + } else { + // Get the file from IndexedDB. + // ... + } + } + + // First, check if the storage is persisted. + // Then, get the file ID from the URL. + checkStoragePersistency().then(() => getFileID()); + }, []); + + return ( + <> + + + + > + ); +} + +export { Page }; diff --git a/pages/home/index.page.tsx b/pages/home/index.page.tsx new file mode 100644 index 0000000..503cedb --- /dev/null +++ b/pages/home/index.page.tsx @@ -0,0 +1,332 @@ +// Lib +import search_logo from "../../assets/icons/search_35dp_E8EAED_FILL0_wght700_GRAD200_opsz40.svg"; +import logo from "../../assets/icons/IdeaDrawnNewLogo.png"; +import new_file_logo from "../../assets/icons/icons_19028.png"; +import open_file_logo from "../../assets/icons/icons_515682.png"; +import shared_file_logo from "../../assets/icons/icons_151918.png"; +import archived_logo from "../../assets/icons/icons_417061.png"; +import exclamation from "../../assets/icons/exclamation.png"; +import del from "../../assets/icons/delete.svg"; +import { useEffect, useRef, useState, memo } from "react"; +import { navigateTo } from "../../lib/utils"; +import { v4 as uuidv4 } from "uuid"; +import useIndexed from "../../state/hooks/useIndexed"; + +// Types +import type { FC } from "react"; +import type { Option, CanvasFile } from "../../types"; + +// Styles +import "./index.styles.css"; +import Dropdown from "../../components/Dropdown/Dropdown"; +import FileCard from "../../components/FileCard/FileCard"; + +export { Page }; + +const MemoizedFileCard = memo(FileCard); + +const FILTER_OPTIONS: Option[] = [ + { + label: "All Files", + value: "all" + }, + { + label: "Alphabetical (A-Z)", + value: "a-z" + }, + { + label: "Alphabetical (Z-A)", + value: "z-a" + }, + { + label: "Last Active", + value: "lastActive" + }, + { + label: "Date Created", + value: "dateCreated" + } +]; + +const BYTES_PER_MEGABYTE = 1_048_576; +const MAX_MEGABYTES = 500; +const MAX_BYTES_SIZE = BYTES_PER_MEGABYTE * MAX_MEGABYTES; + +type ShownFile = { + id: string; + file: CanvasFile; +}; + +const Page: FC = () => { + const [usedBytes, setUsedBytes] = useState(0); + const [canvases, setCanvases] = useState([]); + const [currentPage, setCurrentPage] = useState<"recents" | "archived">( + "recents" + ); + const { set, get, remove } = useIndexed(); + const fileDialogRef = useRef(null); + const barWidth = `${((usedBytes / MAX_BYTES_SIZE) * 100).toFixed(1)}%`; + const usedMegaBytes = (usedBytes / BYTES_PER_MEGABYTE).toFixed(1); + const pageTitle = currentPage === "recents" ? "Recent Projects" : "Archived"; + const filesToDisplay = canvases.filter((canvas) => { + const isArchivePage = currentPage === "archived"; + + return isArchivePage === canvas.file.archived; + }); + + const onFileSelect = async (e: React.ChangeEvent) => { + e.preventDefault(); + const file = e.target.files?.[0]; + + if (!file) { + throw new Error("No file was uploaded when opening a file."); + } + + if (!file.name.match(/\*?.(png|jpg|jpeg)/)) { + alert("Invalid file type."); + return; + } + + if (usedBytes + file.size > MAX_BYTES_SIZE) { + // Check if the file size is within the limit. + // If not, alert the user and return. + alert("File size limit reached."); + return; + } + + const fileId = uuidv4(); + + await set("files", fileId, { + archived: false, + archiveDate: null, + file: new File([file], file.name, { + type: file.type, + lastModified: Date.now() + }) + }); + navigateTo(`/editor?f=${fileId}&open=1`); + }; + + // Get the files to display as well as get + // the current size accumulated by all files. + useEffect(() => { + async function getFilesAndSize() { + const files = await get<[string, CanvasFile][]>("files"); + + if (!files) { + console.error("Cant update size count."); + } else { + let total = 0; + const canvases = files + .map(([id, file]) => { + total += file.file.size; + return { id, file }; + }) + .sort((a, b) => b.file.file.lastModified - a.file.file.lastModified); + + setUsedBytes(total); + setCanvases(canvases); + } + } + + // Update the page title. + const urlParams = new URLSearchParams(window.location.search); + const page = urlParams.get("p"); + + setCurrentPage(page === "archived" ? "archived" : "recents"); + + // Get the files and their sizes. + getFilesAndSize(); + }, [get]); + + const handleClearArchive = async () => { + const confirm = window.confirm( + "Are you sure you want to clear the archive?" + ); + + if (confirm) { + const archivedItems = canvases.filter((canvas) => canvas.file.archived); + + // Remove all archived items. + const promises = archivedItems.map((canvas) => { + return remove("files", canvas.id); + }); + + // Wait for all promises to resolve. + await Promise.all(promises); + + // Update byte count and canvases. + const archivedBytes = archivedItems.reduce((acc, canvas) => { + return acc + canvas.file.file.size; + }, 0); + + setUsedBytes(prev => prev - archivedBytes); + setCanvases(canvases.filter((canvas) => !canvas.file.archived)); + } + }; + + const ArchivedActions = ( + <> + + + Archived items will be automatically deleted in 30 days. + + + + + + Empty Archive + + + > + ); + + return ( + <> + + + + IdeaDrawn + + + DrawnSpace + + + + + + + + + + + + + + New File + + fileDialogRef.current?.click()} + > + + Open File + + + alert("Share files in the full version of IdeaDrawn!") + } + className="sidebarItem" + > + + Shared File + + + + Archived + + + + + + + + {usedMegaBytes} MB of {MAX_MEGABYTES} MB Used + + + Upgrade to Premium + + + + v.0.0.1 + © {new Date().getUTCFullYear()} IdeaDrawn + + + + {pageTitle} + {currentPage === "archived" && ArchivedActions} + + {filesToDisplay.length > 0 ? ( + filesToDisplay.map((canvas, i) => ( + + )) + ) : ( + No files found. + )} + + + + + {/* This is to open the file dialog. We hide it so that it's not visible. */} + + > + ); +}; diff --git a/pages/home/index.styles.css b/pages/home/index.styles.css new file mode 100644 index 0000000..c4d7954 --- /dev/null +++ b/pages/home/index.styles.css @@ -0,0 +1,407 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); + +* { + margin: 0; + padding: 0; + font-family: "Inter", sans-serif; + color-scheme: dark; + --navSize: 50px; + --color: #9b604e; + --color2: #d18970; + --content-margin-top: 90px; + --title2-height: 20px; + --days-left-height: 40px; + outline: none; +} + +::selection { + color: white; + background: var(--color2); +} +body { + background-color: #0f0f0f; + height: 100dvh; +} + +.navbar, +.sidebar { + background: #000000; +} + +.navbar { + display: grid; + align-items: center; + justify-content: center; + grid-template-columns: repeat(22, 1fr); + position: fixed; + width: 100%; + grid-template-rows: var(--navSize); + height: var(--navSize); +} + +.search { + background: #131313; + grid-column: 10 / span 5; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + border: solid 1px rgba(255, 255, 255, 0.2); + width: 100%; + height: 30px; +} +.searchText::placeholder { + color: var(--color2); +} + +.searchText { + width: 100%; + height: 100%; + border: none; + background: none; +} +.searchbtn { + background: none; + border: none; + width: 40px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.searchbtn img { + width: 18px; +} + +.con { + width: 100%; + height: 93%; + display: flex; + gap: 2.2%; + padding-top: var(--navSize); +} + +.sidebar { + width: 200px; + height: calc(100vh - var(--navSize)); + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.sidebarItem { + display: flex; + text-decoration: none; + align-items: center; + justify-content: start; + color: white; + border: none; + background-color: transparent; + gap: 10px; + cursor: pointer; + margin-left: 25px; +} + +.sidebarItemText { + display: flex; + align-items: start; + justify-content: center; + flex-direction: column; +} + +.icons { + filter: invert(1); + width: 29px; +} + +.sidebarBtns { + display: flex; + flex-direction: column; + margin-top: 80px; + width: 100%; + gap: 15px; + width: 100%; +} +.sidebarItemText::after { + display: block; + content: ""; + background: var(--color); + width: 0; + height: 3px; + transition: all 0.3s ease; + border-radius: 10000px; + margin-top: 2px; +} + +.sidebarItemText { + transition: all 0.3s ease; + font-size: 1rem; +} + +.sidebarItemText:hover::after { + width: 60%; +} + +.copyright { + margin-top: auto; + margin-bottom: 10px; + font-size: 0.75rem; + text-align: center; + color: rgba(255, 255, 255, 0.3); +} + +.logo { + display: flex; + align-items: center; + justify-content: center; + grid-column: 1; + margin-left: 22px; + gap: 10px; +} +.logo h1 { + font-weight: 600; + font-size: 1.1rem; +} +.logo img { + width: 30px; + height: 70px; + object-fit: cover; +} + +.drawnspace { + grid-column: 2 / span 4; + color: white; + font-size: 1rem; + margin-left: 54px; + text-decoration: none; + transition: all 0.3s ease; +} +.drawnspace:hover { + color: var(--color); +} + +.accounts { + display: flex; + align-items: center; + justify-content: center; + grid-column: 20; +} + +.content { + margin-top: var(--content-margin-top); + font-size: 0.9rem; + width: 100%; +} + +.files-container { + width: 98%; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 25px; + margin-top: 20px; + overflow-y: auto; + /* Subtracting 20px from the height to account for the margin-top */ + max-height: calc( + 100vh - var(--content-margin-top) - var(--title2-height) - var(--navSize) - + 20px + ); +} + +.title, +.title2 { + display: flex; + flex-direction: column; + text-transform: capitalize; + font-weight: 500; + font-size: 1.1rem; + height: var(--title2-height); +} +.title2 { + margin-top: 0; +} + +.title::after { + display: flex; + content: ""; + background: var(--color); + width: 8%; + height: 3px; + transition: all 0.3s ease; + border-radius: 10000px; + margin-top: 2px; +} + +.storage-con { + display: flex; + align-items: center; + justify-content: center; + margin-top: 60px; + flex-direction: column; + font-size: 0.9rem; + scale: 0.9; +} + +.amountLeft { + font-weight: 500; +} +.upgrade { + display: flex; + align-items: center; + justify-content: center; + /* color: var(--color2); */ + /* border: 1px solid rgba(255, 255, 255, 0.3); */ + border-radius: 9px; + width: 170px; + height: 35px; + margin-top: 13px; + font-weight: 500; + text-decoration: none; + transition: all 0.3s ease; + color: white; + background-color: var(--color2); +} +/* +.upgrade:hover { +} */ + +.progress { + width: 160px; + margin-bottom: 10px; + background: rgba(255, 255, 255, 0.3); + display: flex; + height: 2px; + border-radius: 10000px; +} + +.bar { + width: 50%; + background: var(--color2); + height: 100%; + border-radius: 10000px; +} + +.cta img { + filter: invert(1); + width: 31px; +} +.accounts { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + grid-column: 20 / span 3; +} + +.notification { + scale: 1.1; +} + +.sortby { + color: rgba(255, 255, 255, 0.6); + margin-top: 10px; + display: flex; + align-items: center; + justify-content: center; + width: fit-content; + gap: 10px; +} + +.filter-item[aria-valuetext="active"] > .check { + opacity: 1; +} + +.check { + opacity: 0; +} + +.empty { + background: none; + border: none; + display: flex; + align-items: center; + justify-content: center; + margin-left: auto; + margin-right: 20px; + gap: 5px; + cursor: pointer; + border-radius: 8px; + padding: 5px 10px; + transition: all 0.3s ease; + border: solid 2px transparent; +} + +.daysLeft { + width: 98%; + padding: 12px 0; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + height: var(--days-left-height); + gap: 10px; + opacity: 0.6; + border: 1px solid #d188708a; + background-color: #d1887030; + margin-top: 20px; +} + +.con2 { + display: flex; + align-items: center; + justify-content: center; + padding-top: 0; + margin-top: 10px; +} + +.flip { + filter: invert(1); +} + +.newFileCon { + width: 100%; + height: 100dvh; + position: fixed; + background-color: rgba(0, 0, 0, 0.2); + backdrop-filter: blur(10px); + z-index: 20; + display: flex; + align-items: center; + justify-content: center; +} +.newFile { + display: flex; + flex-direction: column; + border-radius: 5px; + background-color: #000000; + display: flex; + padding: 20px; + gap: 15px; +} +.newFileInput { + width: 50px; +} +.nameInput { + width: 150px; +} + +@media (max-width: 1200px) { + .navbar { + display: flex; + } + .search { + width: 30%; + margin-inline: auto; + } + + .drawnspace { + grid-column: 2 / span 2; + } + .logo { + margin-right: 45px; + } + .accounts { + grid-column: 9 / span 3; + } +} diff --git a/pages/index/index.page.tsx b/pages/index/index.page.tsx index 77c7bd4..9b9a07d 100644 --- a/pages/index/index.page.tsx +++ b/pages/index/index.page.tsx @@ -1,50 +1,13 @@ -"use client"; - // Lib import { useEffect } from "react"; +import { navigateTo } from "../../lib/utils"; -// Components -import Navbar from "../../components/Navbar/Navbar"; -import Main from "../../components/Main/Main"; - -// The tags -// eslint-disable-next-line -export const documentProps = { - title: "IdeaDrawn", // - desc: "A drawing canvas editor on the browser" // -}; +export { Page }; -function Page() { +const Page = () => { useEffect(() => { - async function checkStoragePersistency() { - if (!navigator.storage || !navigator.storage.persist) return; - - const isPersisted = await navigator.storage.persisted(); - - if (!isPersisted) { - const isPersisting = await navigator.storage.persist(); - - if (isPersisting) { - console.log("Storage is now persisted."); - } else { - console.error("Storage was not persisted."); - } - } else { - console.log("Storage is already persisted."); - } - } - - // Check if the database persists. - checkStoragePersistency(); + navigateTo("/home"); }, []); - return ( - <> - - - - > - ); -} - -export { Page }; + return null; +}; diff --git a/renderer/_default.page.server.tsx b/renderer/_default.page.server.tsx index 927a9a9..9c292c5 100644 --- a/renderer/_default.page.server.tsx +++ b/renderer/_default.page.server.tsx @@ -4,7 +4,7 @@ export const passToClient = ["pageProps", "urlPathname"]; import { PageShell } from "./PageShell"; import { escapeInject, dangerouslySkipEscape } from "vite-plugin-ssr/server"; -import logo from "../assets/icons/IdeaDrawnNewLogo.png"; +import logo from "../assets/icons/IdeaDrawnNewLogo_transparent.png"; import type { PageContextServer } from "./types"; import { renderToStream } from "react-streaming/server"; import { initializeStore } from "../state/store"; diff --git a/state/slices/canvasSlice.ts b/state/slices/canvasSlice.ts index 2747ec1..480f5da 100644 --- a/state/slices/canvasSlice.ts +++ b/state/slices/canvasSlice.ts @@ -8,7 +8,7 @@ import type { Dimensions, CanvasStore } from "../../types"; -import * as Utils from "../../utils"; +import * as Utils from "../../lib/utils"; export const createCanvasSlice: StateCreator< CanvasStore, diff --git a/tests/integration/LeftToolbar.test.tsx b/tests/integration/LeftToolbar.test.tsx index e77e847..6122b4c 100644 --- a/tests/integration/LeftToolbar.test.tsx +++ b/tests/integration/LeftToolbar.test.tsx @@ -3,7 +3,7 @@ import { expect, it, describe, beforeEach, afterEach, vi } from "vitest"; import { fireEvent, screen } from "@testing-library/react"; import { renderWithProviders } from "../test-utils"; import { MODES } from "../../state/store"; -import * as Utils from "../../utils"; +import * as Utils from "../../lib/utils"; // Components import LeftToolbar from "../../components/LeftToolbar/LeftToolbar"; diff --git a/tests/integration/Main.test.tsx b/tests/integration/Main.test.tsx index 2a253a6..561a445 100644 --- a/tests/integration/Main.test.tsx +++ b/tests/integration/Main.test.tsx @@ -10,7 +10,7 @@ import { } from "vitest"; import { screen, fireEvent, act } from "@testing-library/react"; import { renderWithProviders } from "../test-utils"; -import * as Utils from "../../utils"; +import * as Utils from "../../lib/utils"; import type { Color } from "react-aria-components"; import { parseColor } from "react-aria-components"; import Main from "../../components/Main/Main"; diff --git a/tests/integration/Page.test.tsx b/tests/integration/Page.test.tsx index 97ad919..cf63266 100644 --- a/tests/integration/Page.test.tsx +++ b/tests/integration/Page.test.tsx @@ -9,9 +9,9 @@ import { } from "vitest"; import { screen, fireEvent, act } from "@testing-library/react"; import { renderWithProviders } from "../test-utils"; -import * as Utils from "../../utils"; +import * as Utils from "../../lib/utils"; -import { Page } from "../../pages/index/index.page"; +import { Page } from "../../pages/editor/index.page"; import { SliceStores } from "../../types"; describe("Page", () => { diff --git a/tests/unit/utils.test.ts b/tests/unit/utils.test.ts index 5ac54ae..158fb38 100644 --- a/tests/unit/utils.test.ts +++ b/tests/unit/utils.test.ts @@ -7,8 +7,8 @@ import { afterAll, afterEach } from "vitest"; -import * as utils from "../../utils"; -import { CanvasElement } from "../../types"; +import * as utils from "../../lib/utils"; +// import { CanvasElement } from "../../types"; describe("capitalize functionality", () => { it("should capitalize the first occurring character", () => { @@ -357,7 +357,7 @@ describe("generateCanvasImage functionality", () => { expect(ellipseSpy).toHaveBeenCalledOnce(); expect(moveToSpy).toHaveBeenCalledOnce(); expect(lineToSpy).toHaveBeenCalledTimes(2); - + expect(fillTextSpy).not.toHaveBeenCalled(); expect(strokeTextSpy).not.toHaveBeenCalled(); }); diff --git a/types/Home.types.ts b/types/Home.types.ts new file mode 100644 index 0000000..d4bc68e --- /dev/null +++ b/types/Home.types.ts @@ -0,0 +1,10 @@ +export type Option = { + label: string; + value: string; +}; + +export type CanvasFile = { + archived: boolean; + archivedDate: number | null; + file: File; +}; diff --git a/types/index.ts b/types/index.ts index 82528af..d4717dc 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,16 +1,14 @@ export * from "./Canvas.types"; export * from "./History.types"; +export * from "./Home.types"; export * from "./Slices.types"; export type ImageUpdateEvent = CustomEvent<{ layer: HTMLCanvasElement }>; declare global { - interface Window { + interface Window {} - } - - - // This is so that TypeScript knows that this custom event exists globally. + // This is so that TypeScript knows that this custom event exists globally. interface DocumentEventMap { imageupdate: ImageUpdateEvent; }
+ Last Updated: {stringDate} +
New File
Open File
Shared File
Archived
+ {usedMegaBytes} MB of {MAX_MEGABYTES} MB Used +
+ v.0.0.1 + © {new Date().getUTCFullYear()} IdeaDrawn +
No files found.