diff --git a/client/package-lock.json b/client/package-lock.json index a36281fc81..4b46a3fe3a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -3576,6 +3576,11 @@ "minimist": "^1.2.0" } }, + "@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==" + }, "@istanbuljs/load-nyc-config": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", @@ -4509,8 +4514,7 @@ "@types/prop-types": { "version": "15.7.3", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", - "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", - "dev": true + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, "@types/qs": { "version": "6.9.4", @@ -4522,12 +4526,28 @@ "version": "16.9.32", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.32.tgz", "integrity": "sha512-fmejdp0CTH00mOJmxUPPbWCEBWPvRIL4m8r0qD+BSDUqmutPyGQCHifzMpMzdvZwROdEdL78IuZItntFWgPXHQ==", - "dev": true, "requires": { "@types/prop-types": "*", "csstype": "^2.2.0" } }, + "@types/react-color": { + "version": "2.17.4", + "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-2.17.4.tgz", + "integrity": "sha512-pAO3+7uHoESg5QMqjnGjw9F7sALjEZsaU41yGiUZbmHiJMoSXH1UklFJ1bZkwhYskaJgiY+AS6wirl17yBh5GA==", + "requires": { + "@types/react": "*", + "@types/reactcss": "*" + } + }, + "@types/react-custom-scrollbars": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@types/react-custom-scrollbars/-/react-custom-scrollbars-4.0.7.tgz", + "integrity": "sha512-4QPZdwd+wmzWq9TyNSA/4MZFYvlQn1GlEFFkpFx8VSs13gR/L+hQne0vFnbzwlQmGG7OksthkoVpYxWJjzz95w==", + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "16.9.6", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.6.tgz", @@ -4546,6 +4566,14 @@ "@types/react": "*" } }, + "@types/reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-d2gQQ0IL6hXLnoRfVYZukQNWHuVsE75DzFTLPUuyyEhJS8G2VvlE+qfQQ91SJjaMqlURRCNIsX7Jcsw6cEuJlA==", + "requires": { + "@types/react": "*" + } + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -4801,6 +4829,11 @@ "object-assign": "4.x" } }, + "add-px-to-style": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/add-px-to-style/-/add-px-to-style-1.0.0.tgz", + "integrity": "sha1-0ME1RB+oAUqBN5BFMQlvZ/KPJjo=" + }, "adjust-sourcemap-loader": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-2.0.0.tgz", @@ -7048,6 +7081,16 @@ "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.0.tgz", "integrity": "sha512-YkoezQuhp3SLFGdOlr5xkqZ640iXrnHAwVYcDg8ZKRUtO7mSzSC2BA5V0VuyAwPSJA4CLIc6EDDJh4bEsD2+zA==" }, + "dom-css": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/dom-css/-/dom-css-2.1.0.tgz", + "integrity": "sha1-/bwtWgFdCj4YcuEUcrvQ57nmogI=", + "requires": { + "add-px-to-style": "1.0.0", + "prefix-style": "2.0.1", + "to-camel-case": "1.0.0" + } + }, "dom-serializer": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", @@ -11641,6 +11684,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, + "lodash-es": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz", + "integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==" + }, "lodash.escape": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", @@ -11738,6 +11786,11 @@ "object-visit": "^1.0.0" } }, + "material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -12050,6 +12103,11 @@ "minimist": "^1.2.5" } }, + "mobile-device-detect": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/mobile-device-detect/-/mobile-device-detect-0.4.3.tgz", + "integrity": "sha512-SN9EBE9SoJgkb83kuUVoIp3R9OGYE5dYEnLEz2oLooh0DzgtQ72BJmpNGqrgFvmfE4iLR2CaVJ3RjUcStheVZg==" + }, "moment": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", @@ -13029,6 +13087,11 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" }, + "prefix-style": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/prefix-style/-/prefix-style-2.0.1.tgz", + "integrity": "sha1-ZrupqHDP2jCKXcIOhekSCTLJWgY=" + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -13941,6 +14004,30 @@ "prop-types": "^15.5.8" } }, + "react-color": { + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", + "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==", + "requires": { + "@icons/material": "^0.2.4", + "lodash": "^4.17.15", + "lodash-es": "^4.17.15", + "material-colors": "^1.2.1", + "prop-types": "^15.5.10", + "reactcss": "^1.2.0", + "tinycolor2": "^1.4.1" + } + }, + "react-custom-scrollbars": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz", + "integrity": "sha1-gw/ZUCkn6X6KeMIIaBOJmyqLZts=", + "requires": { + "dom-css": "^2.0.0", + "prop-types": "^15.5.10", + "raf": "^3.1.0" + } + }, "react-dom": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", @@ -14040,6 +14127,14 @@ } } }, + "reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "requires": { + "lodash": "^4.0.1" + } + }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -15546,11 +15641,24 @@ "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" }, + "to-camel-case": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-camel-case/-/to-camel-case-1.0.0.tgz", + "integrity": "sha1-GlYFSy+daWKYzmamCJcyK29CPkY=", + "requires": { + "to-space-case": "^1.0.0" + } + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, + "to-no-case": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", + "integrity": "sha1-xyKQcWTvaxeBMsjmmTAhLRtKoWo=" + }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -15588,6 +15696,14 @@ "is-number": "^7.0.0" } }, + "to-space-case": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz", + "integrity": "sha1-sFLar7Gysp3HcM6gFj5ewOvJ/Bc=", + "requires": { + "to-no-case": "^1.0.0" + } + }, "toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", @@ -15738,9 +15854,9 @@ } }, "typescript": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", - "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz", + "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==", "dev": true }, "unicode-canonical-property-names-ecmascript": { diff --git a/client/package.json b/client/package.json index 71c4308ae7..2afbdae9e8 100644 --- a/client/package.json +++ b/client/package.json @@ -19,12 +19,15 @@ "coverage": "jest --coverage --config=./src/jest.config.js" }, "dependencies": { + "@types/react-custom-scrollbars": "^4.0.7", + "@types/react-color": "^2.17.4", "algolia-places-react": "^1.5.1", "antd": "^4.6.6", "axios": "~0.20.0", "chart.js": "^2.9.3", "csvtojson": "~2.0.10", "lodash": "~4.17.19", + "mobile-device-detect": "^0.4.3", "moment": "~2.24.0", "moment-timezone": "~0.5.28", "next": "^9.5.4", @@ -32,6 +35,8 @@ "qs": "^6.9.4", "react": "~16.13.1", "react-chartjs-2": "^2.9.0", + "react-custom-scrollbars": "^4.2.1", + "react-color": "^2.19.3", "react-dom": "~16.13.1", "react-gauge-chart": "git+https://github.com/BossBele/react-gauge-chart.git", "react-masonry-css": "^1.0.14", diff --git a/client/src/components/Schedule/CalendarView/Calendar.tsx b/client/src/components/Schedule/CalendarView/Calendar.tsx new file mode 100644 index 0000000000..7549c6e8c0 --- /dev/null +++ b/client/src/components/Schedule/CalendarView/Calendar.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import MobileCalendar from './components/MobileCalendar'; +import DesktopCalendar from './components/DesktopCalendar'; +import { CourseEvent } from 'services/course'; +import { isMobile } from 'mobile-device-detect'; + +type Props = { + data: CourseEvent[]; + timeZone: string; + storedTagColors?: object; + alias: string; +}; + +export const CalendarView: React.FC = ({ data, timeZone, storedTagColors, alias }) => { + return ( + <> + {isMobile ? ( + + ) : ( + + )} + + ); +}; diff --git a/client/src/components/Schedule/CalendarView/components/DesktopCalendar.tsx b/client/src/components/Schedule/CalendarView/components/DesktopCalendar.tsx new file mode 100644 index 0000000000..e4918fb525 --- /dev/null +++ b/client/src/components/Schedule/CalendarView/components/DesktopCalendar.tsx @@ -0,0 +1,86 @@ +import React, { useState } from 'react'; +import { Calendar, Badge, Typography, Tooltip } from 'antd'; +import { getMonthValue, getListData } from '../utils/filters'; +import { Scrollbars } from 'react-custom-scrollbars'; +import ModalWindow from './ModalWindow'; +import { CourseEvent } from 'services/course'; +import { Moment } from 'moment'; + +const { Title } = Typography; + +type Props = { + data: CourseEvent[]; + timeZone: string; + storedTagColors?: object; + alias: string; +}; + +const DesktopCalendar: React.FC = ({ data, timeZone, storedTagColors, alias }) => { + const [modalWindowData, setModalWindowData] = useState(null); + const [showWindow, setShowWindow] = useState(false); + + const handleOnClose = () => { + setShowWindow(false); + }; + + function showModalWindow(id: number) { + setModalWindowData(() => { + setShowWindow(true); + return data.filter(event => event.id === id)[0]; + }); + } + + const dateCellRender = (date: unknown | Moment) => { + return ( + +
    + {getListData((date as unknown) as Moment, data, timeZone, storedTagColors).map(coloredEvent => { + return ( + +
  • showModalWindow(coloredEvent.key)} + > + +
  • +
    + ); + })} +
+
+ ); + }; + + const monthCellRender = (date: unknown | Moment) => { + const num = getMonthValue((date as unknown) as Moment, data, timeZone); + + return !!num && {`Events & tasks: ${num}.`}; + }; + + return ( +
+ {modalWindowData && ( + + )} + +
+ ); +}; + +export default DesktopCalendar; diff --git a/client/src/components/Schedule/CalendarView/components/MobileCalendar.tsx b/client/src/components/Schedule/CalendarView/components/MobileCalendar.tsx new file mode 100644 index 0000000000..9e1e587bd5 --- /dev/null +++ b/client/src/components/Schedule/CalendarView/components/MobileCalendar.tsx @@ -0,0 +1,134 @@ +import React, { useState } from 'react'; +import { Calendar, List, Typography, Col, Button } from 'antd'; +import { getMonthValue, getListData } from '../utils/filters'; +import ModalWindow from './ModalWindow'; +import { CourseEvent } from 'services/course'; +import { Moment } from 'moment'; +import { dateWithTimeZoneRenderer, renderTagWithStyle } from 'components/Table'; +import css from 'styled-jsx/css'; + +const { Text } = Typography; + +const numberEventsStyle = css` + section { + position: absolute; + bottom: 12px; + background: #fb6216; + right: -13px; + border-radius: 100%; + width: 20px; + height: 20px; + line-height: 19px; + color: white; + } +`; + +type Props = { + data: CourseEvent[]; + timeZone: string; + storedTagColors?: object; + alias: string; +}; + +const MobileCalendar: React.FC = ({ data, timeZone, storedTagColors, alias }) => { + const [modalWindowData, setModalWindowData] = useState< + { color: string; name: string; key: number; time: string; type: string }[] | undefined + >(); + const [currentItem, setCurrentItem] = useState(null); + const [showWindow, setShowWindow] = useState(false); + const [calendarMode, setCalendarMode] = useState('month'); + + const handleOnClose = () => { + setShowWindow(false); + }; + + function showModalWindow(id: number) { + setCurrentItem(() => { + setShowWindow(true); + return data.filter(event => event.id === id)[0]; + }); + } + + function onSelect(date: unknown | Moment) { + if (calendarMode === 'month') { + setModalWindowData(getListData((date as unknown) as Moment, data, timeZone, storedTagColors)); + } + } + + function onPanelChange(_: any, mode: string) { + setCalendarMode(mode); + setModalWindowData([]); + } + + function dateCellRender(date: unknown | Moment) { + const numberEvents = getListData((date as unknown) as Moment, data, timeZone, storedTagColors).length; + return ( + !!(numberEvents > 0) && ( + <> +
{numberEvents}
+ + + ) + ); + } + + const monthCellRender = (date: unknown | Moment) => { + const numberEvents = getMonthValue((date as unknown) as Moment, data, timeZone); + return ( + !!numberEvents && ( + <> +
{numberEvents}
+ + + ) + ); + }; + + return ( + <> + + { + if (!data.length) return null; + const dateTime = data.filter(event => event.id === item.key)[0].dateTime; + return ( + showModalWindow(item.key)} type="link"> + more + , + ]} + > + + + {dateWithTimeZoneRenderer(timeZone, 'HH:mm')(dateTime)} + + {renderTagWithStyle(item.type, storedTagColors)} + {item.name} + + + ); + }} + /> + {currentItem && ( + + )} + + ); +}; + +export default MobileCalendar; diff --git a/client/src/components/Schedule/CalendarView/components/ModalWindow.tsx b/client/src/components/Schedule/CalendarView/components/ModalWindow.tsx new file mode 100644 index 0000000000..fc3cd931f3 --- /dev/null +++ b/client/src/components/Schedule/CalendarView/components/ModalWindow.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import Link from 'next/link'; +import { Modal, Space, Typography } from 'antd'; +import moment from 'moment'; +import { GithubUserLink } from 'components'; +import { renderTagWithStyle, urlRenderer } from 'components/Table/renderers'; +import { CourseEvent } from 'services/course'; + +const { Title, Text } = Typography; + +type Props = { + isOpen: boolean; + data: CourseEvent; + handleOnClose: Function; + timeZone: string; + storedTagColors?: object; + alias: string; +}; + +const ModalWindow: React.FC = ({ isOpen, data, handleOnClose, timeZone, storedTagColors, alias }) => { + const typeHeader = data.isTask ? 'Task:' : 'Event:'; + const title = ( + + + + {`${typeHeader} ${data.event.name}`} + + + + ); + + return ( +
+ handleOnClose()} + onCancel={() => handleOnClose()} + visible={isOpen} + > + {moment(data.dateTime).tz(timeZone).format('MMM Do YYYY HH:mm')} + {data.event.description &&
{data.event.description}
} + {data.organizer && data.organizer.githubId && ( +
+ Organizer: +
+ )} + + {data.event.descriptionUrl &&
Url: {urlRenderer(data.event.descriptionUrl)}
} +
{renderTagWithStyle(data.event.type, storedTagColors)}
+
+ +
+
+ ); +}; + +export default ModalWindow; diff --git a/client/src/components/Schedule/CalendarView/index.tsx b/client/src/components/Schedule/CalendarView/index.tsx new file mode 100644 index 0000000000..e1c54a58d0 --- /dev/null +++ b/client/src/components/Schedule/CalendarView/index.tsx @@ -0,0 +1 @@ +export { CalendarView } from './Calendar'; diff --git a/client/src/components/Schedule/CalendarView/utils/filters.ts b/client/src/components/Schedule/CalendarView/utils/filters.ts new file mode 100644 index 0000000000..a9e8ed458a --- /dev/null +++ b/client/src/components/Schedule/CalendarView/utils/filters.ts @@ -0,0 +1,36 @@ +import moment, { Moment } from 'moment'; +import { CourseEvent } from 'services/course'; +import { DEFAULT_COLOR } from 'components/Schedule/UserSettings/userSettingsHandlers'; + +export function getListData( + calendarCellDate: Moment, + data: CourseEvent[], + timeZone: string, + storedTagColors: object = {}, +) { + return filterByDate(calendarCellDate, data, timeZone).map((el: CourseEvent) => { + const tagColor = storedTagColors[el.event.type as keyof typeof storedTagColors]; + return { + color: tagColor || DEFAULT_COLOR, + name: el.event.name, + key: el.id, + time: moment(el.dateTime).tz(timeZone).format('HH:mm'), + type: el.event.type, + }; + }); +} + +export function filterByDate(calendarCellDate: Moment, data: CourseEvent[], timeZone: string) { + return data.filter( + event => + calendarCellDate.format('YYYY-MM-DD') === + moment(event.dateTime, 'YYYY-MM-DD HH:mmZ').tz(timeZone).format('YYYY-MM-DD'), + ); +} + +export function getMonthValue(calendarCellDate: Moment, data: CourseEvent[], timeZone: string) { + return data.filter( + event => + calendarCellDate.format('MM-YYYY') === moment(event.dateTime, 'YYYY-MM-DD HH:mmZ').tz(timeZone).format('MM-YYYY'), + ).length; +} diff --git a/client/src/components/Schedule/EditableCell.tsx b/client/src/components/Schedule/EditableCell.tsx new file mode 100644 index 0000000000..27cd04308f --- /dev/null +++ b/client/src/components/Schedule/EditableCell.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { Form, Input, InputNumber, DatePicker, TimePicker, Select } from 'antd'; +import { Rule } from 'antd/lib/form'; +import { UserSearch } from 'components/UserSearch'; +import { CourseEvent } from 'services/course'; +import { EVENT_TYPES, SPECIAL_ENTITY_TAGS, TASK_TYPES } from './model'; +import { UserService } from 'services/user'; + +const { Option } = Select; +interface EditableCellProps extends React.HTMLAttributes { + editing: boolean; + dataIndex: string; + title: any; + inputType: 'number' | 'text'; + record: CourseEvent; + index: number; + children: React.ReactNode; +} + +const EditableCell: React.FC = ({ + editing, + dataIndex, + title, + record, + index, + children, + ...restProps +}) => { + let inputNode; + let rules: Rule = { type: 'string', required: false }; + + const typesList = record && record.isTask ? TASK_TYPES : EVENT_TYPES; + const types = typesList.map((tag: string) => { + return ( + + ); + }); + + switch (title) { + case 'Date': + inputNode = ; + rules = { type: 'object', required: true }; + break; + case 'Time': + inputNode = ; + rules = { type: 'object', required: true }; + break; + case 'Type': + inputNode = ( + + ); + break; + case 'Special': + inputNode = ( + + ); + rules = { required: false }; + break; + case 'Duration': + inputNode = ; + rules = { type: 'number', required: false }; + break; + case 'Organizer': + inputNode = ; + rules = { required: false, message: 'Please select an organizer' }; + break; + case 'Place': + inputNode = ; + rules = { type: 'string', required: false }; + break; + default: + inputNode = ; + rules = { type: 'string', required: false }; + break; + } + + return ( + + {editing ? ( + + {inputNode} + + ) : ( + children + )} + + ); +}; + +const loadUsers = async (searchText: string) => { + return new UserService().searchUser(searchText); +}; + +export default EditableCell; diff --git a/client/src/components/Schedule/EventDetails.tsx b/client/src/components/Schedule/EventDetails.tsx new file mode 100644 index 0000000000..6636b0b4b4 --- /dev/null +++ b/client/src/components/Schedule/EventDetails.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import { useLocalStorage } from 'react-use'; +import Link from 'next/link'; +import { Row, Col, Typography, Tooltip, Button } from 'antd'; +import { CloseOutlined, EditOutlined } from '@ant-design/icons'; +import moment from 'moment-timezone'; +import css from 'styled-jsx/css'; +import { CourseEvent } from 'services/course'; +import { DEFAULT_COLORS } from './UserSettings/userSettingsHandlers'; +import { renderTagWithStyle, tagsRenderer } from 'components/Table'; +import { GithubUserLink } from '../GithubUserLink'; + +const { Title, Text } = Typography; + +type Props = { + eventData: CourseEvent; + alias: string; + isPreview?: boolean; + onEdit?: (isTask?: boolean) => void; +}; + +const EventDetails: React.FC = ({ eventData, alias, isPreview, onEdit }) => { + const [storedTagColors] = useLocalStorage('tagColors', DEFAULT_COLORS); + const { event, dateTime, place, organizer, special, duration } = eventData; + + return ( + <> +
+ + + {event.name} + + + + {dateTime && ( + + + {moment(dateTime).format('MMM Do YYYY HH:mm')} + + + )} + + {event.type && ( + + {renderTagWithStyle(event.type, storedTagColors)} + {special && {!!special && tagsRenderer(special.split(','))}} + + )} + + {organizer && organizer.githubId && ( + + + + + + + + )} + + {event.descriptionUrl && ( + + + + <a href={event.descriptionUrl} target="_blank"> + Event link + </a> + + + + )} + + + {duration && ( + + {`Duration: ${duration} hours`} + + )} + {place && ( + + Place: {place} + + )} + + + {event.description && ( + + + + {event.description} + + + + )} + + {!isPreview && ( + <> +
+ + +
+
+
+ + )} +
+ + + + ); +}; + +const styles = css` + .container { + position: relative; + max-width: 1200px; + margin: 20px auto; + padding: 20px 10px; + } + .button__close { + position: absolute; + right: 10px; + top: 0; + } + .button__edit { + position: absolute; + left: 10px; + top: 0; + } +`; + +export default EventDetails; diff --git a/client/src/components/Schedule/FormEntity.tsx b/client/src/components/Schedule/FormEntity.tsx new file mode 100644 index 0000000000..df32a3f68e --- /dev/null +++ b/client/src/components/Schedule/FormEntity.tsx @@ -0,0 +1,455 @@ +import React, { useState } from 'react'; +import { Task, TaskService } from 'services/task'; +import { CourseEvent, CourseService } from 'services/course'; +import { withSession } from 'components'; +import { UserSearch } from 'components/UserSearch'; +import { UserService } from 'services/user'; +import { formatTimezoneToUTC } from 'services/formatter'; +import { + Form, + Input, + InputNumber, + Button, + DatePicker, + Select, + Alert, + Row, + Col, + message, + Divider, + Collapse, + Radio, + Checkbox, +} from 'antd'; +import moment from 'moment-timezone'; +import { EVENT_TYPES, SPECIAL_ENTITY_TAGS, TASK_TYPES } from './model'; +import { TIMEZONES } from '../../configs/timezones'; +import { Event, EventService } from 'services/event'; +import { times } from 'lodash'; +import { githubRepoUrl, urlPattern } from 'services/validators'; + +const { Option } = Select; +const { TextArea } = Input; + +const layout = { + labelCol: { span: 6 }, + wrapperCol: { span: 18 }, +}; + +const validateMessages = { + required: '${label} is required!', + types: { + email: '${label} is not validate email!', + number: '${label} is not a validate number!', + }, + number: { + range: '${label} must be between ${min} and ${max}', + }, +}; + +type Props = { + handleCancel: () => void; + onFieldsChange: (values: any) => void; + courseId: number; + entityType: string; + onEntityTypeChange: (type: string) => void; + editableRecord: CourseEvent | null; + refreshData: Function; +}; + +const FormEntity: React.FC = ({ + handleCancel, + courseId, + onFieldsChange, + onEntityTypeChange, + entityType, + editableRecord, + refreshData, +}) => { + const checker = editableRecord?.isTask && editableRecord.checker === 'crossCheck' ? true : false; + const [isFormSubmitted, setFormSubmitted] = useState(false); + const [isCrossCheckChecker, setIsCrossCheckChecker] = useState(checker); + const isUpdateMode = editableRecord ? true : false; + + const handleModalSubmit = async (values: any) => { + try { + if (entityType === 'task') { + await createTask(courseId, values, isUpdateMode, editableRecord); + } else { + await createEvent(courseId, values, isUpdateMode, editableRecord); + } + } catch (error) { + message.error('An error occurred. Please try later.'); + return; + } + + setFormSubmitted(true); + await refreshData(); + }; + + const handleFormChange = (_changedValues: any, allValues: any) => { + if (_changedValues.checker) { + const checker = _changedValues.checker === 'crossCheck' ? true : false; + setIsCrossCheckChecker(checker); + } + + onFieldsChange(allValues); + }; + + if (isFormSubmitted) { + return ; + } + + const typesList = entityType === 'task' ? TASK_TYPES : EVENT_TYPES; + const entityTypes = typesList.map(tag => { + return ( + + ); + }); + + return ( +
+ + + + + + + + + + + + + + + + + + + + + {entityType === 'task' && ( + + + + )} + + {entityType === 'event' && ( + + + + )} + + + + + + + {/* */} + + + + + + + + +