Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 27 additions & 26 deletions src/components/availability/availability-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export function AvailabilityActions({
return (
<div className="flex w-full flex-col gap-2">
{availabilityView === "personal" || availabilityView === "schedule" ? (
<div className="flex flex-row flex-wrap justify-end gap-2">
<div className="flex-row flex-wrap justify-end gap-2">
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
<Button
variant="outlined"
color="inherit"
Expand All @@ -202,7 +202,7 @@ export function AvailabilityActions({
: handleScheduleCancel
}
>
<span className="hidden md:flex">Cancel</span>
<span className="">Cancel</span>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chore: remove redundant <span>.

</Button>
<Button
variant="contained"
Expand All @@ -212,7 +212,7 @@ export function AvailabilityActions({
availabilityView === "personal" ? handleSave : handleScheduleSave
}
>
<span className="hidden md:flex">Save</span>
<span className="">Save</span>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repeat: remove redundant <span>.

</Button>
</div>
) : (
Expand Down Expand Up @@ -260,29 +260,30 @@ export function AvailabilityActions({
Add to Calendar
</Button>
)}

<Button
variant="contained"
startIcon={<Create sx={{ color: "inherit" }} />}
className="w-full max-w-full normal-case"
sx={{ py: 0.75 }}
onClick={() => {
if (!user) {
setIsAuthModalOpen(true);
router.push("/auth/login/google");
return;
}
setChangeableTimezone(false);
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
setAvailabilityView("personal");
}}
>
<span className="hidden md:flex">
{hasAvailability ? "Edit Availability" : "Add Availability"}
</span>
</Button>
<div className="hidden sm:block">
<Button
variant="contained"
startIcon={<Create sx={{ color: "inherit" }} />}
className="w-full max-w-full normal-case"
sx={{ py: 0.75 }}
onClick={() => {
if (!user) {
setIsAuthModalOpen(true);
router.push("/auth/login/google");
return;
}
setChangeableTimezone(false);
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
setAvailabilityView("personal");
}}
>
<span className="hidden md:flex">
{hasAvailability ? "Edit Availability" : "Add Availability"}
</span>
</Button>
</div>
{isOwner && (
<>
<div className="hidden sm:block">
<Button
variant="outlined"
startIcon={<InsertInvitationRounded />}
Expand All @@ -301,7 +302,7 @@ export function AvailabilityActions({
>
<span className="hidden md:flex">Invite Members</span>
</Button>
</>
</div>
)}
</div>
)}
Expand Down
140 changes: 113 additions & 27 deletions src/components/availability/availability.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { Paper } from "@mui/material";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useShallow } from "zustand/shallow";
import { AvailabilityActions } from "@/components/availability/availability-actions";
Expand All @@ -27,7 +28,9 @@ import {
import type { MemberMeetingAvailability } from "@/lib/types/availability";
import type { HourMinuteString } from "@/lib/types/chrono";
import { useAvailabilityStore } from "@/store/useAvailabilityStore";
import { MobilePersonalAvailabilitySidebar } from "../nav/mobile-personal-availability";
import { PersonalAvailabilitySidebar } from "../nav/personal-availability-sidebar";
import { MobileGroupResponses } from "./mobile-group-responses";

export function Availability({
meetingData,
Expand All @@ -48,10 +51,11 @@ export function Availability({

// View + paint mode live in the store (paint mode is reset atomically in
// `setAvailabilityView`, so it cannot drift across view switches).
const { availabilityView, paintMode } = useAvailabilityStore(
const { availabilityView, paintMode, setPaintMode } = useAvailabilityStore(
useShallow((state) => ({
availabilityView: state.availabilityView,
paintMode: state.paintMode,
setPaintMode: state.setPaintMode,
})),
);

Expand Down Expand Up @@ -84,6 +88,15 @@ export function Availability({
})),
);

const { setAvailabilityView, setIsMobileDrawerOpen } = useAvailabilityStore(
useShallow((state) => ({
setAvailabilityView: state.setAvailabilityView,
setIsMobileDrawerOpen: state.setIsMobileDrawerOpen,
})),
);

const router = useRouter();

const isMobile = useIsMobile();
useEffect(() => {
setItemsPerPage(isMobile ? 2 : 5);
Expand Down Expand Up @@ -254,6 +267,51 @@ export function Availability({
onOpenInviteDialog: handleOpenInviteDialog,
} as const;

const isMeetingOwner = Boolean(user && meetingData.hostId === user.memberId);

const groupResponsesProps = useMemo(
() => ({
availabilityDates,
fromTime: fromTimeMinutes,
members,
pendingMembers,
timezone: userTimezone,
anchorNormalizedDate,
currentPageAvailability,
availabilityTimeBlocks,
doesntNeedDay,
}),
[
availabilityDates,
fromTimeMinutes,
members,
pendingMembers,
userTimezone,
anchorNormalizedDate,
currentPageAvailability,
availabilityTimeBlocks,
doesntNeedDay,
],
);

const handleMobileAddAvailability = useCallback(() => {
if (!user) {
router.push("/auth/login/google");
return;
}
setChangeableTimezone(false);
setUserTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
setAvailabilityView("personal");
}, [router, setAvailabilityView, user]);

const handleMobileOpenAttendees = useCallback(() => {
setIsMobileDrawerOpen(true);
}, [setIsMobileDrawerOpen]);

const handleMobileSchedule = useCallback(() => {
setAvailabilityView("schedule");
}, [setAvailabilityView]);

return (
<div className="flex min-h-[80vh] flex-col gap-6">
<AvailabilityHeader
Expand Down Expand Up @@ -337,42 +395,70 @@ export function Availability({
</Paper>

{(availabilityView === "group" || availabilityView === "schedule") && (
<div className="hidden w-96 min-w-0 shrink-0 flex-col items-stretch gap-3 lg:flex lg:min-h-0">
<AvailabilityActions {...actionsProps} />
<Paper
variant="outlined"
className="flex min-h-[24rem] min-w-0 flex-1 flex-col overflow-hidden"
>
<GroupResponses
availabilityDates={availabilityDates}
fromTime={fromTimeMinutes}
members={members}
pendingMembers={pendingMembers}
timezone={userTimezone}
anchorNormalizedDate={anchorNormalizedDate}
currentPageAvailability={currentPageAvailability}
availabilityTimeBlocks={availabilityTimeBlocks}
doesntNeedDay={doesntNeedDay}
<div>
<div className="hidden w-96 min-w-0 shrink-0 flex-col items-stretch gap-3 lg:flex lg:min-h-0">
<AvailabilityActions {...actionsProps} />
<Paper
variant="outlined"
className="flex min-h-[24rem] min-w-0 flex-1 flex-col overflow-hidden"
>
<GroupResponses {...groupResponsesProps} />
</Paper>
</div>

<div className="lg:hidden">
<GroupResponses {...groupResponsesProps} />
</div>

<div className="block sm:hidden">
<MobileGroupResponses
isOwner={isMeetingOwner}
respondedMembersCount={Math.max(
0,
members.length - pendingMembers.length,
)}
pendingMembersCount={pendingMembers.length}
onAddAvailability={handleMobileAddAvailability}
onOpenAttendees={handleMobileOpenAttendees}
onSchedule={handleMobileSchedule}
/>
</Paper>
</div>
</div>
)}
{availabilityView === "personal" && (
<div className="hidden w-96 min-w-0 shrink-0 flex-col items-stretch gap-3 lg:flex lg:min-h-0">
<AvailabilityActions {...actionsProps} />
<Paper
variant="outlined"
className="flex min-h-[24rem] min-w-0 flex-1 flex-col overflow-hidden"
>
<PersonalAvailabilitySidebar
<div>
<div className="hidden w-96 min-w-0 shrink-0 flex-col items-stretch gap-3 lg:flex lg:min-h-0">
<AvailabilityActions {...actionsProps} />
<Paper
variant="outlined"
className="flex min-h-[24rem] min-w-0 flex-1 flex-col overflow-hidden"
>
<PersonalAvailabilitySidebar
meetingId={meetingData.id}
userTimezone={userTimezone}
importGridIsoSet={importGridIsoSet}
canImport={Boolean(user?.memberId)}
onImportSlots={handleImportSlotsFromMeeting}
onClearAvailability={handleClearAvailability}
/>
</Paper>
</div>

<div className="block sm:hidden">
<MobilePersonalAvailabilitySidebar
meetingId={meetingData.id}
userTimezone={userTimezone}
availability={paintMode}
setAvailability={(next) =>
setPaintMode(
typeof next === "function" ? next(paintMode) : next,
)
}
Comment on lines +452 to +456
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: next(paintMode) will resolve against the closed-over paintMode instead of the latest store value.

suggestion: this adapter is actually dead anyway, drop the prop and let the leaf subscribe.

importGridIsoSet={importGridIsoSet}
canImport={Boolean(user?.memberId)}
onImportSlots={handleImportSlotsFromMeeting}
onClearAvailability={handleClearAvailability}
/>
</Paper>
</div>
</div>
)}
</div>
Expand Down
81 changes: 81 additions & 0 deletions src/components/availability/mobile-group-responses.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use client";

import {
CalendarMonthOutlined,
EditCalendarOutlined,
PeopleAltOutlined,
} from "@mui/icons-material";
import { Button, Divider, Paper, Stack, Typography } from "@mui/material";

const actionButtonSx = {
flex: 1,
minWidth: 0,
px: 2,
py: 1.25,
borderRadius: 2,
};

export interface MobileGroupResponsesProps {
isOwner: boolean;
respondedMembersCount: number;
pendingMembersCount: number;
onAddAvailability: () => void;
onOpenAttendees: () => void;
onSchedule: () => void;
}

export function MobileGroupResponses({
isOwner,
respondedMembersCount,
pendingMembersCount,
onAddAvailability,
onOpenAttendees,
onSchedule,
}: MobileGroupResponsesProps) {
return (
<div className="pointer-events-none fixed inset-x-0 bottom-0 z-[1001] flex justify-center px-2 pb-[max(0.5rem,env(safe-area-inset-bottom))]">
<Paper
elevation={3}
sx={{
pointerEvents: "auto",
display: "inline-flex",
borderRadius: 3,
alignItems: "stretch",
p: 0.5,
Comment on lines +36 to +44
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: duplicated mobile-dock shell.

suggestion: extract primitive such that these components become content-only.

width: "min(90vw)",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: min() has 1 argument.

}}
>
<Button color="inherit" sx={actionButtonSx} onClick={onOpenAttendees}>
<Stack spacing={0.5} alignItems="center">
<PeopleAltOutlined fontSize="small" />
<Typography variant="caption">
{respondedMembersCount} /{" "}
{pendingMembersCount + respondedMembersCount} Responders
</Typography>
</Stack>
</Button>

<Divider orientation="vertical" flexItem />

<Button onClick={onAddAvailability} color="inherit" sx={actionButtonSx}>
<Stack spacing={0.5} alignItems="center">
<EditCalendarOutlined fontSize="small" />
<Typography variant="caption">Add Availability</Typography>
</Stack>
</Button>

{isOwner && (
<>
<Divider orientation="vertical" flexItem />
<Button color="inherit" sx={actionButtonSx} onClick={onSchedule}>
<Stack spacing={0.5} alignItems="center">
<CalendarMonthOutlined fontSize="small" />
<Typography variant="caption">Schedule Meeting</Typography>
</Stack>
</Button>
</>
)}
</Paper>
</div>
);
}
Loading
Loading