Skip to content

Commit b36bac4

Browse files
feat: Add Next.js best practices improvements (#104)
* feat: Add Next.js best practices improvements - Add not-found.tsx for (dashboard) and (public) route groups - Add generateMetadata for public slug pages (SEO) - Fix Zod v4 .errors -> .issues migration in slug-actions.ts - Fix Zod v4 .errors -> .issues migration in welcome-stepper-action.ts Partial work on #56 * ci: retrigger E2E tests * perf: cache expensive queries in generateMetadata Add React.cache() wrappers for resolveSlug, resolveOrgSlug, getFeedbackLinkBySlug, and getUser to dedupe database calls between generateMetadata and page render. This should fix E2E test timeouts caused by double-fetching.
1 parent cd0dad8 commit b36bac4

4 files changed

Lines changed: 147 additions & 3 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Link from "next/link";
2+
import { Button } from "@propsto/ui/atoms/button";
3+
import {
4+
Card,
5+
CardContent,
6+
CardFooter,
7+
CardHeader,
8+
CardTitle,
9+
} from "@propsto/ui/atoms/card";
10+
import { FileQuestion } from "lucide-react";
11+
12+
export default function DashboardNotFound(): React.ReactNode {
13+
return (
14+
<div className="flex flex-1 items-center justify-center p-4">
15+
<Card className="w-full max-w-md">
16+
<CardHeader className="text-center">
17+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
18+
<FileQuestion className="h-6 w-6 text-muted-foreground" />
19+
</div>
20+
<CardTitle>Page not found</CardTitle>
21+
</CardHeader>
22+
<CardContent className="text-center text-muted-foreground">
23+
<p>
24+
The page you&apos;re looking for doesn&apos;t exist or has been
25+
moved.
26+
</p>
27+
</CardContent>
28+
<CardFooter className="flex justify-center">
29+
<Button asChild>
30+
<Link href="/">Go to Dashboard</Link>
31+
</Button>
32+
</CardFooter>
33+
</Card>
34+
</div>
35+
);
36+
}

apps/app/src/app/(public)/[...slug]/page.tsx

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
1+
import type { Metadata } from "next";
2+
import { cache } from "react";
13
import { notFound } from "next/navigation";
24
import Link from "next/link";
35
import { CheckCircle, Users } from "lucide-react";
4-
import { resolveSlug, resolveOrgSlug } from "@propsto/data/repos";
56
import {
6-
getFeedbackLinkBySlug,
7+
resolveSlug as _resolveSlug,
8+
resolveOrgSlug as _resolveOrgSlug,
9+
getFeedbackLinkBySlug as _getFeedbackLinkBySlug,
710
getGroupFeedbackLinkBySlug,
811
getUserFeedbackLinks,
12+
getUser as _getUser,
913
getGroupForPublicPage,
1014
} from "@propsto/data/repos";
15+
16+
// Cache expensive queries to dedupe between generateMetadata and page render
17+
const resolveSlug = cache(_resolveSlug);
18+
const resolveOrgSlug = cache(_resolveOrgSlug);
19+
const getFeedbackLinkBySlug = cache(_getFeedbackLinkBySlug);
20+
const getUser = cache(_getUser);
1121
import { Button } from "@propsto/ui/atoms/button";
1222
import {
1323
Card,
@@ -24,6 +34,83 @@ interface PublicPageProps {
2434
params: Promise<{ slug: string[] }>;
2535
}
2636

37+
export async function generateMetadata({
38+
params,
39+
}: PublicPageProps): Promise<Metadata> {
40+
const { slug } = await params;
41+
42+
if (!slug || slug.length === 0) {
43+
return { title: "Not Found" };
44+
}
45+
46+
// Thank you page
47+
if (slug[slug.length - 1] === "thanks") {
48+
return {
49+
title: "Thank You | props.to",
50+
description: "Your feedback has been submitted successfully.",
51+
};
52+
}
53+
54+
// Profile page: /<username>
55+
if (slug.length === 1) {
56+
const slugResult = await resolveSlug(slug[0]);
57+
if (slugResult.success && slugResult.data?.type === "user") {
58+
const userResult = await getUser({ id: slugResult.data.userId });
59+
if (userResult.success && userResult.data) {
60+
const name = userResult.data.firstName ?? slug[0];
61+
return {
62+
title: `${name} | props.to`,
63+
description: `Give feedback to ${name} on props.to`,
64+
};
65+
}
66+
}
67+
return { title: `${slug[0]} | props.to` };
68+
}
69+
70+
// Feedback link: /<username>/<linkSlug> or /<orgSlug>/<username>
71+
if (slug.length === 2) {
72+
const slugResult = await resolveSlug(slug[0]);
73+
if (slugResult.success && slugResult.data?.type === "user") {
74+
const linkResult = await getFeedbackLinkBySlug(
75+
slug[1],
76+
slugResult.data.userId,
77+
null,
78+
);
79+
if (linkResult.success && linkResult.data) {
80+
const name = linkResult.data.user.firstName ?? slug[0];
81+
return {
82+
title: `${linkResult.data.name} - ${name} | props.to`,
83+
description: `Give feedback to ${name}: ${linkResult.data.name}`,
84+
};
85+
}
86+
}
87+
return { title: `Feedback | props.to` };
88+
}
89+
90+
// Org feedback link: /<orgSlug>/<username>/<linkSlug>
91+
if (slug.length === 3) {
92+
const orgUserResult = await resolveOrgSlug(slug[0], slug[1]);
93+
if (orgUserResult.success && orgUserResult.data?.type === "user") {
94+
const linkResult = await getFeedbackLinkBySlug(
95+
slug[2],
96+
orgUserResult.data.userId,
97+
orgUserResult.data.organizationId,
98+
);
99+
if (linkResult.success && linkResult.data) {
100+
const name = linkResult.data.user.firstName ?? slug[1];
101+
const orgName = linkResult.data.organization?.name ?? slug[0];
102+
return {
103+
title: `${linkResult.data.name} - ${name} | ${orgName}`,
104+
description: `Give feedback to ${name} via ${orgName}`,
105+
};
106+
}
107+
}
108+
return { title: `Feedback | props.to` };
109+
}
110+
111+
return { title: "props.to" };
112+
}
113+
27114
/**
28115
* Dynamic route handler for public feedback pages:
29116
* - /<username> - User profile page
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Link from "next/link";
2+
import { Button } from "@propsto/ui/atoms/button";
3+
import { FileQuestion } from "lucide-react";
4+
5+
export default function PublicNotFound(): React.ReactNode {
6+
return (
7+
<div className="flex min-h-[60vh] flex-col items-center justify-center p-4 text-center">
8+
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-muted">
9+
<FileQuestion className="h-8 w-8 text-muted-foreground" />
10+
</div>
11+
<h1 className="mb-2 text-2xl font-bold tracking-tight">Page not found</h1>
12+
<p className="mb-6 max-w-md text-muted-foreground">
13+
The profile or feedback link you&apos;re looking for doesn&apos;t exist
14+
or may have been removed.
15+
</p>
16+
<Button asChild>
17+
<Link href="https://props.to">Go to props.to</Link>
18+
</Button>
19+
</div>
20+
);
21+
}

apps/auth/src/server/welcome-stepper-action.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,7 @@ export async function sendPersonalEmailCodeHandler(
787787
if (!parsed.success) {
788788
return {
789789
success: false,
790-
error: parsed.error.errors[0]?.message ?? "Invalid email",
790+
error: parsed.error.issues[0]?.message ?? "Invalid email",
791791
};
792792
}
793793

0 commit comments

Comments
 (0)