diff --git a/src/client/components/website/WebsiteLighthouseBtn.tsx b/src/client/components/website/WebsiteLighthouseBtn.tsx new file mode 100644 index 0000000..219a3bf --- /dev/null +++ b/src/client/components/website/WebsiteLighthouseBtn.tsx @@ -0,0 +1,178 @@ +import React, { useMemo, useState } from 'react'; +import { Button } from '../ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '../ui/dialog'; +import { useTranslation } from '@i18next-toolkit/react'; +import { TbBuildingLighthouse } from 'react-icons/tb'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from '../ui/sheet'; +import { defaultErrorHandler, defaultSuccessHandler, trpc } from '@/api/trpc'; +import { useCurrentWorkspaceId } from '@/store/user'; +import { formatDate } from '@/utils/date'; +import { Input } from '../ui/input'; +import { toast } from 'sonner'; +import { useEvent } from '@/hooks/useEvent'; +import { Badge } from '../ui/badge'; +import { LuArrowRight, LuPlus } from 'react-icons/lu'; + +interface WebsiteLighthouseBtnProps { + websiteId: string; +} +export const WebsiteLighthouseBtn: React.FC = + React.memo((props) => { + const workspaceId = useCurrentWorkspaceId(); + const { t } = useTranslation(); + const [url, setUrl] = useState(''); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + + const { data, hasNextPage, fetchNextPage, isFetchingNextPage, refetch } = + trpc.website.getLighthouseReport.useInfiniteQuery( + { + workspaceId, + websiteId: props.websiteId, + }, + { + getNextPageParam: (lastPage) => lastPage.nextCursor, + } + ); + const createMutation = trpc.website.generateLighthouseReport.useMutation({ + onSuccess: defaultSuccessHandler, + onError: defaultErrorHandler, + }); + + const allData = useMemo(() => { + if (!data) { + return []; + } + + return [...data.pages.flatMap((p) => p.items)]; + }, [data]); + + const handleGenerateReport = useEvent(async () => { + if (!url) { + toast.error(t('Url is required')); + return; + } + + await createMutation.mutateAsync({ + workspaceId, + websiteId: props.websiteId, + url, + }); + setIsCreateDialogOpen(false); + refetch(); + }); + + return ( + + + + + + + {t('Generate Lighthouse Report')} + + {t('Its will take a while to generate the report.')} + + + +
+ setUrl(e.target.value)} + placeholder="https://google.com" + /> +
+ + + + +
+ + + +
+ {allData.map((report) => { + return ( +
+ {report.status} + +
+
{report.url}
+
+ {formatDate(report.createdAt)} +
+
+ + {report.status === 'Success' && ( +
+ ); + })} +
+ + {hasNextPage && ( + + )} + + +
+ ); + }); +WebsiteLighthouseBtn.displayName = 'WebsiteLighthouseBtn'; diff --git a/src/client/routes/website/$websiteId/index.tsx b/src/client/routes/website/$websiteId/index.tsx index 5768519..9bc72fd 100644 --- a/src/client/routes/website/$websiteId/index.tsx +++ b/src/client/routes/website/$websiteId/index.tsx @@ -7,6 +7,7 @@ import { NotFoundTip } from '@/components/NotFoundTip'; import { Button } from '@/components/ui/button'; import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'; import { WebsiteCodeBtn } from '@/components/website/WebsiteCodeBtn'; +import { WebsiteLighthouseBtn } from '@/components/website/WebsiteLighthouseBtn'; import { WebsiteMetricsTable } from '@/components/website/WebsiteMetricsTable'; import { WebsiteOverview } from '@/components/website/WebsiteOverview'; import { WebsiteVisitorMapBtn } from '@/components/website/WebsiteVisitorMapBtn'; @@ -70,6 +71,9 @@ function WebsiteDetailComponent() { > + + + } diff --git a/src/client/vite.config.ts b/src/client/vite.config.ts index 11c3432..98fa180 100644 --- a/src/client/vite.config.ts +++ b/src/client/vite.config.ts @@ -34,6 +34,9 @@ export default defineConfig({ '/trpc': { target: 'http://localhost:12345', }, + '/lh': { + target: 'http://localhost:12345', + }, '/api/auth/': { target: 'http://localhost:12345', }, diff --git a/src/server/trpc/routers/survey.ts b/src/server/trpc/routers/survey.ts index 167fff8..8fe1f9f 100644 --- a/src/server/trpc/routers/survey.ts +++ b/src/server/trpc/routers/survey.ts @@ -337,8 +337,7 @@ export const surveyRouter = router({ ) .output(buildCursorResponseSchema(SurveyResultModelSchema)) .query(async ({ input }) => { - const limit = input.limit; - const { cursor, surveyId } = input; + const { cursor, surveyId, limit } = input; const where: Prisma.SurveyResultWhereInput = { surveyId, diff --git a/src/server/trpc/routers/website.ts b/src/server/trpc/routers/website.ts index 234af49..3e08237 100644 --- a/src/server/trpc/routers/website.ts +++ b/src/server/trpc/routers/website.ts @@ -32,11 +32,11 @@ import { websiteStatsSchema, } from '../../model/_schema/filter.js'; import dayjs from 'dayjs'; -import { WebsiteQueryFilters } from '../../utils/prisma.js'; +import { fetchDataByCursor, WebsiteQueryFilters } from '../../utils/prisma.js'; import { WebsiteLighthouseReportStatus } from '@prisma/client'; import { generateLighthouse } from '../../utils/screenshot/lighthouse.js'; import { WebsiteLighthouseReportModelSchema } from '../../prisma/zod/websitelighthousereport.js'; -import { method } from 'lodash-es'; +import { buildCursorResponseSchema } from '../../utils/schema.js'; const websiteNameSchema = z.string().max(100); const websiteDomainSchema = z.union([ @@ -644,36 +644,44 @@ export const websiteRouter = router({ .input( z.object({ websiteId: z.string().cuid2(), + limit: z.number().min(1).max(100).default(10), + cursor: z.string().optional(), }) ) .output( - z.array( + buildCursorResponseSchema( WebsiteLighthouseReportModelSchema.pick({ id: true, status: true, + url: true, createdAt: true, }) ) ) .query(async ({ input }) => { - const { websiteId } = input; + const { websiteId, limit, cursor } = input; - const list = await prisma.websiteLighthouseReport.findMany({ - where: { - websiteId, - }, - take: 10, - orderBy: { - createdAt: 'desc', - }, - select: { - id: true, - status: true, - createdAt: true, - }, - }); + const { items, nextCursor } = await fetchDataByCursor( + prisma.websiteLighthouseReport, + { + where: { + websiteId, + }, + select: { + id: true, + status: true, + url: true, + createdAt: true, + }, + limit, + cursor, + } + ); - return list; + return { + items, + nextCursor, + }; }), getLighthouseJSON: publicProcedure .meta({ diff --git a/src/server/utils/prisma.ts b/src/server/utils/prisma.ts index 174434c..89fd540 100644 --- a/src/server/utils/prisma.ts +++ b/src/server/utils/prisma.ts @@ -226,6 +226,12 @@ type ExtractFindManyWhereType< }, > = NonNullable[0]>['where']; +type ExtractFindManySelectType< + T extends { + findMany: (args?: any) => Prisma.PrismaPromise; + }, +> = NonNullable[0]>['select']; + /** * @example * const { items, nextCursor } = await fetchDataByCursor( @@ -249,16 +255,25 @@ export async function fetchDataByCursor< options: { // where: Record; where: ExtractFindManyWhereType; + select?: ExtractFindManySelectType; limit: number; cursor: CursorType; cursorName?: string; order?: 'asc' | 'desc'; } ) { - const { where, limit, cursor, cursorName = 'id', order = 'desc' } = options; + const { + where, + limit, + cursor, + select, + cursorName = 'id', + order = 'desc', + } = options; const items: ExtractFindManyReturnType = await fetchModel.findMany({ where, + select, take: limit + 1, cursor: cursor ? {