generated from Klectr/KTemplate
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:
parent
5e9c7835e5
commit
d14b240333
20
src/App.tsx
20
src/App.tsx
@ -1,21 +1,5 @@
|
|||||||
import { useRef, useState } from "kaioken"
|
import InfiniteCanvas from "./components/InfinateCanvas"
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [text, setText] = useState('')
|
return <InfiniteCanvas />
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
95
src/components/CardSelector.tsx
Normal file
95
src/components/CardSelector.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
61
src/components/InfinateCanvas.tsx
Normal file
61
src/components/InfinateCanvas.tsx
Normal 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
101
src/components/NoteCard.tsx
Normal 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
4
src/components/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./CanvasItem"
|
||||||
|
export * from "./CanvasControls"
|
||||||
|
export * from "./InfinateCanvas"
|
||||||
|
export * from "./CardSelector"
|
0
src/signals/images.ts
Normal file
0
src/signals/images.ts
Normal file
6
src/signals/index.ts
Normal file
6
src/signals/index.ts
Normal 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
46
src/signals/notes.ts
Normal 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,
|
||||||
|
}
|
@ -2,14 +2,28 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||||
color: #fff;
|
|
||||||
background-color: #333;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #111;
|
||||||
}
|
}
|
||||||
|
12
src/types/Card.ts
Normal file
12
src/types/Card.ts
Normal 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
1
src/types/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./Card"
|
32
src/utils/useDebounce.ts
Normal file
32
src/utils/useDebounce.ts
Normal 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 }
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user