diff --git a/frontend/components/AcceptError.tsx b/frontend/components/AcceptError.tsx index 864373d9..86272bbe 100644 --- a/frontend/components/AcceptError.tsx +++ b/frontend/components/AcceptError.tsx @@ -1,6 +1,6 @@ import React from "react"; import Alert from "components/Alert"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; type Props = { title: string; @@ -19,7 +19,7 @@ export default function AcceptError({ title, message, onRetry, retrying, childre
{onRetry && ( - )} diff --git a/frontend/components/AccountDropdown.tsx b/frontend/components/AccountDropdown.tsx index b640c8aa..52185bfb 100644 --- a/frontend/components/AccountDropdown.tsx +++ b/frontend/components/AccountDropdown.tsx @@ -1,10 +1,5 @@ import React from "react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "components/ui/dropdown-menu"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "components/ui/dropdown-menu"; import { User, Feather, LogOut, Shield } from "lucide-react"; import Avatar from "components/Avatar"; import { avatarFromUser } from "lib/avatar"; @@ -36,7 +31,7 @@ const AccountDropdown = ({ className, dropUp }: Props) => { className || "rounded-full transition-all duration-200 hover:ring-2 hover:ring-gray-200 hover:ring-offset-2" } > - +
diff --git a/frontend/components/AuthForm.tsx b/frontend/components/AuthForm.tsx index 5f75a70e..57c169a1 100644 --- a/frontend/components/AuthForm.tsx +++ b/frontend/components/AuthForm.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Link, useNavigate } from "react-router-dom"; import Input from "components/Input"; import Field from "components/Field"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; import Alert from "components/Alert"; import useRequestCode from "hooks/useRequestCode"; import useVerifyCode from "hooks/useVerifyCode"; @@ -126,8 +126,15 @@ export default function AuthForm({ heading, message, email: initialEmail, lockEm )} - {step === "code" && ( @@ -136,9 +143,9 @@ export default function AuthForm({ heading, message, email: initialEmail, lockEm

We sent a code to {email}.{" "} - +

  • It can take 1–2 minutes to arrive.
  • @@ -147,14 +154,15 @@ export default function AuthForm({ heading, message, email: initialEmail, lockEm {cooldown > 0 ? ( Resend in {cooldown}s ) : ( - + )}
@@ -166,16 +174,16 @@ export default function AuthForm({ heading, message, email: initialEmail, lockEm

) : ( - + )}
)} diff --git a/frontend/components/Button.tsx b/frontend/components/Button.tsx deleted file mode 100644 index 4bba9138..00000000 --- a/frontend/components/Button.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from "react"; -import { Link } from "react-router-dom"; -import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "lib/utils"; - -const buttonVariants = cva("font-semibold rounded", { - variants: { - color: { - default: "bg-gray-300 text-gray-700", - gray: "text-secondary-foreground bg-secondary", - red: "bg-red-600 hover:bg-red-700 text-white", - grayOutline: "border border-input hover:bg-secondary transition-colors text-secondary-foreground", - primary: "bg-primary text-primary-foreground hover:bg-primary-hover transition-colors", - pillPrimary: - "bg-primary text-primary-foreground hover:bg-primary-hover transition-colors rounded-full shadow-lg shadow-primary/30", - pillOutlineGray: - "bg-transparent text-secondary-foreground border border-input hover:bg-gray-50 transition-colors rounded-full", - pillWhite: "bg-white text-secondary-foreground hover:bg-gray-50 transition-colors rounded-full shadow-md", - link: "text-link font-medium", - linkBold: "text-link font-bold", - linkDanger: "text-red-700 hover:text-red-800", - pillOutlineAmber: - "bg-transparent text-amber-600 border border-amber-600 hover:bg-amber-500/5 transition-colors rounded-full", - }, - size: { - lg: "text-lg py-2.5 px-4.5", - md: "text-md py-2 px-5", - pill: "text-sm py-3 px-6", - smPill: "text-[14px] py-1.5 px-4", - sm: "text-[14px] py-1.5 px-2.5", - xs: "text-[12px] py-0.5 px-1.5", - xsPill: "text-[12px] py-1.5 px-3", - none: "", - }, - }, - defaultVariants: { - color: "default", - size: "md", - }, -}); - -type Props = VariantProps & { - className?: string; - type?: "submit" | "reset" | "button" | undefined; - disabled?: boolean; - href?: string; - children: React.ReactNode; - [x: string]: any; -}; - -export default function Button({ - className, - disabled, - type = "button", - color, - size, - href, - children, - ...props -}: Props) { - const classes = cn( - buttonVariants({ color, size: color === "link" || color === "linkBold" ? "none" : size }), - disabled && "opacity-60", - className - ); - - if (href) { - return /^(https?:|mailto:|tel:|om:)/.test(href) ? ( - - {children} - - ) : ( - - {children} - - ); - } - - return ( - - ); -} diff --git a/frontend/components/CloseButton.tsx b/frontend/components/CloseButton.tsx deleted file mode 100644 index 4e046e74..00000000 --- a/frontend/components/CloseButton.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; -import Icon from "components/Icon"; -import clsx from "clsx"; - -type Props = { - className?: string; - onClick: () => void; -}; - -export default function CloseButton({ className, onClick }: Props) { - return ( - - ); -} diff --git a/frontend/components/DirectionsButton.tsx b/frontend/components/DirectionsButton.tsx index fce9921f..e74812a7 100644 --- a/frontend/components/DirectionsButton.tsx +++ b/frontend/components/DirectionsButton.tsx @@ -1,5 +1,5 @@ import React from "react"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; import SlideOver from "components/SlideOver"; import { useTrip } from "hooks/useTrip"; import MarkerWithIcon from "components/MarkerWithIcon"; @@ -29,13 +29,13 @@ export default function DirectionsButton({ lat, lng, hotspotId, markerId, google return ( <> setOpen(false)}>

We'll send a 6-digit code to your new email to confirm the change.

- ) : ( @@ -100,12 +107,20 @@ export default function EmailChangeForm({ currentEmail }: Props) { />
- - +
)} diff --git a/frontend/components/EmptyState.tsx b/frontend/components/EmptyState.tsx new file mode 100644 index 00000000..ecd0008f --- /dev/null +++ b/frontend/components/EmptyState.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import Card from "components/Card"; +import Icon from "components/Icon"; +import { IconNameT } from "lib/icons"; +import { cn } from "lib/utils"; + +type Props = { + title: string; + description?: React.ReactNode; + icon?: IconNameT; + action?: React.ReactNode; + className?: string; +}; + +export default function EmptyState({ title, description, icon, action, className }: Props) { + return ( + + {icon && } +

{title}

+ {description &&

{description}

} + {action &&
{action}
} +
+ ); +} diff --git a/frontend/components/Error.tsx b/frontend/components/Error.tsx index 70f9bef5..0f3e5eae 100644 --- a/frontend/components/Error.tsx +++ b/frontend/components/Error.tsx @@ -1,4 +1,5 @@ import Icon from "components/Icon"; +import { Button } from "components/ui/button"; type Props = { onReload?: () => void; @@ -11,10 +12,10 @@ export default function Error({ onReload, message }: Props) {

{message || "Sorry! Something went wrong..."}

{onReload && (

- +

)}
diff --git a/frontend/components/ErrorBoundary.tsx b/frontend/components/ErrorBoundary.tsx index dc8ce76d..e55db21d 100644 --- a/frontend/components/ErrorBoundary.tsx +++ b/frontend/components/ErrorBoundary.tsx @@ -1,5 +1,6 @@ import React, { ReactNode, Component, ErrorInfo } from "react"; import Icon from "components/Icon"; +import { Button } from "components/ui/button"; type ErrorBoundaryProps = { children: ReactNode; @@ -33,10 +34,14 @@ class ErrorBoundary extends Component {

Sorry! Something went wrong...

- +

); diff --git a/frontend/components/GoogleIcon.tsx b/frontend/components/GoogleIcon.tsx deleted file mode 100644 index e3139b48..00000000 --- a/frontend/components/GoogleIcon.tsx +++ /dev/null @@ -1,26 +0,0 @@ -type Props = { - className?: string; -}; - -export default function GoogleIcon({ className }: Props) { - return ( - - - - - - - ); -} diff --git a/frontend/components/Header.tsx b/frontend/components/Header.tsx index b4fe033a..98439479 100644 --- a/frontend/components/Header.tsx +++ b/frontend/components/Header.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Link, useLocation, useNavigate } from "react-router-dom"; import Icon from "components/Icon"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; import useRealtimeStatus from "hooks/useRealtimeStatus"; import clsx from "clsx"; import { useTrip } from "hooks/useTrip"; @@ -74,14 +74,14 @@ export default function Header({ title, parent, border }: Props) { {canEdit && ( )} - + ); } diff --git a/frontend/components/HomeHeader.tsx b/frontend/components/HomeHeader.tsx index 2593b79c..f30906ca 100644 --- a/frontend/components/HomeHeader.tsx +++ b/frontend/components/HomeHeader.tsx @@ -1,6 +1,6 @@ import { Link } from "react-router-dom"; import clsx from "clsx"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; import { useUser } from "hooks/useUser"; import Logo from "components/Logo"; export default function HomeHeader() { @@ -15,15 +15,15 @@ export default function HomeHeader() {

BirdPlan.app

{isLoggedIn ? ( - ) : ( <> - - diff --git a/frontend/components/HotspotTargets.tsx b/frontend/components/HotspotTargets.tsx index 57181426..7a62f3c4 100644 --- a/frontend/components/HotspotTargets.tsx +++ b/frontend/components/HotspotTargets.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useTrip } from "hooks/useTrip"; import Icon from "components/Icon"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; import HotspotTargetRow from "components/HotspotTargetRow"; import SelectDropdown from "components/SelectDropdown"; import useTargetView from "hooks/useTargetView"; @@ -57,7 +57,7 @@ export default function HotspotTargets({ hotspotId, onSpeciesClick }: Props) { Failed to load targets - diff --git a/frontend/components/InputNotesSimple.tsx b/frontend/components/InputNotesSimple.tsx index 3dee89fd..a9084cf4 100644 --- a/frontend/components/InputNotesSimple.tsx +++ b/frontend/components/InputNotesSimple.tsx @@ -1,6 +1,6 @@ import React from "react"; import TextareaAutosize from "react-textarea-autosize"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; type Props = { value?: string; @@ -42,20 +42,20 @@ export default function InputNotesSimple({ value, onBlur, className, canEdit, sh {!inEditMode && canEdit && ( )} {showDone && inEditMode && ( - )} diff --git a/frontend/components/ItineraryDay.tsx b/frontend/components/ItineraryDay.tsx index 644a5285..7c30cfd4 100644 --- a/frontend/components/ItineraryDay.tsx +++ b/frontend/components/ItineraryDay.tsx @@ -1,5 +1,5 @@ import React from "react"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; import { useTrip } from "hooks/useTrip"; import dayjs from "dayjs"; import { useModal } from "stores/modals"; @@ -124,9 +124,14 @@ export default function ItineraryDay({ day, dayIndex, isEditing }: PropsT) { )} -
  • - + {isEditing && ( -
    +
    {index !== locations.length - 1 && ( - + )} {index !== 0 && ( - + )} - +
    )}
  • @@ -192,10 +194,10 @@ export default function ItineraryDay({ day, dayIndex, isEditing }: PropsT) { )} {isEditing && (
    - -
    diff --git a/frontend/components/LifelistCard.tsx b/frontend/components/LifelistCard.tsx index 304a610c..b143c081 100644 --- a/frontend/components/LifelistCard.tsx +++ b/frontend/components/LifelistCard.tsx @@ -1,6 +1,6 @@ import React from "react"; import toast from "react-hot-toast"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; import Icon from "components/Icon"; import { parseLifelistCsv } from "lib/lifelistCsv"; @@ -48,8 +48,7 @@ export default function LifelistCard({ label, count, onImport, onRemove, disable
    ); diff --git a/frontend/components/ObsList.tsx b/frontend/components/ObsList.tsx index 8211a584..54f4e238 100644 --- a/frontend/components/ObsList.tsx +++ b/frontend/components/ObsList.tsx @@ -1,6 +1,6 @@ import React from "react"; import Icon from "components/Icon"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; import { dateTimeToRelative } from "lib/helpers"; import { useTrip } from "hooks/useTrip"; import dayjs from "dayjs"; @@ -79,9 +79,9 @@ export default function ObsList({ hotspotId, speciesCode }: Props) {

    {(data?.length || 0) > previewCount && !viewAll && ( - + )}

    {isLoading && ( @@ -95,7 +95,7 @@ export default function ObsList({ hotspotId, speciesCode }: Props) { Failed to load observations - diff --git a/frontend/components/ParticipantOptionsDropdown.tsx b/frontend/components/ParticipantOptionsDropdown.tsx index 03b8e4dc..6414a2f9 100644 --- a/frontend/components/ParticipantOptionsDropdown.tsx +++ b/frontend/components/ParticipantOptionsDropdown.tsx @@ -6,6 +6,7 @@ import { DropdownMenuTrigger, } from "components/ui/dropdown-menu"; import Icon from "components/Icon"; +import { Button } from "components/ui/button"; export type ParticipantMenuItem = { name: string; @@ -24,10 +25,10 @@ export default function ParticipantOptionsDropdown({ items }: Props) { return ( } title="Options" > - + {items.map(({ name, icon, onClick, danger }) => ( diff --git a/frontend/components/ParticipantRow.tsx b/frontend/components/ParticipantRow.tsx index 1157e1cc..505106fa 100644 --- a/frontend/components/ParticipantRow.tsx +++ b/frontend/components/ParticipantRow.tsx @@ -8,7 +8,7 @@ import { useModal } from "stores/modals"; import useMutation from "hooks/useMutation"; import ParticipantOptionsDropdown, { ParticipantMenuItem } from "components/ParticipantOptionsDropdown"; import Badge from "components/Badge"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; import Avatar from "components/Avatar"; import { avatarFromParticipant } from "lib/avatar"; import { Feather, Pencil, Mail, Trash2 } from "lucide-react"; @@ -105,7 +105,7 @@ export default function ParticipantRow({ participant: p }: Props) { {canChangeList && ( <> - diff --git a/frontend/components/RecentChecklistList.tsx b/frontend/components/RecentChecklistList.tsx index 326d2782..e058f682 100644 --- a/frontend/components/RecentChecklistList.tsx +++ b/frontend/components/RecentChecklistList.tsx @@ -2,7 +2,7 @@ import React from "react"; import dayjs from "dayjs"; import { dateTimeToRelative } from "lib/helpers"; import { useTrip } from "hooks/useTrip"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; import useFetchRecentChecklists from "hooks/useFetchRecentChecklists"; import useFetchRecentSpecies from "hooks/useFetchRecentSpecies"; import useFetchHotspotObs from "hooks/useFetchHotspotObs"; @@ -147,19 +147,20 @@ export default function RecentChecklistList({ hotspotId, speciesCode, speciesNam )} {!expanded && mergedChecklists.length > 10 && ( - + )} {expanded && (

    - View more on eBird - +

    )} {!isLoading && !isLoadingSpecies && checklists.length === 0 && !error && ( @@ -177,7 +178,7 @@ export default function RecentChecklistList({ hotspotId, speciesCode, speciesNam Failed to load recent checklists - diff --git a/frontend/components/RecentSpeciesList.tsx b/frontend/components/RecentSpeciesList.tsx index a8a2cd3a..a77dd98d 100644 --- a/frontend/components/RecentSpeciesList.tsx +++ b/frontend/components/RecentSpeciesList.tsx @@ -4,7 +4,7 @@ import useFetchRecentSpecies from "hooks/useFetchRecentSpecies"; import { dateTimeToRelative } from "lib/helpers"; import { useTrip } from "hooks/useTrip"; import Icon from "components/Icon"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; import Alert from "components/Alert"; type Props = { @@ -70,9 +70,9 @@ export default function RecentSpeciesList({ locId, onSpeciesClick }: Props) { )}

    {recentSpecies.length > previewCount && !viewAll && ( - + )}

    {isLoading && ( @@ -90,7 +90,7 @@ export default function RecentSpeciesList({ locId, onSpeciesClick }: Props) { Failed to load recent species - diff --git a/frontend/components/RegionFields.tsx b/frontend/components/RegionFields.tsx index c947718d..64e4efc3 100644 --- a/frontend/components/RegionFields.tsx +++ b/frontend/components/RegionFields.tsx @@ -1,7 +1,7 @@ import React from "react"; import RegionSelect from "components/RegionSelect"; import Field from "components/Field"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; import { Input } from "components/ui/input"; import { RegionFieldsValue, requiresSubregion } from "lib/region"; @@ -17,7 +17,7 @@ export default function RegionFields({ value, onChange }: Props) { const toggleButton = ( - )} - - ); -} diff --git a/frontend/components/SpeciesCard.tsx b/frontend/components/SpeciesCard.tsx index 21586714..2dd9a3b9 100644 --- a/frontend/components/SpeciesCard.tsx +++ b/frontend/components/SpeciesCard.tsx @@ -1,5 +1,6 @@ import React from "react"; -import CloseButton from "components/CloseButton"; +import { Button } from "components/ui/button"; +import { XIcon } from "lucide-react"; import { useTrip } from "hooks/useTrip"; import { useModal } from "stores/modals"; @@ -29,7 +30,15 @@ export default function Trip({ name, code }: Props) {

    {name}

    - setSelectedSpecies(undefined)} className="ml-auto" /> +

    Showing reports over the last 30 days.{" "} diff --git a/frontend/components/SpeciesHero.tsx b/frontend/components/SpeciesHero.tsx index 9bac1846..ea6084f5 100644 --- a/frontend/components/SpeciesHero.tsx +++ b/frontend/components/SpeciesHero.tsx @@ -6,6 +6,7 @@ import { DropdownMenuTrigger, } from "components/ui/dropdown-menu"; import Icon from "components/Icon"; +import { Button } from "components/ui/button"; import { Map, Star, ExternalLink, Check } from "lucide-react"; import Card from "components/Card"; import MutualBadge from "components/MutualBadge"; @@ -75,10 +76,10 @@ export default function SpeciesHero({

    } aria-label="More actions" > - + diff --git a/frontend/components/SpeciesHotspotList.tsx b/frontend/components/SpeciesHotspotList.tsx index e8c8d8ba..7e1eeaa9 100644 --- a/frontend/components/SpeciesHotspotList.tsx +++ b/frontend/components/SpeciesHotspotList.tsx @@ -2,6 +2,7 @@ import React from "react"; import clsx from "clsx"; import Icon from "components/Icon"; import Card from "components/Card"; +import SelectDropdown from "components/SelectDropdown"; import type { OpenBirdingHotspotRanking } from "@birdplan/shared"; export type HotspotItem = OpenBirdingHotspotRanking & { @@ -34,7 +35,16 @@ export default function SpeciesHotspotList({
    Top hotspots
    {tripRangeLabel && ( - + )}
    {loading ? ( @@ -59,63 +69,6 @@ export default function SpeciesHotspotList({ ); } -function MonthRangeDropdown({ - mode, - onChange, - tripRangeLabel, -}: { - mode: MonthMode; - onChange: (m: MonthMode) => void; - tripRangeLabel: string; -}) { - const [open, setOpen] = React.useState(false); - const options: { value: MonthMode; label: string }[] = [ - { value: "all", label: "All Year" }, - { value: "trip", label: tripRangeLabel }, - ]; - const current = options.find((o) => o.value === mode) ?? options[0]; - - return ( -
    - - {open && ( - <> -
    setOpen(false)} className="fixed inset-0 z-30" /> -
    - {options.map((o) => { - const active = o.value === mode; - return ( - - ); - })} -
    - - )} -
    - ); -} - function SpeciesHotspotRow({ h, rank, onSelect }: { h: HotspotItem; rank: number; onSelect: (id: string) => void }) { const freqDisplay = h.frequency > 1 ? Math.round(h.frequency) : h.frequency; return ( diff --git a/frontend/components/Submit.tsx b/frontend/components/Submit.tsx deleted file mode 100644 index 6becba2f..00000000 --- a/frontend/components/Submit.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import Button from "components/Button"; - -type Props = { - loading: boolean; - children: React.ReactNode; - [key: string]: any; -}; - -export default function Submit({ loading, children, ...props }: Props) { - return ( - - ); -} diff --git a/frontend/components/TargetsOptionsDropdown.tsx b/frontend/components/TargetsOptionsDropdown.tsx index 6fd2903f..10498569 100644 --- a/frontend/components/TargetsOptionsDropdown.tsx +++ b/frontend/components/TargetsOptionsDropdown.tsx @@ -7,6 +7,7 @@ import { DropdownMenuTrigger, } from "components/ui/dropdown-menu"; import Icon from "components/Icon"; +import { Button } from "components/ui/button"; import { Download } from "lucide-react"; type Props = { @@ -30,10 +31,10 @@ export default function TargetsOptionsDropdown({ trip }: Props) { return ( } title="Options" > - + {items.map(({ name, icon, onClick }) => ( diff --git a/frontend/components/TravelTime.tsx b/frontend/components/TravelTime.tsx index 5fb1559d..cfdb1694 100644 --- a/frontend/components/TravelTime.tsx +++ b/frontend/components/TravelTime.tsx @@ -1,5 +1,6 @@ import { useTrip } from "hooks/useTrip"; import Icon from "components/Icon"; +import { Button } from "components/ui/button"; import { PersonStanding, Car, Bike } from "lucide-react"; import { formatTime, formatDistance } from "lib/helpers"; import { @@ -125,13 +126,14 @@ export default function TravelTime({ isEditing, dayId, id, isLoading }: Props) { {travelData && !travelData?.isDeleted && ( - + )}
    ) : ( diff --git a/frontend/components/TripNav.tsx b/frontend/components/TripNav.tsx index 519efac7..9e8237da 100644 --- a/frontend/components/TripNav.tsx +++ b/frontend/components/TripNav.tsx @@ -1,9 +1,11 @@ import React from "react"; import clsx from "clsx"; +import { cn } from "lib/utils"; import { useTrip } from "hooks/useTrip"; import { Link, useLocation } from "react-router-dom"; import { useModal } from "stores/modals"; import TripOptionsDropdown from "components/TripOptionsDropdown"; +import { buttonVariants } from "components/ui/button"; import Icon from "components/Icon"; const links = [ @@ -33,9 +35,10 @@ export default function TripNav({ active, border = true }: Props) { diff --git a/frontend/components/TripOptionsDropdown.tsx b/frontend/components/TripOptionsDropdown.tsx index a29c72b7..912b2f99 100644 --- a/frontend/components/TripOptionsDropdown.tsx +++ b/frontend/components/TripOptionsDropdown.tsx @@ -11,6 +11,8 @@ import { DropdownMenuTrigger, } from "components/ui/dropdown-menu"; import Icon from "components/Icon"; +import { Button } from "components/ui/button"; +import { cn } from "lib/utils"; import { Feather, Users, Settings, Download, Send } from "lucide-react"; type Props = { @@ -64,12 +66,9 @@ export default function TripOptionsDropdown({ className }: Props) { return ( } > - + {filteredLinks.map(({ name, href, onClick, icon }) => { diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx index 16da174d..419cb2ac 100644 --- a/frontend/components/ui/button.tsx +++ b/frontend/components/ui/button.tsx @@ -1,58 +1,97 @@ -import { Button as ButtonPrimitive } from "@base-ui/react/button" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "lib/utils" - -const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/80", - outline: - "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", - ghost: - "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", - destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: - "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", - lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - icon: "size-8", - "icon-xs": - "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", - "icon-sm": - "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", - "icon-lg": "size-9", - }, +import * as React from "react"; +import { Link } from "react-router-dom"; +import { Button as ButtonPrimitive } from "@base-ui/react/button"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "lib/utils"; +import Icon from "components/Icon"; + +const buttonVariants = cva("inline-flex items-center justify-center gap-2 font-semibold rounded-full", { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary-hover transition-colors", + secondary: "bg-secondary text-secondary-foreground", + outline: "border border-input text-secondary-foreground bg-transparent hover:bg-gray-50 transition-colors", + "outline-white": "border border-gray-200 bg-white text-gray-700 shadow-xs hover:bg-gray-50 transition-colors", + ghost: "hover:bg-muted hover:text-foreground transition-colors", + nav: "font-medium text-gray-600 hover:bg-slate-300 transition-colors", + danger: "bg-red-600 text-white hover:bg-red-700", + link: "inline text-link font-medium", + "link-danger": "inline text-sm font-medium text-red-600 hover:text-red-600", }, - defaultVariants: { - variant: "default", - size: "default", + size: { + lg: "text-base py-3 px-6", + md: "py-2 px-5", + sm: "gap-1.5 h-9 px-3.5 text-sm font-medium", + xs: "text-xs py-1.5 px-2.5 font-medium", + none: "", + icon: "size-8 text-sm", + "icon-lg": "size-9 text-lg", }, - } -) + }, + compoundVariants: [{ variant: "default", size: "lg", class: "shadow-lg shadow-primary/30" }], + defaultVariants: { + variant: "default", + size: "md", + }, +}); + +type ButtonSize = Exclude["size"]>, "none">; + +type ButtonProps = ButtonPrimitive.Props & + Omit, "size"> & { + size?: ButtonSize; + href?: string; + target?: React.HTMLAttributeAnchorTarget; + rel?: string; + loading?: boolean; + loadingText?: React.ReactNode; + }; function Button({ className, - variant = "default", - size = "default", + variant, + size, + href, + target, + rel, + loading, + loadingText, + disabled, + type = "button", + children, ...props -}: ButtonPrimitive.Props & VariantProps) { +}: ButtonProps) { + const effectiveSize = variant === "link" || variant === "link-danger" ? "none" : size; + const isDisabled = disabled || loading; + const classes = cn(buttonVariants({ variant, size: effectiveSize }), isDisabled && "opacity-60", className); + + const content = loading ? ( + + + {loadingText} + + ) : ( + children + ); + + if (href) { + return /^(https?:|mailto:|tel:|om:)/.test(href) ? ( + )}> + {content} + + ) : ( + )}> + {content} + + ); + } + return ( - - ) + + {content} + + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx index 42b82051..83410d13 100644 --- a/frontend/components/ui/dialog.tsx +++ b/frontend/components/ui/dialog.tsx @@ -63,7 +63,7 @@ function DialogContent({ render={ +

    )}
    diff --git a/frontend/modals/AddParticipant.tsx b/frontend/modals/AddParticipant.tsx index 8b5f3676..c8bb0836 100644 --- a/frontend/modals/AddParticipant.tsx +++ b/frontend/modals/AddParticipant.tsx @@ -6,7 +6,7 @@ import { Header, Body, Footer } from "components/Modal"; import { useModal } from "stores/modals"; import { useTrip } from "hooks/useTrip"; import useMutation from "hooks/useMutation"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; import Input from "components/Input"; import LifelistField from "components/LifelistField"; @@ -116,17 +116,17 @@ export default function AddParticipant() { footer={tab === "invite" ? "They can change this once they accept the invite." : undefined} /> ) : ( - )}
    - -
    diff --git a/frontend/modals/AddPlace.tsx b/frontend/modals/AddPlace.tsx index 862fdd08..24d4b37f 100644 --- a/frontend/modals/AddPlace.tsx +++ b/frontend/modals/AddPlace.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Header, Body } from "components/Modal"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; import Field from "components/Field"; import { useModal } from "stores/modals"; import { useTrip } from "hooks/useTrip"; @@ -101,7 +101,7 @@ export default function AddPlace() {
    - diff --git a/frontend/modals/DeleteAccount.tsx b/frontend/modals/DeleteAccount.tsx index 845033dd..1f1831bd 100644 --- a/frontend/modals/DeleteAccount.tsx +++ b/frontend/modals/DeleteAccount.tsx @@ -5,7 +5,7 @@ import { Header, Body, Footer } from "components/Modal"; import { useModal } from "stores/modals"; import useMutation from "hooks/useMutation"; import toast from "react-hot-toast"; -import Button from "components/Button"; +import { Button } from "components/ui/button"; import { teardownSession } from "lib/logout"; export default function DeleteAccount() { @@ -66,13 +66,12 @@ export default function DeleteAccount() {