feat: add telemetry route
This commit is contained in:
parent
3c60261f37
commit
f27f3f2f11
@ -87,9 +87,15 @@ importers:
|
|||||||
'@monaco-editor/react':
|
'@monaco-editor/react':
|
||||||
specifier: ^4.6.0
|
specifier: ^4.6.0
|
||||||
version: 4.6.0(monaco-editor@0.46.0)(react-dom@18.2.0)(react@18.2.0)
|
version: 4.6.0(monaco-editor@0.46.0)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-alert-dialog':
|
||||||
|
specifier: ^1.0.5
|
||||||
|
version: 1.0.5(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-avatar':
|
'@radix-ui/react-avatar':
|
||||||
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)
|
||||||
|
'@radix-ui/react-collapsible':
|
||||||
|
specifier: ^1.0.3
|
||||||
|
version: 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-dialog':
|
'@radix-ui/react-dialog':
|
||||||
specifier: ^1.0.5
|
specifier: ^1.0.5
|
||||||
version: 1.0.5(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.5(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -7165,6 +7171,32 @@ packages:
|
|||||||
'@babel/runtime': 7.24.0
|
'@babel/runtime': 7.24.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-alert-dialog@1.0.5(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==}
|
||||||
|
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/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.21)(react@18.2.0)
|
||||||
|
'@radix-ui/react-context': 1.0.1(@types/react@18.2.21)(react@18.2.0)
|
||||||
|
'@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.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)
|
||||||
|
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.21)(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-arrow@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==}
|
resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -7210,6 +7242,34 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==}
|
||||||
|
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/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.21)(react@18.2.0)
|
||||||
|
'@radix-ui/react-context': 1.0.1(@types/react@18.2.21)(react@18.2.0)
|
||||||
|
'@radix-ui/react-id': 1.0.1(@types/react@18.2.21)(react@18.2.0)
|
||||||
|
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.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)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.21)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.21)(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-collection@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
|
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { TipIcon } from './TipIcon';
|
||||||
|
|
||||||
interface CommonHeaderProps {
|
interface CommonHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
desc?: React.ReactNode;
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
}
|
}
|
||||||
export const CommonHeader: React.FC<CommonHeaderProps> = React.memo((props) => {
|
export const CommonHeader: React.FC<CommonHeaderProps> = React.memo((props) => {
|
||||||
@ -9,6 +11,8 @@ export const CommonHeader: React.FC<CommonHeaderProps> = React.memo((props) => {
|
|||||||
<>
|
<>
|
||||||
<h1 className="text-xl font-bold">{props.title}</h1>
|
<h1 className="text-xl font-bold">{props.title}</h1>
|
||||||
|
|
||||||
|
{props.desc && <TipIcon className="ml-1" content={props.desc} />}
|
||||||
|
|
||||||
{props.actions && <div className="ml-auto">{props.actions}</div>}
|
{props.actions && <div className="ml-auto">{props.actions}</div>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -11,7 +11,7 @@ export interface CommonListItem {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
content?: React.ReactNode;
|
content?: React.ReactNode;
|
||||||
tags: string[];
|
tags?: string[];
|
||||||
href: string;
|
href: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
|
|||||||
<div className="text-muted-foreground line-clamp-2 text-xs">
|
<div className="text-muted-foreground line-clamp-2 text-xs">
|
||||||
{item.content}
|
{item.content}
|
||||||
</div>
|
</div>
|
||||||
{item.tags.length > 0 ? (
|
{Array.isArray(item.tags) && item.tags.length > 0 ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{item.tags.map((tag) => (
|
{item.tags.map((tag) => (
|
||||||
<Badge key={tag} variant={getBadgeVariantFromLabel(tag)}>
|
<Badge key={tag} variant={getBadgeVariantFromLabel(tag)}>
|
||||||
|
@ -8,9 +8,11 @@ export const CommonWrapper: React.FC<CommonWrapperProps> = React.memo(
|
|||||||
(props) => {
|
(props) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex h-[52px] items-center px-4 py-2">
|
{props.header && (
|
||||||
{props.header}
|
<div className="flex h-[52px] items-center px-4 py-2">
|
||||||
</div>
|
{props.header}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
|
22
src/client/components/TipIcon.tsx
Normal file
22
src/client/components/TipIcon.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||||
|
import { LuHelpCircle } from 'react-icons/lu';
|
||||||
|
|
||||||
|
interface TipIconProps {
|
||||||
|
className?: string;
|
||||||
|
content: React.ReactNode;
|
||||||
|
}
|
||||||
|
export const TipIcon: React.FC<TipIconProps> = React.memo((props) => {
|
||||||
|
const { className, content } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<LuHelpCircle className={className} />
|
||||||
|
</TooltipTrigger>
|
||||||
|
|
||||||
|
<TooltipContent className="max-w-xl">{content}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
TipIcon.displayName = 'TipIcon';
|
139
src/client/components/ui/alert-dialog.tsx
Normal file
139
src/client/components/ui/alert-dialog.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||||
|
|
||||||
|
import { cn } from "@/utils/style"
|
||||||
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
const AlertDialog = AlertDialogPrimitive.Root
|
||||||
|
|
||||||
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||||
|
|
||||||
|
const AlertDialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
))
|
||||||
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const AlertDialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||||
|
|
||||||
|
const AlertDialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||||
|
|
||||||
|
const AlertDialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const AlertDialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogDescription.displayName =
|
||||||
|
AlertDialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
const AlertDialogAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(buttonVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||||
|
|
||||||
|
const AlertDialogCancel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"mt-2 sm:mt-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
}
|
42
src/client/components/ui/collapsible.tsx
Normal file
42
src/client/components/ui/collapsible.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { cn } from '@/utils/style';
|
||||||
|
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||||
|
import React from 'react';
|
||||||
|
import { LuChevronRight } from 'react-icons/lu';
|
||||||
|
|
||||||
|
const Collapsible = CollapsiblePrimitive.Root;
|
||||||
|
|
||||||
|
const CollapsibleTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CollapsiblePrimitive.CollapsibleTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.CollapsibleTrigger>
|
||||||
|
>(({ asChild, className, ...props }, ref) => {
|
||||||
|
let children = props.children;
|
||||||
|
if (typeof props.children === 'string') {
|
||||||
|
children = (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<LuChevronRight
|
||||||
|
className={cn(
|
||||||
|
'mr-1 transition-transform group-data-[state=open]/collapsible:rotate-90'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="font-semibold">{props.children}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleTrigger
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={cn('group/collapsible', className)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CollapsiblePrimitive.CollapsibleTrigger>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
CollapsibleTrigger.displayName =
|
||||||
|
CollapsiblePrimitive.CollapsibleTrigger.displayName;
|
||||||
|
|
||||||
|
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||||
|
|
||||||
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
@ -7,6 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 10000",
|
"dev": "vite --port 10000",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"ui:add": "shadcn-ui add",
|
||||||
"translation:extract": "i18next-toolkit extract",
|
"translation:extract": "i18next-toolkit extract",
|
||||||
"translation:scan": "i18next-toolkit scan",
|
"translation:scan": "i18next-toolkit scan",
|
||||||
"translation:translate": "i18next-toolkit translate",
|
"translation:translate": "i18next-toolkit translate",
|
||||||
@ -23,7 +24,9 @@
|
|||||||
"@hookform/resolvers": "^3.3.4",
|
"@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-alert-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
|
"@radix-ui/react-collapsible": "^1.0.3",
|
||||||
"@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",
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
import { Route as WebsiteImport } from './routes/website'
|
import { Route as WebsiteImport } from './routes/website'
|
||||||
|
import { Route as TelemetryImport } from './routes/telemetry'
|
||||||
import { Route as RegisterImport } from './routes/register'
|
import { Route as RegisterImport } from './routes/register'
|
||||||
import { Route as MonitorImport } from './routes/monitor'
|
import { Route as MonitorImport } from './routes/monitor'
|
||||||
import { Route as LoginImport } from './routes/login'
|
import { Route as LoginImport } from './routes/login'
|
||||||
@ -19,6 +20,8 @@ import { Route as DashboardImport } from './routes/dashboard'
|
|||||||
import { Route as IndexImport } from './routes/index'
|
import { Route as IndexImport } from './routes/index'
|
||||||
import { Route as WebsiteAddImport } from './routes/website/add'
|
import { Route as WebsiteAddImport } from './routes/website/add'
|
||||||
import { Route as WebsiteWebsiteIdImport } from './routes/website/$websiteId'
|
import { Route as WebsiteWebsiteIdImport } from './routes/website/$websiteId'
|
||||||
|
import { Route as TelemetryAddImport } from './routes/telemetry/add'
|
||||||
|
import { Route as TelemetryTelemetryIdImport } from './routes/telemetry/$telemetryId'
|
||||||
import { Route as MonitorAddImport } from './routes/monitor/add'
|
import { Route as MonitorAddImport } from './routes/monitor/add'
|
||||||
import { Route as MonitorMonitorIdImport } from './routes/monitor/$monitorId'
|
import { Route as MonitorMonitorIdImport } from './routes/monitor/$monitorId'
|
||||||
|
|
||||||
@ -29,6 +32,11 @@ const WebsiteRoute = WebsiteImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const TelemetryRoute = TelemetryImport.update({
|
||||||
|
path: '/telemetry',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const RegisterRoute = RegisterImport.update({
|
const RegisterRoute = RegisterImport.update({
|
||||||
path: '/register',
|
path: '/register',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@ -64,6 +72,16 @@ const WebsiteWebsiteIdRoute = WebsiteWebsiteIdImport.update({
|
|||||||
getParentRoute: () => WebsiteRoute,
|
getParentRoute: () => WebsiteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const TelemetryAddRoute = TelemetryAddImport.update({
|
||||||
|
path: '/add',
|
||||||
|
getParentRoute: () => TelemetryRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const TelemetryTelemetryIdRoute = TelemetryTelemetryIdImport.update({
|
||||||
|
path: '/$telemetryId',
|
||||||
|
getParentRoute: () => TelemetryRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const MonitorAddRoute = MonitorAddImport.update({
|
const MonitorAddRoute = MonitorAddImport.update({
|
||||||
path: '/add',
|
path: '/add',
|
||||||
getParentRoute: () => MonitorRoute,
|
getParentRoute: () => MonitorRoute,
|
||||||
@ -98,6 +116,10 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof RegisterImport
|
preLoaderRoute: typeof RegisterImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/telemetry': {
|
||||||
|
preLoaderRoute: typeof TelemetryImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/website': {
|
'/website': {
|
||||||
preLoaderRoute: typeof WebsiteImport
|
preLoaderRoute: typeof WebsiteImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
@ -110,6 +132,14 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof MonitorAddImport
|
preLoaderRoute: typeof MonitorAddImport
|
||||||
parentRoute: typeof MonitorImport
|
parentRoute: typeof MonitorImport
|
||||||
}
|
}
|
||||||
|
'/telemetry/$telemetryId': {
|
||||||
|
preLoaderRoute: typeof TelemetryTelemetryIdImport
|
||||||
|
parentRoute: typeof TelemetryImport
|
||||||
|
}
|
||||||
|
'/telemetry/add': {
|
||||||
|
preLoaderRoute: typeof TelemetryAddImport
|
||||||
|
parentRoute: typeof TelemetryImport
|
||||||
|
}
|
||||||
'/website/$websiteId': {
|
'/website/$websiteId': {
|
||||||
preLoaderRoute: typeof WebsiteWebsiteIdImport
|
preLoaderRoute: typeof WebsiteWebsiteIdImport
|
||||||
parentRoute: typeof WebsiteImport
|
parentRoute: typeof WebsiteImport
|
||||||
@ -129,6 +159,7 @@ export const routeTree = rootRoute.addChildren([
|
|||||||
LoginRoute,
|
LoginRoute,
|
||||||
MonitorRoute.addChildren([MonitorMonitorIdRoute, MonitorAddRoute]),
|
MonitorRoute.addChildren([MonitorMonitorIdRoute, MonitorAddRoute]),
|
||||||
RegisterRoute,
|
RegisterRoute,
|
||||||
|
TelemetryRoute.addChildren([TelemetryTelemetryIdRoute, TelemetryAddRoute]),
|
||||||
WebsiteRoute.addChildren([WebsiteWebsiteIdRoute, WebsiteAddRoute]),
|
WebsiteRoute.addChildren([WebsiteWebsiteIdRoute, WebsiteAddRoute]),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
115
src/client/routes/telemetry.tsx
Normal file
115
src/client/routes/telemetry.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { trpc } from '@/api/trpc';
|
||||||
|
import { CommonHeader } from '@/components/CommonHeader';
|
||||||
|
import { CommonList } from '@/components/CommonList';
|
||||||
|
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useDataReady } from '@/hooks/useDataReady';
|
||||||
|
import { useEvent } from '@/hooks/useEvent';
|
||||||
|
import { LayoutV2 } from '@/pages/LayoutV2';
|
||||||
|
import { useCurrentWorkspaceId } from '@/store/user';
|
||||||
|
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||||
|
import { Trans, useTranslation } from '@i18next-toolkit/react';
|
||||||
|
import {
|
||||||
|
createFileRoute,
|
||||||
|
useNavigate,
|
||||||
|
useRouterState,
|
||||||
|
} from '@tanstack/react-router';
|
||||||
|
import { LuPlus } from 'react-icons/lu';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/telemetry')({
|
||||||
|
beforeLoad: routeAuthBeforeLoad,
|
||||||
|
component: TelemetryComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function TelemetryComponent() {
|
||||||
|
const workspaceId = useCurrentWorkspaceId();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { data = [] } = trpc.telemetry.all.useQuery({
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const pathname = useRouterState({
|
||||||
|
select: (state) => state.location.pathname,
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = data.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
title: item.name,
|
||||||
|
href: `/telemetry/${item.id}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
useDataReady(
|
||||||
|
() => data.length > 0,
|
||||||
|
() => {
|
||||||
|
if (pathname === Route.fullPath) {
|
||||||
|
navigate({
|
||||||
|
to: '/telemetry/$telemetryId',
|
||||||
|
params: {
|
||||||
|
telemetryId: data[0].id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickAdd = useEvent(() => {
|
||||||
|
navigate({
|
||||||
|
to: '/telemetry/add',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LayoutV2
|
||||||
|
list={
|
||||||
|
<CommonWrapper
|
||||||
|
header={
|
||||||
|
<CommonHeader
|
||||||
|
title={t('Telemetry')}
|
||||||
|
desc={
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
Telemetry is a technology that reports access data even on
|
||||||
|
pages that are not under your control. As long as the
|
||||||
|
other website allows the insertion of third-party images
|
||||||
|
(e.g., forums, blogs, and various rich-text editors), then
|
||||||
|
the data can be collected and used to analyze the images
|
||||||
|
when they are loaded by the user.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
Generally, we will use a one-pixel blank image so that it
|
||||||
|
will not affect the user's normal use.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
At the same time, we can also use it in some client-side
|
||||||
|
application scenarios, such as collecting the frequency of
|
||||||
|
cli usage, such as collecting the installation of
|
||||||
|
selfhosted apps, and so on.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
Icon={LuPlus}
|
||||||
|
onClick={handleClickAdd}
|
||||||
|
>
|
||||||
|
{t('Add')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CommonList hasSearch={true} items={items} />
|
||||||
|
</CommonWrapper>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
170
src/client/routes/telemetry/$telemetryId.tsx
Normal file
170
src/client/routes/telemetry/$telemetryId.tsx
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import { trpc } from '@/api/trpc';
|
||||||
|
import { CommonHeader } from '@/components/CommonHeader';
|
||||||
|
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||||
|
import { TelemetryMetricsTable } from '@/components/telemetry/TelemetryMetricsTable';
|
||||||
|
import { TelemetryOverview } from '@/components/telemetry/TelemetryOverview';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
|
||||||
|
import { useGlobalRangeDate } from '@/hooks/useGlobalRangeDate';
|
||||||
|
import { useCurrentWorkspaceId } from '@/store/user';
|
||||||
|
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||||
|
import { Trans, useTranslation } from '@i18next-toolkit/react';
|
||||||
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
import { Card, Typography } from 'antd';
|
||||||
|
import { LuChevronRight, LuCode2 } from 'react-icons/lu';
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@/components/ui/collapsible';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/telemetry/$telemetryId')({
|
||||||
|
beforeLoad: routeAuthBeforeLoad,
|
||||||
|
component: TelemetryDetailComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function TelemetryDetailComponent() {
|
||||||
|
const { telemetryId } = Route.useParams<{ telemetryId: string }>();
|
||||||
|
const workspaceId = useCurrentWorkspaceId();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { startDate, endDate } = useGlobalRangeDate();
|
||||||
|
const { data: info } = trpc.telemetry.info.useQuery({
|
||||||
|
workspaceId,
|
||||||
|
telemetryId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const startAt = startDate.valueOf();
|
||||||
|
const endAt = endDate.valueOf();
|
||||||
|
|
||||||
|
const blankGif = `${window.location.origin}/telemetry/${workspaceId}/${telemetryId}.gif`;
|
||||||
|
const countBadgeUrl = `${window.location.origin}/telemetry/${workspaceId}/${telemetryId}/badge.svg`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommonWrapper
|
||||||
|
header={
|
||||||
|
<CommonHeader
|
||||||
|
title={info?.name ?? ''}
|
||||||
|
actions={
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="outline" Icon={LuCode2}>
|
||||||
|
{t('Usage')}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Usage</AlertDialogTitle>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Here is some way to use telemetry:</p>
|
||||||
|
<Typography.Title level={3}>
|
||||||
|
Insert to article:
|
||||||
|
</Typography.Title>
|
||||||
|
<p>
|
||||||
|
if your article support raw html, you can direct insert it{' '}
|
||||||
|
<Typography.Text code={true} copyable={{ text: blankGif }}>
|
||||||
|
{blankGif}
|
||||||
|
</Typography.Text>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Collapsible>
|
||||||
|
<CollapsibleTrigger>Advanced</CollapsibleTrigger>
|
||||||
|
|
||||||
|
<CollapsibleContent>
|
||||||
|
<div className="pl-5">
|
||||||
|
<p>
|
||||||
|
Some website will not allow send `referer` field. so
|
||||||
|
its maybe can not track source. so you can mark it by
|
||||||
|
yourself. for example:
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Typography.Text code={true}>
|
||||||
|
{blankGif}?url=https://xxxxxxxx
|
||||||
|
</Typography.Text>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
|
||||||
|
<Typography.Title level={3}>
|
||||||
|
Count your website visitor:
|
||||||
|
</Typography.Title>
|
||||||
|
<p>
|
||||||
|
if your article support raw html, you can direct insert it{' '}
|
||||||
|
<Typography.Text
|
||||||
|
code={true}
|
||||||
|
copyable={{ text: countBadgeUrl }}
|
||||||
|
>
|
||||||
|
{countBadgeUrl}
|
||||||
|
</Typography.Text>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Like this: <img src={countBadgeUrl} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogAction>{t('Get!')}</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ScrollArea className="h-full overflow-hidden p-4">
|
||||||
|
<ScrollBar orientation="horizontal" />
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Card.Grid hoverable={false} className="!w-full">
|
||||||
|
<TelemetryOverview
|
||||||
|
telemetryId={telemetryId}
|
||||||
|
showDateFilter={true}
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
/>
|
||||||
|
</Card.Grid>
|
||||||
|
|
||||||
|
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/3">
|
||||||
|
<TelemetryMetricsTable
|
||||||
|
telemetryId={telemetryId}
|
||||||
|
type="source"
|
||||||
|
title={[t('Source'), t('Views')]}
|
||||||
|
startAt={startAt}
|
||||||
|
endAt={endAt}
|
||||||
|
/>
|
||||||
|
</Card.Grid>
|
||||||
|
|
||||||
|
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/3">
|
||||||
|
<TelemetryMetricsTable
|
||||||
|
telemetryId={telemetryId}
|
||||||
|
type="event"
|
||||||
|
title={[t('Events'), t('Views')]}
|
||||||
|
startAt={startAt}
|
||||||
|
endAt={endAt}
|
||||||
|
/>
|
||||||
|
</Card.Grid>
|
||||||
|
|
||||||
|
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/3">
|
||||||
|
<TelemetryMetricsTable
|
||||||
|
telemetryId={telemetryId}
|
||||||
|
type="country"
|
||||||
|
title={[t('Countries'), t('Visitors')]}
|
||||||
|
startAt={startAt}
|
||||||
|
endAt={endAt}
|
||||||
|
/>
|
||||||
|
</Card.Grid>
|
||||||
|
</Card>
|
||||||
|
</ScrollArea>
|
||||||
|
</CommonWrapper>
|
||||||
|
);
|
||||||
|
}
|
101
src/client/routes/telemetry/add.tsx
Normal file
101
src/client/routes/telemetry/add.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
|
import { useTranslation } from '@i18next-toolkit/react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useEvent } from '@/hooks/useEvent';
|
||||||
|
import { useCurrentWorkspaceId } from '@/store/user';
|
||||||
|
import { trpc } from '@/api/trpc';
|
||||||
|
import { Card, CardContent, CardFooter } from '@/components/ui/card';
|
||||||
|
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||||
|
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/telemetry/add')({
|
||||||
|
beforeLoad: routeAuthBeforeLoad,
|
||||||
|
component: TelemetryAddComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
const addFormSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
function TelemetryAddComponent() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const workspaceId = useCurrentWorkspaceId();
|
||||||
|
const addTelemetryMutation = trpc.telemetry.upsert.useMutation();
|
||||||
|
const utils = trpc.useUtils();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof addFormSchema>>({
|
||||||
|
resolver: zodResolver(addFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = useEvent(async (values: z.infer<typeof addFormSchema>) => {
|
||||||
|
const res = await addTelemetryMutation.mutateAsync({
|
||||||
|
workspaceId,
|
||||||
|
name: values.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
utils.telemetry.all.refetch();
|
||||||
|
form.reset();
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: '/telemetry/$telemetryId',
|
||||||
|
params: {
|
||||||
|
telemetryId: res.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommonWrapper
|
||||||
|
header={<h1 className="text-xl font-bold">{t('Add Website')}</h1>}
|
||||||
|
>
|
||||||
|
<div className="p-4">
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('Telemetry Name')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t('Telemetry Name to Display')}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button type="submit" loading={addTelemetryMutation.isLoading}>
|
||||||
|
{t('Create')}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</CommonWrapper>
|
||||||
|
);
|
||||||
|
}
|
@ -36,7 +36,6 @@ function WebsiteComponent() {
|
|||||||
id: item.id,
|
id: item.id,
|
||||||
title: item.name,
|
title: item.name,
|
||||||
content: item.domain,
|
content: item.domain,
|
||||||
tags: [],
|
|
||||||
href: `/website/${item.id}`,
|
href: `/website/${item.id}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
import { useState } from 'react';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { t, useTranslation } from '@i18next-toolkit/react';
|
import { useTranslation } from '@i18next-toolkit/react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
|
Loading…
Reference in New Issue
Block a user