From 001a653aa54583439fa809aa411a968e540475ed Mon Sep 17 00:00:00 2001 From: darmiel <71837281+darmiel@users.noreply.github.com> Date: Tue, 2 Jan 2024 03:07:12 +0100 Subject: [PATCH 1/2] feat[frontend]: new follow up mode --- .../followup/MeetingFollowUpOverview.tsx | 150 +++++++++++++ .../followup/MeetingFollowUpTabActions.tsx | 203 ++++++++++++++++++ .../ui/card/glow/GlowingCardItem.tsx | 2 +- frontend/src/pages/globals.css | 8 +- .../meeting/[meeting_id]/followup.tsx | 26 +++ 5 files changed, 385 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/meeting/followup/MeetingFollowUpOverview.tsx create mode 100644 frontend/src/components/meeting/followup/MeetingFollowUpTabActions.tsx create mode 100644 frontend/src/pages/project/[project_id]/meeting/[meeting_id]/followup.tsx diff --git a/frontend/src/components/meeting/followup/MeetingFollowUpOverview.tsx b/frontend/src/components/meeting/followup/MeetingFollowUpOverview.tsx new file mode 100644 index 0000000..4d70d3d --- /dev/null +++ b/frontend/src/components/meeting/followup/MeetingFollowUpOverview.tsx @@ -0,0 +1,150 @@ +import { + Accordion, + AccordionItem, + BreadcrumbItem, + Breadcrumbs, + Divider, + ScrollShadow, +} from "@nextui-org/react" +import Head from "next/head" +import { BsChevronRight, BsHouse } from "react-icons/bs" +import { BarLoader } from "react-spinners" + +import { extractErrorMessage } from "@/api/util" +import MeetingSelectBreadcrumbs from "@/components/meeting/breadcrumbs/MeetingSelectBreadcrumbs" +import { MeetingTagChips } from "@/components/meeting/MeetingTagChips" +import ProjectSelectBreadcrumbs from "@/components/project/breadcrumbs/ProjectSelectBreadcrumbs" +import GlowingCard from "@/components/ui/card/glow/GlowingCardItem" +import GlowingCards from "@/components/ui/card/glow/GlowingCardsContainer" +import Flex from "@/components/ui/layout/Flex" +import RenderMarkdown from "@/components/ui/text/RenderMarkdown" +import { useAuth } from "@/contexts/AuthContext" +import { getMeetingURL } from "@/util/url" + +export default function MeetingFollowUpOverview({ + projectID, + meetingID, +}: { + projectID: number + meetingID: number +}) { + const { meetings, topics, actions } = useAuth() + + const meetingInfoQuery = meetings!.useFind(projectID, meetingID) + const topicListQuery = topics!.useList(projectID, meetingID) + const actionListQuery = actions!.useListForMeeting(projectID, meetingID) + + if (meetingInfoQuery.isLoading) { + return + } + if (meetingInfoQuery.isError) { + return <>Error: {extractErrorMessage(meetingInfoQuery.error)} + } + + const meeting = meetingInfoQuery.data.data + + return ( +
+ + Perplex - F/U M# {meeting.name ?? "Unknown Project"} + + +
+ + }> + Home + + + + + + + + Follow Up + +
+ +
+
+
+

+ Follow Up +

+

{meeting.name}

+ + + +
+ + {meeting.description && ( + + + + + + )} + + + + + + +

+ Open Actions +

+

2

+
+ + +

+ Closed Actions +

+

11

+
+
+ + + +

+ Open Topics +

+

0

+
+ + +

+ Closed Topics +

+

11

+
+
+
+
+
+
+ ) +} diff --git a/frontend/src/components/meeting/followup/MeetingFollowUpTabActions.tsx b/frontend/src/components/meeting/followup/MeetingFollowUpTabActions.tsx new file mode 100644 index 0000000..1b8c794 --- /dev/null +++ b/frontend/src/components/meeting/followup/MeetingFollowUpTabActions.tsx @@ -0,0 +1,203 @@ +import { + Accordion, + AccordionItem, + Avatar, + AvatarGroup, + Chip, + Link, + ScrollShadow, +} from "@nextui-org/react" +import clsx from "clsx" +import { BsCheck, BsCheckAll, BsRecord2, BsTriangleFill } from "react-icons/bs" + +import { Action, Meeting } from "@/api/types" +import Admonition from "@/components/ui/Admonition" +import Flex from "@/components/ui/layout/Flex" +import { TruncateTitle } from "@/components/ui/text/TruncateText" +import { useAuth } from "@/contexts/AuthContext" +import { getUserAvatarURL } from "@/util/avatar" +import { getActionURL } from "@/util/url" + +export function MeetingFollowUpTabActions({ + openActions, + closedActions, +}: { + openActions: Action[] + closedActions: Action[] +}) { + return ( + <> + {/* Action Follow Up Banner */} + {openActions.length === 0 ? ( + + + + All actions that were assigned to a meeting topic have been closed. + + + ) : ( + + + + There are still open actions that were assigned to a meeting topic. + + + )} + +
+ {/* Actions closed by last {date-selector} */} + + + + Finished + + {closedActions.length} + + + } + isCompact + > + + + + + {/* Actions still open */} + + + + + Still Open + + + {openActions.length} + + + } + isCompact + > + + + +
+ + ) +} + +export function MeetingFollowUpTabActionsWrapper({ + meeting, +}: { + meeting: Meeting +}) { + const { actions } = useAuth() + + const actionListQuery = actions!.useListForMeeting( + meeting.project_id, + meeting.ID, + ) + + if (actionListQuery.isLoading) { + return Loading Actions... + } + + const actionsList: Action[] = actionListQuery.data?.data || [] + if (actionsList.length === 0) { + return ( + +

No Actions

+ No actions found that were assigned to a meeting topic +
+ ) + } + + const actionListOpen = actionsList + .filter((action) => !action.closed_at.Valid) + .sort((a, b) => a.ID - b.ID) + + const actionListClosed = actionsList + .filter((action) => action.closed_at.Valid) + .sort((a, b) => a.ID - b.ID) + + return ( + <> + + + ) +} + +function MeetingFollowUpActionList({ actions }: { actions: Action[] }) { + return ( + + {actions.map((action) => ( + +
+ + + {action.closed_at.Valid ? : } + + + {action.title} + + #{action.ID} + + + {/* Action Actions */} + + + {action.assigned_users.map((user) => ( + + ))} + + + + {action.tags?.length > 0 ? ( + action.tags.map((tag) => ( + + {tag.title} + + )) + ) : ( + + No Tags + + )} + + + +
+ + ))} +
+ ) +} diff --git a/frontend/src/components/ui/card/glow/GlowingCardItem.tsx b/frontend/src/components/ui/card/glow/GlowingCardItem.tsx index bdc6330..bd77d67 100644 --- a/frontend/src/components/ui/card/glow/GlowingCardItem.tsx +++ b/frontend/src/components/ui/card/glow/GlowingCardItem.tsx @@ -4,7 +4,7 @@ import { ComponentPropsWithoutRef, ElementType, ReactNode } from "react" // GlowingCardProps contains the props for the GlowingCard export type GlowingCardProps = { // children is the content of the GlowingCard - children: ReactNode + children?: ReactNode // classNames is the class names for the GlowingCard classNames?: GlowingCardClassNames // isSingle is whether the GlowingCard is a single card diff --git a/frontend/src/pages/globals.css b/frontend/src/pages/globals.css index f6bdd66..381a60d 100644 --- a/frontend/src/pages/globals.css +++ b/frontend/src/pages/globals.css @@ -6,6 +6,8 @@ --foreground-rgb: 0, 0, 0; --background-start-rgb: 214, 219, 220; --background-end-rgb: 255, 255, 255; + + --glow-color: 255, 255, 255; } @media (prefers-color-scheme: dark) { @@ -50,7 +52,7 @@ body { } .glowing-card { - background-color: rgba(255, 255, 255, 0.2); + background-color: rgba(var(--glow-color), 0.2); border-radius: 10px; position: relative; } @@ -81,7 +83,7 @@ body { .glowing-card::before { background: radial-gradient( 800px circle at var(--mouse-x) var(--mouse-y), - rgba(255, 255, 255, 0.03), + rgba(var(--glow-color), 0.03), transparent 40% ); z-index: 3; @@ -92,7 +94,7 @@ body { .glowing-card::after { background: radial-gradient( 400px circle at var(--mouse-x) var(--mouse-y), - rgba(255, 255, 255, 0.3), + rgba(var(--glow-color), 0.3), transparent 40% ); z-index: 1; diff --git a/frontend/src/pages/project/[project_id]/meeting/[meeting_id]/followup.tsx b/frontend/src/pages/project/[project_id]/meeting/[meeting_id]/followup.tsx new file mode 100644 index 0000000..73d2a20 --- /dev/null +++ b/frontend/src/pages/project/[project_id]/meeting/[meeting_id]/followup.tsx @@ -0,0 +1,26 @@ +import { useRouter } from "next/router" + +import MeetingFollowUpOverview from "@/components/meeting/followup/MeetingFollowUpOverview" + +export default function FollowUpPage() { + const router = useRouter() + + const { project_id: projectIDStr, meeting_id: meetingIDStr } = router.query + if ( + !projectIDStr || + !meetingIDStr || + Array.isArray(projectIDStr) || + Array.isArray(meetingIDStr) + ) { + return
Invalid URL
+ } + + const projectID = Number(projectIDStr) + const meetingID = Number(meetingIDStr) + + return ( +
+ +
+ ) +} From 591c1eb0013729ad4a91158c17cb73497c880662 Mon Sep 17 00:00:00 2001 From: darmiel <71837281+darmiel@users.noreply.github.com> Date: Tue, 2 Jan 2024 18:16:34 +0100 Subject: [PATCH 2/2] feat[followup]: added topic and action stats --- .../followup/MeetingFollowUpOverview.tsx | 131 +++++++++----- .../components/modals/MeetingCreateModal.tsx | 163 ------------------ .../ui/card/glow/GlowingCardItem.tsx | 2 +- .../components/ui/modal/GlowingModalCard.tsx | 53 ------ frontend/src/components/user/UserAvatar.tsx | 1 + 5 files changed, 89 insertions(+), 261 deletions(-) delete mode 100644 frontend/src/components/modals/MeetingCreateModal.tsx delete mode 100644 frontend/src/components/ui/modal/GlowingModalCard.tsx diff --git a/frontend/src/components/meeting/followup/MeetingFollowUpOverview.tsx b/frontend/src/components/meeting/followup/MeetingFollowUpOverview.tsx index 4d70d3d..10f7c53 100644 --- a/frontend/src/components/meeting/followup/MeetingFollowUpOverview.tsx +++ b/frontend/src/components/meeting/followup/MeetingFollowUpOverview.tsx @@ -3,10 +3,13 @@ import { AccordionItem, BreadcrumbItem, Breadcrumbs, + Card, Divider, ScrollShadow, } from "@nextui-org/react" +import clsx from "clsx" import Head from "next/head" +import { Fragment, ReactNode } from "react" import { BsChevronRight, BsHouse } from "react-icons/bs" import { BarLoader } from "react-spinners" @@ -43,6 +46,14 @@ export default function MeetingFollowUpOverview({ const meeting = meetingInfoQuery.data.data + const actionList = actionListQuery.data?.data ?? [] + const openActions = actionList.filter((action) => !action.closed_at.Valid) + const closedActions = actionList.filter((action) => action.closed_at.Valid) + + const topicList = topicListQuery.data?.data ?? [] + const openTopics = topicList.filter((topic) => !topic.closed_at.Valid) + const closedTopics = topicList.filter((topic) => topic.closed_at.Valid) + return (
@@ -98,53 +109,85 @@ export default function MeetingFollowUpOverview({ - - -

- Open Actions -

-

2

-
- - -

- Closed Actions -

-

11

-
-
- - - -

- Open Topics -

-

0

-
- - -

- Closed Topics -

-

11

-
-
+ + +
) } + +type Statistic = { + title: string + value: any + isDanger?: boolean +} + +function StatisticCard({ + title, + stats, +}: { + title: string | ReactNode + stats: Statistic[] +}) { + return ( + + + {title} + +
+ {stats.map((stat, index) => ( + + {index > 0 && } + +

+ {stat.title} +

+

+ {stat.value} +

+
+
+ ))} +
+
+ ) +} diff --git a/frontend/src/components/modals/MeetingCreateModal.tsx b/frontend/src/components/modals/MeetingCreateModal.tsx deleted file mode 100644 index 6ec8a92..0000000 --- a/frontend/src/components/modals/MeetingCreateModal.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { useEffect, useState } from "react" -import DatePicker from "react-datepicker" - -import "react-datepicker/dist/react-datepicker.css" - -import { Checkbox, Input, Textarea } from "@nextui-org/react" -import { BsTriangle } from "react-icons/bs" -import { toast } from "sonner" - -import { extractErrorMessage, PickerCustomInput } from "@/api/util" -import Button from "@/components/ui/Button" -import Hr from "@/components/ui/Hr" -import Flex from "@/components/ui/layout/Flex" -import { useAuth } from "@/contexts/AuthContext" - -export default function MeetingCreateModal({ - projectID, - onClose, -}: { - projectID: number - onClose: (newMeetingID?: number) => void -}) { - const [meetingTitle, setMeetingTitle] = useState("") - const [meetingDescription, setMeetingDescription] = useState("") - const [meetingStartDate, setMeetingStartDate] = useState(new Date()) - const [meetingEndDate, setMeetingEndDate] = useState(new Date()) - - const [createAnother, setCreateAnother] = useState(false) - - useEffect(() => { - // get days since epoch for start and end date - const startDateDays = Math.floor(meetingStartDate.getTime() / 86400000) - const endDateDays = Math.floor(meetingEndDate.getTime() / 86400000) - if (endDateDays < startDateDays) { - // set meeting end date to start date + 30 mins - setMeetingEndDate(new Date(meetingStartDate.getTime() + 30 * 60000)) - } - }, [meetingStartDate, meetingEndDate]) - - const { meetings } = useAuth() - - const createMeetingMutation = meetings!.useCreate( - projectID, - ({ data }, { __should_close }) => { - toast.success(`Meeting #${data.ID} Created`) - - // clear form - setMeetingTitle("") - setMeetingDescription("") - setMeetingStartDate(new Date()) - setMeetingEndDate(new Date()) - - __should_close && onClose?.(data.ID) - }, - ) - - function create(shouldClose: boolean) { - if (createMeetingMutation.isLoading) { - return - } - createMeetingMutation.mutate({ - title: meetingTitle, - description: meetingDescription, - start_date: meetingStartDate, - end_date: meetingEndDate, - __should_close: shouldClose, - }) - } - - return ( -
-

Create New Meeting

- - e.key === "Enter" && create(false)} - autoComplete="off" - /> - -