Skip to content

Commit

Permalink
Fix internal error 500 when navigating to dashboarad after signout (#33)
Browse files Browse the repository at this point in the history
* add key to map child

* page level route protection to prevent internal error from trpc

* verification warning for non-verified users

* max 2 posts on free tier
  • Loading branch information
iamtouha authored Feb 21, 2024
1 parent da8bf0b commit 95ac1e0
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 97 deletions.
26 changes: 14 additions & 12 deletions src/app/(landing)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ const features = [
},
{
name: "Authentication",
description:
"Credential authentication with password reset and email validation",
description: "Credential authentication with password reset and email validation",
logo: LuciaAuth,
},
{
Expand Down Expand Up @@ -78,20 +77,19 @@ const features = [
const HomePage = () => {
return (
<>
<section className="mx-auto grid min-h-[calc(100vh-300px)] max-w-5xl flex-col justify-center gap-4 py-10 md:py-12 text-center items-center">
<section className="mx-auto grid min-h-[calc(100vh-300px)] max-w-5xl flex-col items-center justify-center gap-4 py-10 text-center md:py-12">
<div className="p-4">
<div className="mb-10 flex items-center justify-center gap-3">
<NextjsIcon className="h-[52px] w-[52px]" />
<PlusIcon className="h-8 w-8" />
<LuciaAuth className="h-14 w-14" />
</div>
<h1 className="text-balance text-center bg-gradient-to-tr from-black/70 via-black to-black/60 dark:from-zinc-400/10 dark:via-white/90 dark:to-white/20 bg-clip-text text-transparent text-3xl font-bold sm:text-5xl md:text-6xl lg:text-7xl">
<h1 className="text-balance bg-gradient-to-tr from-black/70 via-black to-black/60 bg-clip-text text-center text-3xl font-bold text-transparent dark:from-zinc-400/10 dark:via-white/90 dark:to-white/20 sm:text-5xl md:text-6xl lg:text-7xl">
Next.js Lucia Auth Starter Template
</h1>
<p className="text-balance mb-10 mt-4 text-center text-muted-foreground md:text-lg lg:text-xl">
A Next.js Authentication starter template (password reset, email
validation and oAuth). Includes Lucia, Drizzle, tRPC, Stripe,
tailwindcss, shadcn-ui and react-email.
A Next.js Authentication starter template (password reset, email validation and oAuth).
Includes Lucia, Drizzle, tRPC, Stripe, tailwindcss, shadcn-ui and react-email.
</p>
<div className="mb-10">
<div className="mx-auto max-w-[430px]">
Expand All @@ -117,13 +115,17 @@ const HomePage = () => {
<a id="features"></a> Features
</h1>
<p className="text-balance mb-10 text-center text-muted-foreground md:text-lg lg:text-xl">
This starter template is a guide to help you get started with
Next.js for large scale applications. Feel free to add or remove
features to suit your needs.
This starter template is a guide to help you get started with Next.js for large scale
applications. Feel free to add or remove features to suit your needs.
</p>
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3">
{features.map((feature) => (
<CardSpotlight name={feature.name} description={feature.description} logo={<feature.logo className='w-12 h-12' />} />
{features.map((feature, i) => (
<CardSpotlight
key={i}
name={feature.name}
description={feature.description}
logo={<feature.logo className="h-12 w-12" />}
/>
))}
</div>
</div>
Expand Down
20 changes: 7 additions & 13 deletions src/app/(main)/_components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
import Link from "next/link";
import { RocketIcon } from "@/components/icons";
import { APP_TITLE } from "@/lib/constants";
import { type User } from "lucia";
import { UserDropdown } from "@/app/(main)/_components/user-dropdown";
import { validateRequest } from "@/lib/auth/validate-request";

export const Header = async () => {
const { user } = await validateRequest();

export const Header = ({ user }: { user: User }) => {
return (
<header className="sticky top-0 border-b bg-background/80 p-0">
<header className="sticky top-0 z-10 border-b bg-background/80 p-0">
<div className="container flex items-center gap-2 px-2 py-2 lg:px-4">
<Link
className="flex items-center justify-center text-xl font-medium"
href="/"
>
<Link className="flex items-center justify-center text-xl font-medium" href="/">
<RocketIcon className="mr-2 h-5 w-5" /> {APP_TITLE} Dashboard
</Link>

<UserDropdown
email={user.email}
avatar={user.avatar}
className="ml-auto"
/>
{user ? <UserDropdown email={user.email} avatar={user.avatar} className="ml-auto" /> : null}
</div>
</header>
);
Expand Down
14 changes: 3 additions & 11 deletions src/app/(main)/dashboard/_components/posts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import { NewPost } from "./new-post";
import { PostCard } from "./post-card";

interface PostsProps {
promises: Promise<
[RouterOutputs["post"]["myPosts"], RouterOutputs["stripe"]["getPlan"]]
>;
promises: Promise<[RouterOutputs["post"]["myPosts"], RouterOutputs["stripe"]["getPlan"]]>;
}

export function Posts({ promises }: PostsProps) {
Expand Down Expand Up @@ -49,17 +47,11 @@ export function Posts({ promises }: PostsProps) {
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<NewPost
isEligible={
(optimisticPosts.length < 3 || subscriptionPlan?.isPro) ?? false
}
isEligible={(optimisticPosts.length < 2 || subscriptionPlan?.isPro) ?? false}
setOptimisticPosts={setOptimisticPosts}
/>
{optimisticPosts.map((post) => (
<PostCard
key={post.id}
post={post}
setOptimisticPosts={setOptimisticPosts}
/>
<PostCard key={post.id} post={post} setOptimisticPosts={setOptimisticPosts} />
))}
</div>
);
Expand Down
28 changes: 28 additions & 0 deletions src/app/(main)/dashboard/_components/verificiation-warning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ExclamationTriangleIcon } from "@/components/icons";

import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { validateRequest } from "@/lib/auth/validate-request";
import Link from "next/link";

export async function VerificiationWarning() {
const { user } = await validateRequest();

return user?.emailVerified === false ? (
<Alert className="rounded-lg bg-yellow-50 text-yellow-700 dark:bg-gray-800 dark:text-yellow-400">
<ExclamationTriangleIcon className="h-5 w-5 !text-yellow-700 dark:!text-yellow-400" />
<div className="flex lg:items-center">
<div className="w-full">
<AlertTitle>Account verification required</AlertTitle>
<AlertDescription>
A verification email has been sent to your email address. Please verify your account to
access all features.
</AlertDescription>
</div>
<Button size="sm" asChild>
<Link href="/verify-email">Verify Email</Link>
</Button>
</div>
</Alert>
) : null;
}
22 changes: 6 additions & 16 deletions src/app/(main)/dashboard/billing/_components/billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,14 @@ export async function Billing({ stripePromises }: BillingProps) {
<>
<section>
<Card className="space-y-2 p-8">
<h3 className="text-lg font-semibold sm:text-xl">
{plan?.name ?? "Free"} plan
</h3>
<h3 className="text-lg font-semibold sm:text-xl">{plan?.name ?? "Free"} plan</h3>
<p className="text-sm text-muted-foreground">
{!plan?.isPro
? "The free plan is limited to 3 posts. Upgrade to the Pro plan to unlock unlimited posts."
? "The free plan is limited to 2 posts. Upgrade to the Pro plan to unlock unlimited posts."
: plan.isCanceled
? "Your plan will be canceled on "
: "Your plan renews on "}
{plan?.stripeCurrentPeriodEnd
? formatDate(plan.stripeCurrentPeriodEnd)
: null}
{plan?.stripeCurrentPeriodEnd ? formatDate(plan.stripeCurrentPeriodEnd) : null}
</p>
</Card>
</section>
Expand All @@ -48,26 +44,20 @@ export async function Billing({ stripePromises }: BillingProps) {
<Card key={item.name} className="flex flex-col p-2">
<CardHeader className="h-full">
<CardTitle className="line-clamp-1">{item.name}</CardTitle>
<CardDescription className="line-clamp-2">
{item.description}
</CardDescription>
<CardDescription className="line-clamp-2">{item.description}</CardDescription>
</CardHeader>
<CardContent className="h-full flex-1 space-y-6">
<div className="text-3xl font-bold">
{item.price}
<span className="text-sm font-normal text-muted-foreground">
/month
</span>
<span className="text-sm font-normal text-muted-foreground">/month</span>
</div>
<div className="space-y-2">
{item.features.map((feature) => (
<div key={feature} className="flex items-center gap-2">
<div className="aspect-square shrink-0 rounded-full bg-foreground p-px text-background">
<CheckIcon className="size-4" aria-hidden="true" />
</div>
<span className="text-sm text-muted-foreground">
{feature}
</span>
<span className="text-sm text-muted-foreground">{feature}</span>
</div>
))}
</div>
Expand Down
17 changes: 6 additions & 11 deletions src/app/(main)/dashboard/billing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,21 @@ export default async function BillingPage() {
redirect("/signin");
}

const stripePromises = Promise.all([
api.stripe.getPlans.query(),
api.stripe.getPlan.query(),
]);
const stripePromises = Promise.all([api.stripe.getPlans.query(), api.stripe.getPlan.query()]);

return (
<div className="grid gap-8 py-10 md:py-8">
<div className="mb-6">
<div className="grid gap-8">
<div>
<h1 className="text-3xl font-bold md:text-4xl">Billing</h1>
<p className="text-sm text-muted-foreground">
Manage your billing and subscription
</p>
<p className="text-sm text-muted-foreground">Manage your billing and subscription</p>
</div>
<section>
<Alert className="p-6 [&>svg]:left-6 [&>svg]:top-6 [&>svg~*]:pl-10">
<ExclamationTriangleIcon className="h-6 w-6" />
<AlertTitle>This is a demo app.</AlertTitle>
<AlertDescription>
{APP_TITLE} app is a demo app using a Stripe test environment. You
can find a list of test card numbers on the{" "}
{APP_TITLE} app is a demo app using a Stripe test environment. You can find a list of
test card numbers on the{" "}
<a
href="https://stripe.com/docs/testing#cards"
target="_blank"
Expand Down
22 changes: 10 additions & 12 deletions src/app/(main)/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { validateRequest } from "@/lib/auth/validate-request";
import { redirects } from "@/lib/constants";
import { redirect } from "next/navigation";
import * as React from "react";
import { DashboardNav } from "./_components/dashboard-nav";
import { VerificiationWarning } from "./_components/verificiation-warning";

interface Props {
children: React.ReactNode;
}

export default async function DashboardLayout({ children }: Props) {
const { user } = await validateRequest();

if (!user) redirect(redirects.toLogin);

export default function DashboardLayout({ children }: Props) {
return (
<div className="container flex min-h-[calc(100vh-180px)] flex-col gap-6 px-2 pt-6 md:flex-row md:px-4 lg:gap-10">
<DashboardNav className="flex flex-shrink-0 gap-2 md:w-48 md:flex-col lg:w-80" />
<main className="w-full">{children}</main>
<div className="container min-h-[calc(100vh-180px)] px-2 pt-6 md:px-4">
<div className="flex flex-col gap-6 md:flex-row lg:gap-10">
<DashboardNav className="flex flex-shrink-0 gap-2 md:w-48 md:flex-col lg:w-80" />
<main className="w-full space-y-4">
<VerificiationWarning />
<div>{children}</div>
</main>
</div>
</div>
);
}
13 changes: 8 additions & 5 deletions src/app/(main)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { redirect } from "next/navigation";
import { env } from "@/env";
import { api } from "@/trpc/server";
import { type Metadata } from "next";
import * as React from "react";
import { z } from "zod";
import { Posts } from "./_components/posts";
import { PostsSkeleton } from "./_components/posts-skeleton";
import { validateRequest } from "@/lib/auth/validate-request";
import { redirects } from "@/lib/constants";

export const metadata: Metadata = {
metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),
Expand All @@ -24,19 +27,19 @@ const schmea = z.object({
export default async function DashboardPage({ searchParams }: Props) {
const { page } = schmea.parse(searchParams);

const { user } = await validateRequest();
if (!user) redirect(redirects.toLogin);

/**
* Passing multiple promises to `Promise.all` to fetch data in parallel to prevent waterfall requests.
* Passing promises to the `Posts` component to make them hot promises (they can run without being awaited) to prevent waterfall requests.
* @see https://www.youtube.com/shorts/A7GGjutZxrs
* @see https://nextjs.org/docs/app/building-your-application/data-fetching/patterns#parallel-data-fetching
*/
const promises = Promise.all([
api.post.myPosts.query({ page }),
api.stripe.getPlan.query(),
]);
const promises = Promise.all([api.post.myPosts.query({ page }), api.stripe.getPlan.query()]);

return (
<div className="py-10 md:py-8">
<div>
<div className="mb-6">
<h1 className="text-3xl font-bold md:text-4xl">Posts</h1>
<p className="text-sm text-muted-foreground">Manage your posts here</p>
Expand Down
8 changes: 3 additions & 5 deletions src/app/(main)/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@ export default async function BillingPage() {
}

return (
<div className="grid gap-8 py-10 md:py-8">
<div className="mb-4">
<div className="grid gap-8">
<div>
<h1 className="text-3xl font-bold md:text-4xl">Settings</h1>
<p className="text-sm text-muted-foreground">
Manage your account settings
</p>
<p className="text-sm text-muted-foreground">Manage your account settings</p>
</div>
<p>Work in progress...</p>
</div>
Expand Down
8 changes: 6 additions & 2 deletions src/app/(main)/editor/[postId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from "react";
import { api } from "@/trpc/server";
import { notFound } from "next/navigation";
import { notFound, redirect } from "next/navigation";
import { PostEditor } from "./_components/post-editor";
import { ArrowLeftIcon } from "@/components/icons";
import Link from "next/link";
import { validateRequest } from "@/lib/auth/validate-request";
import { redirects } from "@/lib/constants";

interface Props {
params: {
Expand All @@ -12,8 +14,10 @@ interface Props {
}

export default async function EditPostPage({ params }: Props) {
const post = await api.post.get.query(params.postId);
const { user } = await validateRequest();
if (!user) redirect(redirects.toLogin);

const post = await api.post.get.query(params.postId);
if (!post) notFound();

return (
Expand Down
12 changes: 2 additions & 10 deletions src/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { type ReactNode } from "react";
import { redirect } from "next/navigation";
import { Header } from "./_components/header";
import { Footer } from "./_components/footer";
import { validateRequest } from "@/lib/auth/validate-request";
import { redirects } from "@/lib/constants";

const MainLayout = async ({ children }: { children: ReactNode }) => {
const { user } = await validateRequest();

if (!user) redirect(redirects.toLogin);
if (user.emailVerified === false) redirect(redirects.toVerify);

const MainLayout = ({ children }: { children: ReactNode }) => {
return (
<>
<Header user={user} />
<Header />
{children}
<Footer />
</>
Expand Down

0 comments on commit 95ac1e0

Please sign in to comment.