Initial skeleton: React/TS frontend + Go backend structure
This commit is contained in:
5
client/src/hooks/index.ts
Normal file
5
client/src/hooks/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { useCanvas } from './useCanvas'
|
||||
export { useDebounce, useKeyPress, useResizeObserver } from './useUtils'
|
||||
export { useKeyboardShortcuts } from './useKeyboardShortcuts'
|
||||
export { fuzzySearch, fuzzyFilter } from '../utils/fuzzy'
|
||||
export { initCRDT, getCRDTDoc, destroyCRDT } from '../utils/crdt'
|
||||
49
client/src/hooks/useCanvas.ts
Normal file
49
client/src/hooks/useCanvas.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useCanvasStore } from '../state/canvasStore'
|
||||
|
||||
export const useCanvas = () => {
|
||||
const { camX, camY, zoom, flying, dragging, collapsed, spacePanning } = useCanvasStore()
|
||||
|
||||
const screenToWorld = (screenX: number, screenY: number) => {
|
||||
return {
|
||||
x: (screenX - window.innerWidth / 2) / zoom + camX,
|
||||
y: (screenY - window.innerHeight / 2) / zoom + camY,
|
||||
}
|
||||
}
|
||||
|
||||
const worldToScreen = (worldX: number, worldY: number) => {
|
||||
return {
|
||||
x: (worldX - camX) * zoom + window.innerWidth / 2,
|
||||
y: (worldY - camY) * zoom + window.innerHeight / 2,
|
||||
}
|
||||
}
|
||||
|
||||
const pan = (dx: number, dy: number) => {
|
||||
useCanvasStore.getState().setCam(camX - dx / zoom, camY - dy / zoom)
|
||||
}
|
||||
|
||||
const zoomAt = (factor: number, centerX: number, centerY: number) => {
|
||||
const state = useCanvasStore.getState()
|
||||
const newZoom = Math.max(0.1, Math.min(5, state.zoom * factor))
|
||||
|
||||
const worldCenter = screenToWorld(centerX, centerY)
|
||||
const newCamX = worldCenter.x - (centerX - window.innerWidth / 2) / newZoom
|
||||
const newCamY = worldCenter.y - (centerY - window.innerHeight / 2) / newZoom
|
||||
|
||||
useCanvasStore.getState().setZoom(newZoom)
|
||||
useCanvasStore.getState().setCam(newCamX, newCamY)
|
||||
}
|
||||
|
||||
return {
|
||||
camX,
|
||||
camY,
|
||||
zoom,
|
||||
flying,
|
||||
dragging,
|
||||
collapsed,
|
||||
spacePanning,
|
||||
screenToWorld,
|
||||
worldToScreen,
|
||||
pan,
|
||||
zoomAt,
|
||||
}
|
||||
}
|
||||
41
client/src/hooks/useKeyboardShortcuts.ts
Normal file
41
client/src/hooks/useKeyboardShortcuts.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const useKeyboardShortcuts = (handlers: {
|
||||
onOpenSpotlight: () => void
|
||||
onCloseSpotlight: () => void
|
||||
onNavigateUp: () => void
|
||||
onNavigateDown: () => void
|
||||
onNavigateLeft: () => void
|
||||
onNavigateRight: () => void
|
||||
onSelect: () => void
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.ctrlKey || e.metaKey) return
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
handlers.onCloseSpotlight()
|
||||
break
|
||||
case 'k':
|
||||
handlers.onNavigateUp()
|
||||
break
|
||||
case 'j':
|
||||
handlers.onNavigateDown()
|
||||
break
|
||||
case 'h':
|
||||
handlers.onNavigateLeft()
|
||||
break
|
||||
case 'l':
|
||||
handlers.onNavigateRight()
|
||||
break
|
||||
case 'Enter':
|
||||
handlers.onSelect()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [handlers])
|
||||
}
|
||||
1
client/src/hooks/useKubernetes.ts
Normal file
1
client/src/hooks/useKubernetes.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useKubernetes } from './useKubernetes'
|
||||
53
client/src/hooks/useSpotlight.ts
Normal file
53
client/src/hooks/useSpotlight.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useMemo } from 'react'
|
||||
|
||||
export const useSpotlight = () => {
|
||||
const fuzzy = (query: string, text: string): number => {
|
||||
if (!query) return 0
|
||||
|
||||
const queryLower = query.toLowerCase()
|
||||
const textLower = text.toLowerCase()
|
||||
|
||||
let score = 0
|
||||
let queryIndex = 0
|
||||
|
||||
for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
|
||||
if (textLower[i] === queryLower[queryIndex]) {
|
||||
score++
|
||||
queryIndex++
|
||||
}
|
||||
}
|
||||
|
||||
return score / Math.max(query.length, text.length)
|
||||
}
|
||||
|
||||
const filterItems = <T extends { name: string; type: string }>(
|
||||
items: T[],
|
||||
query: string,
|
||||
filterType: string | null
|
||||
): T[] => {
|
||||
return items.filter((item) => {
|
||||
const matchesQuery = fuzzy(query, item.name) > 0.3
|
||||
const matchesType = !filterType || filterType === 'all' || item.type === filterType
|
||||
return matchesQuery && matchesType
|
||||
})
|
||||
}
|
||||
|
||||
const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
|
||||
let timeoutId: NodeJS.Timeout | null = null
|
||||
return (...args: Parameters<T>): Promise<ReturnType<T>> => {
|
||||
return new Promise((resolve) => {
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(() => resolve(fn(...args)), delay)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
fuzzy,
|
||||
filterItems,
|
||||
debounce,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
}
|
||||
50
client/src/hooks/useUtils.ts
Normal file
50
client/src/hooks/useUtils.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
|
||||
export const useDebounce = <T>(value: T, delay: number): T => {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
export const useKeyPress = (targetKey: string, callback: () => void) => {
|
||||
useEffect(() => {
|
||||
const downHandler = ({ key }: KeyboardEvent) => {
|
||||
if (key.toLowerCase() === targetKey.toLowerCase()) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', downHandler)
|
||||
return () => window.removeEventListener('keydown', downHandler)
|
||||
}, [targetKey, callback])
|
||||
}
|
||||
|
||||
export const useResizeObserver = (
|
||||
ref: React.RefObject<HTMLElement>,
|
||||
onResize: (rect: DOMRectReadOnly) => void
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const element = ref.current
|
||||
if (!element) return
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
onResize(entry.contentRect)
|
||||
}
|
||||
})
|
||||
|
||||
observer.observe(element)
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [ref, onResize])
|
||||
}
|
||||
1
client/src/hooks/useWebSocket.ts
Normal file
1
client/src/hooks/useWebSocket.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useWebSocket } from './useWebSocket'
|
||||
Reference in New Issue
Block a user