-
Notifications
You must be signed in to change notification settings - Fork 2
feat: mobile availability bar #465
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
43b814d
29f4b94
f7450f1
51d2d2b
8cfe31e
4e66eac
4a8c9eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"> | ||
| <Button | ||
| variant="outlined" | ||
| color="inherit" | ||
|
|
@@ -202,7 +202,7 @@ export function AvailabilityActions({ | |
| : handleScheduleCancel | ||
| } | ||
| > | ||
| <span className="hidden md:flex">Cancel</span> | ||
| <span className="">Cancel</span> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. chore: remove redundant |
||
| </Button> | ||
| <Button | ||
| variant="contained" | ||
|
|
@@ -212,7 +212,7 @@ export function AvailabilityActions({ | |
| availabilityView === "personal" ? handleSave : handleScheduleSave | ||
| } | ||
| > | ||
| <span className="hidden md:flex">Save</span> | ||
| <span className="">Save</span> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. repeat: remove redundant |
||
| </Button> | ||
| </div> | ||
| ) : ( | ||
|
|
@@ -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 />} | ||
|
|
@@ -301,7 +302,7 @@ export function AvailabilityActions({ | |
| > | ||
| <span className="hidden md:flex">Invite Members</span> | ||
| </Button> | ||
| </> | ||
| </div> | ||
| )} | ||
| </div> | ||
| )} | ||
|
|
||
| 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"; | ||
|
|
@@ -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, | ||
|
|
@@ -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, | ||
| })), | ||
| ); | ||
|
|
||
|
|
@@ -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); | ||
|
|
@@ -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 | ||
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: 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> | ||
|
|
||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
| }} | ||
| > | ||
| <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> | ||
| ); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.