feat: add website add button and fuse search
This commit is contained in:
parent
3f13447e1f
commit
68ace91321
@ -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:
|
||||
|
@ -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,16 +16,56 @@ export interface CommonListItem {
|
||||
}
|
||||
|
||||
interface CommonListProps {
|
||||
hasSearch?: boolean;
|
||||
items: CommonListItem[];
|
||||
}
|
||||
export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
|
||||
const { location } = useRouterState();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { searchText, setSearchText, searchResult } = useFuseSearch(
|
||||
props.items,
|
||||
{
|
||||
keys: [
|
||||
{
|
||||
name: 'id',
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
weight: 0.7,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
weight: 0.3,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
const finalList = searchResult ?? props.items;
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.hasSearch && (
|
||||
<div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<form>
|
||||
<div className="relative">
|
||||
<LuSearch className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search"
|
||||
className="pl-8"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ScrollArea className="h-screen">
|
||||
<div className="flex flex-col gap-2 p-4 pt-0">
|
||||
{props.items.map((item) => {
|
||||
{finalList.map((item) => {
|
||||
const isSelected = item.href === location.pathname;
|
||||
|
||||
return (
|
||||
@ -62,6 +105,7 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</>
|
||||
);
|
||||
});
|
||||
CommonList.displayName = 'CommonList';
|
||||
|
@ -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<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
loading?: boolean;
|
||||
Icon?: IconType;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ 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 ? <Icon className="mr-1" /> : undefined;
|
||||
const children = (
|
||||
<>
|
||||
{loading ? <Spinner className="mr-1" /> : icon}
|
||||
{props.children}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
disabled={loading}
|
||||
{...props}
|
||||
children={children}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
176
src/client/components/ui/form.tsx
Normal file
176
src/client/components/ui/form.tsx
Normal file
@ -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<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
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 <FormField>")
|
||||
}
|
||||
|
||||
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<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
24
src/client/components/ui/label.tsx
Normal file
24
src/client/components/ui/label.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/utils/style"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
12
src/client/components/ui/spinner.tsx
Normal file
12
src/client/components/ui/spinner.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { cn } from '@/utils/style';
|
||||
import React from 'react';
|
||||
import { IconBaseProps } from 'react-icons';
|
||||
import { LuLoader } from 'react-icons/lu';
|
||||
|
||||
interface SpinnerProps extends IconBaseProps {}
|
||||
export const Spinner: React.FC<SpinnerProps> = React.memo((props) => {
|
||||
return (
|
||||
<LuLoader {...props} className={cn('animate-spin', props.className)} />
|
||||
);
|
||||
});
|
||||
Spinner.displayName = 'Spinner';
|
121
src/client/components/website/AddWebsiteBtn.tsx
Normal file
121
src/client/components/website/AddWebsiteBtn.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import React, { useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { hostnameRegex } from '@tianji/shared';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { LuPlus } from 'react-icons/lu';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { useEvent, useEventWithLoading } from '@/hooks/useEvent';
|
||||
import { Input } from '../ui/input';
|
||||
import { preventDefault } from '@/utils/dom';
|
||||
import { trpc } from '@/api/trpc';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
|
||||
const addFormSchema = z.object({
|
||||
name: z.string(),
|
||||
domain: z.union([z.string().ip(), z.string().regex(hostnameRegex)]),
|
||||
});
|
||||
|
||||
export const AddWebsiteBtn: React.FC = React.memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const addWebsiteMutation = trpc.website.add.useMutation();
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const form = useForm<z.infer<typeof addFormSchema>>({
|
||||
resolver: zodResolver(addFormSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
domain: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = useEvent(async (values: z.infer<typeof addFormSchema>) => {
|
||||
await addWebsiteMutation.mutateAsync({
|
||||
workspaceId,
|
||||
name: values.name,
|
||||
domain: values.domain,
|
||||
});
|
||||
|
||||
utils.website.all.refetch();
|
||||
form.reset();
|
||||
|
||||
setOpen(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild={false}>
|
||||
<Button variant="outline" Icon={LuPlus}>
|
||||
{t('Add')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent onPointerDownOutside={preventDefault}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('Add Website')}</DialogTitle>
|
||||
<DialogDescription>{t('Add new website')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Website Name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('Website Name to Display')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="domain"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Domain')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" loading={addWebsiteMutation.isLoading}>
|
||||
{t('Submit')}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
AddWebsiteBtn.displayName = 'AddWebsiteBtn';
|
27
src/client/hooks/useFuseSearch.ts
Normal file
27
src/client/hooks/useFuseSearch.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import Fuse, { IFuseOptions } from 'fuse.js';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
export function useFuseSearch<T>(
|
||||
source: T[],
|
||||
options: IFuseOptions<T>
|
||||
): {
|
||||
searchText: string;
|
||||
setSearchText: (text: string) => void;
|
||||
searchResult: T[] | undefined;
|
||||
} {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const fuse = useMemo(() => new Fuse(source, options), [source, options]);
|
||||
|
||||
const searchResult = useMemo(
|
||||
() =>
|
||||
searchText ? fuse.search(searchText).map((item) => item.item) : undefined,
|
||||
[fuse, searchText]
|
||||
);
|
||||
|
||||
return {
|
||||
searchText,
|
||||
setSearchText,
|
||||
searchResult,
|
||||
};
|
||||
}
|
@ -20,12 +20,14 @@
|
||||
"@antv/l7": "^2.20.14",
|
||||
"@antv/larkmap": "^1.4.13",
|
||||
"@i18next-toolkit/react": "^1.0.6",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@loadable/component": "^5.16.3",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-menubar": "^1.0.4",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
@ -53,6 +55,7 @@
|
||||
"dayjs": "^1.11.9",
|
||||
"eventemitter-strict": "^1.0.1",
|
||||
"filesize": "^10.0.12",
|
||||
"fuse.js": "^7.0.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.358.0",
|
||||
@ -63,6 +66,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-easy-sort": "^1.5.3",
|
||||
"react-grid-layout": "1.4.2",
|
||||
"react-hook-form": "^7.51.1",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-resizable": "^3.0.5",
|
||||
|
@ -1,15 +1,9 @@
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: Index,
|
||||
beforeLoad: () => {
|
||||
redirect({
|
||||
to: '/website',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function Index() {
|
||||
return (
|
||||
<div className="p-2">
|
||||
<h3>Welcome Home!</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { trpc } from '@/api/trpc';
|
||||
import { CommonList } from '@/components/CommonList';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { AddWebsiteBtn } from '@/components/website/AddWebsiteBtn';
|
||||
import { useDataReady } from '@/hooks/useDataReady';
|
||||
import { LayoutV2 } from '@/pages/LayoutV2';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { LuSearch } from 'react-icons/lu';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import {
|
||||
createFileRoute,
|
||||
getRouteApi,
|
||||
useNavigate,
|
||||
} from '@tanstack/react-router';
|
||||
|
||||
const routeApi = getRouteApi('/website/$websiteId');
|
||||
|
||||
export const Route = createFileRoute('/website')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
@ -16,9 +22,11 @@ export const Route = createFileRoute('/website')({
|
||||
|
||||
function WebsiteComponent() {
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const { t } = useTranslation();
|
||||
const { data = [] } = trpc.website.all.useQuery({
|
||||
workspaceId,
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
const items = data.map((item) => ({
|
||||
id: item.id,
|
||||
@ -27,44 +35,37 @@ function WebsiteComponent() {
|
||||
tags: [],
|
||||
href: `/website/${item.id}`,
|
||||
}));
|
||||
const params = routeApi.useParams<{ websiteId: string }>();
|
||||
|
||||
useDataReady(
|
||||
() => data.length > 0,
|
||||
() => {
|
||||
if (!params.websiteId && data[0]) {
|
||||
navigate({
|
||||
to: '/website/$websiteId',
|
||||
params: {
|
||||
websiteId: data[0].id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<LayoutV2
|
||||
list={
|
||||
<Tabs defaultValue="all">
|
||||
<div>
|
||||
<div className="flex items-center px-4 py-2">
|
||||
<h1 className="text-xl font-bold">Website</h1>
|
||||
<TabsList className="ml-auto">
|
||||
<TabsTrigger
|
||||
value="all"
|
||||
className="text-zinc-600 dark:text-zinc-200"
|
||||
>
|
||||
All mail
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="unread"
|
||||
className="text-zinc-600 dark:text-zinc-200"
|
||||
>
|
||||
Unread
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<h1 className="text-xl font-bold">{t('Website')}</h1>
|
||||
|
||||
<div className="ml-auto">
|
||||
<AddWebsiteBtn />
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<form>
|
||||
<div className="relative">
|
||||
<LuSearch className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input placeholder="Search" className="pl-8" />
|
||||
|
||||
<CommonList hasSearch={true} items={items} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<TabsContent value="all" className="m-0">
|
||||
<CommonList items={items} />
|
||||
</TabsContent>
|
||||
<TabsContent value="unread" className="m-0">
|
||||
<CommonList items={items} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
13
src/client/utils/dom.ts
Normal file
13
src/client/utils/dom.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* A shortcut for executing directly in the component: stopPropagation
|
||||
*/
|
||||
export function stopPropagation(e: Event) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* A shortcut for executing directly in the component: preventDefault
|
||||
*/
|
||||
export function preventDefault(e: Event) {
|
||||
e.preventDefault();
|
||||
}
|
@ -77,6 +77,9 @@ export const websiteRouter = router({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
return websites;
|
||||
|
Loading…
Reference in New Issue
Block a user