From 343dde5b8902312c28c2c050550e8d7e343a348a Mon Sep 17 00:00:00 2001 From: Martastain Date: Sat, 11 Nov 2023 10:13:50 +0100 Subject: [PATCH 01/41] added dnd --- frontend/package.json | 1 + frontend/src/pages/MAMPage.jsx | 24 ++++++++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 81a96e45..cd9efe33 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@devbookhq/splitter": "^1.4.0", + "@dnd-kit/core": "^6.0.8", "@reduxjs/toolkit": "^1.8.5", "@wfoxall/timeframe": "^1.2.0", "axios": "^0.27.2", diff --git a/frontend/src/pages/MAMPage.jsx b/frontend/src/pages/MAMPage.jsx index fc2d25b1..a5909206 100644 --- a/frontend/src/pages/MAMPage.jsx +++ b/frontend/src/pages/MAMPage.jsx @@ -9,6 +9,8 @@ import { setFocusedAsset, setSelectedAssets } from '/src/actions' import Browser from '/src/containers/Browser' import AssetEditor from '/src/pages/AssetEditor' +import { DndContext } from "@dnd-kit/core"; + const MAMContainer = styled.div` flex-grow: 1; @@ -74,16 +76,22 @@ const MAMPage = () => { setSplitterSizes(size) } + const onDragEnd = (event) => { + console.log(event) + } + return ( - - - {moduleComponent} - + + + + {moduleComponent} + + ) } From 0740e234c5e8d9f91ed8fc1173de9fb8f0b40b7b Mon Sep 17 00:00:00 2001 From: Martastain Date: Fri, 27 Sep 2024 21:47:28 +0200 Subject: [PATCH 02/41] feat: scheduler skeleton --- frontend/src/containers/Calendar/Calendar.jsx | 191 ++++++++++++++++++ .../containers/Calendar/CalendarWrapper.jsx | 43 ++++ .../src/containers/Calendar/ZoomControl.jsx | 14 ++ frontend/src/containers/Calendar/index.jsx | 2 + frontend/src/containers/Navbar.jsx | 2 + frontend/src/pages/MAMPage.jsx | 6 +- frontend/src/pages/Rundown/Rundown.jsx | 15 ++ frontend/src/pages/Rundown/index.jsx | 2 + frontend/src/pages/Scheduler/Scheduler.jsx | 46 +++++ frontend/src/pages/Scheduler/SchedulerNav.jsx | 13 ++ frontend/src/pages/Scheduler/index.jsx | 2 + 11 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 frontend/src/containers/Calendar/Calendar.jsx create mode 100644 frontend/src/containers/Calendar/CalendarWrapper.jsx create mode 100644 frontend/src/containers/Calendar/ZoomControl.jsx create mode 100644 frontend/src/containers/Calendar/index.jsx create mode 100644 frontend/src/pages/Rundown/Rundown.jsx create mode 100644 frontend/src/pages/Rundown/index.jsx create mode 100644 frontend/src/pages/Scheduler/Scheduler.jsx create mode 100644 frontend/src/pages/Scheduler/SchedulerNav.jsx create mode 100644 frontend/src/pages/Scheduler/index.jsx diff --git a/frontend/src/containers/Calendar/Calendar.jsx b/frontend/src/containers/Calendar/Calendar.jsx new file mode 100644 index 00000000..1e26d2fb --- /dev/null +++ b/frontend/src/containers/Calendar/Calendar.jsx @@ -0,0 +1,191 @@ +import styled from 'styled-components' +import { useRef, useEffect, useState } from 'react' +import CalendarWrapper from './CalendarWrapper' +import ZoomControl from './ZoomControl' + +const CalendarCanvas = styled.canvas` + background-color: #24202e; +` + +const Calendar = () => { + const calendarRef = useRef(null) + const dayRef = useRef(null) + const wrapperRef = useRef(null) + + const [scrollbarWidth, setScrollbarWidth] = useState(0) + const [zoom, setZoom] = useState(1) + const [currentTime, setCurrentTime] = useState(null) + const [mousePos, setMousePos] = useState(null) + + const clockWidth = 100 + const offsetHours = 7 + const offsetMinutes = 0 + + const pos2time = (x, y) => { + if (!dayRef.current || !calendarRef.current) { + return + } + const dayWidth = dayRef.current.clientWidth + const hourHeight = calendarRef.current.clientHeight / 24 + const day = Math.floor(x / dayWidth) + let hour = Math.floor(y / hourHeight) + offsetHours + let minute = + Math.floor(((y % hourHeight) / hourHeight) * 60) + offsetMinutes + // round to nearest 5 minutes + minute = Math.round(minute / 5) * 5 + if (minute >= 60) { + minute = 0 + hour += 1 + } + return { day, hour, minute } + } + + const drawCalendar = () => { + if (!dayRef.current || !calendarRef.current) { + return + } + + const dayWidth = dayRef.current.clientWidth + + const canvas = calendarRef.current + const ctx = canvas.getContext('2d') + + ctx.clearRect(0, 0, canvas.width, canvas.height) + + for (let i = 0; i < 24; i++) { + let y = (ctx.canvas.height / 24) * i + if (i === 0) { + continue + } + y += 4 + ctx.font = '14px Arial' + ctx.fillStyle = '#c0c0c0' + const hours = (i + offsetHours).toString().padStart(2, '0') + const minutes = offsetMinutes.toString().padStart(2, '0') + const clock = `${hours}:${minutes}` + ctx.fillText(clock, 14, y) + } + + for (let i = 0; i < 7; i++) { + let x = ((ctx.canvas.width - clockWidth) / 7) * i + x += 2 + x += clockWidth + + for (let j = 0; j < 24; j++) { + const hourHeight = ctx.canvas.height / 24 + const y = hourHeight * j + + if (j > 0) { + ctx.beginPath() + ctx.strokeStyle = '#d7d4d5' + ctx.lineWidth = 1 + ctx.moveTo(x, y) + ctx.lineTo(x + dayWidth - 4, y) + ctx.stroke() + } + + if (ctx.canvas.height / 24 > 40) { + for (let k = 1; k < 4; k++) { + const qy = y + (hourHeight / 4) * k + + ctx.beginPath() + ctx.strokeStyle = '#646464' + ctx.lineWidth = 1 + ctx.moveTo(x, qy) + ctx.lineTo(x + dayWidth - 4, qy) + ctx.stroke() + } + } + } + } + + if (currentTime && mousePos) { + const { day, hour, minute } = currentTime + const { x, y } = mousePos + const dayName = [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', + ][day - 1] + ctx.fillText(`${dayName} ${hour}:${minute}`, x, y) + } + } + + useEffect(() => { + //drawCalendar() + }, [currentTime]) + + const resizeCanvas = () => { + const canvas = calendarRef.current + canvas.width = canvas.parentElement.clientWidth + canvas.height = canvas.parentElement.clientHeight * zoom + + const bodyWrapper = calendarRef.current.parentElement + const scrollbarWidth = bodyWrapper.offsetWidth - bodyWrapper.clientWidth + setScrollbarWidth(scrollbarWidth) + + drawCalendar() + } + + const onMouseMove = (e) => { + if (!calendarRef?.current) { + return + } + const rect = calendarRef.current.getBoundingClientRect() + const x = e.clientX - rect.left + const y = e.clientY - rect.top + const newTime = pos2time(x, y) + setMousePos({ x, y }) + + if (JSON.stringify(newTime) !== JSON.stringify(currentTime)) { + setCurrentTime(newTime) + } + } + + useEffect(() => { + resizeCanvas() + }, [zoom]) + + useEffect(() => { + if (!wrapperRef.current) return + + const resizeObserver = new ResizeObserver(() => resizeCanvas()) + resizeObserver.observe(wrapperRef.current) + + return () => { + if (wrapperRef.current) { + resizeObserver.unobserve(wrapperRef.current) + } + } + }, [wrapperRef.current]) + + return ( + +
+
+ Monday +
+
Tuesday
+
Wednesday
+
Thursday
+
Friday
+
Saturday
+
Sunday
+
+
+
+ +
+
+
+ +
+
+ ) +} + +export default Calendar diff --git a/frontend/src/containers/Calendar/CalendarWrapper.jsx b/frontend/src/containers/Calendar/CalendarWrapper.jsx new file mode 100644 index 00000000..6750b017 --- /dev/null +++ b/frontend/src/containers/Calendar/CalendarWrapper.jsx @@ -0,0 +1,43 @@ +import styled from 'styled-components' + +const CalendarWrapper = styled.div` + display: flex; + flex-grow: 1; + flex-direction: column; + font-family: Arial; + background-color: #19161f; + + .calendar-header { + user-select: none; + user-drag: none; + display: flex; + margin-right: ${(props) => + props.scrollbarWidth}px; /* Dynamic padding to account for scrollbar */ + margin-left: ${(props) => + props.clockWidth}px; /* Dynamic padding to account for scrollbar */ + + .calendar-day { + flex: 1; + padding: 8px 2px; + text-align: center; + color: #c0c0c0; + } + } + + .calendar-body { + display: flex; + flex-grow: 1; + position: relative; + + .calendar-body-wrapper { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow-x: hidden; + overflow-y: scroll; + } + } +` +export default CalendarWrapper diff --git a/frontend/src/containers/Calendar/ZoomControl.jsx b/frontend/src/containers/Calendar/ZoomControl.jsx new file mode 100644 index 00000000..d29f96ca --- /dev/null +++ b/frontend/src/containers/Calendar/ZoomControl.jsx @@ -0,0 +1,14 @@ +const ZoomControl = ({ zoom, setZoom }) => { + return ( + setZoom(e.target.value)} + value={zoom} + /> + ) +} + +export default ZoomControl diff --git a/frontend/src/containers/Calendar/index.jsx b/frontend/src/containers/Calendar/index.jsx new file mode 100644 index 00000000..315cefb7 --- /dev/null +++ b/frontend/src/containers/Calendar/index.jsx @@ -0,0 +1,2 @@ +import Calendar from './Calendar'; +export default Calendar; diff --git a/frontend/src/containers/Navbar.jsx b/frontend/src/containers/Navbar.jsx index e8fd5641..77db405d 100644 --- a/frontend/src/containers/Navbar.jsx +++ b/frontend/src/containers/Navbar.jsx @@ -85,6 +85,8 @@ const NavBar = () => { {false && nebula.settings.system.ui_asset_preview && ( Preview )} + Scheduler + Rundown Jobs {!nebula.user.is_limited && ( <> diff --git a/frontend/src/pages/MAMPage.jsx b/frontend/src/pages/MAMPage.jsx index a5909206..c25d7357 100644 --- a/frontend/src/pages/MAMPage.jsx +++ b/frontend/src/pages/MAMPage.jsx @@ -6,10 +6,13 @@ import styled from 'styled-components' import { useLocalStorage } from '/src/hooks' import { setFocusedAsset, setSelectedAssets } from '/src/actions' + import Browser from '/src/containers/Browser' import AssetEditor from '/src/pages/AssetEditor' +import Scheduler from '/src/pages/Scheduler' +import Rundown from './Rundown' -import { DndContext } from "@dnd-kit/core"; +import { DndContext } from '@dnd-kit/core' const MAMContainer = styled.div` flex-grow: 1; @@ -67,6 +70,7 @@ const MAMPage = () => { const moduleComponent = useMemo(() => { if (module == 'editor') return + if (module == 'scheduler') return return 'Not implemented' }, [module]) diff --git a/frontend/src/pages/Rundown/Rundown.jsx b/frontend/src/pages/Rundown/Rundown.jsx new file mode 100644 index 00000000..d5da3731 --- /dev/null +++ b/frontend/src/pages/Rundown/Rundown.jsx @@ -0,0 +1,15 @@ +import { useEffect } from 'react' +import { useDispatch } from 'react-redux' +import { setPageTitle } from '/src/actions' + +const Rundown = () => { + const dispatch = useDispatch() + + useEffect(() => { + dispatch(setPageTitle({ title: 'Scheduler' })) + }, []) + + return

not implemented

+} + +export default Rundown diff --git a/frontend/src/pages/Rundown/index.jsx b/frontend/src/pages/Rundown/index.jsx new file mode 100644 index 00000000..5226502c --- /dev/null +++ b/frontend/src/pages/Rundown/index.jsx @@ -0,0 +1,2 @@ +import Rundown from './Rundown' +export default Rundown diff --git a/frontend/src/pages/Scheduler/Scheduler.jsx b/frontend/src/pages/Scheduler/Scheduler.jsx new file mode 100644 index 00000000..90af4de1 --- /dev/null +++ b/frontend/src/pages/Scheduler/Scheduler.jsx @@ -0,0 +1,46 @@ +import { useState, useEffect, useMemo } from 'react' +import { useDispatch } from 'react-redux' +import { setPageTitle } from '/src/actions' + +import Calendar from '/src/containers/Calendar' +import SchedulerNav from './SchedulerNav' + +const getWeekStart = () => { + const now = new Date() + const dayOfWeek = now.getDay() + const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1) + const weekStart = new Date(now.setDate(diff)) + weekStart.setHours(0, 0, 0, 0) + return weekStart +} + +const Scheduler = () => { + const dispatch = useDispatch() + const [startDate, setStartDate] = useState(getWeekStart()) + + const pageTitle = useMemo(() => { + const start = startDate.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + }) + const end = new Date( + startDate.getTime() + 6 * 24 * 60 * 60 * 1000 + ).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + return `Scheduler (${start} - ${end})` + }, [startDate]) + + useEffect(() => { + dispatch(setPageTitle({ title: pageTitle })) + }, []) + + return ( +
+ +
+ +
+
+ ) +} + +export default Scheduler diff --git a/frontend/src/pages/Scheduler/SchedulerNav.jsx b/frontend/src/pages/Scheduler/SchedulerNav.jsx new file mode 100644 index 00000000..8ad2ea1c --- /dev/null +++ b/frontend/src/pages/Scheduler/SchedulerNav.jsx @@ -0,0 +1,13 @@ +import { NavLink } from 'react-router-dom' +import { Navbar, InputText, Button, Spacer } from '/src/components' + +const SchedulerNav = ({}) => { + return ( + +