From 1cebe4c7864fac3197c199077c1dd9d83c36bfaf Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Sat, 14 Sep 2024 00:04:04 +0200 Subject: [PATCH 01/18] Update default urls in seeds --- db/seeds.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index 2aa06fc..bc2804a 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -91,8 +91,8 @@ def seed_companies { name: "Company #{i}", slug: "company-#{i}", - website: "about:blank", - careers_page: "about:blank", + website: "https://ruby-companies.org", + careers_page: "https://ruby-companies.org", description: "Welcome to the page of Company #{i}. We are a company that does things.", city_id: @sample_cities.sample.id, } From 28542b88dd8e8e69075392affa37c8d64a2c7482 Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Sat, 14 Sep 2024 00:05:11 +0200 Subject: [PATCH 02/18] Format already added ui components --- app/frontend/components/ui/button.tsx | 26 ++++++++++++------------ app/frontend/components/ui/input.tsx | 16 +++++++-------- app/frontend/components/ui/separator.tsx | 18 ++++++++-------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/app/frontend/components/ui/button.tsx b/app/frontend/components/ui/button.tsx index efd711c..be55654 100644 --- a/app/frontend/components/ui/button.tsx +++ b/app/frontend/components/ui/button.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { type VariantProps, cva } from "class-variance-authority"; -import { cn } from "@/utils/ui" +import { cn } from "@/utils/ui"; const buttonVariants = cva( "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", @@ -30,27 +30,27 @@ const buttonVariants = cva( variant: "default", size: "default", }, - } -) + }, +); export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean + asChild?: boolean; } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : "button"; return ( - ) - } -) -Button.displayName = "Button" + ); + }, +); +Button.displayName = "Button"; -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/app/frontend/components/ui/input.tsx b/app/frontend/components/ui/input.tsx index f7e7a67..8ee4bbb 100644 --- a/app/frontend/components/ui/input.tsx +++ b/app/frontend/components/ui/input.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/utils/ui" +import { cn } from "@/utils/ui"; export interface InputProps extends React.InputHTMLAttributes {} @@ -12,14 +12,14 @@ const Input = React.forwardRef( type={type} className={cn( "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", - className + className, )} ref={ref} {...props} /> - ) - } -) -Input.displayName = "Input" + ); + }, +); +Input.displayName = "Input"; -export { Input } +export { Input }; diff --git a/app/frontend/components/ui/separator.tsx b/app/frontend/components/ui/separator.tsx index 2b73cc3..c4e01ff 100644 --- a/app/frontend/components/ui/separator.tsx +++ b/app/frontend/components/ui/separator.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import * as SeparatorPrimitive from "@radix-ui/react-separator" +import * as SeparatorPrimitive from "@radix-ui/react-separator"; +import * as React from "react"; -import { cn } from "@/utils/ui" +import { cn } from "@/utils/ui"; const Separator = React.forwardRef< React.ElementRef, @@ -9,7 +9,7 @@ const Separator = React.forwardRef< >( ( { className, orientation = "horizontal", decorative = true, ...props }, - ref + ref, ) => ( - ) -) -Separator.displayName = SeparatorPrimitive.Root.displayName + ), +); +Separator.displayName = SeparatorPrimitive.Root.displayName; -export { Separator } +export { Separator }; From 7f4ac47dc628250cf5c78d72f2747fa189faa759 Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Sat, 14 Sep 2024 00:25:09 +0200 Subject: [PATCH 03/18] Refactor `ExternalLink` component and add (internal) `Link` component --- app/frontend/components/external_link.tsx | 15 ++++++++++----- app/frontend/components/link.tsx | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 app/frontend/components/link.tsx diff --git a/app/frontend/components/external_link.tsx b/app/frontend/components/external_link.tsx index d8bdd46..de666d4 100644 --- a/app/frontend/components/external_link.tsx +++ b/app/frontend/components/external_link.tsx @@ -1,4 +1,6 @@ -import { Link, type LucideProps } from "lucide-react"; +import { Link as LinkIcon, type LucideProps } from "lucide-react"; + +import { Button } from "@/components/ui/button"; interface Props { href: string; @@ -9,14 +11,17 @@ interface Props { } function ExternalLink({ href, children, icon }: Props) { - const Icon = icon || Link; + const Icon = icon || LinkIcon; return ( ); } diff --git a/app/frontend/components/link.tsx b/app/frontend/components/link.tsx new file mode 100644 index 0000000..c66c389 --- /dev/null +++ b/app/frontend/components/link.tsx @@ -0,0 +1,15 @@ +import { Link as InertiaLink, InertiaLinkProps } from "@inertiajs/react"; + +import { Button } from "@/components/ui/button"; + +interface Props extends InertiaLinkProps {} + +function Link({ children, ...props }: Props) { + return ( + + ); +} + +export default Link; From 17ecd01601cdce28eb06adc209ef0d55bda2ab52 Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Sat, 14 Sep 2024 00:25:28 +0200 Subject: [PATCH 04/18] Add `Card` ui component --- app/frontend/components/ui/card.tsx | 88 +++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 app/frontend/components/ui/card.tsx diff --git a/app/frontend/components/ui/card.tsx b/app/frontend/components/ui/card.tsx new file mode 100644 index 0000000..3f9d527 --- /dev/null +++ b/app/frontend/components/ui/card.tsx @@ -0,0 +1,88 @@ +import * as React from "react"; + +import { cn } from "@/utils/ui"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, children, ...props }, ref) => ( +

+ {children} +

+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; From b850fb983c47f24e06971a33dd8e804f7d5b209c Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Sat, 14 Sep 2024 00:26:19 +0200 Subject: [PATCH 05/18] Add application layout & refactor companies index page --- app/frontend/entrypoints/application.tsx | 16 ++++++++++++---- app/frontend/layouts/application.tsx | 22 ++++++++++++++++++++++ app/frontend/pages/companies/index.tsx | 23 ++++++++++++++--------- 3 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 app/frontend/layouts/application.tsx diff --git a/app/frontend/entrypoints/application.tsx b/app/frontend/entrypoints/application.tsx index 04ca9ed..9b636b3 100644 --- a/app/frontend/entrypoints/application.tsx +++ b/app/frontend/entrypoints/application.tsx @@ -4,16 +4,24 @@ import { createRoot } from "react-dom/client"; import NotFoundPage from "@/pages/errors/not_found"; +import Layout from "@/layouts/application"; import "@/stylesheets/globals.css"; createInertiaApp({ resolve: (name) => { - const pages = import.meta.glob("../pages/**/*.tsx", { eager: true }); - const page = pages[`../pages/${name}.tsx`]; + const pages = import.meta.glob("../pages/**/*.tsx", { + eager: true, + }); + const page = pages[`../pages/${name}.tsx`] as any; - if (page) return page; + if (!page) { + return { default: NotFoundPage }; + } - return NotFoundPage; + page.default.layout = + page.default.layout || ((page: any) => {page}); + + return page; }, setup({ el, App, props }) { createRoot(el).render( diff --git a/app/frontend/layouts/application.tsx b/app/frontend/layouts/application.tsx new file mode 100644 index 0000000..23c687e --- /dev/null +++ b/app/frontend/layouts/application.tsx @@ -0,0 +1,22 @@ +interface Props { + children: React.ReactNode; +} + +function Layout({ children }: Props) { + return ( +
+
+
+

Ruby Companies

+
+
+
+
{children}
+
+
+ ); +} + +Layout.displayName = "layouts/application"; + +export default Layout; diff --git a/app/frontend/pages/companies/index.tsx b/app/frontend/pages/companies/index.tsx index 973ca19..100c335 100644 --- a/app/frontend/pages/companies/index.tsx +++ b/app/frontend/pages/companies/index.tsx @@ -1,9 +1,10 @@ -import { Link } from "@inertiajs/react"; import { ColumnDef } from "@tanstack/react-table"; import { useMemo } from "react"; import { DataTable } from "@/components/data_table"; import ExternalLink from "@/components/external_link"; +import Link from "@/components/link"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import type { Company } from "@/types/company"; import type { PaginationData } from "@/types/pagination_data"; @@ -45,14 +46,18 @@ function Index({ companies, pagination }: Props) { ); return ( -
-

Companies

- -
+ + + Companies + + + + + ); } From 9c6e5cfc000c993c451345f2741d877e23dd48a2 Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Sat, 14 Sep 2024 00:26:32 +0200 Subject: [PATCH 06/18] Improve `NotFound` error page --- app/frontend/pages/errors/not_found.tsx | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/app/frontend/pages/errors/not_found.tsx b/app/frontend/pages/errors/not_found.tsx index d8ead05..1923444 100644 --- a/app/frontend/pages/errors/not_found.tsx +++ b/app/frontend/pages/errors/not_found.tsx @@ -1,19 +1,14 @@ -import { Link } from "@inertiajs/react"; +import Link from "@/components/link"; function NotFound() { return ( -
-
-
-
-

404 - Not Found

-

- Try again on your homepage -

-
-
+
+
+

404 - Not Found

+

+ Try again on the homepage +

-
); } From e7b1d36e8da775b04cc6bc151839897a535ed968 Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Sat, 14 Sep 2024 00:26:55 +0200 Subject: [PATCH 07/18] Support className prop for `DataTable` component --- app/frontend/components/data_table.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/frontend/components/data_table.tsx b/app/frontend/components/data_table.tsx index 7a83577..30fb045 100644 --- a/app/frontend/components/data_table.tsx +++ b/app/frontend/components/data_table.tsx @@ -18,16 +18,20 @@ import { TableRow, } from "@/components/ui/table"; +import { cn } from "@/utils/ui"; + interface DataTableProps { columns: ColumnDef[]; data: TData[]; pagination: PaginatorProps; + className?: string; } export function DataTable({ columns, data, pagination, + className, }: DataTableProps) { const table = useReactTable({ data, @@ -38,7 +42,7 @@ export function DataTable({ return ( <> -
+
{table.getHeaderGroups().map((headerGroup) => ( From 03023ee4c52fd64ebd803763bd02833468528747 Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Sat, 14 Sep 2024 01:03:00 +0200 Subject: [PATCH 08/18] Add footer to application layout --- app/frontend/components/external_link.tsx | 13 ++++-- app/frontend/images/brands/github.svg | 1 + app/frontend/layouts/application.tsx | 3 ++ app/frontend/layouts/application/footer.tsx | 46 +++++++++++++++++++++ 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 app/frontend/images/brands/github.svg create mode 100644 app/frontend/layouts/application/footer.tsx diff --git a/app/frontend/components/external_link.tsx b/app/frontend/components/external_link.tsx index de666d4..54f9df8 100644 --- a/app/frontend/components/external_link.tsx +++ b/app/frontend/components/external_link.tsx @@ -2,20 +2,25 @@ import { Link as LinkIcon, type LucideProps } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { cn } from "@/utils/ui"; + interface Props { href: string; children: React.ReactNode; icon?: React.ForwardRefExoticComponent< Omit & React.RefAttributes - >; + > | null; + className?: string; } -function ExternalLink({ href, children, icon }: Props) { +function ExternalLink({ href, children, icon, className }: Props) { const Icon = icon || LinkIcon; return ( -
- + ); } diff --git a/app/frontend/layouts/application/footer.tsx b/app/frontend/layouts/application/footer.tsx new file mode 100644 index 0000000..f45d22d --- /dev/null +++ b/app/frontend/layouts/application/footer.tsx @@ -0,0 +1,46 @@ +import ExternalLink from "@/components/external_link"; +import { Button } from "@/components/ui/button"; + +import githubImage from "@/images/brands/github.svg"; + +function Footer() { + return ( + + ); +} + +Footer.displayName = "layouts/application/footer"; + +export default Footer; From 1b012a609dff8c17e2f7657d098122758414ac0b Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Tue, 17 Sep 2024 22:45:39 +0200 Subject: [PATCH 09/18] Support className prop in `Link` component --- app/frontend/components/link.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/frontend/components/link.tsx b/app/frontend/components/link.tsx index c66c389..b4fa590 100644 --- a/app/frontend/components/link.tsx +++ b/app/frontend/components/link.tsx @@ -2,11 +2,13 @@ import { Link as InertiaLink, InertiaLinkProps } from "@inertiajs/react"; import { Button } from "@/components/ui/button"; +import { cn } from "@/utils/ui"; + interface Props extends InertiaLinkProps {} -function Link({ children, ...props }: Props) { +function Link({ children, className, ...props }: Props) { return ( - ); From 14483a0978f99f285ec689f014eccd1c06f5e273 Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Tue, 17 Sep 2024 22:45:52 +0200 Subject: [PATCH 10/18] Add `Spinner` ui component --- app/frontend/components/ui/spinner.tsx | 50 ++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 app/frontend/components/ui/spinner.tsx diff --git a/app/frontend/components/ui/spinner.tsx b/app/frontend/components/ui/spinner.tsx new file mode 100644 index 0000000..8a886b4 --- /dev/null +++ b/app/frontend/components/ui/spinner.tsx @@ -0,0 +1,50 @@ +import { VariantProps, cva } from "class-variance-authority"; +import { Loader2 } from "lucide-react"; + +import { cn } from "@/utils/ui"; + +const spinnerVariants = cva("flex-col items-center justify-center", { + variants: { + show: { + true: "flex", + false: "hidden", + }, + }, + defaultVariants: { + show: true, + }, +}); + +const loaderVariants = cva("animate-spin text-primary", { + variants: { + size: { + small: "size-6", + medium: "size-8", + large: "size-12", + }, + }, + defaultVariants: { + size: "medium", + }, +}); + +interface SpinnerContentProps + extends VariantProps, + VariantProps { + className?: string; + children?: React.ReactNode; +} + +export function Spinner({ + size, + show, + children, + className, +}: SpinnerContentProps) { + return ( + + + {children} + + ); +} From 2a51ea0be5742047cd5e60db2503729f58cef63a Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Wed, 18 Sep 2024 18:37:42 +0200 Subject: [PATCH 11/18] Add companies filtering --- Gemfile | 2 + Gemfile.lock | 3 + app/controllers/companies_controller.rb | 23 ++++---- app/controllers/inertia_controller.rb | 4 +- app/frontend/layouts/application.tsx | 11 ++-- app/frontend/layouts/application/header.tsx | 56 +++++++++++++++++++ app/models/company.rb | 4 ++ db/seeds.rb | 4 ++ spec/controllers/companies_controller_spec.rb | 4 ++ spec/support/inertia.rb | 2 +- 10 files changed, 93 insertions(+), 20 deletions(-) create mode 100644 app/frontend/layouts/application/header.tsx diff --git a/Gemfile b/Gemfile index 1a1aade..813bdae 100644 --- a/Gemfile +++ b/Gemfile @@ -99,3 +99,5 @@ gem "ruby-lsp-rspec", "~> 0.1.12", group: :development, require: false gem "bullet", "~> 7.2", group: [:development, :test] gem "friendly_id", "~> 5.5" + +gem "filterameter", "~> 1.0" diff --git a/Gemfile.lock b/Gemfile.lock index 732ee28..b77743c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -124,6 +124,8 @@ GEM railties (>= 5.0.0) faker (3.4.2) i18n (>= 1.8.11, < 2) + filterameter (1.0.0) + rails (>= 6.1) friendly_id (5.5.1) activerecord (>= 4.0.0) globalid (1.2.1) @@ -379,6 +381,7 @@ DEPENDENCIES debug factory_bot_rails (~> 6.4) faker (~> 3.3) + filterameter (~> 1.0) friendly_id (~> 5.5) inertia_rails (~> 3.1) jbuilder diff --git a/app/controllers/companies_controller.rb b/app/controllers/companies_controller.rb index 411a150..a4c6a44 100644 --- a/app/controllers/companies_controller.rb +++ b/app/controllers/companies_controller.rb @@ -1,23 +1,26 @@ # frozen_string_literal: true class CompaniesController < InertiaController + include Filterameter::DeclarativeFilters include Pagy::Backend + default_sort name: :asc + + filter :name, partial: true + filter :city, name: :id, association: :city + filter :region, name: :id, association: :region + filter :country, name: :id, association: :country + filter :continent, name: :id, association: :continent + def index - pagy, companies = pagy(company_scope.all) + companies = build_query_from_filters(Company.with_all_associations) + pagy, companies = pagy(companies) @companies = CompanySerializer.many(companies) - - paginate(pagy) + @pagination = inertia_pagination(pagy) end def show - company = company_scope.friendly.find(params[:id]) + company = Company.with_all_associations.friendly.find(params[:id]) @company = CompanySerializer.one(company) end - - private - - def company_scope - Company.includes(:technologies, :continent, :country, :region, :city) - end end diff --git a/app/controllers/inertia_controller.rb b/app/controllers/inertia_controller.rb index fe0a3f1..1b47902 100644 --- a/app/controllers/inertia_controller.rb +++ b/app/controllers/inertia_controller.rb @@ -11,8 +11,8 @@ def set_default_inertia_rendering inertia_render(inertia: true) unless performed? end - def paginate(pagy) + def inertia_pagination(pagy) pagination = pagy_metadata(pagy) - @pagination = PaginationSerializer.render(pagination) + PaginationSerializer.render(pagination) end end diff --git a/app/frontend/layouts/application.tsx b/app/frontend/layouts/application.tsx index c8d7d80..82c5da4 100644 --- a/app/frontend/layouts/application.tsx +++ b/app/frontend/layouts/application.tsx @@ -1,4 +1,5 @@ import Footer from "@/layouts/application/footer"; +import Header from "@/layouts/application/header"; interface Props { children: React.ReactNode; @@ -6,13 +7,9 @@ interface Props { function Layout({ children }: Props) { return ( -
-
-
-

Ruby Companies

-
-
-
+
+
+
{children}
diff --git a/app/frontend/layouts/application/header.tsx b/app/frontend/layouts/application/header.tsx new file mode 100644 index 0000000..c1c0869 --- /dev/null +++ b/app/frontend/layouts/application/header.tsx @@ -0,0 +1,56 @@ +import { useForm, usePage } from "@inertiajs/react"; +import { Search } from "lucide-react"; + +import Link from "@/components/link"; +import { Input } from "@/components/ui/input"; +import { Spinner } from "@/components/ui/spinner"; + +function Header() { + const { props } = usePage(); + + const { data, setData, get, processing } = useForm({ + q: (props.q as string) || "", + }); + + const handleSearch = (event: React.FormEvent) => { + event.preventDefault(); + get("/companies", { + preserveState: true, + preserveScroll: true, + }); + }; + + const Icon = processing ? Spinner : Search; + + return ( +
+
+
+ + Ruby Companies + +
+
+ + setData("q", event.target.value)} + disabled={processing} + /> +
+ +
+
+
+ ); +} + +Header.displayName = "layouts/application/header"; + +export default Header; diff --git a/app/models/company.rb b/app/models/company.rb index 7652c29..80d989a 100644 --- a/app/models/company.rb +++ b/app/models/company.rb @@ -18,6 +18,10 @@ class Company < ApplicationRecord validates :website, url: { no_local: true, public_suffix: true } validates :careers_page, url: { no_local: true, public_suffix: true }, allow_blank: true validates :slug, presence: true + + scope :with_technologies, -> { includes(:technologies) } + scope :with_locations, -> { includes(:continent, country: :continent, region: :country, city: :region) } + scope :with_all_associations, -> { with_technologies.with_locations } private diff --git a/db/seeds.rb b/db/seeds.rb index bc2804a..70bdea4 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -17,6 +17,10 @@ def destroy_data! CompanyTechnology.destroy_all Technology.destroy_all Company.destroy_all + City.destroy_all + Country.destroy_all + Region.destroy_all + Continent.destroy_all end def seed_geo_data diff --git a/spec/controllers/companies_controller_spec.rb b/spec/controllers/companies_controller_spec.rb index abb82eb..f4262ee 100644 --- a/spec/controllers/companies_controller_spec.rb +++ b/spec/controllers/companies_controller_spec.rb @@ -15,6 +15,10 @@ expect_inertia.to(render_component("companies/index")) expect(inertia.props[:companies]).to(be_serialized_many(companies)) end + + it "has a valid filter declaration" do + expect(described_class.declarations_validator).to(be_valid) + end end describe "#show", inertia: true do diff --git a/spec/support/inertia.rb b/spec/support/inertia.rb index 8ec6885..029afb9 100644 --- a/spec/support/inertia.rb +++ b/spec/support/inertia.rb @@ -13,7 +13,7 @@ @actual_ids = actual&.map { |item| item[:id] } @expected_ids = expected_scope&.map(&:id) - expect(@actual_ids).to(eq(@expected_ids)) + expect(@actual_ids).to(contain_exactly(*@expected_ids)) end failure_message do From d7f3764aaa188c52343f76ceb8c404be03d3a47c Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Fri, 20 Sep 2024 17:08:37 +0200 Subject: [PATCH 12/18] Add `command`, `dialog` & `popover` ui components --- app/frontend/components/ui/command.tsx | 154 ++++ app/frontend/components/ui/dialog.tsx | 120 +++ app/frontend/components/ui/popover.tsx | 29 + package.json | 3 + yarn.lock | 986 +++++++++++++++++++++++-- 5 files changed, 1217 insertions(+), 75 deletions(-) create mode 100644 app/frontend/components/ui/command.tsx create mode 100644 app/frontend/components/ui/dialog.tsx create mode 100644 app/frontend/components/ui/popover.tsx diff --git a/app/frontend/components/ui/command.tsx b/app/frontend/components/ui/command.tsx new file mode 100644 index 0000000..a7bd370 --- /dev/null +++ b/app/frontend/components/ui/command.tsx @@ -0,0 +1,154 @@ +import * as React from "react"; +import { type DialogProps } from "@radix-ui/react-dialog"; +import { Command as CommandPrimitive } from "cmdk"; +import { Search } from "lucide-react"; + +import { Dialog, DialogContent } from "@/components/ui/dialog"; + +import { cn } from "@/utils/ui"; + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +CommandShortcut.displayName = "CommandShortcut"; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/app/frontend/components/ui/dialog.tsx b/app/frontend/components/ui/dialog.tsx new file mode 100644 index 0000000..1c82f9b --- /dev/null +++ b/app/frontend/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import * as React from "react"; +import { X } from "lucide-react"; + +import { cn } from "@/utils/ui"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/app/frontend/components/ui/popover.tsx b/app/frontend/components/ui/popover.tsx new file mode 100644 index 0000000..70daa8c --- /dev/null +++ b/app/frontend/components/ui/popover.tsx @@ -0,0 +1,29 @@ +import * as PopoverPrimitive from "@radix-ui/react-popover"; +import * as React from "react"; + +import { cn } from "@/utils/ui"; + +const Popover = PopoverPrimitive.Root; + +const PopoverTrigger = PopoverPrimitive.Trigger; + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent }; diff --git a/package.json b/package.json index 5ed9ca3..7cf75f9 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,14 @@ }, "dependencies": { "@inertiajs/react": "^1.2.0", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@tanstack/react-table": "^8.19.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "1.0.0", "lucide-react": "^0.412.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/yarn.lock b/yarn.lock index aecb5c8..ac325ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -376,6 +376,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.13.10": + version: 7.25.6 + resolution: "@babel/runtime@npm:7.25.6" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10c0/d6143adf5aa1ce79ed374e33fdfd74fa975055a80bc6e479672ab1eadc4e4bfd7484444e17dd063a1d180e051f3ec62b357c7a2b817e7657687b47313158c3d2 + languageName: node + linkType: hard + "@babel/template@npm:^7.22.15": version: 7.24.0 resolution: "@babel/template@npm:7.24.0" @@ -669,6 +678,44 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.6.0": + version: 1.6.8 + resolution: "@floating-ui/core@npm:1.6.8" + dependencies: + "@floating-ui/utils": "npm:^0.2.8" + checksum: 10c0/d6985462aeccae7b55a2d3f40571551c8c42bf820ae0a477fc40ef462e33edc4f3f5b7f11b100de77c9b58ecb581670c5c3f46d0af82b5e30aa185c735257eb9 + languageName: node + linkType: hard + +"@floating-ui/dom@npm:^1.0.0": + version: 1.6.11 + resolution: "@floating-ui/dom@npm:1.6.11" + dependencies: + "@floating-ui/core": "npm:^1.6.0" + "@floating-ui/utils": "npm:^0.2.8" + checksum: 10c0/02ef34a75a515543c772880338eea7b66724997bd5ec7cd58d26b50325709d46d480a306b84e7d5509d734434411a4bcf23af5680c2e461e6e6a8bf45d751df8 + languageName: node + linkType: hard + +"@floating-ui/react-dom@npm:^2.0.0": + version: 2.1.2 + resolution: "@floating-ui/react-dom@npm:2.1.2" + dependencies: + "@floating-ui/dom": "npm:^1.0.0" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 10c0/e855131c74e68cab505f7f44f92cd4e2efab1c125796db3116c54c0859323adae4bf697bf292ee83ac77b9335a41ad67852193d7aeace90aa2e1c4a640cafa60 + languageName: node + linkType: hard + +"@floating-ui/utils@npm:^0.2.8": + version: 0.2.8 + resolution: "@floating-ui/utils@npm:0.2.8" + checksum: 10c0/a8cee5f17406c900e1c3ef63e3ca89b35e7a2ed645418459a73627b93b7377477fc888081011c6cd177cac45ec2b92a6cab018c14ea140519465498dddd2d3f9 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.14": version: 0.11.14 resolution: "@humanwhocodes/config-array@npm:0.11.14" @@ -764,151 +811,776 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": - version: 0.3.25 - resolution: "@jridgewell/trace-mapping@npm:0.3.25" +"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": + version: 0.3.25 + resolution: "@jridgewell/trace-mapping@npm:0.3.25" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 10c0/3d1ce6ebc69df9682a5a8896b414c6537e428a1d68b02fcc8363b04284a8ca0df04d0ee3013132252ab14f2527bc13bea6526a912ecb5658f0e39fd2860b4df4 + languageName: node + linkType: hard + +"@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1": + version: 5.1.1-v1 + resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1" + dependencies: + eslint-scope: "npm:5.1.1" + checksum: 10c0/75dda3e623b8ad7369ca22552d6beee337a814b2d0e8a32d23edd13fcb65c8082b32c5d86e436f3860dd7ade30d91d5db55d4ef9a08fb5a976c718ecc0d88a74 + languageName: node + linkType: hard + +"@nodelib/fs.scandir@npm:2.1.5": + version: 2.1.5 + resolution: "@nodelib/fs.scandir@npm:2.1.5" + dependencies: + "@nodelib/fs.stat": "npm:2.0.5" + run-parallel: "npm:^1.1.9" + checksum: 10c0/732c3b6d1b1e967440e65f284bd06e5821fedf10a1bea9ed2bb75956ea1f30e08c44d3def9d6a230666574edbaf136f8cfd319c14fd1f87c66e6a44449afb2eb + languageName: node + linkType: hard + +"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": + version: 2.0.5 + resolution: "@nodelib/fs.stat@npm:2.0.5" + checksum: 10c0/88dafe5e3e29a388b07264680dc996c17f4bda48d163a9d4f5c1112979f0ce8ec72aa7116122c350b4e7976bc5566dc3ddb579be1ceaacc727872eb4ed93926d + languageName: node + linkType: hard + +"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": + version: 1.2.8 + resolution: "@nodelib/fs.walk@npm:1.2.8" + dependencies: + "@nodelib/fs.scandir": "npm:2.1.5" + fastq: "npm:^1.6.0" + checksum: 10c0/db9de047c3bb9b51f9335a7bb46f4fcfb6829fb628318c12115fbaf7d369bfce71c15b103d1fc3b464812d936220ee9bc1c8f762d032c9f6be9acc99249095b1 + languageName: node + linkType: hard + +"@npmcli/agent@npm:^2.0.0": + version: 2.2.2 + resolution: "@npmcli/agent@npm:2.2.2" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^10.0.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 10c0/325e0db7b287d4154ecd164c0815c08007abfb07653cc57bceded17bb7fd240998a3cbdbe87d700e30bef494885eccc725ab73b668020811d56623d145b524ae + languageName: node + linkType: hard + +"@npmcli/fs@npm:^3.1.0": + version: 3.1.0 + resolution: "@npmcli/fs@npm:3.1.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10c0/162b4a0b8705cd6f5c2470b851d1dc6cd228c86d2170e1769d738c1fbb69a87160901411c3c035331e9e99db72f1f1099a8b734bf1637cc32b9a5be1660e4e1e + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 10c0/5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd + languageName: node + linkType: hard + +"@pkgr/core@npm:^0.1.0": + version: 0.1.1 + resolution: "@pkgr/core@npm:0.1.1" + checksum: 10c0/3f7536bc7f57320ab2cf96f8973664bef624710c403357429fbf680a5c3b4843c1dbd389bb43daa6b1f6f1f007bb082f5abcb76bb2b5dc9f421647743b71d3d8 + languageName: node + linkType: hard + +"@radix-ui/primitive@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/primitive@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + checksum: 10c0/912216455537db3ca77f3e7f70174fb2b454fbd4a37a0acb7cfadad9ab6131abdfb787472242574460a3c301edf45738340cc84f6717982710082840fde7d916 + languageName: node + linkType: hard + +"@radix-ui/primitive@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/primitive@npm:1.1.0" + checksum: 10c0/1dcc8b5401799416ff8bdb15c7189b4536c193220ad8fd348a48b88f804ee38cec7bd03e2b9641f7da24610e2f61f23a306911ce883af92c4e8c1abac634cb61 + languageName: node + linkType: hard + +"@radix-ui/react-arrow@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-arrow@npm:1.1.0" + dependencies: + "@radix-ui/react-primitive": "npm:2.0.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/cbe059dfa5a9c1677478d363bb5fd75b0c7a08221d0ac7f8e7b9aec9dbae9754f6a3518218cf63e4ed53df6c36d193c8d2618d03433a37aa0cb7ee77a60a591f + languageName: node + linkType: hard + +"@radix-ui/react-compose-refs@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-compose-refs@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/be06f8dab35b5a1bffa7a5982fb26218ddade1acb751288333e3b89d7b4a7dfb5a6371be83876dac0ec2ebe0866d295e8618b778608e1965342986ea448040ec + languageName: node + linkType: hard + +"@radix-ui/react-compose-refs@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-compose-refs@npm:1.1.0" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/7e18706084397d9458ca3473d8565b10691da06f6499a78edbcc4bd72cde08f62e91120658d17d58c19fc39d6b1dffe0133cc4535c8f5fce470abd478f6107e5 + languageName: node + linkType: hard + +"@radix-ui/react-context@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-context@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/3de5761b32cc70cd61715527f29d8c699c01ab28c195ced972ccbc7025763a373a68f18c9f948c7a7b922e469fd2df7fee5f7536e3f7bad44ffc06d959359333 + languageName: node + linkType: hard + +"@radix-ui/react-context@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-context@npm:1.1.0" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/c843980f568cc61b512708863ec84c42a02e0f88359b22ad1c0e290cea3e6d7618eccbd2cd37bd974fadaa7636cbed5bda27553722e61197eb53852eaa34f1bb + languageName: node + linkType: hard + +"@radix-ui/react-dialog@npm:1.0.5": + version: 1.0.5 + resolution: "@radix-ui/react-dialog@npm:1.0.5" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/primitive": "npm:1.0.1" + "@radix-ui/react-compose-refs": "npm:1.0.1" + "@radix-ui/react-context": "npm:1.0.1" + "@radix-ui/react-dismissable-layer": "npm:1.0.5" + "@radix-ui/react-focus-guards": "npm:1.0.1" + "@radix-ui/react-focus-scope": "npm:1.0.4" + "@radix-ui/react-id": "npm:1.0.1" + "@radix-ui/react-portal": "npm:1.0.4" + "@radix-ui/react-presence": "npm:1.0.1" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-slot": "npm:1.0.2" + "@radix-ui/react-use-controllable-state": "npm:1.0.1" + aria-hidden: "npm:^1.1.1" + react-remove-scroll: "npm:2.5.5" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/c5b3069397379e79857a3203f3ead4d12d87736b59899f02a63e620a07dd1e6704e15523926cdf8e39afe1c945a7ff0f2533c5ea5be1e17c3114820300a51133 + languageName: node + linkType: hard + +"@radix-ui/react-dialog@npm:^1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-dialog@npm:1.1.1" + dependencies: + "@radix-ui/primitive": "npm:1.1.0" + "@radix-ui/react-compose-refs": "npm:1.1.0" + "@radix-ui/react-context": "npm:1.1.0" + "@radix-ui/react-dismissable-layer": "npm:1.1.0" + "@radix-ui/react-focus-guards": "npm:1.1.0" + "@radix-ui/react-focus-scope": "npm:1.1.0" + "@radix-ui/react-id": "npm:1.1.0" + "@radix-ui/react-portal": "npm:1.1.1" + "@radix-ui/react-presence": "npm:1.1.0" + "@radix-ui/react-primitive": "npm:2.0.0" + "@radix-ui/react-slot": "npm:1.1.0" + "@radix-ui/react-use-controllable-state": "npm:1.1.0" + aria-hidden: "npm:^1.1.1" + react-remove-scroll: "npm:2.5.7" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/a21e318e8d45bed22067880f66beb4ea91118a6c0d43aa20de495c0373b53c12dfe28f58196d5b33300573a5e24e064ec53648a576f02366fb5a297d887b0860 + languageName: node + linkType: hard + +"@radix-ui/react-dismissable-layer@npm:1.0.5": + version: 1.0.5 + resolution: "@radix-ui/react-dismissable-layer@npm:1.0.5" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/primitive": "npm:1.0.1" + "@radix-ui/react-compose-refs": "npm:1.0.1" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + "@radix-ui/react-use-escape-keydown": "npm:1.0.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/7e4308867aecfb07b506330c1964d94a52247ab9453725613cd326762aa13e483423c250f107219c131b0449600eb8d1576ce3159c2b96e8c978f75e46062cb2 + languageName: node + linkType: hard + +"@radix-ui/react-dismissable-layer@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-dismissable-layer@npm:1.1.0" + dependencies: + "@radix-ui/primitive": "npm:1.1.0" + "@radix-ui/react-compose-refs": "npm:1.1.0" + "@radix-ui/react-primitive": "npm:2.0.0" + "@radix-ui/react-use-callback-ref": "npm:1.1.0" + "@radix-ui/react-use-escape-keydown": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/72967068ab02127b668ecfd0a1863149e2a42d9fd12d3247f51422a41f3d5faa82a147a5b0a8a6ec609eff8fe6baede6fb7d6111f76896656d13567e3ec29ba8 + languageName: node + linkType: hard + +"@radix-ui/react-focus-guards@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-focus-guards@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/d5fd4e5aa9d9a87c8ad490b3b4992d6f1d9eddf18e56df2a2bcf8744c4332b275d73377fd193df3e6ba0ad9608dc497709beca5c64de2b834d5f5350b3c9a272 + languageName: node + linkType: hard + +"@radix-ui/react-focus-guards@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-focus-guards@npm:1.1.0" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/23af9ff17244568db9b2e99ae6e5718747a4b656bf12b1b15b0d3adca407988641a930612eca35a61b7e15d1ce312b3db13ea95999fa31ae641aaaac1e325df8 + languageName: node + linkType: hard + +"@radix-ui/react-focus-scope@npm:1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-focus-scope@npm:1.0.4" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-compose-refs": "npm:1.0.1" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/2fce0bafcab4e16cf4ed7560bda40654223f3d0add6b231e1c607433030c14e6249818b444b7b58ee7a6ff6bbf8e192c9c81d22c3a5c88c2daade9d1f881b5be + languageName: node + linkType: hard + +"@radix-ui/react-focus-scope@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-focus-scope@npm:1.1.0" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.0" + "@radix-ui/react-primitive": "npm:2.0.0" + "@radix-ui/react-use-callback-ref": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/2593d4bbd4a3525624675ec1d5a591a44f015f43f449b99a5a33228159b83f445e8f1c6bc6f9f2011387abaeadd3df406623c08d4e795b7ae509795652a1d069 + languageName: node + linkType: hard + +"@radix-ui/react-id@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-id@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-use-layout-effect": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/e2859ca58bea171c956098ace7ecf615cf9432f58a118b779a14720746b3adcf0351c36c75de131548672d3cd290ca238198acbd33b88dc4706f98312e9317ad + languageName: node + linkType: hard + +"@radix-ui/react-id@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-id@npm:1.1.0" + dependencies: + "@radix-ui/react-use-layout-effect": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/acf13e29e51ee96336837fc0cfecc306328b20b0e0070f6f0f7aa7a621ded4a1ee5537cfad58456f64bae76caa7f8769231e88dc7dc106197347ee433c275a79 + languageName: node + linkType: hard + +"@radix-ui/react-popover@npm:^1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-popover@npm:1.1.1" + dependencies: + "@radix-ui/primitive": "npm:1.1.0" + "@radix-ui/react-compose-refs": "npm:1.1.0" + "@radix-ui/react-context": "npm:1.1.0" + "@radix-ui/react-dismissable-layer": "npm:1.1.0" + "@radix-ui/react-focus-guards": "npm:1.1.0" + "@radix-ui/react-focus-scope": "npm:1.1.0" + "@radix-ui/react-id": "npm:1.1.0" + "@radix-ui/react-popper": "npm:1.2.0" + "@radix-ui/react-portal": "npm:1.1.1" + "@radix-ui/react-presence": "npm:1.1.0" + "@radix-ui/react-primitive": "npm:2.0.0" + "@radix-ui/react-slot": "npm:1.1.0" + "@radix-ui/react-use-controllable-state": "npm:1.1.0" + aria-hidden: "npm:^1.1.1" + react-remove-scroll: "npm:2.5.7" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/4539082143c6c006727cf4a6300479f3dd912e85291d5ed7f084d8a7730acc3b5f6589925ab70eca025d3c78026f52f99c0155e11a35de37fe26b8078e6802b3 + languageName: node + linkType: hard + +"@radix-ui/react-popper@npm:1.2.0": + version: 1.2.0 + resolution: "@radix-ui/react-popper@npm:1.2.0" + dependencies: + "@floating-ui/react-dom": "npm:^2.0.0" + "@radix-ui/react-arrow": "npm:1.1.0" + "@radix-ui/react-compose-refs": "npm:1.1.0" + "@radix-ui/react-context": "npm:1.1.0" + "@radix-ui/react-primitive": "npm:2.0.0" + "@radix-ui/react-use-callback-ref": "npm:1.1.0" + "@radix-ui/react-use-layout-effect": "npm:1.1.0" + "@radix-ui/react-use-rect": "npm:1.1.0" + "@radix-ui/react-use-size": "npm:1.1.0" + "@radix-ui/rect": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/a78ea534b9822d07153fff0895b6cdf742e7213782b140b3ab94a76df0ca70e6001925aea946e99ca680fc63a7fcca49c1d62e8dc5a2f651692fba3541e180c0 + languageName: node + linkType: hard + +"@radix-ui/react-portal@npm:1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-portal@npm:1.0.4" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-primitive": "npm:1.0.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/fed32f8148b833fe852fb5e2f859979ffdf2fb9a9ef46583b9b52915d764ad36ba5c958a64e61d23395628ccc09d678229ee94cd112941e8fe2575021f820c29 + languageName: node + linkType: hard + +"@radix-ui/react-portal@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-portal@npm:1.1.1" + dependencies: + "@radix-ui/react-primitive": "npm:2.0.0" + "@radix-ui/react-use-layout-effect": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/7e7130fcb0d99197322cd97987e1d7279b6c264fb6be3d883cbfcd49267740d83ca17b431e0d98848afd6067a13ee823ca396a8b63ae68f18a728cf70398c830 + languageName: node + linkType: hard + +"@radix-ui/react-presence@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-presence@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-compose-refs": "npm:1.0.1" + "@radix-ui/react-use-layout-effect": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/90780618b265fe794a8f1ddaa5bfd3c71a1127fa79330a14d32722e6265b44452a9dd36efe4e769129d33e57f979f6b8713e2cbf2e2755326aa3b0f337185b6e + languageName: node + linkType: hard + +"@radix-ui/react-presence@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-presence@npm:1.1.0" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.0" + "@radix-ui/react-use-layout-effect": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/58acb658b15b72991ad7a234ea90995902c470b3a182aa90ad03145cbbeaa40f211700c444bfa14cf47537cbb6b732e1359bc5396182de839bd680843c11bf31 + languageName: node + linkType: hard + +"@radix-ui/react-primitive@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-primitive@npm:1.0.3" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-slot": "npm:1.0.2" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/67a66ff8898a5e7739eda228ab6f5ce808858da1dce967014138d87e72b6bbfc93dc1467c706d98d1a2b93bf0b6e09233d1a24d31c78227b078444c1a69c42be + languageName: node + linkType: hard + +"@radix-ui/react-primitive@npm:2.0.0": + version: 2.0.0 + resolution: "@radix-ui/react-primitive@npm:2.0.0" dependencies: - "@jridgewell/resolve-uri": "npm:^3.1.0" - "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 10c0/3d1ce6ebc69df9682a5a8896b414c6537e428a1d68b02fcc8363b04284a8ca0df04d0ee3013132252ab14f2527bc13bea6526a912ecb5658f0e39fd2860b4df4 + "@radix-ui/react-slot": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/00cb6ca499252ca848c299212ba6976171cea7608b10b3f9a9639d6732dea2df1197ba0d97c001a4fdb29313c3e7fc2a490f6245dd3579617a0ffd85ae964fdd languageName: node linkType: hard -"@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1": - version: 5.1.1-v1 - resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1" +"@radix-ui/react-separator@npm:^1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-separator@npm:1.1.0" dependencies: - eslint-scope: "npm:5.1.1" - checksum: 10c0/75dda3e623b8ad7369ca22552d6beee337a814b2d0e8a32d23edd13fcb65c8082b32c5d86e436f3860dd7ade30d91d5db55d4ef9a08fb5a976c718ecc0d88a74 + "@radix-ui/react-primitive": "npm:2.0.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/0ca9e25db27b6b001f3c0c50b2df9d6cf070b949f183043e263115d694a25b7268fecd670572469a512e556deca25ebb08b3aec4a870f0309eed728eef19ab8a languageName: node linkType: hard -"@nodelib/fs.scandir@npm:2.1.5": - version: 2.1.5 - resolution: "@nodelib/fs.scandir@npm:2.1.5" +"@radix-ui/react-slot@npm:1.0.2": + version: 1.0.2 + resolution: "@radix-ui/react-slot@npm:1.0.2" dependencies: - "@nodelib/fs.stat": "npm:2.0.5" - run-parallel: "npm:^1.1.9" - checksum: 10c0/732c3b6d1b1e967440e65f284bd06e5821fedf10a1bea9ed2bb75956ea1f30e08c44d3def9d6a230666574edbaf136f8cfd319c14fd1f87c66e6a44449afb2eb + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-compose-refs": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/3af6ea4891e6fa8091e666802adffe7718b3cd390a10fa9229a5f40f8efded9f3918ea01b046103d93923d41cc32119505ebb6bde76cad07a87b6cf4f2119347 languageName: node linkType: hard -"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": - version: 2.0.5 - resolution: "@nodelib/fs.stat@npm:2.0.5" - checksum: 10c0/88dafe5e3e29a388b07264680dc996c17f4bda48d163a9d4f5c1112979f0ce8ec72aa7116122c350b4e7976bc5566dc3ddb579be1ceaacc727872eb4ed93926d +"@radix-ui/react-slot@npm:1.1.0, @radix-ui/react-slot@npm:^1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-slot@npm:1.1.0" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/a2e8bfb70c440506dd84a1a274f9a8bc433cca37ceae275e53552c9122612e3837744d7fc6f113d6ef1a11491aa914f4add71d76de41cb6d4db72547a8e261ae languageName: node linkType: hard -"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": - version: 1.2.8 - resolution: "@nodelib/fs.walk@npm:1.2.8" +"@radix-ui/react-use-callback-ref@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-callback-ref@npm:1.0.1" dependencies: - "@nodelib/fs.scandir": "npm:2.1.5" - fastq: "npm:^1.6.0" - checksum: 10c0/db9de047c3bb9b51f9335a7bb46f4fcfb6829fb628318c12115fbaf7d369bfce71c15b103d1fc3b464812d936220ee9bc1c8f762d032c9f6be9acc99249095b1 + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/331b432be1edc960ca148637ae6087220873ee828ceb13bd155926ef8f49e862812de5b379129f6aaefcd11be53715f3237e6caa9a33d9c0abfff43f3ba58938 languageName: node linkType: hard -"@npmcli/agent@npm:^2.0.0": - version: 2.2.2 - resolution: "@npmcli/agent@npm:2.2.2" - dependencies: - agent-base: "npm:^7.1.0" - http-proxy-agent: "npm:^7.0.0" - https-proxy-agent: "npm:^7.0.1" - lru-cache: "npm:^10.0.1" - socks-proxy-agent: "npm:^8.0.3" - checksum: 10c0/325e0db7b287d4154ecd164c0815c08007abfb07653cc57bceded17bb7fd240998a3cbdbe87d700e30bef494885eccc725ab73b668020811d56623d145b524ae +"@radix-ui/react-use-callback-ref@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-use-callback-ref@npm:1.1.0" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/e954863f3baa151faf89ac052a5468b42650efca924417470efd1bd254b411a94c69c30de2fdbb90187b38cb984795978e12e30423dc41e4309d93d53b66d819 languageName: node linkType: hard -"@npmcli/fs@npm:^3.1.0": - version: 3.1.0 - resolution: "@npmcli/fs@npm:3.1.0" +"@radix-ui/react-use-controllable-state@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-controllable-state@npm:1.0.1" dependencies: - semver: "npm:^7.3.5" - checksum: 10c0/162b4a0b8705cd6f5c2470b851d1dc6cd228c86d2170e1769d738c1fbb69a87160901411c3c035331e9e99db72f1f1099a8b734bf1637cc32b9a5be1660e4e1e + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/29b069dbf09e48bca321af6272574ad0fc7283174e7d092731a10663fe00c0e6b4bde5e1b5ea67725fe48dcbe8026e7ff0d69d42891c62cbb9ca408498171fbe languageName: node linkType: hard -"@pkgjs/parseargs@npm:^0.11.0": - version: 0.11.0 - resolution: "@pkgjs/parseargs@npm:0.11.0" - checksum: 10c0/5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd +"@radix-ui/react-use-controllable-state@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-use-controllable-state@npm:1.1.0" + dependencies: + "@radix-ui/react-use-callback-ref": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/2af883b5b25822ac226e60a6bfde647c0123a76345052a90219026059b3f7225844b2c13a9a16fba859c1cda5fb3d057f2a04503f71780e607516492db4eb3a1 languageName: node linkType: hard -"@pkgr/core@npm:^0.1.0": - version: 0.1.1 - resolution: "@pkgr/core@npm:0.1.1" - checksum: 10c0/3f7536bc7f57320ab2cf96f8973664bef624710c403357429fbf680a5c3b4843c1dbd389bb43daa6b1f6f1f007bb082f5abcb76bb2b5dc9f421647743b71d3d8 +"@radix-ui/react-use-escape-keydown@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-use-escape-keydown@npm:1.0.3" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/3c94c78902dcb40b60083ee2184614f45c95a189178f52d89323b467bd04bcf5fdb1bc4d43debecd7f0b572c3843c7e04edbcb56f40a4b4b43936fb2770fb8ad languageName: node linkType: hard -"@radix-ui/react-compose-refs@npm:1.1.0": +"@radix-ui/react-use-escape-keydown@npm:1.1.0": version: 1.1.0 - resolution: "@radix-ui/react-compose-refs@npm:1.1.0" + resolution: "@radix-ui/react-use-escape-keydown@npm:1.1.0" + dependencies: + "@radix-ui/react-use-callback-ref": "npm:1.1.0" peerDependencies: "@types/react": "*" react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10c0/7e18706084397d9458ca3473d8565b10691da06f6499a78edbcc4bd72cde08f62e91120658d17d58c19fc39d6b1dffe0133cc4535c8f5fce470abd478f6107e5 + checksum: 10c0/910fd696e5a0994b0e06b9cb68def8a865f47951a013ec240c77db2a9e1e726105602700ef5e5f01af49f2f18fe0e73164f9a9651021f28538ef8a30d91f3fbb languageName: node linkType: hard -"@radix-ui/react-primitive@npm:2.0.0": - version: 2.0.0 - resolution: "@radix-ui/react-primitive@npm:2.0.0" +"@radix-ui/react-use-layout-effect@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-layout-effect@npm:1.0.1" dependencies: - "@radix-ui/react-slot": "npm:1.1.0" + "@babel/runtime": "npm:^7.13.10" peerDependencies: "@types/react": "*" - "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: "@types/react": optional: true - "@types/react-dom": + checksum: 10c0/13cd0c38395c5838bc9a18238020d3bcf67fb340039e6d1cbf438be1b91d64cf6900b78121f3dc9219faeb40dcc7b523ce0f17e4a41631655690e5a30a40886a + languageName: node + linkType: hard + +"@radix-ui/react-use-layout-effect@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-use-layout-effect@npm:1.1.0" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": optional: true - checksum: 10c0/00cb6ca499252ca848c299212ba6976171cea7608b10b3f9a9639d6732dea2df1197ba0d97c001a4fdb29313c3e7fc2a490f6245dd3579617a0ffd85ae964fdd + checksum: 10c0/9bf87ece1845c038ed95863cfccf9d75f557c2400d606343bab0ab3192b9806b9840e6aa0a0333fdf3e83cf9982632852192f3e68d7d8367bc8c788dfdf8e62b languageName: node linkType: hard -"@radix-ui/react-separator@npm:^1.1.0": +"@radix-ui/react-use-rect@npm:1.1.0": version: 1.1.0 - resolution: "@radix-ui/react-separator@npm:1.1.0" + resolution: "@radix-ui/react-use-rect@npm:1.1.0" dependencies: - "@radix-ui/react-primitive": "npm:2.0.0" + "@radix-ui/rect": "npm:1.1.0" peerDependencies: "@types/react": "*" - "@types/react-dom": "*" react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - "@types/react-dom": - optional: true - checksum: 10c0/0ca9e25db27b6b001f3c0c50b2df9d6cf070b949f183043e263115d694a25b7268fecd670572469a512e556deca25ebb08b3aec4a870f0309eed728eef19ab8a + checksum: 10c0/c2e30150ab49e2cec238cda306fd748c3d47fb96dcff69a3b08e1d19108d80bac239d48f1747a25dadca614e3e967267d43b91e60ea59db2befbc7bea913ff84 languageName: node linkType: hard -"@radix-ui/react-slot@npm:1.1.0, @radix-ui/react-slot@npm:^1.1.0": +"@radix-ui/react-use-size@npm:1.1.0": version: 1.1.0 - resolution: "@radix-ui/react-slot@npm:1.1.0" + resolution: "@radix-ui/react-use-size@npm:1.1.0" dependencies: - "@radix-ui/react-compose-refs": "npm:1.1.0" + "@radix-ui/react-use-layout-effect": "npm:1.1.0" peerDependencies: "@types/react": "*" react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10c0/a2e8bfb70c440506dd84a1a274f9a8bc433cca37ceae275e53552c9122612e3837744d7fc6f113d6ef1a11491aa914f4add71d76de41cb6d4db72547a8e261ae + checksum: 10c0/4c8b89037597fdc1824d009e0c941b510c7c6c30f83024cc02c934edd748886786e7d9f36f57323b02ad29833e7fa7e8974d81969b4ab33d8f41661afa4f30a6 + languageName: node + linkType: hard + +"@radix-ui/rect@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/rect@npm:1.1.0" + checksum: 10c0/a26ff7f8708fb5f2f7949baad70a6b2a597d761ee4dd4aadaf1c1a33ea82ea23dfef6ce6366a08310c5d008cdd60b2e626e4ee03fa342bd5f246ddd9d427f6be languageName: node linkType: hard @@ -1530,6 +2202,15 @@ __metadata: languageName: node linkType: hard +"aria-hidden@npm:^1.1.1": + version: 1.2.4 + resolution: "aria-hidden@npm:1.2.4" + dependencies: + tslib: "npm:^2.0.0" + checksum: 10c0/8abcab2e1432efc4db415e97cb3959649ddf52c8fc815d7384f43f3d3abf56f1c12852575d00df9a8927f421d7e0712652dd5f8db244ea57634344e29ecfc74a + languageName: node + linkType: hard + "aria-query@npm:~5.1.3": version: 5.1.3 resolution: "aria-query@npm:5.1.3" @@ -1966,6 +2647,19 @@ __metadata: languageName: node linkType: hard +"cmdk@npm:1.0.0": + version: 1.0.0 + resolution: "cmdk@npm:1.0.0" + dependencies: + "@radix-ui/react-dialog": "npm:1.0.5" + "@radix-ui/react-primitive": "npm:1.0.3" + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 10c0/bf1c9cfce46f2f507ab95735fa08c9aa27e76ecdff87720cc51ae89dbf4814b7559668458f66ff4c3932a88a6b9d8817be05c3cc4ff98bc40c3645acf4a97376 + languageName: node + linkType: hard + "color-convert@npm:^1.9.0": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -2203,6 +2897,13 @@ __metadata: languageName: node linkType: hard +"detect-node-es@npm:^1.1.0": + version: 1.1.0 + resolution: "detect-node-es@npm:1.1.0" + checksum: 10c0/e562f00de23f10c27d7119e1af0e7388407eb4b06596a25f6d79a360094a109ff285de317f02b090faae093d314cf6e73ac3214f8a5bb3a0def5bece94557fbe + languageName: node + linkType: hard + "didyoumean@npm:^1.2.2": version: 1.2.2 resolution: "didyoumean@npm:1.2.2" @@ -3187,6 +3888,13 @@ __metadata: languageName: node linkType: hard +"get-nonce@npm:^1.0.0": + version: 1.0.1 + resolution: "get-nonce@npm:1.0.1" + checksum: 10c0/2d7df55279060bf0568549e1ffc9b84bc32a32b7541675ca092dce56317cdd1a59a98dcc4072c9f6a980779440139a3221d7486f52c488e69dc0fd27b1efb162 + languageName: node + linkType: hard + "get-symbol-description@npm:^1.0.2": version: 1.0.2 resolution: "get-symbol-description@npm:1.0.2" @@ -3481,6 +4189,15 @@ __metadata: languageName: node linkType: hard +"invariant@npm:^2.2.4": + version: 2.2.4 + resolution: "invariant@npm:2.2.4" + dependencies: + loose-envify: "npm:^1.0.0" + checksum: 10c0/5af133a917c0bcf65e84e7f23e779e7abc1cd49cb7fdc62d00d1de74b0d8c1b5ee74ac7766099fb3be1b05b26dfc67bab76a17030d2fe7ea2eef867434362dfc + languageName: node + linkType: hard + "ip-address@npm:^9.0.5": version: 9.0.5 resolution: "ip-address@npm:9.0.5" @@ -3991,7 +4708,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": +"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -4834,6 +5551,77 @@ __metadata: languageName: node linkType: hard +"react-remove-scroll-bar@npm:^2.3.3, react-remove-scroll-bar@npm:^2.3.4": + version: 2.3.6 + resolution: "react-remove-scroll-bar@npm:2.3.6" + dependencies: + react-style-singleton: "npm:^2.2.1" + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/4e32ee04bf655a8bd3b4aacf6ffc596ae9eb1b9ba27eef83f7002632ee75371f61516ae62250634a9eae4b2c8fc6f6982d9b182de260f6c11841841e6e2e7515 + languageName: node + linkType: hard + +"react-remove-scroll@npm:2.5.5": + version: 2.5.5 + resolution: "react-remove-scroll@npm:2.5.5" + dependencies: + react-remove-scroll-bar: "npm:^2.3.3" + react-style-singleton: "npm:^2.2.1" + tslib: "npm:^2.1.0" + use-callback-ref: "npm:^1.3.0" + use-sidecar: "npm:^1.1.2" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/4952657e6a7b9d661d4ad4dfcef81b9c7fa493e35164abff99c35c0b27b3d172ef7ad70c09416dc44dd14ff2e6b38a5ec7da27e27e90a15cbad36b8fd2fd8054 + languageName: node + linkType: hard + +"react-remove-scroll@npm:2.5.7": + version: 2.5.7 + resolution: "react-remove-scroll@npm:2.5.7" + dependencies: + react-remove-scroll-bar: "npm:^2.3.4" + react-style-singleton: "npm:^2.2.1" + tslib: "npm:^2.1.0" + use-callback-ref: "npm:^1.3.0" + use-sidecar: "npm:^1.1.2" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/dcd523ada602bd0a839c2032cadf0b3e4af55ee85acefee3760976a9cceaa4606927801b093bbb8bf3c2989c71e048f5428c2c6eb9e6681762e86356833d039b + languageName: node + linkType: hard + +"react-style-singleton@npm:^2.2.1": + version: 2.2.1 + resolution: "react-style-singleton@npm:2.2.1" + dependencies: + get-nonce: "npm:^1.0.0" + invariant: "npm:^2.2.4" + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/6d66f3bdb65e1ec79089f80314da97c9a005087a04ee034255a5de129a4c0d9fd0bf99fa7bf642781ac2dc745ca687aae3de082bd8afdd0d117bc953241e15ad + languageName: node + linkType: hard + "react@npm:^18.3.1": version: 18.3.1 resolution: "react@npm:18.3.1" @@ -4876,6 +5664,13 @@ __metadata: languageName: node linkType: hard +"regenerator-runtime@npm:^0.14.0": + version: 0.14.1 + resolution: "regenerator-runtime@npm:0.14.1" + checksum: 10c0/1b16eb2c4bceb1665c89de70dcb64126a22bc8eb958feef3cd68fe11ac6d2a4899b5cd1b80b0774c7c03591dc57d16631a7f69d2daa2ec98100e2f29f7ec4cc4 + languageName: node + linkType: hard + "regexp.prototype.flags@npm:^1.5.1, regexp.prototype.flags@npm:^1.5.2": version: 1.5.2 resolution: "regexp.prototype.flags@npm:1.5.2" @@ -5047,6 +5842,8 @@ __metadata: resolution: "root-workspace-0b6124@workspace:." dependencies: "@inertiajs/react": "npm:^1.2.0" + "@radix-ui/react-dialog": "npm:^1.1.1" + "@radix-ui/react-popover": "npm:^1.1.1" "@radix-ui/react-separator": "npm:^1.1.0" "@radix-ui/react-slot": "npm:^1.1.0" "@shopify/eslint-plugin": "npm:^46.0.0" @@ -5059,6 +5856,7 @@ __metadata: autoprefixer: "npm:^10.4.20" class-variance-authority: "npm:^0.7.0" clsx: "npm:^2.1.1" + cmdk: "npm:1.0.0" eslint: "npm:8.57.0" lucide-react: "npm:^0.412.0" postcss: "npm:^8.4.45" @@ -5612,6 +6410,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.0.0, tslib@npm:^2.1.0": + version: 2.7.0 + resolution: "tslib@npm:2.7.0" + checksum: 10c0/469e1d5bf1af585742128827000711efa61010b699cb040ab1800bcd3ccdd37f63ec30642c9e07c4439c1db6e46345582614275daca3e0f4abae29b0083f04a6 + languageName: node + linkType: hard + "tslib@npm:^2.0.3, tslib@npm:^2.6.2": version: 2.6.2 resolution: "tslib@npm:2.6.2" @@ -5801,6 +6606,37 @@ __metadata: languageName: node linkType: hard +"use-callback-ref@npm:^1.3.0": + version: 1.3.2 + resolution: "use-callback-ref@npm:1.3.2" + dependencies: + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/d232c37160fe3970c99255da19b5fb5299fb5926a5d6141d928a87feb47732c323d29be2f8137d3b1e5499c70d284cd1d9cfad703cc58179db8be24d7dd8f1f2 + languageName: node + linkType: hard + +"use-sidecar@npm:^1.1.2": + version: 1.1.2 + resolution: "use-sidecar@npm:1.1.2" + dependencies: + detect-node-es: "npm:^1.1.0" + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/89f0018fd9aee1fc17c85ac18c4bf8944d460d453d0d0e04ddbc8eaddf3fa591e9c74a1f8a438a1bff368a7a2417fab380bdb3df899d2194c4375b0982736de0 + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.2": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" From b9650b4ccfaf2c25550ff404aa96a6e31596ad1b Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Fri, 20 Sep 2024 17:09:19 +0200 Subject: [PATCH 13/18] More work --- app/controllers/companies_controller.rb | 48 +++++++++- app/frontend/components/combobox.tsx | 95 ++++++++++++++++++ .../hooks/use_auto_apply_resource_filter.ts | 25 +++++ app/frontend/hooks/use_resource_filter.ts | 31 ++++++ app/frontend/layouts/application/header.tsx | 23 ++--- app/frontend/pages/companies/_filters.tsx | 96 +++++++++++++++++++ app/frontend/pages/companies/index.tsx | 17 +++- app/frontend/types/company.ts | 2 +- app/frontend/types/company_filter.ts | 7 ++ .../{geo_fragment.ts => fragments/geo.ts} | 0 app/frontend/types/fragments/option.ts | 4 + app/serializers/company_serializer.rb | 8 +- app/serializers/fragment/geo_serializer.rb | 11 +++ app/serializers/fragment/option_serializer.rb | 10 ++ app/serializers/geo_fragment_serializer.rb | 9 -- 15 files changed, 356 insertions(+), 30 deletions(-) create mode 100644 app/frontend/components/combobox.tsx create mode 100644 app/frontend/hooks/use_auto_apply_resource_filter.ts create mode 100644 app/frontend/hooks/use_resource_filter.ts create mode 100644 app/frontend/pages/companies/_filters.tsx create mode 100644 app/frontend/types/company_filter.ts rename app/frontend/types/{geo_fragment.ts => fragments/geo.ts} (100%) create mode 100644 app/frontend/types/fragments/option.ts create mode 100644 app/serializers/fragment/geo_serializer.rb create mode 100644 app/serializers/fragment/option_serializer.rb delete mode 100644 app/serializers/geo_fragment_serializer.rb diff --git a/app/controllers/companies_controller.rb b/app/controllers/companies_controller.rb index a4c6a44..d414265 100644 --- a/app/controllers/companies_controller.rb +++ b/app/controllers/companies_controller.rb @@ -7,20 +7,60 @@ class CompaniesController < InertiaController default_sort name: :asc filter :name, partial: true - filter :city, name: :id, association: :city - filter :region, name: :id, association: :region - filter :country, name: :id, association: :country - filter :continent, name: :id, association: :continent + filter :continent, name: :slug, association: :continent + filter :country, name: :slug, association: :country + filter :region, name: :slug, association: :region + filter :city, name: :slug, association: :city def index companies = build_query_from_filters(Company.with_all_associations) pagy, companies = pagy(companies) @companies = CompanySerializer.many(companies) @pagination = inertia_pagination(pagy) + + @filters = { + name: filter_params[:name], + continent: filter_params[:continent], + country: filter_params[:country], + region: filter_params[:region], + city: filter_params[:city], + }.compact_blank + + # TODO: Refactor into serializer/service + countries = if filter_params[:continent] + Country.includes(:continent).where(continent: { slug: filter_params[:continent] }) + else + [] + end + + regions = if filter_params[:continent] && filter_params[:country] + Region.includes(:country).where(country: { slug: filter_params[:country] }) + else + [] + end + + cities = if filter_params[:continent] && filter_params[:country] && filter_params[:region] + City.includes(:region).where(region: { slug: filter_params[:region] }) + else + [] + end + + @options = { + continents: Fragment::OptionSerializer.many(Continent.all), + countries: Fragment::OptionSerializer.many(countries), + regions: Fragment::OptionSerializer.many(regions), + cities: Fragment::OptionSerializer.many(cities), + } end def show company = Company.with_all_associations.friendly.find(params[:id]) @company = CompanySerializer.one(company) end + + private + + def filter_params + params.fetch(:filter, {}) + end end diff --git a/app/frontend/components/combobox.tsx b/app/frontend/components/combobox.tsx new file mode 100644 index 0000000..819332d --- /dev/null +++ b/app/frontend/components/combobox.tsx @@ -0,0 +1,95 @@ +"use client"; + +import * as React from "react"; +import { Check, ChevronsUpDown } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +import { cn } from "@/utils/ui"; + +import type { Option } from "@/types/fragments/option"; + +interface ComboboxProps { + options: Option[]; + value?: string; + onChange: (value: string) => void; + placeholder?: string; + emptyMessage?: string; + searchPlaceholder?: string; + disabled?: boolean; +} + +function Combobox({ + options, + value, + onChange, + placeholder = "Select option...", + emptyMessage = "No option found.", + searchPlaceholder = "Search option...", + disabled = false, +}: ComboboxProps) { + const [open, setOpen] = React.useState(false); + + const selectedOption = options.find((option) => option.value === value); + const displayValue = selectedOption ? selectedOption.label : placeholder; + + return ( + + + + + + + + + {emptyMessage} + + {options.map((option) => ( + { + onChange(currentValue === value ? "" : currentValue); + setOpen(false); + }} + > + + {option.label} + + ))} + + + + + + ); +} + +export default Combobox; diff --git a/app/frontend/hooks/use_auto_apply_resource_filter.ts b/app/frontend/hooks/use_auto_apply_resource_filter.ts new file mode 100644 index 0000000..0d45e65 --- /dev/null +++ b/app/frontend/hooks/use_auto_apply_resource_filter.ts @@ -0,0 +1,25 @@ +import type { VisitOptions } from "@inertiajs/core"; +import { useEffect, useState } from "react"; + +import type { ResourceFilterHookResult } from "./use_resource_filter"; + +export const useAutoApplyResourceFilter = ( + { filter, get }: ResourceFilterHookResult, + url: string, + options?: VisitOptions, +) => { + // We want to skip the first useEffect, since it triggers automatically on page load. + const [isFirstRender, setIsFirstRender] = useState(true); + + useEffect(() => { + if (isFirstRender) { + setIsFirstRender(false); + return; + } + + get(url, options); + + // If we add the other dependencies it triggers an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filter]); +}; diff --git a/app/frontend/hooks/use_resource_filter.ts b/app/frontend/hooks/use_resource_filter.ts new file mode 100644 index 0000000..88c8f2b --- /dev/null +++ b/app/frontend/hooks/use_resource_filter.ts @@ -0,0 +1,31 @@ +import type { VisitOptions } from "@inertiajs/core"; +import { useForm } from "@inertiajs/react"; + +export interface ResourceFilterHookResult { + filter: T; + setFilter: (key: keyof T, value: string) => void; + get: (url: string, options?: VisitOptions) => void; + processing: boolean; + isDirty: boolean; +} + +export const useResourceFilter = (initialState: T) => { + const { data, setData, get, processing, isDirty } = useForm({ + filter: initialState, + }); + + const setFilter = (key: keyof T, value: string) => { + const cleanedValue = value.trim() === "" ? undefined : value; + setData((prevData) => ({ + filter: { ...prevData.filter, [key]: cleanedValue }, + })); + }; + + return { + filter: data.filter as T, + setFilter, + get, + processing, + isDirty, + }; +}; diff --git a/app/frontend/layouts/application/header.tsx b/app/frontend/layouts/application/header.tsx index c1c0869..d9de654 100644 --- a/app/frontend/layouts/application/header.tsx +++ b/app/frontend/layouts/application/header.tsx @@ -1,18 +1,21 @@ -import { useForm, usePage } from "@inertiajs/react"; import { Search } from "lucide-react"; import Link from "@/components/link"; import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; +import { useResourceFilter } from "@/hooks/use_resource_filter"; +import { CompanyFilter } from "@/types/company_filter"; + function Header() { - const { props } = usePage(); + const { filter, setFilter, get, processing } = + useResourceFilter({ + name: undefined, + }); - const { data, setData, get, processing } = useForm({ - q: (props.q as string) || "", - }); + const Icon = processing ? Spinner : Search; - const handleSearch = (event: React.FormEvent) => { + const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); get("/companies", { preserveState: true, @@ -20,8 +23,6 @@ function Header() { }); }; - const Icon = processing ? Spinner : Search; - return (
@@ -32,15 +33,15 @@ function Header() { > Ruby Companies -
+
setData("q", event.target.value)} + value={filter.name} + onChange={(event) => setFilter("name", event.target.value)} disabled={processing} />
diff --git a/app/frontend/pages/companies/_filters.tsx b/app/frontend/pages/companies/_filters.tsx new file mode 100644 index 0000000..3d9925c --- /dev/null +++ b/app/frontend/pages/companies/_filters.tsx @@ -0,0 +1,96 @@ +import Combobox from "@/components/combobox"; + +import { useAutoApplyResourceFilter } from "@/hooks/use_auto_apply_resource_filter"; +import { useResourceFilter } from "@/hooks/use_resource_filter"; +import { CompanyFilter } from "@/types/company_filter"; +import type { Option } from "@/types/fragments/option"; + +interface Props { + filters: CompanyFilter; + options: { + continents: Option[]; + countries: Option[]; + regions: Option[]; + cities: Option[]; + }; +} + +function Filters({ filters, options }: Props) { + const resourceFilter = useResourceFilter({ + continent: filters.continent, + country: filters.country, + region: filters.region, + city: filters.city, + }); + useAutoApplyResourceFilter(resourceFilter, "/companies", { + preserveState: true, + preserveScroll: true, + only: ["companies", "pagination", "options"], + }); + const { filter, setFilter } = resourceFilter; + + const handleSetContinentFilter = (value: string) => { + setFilter("continent", value); + setFilter("country", ""); + setFilter("region", ""); + setFilter("city", ""); + }; + + const handleSetCountryFilter = (value: string) => { + setFilter("country", value); + setFilter("region", ""); + setFilter("city", ""); + }; + + const handleSetRegionFilter = (value: string) => { + setFilter("region", value); + setFilter("city", ""); + }; + const handleSetCityFilter = (value: string) => { + setFilter("city", value); + }; + + return ( + <> + + + + + + ); +} + +Filters.displayName = "companies/_filters"; + +export default Filters; diff --git a/app/frontend/pages/companies/index.tsx b/app/frontend/pages/companies/index.tsx index 100c335..eb099c4 100644 --- a/app/frontend/pages/companies/index.tsx +++ b/app/frontend/pages/companies/index.tsx @@ -7,14 +7,25 @@ import Link from "@/components/link"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import type { Company } from "@/types/company"; +import { CompanyFilter } from "@/types/company_filter"; +import type { Option } from "@/types/fragments/option"; import type { PaginationData } from "@/types/pagination_data"; +import CompanyFilters from "./_filters"; + interface Props { companies: Company[]; pagination: PaginationData; + filters: CompanyFilter; + options: { + continents: Option[]; + countries: Option[]; + regions: Option[]; + cities: Option[]; + }; } -function Index({ companies, pagination }: Props) { +function Index({ companies, pagination, filters, options }: Props) { const columns: ColumnDef[] = useMemo( () => [ { @@ -51,6 +62,10 @@ function Index({ companies, pagination }: Props) { Companies +
+ +
+ Date: Fri, 20 Sep 2024 18:23:14 +0200 Subject: [PATCH 14/18] Refactor filters and options in companies controller index --- app/controllers/companies_controller.rb | 35 ++----------------- app/serializers/company_options_serializer.rb | 33 +++++++++++++++++ 2 files changed, 35 insertions(+), 33 deletions(-) create mode 100644 app/serializers/company_options_serializer.rb diff --git a/app/controllers/companies_controller.rb b/app/controllers/companies_controller.rb index d414265..b362e4e 100644 --- a/app/controllers/companies_controller.rb +++ b/app/controllers/companies_controller.rb @@ -18,39 +18,8 @@ def index @companies = CompanySerializer.many(companies) @pagination = inertia_pagination(pagy) - @filters = { - name: filter_params[:name], - continent: filter_params[:continent], - country: filter_params[:country], - region: filter_params[:region], - city: filter_params[:city], - }.compact_blank - - # TODO: Refactor into serializer/service - countries = if filter_params[:continent] - Country.includes(:continent).where(continent: { slug: filter_params[:continent] }) - else - [] - end - - regions = if filter_params[:continent] && filter_params[:country] - Region.includes(:country).where(country: { slug: filter_params[:country] }) - else - [] - end - - cities = if filter_params[:continent] && filter_params[:country] && filter_params[:region] - City.includes(:region).where(region: { slug: filter_params[:region] }) - else - [] - end - - @options = { - continents: Fragment::OptionSerializer.many(Continent.all), - countries: Fragment::OptionSerializer.many(countries), - regions: Fragment::OptionSerializer.many(regions), - cities: Fragment::OptionSerializer.many(cities), - } + @filters = filter_params.slice(:name, :continent, :country, :region, :city).compact_blank + @options = CompanyOptionsSerializer.render(filter_params) end def show diff --git a/app/serializers/company_options_serializer.rb b/app/serializers/company_options_serializer.rb new file mode 100644 index 0000000..6984d16 --- /dev/null +++ b/app/serializers/company_options_serializer.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class CompanyOptionsSerializer < Oj::Serializer + object_as :filter_params + + has_many :continents, serializer: Fragment::OptionSerializer do + Continent.all + end + + has_many :countries, serializer: Fragment::OptionSerializer do + if filter_params[:continent] + Country.includes(:continent).where(continent: { slug: filter_params[:continent] }) + else + [] + end + end + + has_many :regions, serializer: Fragment::OptionSerializer do + if filter_params[:continent] && filter_params[:country] + Region.includes(:country).where(country: { slug: filter_params[:country] }) + else + [] + end + end + + has_many :cities, serializer: Fragment::OptionSerializer do + if filter_params[:continent] && filter_params[:country] && filter_params[:region] + City.includes(:region).where(region: { slug: filter_params[:region] }) + else + [] + end + end +end From d36d2d37ab6e51ec9a4fd6d6136ee0a2da9ff4fa Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Fri, 20 Sep 2024 18:24:07 +0200 Subject: [PATCH 15/18] Remove whitespace --- app/models/company.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/company.rb b/app/models/company.rb index 80d989a..d302937 100644 --- a/app/models/company.rb +++ b/app/models/company.rb @@ -18,7 +18,7 @@ class Company < ApplicationRecord validates :website, url: { no_local: true, public_suffix: true } validates :careers_page, url: { no_local: true, public_suffix: true }, allow_blank: true validates :slug, presence: true - + scope :with_technologies, -> { includes(:technologies) } scope :with_locations, -> { includes(:continent, country: :continent, region: :country, city: :region) } scope :with_all_associations, -> { with_technologies.with_locations } From d7638ec484fc0a7acbc32e0eac89e091f01a150b Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Fri, 20 Sep 2024 19:08:13 +0200 Subject: [PATCH 16/18] Improve seeds --- db/seeds.rb | 57 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index 70bdea4..968fe7e 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -7,22 +7,11 @@ class Seeder class << self def perform - destroy_data! seed_geo_data seed_technologies seed_companies end - def destroy_data! - CompanyTechnology.destroy_all - Technology.destroy_all - Company.destroy_all - City.destroy_all - Country.destroy_all - Region.destroy_all - Continent.destroy_all - end - def seed_geo_data africa = Continent.create!(name: "Africa") asia = Continent.create!(name: "Asia") @@ -66,12 +55,40 @@ def seed_geo_data sao_paulo = Region.create!(name: "Sao Paulo", country: brazil) sao_paulo_city = City.create!(name: "Sao Paulo City", region: sao_paulo) - @sample_cities = [nairobi, tokyo, london, new_york_city, sydney, sao_paulo_city] + @sample_cities = [ + nairobi, + tokyo, + london, + new_york_city, + sydney, + sao_paulo_city, + ] end def seed_technologies @ruby = Technology.create!(name: "Ruby", background_color: "#cc0000", text_color: "#ffffff") @rails = Technology.create!(name: "Ruby on Rails", background_color: "#cc0000", text_color: "#ffffff") + + # create 8 more technologies related to Ruby and Ruby on Rails + rspec = Technology.create!(name: "RSpec", background_color: "#cc0000", text_color: "#ffffff") + sidekiq = Technology.create!(name: "Sidekiq", background_color: "#cc0000", text_color: "#ffffff") + postgres = Technology.create!(name: "PostgreSQL", background_color: "#336791", text_color: "#ffffff") + redis = Technology.create!(name: "Redis", background_color: "#dc382d", text_color: "#ffffff") + react = Technology.create!(name: "React", background_color: "#61dafb", text_color: "#000000") + vuejs = Technology.create!(name: "Vue.js", background_color: "#4fc08d", text_color: "#ffffff") + graphql = Technology.create!(name: "GraphQL", background_color: "#e535ab", text_color: "#ffffff") + elasticsearch = Technology.create!(name: "Elasticsearch", background_color: "#005571", text_color: "#ffffff") + + @sample_technologies = [ + rspec, + sidekiq, + postgres, + redis, + react, + vuejs, + graphql, + elasticsearch, + ] end def seed_companies @@ -91,24 +108,18 @@ def seed_companies technologies: [@ruby, @rails], ) - create_records(Company, 48) do |i| - { + 48.times do |i| + Company.create!( name: "Company #{i}", slug: "company-#{i}", website: "https://ruby-companies.org", careers_page: "https://ruby-companies.org", description: "Welcome to the page of Company #{i}. We are a company that does things.", - city_id: @sample_cities.sample.id, - } + city: @sample_cities.sample, + technologies: [@ruby, @rails] + @sample_technologies.sample(2), + ) end end - - private - - def create_records(model, count, &block) - record_data = Array.new(count, &block) - model.insert_all!(record_data) - end end end From 9479572090c7e2c2e9faff0ecede9c70aca9e3d4 Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Fri, 20 Sep 2024 19:08:20 +0200 Subject: [PATCH 17/18] Improve controller specs --- spec/controllers/companies_controller_spec.rb | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/spec/controllers/companies_controller_spec.rb b/spec/controllers/companies_controller_spec.rb index f4262ee..64c7cc9 100644 --- a/spec/controllers/companies_controller_spec.rb +++ b/spec/controllers/companies_controller_spec.rb @@ -19,6 +19,52 @@ it "has a valid filter declaration" do expect(described_class.declarations_validator).to(be_valid) end + + it "applies filters" do + create(:company, name: "Ruby Corp") + create(:company, name: "Python Inc") + + get "/companies", params: { filter: { name: "Ruby" } } + + expect(inertia.props[:companies].length).to(eq(1)) + expect(inertia.props[:companies].first[:name]).to(eq("Ruby Corp")) + end + + it "sorts companies by name in ascending order by default" do + create(:company, name: "Zebra Tech") + create(:company, name: "Aardvark Solutions") + + get "/companies" + + expect(inertia.props[:companies].first[:name]).to(eq("Aardvark Solutions")) + expect(inertia.props[:companies].last[:name]).to(eq("Zebra Tech")) + end + + it "includes pagination in the response" do + create_list(:company, 5) + + get "/companies" + + expect(inertia.props[:pagination]).to(be_present) + end + + it "includes company options in the response" do + get "/companies" + + expect(inertia.props[:options]).to(be_present) + expect(inertia.props[:options]).to(have_key(:continents)) + expect(inertia.props[:options]).to(have_key(:countries)) + expect(inertia.props[:options]).to(have_key(:regions)) + expect(inertia.props[:options]).to(have_key(:cities)) + end + + it "assigns compact filters" do + get "/companies", params: { filter: { name: "Ruby", continent: "", country: "USA", region: nil, city: " " } } + + expect(inertia.props[:filters].keys.count).to(eq(2)) + expect(inertia.props[:filters][:name]).to(eq("Ruby")) + expect(inertia.props[:filters][:country]).to(eq("USA")) + end end describe "#show", inertia: true do @@ -30,5 +76,11 @@ expect_inertia.to(render_component("companies/show")) expect(inertia.props[:company]).to(be_serialized_one(company)) end + + it "returns a 404 for non-existent company" do + get "/companies/non-existent-slug" + + expect(response).to(have_http_status(:not_found)) + end end end From a239801b7de7b780a186f92a5c5f64b49b983709 Mon Sep 17 00:00:00 2001 From: Calvin Walzel Date: Fri, 20 Sep 2024 21:13:04 +0200 Subject: [PATCH 18/18] Use permit on filter_params --- app/controllers/companies_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/companies_controller.rb b/app/controllers/companies_controller.rb index b362e4e..1cdfae6 100644 --- a/app/controllers/companies_controller.rb +++ b/app/controllers/companies_controller.rb @@ -30,6 +30,6 @@ def show private def filter_params - params.fetch(:filter, {}) + params.fetch(:filter, {}).permit(:name, :continent, :country, :region, :city) end end