mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
* feat(companion): add DropdownMenu for Android event types list Replace Alert.alert with react-native-reusables DropdownMenu component for Android event type list items, providing a native-feeling menu experience that matches iOS functionality. Changes: - Install react-native-reusables dropdown-menu component and dependencies (lucide-react-native, tailwindcss-animate, class-variance-authority, clsx, tailwind-merge) - Create EventTypeListItem.android.tsx with DropdownMenu implementation - Single ellipsis button triggers dropdown menu - Menu includes: Preview, Copy link, Edit, Duplicate, Delete - Delete action marked as destructive variant - Proper safe area insets handling - Add PortalHost to app/_layout.tsx for portal rendering support - Create lib/utils.ts with cn() helper for className merging - Update global.css with theme CSS variables (popover, border, accent, destructive, etc.) for dropdown menu styling - Update tailwind.config.js with theme colors and tailwindcss-animate plugin - Update metro.config.js with inlineRem: 16 for proper rem unit handling - Remove Android Alert.alert fallback from event types index.tsx - Fix lint issues in generated dropdown-menu.tsx (remove unnecessary fragments) The Android experience now matches iOS with a single menu button that opens a dropdown containing all event type actions, replacing the previous Alert.alert dialog and separate action buttons. Refs: https://reactnativereusables.com/docs/components/dropdown-menu * adjust with of menu * feat(companion): add DropdownMenu for Android booking and availability list items - Add BookingListItem.android.tsx with DropdownMenu for booking actions - Add BookingDetailScreen.android.tsx with DropdownMenu in header - Add AvailabilityListItem.android.tsx with DropdownMenu for schedule actions Actions include: Reschedule, Edit Location, Add Guests, View Recordings, Meeting Session Details, Mark as No-Show, Report Booking, Cancel Event for bookings; Set as Default, Duplicate, Delete for availability schedules. * fix lint issues * feat(android): replace native alerts with AlertDialog and Toast in event types - Install AlertDialog and Alert components from react-native-reusables - Create Android-specific event types screen (index.android.tsx) - Replace native Alert.alert() with AlertDialog for delete confirmation - Add inline validation errors (red border + error text) in create modal - Implement Toast snackbar for success/error notifications (no layout shift) - Auto-dismiss toast after 2.5 seconds - Fix React Compiler compatibility by removing animated refs This provides a more consistent and polished UI experience on Android, matching the design system used in other parts of the app. * feat(android): add AlertDialog for availability delete and booking cancel - Add index.android.tsx for availability with AlertDialog for delete confirmation - Add index.android.tsx for bookings with AlertDialog for cancel (with reason input) - Both screens include Toast snackbar for success/error notifications - Follows the same pattern as event types Android implementation * feat(companion): add DropdownMenu and AlertDialog for Android - Replace native Alert.alert context menus with DropdownMenu component for event types, bookings, and availability lists - Replace FullScreenModal with AlertDialog for create/delete/cancel flows - Add toast notifications for success/error feedback on Android - Set consistent 380px max-width for all AlertDialogs - Fix layout headers showing "index" text on event-types and availability - Create Android-specific More screen with AlertDialog logout confirmation Uses react-native-reusables components for polished Android UI * better code * fix cubics comments * refactor(android): revert delete confirmations to native Alert.alert() - Logout: reverted to native Alert.alert() (simple yes/no) - Event Types Delete: reverted to native Alert.alert() (simple yes/no) - Availability Delete: reverted to native Alert.alert() (simple yes/no) Keep AlertDialog only where user input is needed: - Event Types Create (has TextInput for title) - Availability Create (has TextInput for name) - Bookings Cancel (has TextInput for cancellation reason) * refactor(android): remove redundant index.android.tsx files The main index.tsx files already handle Android via Platform.OS checks. Android-specific behavior for actions is in the list item components: - EventTypeListItem.android.tsx - BookingListItem.android.tsx - AvailabilityListItem.android.tsx This consolidates the code and reduces duplication. * corrected the implementation both native and alert dialog * address cubics comments * fix lint issue and address cubic comments
312 lines
9.9 KiB
TypeScript
312 lines
9.9 KiB
TypeScript
import * as DropdownMenuPrimitive from "@rn-primitives/dropdown-menu";
|
|
import { Check, ChevronDown, ChevronRight, ChevronUp } from "lucide-react-native";
|
|
import * as React from "react";
|
|
import {
|
|
Platform,
|
|
type StyleProp,
|
|
StyleSheet,
|
|
Text,
|
|
type TextProps,
|
|
View,
|
|
type ViewStyle,
|
|
} from "react-native";
|
|
import { FadeIn } from "react-native-reanimated";
|
|
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
import { Icon } from "@/components/ui/icon";
|
|
import { NativeOnlyAnimatedView } from "@/components/ui/native-only-animated-view";
|
|
import { TextClassContext } from "@/components/ui/text";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
|
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
|
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
|
|
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
|
|
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
|
|
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
|
|
|
function DropdownMenuSubTrigger({
|
|
className,
|
|
inset,
|
|
children,
|
|
iconClassName,
|
|
...props
|
|
}: DropdownMenuPrimitive.SubTriggerProps &
|
|
React.RefAttributes<DropdownMenuPrimitive.SubTriggerRef> & {
|
|
children?: React.ReactNode;
|
|
iconClassName?: string;
|
|
inset?: boolean;
|
|
}) {
|
|
const { open } = DropdownMenuPrimitive.useSubContext();
|
|
const icon = Platform.OS === "web" ? ChevronRight : open ? ChevronUp : ChevronDown;
|
|
return (
|
|
<TextClassContext.Provider
|
|
value={cn(
|
|
"text-sm select-none group-active:text-accent-foreground",
|
|
open && "text-accent-foreground"
|
|
)}
|
|
>
|
|
<DropdownMenuPrimitive.SubTrigger
|
|
className={cn(
|
|
"active:bg-accent group flex flex-row items-center rounded-sm px-2 py-2 sm:py-1.5",
|
|
Platform.select({
|
|
web: "focus:bg-accent focus:text-accent-foreground cursor-default outline-none [&_svg]:pointer-events-none",
|
|
}),
|
|
open && "bg-accent",
|
|
inset && "pl-8"
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
<Icon as={icon} className={cn("text-foreground ml-auto size-4 shrink-0", iconClassName)} />
|
|
</DropdownMenuPrimitive.SubTrigger>
|
|
</TextClassContext.Provider>
|
|
);
|
|
}
|
|
|
|
function DropdownMenuSubContent({
|
|
className,
|
|
...props
|
|
}: DropdownMenuPrimitive.SubContentProps &
|
|
React.RefAttributes<DropdownMenuPrimitive.SubContentRef>) {
|
|
return (
|
|
<NativeOnlyAnimatedView entering={FadeIn}>
|
|
<DropdownMenuPrimitive.SubContent
|
|
className={cn(
|
|
"bg-popover border-border overflow-hidden rounded-md border p-1 shadow-lg shadow-black/5",
|
|
Platform.select({
|
|
web: "animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 fade-in-0 data-[state=closed]:zoom-out-95 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin) z-50 min-w-[8rem]",
|
|
}),
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
</NativeOnlyAnimatedView>
|
|
);
|
|
}
|
|
|
|
const FullWindowOverlay = Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment;
|
|
|
|
function DropdownMenuContent({
|
|
className,
|
|
overlayClassName,
|
|
overlayStyle,
|
|
portalHost,
|
|
...props
|
|
}: DropdownMenuPrimitive.ContentProps &
|
|
React.RefAttributes<DropdownMenuPrimitive.ContentRef> & {
|
|
overlayStyle?: StyleProp<ViewStyle>;
|
|
overlayClassName?: string;
|
|
portalHost?: string;
|
|
}) {
|
|
return (
|
|
<DropdownMenuPrimitive.Portal hostName={portalHost}>
|
|
<FullWindowOverlay>
|
|
<DropdownMenuPrimitive.Overlay
|
|
style={Platform.select({
|
|
web: overlayStyle ?? undefined,
|
|
native: overlayStyle
|
|
? StyleSheet.flatten([
|
|
StyleSheet.absoluteFill,
|
|
overlayStyle as typeof StyleSheet.absoluteFill,
|
|
])
|
|
: StyleSheet.absoluteFill,
|
|
})}
|
|
className={overlayClassName}
|
|
>
|
|
<NativeOnlyAnimatedView entering={FadeIn}>
|
|
<TextClassContext.Provider value="text-popover-foreground">
|
|
<DropdownMenuPrimitive.Content
|
|
className={cn(
|
|
"bg-popover border-border min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg shadow-black/5",
|
|
Platform.select({
|
|
web: cn(
|
|
"animate-in fade-in-0 zoom-in-95 max-h-(--radix-context-menu-content-available-height) origin-(--radix-context-menu-content-transform-origin) z-50 cursor-default",
|
|
props.side === "bottom" && "slide-in-from-top-2",
|
|
props.side === "top" && "slide-in-from-bottom-2"
|
|
),
|
|
}),
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
</TextClassContext.Provider>
|
|
</NativeOnlyAnimatedView>
|
|
</DropdownMenuPrimitive.Overlay>
|
|
</FullWindowOverlay>
|
|
</DropdownMenuPrimitive.Portal>
|
|
);
|
|
}
|
|
|
|
function DropdownMenuItem({
|
|
className,
|
|
inset,
|
|
variant,
|
|
...props
|
|
}: DropdownMenuPrimitive.ItemProps &
|
|
React.RefAttributes<DropdownMenuPrimitive.ItemRef> & {
|
|
className?: string;
|
|
inset?: boolean;
|
|
variant?: "default" | "destructive";
|
|
}) {
|
|
return (
|
|
<TextClassContext.Provider
|
|
value={cn(
|
|
"select-none text-sm text-popover-foreground group-active:text-popover-foreground",
|
|
variant === "destructive" && "text-destructive group-active:text-destructive"
|
|
)}
|
|
>
|
|
<DropdownMenuPrimitive.Item
|
|
className={cn(
|
|
"active:bg-accent group relative flex flex-row items-center gap-2 rounded-sm px-2 py-2 sm:py-1.5",
|
|
Platform.select({
|
|
web: cn(
|
|
"focus:bg-accent focus:text-accent-foreground cursor-default outline-none data-[disabled]:pointer-events-none",
|
|
variant === "destructive" && "focus:bg-destructive/10 dark:focus:bg-destructive/20"
|
|
),
|
|
}),
|
|
variant === "destructive" && "active:bg-destructive/10 dark:active:bg-destructive/20",
|
|
props.disabled && "opacity-50",
|
|
inset && "pl-8",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
</TextClassContext.Provider>
|
|
);
|
|
}
|
|
|
|
function DropdownMenuCheckboxItem({
|
|
className,
|
|
children,
|
|
...props
|
|
}: DropdownMenuPrimitive.CheckboxItemProps &
|
|
React.RefAttributes<DropdownMenuPrimitive.CheckboxItemRef> & {
|
|
children?: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<TextClassContext.Provider value="text-sm text-popover-foreground select-none group-active:text-accent-foreground">
|
|
<DropdownMenuPrimitive.CheckboxItem
|
|
className={cn(
|
|
"active:bg-accent group relative flex flex-row items-center gap-2 rounded-sm py-2 pl-8 pr-2 sm:py-1.5",
|
|
Platform.select({
|
|
web: "focus:bg-accent focus:text-accent-foreground cursor-default outline-none data-[disabled]:pointer-events-none",
|
|
}),
|
|
props.disabled && "opacity-50",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<View className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
<DropdownMenuPrimitive.ItemIndicator>
|
|
<Icon
|
|
as={Check}
|
|
className={cn(
|
|
"text-foreground size-4",
|
|
Platform.select({ web: "pointer-events-none" })
|
|
)}
|
|
/>
|
|
</DropdownMenuPrimitive.ItemIndicator>
|
|
</View>
|
|
{children}
|
|
</DropdownMenuPrimitive.CheckboxItem>
|
|
</TextClassContext.Provider>
|
|
);
|
|
}
|
|
|
|
function DropdownMenuRadioItem({
|
|
className,
|
|
children,
|
|
...props
|
|
}: DropdownMenuPrimitive.RadioItemProps &
|
|
React.RefAttributes<DropdownMenuPrimitive.RadioItemRef> & {
|
|
children?: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<TextClassContext.Provider value="text-sm text-popover-foreground select-none group-active:text-accent-foreground">
|
|
<DropdownMenuPrimitive.RadioItem
|
|
className={cn(
|
|
"active:bg-accent group relative flex flex-row items-center gap-2 rounded-sm py-2 pl-8 pr-2 sm:py-1.5",
|
|
Platform.select({
|
|
web: "focus:bg-accent focus:text-accent-foreground cursor-default outline-none data-[disabled]:pointer-events-none",
|
|
}),
|
|
props.disabled && "opacity-50",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<View className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
<DropdownMenuPrimitive.ItemIndicator>
|
|
<View className="bg-foreground h-2 w-2 rounded-full" />
|
|
</DropdownMenuPrimitive.ItemIndicator>
|
|
</View>
|
|
{children}
|
|
</DropdownMenuPrimitive.RadioItem>
|
|
</TextClassContext.Provider>
|
|
);
|
|
}
|
|
|
|
function DropdownMenuLabel({
|
|
className,
|
|
inset,
|
|
...props
|
|
}: DropdownMenuPrimitive.LabelProps &
|
|
React.RefAttributes<DropdownMenuPrimitive.LabelRef> & {
|
|
className?: string;
|
|
inset?: boolean;
|
|
}) {
|
|
return (
|
|
<DropdownMenuPrimitive.Label
|
|
className={cn(
|
|
"text-foreground px-2 py-2 text-sm font-medium sm:py-1.5",
|
|
inset && "pl-8",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function DropdownMenuSeparator({
|
|
className,
|
|
...props
|
|
}: DropdownMenuPrimitive.SeparatorProps & React.RefAttributes<DropdownMenuPrimitive.SeparatorRef>) {
|
|
return (
|
|
<DropdownMenuPrimitive.Separator
|
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function DropdownMenuShortcut({ className, ...props }: TextProps & React.RefAttributes<Text>) {
|
|
return (
|
|
<Text
|
|
className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export {
|
|
DropdownMenu,
|
|
DropdownMenuCheckboxItem,
|
|
DropdownMenuContent,
|
|
DropdownMenuGroup,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuPortal,
|
|
DropdownMenuRadioGroup,
|
|
DropdownMenuRadioItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuShortcut,
|
|
DropdownMenuSub,
|
|
DropdownMenuSubContent,
|
|
DropdownMenuSubTrigger,
|
|
DropdownMenuTrigger,
|
|
};
|