Skip to content

Commit d29785a

Browse files
committed
feat: add lighthouse reporter generate in website
1 parent fb75a8b commit d29785a

File tree

6 files changed

+229
-22
lines changed

6 files changed

+229
-22
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import React, { useMemo, useState } from 'react';
2+
import { Button } from '../ui/button';
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogDescription,
7+
DialogFooter,
8+
DialogHeader,
9+
DialogTitle,
10+
DialogTrigger,
11+
} from '../ui/dialog';
12+
import { useTranslation } from '@i18next-toolkit/react';
13+
import { TbBuildingLighthouse } from 'react-icons/tb';
14+
import {
15+
Sheet,
16+
SheetContent,
17+
SheetDescription,
18+
SheetHeader,
19+
SheetTitle,
20+
SheetTrigger,
21+
} from '../ui/sheet';
22+
import { defaultErrorHandler, defaultSuccessHandler, trpc } from '@/api/trpc';
23+
import { useCurrentWorkspaceId } from '@/store/user';
24+
import { formatDate } from '@/utils/date';
25+
import { Input } from '../ui/input';
26+
import { toast } from 'sonner';
27+
import { useEvent } from '@/hooks/useEvent';
28+
import { Badge } from '../ui/badge';
29+
import { LuArrowRight, LuPlus } from 'react-icons/lu';
30+
31+
interface WebsiteLighthouseBtnProps {
32+
websiteId: string;
33+
}
34+
export const WebsiteLighthouseBtn: React.FC<WebsiteLighthouseBtnProps> =
35+
React.memo((props) => {
36+
const workspaceId = useCurrentWorkspaceId();
37+
const { t } = useTranslation();
38+
const [url, setUrl] = useState('');
39+
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
40+
41+
const { data, hasNextPage, fetchNextPage, isFetchingNextPage, refetch } =
42+
trpc.website.getLighthouseReport.useInfiniteQuery(
43+
{
44+
workspaceId,
45+
websiteId: props.websiteId,
46+
},
47+
{
48+
getNextPageParam: (lastPage) => lastPage.nextCursor,
49+
}
50+
);
51+
const createMutation = trpc.website.generateLighthouseReport.useMutation({
52+
onSuccess: defaultSuccessHandler,
53+
onError: defaultErrorHandler,
54+
});
55+
56+
const allData = useMemo(() => {
57+
if (!data) {
58+
return [];
59+
}
60+
61+
return [...data.pages.flatMap((p) => p.items)];
62+
}, [data]);
63+
64+
const handleGenerateReport = useEvent(async () => {
65+
if (!url) {
66+
toast.error(t('Url is required'));
67+
return;
68+
}
69+
70+
await createMutation.mutateAsync({
71+
workspaceId,
72+
websiteId: props.websiteId,
73+
url,
74+
});
75+
setIsCreateDialogOpen(false);
76+
refetch();
77+
});
78+
79+
return (
80+
<Sheet>
81+
<SheetTrigger>
82+
<Button variant="outline" size="icon" Icon={TbBuildingLighthouse} />
83+
</SheetTrigger>
84+
<SheetContent>
85+
<SheetHeader>
86+
<SheetTitle>{t('Website Lighthouse Reports')}</SheetTitle>
87+
<SheetDescription>
88+
{t(
89+
'Lighthouse is an open-source, automated tool developed by Google, designed to evaluate the quality of web applications.'
90+
)}
91+
</SheetDescription>
92+
</SheetHeader>
93+
94+
<div className="mt-2 flex flex-col gap-2">
95+
<div>
96+
<Dialog
97+
open={isCreateDialogOpen}
98+
onOpenChange={setIsCreateDialogOpen}
99+
>
100+
<DialogTrigger>
101+
<Button
102+
variant="outline"
103+
loading={createMutation.isLoading}
104+
Icon={LuPlus}
105+
>
106+
{t('Create Report')}
107+
</Button>
108+
</DialogTrigger>
109+
<DialogContent>
110+
<DialogHeader>
111+
<DialogTitle>{t('Generate Lighthouse Report')}</DialogTitle>
112+
<DialogDescription>
113+
{t('Its will take a while to generate the report.')}
114+
</DialogDescription>
115+
</DialogHeader>
116+
117+
<div>
118+
<Input
119+
value={url}
120+
onChange={(e) => setUrl(e.target.value)}
121+
placeholder="https://google.com"
122+
/>
123+
</div>
124+
125+
<DialogFooter>
126+
<Button
127+
loading={createMutation.isLoading}
128+
onClick={handleGenerateReport}
129+
>
130+
{t('Create')}
131+
</Button>
132+
</DialogFooter>
133+
</DialogContent>
134+
</Dialog>
135+
</div>
136+
137+
<div className="flex flex-col gap-2">
138+
{allData.map((report) => {
139+
return (
140+
<div className="border-border flex items-start gap-2 rounded-lg border p-2">
141+
<Badge>{report.status}</Badge>
142+
143+
<div className="flex-1 overflow-hidden">
144+
<div className="text-base">{report.url}</div>
145+
<div className="text-sm opacity-50">
146+
{formatDate(report.createdAt)}
147+
</div>
148+
</div>
149+
150+
{report.status === 'Success' && (
151+
<Button
152+
variant="outline"
153+
size="icon"
154+
Icon={LuArrowRight}
155+
onClick={() => window.open(`/lh/${report.id}/html`)}
156+
/>
157+
)}
158+
</div>
159+
);
160+
})}
161+
</div>
162+
163+
{hasNextPage && (
164+
<Button
165+
variant="outline"
166+
size="icon"
167+
loading={isFetchingNextPage}
168+
onClick={() => fetchNextPage()}
169+
>
170+
{t('Load More')}
171+
</Button>
172+
)}
173+
</div>
174+
</SheetContent>
175+
</Sheet>
176+
);
177+
});
178+
WebsiteLighthouseBtn.displayName = 'WebsiteLighthouseBtn';

src/client/routes/website/$websiteId/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { NotFoundTip } from '@/components/NotFoundTip';
77
import { Button } from '@/components/ui/button';
88
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
99
import { WebsiteCodeBtn } from '@/components/website/WebsiteCodeBtn';
10+
import { WebsiteLighthouseBtn } from '@/components/website/WebsiteLighthouseBtn';
1011
import { WebsiteMetricsTable } from '@/components/website/WebsiteMetricsTable';
1112
import { WebsiteOverview } from '@/components/website/WebsiteOverview';
1213
import { WebsiteVisitorMapBtn } from '@/components/website/WebsiteVisitorMapBtn';
@@ -70,6 +71,9 @@ function WebsiteDetailComponent() {
7071
>
7172
<LuSettings />
7273
</Button>
74+
75+
<WebsiteLighthouseBtn websiteId={website.id} />
76+
7377
<WebsiteCodeBtn websiteId={website.id} />
7478
</div>
7579
}

src/client/vite.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ export default defineConfig({
3434
'/trpc': {
3535
target: 'http://localhost:12345',
3636
},
37+
'/lh': {
38+
target: 'http://localhost:12345',
39+
},
3740
'/api/auth/': {
3841
target: 'http://localhost:12345',
3942
},

src/server/trpc/routers/survey.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,8 +337,7 @@ export const surveyRouter = router({
337337
)
338338
.output(buildCursorResponseSchema(SurveyResultModelSchema))
339339
.query(async ({ input }) => {
340-
const limit = input.limit;
341-
const { cursor, surveyId } = input;
340+
const { cursor, surveyId, limit } = input;
342341

343342
const where: Prisma.SurveyResultWhereInput = {
344343
surveyId,

src/server/trpc/routers/website.ts

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ import {
3232
websiteStatsSchema,
3333
} from '../../model/_schema/filter.js';
3434
import dayjs from 'dayjs';
35-
import { WebsiteQueryFilters } from '../../utils/prisma.js';
35+
import { fetchDataByCursor, WebsiteQueryFilters } from '../../utils/prisma.js';
3636
import { WebsiteLighthouseReportStatus } from '@prisma/client';
3737
import { generateLighthouse } from '../../utils/screenshot/lighthouse.js';
3838
import { WebsiteLighthouseReportModelSchema } from '../../prisma/zod/websitelighthousereport.js';
39-
import { method } from 'lodash-es';
39+
import { buildCursorResponseSchema } from '../../utils/schema.js';
4040

4141
const websiteNameSchema = z.string().max(100);
4242
const websiteDomainSchema = z.union([
@@ -644,36 +644,44 @@ export const websiteRouter = router({
644644
.input(
645645
z.object({
646646
websiteId: z.string().cuid2(),
647+
limit: z.number().min(1).max(100).default(10),
648+
cursor: z.string().optional(),
647649
})
648650
)
649651
.output(
650-
z.array(
652+
buildCursorResponseSchema(
651653
WebsiteLighthouseReportModelSchema.pick({
652654
id: true,
653655
status: true,
656+
url: true,
654657
createdAt: true,
655658
})
656659
)
657660
)
658661
.query(async ({ input }) => {
659-
const { websiteId } = input;
662+
const { websiteId, limit, cursor } = input;
660663

661-
const list = await prisma.websiteLighthouseReport.findMany({
662-
where: {
663-
websiteId,
664-
},
665-
take: 10,
666-
orderBy: {
667-
createdAt: 'desc',
668-
},
669-
select: {
670-
id: true,
671-
status: true,
672-
createdAt: true,
673-
},
674-
});
664+
const { items, nextCursor } = await fetchDataByCursor(
665+
prisma.websiteLighthouseReport,
666+
{
667+
where: {
668+
websiteId,
669+
},
670+
select: {
671+
id: true,
672+
status: true,
673+
url: true,
674+
createdAt: true,
675+
},
676+
limit,
677+
cursor,
678+
}
679+
);
675680

676-
return list;
681+
return {
682+
items,
683+
nextCursor,
684+
};
677685
}),
678686
getLighthouseJSON: publicProcedure
679687
.meta({

src/server/utils/prisma.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,12 @@ type ExtractFindManyWhereType<
226226
},
227227
> = NonNullable<Parameters<T['findMany']>[0]>['where'];
228228

229+
type ExtractFindManySelectType<
230+
T extends {
231+
findMany: (args?: any) => Prisma.PrismaPromise<any>;
232+
},
233+
> = NonNullable<Parameters<T['findMany']>[0]>['select'];
234+
229235
/**
230236
* @example
231237
* const { items, nextCursor } = await fetchDataByCursor(
@@ -249,16 +255,25 @@ export async function fetchDataByCursor<
249255
options: {
250256
// where: Record<string, any>;
251257
where: ExtractFindManyWhereType<Model>;
258+
select?: ExtractFindManySelectType<Model>;
252259
limit: number;
253260
cursor: CursorType;
254261
cursorName?: string;
255262
order?: 'asc' | 'desc';
256263
}
257264
) {
258-
const { where, limit, cursor, cursorName = 'id', order = 'desc' } = options;
265+
const {
266+
where,
267+
limit,
268+
cursor,
269+
select,
270+
cursorName = 'id',
271+
order = 'desc',
272+
} = options;
259273
const items: ExtractFindManyReturnType<Model['findMany']> =
260274
await fetchModel.findMany({
261275
where,
276+
select,
262277
take: limit + 1,
263278
cursor: cursor
264279
? {

0 commit comments

Comments
 (0)