diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4fd5609..736049f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "KlectrTemplate" +version = "0.0.0" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-build", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -119,16 +129,6 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" -[[package]] -name = "big-word" -version = "0.0.0" -dependencies = [ - "serde", - "serde_json", - "tauri", - "tauri-build", -] - [[package]] name = "bitflags" version = "1.3.2" diff --git a/src/App.tsx b/src/App.tsx index 6a916bc..9eb11cf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,21 +1,28 @@ -import { useRef, useState } from "kaioken" +import { Route, Router } from "kaioken" +import { GlobalProvider } from "./state/GlobalProvider" +import { GithubIcon } from "./components/icons/GithubIcon" +import { BoardPage } from "./BoardPage" +import { HomePage } from "./HomePage" export function App() { - const [text, setText] = useState('') - const inputRef = useRef(null) - - function _handleTextInputChange(e: Event) { - setText((e.target as HTMLInputElement).value) - if (inputRef.current) inputRef.current.value = "" - } - return ( -
-

{text}

-
-
- Text here -
+ + + + + + + ) } - diff --git a/src/BoardPage.tsx b/src/BoardPage.tsx new file mode 100644 index 0000000..a51a3e2 --- /dev/null +++ b/src/BoardPage.tsx @@ -0,0 +1,20 @@ +import { useRef, useEffect } from "kaioken" +import { Board } from "./components/Board" +import { useGlobal } from "./state/global" + +export function BoardPage({ params }: { params: Record }) { + const rootElementRef = useRef(null) + const { setRootElement } = useGlobal() + + useEffect(() => { + if (!rootElementRef.current) return + setRootElement(rootElementRef.current) + }, [rootElementRef.current]) + + const { boardId } = params + return ( +
+ +
+ ) +} diff --git a/src/HomePage.css b/src/HomePage.css new file mode 100644 index 0000000..116f703 --- /dev/null +++ b/src/HomePage.css @@ -0,0 +1,9 @@ +.board-item { + background-color: rgba(255, 255, 255, 1); + transition: 0.15s; +} + +.board-item:hover { + background-color: rgba(255, 255, 255, 1); +} + diff --git a/src/HomePage.tsx b/src/HomePage.tsx new file mode 100644 index 0000000..8555ae0 --- /dev/null +++ b/src/HomePage.tsx @@ -0,0 +1,138 @@ +import "./HomePage.css" +import { ActionMenu } from "./components/ActionMenu" +import { Button } from "./components/atoms/Button" +import { LogoIcon } from "./components/icons/LogoIcon" +import { MoreIcon } from "./components/icons/MoreIcon" +import { JsonUtils } from "./idb" +import { useGlobal } from "./state/global" +import { Board } from "./types" +import { Link, useState } from "kaioken" + +function readFile(file: Blob): Promise { + return new Promise((resolve) => { + const reader = new FileReader() + reader.addEventListener("load", () => resolve(reader.result as string)) + reader.readAsText(file, "UTF-8") + }) +} + +export function HomePage() { + const [showArchived, setShowArchived] = useState(false) + const [menuOpen, setMenuOpen] = useState(false) + const { boards, addBoard } = useGlobal() + const activeBoards = boards.filter((b) => !b.archived) + const archivedBoards = boards.filter((b) => b.archived) + + return ( +
+
+

+ + Kaioban +

+
+ + setMenuOpen(false)} + items={[ + { + text: `${showArchived ? "Hide" : "Show"} archived boards`, + onclick: () => { + setShowArchived((prev) => !prev) + setMenuOpen(false) + }, + }, + { + text: "Export data", + onclick: async () => { + const data = await JsonUtils.export() + const dateStr = new Date() + .toLocaleString() + .split(",")[0] + .replaceAll("/", "-") + + const a = document.createElement("a") + const file = new Blob([data], { type: "application/json" }) + a.href = URL.createObjectURL(file) + a.download = `kaioban-export-${dateStr}.json` + a.click() + setMenuOpen(false) + }, + }, + { + text: "Import data", + onclick: () => { + const confirmOverwrite = confirm( + "Continuing will overwrite your existing data. Are you sure you want to continue?" + ) + if (!confirmOverwrite) return + const input = Object.assign(document.createElement("input"), { + type: "file", + accept: ".json", + onchange: async () => { + const file = input.files?.[0] + if (!file) return + const data = await readFile(file) + console.log("IMPORT", data) + await JsonUtils.import(data) + //@ts-ignore + window.location = "/" + }, + }) + input.click() + }, + }, + ]} + /> +
+
+
+
+

Boards

+
+ {activeBoards.length > 0 && ( +
+ {activeBoards.map((board) => ( + + ))} +
+ )} + +
+
+ {showArchived && ( + <> +
+
+

Archived Boards

+
+ {archivedBoards.length > 0 ? ( + archivedBoards.map((board) => ) + ) : ( +
+ No archived boards +
+ )} +
+
+ + )} +
+ ) +} + +function BoardCard({ board }: { board: Board }) { + return ( + + {board.title || "(Unnamed board)"} + + ) +} diff --git a/src/assets/tauri.svg b/src/assets/tauri.svg deleted file mode 100644 index 31b62c9..0000000 --- a/src/assets/tauri.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/assets/typescript.svg b/src/assets/typescript.svg deleted file mode 100644 index 30a5edd..0000000 --- a/src/assets/typescript.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - diff --git a/src/assets/vite.svg b/src/assets/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/ActionMenu.css b/src/components/ActionMenu.css new file mode 100644 index 0000000..45d6fe6 --- /dev/null +++ b/src/components/ActionMenu.css @@ -0,0 +1,24 @@ +.action-menu { + z-index: 99999; + background-color: var(--primary); + box-shadow: 0 0 10px 1px #0002; + top: 100%; + right: 0; + border-radius: calc(var(--radius) / 2); + overflow: hidden; + transition: 0.15s ease; + filter: grayscale(0.85); +} + +.action-menu-item { + border-bottom: 2px solid #0002; +} + +.action-menu-item button:hover { + background-color: #fff1; +} + +.action-menu-item:last-child { + border-bottom: 0; +} + diff --git a/src/components/ActionMenu.tsx b/src/components/ActionMenu.tsx new file mode 100644 index 0000000..043bf5c --- /dev/null +++ b/src/components/ActionMenu.tsx @@ -0,0 +1,81 @@ +import { Transition, useEffect, useRef } from "kaioken" +import { Button } from "./atoms/Button" +import "./ActionMenu.css" + +type ActionMenuItem = { + text: string + onclick: (e: Event) => void +} + +interface ActionMenuProps { + items: ActionMenuItem[] + open: boolean + close: () => void +} + +export function ActionMenu({ open, items, close }: ActionMenuProps) { + const ref = useRef(null) + + useEffect(() => { + window.addEventListener("click", handleClickOutside) + window.addEventListener("keyup", handleEscapeKey) + return () => { + window.removeEventListener("click", handleClickOutside) + window.removeEventListener("keyup", handleEscapeKey) + } + }, []) + + function handleClickOutside(e: MouseEvent) { + if (!ref.current || !e.target) return + const tgt = e.target as Node + if (!ref.current.contains(tgt)) { + close() + } + } + + function handleEscapeKey(e: KeyboardEvent) { + if (e.key !== "Escape") return + if (!document.activeElement || !ref.current) return + if (!ref.current.contains(document.activeElement)) return + close() + } + + return ( + <> + { + if (state == "exited") return null + const opacity = state === "entered" ? "1" : "0" + const scale = state === "entered" ? 1 : 0.85 + const translateY = state === "entered" ? 0 : -25 + const pointerEvents = state === "entered" ? "unset" : "none" + return ( +
+ {items.map((item) => ( +
+ +
+ ))} +
+ ) + }} + /> + + ) +} diff --git a/src/components/Board.css b/src/components/Board.css new file mode 100644 index 0000000..1147e83 --- /dev/null +++ b/src/components/Board.css @@ -0,0 +1,33 @@ +#board { + width: fit-content; + min-width: calc(100vw - .5rem); + flex-grow: 1; + display: flex; + flex-direction: column; +} + +#board * { + user-select: none; +} + +#board .inner { + --lists-gap: 1rem; + min-width: 100%; + display: flex; + cursor: grab; + min-height: 100%; + flex-grow: 1; + gap: var(--lists-gap); + padding: 1rem; +} + +#board .inner.dragging, +#board .inner.dragging * { + cursor: grabbing; +} + +#board-selector { + width: 264px; + text-overflow: ellipsis; + white-space: nowrap; +} \ No newline at end of file diff --git a/src/components/Board.tsx b/src/components/Board.tsx new file mode 100644 index 0000000..5c72724 --- /dev/null +++ b/src/components/Board.tsx @@ -0,0 +1,245 @@ +import "./Board.css" +import { Link, Portal, useEffect, useRef, useState } from "kaioken" +import { ItemList } from "./ItemList" +import type { Board as BoardType, Vector2 } from "../types" +import { useGlobal } from "../state/global" +import { Button } from "./atoms/Button" +import { ItemEditorModal } from "./ItemEditor" +import { ListEditorModal } from "./ListEditor" +import { ListItemClone } from "./ListItemClone" +import { ListClone } from "./ListClone" +import { MouseCtx } from "../state/mouse" +import { BoardEditorDrawer } from "./BoardEditor" +import { ChevronLeftIcon } from "./icons/ChevronLeftIcon" +import { MoreIcon } from "./icons/MoreIcon" +import { useListsStore } from "../state/lists" +import { useBoardStore } from "../state/board" +import { useItemsStore } from "../state/items" +import { ContextMenu } from "./ContextMenu" + +const autoScrollSpeed = 10 + +export function Board({ boardId }: { boardId: string }) { + const { + rootElement, + clickedItem, + setClickedItem, + dragging, + setDragging, + itemDragTarget, + setItemDragTarget, + clickedList, + setClickedList, + listDragTarget, + setListDragTarget, + handleListDrag, + } = useGlobal() + const animFrameRef = useRef(-1) + const [mousePos, setMousePos] = useState({ x: 0, y: 0 }) + const [autoScrollVec, setAutoScrollVec] = useState({ x: 0, y: 0 }) + const { + value: { board }, + selectBoard, + } = useBoardStore() + const { handleItemDrop } = useItemsStore() + const { handleListDrop } = useListsStore() + const boardInnerRef = useRef(null) + + const { boards, boardsLoaded } = useGlobal() + const { + value: { lists }, + } = useListsStore() + + useEffect(() => { + if (!boardsLoaded) return + const board = boards.find( + (b) => String(b.id) === boardId || b.uuid === boardId + ) + if (!board) { + // @ts-ignore + window.location = "/" + return + } + selectBoard(board) + }, [boardsLoaded]) + + useEffect(() => { + const v = getAutoScrollVec() + if (v.x !== autoScrollVec.x || v.y !== autoScrollVec.y) { + setAutoScrollVec(v) + } + }, [mousePos, clickedItem, clickedList]) + + useEffect(() => { + animFrameRef.current = requestAnimationFrame(applyAutoScroll) + return () => { + if (animFrameRef.current !== -1) { + cancelAnimationFrame(animFrameRef.current!) + animFrameRef.current = -1 + } + } + }, [rootElement, autoScrollVec]) + + function applyAutoScroll() { + if (rootElement) { + if (autoScrollVec.x !== 0) + rootElement.scrollLeft += autoScrollVec.x * autoScrollSpeed + if (autoScrollVec.y !== 0) + rootElement.scrollTop += autoScrollVec.y * autoScrollSpeed + } + + animFrameRef.current = requestAnimationFrame(applyAutoScroll) + } + + function getAutoScrollVec() { + const scrollPadding = 100 + const res: Vector2 = { x: 0, y: 0 } + if (!clickedItem?.dragging && !clickedList?.dragging) return res + + if (mousePos.x + scrollPadding > window.innerWidth) { + res.x++ + } else if (mousePos.x - scrollPadding < 0) { + res.x-- + } + if (mousePos.y + scrollPadding > window.innerHeight) { + res.y++ + } else if (mousePos.y - scrollPadding < 0) { + res.y-- + } + return res + } + + function handleMouseDown(e: MouseEvent) { + if (e.buttons !== 1) return + if (!boardInnerRef.current) return + if (e.target !== boardInnerRef.current) return + setDragging(true) + } + + async function handleMouseUp() { + // item drag + clickedItem && itemDragTarget && handleItemDrop(clickedItem, itemDragTarget) + clickedItem && setClickedItem(null) + itemDragTarget && setItemDragTarget(null) + + // list drag + clickedList && listDragTarget && handleListDrop(clickedList, listDragTarget) + clickedList && setClickedList(null) + listDragTarget && setListDragTarget(null) + + // setAutoScrollVec({ x: 0, y: 0 }) + + // board drag + dragging && setDragging(false) + } + + function handleMouseMove(e: MouseEvent) { + setMousePos({ + x: e.clientX, + y: e.clientY, + }) + if (clickedList && !clickedList.dragging) { + setClickedList({ + ...clickedList, + dragging: true, + }) + } else if (clickedList && clickedList.dragging) { + handleListDrag(e, clickedList) + } + if (!dragging || !rootElement) return + rootElement.scrollLeft -= e.movementX + rootElement.scrollTop -= e.movementY + } + + return ( + +