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"
- />
-
-
-
-
-
-
-
-
date && setMeetingStartDate(date)}
- timeInputLabel="Time:"
- dateFormat="MM/dd/yyyy h:mm aa"
- customInput={}
- showTimeInput
- />
-
-
-
-
-
-
date && setMeetingEndDate(date)}
- timeInputLabel="Time:"
- dateFormat="MM/dd/yyyy h:mm aa"
- customInput={}
- showTimeInput
- minDate={meetingStartDate}
- />
-
-
-
-
-
-
- {createMeetingMutation.isError && (
-
-
-
-
-
{extractErrorMessage(createMeetingMutation.error)}
-
- )}
-
-
-
-
-
- Create Another?
-
-
-
-
-
- )
-}
diff --git a/frontend/src/components/ui/card/glow/GlowingCardItem.tsx b/frontend/src/components/ui/card/glow/GlowingCardItem.tsx
index bd77d67..1df1844 100644
--- a/frontend/src/components/ui/card/glow/GlowingCardItem.tsx
+++ b/frontend/src/components/ui/card/glow/GlowingCardItem.tsx
@@ -12,7 +12,7 @@ export type GlowingCardProps = {
isSingle?: boolean
// as is the element type for the GlowingCard
as?: As
-} & Omit, "as">
+} & Omit, "as" | "onMouseMove">
export default function GlowingCard(
props: GlowingCardProps,
diff --git a/frontend/src/components/ui/modal/GlowingModalCard.tsx b/frontend/src/components/ui/modal/GlowingModalCard.tsx
deleted file mode 100644
index 24a119e..0000000
--- a/frontend/src/components/ui/modal/GlowingModalCard.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import clsx from "clsx"
-import { ReactNode } from "react"
-
-// GlowingModalCardProps contains the props for the GlowingModalCard
-export type GlowingModalCardProps = {
- // children is the content of the GlowingModalCard
- children: ReactNode
- // classNames is the class names for the GlowingModalCard
- classNames?: GlowingModalCardClassNames
- // onClick is the click event handler for the GlowingModalCard
- onClick?: () => void
-}
-
-export default function GlowingModalCard({
- children,
- classNames,
- onClick,
-}: GlowingModalCardProps) {
- const containerCls = classNames?.container ?? defaultClassNames.container
- const contentCls = classNames?.content ?? defaultClassNames.content
- return (
-
- )
-}
-
-// GlowingModalCardClassNames contains the class names for the GlowingModalCard
-type GlowingModalCardClassNames = {
- container?: string
- content?: string
-}
-
-// defaultClassNames is the default class names for the GlowingModalCard
-const defaultClassNames: GlowingModalCardClassNames = {
- container: "h-fit w-1/2 min-w-fit",
- content: "space-y-4 bg-neutral-950 p-4",
-} as const
-
-// onMouseMove is the mouse move event handler for the GlowingModalCard
-// It sets the CSS variables for the mouse position (relative to the card)
-const onMouseMove = (event: React.MouseEvent) => {
- const { clientX, clientY } = event
- const { left, top } = event.currentTarget.getBoundingClientRect()
- event.currentTarget.style.setProperty("--mouse-x", `${clientX - left}px`)
- event.currentTarget.style.setProperty("--mouse-y", `${clientY - top}px`)
-}
diff --git a/frontend/src/components/user/UserAvatar.tsx b/frontend/src/components/user/UserAvatar.tsx
index aa36c33..2fff50e 100644
--- a/frontend/src/components/user/UserAvatar.tsx
+++ b/frontend/src/components/user/UserAvatar.tsx
@@ -31,6 +31,7 @@ export function UserAvatarImage({
className={`${className ?? "w-10 rounded-full"} `}
height={height}
width={width}
+ onDragStart={() => false}
/>
)
}