make updates to notes

- creates useDebounce hook for debouncing LS updates
- ensures focused item has highest zIndex
- makes drag work
- add unsaved dot on card header
This commit is contained in:
Triston Armstrong 2024-10-04 06:30:55 -04:00
parent 5e9c7835e5
commit d14b240333
12 changed files with 377 additions and 21 deletions

View File

@ -1,21 +1,5 @@
import { useRef, useState } from "kaioken"
import InfiniteCanvas from "./components/InfinateCanvas"
export function App() {
const [text, setText] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
function _handleTextInputChange(e: Event) {
setText((e.target as HTMLInputElement).value)
if (inputRef.current) inputRef.current.value = ""
}
return (
<div className="w-full min-h-screen flex items-center justify-center flex-col">
<h1 className="text-5xl text-center">{text}</h1>
<br />
<br />
<input ref={inputRef} className="text-center outline-none text-4xl bg-transparent border-b focus-visible:border-b focus-visible:border-blue-500" placeholder="Put Text Here" onchange={_handleTextInputChange}>Text here</input>
</div>
)
return <InfiniteCanvas />
}

View File

@ -0,0 +1,95 @@
import { useRef } from "kaioken"
import { NotesSigal } from "../signals"
export function CardSelector() {
const containerRef = useRef<HTMLDivElement>(null)
return (
<div
ref={containerRef}
className="z-50 flex gap-1 border border-[#9c9c9c] rounded-full fixed px-4 bg-[#181818] top-2 py-1 shadow-xl"
style={{
left: `${window.innerWidth / 2 - (containerRef.current?.getBoundingClientRect().width ?? 1) / 2}px`
}}>
<StickyNote />
<Image />
</div>
)
}
function StickyNote() {
function _handleClick(e: MouseEvent) {
NotesSigal.default.addNote({
type: "note",
title: "New Note",
contents: "",
position: {
x: e.pageX - 100,
y: e.pageY + (window.innerHeight / 2) - 100
},
dimensions: {
w: 200,
h: 200
}
})
}
return (
<button onclick={_handleClick} className="cursor-pointer">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="w-5 h-5 text-[#9c9c9c] hover:text-[#ccc] transition-color duration-300">
<path
d="M16 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8Z" />
<path
d="M15 3v4a2 2 0 0 0 2 2h4" />
</svg>
</button>
)
}
function Image() {
function _handleClick() {
alert("created image")
}
return (
<button onclick={_handleClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="w-5 h-5 text-[#9c9c9c] hover:text-[#ccc] transition-color duration-300">
<rect
width="18"
height="18"
x="3"
y="3"
rx="2"
ry="2" />
<circle
cx="9"
cy="9"
r="2" />
<path
d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
</svg>
</button>
)
}

View File

@ -0,0 +1,61 @@
import { useState, useRef, useEffect } from "kaioken"
import { CardSelector } from "./CardSelector"
import { NotesSigal } from "../signals"
import { NoteCard } from "./NoteCard"
import notes from "../signals/notes"
export default function InfiniteCanvas() {
const [dimensions, setDimensions] = useState({ width: 3000, height: 3000 })
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
window.scrollTo({
left: (dimensions.width / 2) - (window.innerWidth / 2),
top: (dimensions.height / 2) - (window.innerHeight / 2)
})
const updateDimensions = () => {
setDimensions((prevDimensions) => ({
width: Math.max(prevDimensions.width, window.innerWidth),
height: Math.max(prevDimensions.height, window.innerHeight),
}))
}
updateDimensions()
window.addEventListener("resize", updateDimensions)
notes.loadLocalStorage()
return () => {
window.removeEventListener("resize", updateDimensions)
}
}, [])
return (
<>
<CardSelector />
<div
className="h-screen w-full absolute top-0 left-0"
>
<div
className="absolute top-0 left-0"
ref={containerRef}
style={{
width: `${dimensions.width.toString()}px`,
height: `${dimensions.width.toString()}px`,
backgroundSize: "30px 30px",
backgroundImage: "radial-gradient(circle, rgba(255, 255, 255, 0.2) 1px, transparent 1px)",
}}>
{Object.keys(NotesSigal.default.notes.value).map((itemKey: string) => {
const item = NotesSigal.default.notes.value[itemKey]
return (
<NoteCard key={itemKey} data={item} />
)
})}
</div>
</div>
</>
)
}

101
src/components/NoteCard.tsx Normal file
View File

@ -0,0 +1,101 @@
import { signal, useRef } from "kaioken"
import { NotesSigal, focusedItem } from "../signals"
import { Card } from "../types"
import { useDebounce } from "../utils/useDebounce"
import notes from "../signals/notes"
import { save } from "@tauri-apps/api/dialog"
namespace NoteCard {
export interface NoteCardProps {
key: Card['id']
data: Card
}
}
export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
const saved = signal(true)
const pressed = signal(false)
const newX = useRef(0)
const newY = useRef(0)
const offsetX = useRef(0)
const offsetY = useRef(0)
const { debounce } = useDebounce()
function updateLocalStorage(time?: number) {
debounce(() => {
console.log(itemKey, "updated storage")
localStorage.setItem("notes", JSON.stringify(notes.notes.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 }
NotesSigal.default.updateNoteProperty(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) {
e.preventDefault()
offsetX.current = e.offsetX
offsetY.current = e.offsetY
pressed.value = true
window.addEventListener('mousemove', _handleMouseMove)
window.addEventListener('mouseup', _handleMouseUp)
}
return (
<div
onmousedown={() => focusedItem.value = itemKey}
className="select-none transition flex flex-col justify-stretch shadow-lg rounded border border-[#3c3c3c] absolute"
style={{
zIndex: focusedItem.value == itemKey ? '999' : '0',
width: `${item.dimensions.w}px`,
height: `${item.dimensions.h}px`,
top: `${item.position.y}px`,
left: `${item.position.x}px`,
backgroundColor: '#181818',
}}
>
<div className="flex-1 flex flex-col gap-1">
<div className="px-2 flex justify-between items-center cursor-move" onmousedown={_handleMouseDown}>
<div style={{
opacity: saved.value ? '0' : '100'
}} className={`rounded-full w-1 h-1 bg-white`}></div>
<button className="text-md" onclick={(_e: Event) => {
NotesSigal.default.removeNote(item.id)
NotesSigal.default.notes.notify()
}}>x</button>
</div>
<hr className="border border-[#3c3c3c]" />
<textarea
placeholder={"Todo: put some note here"}
className="flex resize-none px-2 w-full h-full bg-transparent resize-none focus:outline-none text-gray-300"
value={item.contents}
onkeypress={() => { saved.value = false }}
onchange={(e) => {
NotesSigal.default.updateNoteProperty(itemKey, 'contents', e.target.value)
NotesSigal.default.notes.notify()
updateLocalStorage()
saved.value = true
}}
/>
</div>
</div >
)
}

4
src/components/index.ts Normal file
View File

@ -0,0 +1,4 @@
export * from "./CanvasItem"
export * from "./CanvasControls"
export * from "./InfinateCanvas"
export * from "./CardSelector"

0
src/signals/images.ts Normal file
View File

6
src/signals/index.ts Normal file
View File

@ -0,0 +1,6 @@
import { signal } from "kaioken"
/** this should be an ID of some card/item */
export const focusedItem = signal<string | null>(null)
export * as NotesSigal from "./notes"

46
src/signals/notes.ts Normal file
View File

@ -0,0 +1,46 @@
import { signal } from "kaioken"
import { Card } from "../types"
const notes = signal<Record<Card["id"], Card>>({})
function loadLocalStorage() {
notes.value = JSON.parse(localStorage.getItem("notes") ?? "{}")
}
function addNote(data: Omit<Card, "id">) {
const newCard = {
...data,
id: crypto.randomUUID(),
}
notes.value[newCard.id] = newCard
notes.notify()
//updateLocalStorage()
}
function removeNote(id: Card["id"]) {
delete notes.value[id]
notes.notify()
//updateLocalStorage()
}
function updateNoteProperty<K extends keyof Card>(
id: Card["id"],
property: K,
data: Card[K]
) {
const newData = {
...notes.value[id],
[property]: data,
}
notes.value[id] = newData
notes.notify()
//updateLocalStorage()
}
export default {
notes,
addNote,
removeNote,
updateNoteProperty,
//updateLocalStorage,
loadLocalStorage,
}

View File

@ -2,14 +2,28 @@
@tailwind components;
@tailwind utilities;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
overflow: scroll;
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
color: #fff;
background-color: #333;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
color: white;
}
body {
margin: 0;
padding: 0;
background-color: #111;
}

12
src/types/Card.ts Normal file
View File

@ -0,0 +1,12 @@
export type CardTypes = "note" | "image"
export type positionCoords = { x: number; y: number }
export type dimensionCoords = { w: number; h: number }
export interface Card {
id: string
type: CardTypes
title: string
contents: string
position: positionCoords
dimensions: dimensionCoords
}

1
src/types/index.ts Normal file
View File

@ -0,0 +1 @@
export * from "./Card"

32
src/utils/useDebounce.ts Normal file
View File

@ -0,0 +1,32 @@
import { sideEffectsEnabled, useHook } from "kaioken"
import { noop } from "kaioken/utils"
type UseDebounceState = {
timer: number
debounce: (this: any, func: Function, timeout?: number) => void
}
function createState(): UseDebounceState {
return { timer: 0, debounce: noop }
}
export function useDebounce() {
if (!sideEffectsEnabled()) return createState()
return useHook("useDebounce", createState, ({ hook, update, isInit }) => {
if (!isInit) return { timer: hook.timer, debounce: hook.debounce }
hook.debounce = function debounce(
this: any,
func: Function,
timeout = 300
) {
clearTimeout(hook.timer)
hook.timer = setTimeout(() => {
func.apply(this)
}, timeout)
update()
}
return { timer: hook.timer, debounce: hook.debounce }
})
}