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() {
|
||||
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 />
|
||||
}
|
||||
|
||||
|
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 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
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