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
}
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
}
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 = }
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
}