RecordingPlaylist.jsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import { h } from 'preact';
  2. import { useState } from 'preact/hooks';
  3. import { addSeconds, differenceInSeconds, fromUnixTime, format, parseISO, startOfHour } from 'date-fns';
  4. import ArrowDropdown from '../icons/ArrowDropdown';
  5. import ArrowDropup from '../icons/ArrowDropup';
  6. import Link from '../components/Link';
  7. import Menu from '../icons/Menu';
  8. import MenuOpen from '../icons/MenuOpen';
  9. import { useApiHost } from '../api';
  10. export default function RecordingPlaylist({ camera, recordings, selectedDate, selectedHour }) {
  11. const [active, setActive] = useState(true);
  12. const toggle = () => setActive(!active);
  13. const result = [];
  14. for (const recording of recordings.slice().reverse()) {
  15. const date = parseISO(recording.date);
  16. result.push(
  17. <ExpandableList
  18. title={format(date, 'MMM d, yyyy')}
  19. events={recording.events}
  20. selected={recording.date === selectedDate}
  21. >
  22. {recording.recordings.map((item, i) => (
  23. <div className="mb-2 w-full">
  24. <div
  25. className={`flex w-full text-md text-white px-8 py-2 mb-2 ${
  26. i === 0 ? 'border-t border-white border-opacity-50' : ''
  27. }`}
  28. >
  29. <div className="flex-1">
  30. <Link href={`/recording/${camera}/${recording.date}/${item.hour}`} type="text">
  31. {item.hour}:00
  32. </Link>
  33. </div>
  34. <div className="flex-1 text-right">{item.events.length} Events</div>
  35. </div>
  36. {item.events.map((event) => (
  37. <EventCard camera={camera} event={event} delay={item.delay} />
  38. ))}
  39. </div>
  40. ))}
  41. </ExpandableList>
  42. );
  43. }
  44. const openClass = active ? '-left-6' : 'right-0';
  45. return (
  46. <div className="flex absolute inset-y-0 right-0 w-9/12 md:w-1/2 lg:w-3/5 max-w-md text-base text-white font-sans">
  47. <div
  48. onClick={toggle}
  49. className={`absolute ${openClass} cursor-pointer items-center self-center rounded-tl-lg rounded-bl-lg border border-r-0 w-6 h-20 py-7 bg-gray-800 bg-opacity-70`}
  50. >
  51. {active ? <Menu /> : <MenuOpen />}
  52. </div>
  53. <div
  54. className={`w-full h-full bg-gray-800 bg-opacity-70 border-l overflow-x-hidden overflow-y-auto${
  55. active ? '' : ' hidden'
  56. }`}
  57. >
  58. {result}
  59. </div>
  60. </div>
  61. );
  62. }
  63. export function ExpandableList({ title, events = 0, children, selected = false }) {
  64. const [active, setActive] = useState(selected);
  65. const toggle = () => setActive(!active);
  66. return (
  67. <div className={`w-full text-sm ${active ? 'border-b border-white border-opacity-50' : ''}`}>
  68. <div className="flex items-center w-full p-2 cursor-pointer md:text-lg" onClick={toggle}>
  69. <div className="flex-1 font-bold">{title}</div>
  70. <div className="flex-1 text-right mr-4">{events} Events</div>
  71. <div className="w-6 md:w-10 h-6 md:h-10">{active ? <ArrowDropup /> : <ArrowDropdown />}</div>
  72. </div>
  73. <div className={`bg-gray-800 bg-opacity-50 ${active ? '' : 'hidden'}`}>{children}</div>
  74. </div>
  75. );
  76. }
  77. export function EventCard({ camera, event, delay }) {
  78. const apiHost = useApiHost();
  79. const start = fromUnixTime(event.start_time);
  80. const end = fromUnixTime(event.end_time);
  81. const duration = addSeconds(new Date(0), differenceInSeconds(end, start));
  82. const position = differenceInSeconds(start, startOfHour(start));
  83. const offset = Object.entries(delay)
  84. .map(([p, d]) => (position > p ? d : 0))
  85. .reduce((p, c) => p + c, 0);
  86. const seconds = Math.max(position - offset - 10, 0);
  87. return (
  88. <Link className="" href={`/recording/${camera}/${format(start, 'yyyy-MM-dd')}/${format(start, 'HH')}/${seconds}`}>
  89. <div className="flex flex-row mb-2">
  90. <div className="w-28 mr-4">
  91. <img className="antialiased" src={`${apiHost}/api/events/${event.id}/thumbnail.jpg`} />
  92. </div>
  93. <div className="flex flex-row w-full border-b">
  94. <div className="w-full text-gray-700 font-semibold relative pt-0">
  95. <div className="flex flex-row items-center">
  96. <div className="flex-1">
  97. <div className="text-2xl text-white leading-tight capitalize">{event.label}</div>
  98. <div className="text-xs md:text-normal text-gray-300">Start: {format(start, 'HH:mm:ss')}</div>
  99. <div className="text-xs md:text-normal text-gray-300">Duration: {format(duration, 'mm:ss')}</div>
  100. </div>
  101. <div className="text-lg text-white text-right leading-tight">{(event.top_score * 100).toFixed(1)}%</div>
  102. </div>
  103. </div>
  104. </div>
  105. <div className="w-6" />
  106. </div>
  107. </Link>
  108. );
  109. }