Skip to content
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

feat(currency-selector): create new currency selector with search #1641

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,33 @@ export function FieldSelector({
setSelectedIndex(0); // Reset selection when search changes
};

// Ensure selected item is visible in viewport
const scrollToIndex = (index: number) => {
setTimeout(() => {
const selectedElement = document.getElementById(`column-option-${index}`);
if (selectedElement) {
selectedElement.scrollIntoView({ block: "nearest" });
}
}, 0);
};

const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
switch (event.key) {
case "ArrowDown":
event.preventDefault();
setSelectedIndex((prev) =>
prev < filteredColumns.length - 1 ? prev + 1 : prev
);
setSelectedIndex((prev) => {
const newIndex = prev < filteredColumns.length - 1 ? prev + 1 : prev;
scrollToIndex(newIndex);
return newIndex;
});
break;
case "ArrowUp":
event.preventDefault();
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
setSelectedIndex((prev) => {
const newIndex = prev > 0 ? prev - 1 : prev;
scrollToIndex(newIndex);
return newIndex;
});
break;
case "Enter":
event.preventDefault();
Expand All @@ -80,16 +96,6 @@ export function FieldSelector({
}
};

// Ensure selected item is visible in viewport
useEffect(() => {
const selectedElement = document.getElementById(
`column-option-${selectedIndex}`
);
if (selectedElement) {
selectedElement.scrollIntoView({ block: "nearest" });
}
}, [selectedIndex]);

const displayText = filter.isNew
? "Select column"
: parseColumnName(fieldName);
Expand Down
163 changes: 163 additions & 0 deletions app/components/workspace/currency-selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { useMemo, useRef, useState } from "react";
import type { KeyboardEvent } from "react";

import {
Popover,
PopoverContent,
PopoverPortal,
PopoverTrigger,
} from "@radix-ui/react-popover";
import { useLoaderData } from "@remix-run/react";
import { CheckIcon, ChevronDownIcon, SearchIcon } from "lucide-react";
import type { loader } from "~/routes/_layout+/account-details.workspace.$workspaceId.edit";
import { tw } from "~/utils/tw";
import When from "../when/when";

type CurrencySelectorProps = {
className?: string;
defaultValue: string;
name?: string;
};

export default function CurrencySelector({
className,
defaultValue,
name,
}: CurrencySelectorProps) {
const triggerRef = useRef<HTMLButtonElement>(null);
const { curriences } = useLoaderData<typeof loader>();

const [isOpen, setIsOpen] = useState(false);
const [selectedCurrency, setSelectedCurrency] = useState(defaultValue);
const [selectedIndex, setSelectedIndex] = useState<number>(0);

const [searchQuery, setSearchQuery] = useState("");

const filteredCurrencies = useMemo(() => {
if (!searchQuery) {
return curriences;
}

return curriences.filter((currency) =>
currency.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [curriences, searchQuery]);

function handleSelect(currency: string) {
setSelectedCurrency(currency);
setIsOpen(false);
}

// Ensure selected item is visible in viewport
const scrollToIndex = (index: number) => {
setTimeout(() => {
const selectedElement = document.getElementById(
`currency-option-${index}`
);
if (selectedElement) {
selectedElement.scrollIntoView({ block: "nearest" });
}
}, 0);
};

const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
switch (event.key) {
case "ArrowDown":
event.preventDefault();
setSelectedIndex((prev) => {
const newIndex =
prev < filteredCurrencies.length - 1 ? prev + 1 : prev;
scrollToIndex(newIndex);
return newIndex;
});
break;
case "ArrowUp":
event.preventDefault();
setSelectedIndex((prev) => {
const newIndex = prev > 0 ? prev - 1 : prev;
scrollToIndex(newIndex);
return newIndex;
});
break;
case "Enter":
event.preventDefault();
if (filteredCurrencies[selectedIndex]) {
setSelectedCurrency(filteredCurrencies[selectedIndex]);
setIsOpen(false);
}
break;
}
};

return (
<Popover
open={isOpen}
onOpenChange={(v) => {
scrollToIndex(selectedIndex);
setIsOpen(v);
}}
>
<PopoverTrigger asChild>
<button
ref={triggerRef}
className={tw(
"flex w-full items-center justify-between rounded-md border p-3",
className
)}
>
<span>{selectedCurrency}</span>
<ChevronDownIcon className="inline-block size-4 text-gray-500" />
<input type="hidden" name={name} value={selectedCurrency} />
</button>
</PopoverTrigger>
<PopoverPortal>
<PopoverContent
className="z-[999999] max-h-[400px] overflow-scroll rounded-md border bg-white"
side="bottom"
style={{ width: triggerRef?.current?.clientWidth }}
>
<div className="flex items-center border-b">
<SearchIcon className="ml-4 size-4 text-gray-500" />
<input
placeholder="Search currency..."
className="border-0 px-4 py-2 pl-2 text-[14px] focus:border-0 focus:ring-0"
value={searchQuery}
onChange={(event) => {
setSearchQuery(event.target.value);
}}
onKeyDown={handleKeyDown}
/>
</div>
{filteredCurrencies.map((currency, index) => {
const isSelected = selectedCurrency === currency;
const isHovered = selectedIndex === index;

return (
<div
id={`currency-option-${index}`}
key={currency}
className={tw(
"flex items-center justify-between px-4 py-3 text-sm text-gray-600 hover:cursor-pointer hover:bg-gray-50",
isHovered && "bg-gray-50"
)}
onClick={() => {
handleSelect(currency);
}}
>
<span>{currency}</span>
<When truthy={isSelected}>
<CheckIcon className="size-4 text-primary" />
</When>
</div>
);
})}
{filteredCurrencies.length === 0 && (
<div className="px-4 py-2 text-sm text-gray-500">
No currency found
</div>
)}
</PopoverContent>
</PopoverPortal>
</Popover>
);
}
38 changes: 4 additions & 34 deletions app/components/workspace/edit-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,10 @@ import { ACCEPT_SUPPORTED_IMAGES } from "~/utils/constants";
import { isFormProcessing } from "~/utils/form";
import { tw } from "~/utils/tw";
import { zodFieldIsRequired } from "~/utils/zod";
import CurrencySelector from "./currency-selector";
import FormRow from "../forms/form-row";
import { InnerLabel } from "../forms/inner-label";
import Input from "../forms/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../forms/select";
import { CrispButton } from "../marketing/crisp";
import { Button } from "../shared/button";
import { Card } from "../shared/card";
Expand Down Expand Up @@ -61,8 +55,7 @@ export const WorkspaceEditForm = ({
children,
className,
}: Props) => {
const { curriences, organization, isPersonalWorkspace } =
useLoaderData<typeof loader>();
const { organization, isPersonalWorkspace } = useLoaderData<typeof loader>();
const navigation = useNavigation();

let schema = EditWorkspaceFormSchema(
Expand Down Expand Up @@ -144,33 +137,10 @@ export const WorkspaceEditForm = ({
}
>
<InnerLabel hideLg>Currency</InnerLabel>
<Select
<CurrencySelector
defaultValue={currency || "USD"}
disabled={disabled}
name={zo.fields.currency()}
>
<SelectTrigger
className="px-3.5 py-3"
aria-label="Select currency"
>
<SelectValue placeholder="Choose a field type" />
</SelectTrigger>
<SelectContent
position="popper"
className="w-full min-w-[300px]"
align="start"
>
<div className=" max-h-[320px] overflow-auto">
{curriences.map((value) => (
<SelectItem value={value} key={value}>
<span className="mr-4 text-[14px] text-gray-700">
{value}
</span>
</SelectItem>
))}
</div>
</SelectContent>
</Select>
/>
</FormRow>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterEnum
ALTER TYPE "Currency" ADD VALUE 'PKR';

1 change: 1 addition & 0 deletions app/database/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,7 @@ enum Currency {
SEK // Swedish Krona
SGD // Singapore Dollar
ZAR // South African Rand
PKR // Pakistani Rupee
}

enum InviteStatuses {
Expand Down
2 changes: 1 addition & 1 deletion app/routes/_layout+/settings.general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export default function GeneralPage() {
const { organization, canExportAssets } = useLoaderData<typeof loader>();
return (
<div className="mb-2.5 flex flex-col justify-between bg-white md:rounded md:border md:border-gray-200 md:px-6 md:py-5">
<div className=" mb-6">
<div className="mb-6">
<h3 className="text-text-lg font-semibold">General</h3>
<p className="text-sm text-gray-600">
Manage general workspace settings.
Expand Down