Skip to content

Commit 18f3876

Browse files
Windslash123Arshia Aravinthanethancha0amhaiskar0921cubic-dev-ai[bot]
authored
feat: restyle and sort meeting cards (#526)
* feat: ✨ resdesign meeting cards + add query * chore: πŸ”§ remove dead hardcoded groups display * feat: mobile notifs entry point (#464) * chore: πŸ”§ pull notifs into page * chore: πŸ”§ create mobile notif drawer * chore: πŸ”§ hook notifs up into groups and meetings * chore: πŸ”§ hook meetings up to notifs * chore: πŸ”§ add select all / mark as read functions * chore: πŸ”§ use styled components, remove sx * fix: πŸ› only render finished message when no unreads * chore: πŸ”§ only render mark as read when notifs are selected * fix: πŸ› merge conflict typo * fix: πŸ› duplicate groups headers, add indicator for notifs * fix: πŸ› realign headers * chore: πŸ”§ switch from iconbutton to MUI button (remove boxshadow) * refactor: ♻️ pull out notif utils/actions * refactor: ♻️ use inline sx and use MUIBottomSheet * style: 🎨 buttons --------- Co-authored-by: Arshia Aravinthan <aravinta@uci.edu> Co-authored-by: ethancha0 <ethanchao2005@gmail.com> * fix: πŸ› non-admins can now invite members to meetings on desktop (#524) * fix: πŸ› non-admins can now invite members to meetings on desktop * fix: πŸ› verify that user is a meeting member or host before allowing them to invite others * feat: ✨ hosts can toggle whether members are allowed to invite others to a meeting * fix: πŸ› updated earlier membership check logic for inviting members * fix: πŸ› disable toggle while invite permissions are being updated in the DB * feat: ✨ revalidatePath, consolidated types --------- Co-authored-by: ethancha0 <ethanchao2005@gmail.com> * fix: πŸ› inline sx * fix: πŸ› dup name * fix: πŸ› merge typo * chore: allow filters to update when online between days Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * chore: πŸ”§ move functions into utils --------- Co-authored-by: Arshia Aravinthan <aravinta@uci.edu> Co-authored-by: ethancha0 <ethanchao2005@gmail.com> Co-authored-by: Arya Mhaiskar <43621944+amhaiskar0921@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
1 parent f7b217f commit 18f3876

8 files changed

Lines changed: 389 additions & 317 deletions

File tree

β€Žsrc/app/summary/page.tsxβ€Ž

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { notFound, redirect } from "next/navigation";
22
import { Meetings } from "@/components/summary/meetings";
33
import { getCurrentSession } from "@/lib/auth";
4-
import { buildScheduledLabel } from "@/lib/meetings/utils";
4+
import {
5+
buildScheduledLabel,
6+
getUpcomingMeetingIds,
7+
} from "@/lib/meetings/utils";
58
import {
69
getMeetings,
710
getResponderCountsByMeetingIds,
@@ -43,17 +46,7 @@ export default async function Page() {
4346
scheduledDates[id] = sm.scheduledDate.getTime();
4447
}
4548

46-
const startOfToday = new Date();
47-
startOfToday.setUTCHours(0, 0, 0, 0);
48-
const threeDaysLater = new Date(
49-
startOfToday.getTime() + 3 * 24 * 60 * 60 * 1000,
50-
);
51-
const upcomingMeetingIds = Object.entries(scheduledMeetingMap)
52-
.filter(
53-
([, sm]) =>
54-
sm.scheduledDate >= startOfToday && sm.scheduledDate <= threeDaysLater,
55-
)
56-
.map(([id]) => id);
49+
const upcomingMeetingIds = getUpcomingMeetingIds(scheduledMeetingMap);
5750

5851
return (
5952
<div className="px-4 py-8 sm:px-8">

β€Žsrc/components/summary/GroupCard.tsxβ€Ž

Lines changed: 0 additions & 24 deletions
This file was deleted.

β€Žsrc/components/summary/GroupsDisplay.tsxβ€Ž

Lines changed: 0 additions & 14 deletions
This file was deleted.

β€Žsrc/components/summary/meetings.tsxβ€Ž

Lines changed: 68 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,22 @@ import { FilterChip } from "@/components/ui/filter-chip";
1111
import MeetingCard from "@/components/ui/meeting-card";
1212
import type { SelectMeeting } from "@/db/schema";
1313
import type { NotificationItem } from "@/lib/auth/user";
14+
import { toMeetingCardData } from "@/lib/meeting-card/mapper";
1415
import {
1516
filterMeetingsByQuery,
16-
toMeetingCardData,
17-
} from "@/lib/meeting-card/mapper";
17+
getMeetingSortTime,
18+
getMeetingUpcomingPriority,
19+
getStartOfTodayMs,
20+
isMeetingPast,
21+
} from "@/lib/meetings/utils";
1822

19-
type FilterType = "all" | "unscheduled" | "scheduled" | "by-you";
23+
type FilterType = "upcoming" | "past" | "by-you";
2024

2125
interface MeetingsProps {
2226
meetings: (SelectMeeting & {
2327
hostDisplayName: string | null;
2428
needsAvailability: boolean;
29+
allAvailabilityFilled: boolean;
2530
})[];
2631
memberId: string;
2732
meetingCounts: Record<string, number>;
@@ -33,26 +38,31 @@ interface MeetingsProps {
3338

3439
type DisplayMeeting = MeetingsProps["meetings"][number];
3540

36-
const sectionLabelSx = {
37-
fontSize: 12,
38-
fontWeight: 500,
39-
letterSpacing: "1px",
40-
textTransform: "uppercase",
41-
lineHeight: 1,
42-
color: "text.disabled",
43-
} as const;
44-
4541
type DeleteTarget = {
4642
meeting: DisplayMeeting;
4743
isOwner: boolean;
4844
};
4945

46+
const FILTER_LABELS: Record<FilterType, string> = {
47+
upcoming: "Upcoming",
48+
past: "Past",
49+
"by-you": "By You",
50+
};
51+
52+
const cardGridSx = {
53+
display: { xs: "flex", sm: "grid" },
54+
flexDirection: "column",
55+
gridTemplateColumns: { sm: "repeat(2, 1fr)", lg: "repeat(3, 1fr)" },
56+
gap: { xs: 1.5, sm: 2 },
57+
} as const;
58+
5059
const toCard = (
5160
meeting: DisplayMeeting,
5261
memberId: string,
5362
meetingCounts: Record<string, number>,
5463
onDeleteLeave: (target: DeleteTarget) => void,
5564
scheduledLabels?: Record<string, string>,
65+
upcomingSet?: Set<string>,
5666
) => {
5767
const { meeting: _meeting, ...cardProps } = toMeetingCardData(
5868
meeting,
@@ -67,50 +77,16 @@ const toCard = (
6777
<MeetingCard
6878
key={meeting.id}
6979
{...cardProps}
80+
needsAvailability={meeting.needsAvailability}
81+
allAvailabilityFilled={meeting.allAvailabilityFilled}
82+
isUpcoming={upcomingSet?.has(meeting.id) ?? false}
7083
onDeleteLeave={() =>
7184
onDeleteLeave({ meeting, isOwner: cardProps.isOwner })
7285
}
7386
/>
7487
);
7588
};
7689

77-
const MeetingSection = ({
78-
label,
79-
meetings,
80-
memberId,
81-
meetingCounts,
82-
onDeleteLeave,
83-
scheduledLabels,
84-
}: {
85-
label: string;
86-
meetings: DisplayMeeting[];
87-
memberId: string;
88-
meetingCounts: Record<string, number>;
89-
onDeleteLeave: (target: DeleteTarget) => void;
90-
scheduledLabels?: Record<string, string>;
91-
}) => {
92-
if (meetings.length === 0) return null;
93-
return (
94-
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
95-
<Typography sx={sectionLabelSx}>
96-
{label} ({meetings.length})
97-
</Typography>
98-
<Box
99-
sx={{
100-
display: { xs: "flex", sm: "grid" },
101-
flexDirection: "column",
102-
gridTemplateColumns: { sm: "repeat(2, 1fr)", lg: "repeat(3, 1fr)" },
103-
gap: { xs: 1.5, sm: 2 },
104-
}}
105-
>
106-
{meetings.map((m) =>
107-
toCard(m, memberId, meetingCounts, onDeleteLeave, scheduledLabels),
108-
)}
109-
</Box>
110-
</Box>
111-
);
112-
};
113-
11490
export const Meetings = ({
11591
meetings,
11692
memberId,
@@ -121,7 +97,7 @@ export const Meetings = ({
12197
notifications,
12298
}: MeetingsProps) => {
12399
const [search, setSearch] = useState("");
124-
const [activeFilter, setActiveFilter] = useState<FilterType>("all");
100+
const [activeFilter, setActiveFilter] = useState<FilterType>("upcoming");
125101
const [notificationsOpen, setNotificationsOpen] = useState(false);
126102

127103
const unreadCount = notifications.filter((n) => !n.readAt).length;
@@ -137,32 +113,37 @@ export const Meetings = ({
137113
[upcomingMeetingIds],
138114
);
139115

116+
const todayTimestamp = getStartOfTodayMs();
117+
118+
const isPastMeeting = useCallback(
119+
(m: DisplayMeeting): boolean =>
120+
isMeetingPast(m, scheduledDates, todayTimestamp),
121+
[scheduledDates, todayTimestamp],
122+
);
123+
140124
const counts = useMemo(
141125
() => ({
142-
all: meetings.length,
143-
unscheduled: meetings.filter((m) => !m.scheduled).length,
144-
scheduled: meetings.filter((m) => m.scheduled === true).length,
126+
upcoming: meetings.filter((m) => !isPastMeeting(m)).length,
127+
past: meetings.filter(isPastMeeting).length,
145128
"by-you": meetings.filter((m) => m.hostId === memberId).length,
146129
}),
147-
[meetings, memberId],
130+
[meetings, memberId, isPastMeeting],
148131
);
149132

150-
const filteredByOwner = useMemo(() => {
133+
const filteredMeetings = useMemo(() => {
151134
switch (activeFilter) {
152135
case "by-you":
153136
return meetings.filter((m) => m.hostId === memberId);
154-
case "unscheduled":
155-
return meetings.filter((m) => !m.scheduled);
156-
case "scheduled":
157-
return meetings.filter((m) => m.scheduled === true);
137+
case "past":
138+
return meetings.filter(isPastMeeting);
158139
default:
159-
return meetings;
140+
return meetings.filter((m) => !isPastMeeting(m));
160141
}
161-
}, [meetings, memberId, activeFilter]);
142+
}, [meetings, memberId, activeFilter, isPastMeeting]);
162143

163144
const displayMeetings = useMemo(
164-
() => filterMeetingsByQuery(filteredByOwner, search),
165-
[filteredByOwner, search],
145+
() => filterMeetingsByQuery(filteredMeetings, search),
146+
[filteredMeetings, search],
166147
);
167148

168149
const renderMeetings = () => {
@@ -209,76 +190,32 @@ export const Meetings = ({
209190
);
210191
}
211192

212-
if (activeFilter === "scheduled") {
213-
const sorted = [...displayMeetings].sort(
214-
(a, b) => (scheduledDates?.[a.id] ?? 0) - (scheduledDates?.[b.id] ?? 0),
215-
);
216-
const upcoming = sorted.filter((m) => upcomingSet.has(m.id));
217-
const rest = sorted.filter((m) => !upcomingSet.has(m.id));
218-
return (
219-
<Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
220-
<MeetingSection
221-
label="Upcoming"
222-
meetings={upcoming}
223-
memberId={memberId}
224-
meetingCounts={meetingCounts}
225-
onDeleteLeave={handleDeleteLeaveRequest}
226-
scheduledLabels={scheduledLabels}
227-
/>
228-
<MeetingSection
229-
label="Scheduled"
230-
meetings={rest}
231-
memberId={memberId}
232-
meetingCounts={meetingCounts}
233-
onDeleteLeave={handleDeleteLeaveRequest}
234-
scheduledLabels={scheduledLabels}
235-
/>
236-
</Box>
237-
);
238-
}
193+
let sorted: DisplayMeeting[];
239194

240-
if (activeFilter === "unscheduled") {
241-
const actionRequired = displayMeetings.filter((m) => m.needsAvailability);
242-
const rest = displayMeetings.filter((m) => !m.needsAvailability);
243-
return (
244-
<Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
245-
<MeetingSection
246-
label="Action Required"
247-
meetings={actionRequired}
248-
memberId={memberId}
249-
meetingCounts={meetingCounts}
250-
onDeleteLeave={handleDeleteLeaveRequest}
251-
scheduledLabels={scheduledLabels}
252-
/>
253-
<MeetingSection
254-
label="Unscheduled"
255-
meetings={rest}
256-
memberId={memberId}
257-
meetingCounts={meetingCounts}
258-
onDeleteLeave={handleDeleteLeaveRequest}
259-
scheduledLabels={scheduledLabels}
260-
/>
261-
</Box>
195+
if (activeFilter === "past") {
196+
sorted = [...displayMeetings].sort(
197+
(a, b) =>
198+
getMeetingSortTime(b, scheduledDates) -
199+
getMeetingSortTime(a, scheduledDates),
200+
);
201+
} else {
202+
sorted = [...displayMeetings].sort(
203+
(a, b) =>
204+
getMeetingUpcomingPriority(a, memberId, upcomingSet) -
205+
getMeetingUpcomingPriority(b, memberId, upcomingSet),
262206
);
263207
}
264208

265-
// "all" and "by-you": flat grid, no sections
266209
return (
267-
<Box
268-
sx={{
269-
display: { xs: "flex", sm: "grid" },
270-
flexDirection: "column",
271-
gridTemplateColumns: { sm: "repeat(2, 1fr)", lg: "repeat(3, 1fr)" },
272-
gap: { xs: 1.5, sm: 2 },
273-
}}
274-
>
275-
{displayMeetings.map((m) =>
210+
<Box sx={cardGridSx}>
211+
{sorted.map((m) =>
276212
toCard(
277213
m,
278214
memberId,
279215
meetingCounts,
280216
handleDeleteLeaveRequest,
281217
scheduledLabels,
218+
upcomingSet,
282219
),
283220
)}
284221
</Box>
@@ -341,25 +278,15 @@ export const Meetings = ({
341278
/>
342279

343280
<Box sx={{ display: "flex", gap: 0.75 }}>
344-
{(["all", "unscheduled", "scheduled", "by-you"] as const).map(
345-
(f) => {
346-
const labels: Record<FilterType, string> = {
347-
all: "All",
348-
unscheduled: "Unscheduled",
349-
scheduled: "Scheduled",
350-
"by-you": "By You",
351-
};
352-
return (
353-
<FilterChip
354-
key={f}
355-
label={labels[f]}
356-
count={counts[f]}
357-
active={activeFilter === f}
358-
onClick={() => setActiveFilter(f)}
359-
/>
360-
);
361-
},
362-
)}
281+
{(["upcoming", "past", "by-you"] as const).map((f) => (
282+
<FilterChip
283+
key={f}
284+
label={FILTER_LABELS[f]}
285+
count={counts[f]}
286+
active={activeFilter === f}
287+
onClick={() => setActiveFilter(f)}
288+
/>
289+
))}
363290
</Box>
364291
</Box>
365292

0 commit comments

Comments
Β (0)