From 68ace913217bb033537f41a32c224e1fcaf65566 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sat, 23 Mar 2024 00:06:56 +0800 Subject: [PATCH] feat: add website add button and fuse search --- pnpm-lock.yaml | 55 ++++++ src/client/components/CommonList.tsx | 122 ++++++++---- src/client/components/ui/button.tsx | 28 ++- src/client/components/ui/form.tsx | 176 ++++++++++++++++++ src/client/components/ui/label.tsx | 24 +++ src/client/components/ui/spinner.tsx | 12 ++ .../components/website/AddWebsiteBtn.tsx | 121 ++++++++++++ src/client/hooks/useFuseSearch.ts | 27 +++ src/client/package.json | 4 + src/client/routes/index.tsx | 18 +- src/client/routes/website.tsx | 71 +++---- src/client/utils/dom.ts | 13 ++ src/server/trpc/routers/website.ts | 3 + 13 files changed, 587 insertions(+), 87 deletions(-) create mode 100644 src/client/components/ui/form.tsx create mode 100644 src/client/components/ui/label.tsx create mode 100644 src/client/components/ui/spinner.tsx create mode 100644 src/client/components/website/AddWebsiteBtn.tsx create mode 100644 src/client/hooks/useFuseSearch.ts create mode 100644 src/client/utils/dom.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb2041d..e16ef90 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,6 +75,9 @@ importers: '@antv/larkmap': specifier: ^1.4.13 version: 1.4.13(@antv/l7@2.20.14)(react-dom@18.2.0)(react@18.2.0) + '@hookform/resolvers': + specifier: ^3.3.4 + version: 3.3.4(react-hook-form@7.51.1) '@i18next-toolkit/react': specifier: ^1.0.6 version: 1.0.6(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) @@ -96,6 +99,9 @@ importers: '@radix-ui/react-icons': specifier: ^1.3.0 version: 1.3.0(react@18.2.0) + '@radix-ui/react-label': + specifier: ^2.0.2 + version: 2.0.2(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-menubar': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) @@ -177,6 +183,9 @@ importers: filesize: specifier: ^10.0.12 version: 10.0.12 + fuse.js: + specifier: ^7.0.0 + version: 7.0.0 leaflet: specifier: ^1.9.4 version: 1.9.4 @@ -207,6 +216,9 @@ importers: react-grid-layout: specifier: 1.4.2 version: 1.4.2(react-dom@18.2.0)(react@18.2.0) + react-hook-form: + specifier: ^7.51.1 + version: 7.51.1(react@18.2.0) react-icons: specifier: ^4.12.0 version: 4.12.0(react@18.2.0) @@ -6251,6 +6263,14 @@ packages: dependencies: '@hapi/hoek': 9.3.0 + /@hookform/resolvers@3.3.4(react-hook-form@7.51.1): + resolution: {integrity: sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==} + peerDependencies: + react-hook-form: ^7.0.0 + dependencies: + react-hook-form: 7.51.1(react@18.2.0) + dev: false + /@hutson/parse-repository-url@5.0.0: resolution: {integrity: sha512-e5+YUKENATs1JgYHMzTr2MW/NDcXGfYFAuOQU8gJgF/kEh4EqKgfGrfLI67bMD4tbhZVlkigz/9YYwWcbOFthg==} engines: {node: '>=10.13.0'} @@ -7402,6 +7422,27 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-label@2.0.2(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==} + 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 + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-menu@2.0.6(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==} peerDependencies: @@ -14953,6 +14994,11 @@ packages: /functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + /fuse.js@7.0.0: + resolution: {integrity: sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==} + engines: {node: '>=10'} + dev: false + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -22274,6 +22320,15 @@ packages: react-fast-compare: 3.2.2 shallowequal: 1.1.0 + /react-hook-form@7.51.1(react@18.2.0): + resolution: {integrity: sha512-ifnBjl+kW0ksINHd+8C/Gp6a4eZOdWyvRv0UBaByShwU8JbVx5hTcTWEcd5VdybvmPTATkVVXk9npXArHmo56w==} + engines: {node: '>=12.22.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + react: 18.2.0 + dev: false + /react-i18next@14.0.5(i18next@23.10.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-5+bQSeEtgJrMBABBL5lO7jPdSNAbeAZ+MlFWDw//7FnVacuVu3l9EeWFzBQvZsKy+cihkbThWOAThEdH8YjGEw==} peerDependencies: diff --git a/src/client/components/CommonList.tsx b/src/client/components/CommonList.tsx index 8033123..38bd795 100644 --- a/src/client/components/CommonList.tsx +++ b/src/client/components/CommonList.tsx @@ -3,6 +3,9 @@ import { ScrollArea } from './ui/scroll-area'; import { cn } from '@/utils/style'; import { Badge } from './ui/badge'; import { useNavigate, useRouterState } from '@tanstack/react-router'; +import { LuSearch } from 'react-icons/lu'; +import { Input } from './ui/input'; +import { useFuseSearch } from '@/hooks/useFuseSearch'; export interface CommonListItem { id: string; @@ -13,55 +16,96 @@ export interface CommonListItem { } interface CommonListProps { + hasSearch?: boolean; items: CommonListItem[]; } export const CommonList: React.FC = React.memo((props) => { const { location } = useRouterState(); const navigate = useNavigate(); - return ( - -
- {props.items.map((item) => { - const isSelected = item.href === location.pathname; + const { searchText, setSearchText, searchResult } = useFuseSearch( + props.items, + { + keys: [ + { + name: 'id', + weight: 1, + }, + { + name: 'title', + weight: 0.7, + }, + { + name: 'tags', + weight: 0.3, + }, + ], + } + ); - return ( -
-
- {item.content} -
- {item.tags.length > 0 ? ( -
- {item.tags.map((tag) => ( - - {tag} - - ))} +
+ {item.content}
- ) : null} - - ); - })} -
-
+ {item.tags.length > 0 ? ( +
+ {item.tags.map((tag) => ( + + {tag} + + ))} +
+ ) : null} + + ); + })} + + + ); }); CommonList.displayName = 'CommonList'; diff --git a/src/client/components/ui/button.tsx b/src/client/components/ui/button.tsx index c4c225e..af8386f 100644 --- a/src/client/components/ui/button.tsx +++ b/src/client/components/ui/button.tsx @@ -3,6 +3,8 @@ import { Slot } from '@radix-ui/react-slot'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/utils/style'; +import { Spinner } from './spinner'; +import { IconType } from 'react-icons'; const buttonVariants = cva( 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-zinc-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-zinc-300', @@ -39,16 +41,40 @@ export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; + loading?: boolean; + Icon?: IconType; } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { + ( + { + className, + variant, + size, + asChild = false, + loading = false, + Icon, + ...props + }, + ref + ) => { const Comp = asChild ? Slot : 'button'; + + const icon = Icon ? : undefined; + const children = ( + <> + {loading ? : icon} + {props.children} + + ); + return ( ); } diff --git a/src/client/components/ui/form.tsx b/src/client/components/ui/form.tsx new file mode 100644 index 0000000..b0986d1 --- /dev/null +++ b/src/client/components/ui/form.tsx @@ -0,0 +1,176 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/utils/style" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +