import { h, Fragment } from 'preact'; import { route } from 'preact-router'; import { useEffect, useReducer, useState, useRef, useLayoutEffect } from 'preact/hooks'; import * as meerkat from './meerkat' import { routeParam, removeParam, TagEditor } from './util'; import { CheckCard, CheckCardOptions } from './elements/card'; import { CheckSVG, CheckSVGOptions, CheckSVGDefaults } from './elements/svg'; import { CheckImage, CheckImageOptions } from './elements/image'; import { CheckLine, CheckLineOptions, CheckLineDefaults } from './elements/line'; import { StaticText, StaticTextOptions, StaticTextDefaults } from './statics/text'; import { StaticSVG, StaticSVGOptions, StaticSVGDefaults } from './statics/svg'; import { StaticImage, StaticImageOptions } from './statics/image'; import { IframeVideo, IframeVideoOptions } from './elements/video'; import { AudioStream, AudioOptions } from './elements/audio'; //Manage dashboard state const dashboardReducer = (state, action) => { switch (action.type) { case 'setDashboard': return action.dashboard; case 'setTitle': console.log('Setting title to ' + action.title) return {...state, title: action.title}; case 'setTags': console.log(`Setting tags to ${action.tags}`); return {...state, tags: action.tags}; case 'setBackground': console.log('Setting background to ' + action.background) return {...state, background: action.background}; case 'addElement': console.log('Adding new element') const newElement = { type: 'check-card', title: 'New Element', rect:{ x: 0, y: 0, w: 15, h: 15}, options: { checkId: null, nameFontSize: 40, statusFontSize: 60 } }; return { ...state, elements: state.elements.concat(newElement) }; case 'deleteElement': console.log('Deleting element') case 'duplicateElement': console.log('Duplicating element') case 'getDimensions' : console.log('Getting Dimensions') console.log({height: action.height, width: action.width }) return {...state, height: action.height, width: action.width}; case 'updateElement': console.log('Updating element') const newState = {...state}; newState.elements[action.elementIndex] = action.element; return newState; case 'reorderElements': console.log('Reordering elements') const ns = {...state}; const element = ns.elements[action.sourcePosition]; ns.elements.splice(action.sourcePosition, 1); ns.elements.splice(action.destinationPosition, 0, element); return ns; default: throw new Error(`Unexpected action`); } }; //Edit page export function Editor({slug, selectedElementId}) { const [dashboard, dashboardDispatch] = useReducer(dashboardReducer, null); const [savingDashboard, setSavingDashboard] = useState(false); const [highlightedElementId, setHighlightedElementId] = useState(null); useEffect(() => { meerkat.getDashboard(slug).then(async d => { dashboardDispatch({ type: 'setDashboard', dashboard: d }); }); }, [slug]); if(dashboard === null) { return
Loading dashboard
} const selectedElement = selectedElementId ? dashboard.elements[selectedElementId] : null; if(typeof selectedElement === 'undefined') { removeParam('selectedElementId'); return } const updateElement = element => { dashboardDispatch({ type: 'updateElement', elementIndex: selectedElementId, element: element }); } const saveDashboard = async e => { setSavingDashboard(true); console.log(dashboard); try { const data = await meerkat.saveDashboard(slug, dashboard); route(`/edit/${data.slug}${window.location.search}`) //TODO show success } catch (e) { //TODO improve console.log('error saving dashboard:'); console.log(e); } setSavingDashboard(false); } return

{dashboard.title}


} function TransformableElement({rect, updateRect, rotation, updateRotation, children, glow, highlight}) { //Handle dragging elements const handleMove = downEvent => { const mousemove = moveEvent => { const elementNode = downEvent.target; const dashboardNode = elementNode.parentElement; //Get max dimensions let left = elementNode.offsetLeft + moveEvent.movementX; let top = elementNode.offsetTop + moveEvent.movementY; const maxLeft = dashboardNode.clientWidth - elementNode.clientWidth; const maxTop = dashboardNode.clientHeight - elementNode.clientHeight; //limit movement to max dimensions left = left < 0 ? 0 : left; left = left > maxLeft ? maxLeft : left; top = top < 0 ? 0 : top; top = top > maxTop ? maxTop : top; //convert dimensions to relative (px -> percentage based) const relativeLeft = left / dashboardNode.clientWidth * 100; const relativeTop = top / dashboardNode.clientHeight * 100; //set position updateRect({x: relativeLeft, y: relativeTop, w: rect.w, h: rect.h}); } //Remove listeners on mouse button up const mouseup = () => { window.removeEventListener('mousemove', mousemove); window.removeEventListener('mouseup', mouseup); } //Add movement and mouseup events window.addEventListener('mouseup', mouseup); window.addEventListener('mousemove', mousemove); } const handleResize = downEvent => { downEvent.stopPropagation(); const mousemove = moveEvent => { //Go up an element due to resize dot const elementNode = downEvent.target.parentElement; const dashboardNode = elementNode.parentElement; //Get max dimensions let width = elementNode.clientWidth + moveEvent.movementX; let height = elementNode.clientHeight + moveEvent.movementY; let maxWidth = dashboardNode.clientWidth - elementNode.offsetLeft; let maxHeight = dashboardNode.clientHeight - elementNode.offsetTop; //limit minimun resize width = width < 40 ? 40 : width; width = width < maxWidth ? width : maxWidth; height = height < 40 ? 40 : height; height = height < maxHeight ? height : maxHeight; //convert dimensions to relative (px -> percentage based) const relativeWidth = width / dashboardNode.clientWidth * 100; const relativeHeight = height / dashboardNode.clientHeight * 100; //set position updateRect({x: rect.x, y: rect.y, w: relativeWidth, h: relativeHeight}); } //Remove listeners on mouse button up const mouseup = () => { window.removeEventListener('mousemove', mousemove); window.removeEventListener('mouseup', mouseup); } //Add movement and mouseup events window.addEventListener('mouseup', mouseup); window.addEventListener('mousemove', mousemove); } const handleRotate = downEvent => { downEvent.stopPropagation(); const mousemove = moveEvent => { //Go up an element due to rotate dot const elementRect = downEvent.target.parentElement.getBoundingClientRect(); let centerX = elementRect.left + ((elementRect.right - elementRect.left) / 2.0); let centerY = elementRect.top + ((elementRect.bottom - elementRect.top) / 2.0); const mouseX = moveEvent.clientX; const mouseY = moveEvent.clientY; const radians = Math.atan2(mouseY - centerY, mouseX - centerX); updateRotation(radians); } //Remove listeners on mouse button up const mouseup = () => { window.removeEventListener('mousemove', mousemove); window.removeEventListener('mouseup', mouseup); } //Add movement and mouseup events window.addEventListener('mouseup', mouseup); window.addEventListener('mousemove', mousemove); } const left = `${rect.x}%`; const top = `${rect.y}%`; const width = `${rect.w}%`; const height = `${rect.h}%`; const _rotation = rotation ? `rotate(${rotation}rad)` : `rotate(0rad)`; return
{children}
} function DashboardElements({dashboardDispatch, selectedElementId, elements, highlightedElementId}) { return elements.map((element, index) => { const updateRect = rect => { dashboardDispatch({ type: 'updateElement', elementIndex: index, element: { ...element, rect: rect } }); } const updateRotation = radian => { dashboardDispatch({ type: 'updateElement', elementIndex: index, element: { ...element, rotation: radian } }); } let ele = null; if(element.type === 'check-card') { ele = } if(element.type === 'check-svg') { ele = } if(element.type === 'check-image') { ele = } if(element.type === 'check-line') { ele = } if(element.type === 'static-text') { ele = } if(element.type === 'static-svg') { ele = } if(element.type === 'static-image') { ele = } if(element.type === 'iframe-video') { ele =
Move
} if(element.type === 'audio-stream') { ele = } return {ele} }); } //The actual dashboard being rendered export function DashboardView({dashboard, dashboardDispatch, selectedElementId, highlightedElementId}) { const backgroundImage = dashboard.background ? dashboard.background : 'none'; return
{console.log(dashboard.height)}
} //Settings view for the sidebar function SidePanelSettings({dashboardDispatch, dashboard}) { const handleBackgroundImg = async e => { try { const res = await meerkat.uploadFile(e.target.files[0]); dashboardDispatch({ type: 'setBackground', background: res.url }); } catch (e) { //TODO improve console.log('failed to upload image and set background'); console.log(e); } } const updateTags = tags => { dashboardDispatch({ type: 'setTags', tags: tags.map(v => v.toLowerCase().trim()) }); } const clearBackground = e => { e.preventDefault(); dashboardDispatch({ type: 'setBackground', background: null }); } const imgControls = src => { if(src) { return clear  view } return null; } return dashboardDispatch({type: 'setTitle', title: e.currentTarget.value})} /> updateTags(tags)} /> } function SidePanelElements({dashboard, dashboardDispatch, setHighlightedElementId}) { const addElement = e => { const newId = dashboard.elements.length; dashboardDispatch({type: 'addElement'}); routeParam('selectedElementId', newId); } const deleteElement = (e, index) => { e.preventDefault(); let elements = dashboard.elements; elements.splice(index, 1) dashboardDispatch({ type: 'deleteElement', }); }; const duplicateElement = (e, index) => { e.preventDefault(); let elements = dashboard.elements; elements.splice(index, 0, elements[index]) dashboardDispatch({ type: 'duplicateElement', }); }; const handleDragStart = e => { e.dataTransfer.setData("source-id", e.target.id); } const handleDrop = e => { e.preventDefault(); e.currentTarget.classList.remove('active'); const sourceId = e.dataTransfer.getData("source-id"); const destId = e.target.id; dashboardDispatch({ type: 'reorderElements', sourcePosition: sourceId, destinationPosition: destId }); } let elementList = dashboard.elements.map((element, index) => (
routeParam('selectedElementId', index.toString()) }>
{element.title}
{e.preventDefault(); e.currentTarget.classList.add('active')}} onDragOver={e => e.preventDefault()} onDragExit={e => e.currentTarget.classList.remove('active')}>
)); if(elementList.length < 1) { elementList =
No elements added.
} return

Elements

{elementList}
} export function ElementSettings({selectedElement, updateElement}) { if(selectedElement === null) { return null; } const updateElementOptions = (options) => { const newOptions = Object.assign(selectedElement.options, options) updateElement({...selectedElement, options: newOptions}) } //sets good default values for each visual type when they're selected const updateType = e => { const newType = e.currentTarget.value let defaults = {}; switch(newType) { case 'check-svg': defaults = CheckSVGDefaults; break; case 'check-line': defaults = CheckLineDefaults; break; case 'static-text': defaults = StaticTextDefaults; break; case 'static-svg': defaults = StaticSVGDefaults; break; } updateElement({ ...selectedElement, type: newType, options: Object.assign(selectedElement.options, defaults) }); } let ElementOptions = null; if(selectedElement.type === 'check-card') { ElementOptions = } if(selectedElement.type === 'check-svg') { ElementOptions = } if(selectedElement.type === 'check-image') { ElementOptions = } if(selectedElement.type === 'check-line') { ElementOptions = } if(selectedElement.type === 'static-text') { ElementOptions = } if(selectedElement.type === 'static-svg') { ElementOptions = } if(selectedElement.type === 'static-image') { ElementOptions = } if(selectedElement.type === 'iframe-video') { ElementOptions = } if(selectedElement.type === 'audio-stream') { ElementOptions = } return

{selectedElement.title}

removeParam('selectedElementId')}>
updateElement({...selectedElement, title: e.currentTarget.value})} />
{ElementOptions}
}