diff --git a/src/App.tsx b/src/App.tsx index 46d4d53..4189a94 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,4 @@ +import { memo } from "kaioken" import InfiniteCanvas from "./components/InfinateCanvas" import { ToastContextProvider } from "./components/Toast" import { useThemeDetector } from "./utils/useThemeDetector" @@ -7,7 +8,9 @@ export function App() { return ( - + ) } + +const MemoInfyCanvas = memo(InfiniteCanvas) diff --git a/src/components/ImageCard.tsx b/src/components/ImageCard.tsx index 1f750ff..bbecaaa 100644 --- a/src/components/ImageCard.tsx +++ b/src/components/ImageCard.tsx @@ -88,6 +88,11 @@ export function ImageCard({ key: itemKey, data: item }: ImageCard.ImageCardProps window.removeEventListener('mouseup', _handleResizeMouseUp) } + function _handleClose(_e: Event) { + ImagesSignal.default.removeImage(item.id) + ImagesSignal.default.images.notify() + } + return (
- +
diff --git a/src/components/InfinateCanvas.tsx b/src/components/InfinateCanvas.tsx index 5a7eb0c..b7976c1 100644 --- a/src/components/InfinateCanvas.tsx +++ b/src/components/InfinateCanvas.tsx @@ -1,5 +1,5 @@ import { useRef, useEffect } from "kaioken" -import { ImagesSignal, NotesSigal, canvasDimentsion } from "../signals" +import { ImagesSignal, NotesSigal, TextSignal, canvasDimentsion } from "../signals" import { NoteCard } from "./NoteCard" import notes from "../signals/notes" import { MiniMap } from "./MiniMap" @@ -8,6 +8,8 @@ import images from "../signals/images" import { CardSelector } from "./cardSelector/CardSelector" import { Logo } from "./Logo" import { useThemeDetector } from "../utils/useThemeDetector" +import { TextItem } from "./TextItem" +import texts from "../signals/texts" export default function InfiniteCanvas() { const containerRef = useRef(null) @@ -36,6 +38,7 @@ export default function InfiniteCanvas() { window.addEventListener("scrollend", _updatePosition) notes.loadLocalStorage() images.loadLocalStorage() + texts.loadLocalStorage() return () => { window.removeEventListener("resize", _updateDimensions) @@ -74,6 +77,14 @@ export default function InfiniteCanvas() { ) })} + + {Object.keys(TextSignal.default.texts.value).map((itemKey: string) => { + const item = TextSignal.default.texts.value[itemKey] + return ( + + ) + })} + diff --git a/src/components/MiniMap.tsx b/src/components/MiniMap.tsx index 24e524a..c9c60bc 100644 --- a/src/components/MiniMap.tsx +++ b/src/components/MiniMap.tsx @@ -3,6 +3,7 @@ import notes, { NoteCardType } from "../signals/notes" import { canvasDimentsion } from "../signals" import { LayerEnum } from "../utils/enums" import images, { ImageCardType } from "../signals/images" +import texts, { TextCardType } from "../signals/texts" const _MAP_OFFSET = 20 const _MAP_SCALE_FACTOR = 10 @@ -111,6 +112,34 @@ export function MiniMap() { ) })} + {Object.keys(texts.texts.value).map((textKey: TextCardType['id']) => { + const text = texts.texts.value[textKey] + const el = useRef(null) + + function _handleItemClick(_e: MouseEvent) { + window.scrollTo({ + left: text.position.x - ((viewportWidth / 2) - (text.dimensions.w / 2)), + top: text.position.y - ((viewportHeight / 2) - (text.dimensions.h / 2)), + behavior: 'smooth' + }) + } + + return ( +
+ ) + })} +
{/* Header Bar */} diff --git a/src/components/TextItem.tsx b/src/components/TextItem.tsx new file mode 100644 index 0000000..a7078d4 --- /dev/null +++ b/src/components/TextItem.tsx @@ -0,0 +1,235 @@ +import { signal, useEffect, useLayoutEffect, useRef } from "kaioken" +import { TextSignal, focusedItem } from "../signals" +import { useDebounce } from "../utils/useDebounce" +import texts, { TextCardType } from "../signals/texts" +import { LayerEnum } from "../utils/enums" +import { Card } from "../types" +import { useThemeDetector } from "../utils/useThemeDetector" + +namespace TextItem { + export interface TextCardProps { + key: TextCardType['id'] + data: TextCardType + } +} + +export function TextItem({ key: itemKey, data: item }: TextItem.TextCardProps) { + const { debounce } = useDebounce() + const pressed = signal(false) + const newX = useRef(0) + const newY = useRef(0) + const offsetX = useRef(0) + const offsetY = useRef(0) + const initialResizeX = useRef(0) + const elRef = useRef(null) + const pRef = useRef(null) + + useEffect(() => { + const elDems = elRef.current?.getBoundingClientRect() + const elW = elDems?.width ?? 100 + const elH = elDems?.height ?? 100 + const newDems: Card<'texts'>['dimensions'] = { w: elW, h: elH } + TextSignal.default.updateTextProperty(itemKey, 'dimensions', newDems) + TextSignal.default.texts.notify() + }, [elRef.current, item.fontSize]) + + + + function updateLocalStorage(time?: number) { + debounce(() => { + localStorage.setItem("texts", JSON.stringify(texts.texts.value)) + }, time) + } + + function _handleMouseMove(e: MouseEvent) { + e.preventDefault() + if (!pressed.value) return + + newX.current = e.pageX - offsetX.current + newY.current = e.pageY - offsetY.current + const newPos = { x: newX.current, y: newY.current } + + TextSignal.default.updateTextProperty(itemKey, 'position', newPos) + updateLocalStorage() + } + + function _handleMouseUp(e: MouseEvent) { + e.preventDefault() + pressed.value = false + window.removeEventListener('mousemove', _handleMouseMove) + window.removeEventListener('mouseup', _handleMouseUp) + } + + function _handleMouseDown(e: MouseEvent) { + focusedItem.value = itemKey + offsetX.current = e.offsetX + offsetY.current = e.offsetY + pressed.value = true + window.addEventListener('mousemove', _handleMouseMove) + window.addEventListener('mouseup', _handleMouseUp) + } + + function _handleResizeMove(e: MouseEvent) { + const { pageX } = e + const newX = initialResizeX.current - pageX + const newFontSize = Math.floor(-newX + item.fontSize) + + TextSignal.default.updateTextProperty(itemKey, 'fontSize', newFontSize) + TextSignal.default.texts.notify() + updateLocalStorage() + } + + function _handleResizeMouseDown(e: MouseEvent) { + e.stopPropagation() + initialResizeX.current = e.pageX + pressed.value = true + window.addEventListener('mousemove', _handleResizeMove) + window.addEventListener('mouseup', _handleResizeMouseUp) + } + + function _handleResizeMouseUp(_e: MouseEvent) { + pressed.value = false + window.removeEventListener('mousemove', _handleResizeMove) + window.removeEventListener('mouseup', _handleResizeMouseUp) + updateLocalStorage() + } + + function _handleClose(e: MouseEvent) { + e.stopPropagation() + TextSignal.default.removeText(item.id) + TextSignal.default.texts.notify() + } + + function _handleContentInput(e: InputEvent) { + const el = e.target as HTMLParagraphElement & Pick + const val = el.innerHTML.split('
').join('\n') + texts.updateTextProperty(itemKey, 'contents', val) + updateLocalStorage() + } + + useLayoutEffect(() => { + if (!pRef.current) return + pRef.current.textContent = item.contents + }, []) + + useEffect(() => { + function _handleClick(e: MouseEvent) { + const target = e.target as HTMLElement + + if (target === elRef.current) return + if (elRef.current?.contains(target)) return + + const classes = '.text-container, .text-p, .text-pre, .text-close' + if (target.closest(classes)) return + if (target.matches(classes)) return + + focusedItem.value = null + document.removeEventListener('mousedown', _handleClick) + } + + if (focusedItem.value !== itemKey) return + document.addEventListener('mousedown', _handleClick) + return () => { + document.removeEventListener('mousedown', _handleClick) + } + }, [focusedItem.value]) + + const outline = `${focusedItem.value === item.id ? '1px solid' : ''}` + const fontSize = `${item.fontSize / 6}px` + const zIndex = `${focusedItem.value == itemKey ? LayerEnum.CARD_ELEVATED : LayerEnum.CARD}` + + return ( +
+
+        

+

+
+ + + +
+ + ) +} + +namespace CloseIcon { + export interface Props { + cb: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null | undefined, + item: TextSignal.TextCardType + } +} +function CloseIcon({ item, cb }: CloseIcon.Props) { + const isDark = useThemeDetector() + return ( + + + + + + ) +} + +namespace ExpandIcon { + export interface Props { + cb: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null | undefined, + item: TextSignal.TextCardType + } +} +function ExpandIcon({ cb, item }: ExpandIcon.Props) { + const isDark = useThemeDetector() + return ( + + + + ) +} diff --git a/src/components/cardSelector/ImportButton.tsx b/src/components/cardSelector/ImportButton.tsx index 5a41c7d..7226b8c 100644 --- a/src/components/cardSelector/ImportButton.tsx +++ b/src/components/cardSelector/ImportButton.tsx @@ -21,15 +21,15 @@ export function ImportButton() { let content = readerEvent.target?.result; // get only the base64 parts and not any identifiers content = (content as string).split(',')[1] - const data: Record | Card<'image'>> = convertBase64ToJson(content) + const data: Record | Card<'images'>> = convertBase64ToJson(content) for (let key in data) { const item = data[key] - if (item.type == 'image') { + if (item.type == 'images') { const { id, ...rest } = item images.addImage(rest) } - if (item.type == 'note') { + if (item.type == 'notes') { const { id, ...rest } = item notes.addNote(rest) } diff --git a/src/components/cardSelector/TextButton.tsx b/src/components/cardSelector/TextButton.tsx index 0abe079..bcc28c9 100644 --- a/src/components/cardSelector/TextButton.tsx +++ b/src/components/cardSelector/TextButton.tsx @@ -1,11 +1,33 @@ import { Tooltip } from "./Tooltip"; +import { TextSignal } from "../../signals"; +import texts from "../../signals/texts"; +import { updateLocalStorage } from "../../utils/localStorage"; import { defaultClassName } from "./utils"; export function TextButton() { + function _handleClick(e: MouseEvent) { + TextSignal.default.addText({ + fontSize: 84, + type: "texts", + title: "New Note", + contents: "todo: fill me", + position: { + x: e.pageX - 100, + y: e.pageY + (window.innerHeight / 2) - 100 + }, + dimensions: { + w: 200, + h: 100 + } + }) + updateLocalStorage("texts", texts.texts.value) + } + return ( +export type ImageCardType = Card<"images"> const images = signal>({}) diff --git a/src/signals/index.ts b/src/signals/index.ts index 21dfeb8..09d8b5e 100644 --- a/src/signals/index.ts +++ b/src/signals/index.ts @@ -6,3 +6,4 @@ export const canvasDimentsion = signal({ width: 3000, height: 3000 }) export * as NotesSigal from "./notes" export * as ImagesSignal from "./images" +export * as TextSignal from "./texts" diff --git a/src/signals/notes.ts b/src/signals/notes.ts index 755fb69..71be04a 100644 --- a/src/signals/notes.ts +++ b/src/signals/notes.ts @@ -2,7 +2,7 @@ import { signal } from "kaioken" import { Card } from "../types" import { focusedItem } from "." -export type NoteCardType = Card<"note"> +export type NoteCardType = Card<"notes"> const notes = signal>({}) diff --git a/src/signals/texts.ts b/src/signals/texts.ts new file mode 100644 index 0000000..e56eb60 --- /dev/null +++ b/src/signals/texts.ts @@ -0,0 +1,48 @@ +import { signal } from "kaioken" +import { Card } from "../types" +import { focusedItem } from "." + +export type TextCardType = Card<"texts"> & { + fontSize: number +} + +const texts = signal>({}) + +function loadLocalStorage() { + texts.value = JSON.parse(localStorage.getItem("texts") ?? "{}") +} + +function addText(data: Omit) { + const newCard = { + ...data, + id: crypto.randomUUID(), + } + texts.value[newCard.id] = newCard + texts.notify() + focusedItem.value = newCard.id +} + +function removeText(id: TextCardType["id"]) { + delete texts.value[id] + texts.notify() +} +function updateTextProperty( + id: TextCardType["id"], + property: K, + data: TextCardType[K] +) { + const newData = { + ...texts.value[id], + [property]: data, + } + texts.value[id] = newData + texts.notify() +} + +export default { + texts, + addText, + removeText, + updateTextProperty, + loadLocalStorage, +} diff --git a/src/styles.css b/src/styles.css index ef9368e..b41f51d 100644 --- a/src/styles.css +++ b/src/styles.css @@ -13,6 +13,10 @@ box-sizing: border-box; } +*:focus { + outline: none; +} + html { overflow: scroll; } diff --git a/src/types/Card.ts b/src/types/Card.ts index cd5ae2c..c2dad2e 100644 --- a/src/types/Card.ts +++ b/src/types/Card.ts @@ -1,4 +1,4 @@ -export type CardTypes = "note" | "image" +export type CardTypes = "notes" | "images" | "texts" export type positionCoords = { x: number; y: number } export type dimensionCoords = { w: number; h: number } diff --git a/src/utils/localStorage.ts b/src/utils/localStorage.ts index 57316bf..fcd9549 100644 --- a/src/utils/localStorage.ts +++ b/src/utils/localStorage.ts @@ -1,5 +1,7 @@ +import { CardTypes } from "../types" + export function updateLocalStorage( - location: "notes" | "images", + location: CardTypes, collection: unknown[] | Record ) { localStorage.setItem(location, JSON.stringify(collection))