From 92f9106d1ca9f9f27b5baf9b665f84a57ba429df Mon Sep 17 00:00:00 2001 From: Cheerego7 <48879533+Cheerego7@users.noreply.github.com> Date: Fri, 4 Jun 2021 13:47:45 +0800 Subject: [PATCH] feat(flat-web): add HomePage to Flat-web (#702) --- .../MainPageNavHorizontal/style.less | 2 +- .../MainPageLayoutHorizontal/index.tsx | 63 ++++ .../MainPageLayoutHorizontal/style.less | 8 + packages/flat-components/src/index.ts | 1 + web/flat-web/src/AppRoutes/route-pages.ts | 50 ++- web/flat-web/src/assets/image/book.svg | 17 + web/flat-web/src/assets/image/creat.svg | 15 + web/flat-web/src/assets/image/join.svg | 15 + .../icons/device-active.svg | 12 + .../icons/device.svg | 15 + .../icons/feedback.svg | 15 + .../icons/github.svg | 18 + .../icons/logout.svg | 17 + .../icons/setting.svg | 16 + .../index.tsx | 112 ++++++ .../HomePage/MainRoomHistoryPanel/index.tsx | 17 + .../MainRoomListPanel/MainRoomList.tsx | 328 ++++++++++++++++++ .../HomePage/MainRoomListPanel/index.tsx | 41 +++ .../HomePage/MainRoomListPanel/style.less | 3 + .../HomePage/MainRoomMenu/CreateRoomBox.less | 19 + .../HomePage/MainRoomMenu/CreateRoomBox.tsx | 153 ++++++++ .../HomePage/MainRoomMenu/JoinRoomBox.less | 18 + .../HomePage/MainRoomMenu/JoinRoomBox.tsx | 186 ++++++++++ .../HomePage/MainRoomMenu/MainRoomMenu.less | 88 +++++ .../HomePage/MainRoomMenu/ScheduleRoomBox.tsx | 18 + .../src/pages/HomePage/MainRoomMenu/index.tsx | 40 +++ web/flat-web/src/pages/HomePage/index.tsx | 69 +++- web/flat-web/src/pages/HomePage/style.less | 19 + .../src/pages/utils/joinRoomHandler.ts | 33 ++ web/flat-web/src/route-config.ts | 48 +++ 30 files changed, 1452 insertions(+), 4 deletions(-) create mode 100644 packages/flat-components/src/components/MainPageLayoutHorizontal/index.tsx create mode 100644 packages/flat-components/src/components/MainPageLayoutHorizontal/style.less create mode 100644 web/flat-web/src/assets/image/book.svg create mode 100644 web/flat-web/src/assets/image/creat.svg create mode 100644 web/flat-web/src/assets/image/join.svg create mode 100644 web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/device-active.svg create mode 100644 web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/device.svg create mode 100644 web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/feedback.svg create mode 100644 web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/github.svg create mode 100644 web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/logout.svg create mode 100644 web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/setting.svg create mode 100644 web/flat-web/src/components/MainPageLayoutHorizontalContainer/index.tsx create mode 100644 web/flat-web/src/pages/HomePage/MainRoomHistoryPanel/index.tsx create mode 100644 web/flat-web/src/pages/HomePage/MainRoomListPanel/MainRoomList.tsx create mode 100644 web/flat-web/src/pages/HomePage/MainRoomListPanel/index.tsx create mode 100644 web/flat-web/src/pages/HomePage/MainRoomListPanel/style.less create mode 100644 web/flat-web/src/pages/HomePage/MainRoomMenu/CreateRoomBox.less create mode 100644 web/flat-web/src/pages/HomePage/MainRoomMenu/CreateRoomBox.tsx create mode 100644 web/flat-web/src/pages/HomePage/MainRoomMenu/JoinRoomBox.less create mode 100644 web/flat-web/src/pages/HomePage/MainRoomMenu/JoinRoomBox.tsx create mode 100644 web/flat-web/src/pages/HomePage/MainRoomMenu/MainRoomMenu.less create mode 100644 web/flat-web/src/pages/HomePage/MainRoomMenu/ScheduleRoomBox.tsx create mode 100644 web/flat-web/src/pages/HomePage/MainRoomMenu/index.tsx create mode 100644 web/flat-web/src/pages/HomePage/style.less create mode 100644 web/flat-web/src/pages/utils/joinRoomHandler.ts diff --git a/packages/flat-components/src/components/MainPageLayout/MainPageNavHorizontal/style.less b/packages/flat-components/src/components/MainPageLayout/MainPageNavHorizontal/style.less index 8b07a653588..71036283294 100644 --- a/packages/flat-components/src/components/MainPageLayout/MainPageNavHorizontal/style.less +++ b/packages/flat-components/src/components/MainPageLayout/MainPageNavHorizontal/style.less @@ -6,8 +6,8 @@ .main-page-nav-horizontal-content { display: flex; justify-content: space-between; + height: 50px; max-width: 960px; - max-height: 50px; margin: 0 auto; } diff --git a/packages/flat-components/src/components/MainPageLayoutHorizontal/index.tsx b/packages/flat-components/src/components/MainPageLayoutHorizontal/index.tsx new file mode 100644 index 00000000000..1348599d11a --- /dev/null +++ b/packages/flat-components/src/components/MainPageLayoutHorizontal/index.tsx @@ -0,0 +1,63 @@ +import "./style.less"; +import React from "react"; +import classNames from "classnames"; +import { MainPageLayoutItem } from "../MainPageLayout/types"; +import { + MainPageNavHorizontal, + MainPageNavHorizontalProps, +} from "../MainPageLayout/MainPageNavHorizontal"; + +export * from "../MainPageLayout/MainPageHeader"; +export type { MainPageLayoutItem } from "../MainPageLayout/types"; + +export interface MainPageLayoutHorizontalProps extends MainPageNavHorizontalProps { + /** when an item is clicked */ + onClick: (mainPageLayoutItem: MainPageLayoutItem) => void; + /** a list of keys to highlight the items */ + activeKeys: string[]; + /** inside sub menu in MainPageLayout */ + subMenu?: MainPageLayoutItem[]; +} + +export const MainPageLayoutHorizontal: React.FC = ({ + onClick, + activeKeys, + subMenu, + children, + ...restProps +}) => { + return ( +
+ + {subMenu && ( +
+ +
+ )} +
+ {children} +
+
+ ); +}; diff --git a/packages/flat-components/src/components/MainPageLayoutHorizontal/style.less b/packages/flat-components/src/components/MainPageLayoutHorizontal/style.less new file mode 100644 index 00000000000..804692342e7 --- /dev/null +++ b/packages/flat-components/src/components/MainPageLayoutHorizontal/style.less @@ -0,0 +1,8 @@ +.main-horizontal-layout-container { + height: 100vh; +} + +.main-horizontal-layout-container-content { + height: calc(100vh - 50px); + background-color: #f3f6f9; +} diff --git a/packages/flat-components/src/index.ts b/packages/flat-components/src/index.ts index 4311c401b3b..19d66bc1885 100644 --- a/packages/flat-components/src/index.ts +++ b/packages/flat-components/src/index.ts @@ -13,6 +13,7 @@ export * from "./components/InviteModal"; export * from "./components/LoadingPage"; export * from "./components/LoginPage"; export * from "./components/MainPageLayout"; +export * from "./components/MainPageLayoutHorizontal"; export * from "./components/PeriodicRoomPage"; export * from "./components/RemoveRoomModal"; export * from "./components/RoomDetailPage"; diff --git a/web/flat-web/src/AppRoutes/route-pages.ts b/web/flat-web/src/AppRoutes/route-pages.ts index 734d036ceea..3d36372ebc7 100644 --- a/web/flat-web/src/AppRoutes/route-pages.ts +++ b/web/flat-web/src/AppRoutes/route-pages.ts @@ -15,6 +15,54 @@ export const routePages: RoutePages = { }, [RouteNameType.HomePage]: { title: "Flat", - component: () => import("../pages/Homepage"), + component: () => import("../pages/HomePage"), + }, + [RouteNameType.SmallClassPage]: { + title: "SmallClassPage", + component: () => Promise.resolve({ default: () => null }), + }, + [RouteNameType.OneToOnePage]: { + title: "OneToOnePage", + component: () => Promise.resolve({ default: () => null }), + }, + [RouteNameType.BigClassPage]: { + title: "BigClassPage", + component: () => Promise.resolve({ default: () => null }), + }, + [RouteNameType.RoomDetailPage]: { + title: "RoomDetailPage", + component: () => Promise.resolve({ default: () => null }), + }, + [RouteNameType.UserScheduledPage]: { + title: "UserScheduledPage", + component: () => Promise.resolve({ default: () => null }), + }, + [RouteNameType.PeriodicRoomDetailPage]: { + title: "PeriodicRoomDetailPage", + component: () => Promise.resolve({ default: () => null }), + }, + [RouteNameType.ReplayPage]: { + title: "ReplayPage", + component: () => Promise.resolve({ default: () => null }), + }, + [RouteNameType.ModifyOrdinaryRoomPage]: { + title: "ModifyOrdinaryRoomPage", + component: () => Promise.resolve({ default: () => null }), + }, + [RouteNameType.ModifyPeriodicRoomPage]: { + title: "ModifyPeriodicRoomPage", + component: () => Promise.resolve({ default: () => null }), + }, + [RouteNameType.SystemCheckPage]: { + title: "SystemCheckPage", + component: () => Promise.resolve({ default: () => null }), + }, + [RouteNameType.GeneralSettingPage]: { + title: "Flat", + component: () => Promise.resolve({ default: () => null }), + }, + [RouteNameType.CloudStoragePage]: { + title: "Flat", + component: () => Promise.resolve({ default: () => null }), }, }; diff --git a/web/flat-web/src/assets/image/book.svg b/web/flat-web/src/assets/image/book.svg new file mode 100644 index 00000000000..3a964ee0549 --- /dev/null +++ b/web/flat-web/src/assets/image/book.svg @@ -0,0 +1,17 @@ + + + 编组 12 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/flat-web/src/assets/image/creat.svg b/web/flat-web/src/assets/image/creat.svg new file mode 100644 index 00000000000..7d95216dc4e --- /dev/null +++ b/web/flat-web/src/assets/image/creat.svg @@ -0,0 +1,15 @@ + + + 编组 10 + + + + + + + + + + + + \ No newline at end of file diff --git a/web/flat-web/src/assets/image/join.svg b/web/flat-web/src/assets/image/join.svg new file mode 100644 index 00000000000..9d048ee8dd1 --- /dev/null +++ b/web/flat-web/src/assets/image/join.svg @@ -0,0 +1,15 @@ + + + 编组 6 + + + + + + + + + + + + \ No newline at end of file diff --git a/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/device-active.svg b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/device-active.svg new file mode 100644 index 00000000000..70b096bba43 --- /dev/null +++ b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/device-active.svg @@ -0,0 +1,12 @@ + + + 矩形备份 25 + + + + + + + + + \ No newline at end of file diff --git a/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/device.svg b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/device.svg new file mode 100644 index 00000000000..45af8eb857c --- /dev/null +++ b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/device.svg @@ -0,0 +1,15 @@ + + + 矩形备份 25 + + + + + + + + + + + + \ No newline at end of file diff --git a/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/feedback.svg b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/feedback.svg new file mode 100644 index 00000000000..b874670dc40 --- /dev/null +++ b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/feedback.svg @@ -0,0 +1,15 @@ + + + invite备份 + + + + + + + + + + + + \ No newline at end of file diff --git a/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/github.svg b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/github.svg new file mode 100644 index 00000000000..cb6b54215e0 --- /dev/null +++ b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/github.svg @@ -0,0 +1,18 @@ + + + 矩形备份 24 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/logout.svg b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/logout.svg new file mode 100644 index 00000000000..d6b2a0b7486 --- /dev/null +++ b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/logout.svg @@ -0,0 +1,17 @@ + + + 矩形备份 24 + + + + + + + + + + + + + + diff --git a/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/setting.svg b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/setting.svg new file mode 100644 index 00000000000..3de0838311d --- /dev/null +++ b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/icons/setting.svg @@ -0,0 +1,16 @@ + + + 矩形备份 24 + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/flat-web/src/components/MainPageLayoutHorizontalContainer/index.tsx b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/index.tsx new file mode 100644 index 00000000000..66b0ed6d39a --- /dev/null +++ b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/index.tsx @@ -0,0 +1,112 @@ +/* eslint-disable react/display-name */ +import deviceSVG from "./icons/device.svg"; +import deviceActiveSVG from "./icons/device-active.svg"; +import settingSVG from "./icons/setting.svg"; +import gitHubSVG from "./icons/github.svg"; +import feedbackSVG from "./icons/feedback.svg"; +import logoutSVG from "./icons/logout.svg"; + +import React, { useContext } from "react"; +import { useHistory, useLocation } from "react-router-dom"; +import { MainPageLayoutHorizontal, MainPageLayoutItem, MainPageLayoutProps } from "flat-components"; +import { routeConfig, RouteNameType } from "../../route-config"; +import { GlobalStoreContext } from "../StoreProvider"; + +export interface MainPageLayoutHorizontalContainerProps { + subMenu?: MainPageLayoutItem[]; + activeKeys?: string[]; + onRouteChange?: MainPageLayoutProps["onClick"]; +} + +export const MainPageLayoutHorizontalContainer: React.FC = + ({ subMenu, children, activeKeys, onRouteChange }) => { + const leftMenu = [ + { + key: routeConfig[RouteNameType.HomePage].path, + icon: (active: boolean): React.ReactNode => <>, + title: "首页", + route: routeConfig[RouteNameType.HomePage].path, + }, + { + key: routeConfig[RouteNameType.CloudStoragePage].path, + icon: (active: boolean): React.ReactNode => <>, + title: "云盘", + route: routeConfig[RouteNameType.CloudStoragePage].path, + }, + ]; + + const rightMenu = [ + { + key: "deviceCheck", + icon: (active: boolean): React.ReactNode => ( + + ), + title: "deviceCheck", + route: routeConfig[RouteNameType.SystemCheckPage].path, + }, + ]; + + const popMenu = [ + { + key: routeConfig[RouteNameType.GeneralSettingPage].path, + icon: (): React.ReactNode => , + title: "个人设置", + route: routeConfig[RouteNameType.GeneralSettingPage].path, + }, + { + key: "getGitHubCode", + icon: (): React.ReactNode => , + title: "获取源码", + route: "https://github.com/netless-io/flat/", + }, + { + key: "feedback", + icon: (): React.ReactNode => , + title: "反馈意见", + route: "https://github.com/netless-io/flat/issues", + }, + { + key: "logout", + icon: (): React.ReactNode => , + title: ( + localStorage.clear()}> + 退出登录 + + ), + route: routeConfig[RouteNameType.LoginPage].path, + }, + ]; + + const location = useLocation(); + + activeKeys ??= [location.pathname]; + + const history = useHistory(); + + const globalStore = useContext(GlobalStoreContext); + + const historyPush = (mainPageLayoutItem: MainPageLayoutItem): void => { + if (mainPageLayoutItem.route.startsWith("/")) { + onRouteChange + ? onRouteChange(mainPageLayoutItem) + : history.push(mainPageLayoutItem.route); + } else { + void window.open(mainPageLayoutItem.route); + } + }; + + return ( + + {children} + + ); + }; diff --git a/web/flat-web/src/pages/HomePage/MainRoomHistoryPanel/index.tsx b/web/flat-web/src/pages/HomePage/MainRoomHistoryPanel/index.tsx new file mode 100644 index 00000000000..058694d2f2e --- /dev/null +++ b/web/flat-web/src/pages/HomePage/MainRoomHistoryPanel/index.tsx @@ -0,0 +1,17 @@ +// import "../MainRoomListPanel/MainRoomList.less"; + +import React from "react"; +import { observer } from "mobx-react-lite"; +import { MainRoomList } from "../MainRoomListPanel/MainRoomList"; +import { ListRoomsType } from "../../../apiMiddleware/flatServer"; +import { RoomList } from "flat-components"; + +export const MainRoomHistoryPanel = observer<{}>(function MainRoomHistoryPanel() { + return ( + + + + ); +}); + +export default MainRoomHistoryPanel; diff --git a/web/flat-web/src/pages/HomePage/MainRoomListPanel/MainRoomList.tsx b/web/flat-web/src/pages/HomePage/MainRoomListPanel/MainRoomList.tsx new file mode 100644 index 00000000000..d265ff133f0 --- /dev/null +++ b/web/flat-web/src/pages/HomePage/MainRoomListPanel/MainRoomList.tsx @@ -0,0 +1,328 @@ +// import { clipboard } from "electron"; +import { message } from "antd"; +import React, { Fragment, useCallback, useContext, useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { isSameDay } from "date-fns"; +import { + InviteModal, + RemoveRoomModal, + RoomListAlreadyLoaded, + RoomListDate, + RoomListEmpty, + RoomListItem, + RoomListItemButton, + RoomListSkeletons, + RoomStatusType, +} from "flat-components"; +import { ListRoomsType } from "../../../apiMiddleware/flatServer"; +import { RoomStatus, RoomType } from "../../../apiMiddleware/flatServer/constants"; +// import { RemoveHistoryRoomModal } from "../../../components/Modal/RemoveHistoryRoomModal"; +import { GlobalStoreContext, RoomStoreContext } from "../../../components/StoreProvider"; +import { errorTips } from "../../../components/Tips/ErrorTips"; +import { RoomItem } from "../../../stores/RoomStore"; +import { useSafePromise } from "../../../utils/hooks/lifecycle"; +import { RouteNameType, usePushHistory } from "../../../utils/routes"; +import { joinRoomHandler } from "../../utils/joinRoomHandler"; + +export interface MainRoomListProps { + listRoomsType: ListRoomsType; +} + +export const MainRoomList = observer(function MainRoomList({ listRoomsType }) { + const roomStore = useContext(RoomStoreContext); + const [roomUUIDs, setRoomUUIDs] = useState(); + const [cancelModalVisible, setCancelModalVisible] = useState(false); + const [inviteModalVisible, setInviteModalVisible] = useState(false); + const [removeHistoryVisible, setRemoveHistoryVisible] = useState(false); + const [removeHistoryLoading, setRemoveHistoryLoading] = useState(false); + const [currentRoom, setCurrentRoom] = useState(undefined); + const pushHistory = usePushHistory(); + const sp = useSafePromise(); + const globalStore = useContext(GlobalStoreContext); + const isHistoryList = listRoomsType === ListRoomsType.History; + + const refreshRooms = useCallback( + async function refreshRooms(): Promise { + try { + const roomUUIDs = await sp(roomStore.listRooms(listRoomsType, { page: 1 })); + setRoomUUIDs(roomUUIDs); + } catch (e) { + setRoomUUIDs([]); + errorTips(e); + } + }, + [listRoomsType, roomStore, sp], + ); + + useEffect(() => { + void refreshRooms(); + + const ticket = window.setInterval(refreshRooms, 30 * 1000); + + return () => { + window.clearInterval(ticket); + }; + }, [refreshRooms]); + + if (!roomUUIDs) { + return ; + } + + if (roomUUIDs.length <= 0) { + return ; + } + + const periodicInfo = currentRoom?.periodicUUID + ? roomStore.periodicRooms.get(currentRoom?.periodicUUID) + : undefined; + + return ( + <> + {customSort(roomUUIDs.map(roomUUID => roomStore.rooms.get(roomUUID))).map( + (room, index, rooms) => { + if (!room) { + return null; + } + + const lastRoom = index > 0 ? rooms[index - 1] : void 0; + // const nextRoom = index < rooms.length - 1 ? rooms[index + 1] : void 0; + + // show date title when two adjacent rooms are not the same day + const shouldShowDate = !( + room.beginTime && + lastRoom?.beginTime && + isSameDay(room.beginTime, lastRoom.beginTime) + ); + + // show divider when two adjacent rooms are not the same day + // const shouldShowDivider = !( + // room.beginTime && + // nextRoom?.beginTime && + // isSameDay(room.beginTime, nextRoom.beginTime) + // ); + + const beginTime = room.beginTime ? new Date(room.beginTime) : void 0; + const endTime = room.endTime ? new Date(room.endTime) : void 0; + + const primaryAction: RoomListItemButton<"replay" | "join"> = isHistoryList + ? { key: "replay", text: "回放", disabled: !room.hasRecord } + : { key: "join", text: "加入" }; + + return ( + + {shouldShowDate && beginTime && } + { + switch (key) { + case "details": { + pushHistory(RouteNameType.RoomDetailPage, { + roomUUID: room.roomUUID, + periodicUUID: room.periodicUUID, + }); + break; + } + case "modify": { + pushHistory(RouteNameType.ModifyOrdinaryRoomPage, { + roomUUID: room.roomUUID, + periodicUUID: room.periodicUUID, + }); + break; + } + case "cancel": { + setCurrentRoom(room); + setCancelModalVisible(true); + break; + } + case "invite": { + setCurrentRoom(room); + setInviteModalVisible(true); + break; + } + case "delete-history": { + setCurrentRoom(room); + setRemoveHistoryVisible(true); + break; + } + case "replay": { + replayRoom({ + ownerUUID: room.ownerUUID, + roomUUID: room.roomUUID, + roomType: room.roomType || RoomType.OneToOne, + }); + break; + } + case "join": { + void joinRoomHandler(room.roomUUID, pushHistory); + break; + } + default: + } + }} + /> + + ); + }, + )} + + {currentRoom && ( + + )} + {currentRoom && ( + + )} + {/* TODO: add removeHistoryLoading to flat-component */} + {/* {currentRoom && ( + + )} */} + + ); + + function replayRoom(config: { roomUUID: string; ownerUUID: string; roomType: RoomType }): void { + pushHistory(RouteNameType.ReplayPage, config); + } + + function hideCancelModal(): void { + setCancelModalVisible(false); + } + + function hideInviteModal(): void { + setInviteModalVisible(false); + } + + function hideRemoveHistoryModal(): void { + setRemoveHistoryVisible(false); + } + + function onCopy(text: string): void { + navigator.clipboard.writeText(text); + void message.success("复制成功"); + hideInviteModal(); + } + + async function removeRoomHandler(isCancelAll: boolean): Promise { + const { ownerUUID, roomUUID, periodicUUID } = currentRoom!; + const isCreator = ownerUUID === globalStore.userUUID; + try { + if (!isCreator && periodicUUID) { + await roomStore.cancelRoom({ + all: true, + periodicUUID, + }); + } else { + await roomStore.cancelRoom({ + all: isCancelAll || (!roomUUID && !!periodicUUID), + roomUUID, + periodicUUID, + }); + } + setCancelModalVisible(false); + void refreshRooms(); + const content = isCreator ? "已取消该房间" : "已移除该房间"; + void message.success(content); + } catch (e) { + console.error(e); + errorTips(e); + } + } + + async function removeConfirm(): Promise { + setRemoveHistoryLoading(true); + try { + await sp( + roomStore.cancelRoom({ + isHistory: true, + roomUUID: currentRoom!.roomUUID, + }), + ); + hideRemoveHistoryModal(); + void refreshRooms(); + } catch (e) { + console.error(e); + errorTips(e); + } finally { + setRemoveHistoryLoading(false); + } + } + + type SubActions = + | Array<{ key: "details" | "delete-history"; text: string }> + | Array<{ key: "details" | "modify" | "cancel" | "invite"; text: string }>; + + function getSubActions(room: RoomItem): SubActions { + const result = [{ key: "details", text: "房间详情" }]; + if (isHistoryList) { + if (room.roomUUID) { + result.push({ key: "delete-history", text: "删除记录" }); + } + } else { + const ownerUUID = room.ownerUUID; + const isCreator = ownerUUID === globalStore.userUUID; + if ( + (room.roomUUID || room.periodicUUID) && + isCreator && + room.roomStatus === RoomStatus.Idle + ) { + result.push({ key: "modify", text: "修改房间" }); + } + if (!isCreator || room.roomStatus === RoomStatus.Idle) { + result.push({ key: "cancel", text: isCreator ? "取消房间" : "移除房间" }); + } + if (room.roomUUID) { + result.push({ key: "invite", text: "复制邀请" }); + } + } + return result as SubActions; + } + + function customSort(rooms: Array): Array { + if (listRoomsType === ListRoomsType.History) { + return rooms.sort((a, b) => (a && b ? Number(b.beginTime) - Number(a.beginTime) : 0)); + } else { + return rooms; + } + } + + function getRoomStatus(roomStatus?: RoomStatus): RoomStatusType { + switch (roomStatus) { + case RoomStatus.Idle: { + return "upcoming"; + } + case RoomStatus.Started: + case RoomStatus.Paused: { + return "running"; + } + case RoomStatus.Stopped: { + return "stopped"; + } + default: { + return "upcoming"; + } + } + } +}); diff --git a/web/flat-web/src/pages/HomePage/MainRoomListPanel/index.tsx b/web/flat-web/src/pages/HomePage/MainRoomListPanel/index.tsx new file mode 100644 index 00000000000..bc655714b26 --- /dev/null +++ b/web/flat-web/src/pages/HomePage/MainRoomListPanel/index.tsx @@ -0,0 +1,41 @@ +import "./style.less"; + +import React, { useMemo, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { RoomList } from "flat-components"; +import { MainRoomList } from "./MainRoomList"; +import { ListRoomsType } from "../../../apiMiddleware/flatServer"; + +export const MainRoomListPanel = observer<{}>(function MainRoomListPanel() { + const [activeTab, setActiveTab] = useState<"all" | "today" | "periodic">("all"); + const filters = useMemo>( + () => [ + { + key: "all", + title: "全部", + }, + { + key: "today", + title: "今天", + }, + { + key: "periodic", + title: "周期", + }, + ], + [], + ); + + return ( + + + + ); +}); + +export default MainRoomListPanel; diff --git a/web/flat-web/src/pages/HomePage/MainRoomListPanel/style.less b/web/flat-web/src/pages/HomePage/MainRoomListPanel/style.less new file mode 100644 index 00000000000..a7ab2cebd3a --- /dev/null +++ b/web/flat-web/src/pages/HomePage/MainRoomListPanel/style.less @@ -0,0 +1,3 @@ +.room-list { + border-radius: 8px 8px 0 0; +} diff --git a/web/flat-web/src/pages/HomePage/MainRoomMenu/CreateRoomBox.less b/web/flat-web/src/pages/HomePage/MainRoomMenu/CreateRoomBox.less new file mode 100644 index 00000000000..c43e2b9db13 --- /dev/null +++ b/web/flat-web/src/pages/HomePage/MainRoomMenu/CreateRoomBox.less @@ -0,0 +1,19 @@ +.create-room-box-container { + > .ant-modal { + top: 50px; + > .ant-modal-content { + > .ant-modal-header { + border-bottom: none; + padding: 16px; + } + + > .ant-modal-body { + padding: 0 16px; + } + + > .ant-modal-footer { + border-top: none; + } + } + } +} diff --git a/web/flat-web/src/pages/HomePage/MainRoomMenu/CreateRoomBox.tsx b/web/flat-web/src/pages/HomePage/MainRoomMenu/CreateRoomBox.tsx new file mode 100644 index 00000000000..1d71078de91 --- /dev/null +++ b/web/flat-web/src/pages/HomePage/MainRoomMenu/CreateRoomBox.tsx @@ -0,0 +1,153 @@ +import createSVG from "../../../assets/image/creat.svg"; +import "./CreateRoomBox.less"; + +import React, { useContext, useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Button, Input, Modal, Checkbox, Form } from "antd"; +import { RoomType } from "../../../apiMiddleware/flatServer/constants"; +import { ConfigStoreContext, GlobalStoreContext } from "../../../components/StoreProvider"; +import { useSafePromise } from "../../../utils/hooks/lifecycle"; +import { ClassPicker } from "flat-components"; + +interface CreateRoomFormValues { + roomTitle: string; + roomType: RoomType; + autoCameraOn: boolean; +} + +export interface CreateRoomBoxProps { + onCreateRoom: (title: string, type: RoomType) => Promise; +} + +export const CreateRoomBox = observer(function CreateRoomBox({ onCreateRoom }) { + const sp = useSafePromise(); + const globalStore = useContext(GlobalStoreContext); + const configStore = useContext(ConfigStoreContext); + const [form] = Form.useForm(); + + const [isLoading, setLoading] = useState(false); + const [isShowModal, showModal] = useState(false); + const [isFormValidated, setIsFormValidated] = useState(false); + const [classType, setClassType] = useState(RoomType.BigClass); + const roomTitleInputRef = useRef(null); + + const defaultValues: CreateRoomFormValues = { + roomTitle: globalStore.userInfo?.name ? `${globalStore.userInfo.name}创建的房间` : "", + roomType: RoomType.BigClass, + autoCameraOn: configStore.autoCameraOn, + }; + + useEffect(() => { + let ticket = NaN; + if (isShowModal) { + // wait a cycle till antd modal updated + ticket = window.setTimeout(() => { + if (roomTitleInputRef.current) { + roomTitleInputRef.current.focus(); + roomTitleInputRef.current.select(); + } + }, 0); + } + return () => { + window.clearTimeout(ticket); + }; + }, [isShowModal]); + + return ( + <> + + + 取消 + , + , + ]} + > +
+ + + + + setClassType(RoomType[e])} /> + + + + 开启摄像头 + + +
+
+ + ); + + async function handleOk(): Promise { + try { + await sp(form.validateFields()); + } catch (e) { + // errors are showed on form + return; + } + + setLoading(true); + + try { + const values = form.getFieldsValue(); + configStore.updateAutoCameraOn(values.autoCameraOn); + await sp(onCreateRoom(values.roomTitle, values.roomType)); + setLoading(false); + showModal(false); + } catch (e) { + console.error(e); + setLoading(false); + } + } + + function handleCancel(): void { + showModal(false); + } + + function formValidateStatus(): void { + setIsFormValidated(form.getFieldsError().every(field => field.errors.length <= 0)); + } +}); + +export default CreateRoomBox; diff --git a/web/flat-web/src/pages/HomePage/MainRoomMenu/JoinRoomBox.less b/web/flat-web/src/pages/HomePage/MainRoomMenu/JoinRoomBox.less new file mode 100644 index 00000000000..1bd3397dc56 --- /dev/null +++ b/web/flat-web/src/pages/HomePage/MainRoomMenu/JoinRoomBox.less @@ -0,0 +1,18 @@ +.join-room-box-container { + > .ant-modal { + > .ant-modal-content { + > .ant-modal-header { + border-bottom: none; + padding: 16px; + } + + > .ant-modal-body { + padding: 0 16px; + } + + > .ant-modal-footer { + border-top: none; + } + } + } +} diff --git a/web/flat-web/src/pages/HomePage/MainRoomMenu/JoinRoomBox.tsx b/web/flat-web/src/pages/HomePage/MainRoomMenu/JoinRoomBox.tsx new file mode 100644 index 00000000000..38592ea56c0 --- /dev/null +++ b/web/flat-web/src/pages/HomePage/MainRoomMenu/JoinRoomBox.tsx @@ -0,0 +1,186 @@ +import joinSVG from "../../../assets/image/join.svg"; +import "./JoinRoomBox.less"; + +import React, { useContext, useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Button, Input, Modal, Checkbox, Form } from "antd"; +import { validate, version } from "uuid"; +import { ConfigStoreContext } from "../../../components/StoreProvider"; +import { useSafePromise } from "../../../utils/hooks/lifecycle"; + +interface JoinRoomFormValues { + roomUUID: string; + autoCameraOn: boolean; + autoMicOn: boolean; +} + +export interface JoinRoomBoxProps { + onJoinRoom: (roomUUID: string) => Promise; +} + +export const JoinRoomBox = observer(function JoinRoomBox({ onJoinRoom }) { + const sp = useSafePromise(); + const configStore = useContext(ConfigStoreContext); + const [form] = Form.useForm(); + + const [isLoading, setLoading] = useState(false); + const [isShowModal, showModal] = useState(false); + const [isFormValidated, setIsFormValidated] = useState(false); + const roomTitleInputRef = useRef(null); + + useEffect(() => { + let ticket = NaN; + if (isShowModal) { + // wait a cycle till antd modal updated + ticket = window.setTimeout(() => { + if (roomTitleInputRef.current) { + roomTitleInputRef.current.focus(); + roomTitleInputRef.current.select(); + } + }, 0); + } + return () => { + window.clearTimeout(ticket); + }; + }, [isShowModal]); + + const defaultValues: JoinRoomFormValues = { + roomUUID: "", + autoCameraOn: configStore.autoCameraOn, + autoMicOn: configStore.autoMicOn, + }; + + // const historyMenu = ( + // + // {/* {// @TODO add join room history + // joinRoomHistories.map(room => ( + // {room.name || room.uuid} + // ))} */} + // + // + // + // ); + + return ( + <> + + + 取消 + , + , + ]} + > +
+ + + // {"dropdown"} + // + // } + /> + + {/* + + */} + + + 开启麦克风 + + + 开启摄像头 + + +
+
+ + ); + + async function handleShowModal(): Promise { + try { + const roomUUID = await navigator.clipboard.readText(); + if (validate(roomUUID) && version(roomUUID) === 4) { + form.setFieldsValue({ roomUUID }); + setIsFormValidated(true); + } + } catch { + // ignore + } + showModal(true); + } + + async function handleOk(): Promise { + try { + await sp(form.validateFields()); + } catch (e) { + // errors are displayed on the form + return; + } + + setLoading(true); + + try { + const values = form.getFieldsValue(); + configStore.updateAutoMicOn(values.autoMicOn); + configStore.updateAutoCameraOn(values.autoCameraOn); + await sp(onJoinRoom(values.roomUUID)); + setLoading(false); + showModal(false); + } catch (e) { + console.error(e); + setLoading(false); + } + } + + function handleCancel(): void { + showModal(false); + } + + function formValidateStatus(): void { + setIsFormValidated(form.getFieldsError().every(field => field.errors.length <= 0)); + } +}); diff --git a/web/flat-web/src/pages/HomePage/MainRoomMenu/MainRoomMenu.less b/web/flat-web/src/pages/HomePage/MainRoomMenu/MainRoomMenu.less new file mode 100644 index 00000000000..380a3714ae8 --- /dev/null +++ b/web/flat-web/src/pages/HomePage/MainRoomMenu/MainRoomMenu.less @@ -0,0 +1,88 @@ +.main-room-menu-container { + font-size: 14px; + + .ant-btn { + display: inline-flex; + align-items: center; + width: 176px; + height: 72px; + margin: 24px 0px; + margin-right: 16px; + border-radius: 8px; + background-color: white; + font-size: 16px; + border-color: white; + + img { + margin-right: 14px; + } + + &:hover { + color: inherit; + border-color: white; + box-shadow: 0px 16px 32px 0px rgba(0, 0, 0, 0.08); + } + + .label { + margin-right: 12px; + } + } +} + +.main-room-menu-form { + .ant-form-item-required::before { + display: none !important; + } + + .ant-form-item-label > label { + color: #7a7b7c; + } + + .ant-checkbox + span { + font-size: 12px; + color: #7a7b7c; + } + + .ant-form-item { + margin-bottom: 0; + margin-top: 13px; + } +} + +.modal-inner-name { + font-size: 14px; + font-weight: 400; + color: #7a7b7c; + line-height: 21px; + margin-bottom: 8px; +} + +.modal-inner-input { + margin-bottom: 24px; +} + +.modal-inner-check { + margin-bottom: 8px; +} + +.modal-inner-text { + font-size: 12px; + font-weight: 400; + color: #7a7b7c; + line-height: 18px; +} + +.modal-dropdown-icon { + margin-right: -8px; + cursor: pointer; +} + +.modal-menu-item { + margin-top: 12px; + width: 320px; + margin-right: -4px; +} + +.modal-inner-select { + width: 320px; +} diff --git a/web/flat-web/src/pages/HomePage/MainRoomMenu/ScheduleRoomBox.tsx b/web/flat-web/src/pages/HomePage/MainRoomMenu/ScheduleRoomBox.tsx new file mode 100644 index 00000000000..6fb56a88467 --- /dev/null +++ b/web/flat-web/src/pages/HomePage/MainRoomMenu/ScheduleRoomBox.tsx @@ -0,0 +1,18 @@ +import bookSVG from "../../../assets/image/book.svg"; + +import React from "react"; +import { Button } from "antd"; +import { RouteNameType, usePushHistory } from "../../../utils/routes"; + +export const ScheduleRoomBox = React.memo<{}>(function ScheduleRoomBox() { + const pushHistory = usePushHistory(); + + return ( + + ); +}); + +export default ScheduleRoomBox; diff --git a/web/flat-web/src/pages/HomePage/MainRoomMenu/index.tsx b/web/flat-web/src/pages/HomePage/MainRoomMenu/index.tsx new file mode 100644 index 00000000000..6b1f0a4188c --- /dev/null +++ b/web/flat-web/src/pages/HomePage/MainRoomMenu/index.tsx @@ -0,0 +1,40 @@ +import "./MainRoomMenu.less"; + +import React, { FC, useContext } from "react"; +import { RoomType } from "../../../apiMiddleware/flatServer/constants"; +import { RoomStoreContext } from "../../../components/StoreProvider"; +import { usePushHistory } from "../../../utils/routes"; +import { CreateRoomBox } from "./CreateRoomBox"; +import { JoinRoomBox } from "./JoinRoomBox"; +import { ScheduleRoomBox } from "./ScheduleRoomBox"; +import { joinRoomHandler } from "../../utils/joinRoomHandler"; +import { errorTips } from "../../../components/Tips/ErrorTips"; + +export const MainRoomMenu: FC = () => { + const roomStore = useContext(RoomStoreContext); + const pushHistory = usePushHistory(); + + return ( +
+ joinRoomHandler(roomUUID, pushHistory)} /> + + +
+ ); + + async function createOrdinaryRoom(title: string, type: RoomType): Promise { + try { + const roomUUID = await roomStore.createOrdinaryRoom({ + title, + type, + beginTime: Date.now(), + // TODO docs:[] + }); + await joinRoomHandler(roomUUID, pushHistory); + } catch (e) { + errorTips(e); + } + } +}; + +export default MainRoomMenu; diff --git a/web/flat-web/src/pages/HomePage/index.tsx b/web/flat-web/src/pages/HomePage/index.tsx index 6b0bef8bb44..310347ca1fc 100644 --- a/web/flat-web/src/pages/HomePage/index.tsx +++ b/web/flat-web/src/pages/HomePage/index.tsx @@ -1,5 +1,70 @@ -import React from "react"; +import "./style.less"; -export const HomePage: React.FC = () => <>HomePage; +import React, { useContext, useEffect } from "react"; +import { observer } from "mobx-react-lite"; +import { MainRoomMenu } from "./MainRoomMenu"; +import { MainPageLayoutHorizontalContainer } from "../../components/MainPageLayoutHorizontalContainer"; +import { MainRoomListPanel } from "./MainRoomListPanel"; +import { MainRoomHistoryPanel } from "./MainRoomHistoryPanel"; +import { RouteNameType, usePushHistory } from "../../utils/routes"; +import { GlobalStoreContext } from "../../components/StoreProvider"; +import { loginCheck } from "../../apiMiddleware/flatServer"; +import { errorTips } from "../../components/Tips/ErrorTips"; + +export const HomePage = observer(function HomePage() { + const pushHistory = usePushHistory(); + const globalStore = useContext(GlobalStoreContext); + + useEffect(() => { + let isUnMount = false; + + async function checkLogin(): Promise { + let nextPage = RouteNameType.LoginPage; + + const token = globalStore.userInfo?.token; + if (token) { + try { + await loginCheck(); + nextPage = RouteNameType.HomePage; + } catch (e) { + console.error(e); + errorTips(e); + } + } + + // if (!isUnMount) { + // updateLoginStatus(LoginStatusType.Success); + // } + + return nextPage; + } + + void Promise.all([checkLogin()]).then(([nextPage]) => { + if (!isUnMount) { + if (nextPage !== RouteNameType.HomePage) { + pushHistory(nextPage); + } + } + }); + + return () => { + isUnMount = true; + }; + // Only check login once on start + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + +
+ +
+ + +
+
+
+ ); +}); export default HomePage; diff --git a/web/flat-web/src/pages/HomePage/style.less b/web/flat-web/src/pages/HomePage/style.less new file mode 100644 index 00000000000..e3a3e63b9f6 --- /dev/null +++ b/web/flat-web/src/pages/HomePage/style.less @@ -0,0 +1,19 @@ +.homepage-layout-horizontal-container { + display: flex; + flex-direction: column; + height: 100%; + width: 960px; + margin: 0 auto; + overflow: hidden; +} + +.homepage-layout-horizontal-content { + flex: 1; + min-width: 960px; + width: 960px; + display: grid; + grid-template-columns: repeat(2, 1fr); + column-gap: 16px; + justify-content: space-between; + overflow: hidden; +} diff --git a/web/flat-web/src/pages/utils/joinRoomHandler.ts b/web/flat-web/src/pages/utils/joinRoomHandler.ts new file mode 100644 index 00000000000..ec218b58ff7 --- /dev/null +++ b/web/flat-web/src/pages/utils/joinRoomHandler.ts @@ -0,0 +1,33 @@ +import { RouteNameType, usePushHistory } from "../../utils/routes"; +import { roomStore } from "../../stores/RoomStore"; +import { RoomType } from "../../apiMiddleware/flatServer/constants"; +import { errorTips } from "../../components/Tips/ErrorTips"; + +export const joinRoomHandler = async ( + roomUUID: string, + pushHistory: ReturnType, +): Promise => { + try { + const data = await roomStore.joinRoom(roomUUID); + // @TODO make roomType a param + switch (data.roomType) { + case RoomType.BigClass: { + pushHistory(RouteNameType.BigClassPage, data); + break; + } + case RoomType.SmallClass: { + pushHistory(RouteNameType.SmallClassPage, data); + break; + } + case RoomType.OneToOne: { + pushHistory(RouteNameType.OneToOnePage, data); + break; + } + default: { + new Error("failed to join room: incorrect room type"); + } + } + } catch (e) { + errorTips(e); + } +}; diff --git a/web/flat-web/src/route-config.ts b/web/flat-web/src/route-config.ts index bce7664cc3a..fe071359796 100644 --- a/web/flat-web/src/route-config.ts +++ b/web/flat-web/src/route-config.ts @@ -1,6 +1,18 @@ export enum RouteNameType { LoginPage = "LoginPage", HomePage = "HomePage", + SmallClassPage = "SmallClassPage", + BigClassPage = "BigClassPage", + OneToOnePage = "OneToOnePage", + UserScheduledPage = "UserScheduledPage", + RoomDetailPage = "RoomDetailPage", + PeriodicRoomDetailPage = "PeriodicRoomDetailPage", + ReplayPage = "ReplayPage", + ModifyOrdinaryRoomPage = "ModifyOrdinaryRoomPage", + ModifyPeriodicRoomPage = "ModifyPeriodicRoomPage", + SystemCheckPage = "SystemCheckPage", + GeneralSettingPage = "GeneralSettingPage", + CloudStoragePage = "CloudStoragePage", } export const routeConfig = { @@ -10,6 +22,42 @@ export const routeConfig = { [RouteNameType.HomePage]: { path: "/", }, + [RouteNameType.SmallClassPage]: { + path: "/classroom/SmallClass/:roomUUID/:ownerUUID/", + }, + [RouteNameType.OneToOnePage]: { + path: "/classroom/OneToOne/:roomUUID/:ownerUUID/", + }, + [RouteNameType.BigClassPage]: { + path: "/classroom/BigClass/:roomUUID/:ownerUUID/", + }, + [RouteNameType.UserScheduledPage]: { + path: "/user/scheduled/", + }, + [RouteNameType.RoomDetailPage]: { + path: "/user/room/:roomUUID/:periodicUUID?/", + }, + [RouteNameType.PeriodicRoomDetailPage]: { + path: "/user/periodic/info/:periodicUUID", + }, + [RouteNameType.ReplayPage]: { + path: "/replay/:roomType/:roomUUID/:ownerUUID/", + }, + [RouteNameType.ModifyOrdinaryRoomPage]: { + path: "/modify/:roomUUID/:periodicUUID?/", + }, + [RouteNameType.ModifyPeriodicRoomPage]: { + path: "/modify/periodic/room/:periodicUUID/", + }, + [RouteNameType.SystemCheckPage]: { + path: "/device/system/", + }, + [RouteNameType.GeneralSettingPage]: { + path: "/general-settings/", + }, + [RouteNameType.CloudStoragePage]: { + path: "/pan/", + }, } as const; export type ExtraRouteConfig = {};