summaryrefslogtreecommitdiff
path: root/apps/web/src/components/ui
diff options
context:
space:
mode:
authorKha Nguyen <hi@khanguyen.com>2025-07-24 17:04:27 -0500
committerKha Nguyen <hi@khanguyen.com>2025-07-24 17:04:27 -0500
commit8e737d87e0accbf1d433f4c8b1e8e0f125849799 (patch)
tree386560ac1449991da1ab6d43637e82046a725e71 /apps/web/src/components/ui
parent1ccacf6bb5db579cf6f343d9de57bce92d296f5f (diff)
downloadopencut-8e737d87e0accbf1d433f4c8b1e8e0f125849799.tar.gz
Revert "merge main"
This reverts commit 1ccacf6bb5db579cf6f343d9de57bce92d296f5f.
Diffstat (limited to 'apps/web/src/components/ui')
-rw-r--r--apps/web/src/components/ui/aspect-ratio.tsx8
-rw-r--r--apps/web/src/components/ui/audio-player.tsx254
-rw-r--r--apps/web/src/components/ui/button.tsx3
-rw-r--r--apps/web/src/components/ui/card.tsx5
-rw-r--r--apps/web/src/components/ui/carousel.tsx2
-rw-r--r--apps/web/src/components/ui/chart.tsx4
-rw-r--r--apps/web/src/components/ui/collapsible.tsx12
-rw-r--r--apps/web/src/components/ui/font-picker.tsx80
-rw-r--r--apps/web/src/components/ui/form.tsx4
-rw-r--r--apps/web/src/components/ui/image-timeline-treatment.tsx204
-rw-r--r--apps/web/src/components/ui/phone-input.tsx334
-rw-r--r--apps/web/src/components/ui/separator.tsx14
-rw-r--r--apps/web/src/components/ui/sonner.tsx16
-rw-r--r--apps/web/src/components/ui/sponsor-button.tsx1
-rw-r--r--apps/web/src/components/ui/toaster.tsx24
15 files changed, 488 insertions, 477 deletions
diff --git a/apps/web/src/components/ui/aspect-ratio.tsx b/apps/web/src/components/ui/aspect-ratio.tsx
index 082de9df..6f90bbb8 100644
--- a/apps/web/src/components/ui/aspect-ratio.tsx
+++ b/apps/web/src/components/ui/aspect-ratio.tsx
@@ -1,7 +1,7 @@
-"use client";
+"use client"
-import { AspectRatio as AspectRatioPrimitive } from "radix-ui";
+import { AspectRatio as AspectRatioPrimitive } from "radix-ui"
-const AspectRatio = AspectRatioPrimitive.Root;
+const AspectRatio = AspectRatioPrimitive.Root
-export { AspectRatio };
+export { AspectRatio }
diff --git a/apps/web/src/components/ui/audio-player.tsx b/apps/web/src/components/ui/audio-player.tsx
index 37bf8ca9..774dd95c 100644
--- a/apps/web/src/components/ui/audio-player.tsx
+++ b/apps/web/src/components/ui/audio-player.tsx
@@ -1,127 +1,127 @@
-"use client";
-
-import { useRef, useEffect } from "react";
-import { usePlaybackStore } from "@/stores/playback-store";
-
-interface AudioPlayerProps {
- src: string;
- className?: string;
- clipStartTime: number;
- trimStart: number;
- trimEnd: number;
- clipDuration: number;
- trackMuted?: boolean;
-}
-
-export function AudioPlayer({
- src,
- className = "",
- clipStartTime,
- trimStart,
- trimEnd,
- clipDuration,
- trackMuted = false,
-}: AudioPlayerProps) {
- const audioRef = useRef<HTMLAudioElement>(null);
- const { isPlaying, currentTime, volume, speed, muted } = usePlaybackStore();
-
- // Calculate if we're within this clip's timeline range
- const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd);
- const isInClipRange =
- currentTime >= clipStartTime && currentTime < clipEndTime;
-
- // Sync playback events
- useEffect(() => {
- const audio = audioRef.current;
- if (!audio || !isInClipRange) return;
-
- const handleSeekEvent = (e: CustomEvent) => {
- // Always update audio time, even if outside clip range
- const timelineTime = e.detail.time;
- const audioTime = Math.max(
- trimStart,
- Math.min(
- clipDuration - trimEnd,
- timelineTime - clipStartTime + trimStart
- )
- );
- audio.currentTime = audioTime;
- };
-
- const handleUpdateEvent = (e: CustomEvent) => {
- // Always update audio time, even if outside clip range
- const timelineTime = e.detail.time;
- const targetTime = Math.max(
- trimStart,
- Math.min(
- clipDuration - trimEnd,
- timelineTime - clipStartTime + trimStart
- )
- );
-
- if (Math.abs(audio.currentTime - targetTime) > 0.5) {
- audio.currentTime = targetTime;
- }
- };
-
- const handleSpeed = (e: CustomEvent) => {
- audio.playbackRate = e.detail.speed;
- };
-
- window.addEventListener("playback-seek", handleSeekEvent as EventListener);
- window.addEventListener(
- "playback-update",
- handleUpdateEvent as EventListener
- );
- window.addEventListener("playback-speed", handleSpeed as EventListener);
-
- return () => {
- window.removeEventListener(
- "playback-seek",
- handleSeekEvent as EventListener
- );
- window.removeEventListener(
- "playback-update",
- handleUpdateEvent as EventListener
- );
- window.removeEventListener(
- "playback-speed",
- handleSpeed as EventListener
- );
- };
- }, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]);
-
- // Sync playback state
- useEffect(() => {
- const audio = audioRef.current;
- if (!audio) return;
-
- if (isPlaying && isInClipRange && !trackMuted) {
- audio.play().catch(() => {});
- } else {
- audio.pause();
- }
- }, [isPlaying, isInClipRange, trackMuted]);
-
- // Sync volume and speed
- useEffect(() => {
- const audio = audioRef.current;
- if (!audio) return;
-
- audio.volume = volume;
- audio.muted = muted || trackMuted;
- audio.playbackRate = speed;
- }, [volume, speed, muted, trackMuted]);
-
- return (
- <audio
- ref={audioRef}
- src={src}
- className={className}
- preload="auto"
- controls={false}
- style={{ display: "none" }} // Audio elements don't need visual representation
- onContextMenu={(e) => e.preventDefault()}
- />
- );
-}
+"use client";
+
+import { useRef, useEffect } from "react";
+import { usePlaybackStore } from "@/stores/playback-store";
+
+interface AudioPlayerProps {
+ src: string;
+ className?: string;
+ clipStartTime: number;
+ trimStart: number;
+ trimEnd: number;
+ clipDuration: number;
+ trackMuted?: boolean;
+}
+
+export function AudioPlayer({
+ src,
+ className = "",
+ clipStartTime,
+ trimStart,
+ trimEnd,
+ clipDuration,
+ trackMuted = false,
+}: AudioPlayerProps) {
+ const audioRef = useRef<HTMLAudioElement>(null);
+ const { isPlaying, currentTime, volume, speed, muted } = usePlaybackStore();
+
+ // Calculate if we're within this clip's timeline range
+ const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd);
+ const isInClipRange =
+ currentTime >= clipStartTime && currentTime < clipEndTime;
+
+ // Sync playback events
+ useEffect(() => {
+ const audio = audioRef.current;
+ if (!audio || !isInClipRange) return;
+
+ const handleSeekEvent = (e: CustomEvent) => {
+ // Always update audio time, even if outside clip range
+ const timelineTime = e.detail.time;
+ const audioTime = Math.max(
+ trimStart,
+ Math.min(
+ clipDuration - trimEnd,
+ timelineTime - clipStartTime + trimStart
+ )
+ );
+ audio.currentTime = audioTime;
+ };
+
+ const handleUpdateEvent = (e: CustomEvent) => {
+ // Always update audio time, even if outside clip range
+ const timelineTime = e.detail.time;
+ const targetTime = Math.max(
+ trimStart,
+ Math.min(
+ clipDuration - trimEnd,
+ timelineTime - clipStartTime + trimStart
+ )
+ );
+
+ if (Math.abs(audio.currentTime - targetTime) > 0.5) {
+ audio.currentTime = targetTime;
+ }
+ };
+
+ const handleSpeed = (e: CustomEvent) => {
+ audio.playbackRate = e.detail.speed;
+ };
+
+ window.addEventListener("playback-seek", handleSeekEvent as EventListener);
+ window.addEventListener(
+ "playback-update",
+ handleUpdateEvent as EventListener
+ );
+ window.addEventListener("playback-speed", handleSpeed as EventListener);
+
+ return () => {
+ window.removeEventListener(
+ "playback-seek",
+ handleSeekEvent as EventListener
+ );
+ window.removeEventListener(
+ "playback-update",
+ handleUpdateEvent as EventListener
+ );
+ window.removeEventListener(
+ "playback-speed",
+ handleSpeed as EventListener
+ );
+ };
+ }, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]);
+
+ // Sync playback state
+ useEffect(() => {
+ const audio = audioRef.current;
+ if (!audio) return;
+
+ if (isPlaying && isInClipRange && !trackMuted) {
+ audio.play().catch(() => {});
+ } else {
+ audio.pause();
+ }
+ }, [isPlaying, isInClipRange, trackMuted]);
+
+ // Sync volume and speed
+ useEffect(() => {
+ const audio = audioRef.current;
+ if (!audio) return;
+
+ audio.volume = volume;
+ audio.muted = muted || trackMuted;
+ audio.playbackRate = speed;
+ }, [volume, speed, muted, trackMuted]);
+
+ return (
+ <audio
+ ref={audioRef}
+ src={src}
+ className={className}
+ preload="auto"
+ controls={false}
+ style={{ display: "none" }} // Audio elements don't need visual representation
+ onContextMenu={(e) => e.preventDefault()}
+ />
+ );
+}
diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx
index 5902af65..b8b9ab2f 100644
--- a/apps/web/src/components/ui/button.tsx
+++ b/apps/web/src/components/ui/button.tsx
@@ -9,7 +9,8 @@ const buttonVariants = cva(
{
variants: {
variant: {
- default: "bg-foreground text-background shadow hover:bg-foreground/90",
+ default:
+ "bg-foreground text-background shadow hover:bg-foreground/90",
primary:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx
index 6d0b37cc..c5ca9d9f 100644
--- a/apps/web/src/components/ui/card.tsx
+++ b/apps/web/src/components/ui/card.tsx
@@ -8,7 +8,10 @@ const Card = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
- className={cn("rounded-xl border bg-card text-card-foreground", className)}
+ className={cn(
+ "rounded-xl border bg-card text-card-foreground",
+ className
+ )}
{...props}
/>
));
diff --git a/apps/web/src/components/ui/carousel.tsx b/apps/web/src/components/ui/carousel.tsx
index a9c98b67..3c0efdc6 100644
--- a/apps/web/src/components/ui/carousel.tsx
+++ b/apps/web/src/components/ui/carousel.tsx
@@ -124,7 +124,7 @@ const Carousel = React.forwardRef<
<CarouselContext.Provider
value={{
carouselRef,
- api,
+ api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
diff --git a/apps/web/src/components/ui/chart.tsx b/apps/web/src/components/ui/chart.tsx
index 82d77752..a2c19116 100644
--- a/apps/web/src/components/ui/chart.tsx
+++ b/apps/web/src/components/ui/chart.tsx
@@ -188,7 +188,7 @@ const ChartTooltipContent = React.forwardRef<
className
)}
>
- {nestLabel ? null : tooltipLabel}
+ {!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
@@ -328,7 +328,7 @@ function getPayloadConfigFromPayload(
key: string
) {
if (typeof payload !== "object" || payload === null) {
- return;
+ return undefined;
}
const payloadPayload =
diff --git a/apps/web/src/components/ui/collapsible.tsx b/apps/web/src/components/ui/collapsible.tsx
index acb56414..6642d604 100644
--- a/apps/web/src/components/ui/collapsible.tsx
+++ b/apps/web/src/components/ui/collapsible.tsx
@@ -1,11 +1,11 @@
-"use client";
+"use client"
-import { Collapsible as CollapsiblePrimitive } from "radix-ui";
+import { Collapsible as CollapsiblePrimitive } from "radix-ui"
-const Collapsible = CollapsiblePrimitive.Root;
+const Collapsible = CollapsiblePrimitive.Root
-const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
-const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
+const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
-export { Collapsible, CollapsibleTrigger, CollapsibleContent };
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/apps/web/src/components/ui/font-picker.tsx b/apps/web/src/components/ui/font-picker.tsx
index 479a5f0c..9a152fd4 100644
--- a/apps/web/src/components/ui/font-picker.tsx
+++ b/apps/web/src/components/ui/font-picker.tsx
@@ -1,40 +1,40 @@
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { FONT_OPTIONS, FontFamily } from "@/constants/font-constants";
-
-interface FontPickerProps {
- defaultValue?: FontFamily;
- onValueChange?: (value: FontFamily) => void;
- className?: string;
-}
-
-export function FontPicker({
- defaultValue,
- onValueChange,
- className,
-}: FontPickerProps) {
- return (
- <Select defaultValue={defaultValue} onValueChange={onValueChange}>
- <SelectTrigger className={`w-full text-xs ${className || ""}`}>
- <SelectValue placeholder="Select a font" />
- </SelectTrigger>
- <SelectContent>
- {FONT_OPTIONS.map((font) => (
- <SelectItem
- key={font.value}
- value={font.value}
- className="text-xs"
- style={{ fontFamily: font.value }}
- >
- {font.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- );
-}
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { FONT_OPTIONS, FontFamily } from "@/constants/font-constants";
+
+interface FontPickerProps {
+ defaultValue?: FontFamily;
+ onValueChange?: (value: FontFamily) => void;
+ className?: string;
+}
+
+export function FontPicker({
+ defaultValue,
+ onValueChange,
+ className,
+}: FontPickerProps) {
+ return (
+ <Select defaultValue={defaultValue} onValueChange={onValueChange}>
+ <SelectTrigger className={`w-full text-xs ${className || ""}`}>
+ <SelectValue placeholder="Select a font" />
+ </SelectTrigger>
+ <SelectContent>
+ {FONT_OPTIONS.map((font) => (
+ <SelectItem
+ key={font.value}
+ value={font.value}
+ className="text-xs"
+ style={{ fontFamily: font.value }}
+ >
+ {font.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ );
+}
diff --git a/apps/web/src/components/ui/form.tsx b/apps/web/src/components/ui/form.tsx
index cab151e1..cb4f53a9 100644
--- a/apps/web/src/components/ui/form.tsx
+++ b/apps/web/src/components/ui/form.tsx
@@ -115,7 +115,9 @@ const FormControl = React.forwardRef<
ref={ref}
id={formItemId}
aria-describedby={
- error ? `${formDescriptionId} ${formMessageId}` : `${formDescriptionId}`
+ !error
+ ? `${formDescriptionId}`
+ : `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
diff --git a/apps/web/src/components/ui/image-timeline-treatment.tsx b/apps/web/src/components/ui/image-timeline-treatment.tsx
index 7a64be18..31847df5 100644
--- a/apps/web/src/components/ui/image-timeline-treatment.tsx
+++ b/apps/web/src/components/ui/image-timeline-treatment.tsx
@@ -1,102 +1,102 @@
-"use client";
-
-import { useState } from "react";
-import { cn } from "@/lib/utils";
-import type { BackgroundType } from "@/types/editor";
-
-interface ImageTimelineTreatmentProps {
- src: string;
- alt: string;
- targetAspectRatio?: number; // Default to 16:9 for video
- className?: string;
- backgroundType?: BackgroundType;
- backgroundColor?: string;
-}
-
-export function ImageTimelineTreatment({
- src,
- alt,
- targetAspectRatio = 16 / 9,
- className,
- backgroundType = "blur",
- backgroundColor = "#000000",
-}: ImageTimelineTreatmentProps) {
- const [imageLoaded, setImageLoaded] = useState(false);
- const [imageDimensions, setImageDimensions] = useState<{
- width: number;
- height: number;
- } | null>(null);
-
- const handleImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
- const img = e.currentTarget;
- setImageDimensions({
- width: img.naturalWidth,
- height: img.naturalHeight,
- });
- setImageLoaded(true);
- };
-
- const imageAspectRatio = imageDimensions
- ? imageDimensions.width / imageDimensions.height
- : 1;
-
- const needsAspectRatioTreatment = imageAspectRatio !== targetAspectRatio;
-
- return (
- <div
- className={cn("relative overflow-hidden", className)}
- style={{ aspectRatio: targetAspectRatio }}
- >
- {/* Background Layer */}
- {needsAspectRatioTreatment && imageLoaded && (
- <>
- {backgroundType === "blur" && (
- <div className="absolute inset-0">
- <img
- src={src}
- alt=""
- className="w-full h-full object-cover filter blur-xl scale-110 opacity-60"
- aria-hidden="true"
- />
- <div className="absolute inset-0 bg-black/20" />
- </div>
- )}
-
- {backgroundType === "mirror" && (
- <div className="absolute inset-0">
- <img
- src={src}
- alt=""
- className="w-full h-full object-cover opacity-30"
- aria-hidden="true"
- />
- </div>
- )}
-
- {backgroundType === "color" && (
- <div className="absolute inset-0" style={{ backgroundColor }} />
- )}
- </>
- )}
-
- {/* Main Image Layer */}
- <div className="absolute inset-0">
- <img
- src={src}
- alt={alt}
- className="w-full h-full object-cover"
- onLoad={handleImageLoad}
- />
- </div>
-
- {/* Loading state */}
- {!imageLoaded && (
- <div className="absolute inset-0 flex items-center justify-center bg-muted/30">
- <div className="animate-pulse text-xs text-muted-foreground">
- Loading...
- </div>
- </div>
- )}
- </div>
- );
-}
+"use client";
+
+import { useState } from "react";
+import { cn } from "@/lib/utils";
+import type { BackgroundType } from "@/types/editor";
+
+interface ImageTimelineTreatmentProps {
+ src: string;
+ alt: string;
+ targetAspectRatio?: number; // Default to 16:9 for video
+ className?: string;
+ backgroundType?: BackgroundType;
+ backgroundColor?: string;
+}
+
+export function ImageTimelineTreatment({
+ src,
+ alt,
+ targetAspectRatio = 16 / 9,
+ className,
+ backgroundType = "blur",
+ backgroundColor = "#000000",
+}: ImageTimelineTreatmentProps) {
+ const [imageLoaded, setImageLoaded] = useState(false);
+ const [imageDimensions, setImageDimensions] = useState<{
+ width: number;
+ height: number;
+ } | null>(null);
+
+ const handleImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
+ const img = e.currentTarget;
+ setImageDimensions({
+ width: img.naturalWidth,
+ height: img.naturalHeight,
+ });
+ setImageLoaded(true);
+ };
+
+ const imageAspectRatio = imageDimensions
+ ? imageDimensions.width / imageDimensions.height
+ : 1;
+
+ const needsAspectRatioTreatment = imageAspectRatio !== targetAspectRatio;
+
+ return (
+ <div
+ className={cn("relative overflow-hidden", className)}
+ style={{ aspectRatio: targetAspectRatio }}
+ >
+ {/* Background Layer */}
+ {needsAspectRatioTreatment && imageLoaded && (
+ <>
+ {backgroundType === "blur" && (
+ <div className="absolute inset-0">
+ <img
+ src={src}
+ alt=""
+ className="w-full h-full object-cover filter blur-xl scale-110 opacity-60"
+ aria-hidden="true"
+ />
+ <div className="absolute inset-0 bg-black/20" />
+ </div>
+ )}
+
+ {backgroundType === "mirror" && (
+ <div className="absolute inset-0">
+ <img
+ src={src}
+ alt=""
+ className="w-full h-full object-cover opacity-30"
+ aria-hidden="true"
+ />
+ </div>
+ )}
+
+ {backgroundType === "color" && (
+ <div className="absolute inset-0" style={{ backgroundColor }} />
+ )}
+ </>
+ )}
+
+ {/* Main Image Layer */}
+ <div className="absolute inset-0">
+ <img
+ src={src}
+ alt={alt}
+ className="w-full h-full object-cover"
+ onLoad={handleImageLoad}
+ />
+ </div>
+
+ {/* Loading state */}
+ {!imageLoaded && (
+ <div className="absolute inset-0 flex items-center justify-center bg-muted/30">
+ <div className="animate-pulse text-xs text-muted-foreground">
+ Loading...
+ </div>
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/apps/web/src/components/ui/phone-input.tsx b/apps/web/src/components/ui/phone-input.tsx
index ca85df8f..95ad9131 100644
--- a/apps/web/src/components/ui/phone-input.tsx
+++ b/apps/web/src/components/ui/phone-input.tsx
@@ -1,167 +1,167 @@
-import * as React from "react";
-import { CheckIcon, ChevronsUpDown } from "lucide-react";
-import * as RPNInput from "react-phone-number-input";
-import flags from "react-phone-number-input/flags";
-
-import { Button } from "./button";
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
-} from "./command";
-import { Input } from "./input";
-import { Popover, PopoverContent, PopoverTrigger } from "./popover";
-import { ScrollArea } from "./scroll-area";
-import { cn } from "../../lib/utils";
-
-type PhoneInputProps = Omit<
- React.ComponentProps<"input">,
- "onChange" | "value" | "ref"
-> &
- Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & {
- onChange?: (value: RPNInput.Value) => void;
- };
-
-const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =
- React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>(
- ({ className, onChange, defaultCountry = "US", ...props }, ref) => {
- return (
- <RPNInput.default
- ref={ref}
- className={cn("flex", className)}
- flagComponent={FlagComponent}
- countrySelectComponent={CountrySelect}
- inputComponent={InputComponent}
- defaultCountry={defaultCountry}
- international
- addInternationalOption
- smartCaret={false}
- /**
- * Handles the onChange event.
- *
- * react-phone-number-input might trigger the onChange event as undefined
- * when a valid phone number is not entered. To prevent this,
- * the value is coerced to an empty string.
- *
- * @param {E164Number | undefined} value - The entered value
- */
- onChange={(value) => onChange?.(value || ("" as RPNInput.Value))}
- {...props}
- />
- );
- }
- );
-PhoneInput.displayName = "PhoneInput";
-
-const InputComponent = React.forwardRef<
- HTMLInputElement,
- React.ComponentProps<"input">
->(({ className, ...props }, ref) => (
- <Input
- className={cn("rounded-e-lg rounded-s-none", className)}
- {...props}
- ref={ref}
- />
-));
-InputComponent.displayName = "InputComponent";
-
-type CountryEntry = { label: string; value: RPNInput.Country | undefined };
-
-type CountrySelectProps = {
- disabled?: boolean;
- value: RPNInput.Country;
- options: CountryEntry[];
- onChange: (country: RPNInput.Country) => void;
-};
-
-const CountrySelect = ({
- disabled,
- value: selectedCountry,
- options: countryList,
- onChange,
-}: CountrySelectProps) => {
- return (
- <Popover>
- <PopoverTrigger asChild>
- <Button
- type="button"
- variant="outline"
- className="flex gap-1 rounded-e-none rounded-s-lg border-r-0 px-3 focus:z-10"
- disabled={disabled}
- >
- <FlagComponent
- country={selectedCountry}
- countryName={selectedCountry}
- />
- <ChevronsUpDown
- className={cn(
- "-mr-2 size-4 opacity-50",
- disabled ? "hidden" : "opacity-100"
- )}
- />
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-[300px] p-0">
- <Command>
- <CommandInput placeholder="Search country..." />
- <CommandList>
- <ScrollArea className="h-72">
- <CommandEmpty>No country found.</CommandEmpty>
- <CommandGroup>
- {countryList.map(({ value, label }) =>
- value ? (
- <CountrySelectOption
- key={value}
- country={value}
- countryName={label}
- selectedCountry={selectedCountry}
- onChange={onChange}
- />
- ) : null
- )}
- </CommandGroup>
- </ScrollArea>
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- );
-};
-
-interface CountrySelectOptionProps extends RPNInput.FlagProps {
- selectedCountry: RPNInput.Country;
- onChange: (country: RPNInput.Country) => void;
-}
-
-const CountrySelectOption = ({
- country,
- countryName,
- selectedCountry,
- onChange,
-}: CountrySelectOptionProps) => {
- return (
- <CommandItem className="gap-2" onSelect={() => onChange(country)}>
- <FlagComponent country={country} countryName={countryName} />
- <span className="flex-1 text-sm">{countryName}</span>
- <span className="text-sm text-foreground/50">{`+${RPNInput.getCountryCallingCode(country)}`}</span>
- <CheckIcon
- className={`ml-auto size-4 ${country === selectedCountry ? "opacity-100" : "opacity-0"}`}
- />
- </CommandItem>
- );
-};
-
-const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => {
- const Flag = flags[country];
-
- return (
- <span className="flex h-4 w-6 overflow-hidden rounded-sm bg-foreground/20 [&_svg]:size-full">
- {Flag && <Flag title={countryName} />}
- </span>
- );
-};
-
-export { PhoneInput };
+import * as React from "react";
+import { CheckIcon, ChevronsUpDown } from "lucide-react";
+import * as RPNInput from "react-phone-number-input";
+import flags from "react-phone-number-input/flags";
+
+import { Button } from "./button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "./command";
+import { Input } from "./input";
+import { Popover, PopoverContent, PopoverTrigger } from "./popover";
+import { ScrollArea } from "./scroll-area";
+import { cn } from "../../lib/utils";
+
+type PhoneInputProps = Omit<
+ React.ComponentProps<"input">,
+ "onChange" | "value" | "ref"
+> &
+ Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & {
+ onChange?: (value: RPNInput.Value) => void;
+ };
+
+const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =
+ React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>(
+ ({ className, onChange, defaultCountry = "US", ...props }, ref) => {
+ return (
+ <RPNInput.default
+ ref={ref}
+ className={cn("flex", className)}
+ flagComponent={FlagComponent}
+ countrySelectComponent={CountrySelect}
+ inputComponent={InputComponent}
+ defaultCountry={defaultCountry}
+ international
+ addInternationalOption
+ smartCaret={false}
+ /**
+ * Handles the onChange event.
+ *
+ * react-phone-number-input might trigger the onChange event as undefined
+ * when a valid phone number is not entered. To prevent this,
+ * the value is coerced to an empty string.
+ *
+ * @param {E164Number | undefined} value - The entered value
+ */
+ onChange={(value) => onChange?.(value || ("" as RPNInput.Value))}
+ {...props}
+ />
+ );
+ }
+ );
+PhoneInput.displayName = "PhoneInput";
+
+const InputComponent = React.forwardRef<
+ HTMLInputElement,
+ React.ComponentProps<"input">
+>(({ className, ...props }, ref) => (
+ <Input
+ className={cn("rounded-e-lg rounded-s-none", className)}
+ {...props}
+ ref={ref}
+ />
+));
+InputComponent.displayName = "InputComponent";
+
+type CountryEntry = { label: string; value: RPNInput.Country | undefined };
+
+type CountrySelectProps = {
+ disabled?: boolean;
+ value: RPNInput.Country;
+ options: CountryEntry[];
+ onChange: (country: RPNInput.Country) => void;
+};
+
+const CountrySelect = ({
+ disabled,
+ value: selectedCountry,
+ options: countryList,
+ onChange,
+}: CountrySelectProps) => {
+ return (
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button
+ type="button"
+ variant="outline"
+ className="flex gap-1 rounded-e-none rounded-s-lg border-r-0 px-3 focus:z-10"
+ disabled={disabled}
+ >
+ <FlagComponent
+ country={selectedCountry}
+ countryName={selectedCountry}
+ />
+ <ChevronsUpDown
+ className={cn(
+ "-mr-2 size-4 opacity-50",
+ disabled ? "hidden" : "opacity-100"
+ )}
+ />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-[300px] p-0">
+ <Command>
+ <CommandInput placeholder="Search country..." />
+ <CommandList>
+ <ScrollArea className="h-72">
+ <CommandEmpty>No country found.</CommandEmpty>
+ <CommandGroup>
+ {countryList.map(({ value, label }) =>
+ value ? (
+ <CountrySelectOption
+ key={value}
+ country={value}
+ countryName={label}
+ selectedCountry={selectedCountry}
+ onChange={onChange}
+ />
+ ) : null
+ )}
+ </CommandGroup>
+ </ScrollArea>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ );
+};
+
+interface CountrySelectOptionProps extends RPNInput.FlagProps {
+ selectedCountry: RPNInput.Country;
+ onChange: (country: RPNInput.Country) => void;
+}
+
+const CountrySelectOption = ({
+ country,
+ countryName,
+ selectedCountry,
+ onChange,
+}: CountrySelectOptionProps) => {
+ return (
+ <CommandItem className="gap-2" onSelect={() => onChange(country)}>
+ <FlagComponent country={country} countryName={countryName} />
+ <span className="flex-1 text-sm">{countryName}</span>
+ <span className="text-sm text-foreground/50">{`+${RPNInput.getCountryCallingCode(country)}`}</span>
+ <CheckIcon
+ className={`ml-auto size-4 ${country === selectedCountry ? "opacity-100" : "opacity-0"}`}
+ />
+ </CommandItem>
+ );
+};
+
+const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => {
+ const Flag = flags[country];
+
+ return (
+ <span className="flex h-4 w-6 overflow-hidden rounded-sm bg-foreground/20 [&_svg]:size-full">
+ {Flag && <Flag title={countryName} />}
+ </span>
+ );
+};
+
+export { PhoneInput };
diff --git a/apps/web/src/components/ui/separator.tsx b/apps/web/src/components/ui/separator.tsx
index 5b6774dc..12d81c4a 100644
--- a/apps/web/src/components/ui/separator.tsx
+++ b/apps/web/src/components/ui/separator.tsx
@@ -1,9 +1,9 @@
-"use client";
+"use client"
-import * as React from "react";
-import * as SeparatorPrimitive from "@radix-ui/react-separator";
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
@@ -25,7 +25,7 @@ const Separator = React.forwardRef<
{...props}
/>
)
-);
-Separator.displayName = SeparatorPrimitive.Root.displayName;
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
-export { Separator };
+export { Separator }
diff --git a/apps/web/src/components/ui/sonner.tsx b/apps/web/src/components/ui/sonner.tsx
index 549cf841..452f4d9f 100644
--- a/apps/web/src/components/ui/sonner.tsx
+++ b/apps/web/src/components/ui/sonner.tsx
@@ -1,12 +1,12 @@
-"use client";
+"use client"
-import { useTheme } from "next-themes";
-import { Toaster as Sonner } from "sonner";
+import { useTheme } from "next-themes"
+import { Toaster as Sonner } from "sonner"
-type ToasterProps = React.ComponentProps<typeof Sonner>;
+type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
- const { theme = "system" } = useTheme();
+ const { theme = "system" } = useTheme()
return (
<Sonner
@@ -25,7 +25,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
}}
{...props}
/>
- );
-};
+ )
+}
-export { Toaster };
+export { Toaster }
diff --git a/apps/web/src/components/ui/sponsor-button.tsx b/apps/web/src/components/ui/sponsor-button.tsx
index a060e428..79c708a2 100644
--- a/apps/web/src/components/ui/sponsor-button.tsx
+++ b/apps/web/src/components/ui/sponsor-button.tsx
@@ -14,6 +14,7 @@ export function SponsorButton({
companyName,
className = "",
}: SponsorButtonProps) {
+
return (
<motion.a
href={href}
diff --git a/apps/web/src/components/ui/toaster.tsx b/apps/web/src/components/ui/toaster.tsx
index baf623f0..3eb8a720 100644
--- a/apps/web/src/components/ui/toaster.tsx
+++ b/apps/web/src/components/ui/toaster.tsx
@@ -15,16 +15,20 @@ export function Toaster() {
return (
<ToastProvider>
- {toasts.map(({ id, title, description, action, ...props }) => (
- <Toast key={id} {...props}>
- <div className="grid gap-1">
- {title && <ToastTitle>{title}</ToastTitle>}
- {description && <ToastDescription>{description}</ToastDescription>}
- </div>
- {action}
- <ToastClose />
- </Toast>
- ))}
+ {toasts.map(function ({ id, title, description, action, ...props }) {
+ return (
+ <Toast key={id} {...props}>
+ <div className="grid gap-1">
+ {title && <ToastTitle>{title}</ToastTitle>}
+ {description && (
+ <ToastDescription>{description}</ToastDescription>
+ )}
+ </div>
+ {action}
+ <ToastClose />
+ </Toast>
+ );
+ })}
<ToastViewport />
</ToastProvider>
);