feat: add website add button and fuse search

This commit is contained in:
moonrailgun 2024-03-23 00:06:56 +08:00
parent 3f13447e1f
commit 68ace91321
13 changed files with 587 additions and 87 deletions

View File

@ -75,6 +75,9 @@ importers:
'@antv/larkmap': '@antv/larkmap':
specifier: ^1.4.13 specifier: ^1.4.13
version: 1.4.13(@antv/l7@2.20.14)(react-dom@18.2.0)(react@18.2.0) 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': '@i18next-toolkit/react':
specifier: ^1.0.6 specifier: ^1.0.6
version: 1.0.6(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) 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': '@radix-ui/react-icons':
specifier: ^1.3.0 specifier: ^1.3.0
version: 1.3.0(react@18.2.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': '@radix-ui/react-menubar':
specifier: ^1.0.4 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) 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: filesize:
specifier: ^10.0.12 specifier: ^10.0.12
version: 10.0.12 version: 10.0.12
fuse.js:
specifier: ^7.0.0
version: 7.0.0
leaflet: leaflet:
specifier: ^1.9.4 specifier: ^1.9.4
version: 1.9.4 version: 1.9.4
@ -207,6 +216,9 @@ importers:
react-grid-layout: react-grid-layout:
specifier: 1.4.2 specifier: 1.4.2
version: 1.4.2(react-dom@18.2.0)(react@18.2.0) 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: react-icons:
specifier: ^4.12.0 specifier: ^4.12.0
version: 4.12.0(react@18.2.0) version: 4.12.0(react@18.2.0)
@ -6251,6 +6263,14 @@ packages:
dependencies: dependencies:
'@hapi/hoek': 9.3.0 '@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: /@hutson/parse-repository-url@5.0.0:
resolution: {integrity: sha512-e5+YUKENATs1JgYHMzTr2MW/NDcXGfYFAuOQU8gJgF/kEh4EqKgfGrfLI67bMD4tbhZVlkigz/9YYwWcbOFthg==} resolution: {integrity: sha512-e5+YUKENATs1JgYHMzTr2MW/NDcXGfYFAuOQU8gJgF/kEh4EqKgfGrfLI67bMD4tbhZVlkigz/9YYwWcbOFthg==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@ -7402,6 +7422,27 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false 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): /@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==} resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==}
peerDependencies: peerDependencies:
@ -14953,6 +14994,11 @@ packages:
/functions-have-names@1.2.3: /functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} 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: /gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@ -22274,6 +22320,15 @@ packages:
react-fast-compare: 3.2.2 react-fast-compare: 3.2.2
shallowequal: 1.1.0 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): /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==} resolution: {integrity: sha512-5+bQSeEtgJrMBABBL5lO7jPdSNAbeAZ+MlFWDw//7FnVacuVu3l9EeWFzBQvZsKy+cihkbThWOAThEdH8YjGEw==}
peerDependencies: peerDependencies:

View File

@ -3,6 +3,9 @@ import { ScrollArea } from './ui/scroll-area';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
import { Badge } from './ui/badge'; import { Badge } from './ui/badge';
import { useNavigate, useRouterState } from '@tanstack/react-router'; 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 { export interface CommonListItem {
id: string; id: string;
@ -13,55 +16,96 @@ export interface CommonListItem {
} }
interface CommonListProps { interface CommonListProps {
hasSearch?: boolean;
items: CommonListItem[]; items: CommonListItem[];
} }
export const CommonList: React.FC<CommonListProps> = React.memo((props) => { export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
const { location } = useRouterState(); const { location } = useRouterState();
const navigate = useNavigate(); const navigate = useNavigate();
return ( const { searchText, setSearchText, searchResult } = useFuseSearch(
<ScrollArea className="h-screen"> props.items,
<div className="flex flex-col gap-2 p-4 pt-0"> {
{props.items.map((item) => { keys: [
const isSelected = item.href === location.pathname; {
name: 'id',
weight: 1,
},
{
name: 'title',
weight: 0.7,
},
{
name: 'tags',
weight: 0.3,
},
],
}
);
return ( const finalList = searchResult ?? props.items;
<button
key={item.id} return (
className={cn( <>
'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent', {props.hasSearch && (
isSelected && 'bg-muted' <div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
)} <form>
onClick={() => <div className="relative">
navigate({ <LuSearch className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
to: item.href, <Input
}) placeholder="Search"
} className="pl-8"
> value={searchText}
<div className="flex w-full flex-col gap-1"> onChange={(e) => setSearchText(e.target.value)}
<div className="flex items-center"> />
<div className="flex items-center gap-2"> </div>
<div className="font-semibold">{item.title}</div> </form>
</div>
)}
<ScrollArea className="h-screen">
<div className="flex flex-col gap-2 p-4 pt-0">
{finalList.map((item) => {
const isSelected = item.href === location.pathname;
return (
<button
key={item.id}
className={cn(
'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent',
isSelected && 'bg-muted'
)}
onClick={() =>
navigate({
to: item.href,
})
}
>
<div className="flex w-full flex-col gap-1">
<div className="flex items-center">
<div className="flex items-center gap-2">
<div className="font-semibold">{item.title}</div>
</div>
</div> </div>
</div> </div>
</div> <div className="line-clamp-2 text-xs text-muted-foreground">
<div className="line-clamp-2 text-xs text-muted-foreground"> {item.content}
{item.content}
</div>
{item.tags.length > 0 ? (
<div className="flex items-center gap-2">
{item.tags.map((tag) => (
<Badge key={tag} variant={getBadgeVariantFromLabel(tag)}>
{tag}
</Badge>
))}
</div> </div>
) : null} {item.tags.length > 0 ? (
</button> <div className="flex items-center gap-2">
); {item.tags.map((tag) => (
})} <Badge key={tag} variant={getBadgeVariantFromLabel(tag)}>
</div> {tag}
</ScrollArea> </Badge>
))}
</div>
) : null}
</button>
);
})}
</div>
</ScrollArea>
</>
); );
}); });
CommonList.displayName = 'CommonList'; CommonList.displayName = 'CommonList';

View File

@ -3,6 +3,8 @@ import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
import { Spinner } from './spinner';
import { IconType } from 'react-icons';
const buttonVariants = cva( 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', '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>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean; asChild?: boolean;
loading?: boolean;
Icon?: IconType;
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( 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 Comp = asChild ? Slot : 'button';
const icon = Icon ? <Icon className="mr-1" /> : undefined;
const children = (
<>
{loading ? <Spinner className="mr-1" /> : icon}
{props.children}
</>
);
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
ref={ref} ref={ref}
disabled={loading}
{...props} {...props}
children={children}
/> />
); );
} }

View 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,
}

View 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 }

View 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';

View 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';

View 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,
};
}

View File

@ -20,12 +20,14 @@
"@antv/l7": "^2.20.14", "@antv/l7": "^2.20.14",
"@antv/larkmap": "^1.4.13", "@antv/larkmap": "^1.4.13",
"@i18next-toolkit/react": "^1.0.6", "@i18next-toolkit/react": "^1.0.6",
"@hookform/resolvers": "^3.3.4",
"@loadable/component": "^5.16.3", "@loadable/component": "^5.16.3",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.4", "@radix-ui/react-menubar": "^1.0.4",
"@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
@ -53,6 +55,7 @@
"dayjs": "^1.11.9", "dayjs": "^1.11.9",
"eventemitter-strict": "^1.0.1", "eventemitter-strict": "^1.0.1",
"filesize": "^10.0.12", "filesize": "^10.0.12",
"fuse.js": "^7.0.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lucide-react": "^0.358.0", "lucide-react": "^0.358.0",
@ -63,6 +66,7 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-easy-sort": "^1.5.3", "react-easy-sort": "^1.5.3",
"react-grid-layout": "1.4.2", "react-grid-layout": "1.4.2",
"react-hook-form": "^7.51.1",
"react-icons": "^4.12.0", "react-icons": "^4.12.0",
"react-leaflet": "^4.2.1", "react-leaflet": "^4.2.1",
"react-resizable": "^3.0.5", "react-resizable": "^3.0.5",

View File

@ -1,15 +1,9 @@
import { routeAuthBeforeLoad } from '@/utils/route'; import { createFileRoute, redirect } from '@tanstack/react-router';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/')({ export const Route = createFileRoute('/')({
beforeLoad: routeAuthBeforeLoad, beforeLoad: () => {
component: Index, redirect({
to: '/website',
});
},
}); });
function Index() {
return (
<div className="p-2">
<h3>Welcome Home!</h3>
</div>
);
}

View File

@ -1,13 +1,19 @@
import { trpc } from '@/api/trpc'; import { trpc } from '@/api/trpc';
import { CommonList } from '@/components/CommonList'; import { CommonList } from '@/components/CommonList';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator'; 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 { LayoutV2 } from '@/pages/LayoutV2';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { createFileRoute } from '@tanstack/react-router'; import { useTranslation } from '@i18next-toolkit/react';
import { LuSearch } from 'react-icons/lu'; import {
createFileRoute,
getRouteApi,
useNavigate,
} from '@tanstack/react-router';
const routeApi = getRouteApi('/website/$websiteId');
export const Route = createFileRoute('/website')({ export const Route = createFileRoute('/website')({
beforeLoad: routeAuthBeforeLoad, beforeLoad: routeAuthBeforeLoad,
@ -16,9 +22,11 @@ export const Route = createFileRoute('/website')({
function WebsiteComponent() { function WebsiteComponent() {
const workspaceId = useCurrentWorkspaceId(); const workspaceId = useCurrentWorkspaceId();
const { t } = useTranslation();
const { data = [] } = trpc.website.all.useQuery({ const { data = [] } = trpc.website.all.useQuery({
workspaceId, workspaceId,
}); });
const navigate = useNavigate();
const items = data.map((item) => ({ const items = data.map((item) => ({
id: item.id, id: item.id,
@ -27,44 +35,37 @@ function WebsiteComponent() {
tags: [], tags: [],
href: `/website/${item.id}`, 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 ( return (
<LayoutV2 <LayoutV2
list={ list={
<Tabs defaultValue="all"> <div>
<div className="flex items-center px-4 py-2"> <div className="flex items-center px-4 py-2">
<h1 className="text-xl font-bold">Website</h1> <h1 className="text-xl font-bold">{t('Website')}</h1>
<TabsList className="ml-auto">
<TabsTrigger <div className="ml-auto">
value="all" <AddWebsiteBtn />
className="text-zinc-600 dark:text-zinc-200" </div>
>
All mail
</TabsTrigger>
<TabsTrigger
value="unread"
className="text-zinc-600 dark:text-zinc-200"
>
Unread
</TabsTrigger>
</TabsList>
</div> </div>
<Separator /> <Separator />
<div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<form> <CommonList hasSearch={true} items={items} />
<div className="relative"> </div>
<LuSearch className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input placeholder="Search" className="pl-8" />
</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
View 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();
}

View File

@ -77,6 +77,9 @@ export const websiteRouter = router({
where: { where: {
workspaceId, workspaceId,
}, },
orderBy: {
updatedAt: 'desc',
},
}); });
return websites; return websites;