Skip to content

Commit

Permalink
Add: Redirect to origin url feature after user has been told to sign-…
Browse files Browse the repository at this point in the history
…in using query params, route protection for admin, icons for customer face dropdown, responsives for admin pages and route for free products download.
  • Loading branch information
saif-gitreps committed Nov 7, 2024
1 parent 8115941 commit b9488ca
Show file tree
Hide file tree
Showing 17 changed files with 190 additions and 82 deletions.
30 changes: 23 additions & 7 deletions src/app/(auth)/_actions/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import bcrypt from "bcrypt";
import db from "@/db/db";
import { cookies } from "next/headers";
import { JWTPayload } from "jose";
import { revalidatePath } from "next/cache";

const authSchema = z.object({
email: z.string().email({ message: "Invalid email address" }).trim(),
Expand All @@ -29,7 +30,8 @@ export type AuthState = {
export async function signIn(
prevState: AuthState,
formData: FormData,
toSeller?: boolean
toSeller?: boolean,
origin?: string | null
): Promise<AuthState> {
const result = authSchema.safeParse(Object.fromEntries(formData));

Expand All @@ -53,9 +55,9 @@ export async function signIn(

await createSession(userFromDb.id, userFromDb.role);

if (toSeller) {
redirect("/admin");
}
if (toSeller) redirect("/admin");

if (origin) redirect(origin);

redirect("/");
}
Expand Down Expand Up @@ -98,13 +100,27 @@ export async function signUp(prevState: AuthState, formData: FormData) {

await createSession(newUser.id, newUser.role);

// TODO: Redirect to the origin at which the user was prompted to sign in
redirect("/");
}

export async function logout() {
const protectedRoutes = [
/^\/orders$/,
/^\/admin\/.*/,
/^\/products\/checkout$/,
/^\/products\/download\/.+$/,
/^\/profile$/,
];

export async function logout(pathname: string) {
await deleteSession();
// redirect("/sign-in");

const isProtectedRoute = protectedRoutes.some((route) => route.test(pathname));

if (isProtectedRoute) {
redirect("/sign-in");
} else {
revalidatePath(pathname);
}
}

const updateSchema = z.object({
Expand Down
8 changes: 6 additions & 2 deletions src/app/(auth)/_components/SignInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ export function SignInForm() {

const isSeller: boolean = searchParams.get("as") === "seller";

// simple closure to pass the isSeller value to the signIn function
const encodedOrigin = searchParams.get("origin") || "/";

const origin = decodeURIComponent(encodedOrigin);

// simple closure to pass the isSeller & origin value to the signIn function
const signInWithSeller = async (prevState: AuthState, formData: FormData) => {
return signIn(prevState, formData, isSeller);
return signIn(prevState, formData, isSeller, origin);
};

const [state, action] = useFormState<AuthState, FormData>(signInWithSeller, {});
Expand Down
4 changes: 0 additions & 4 deletions src/app/(auth)/sign-in/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ export default function SignIn() {
const isSeller: boolean = searchParams.get("as") === "seller";

const router = useRouter();

const origin = searchParams.get("origin");
// TODO: redirect from example: cart page to sign-in

return (
<div className="mt-20 p-2 space-y-5">
<PageHeader className="text-center">Sign in</PageHeader>
Expand Down
13 changes: 0 additions & 13 deletions src/app/(customerFacing)/_components/LogoutButton.tsx

This file was deleted.

36 changes: 30 additions & 6 deletions src/app/(customerFacing)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import Cart from "@/app/(customerFacing)/_components/Cart";
import SearchBar from "@/components/SearchBar";
export const dynamic = "force-dynamic";

import { CircleUser } from "lucide-react";
import {
ChartNoAxesCombined,
CircleUser,
ShoppingBasket,
UserRoundPen,
} from "lucide-react";
import { cookies } from "next/headers";
import { decrypt } from "@/lib/session";
import { MobileNav, Nav, NavLink } from "@/components/Nav";
Expand All @@ -15,7 +20,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import Link from "next/link";
import LogoutButton from "./_components/LogoutButton";
import LogoutButton from "../../components/LogoutButton";

export default async function Layout({ children }: Readonly<{ children: ReactNode }>) {
const cookie = cookies().get("session")?.value;
Expand Down Expand Up @@ -43,8 +48,9 @@ export default async function Layout({ children }: Readonly<{ children: ReactNod
)
)}
</div>

<div className="flex items-center justify-between w-full md:w-auto gap-2 px-2">
<MobileNav />
<MobileNav navItems={navItems} />

<SearchBar />

Expand All @@ -58,13 +64,31 @@ export default async function Layout({ children }: Readonly<{ children: ReactNod
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48">
<DropdownMenuItem asChild>
<Link href="/profile">Profile</Link>
<Link href="/profile">
<UserRoundPen
size={20}
className="mr-1 stroke-blue-700"
/>
Profile
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/orders">My Orders</Link>
<Link href="/orders">
<ShoppingBasket
size={20}
className="mr-1 stroke-green-500"
/>{" "}
My Orders
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/admin">Sales Dashboard</Link>
<Link href="/admin">
<ChartNoAxesCombined
size={20}
className="mr-1 stroke-purple-700"
/>
Sales Dashboard
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive">
Expand Down
6 changes: 3 additions & 3 deletions src/app/(customerFacing)/products/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import db from "@/db/db";
import { cache } from "@/lib/cache";
import { formatCurrency } from "@/lib/formatter";
import { Product } from "@prisma/client";
import { PackageSearch, Star } from "lucide-react";
import { Download, PackageSearch, Star } from "lucide-react";

import Image from "next/image";
import Link from "next/link";
Expand Down Expand Up @@ -101,12 +101,12 @@ export default async function ProductViewPage({
</div>
<CardFooter className="mt-auto flex flex-col py-1">
{product.priceInCents === 1 ? (
// TODO : Download functionality
<Button
size="lg"
className="w-full bg-green-700 text-white hover:bg-green-600"
>
Download now
<Link href={`/products/download/free/${id}`}>Download now</Link>
<Download size={18} className="ml-1" />
</Button>
) : (
<div className="flex items-center flex-col">
Expand Down
2 changes: 0 additions & 2 deletions src/app/(customerFacing)/products/checkout/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ export default async function CheckoutPage({
}: {
searchParams: SearchParams;
}) {
// TODO: handle error cases where params is empty or cart is empty, also handle case where product is null

const productIds = searchParams.pid;

const ids = Array.isArray(productIds) ? productIds : [productIds];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import db from "@/db/db";
import fs from "fs/promises";
import { NextRequest, NextResponse } from "next/server";

export async function GET(
req: NextRequest,
{ params: { productId } }: { params: { productId: string } }
) {
const product = await db.product.findUnique({
where: { id: productId },
select: { filePath: true, name: true },
});

if (product == null)
return NextResponse.redirect(new URL("/products/download/expired", req.url));

// TODO: create an order or something to track number of downloads

const { size } = await fs.stat(product.filePath);
const file = await fs.readFile(product.filePath);
const extension = product.filePath.split(".").pop();

return new NextResponse(file, {
headers: {
"Content-Disposition": `attachment; filename="${product.name}.${extension}"`,
"Content-Length": size.toString(),
},
});
}
1 change: 1 addition & 0 deletions src/app/(customerFacing)/products/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type ProductPageProps = {
};

export default function ProductPage({ searchParams }: ProductPageProps) {
// TODO : on related pages, add a params as category on view all button
return (
<div className="space-y-4">
{searchParams.searchQuery && (
Expand Down
54 changes: 33 additions & 21 deletions src/app/admin/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Nav, NavItem, NavLink } from "@/components/Nav";
import LogoutButton from "../(customerFacing)/_components/LogoutButton";
import { MobileNav, Nav, NavItem, NavLink } from "@/components/Nav";
import LogoutButton from "../../components/LogoutButton";
import { getCurrentUserFromSession } from "../(auth)/_actions/auth";

export const dynamic = "force-dynamic"; // We want to prevent caching in admin page, as we need the most updated data.
Expand All @@ -13,30 +13,42 @@ export default async function AdminLayout({

const isAdmin = user?.role === "admin";

const navItems = [
{ name: "Dashboard", href: "/admin", isVisible: true },
{ name: "Products", href: "/admin/products", isVisible: true },
{ name: "Sales", href: "/admin/orders", isVisible: true },
{ name: "Users", href: "/admin/users", isVisible: isAdmin },
{
name: "Buy Products",
href: "/products",
isVisible: !isAdmin,
},
];

return (
<div className="min-h-screen">
<Nav>
<div className="flex flex-row items-center justify-between h-auto">
<div className="flex flex-row items-center ">
<NavLink href="/admin">Dashboard</NavLink>

<NavLink href="/admin/products">
{isAdmin ? "All" : "My"} Products
</NavLink>

{/* Users page will have the people that are selling products and has bought the products */}
{isAdmin && <NavLink href="/admin/users">Users</NavLink>}

{!isAdmin && <NavLink href="/admin/customers">Customers</NavLink>}

<NavLink href="/admin/orders">Sales</NavLink>

{!isAdmin && <NavLink href="/">Buy Products</NavLink>}

<NavItem className="text-destructive">
<LogoutButton isAuthenticated={true} />
</NavItem>
<div className="md:flex flex-row items-center hidden">
{navItems.map(
(item) =>
item.isVisible && (
<NavLink key={item.href} href={item.href}>
{item.name}
</NavLink>
)
)}
</div>
<NavItem className="text-destructive hidden md:block">
<LogoutButton isAuthenticated={true} />
</NavItem>
</div>

<div className="flex items-center justify-between w-full md:w-auto gap-2 ps-2">
<MobileNav navItems={navItems} />
<NavItem className="text-destructive md:hidden">
<LogoutButton isAuthenticated={true} />
</NavItem>
</div>
</Nav>
<div className="mt-16 flex-1 container py-6">{children}</div>
Expand Down
4 changes: 1 addition & 3 deletions src/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,7 @@ export default async function AdminDashboard() {
/>
<DashboardCard
title={isAdmin ? "All Users" : "Customers"}
subtitle={`${formatNumber(
userData.averageValuePerUser
)} Average sales per user`}
subtitle={`${formatNumber(userData.averageValuePerUser)} Average sales`}
body={formatNumber(userData.userCount)}
/>
<DashboardCard
Expand Down
16 changes: 14 additions & 2 deletions src/app/admin/users/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,19 @@ import {
import { MoreVertical } from "lucide-react";
import { DeleteDropDownItem } from "./_components/UserActions";
import { PageHeader } from "@/components/PageHeader";
import { getCurrentUserFromSession } from "@/app/(auth)/_actions/auth";
import { redirect } from "next/navigation";

function getUsers() {
function getUsers(userId: string) {
return db.user.findMany({
select: {
id: true,
email: true,
orders: { select: { pricePaidInCents: true } },
products: { select: { id: true } },
},
where: {
id: { not: userId },
},
orderBy: { createdAt: "desc" },
});
Expand All @@ -39,7 +45,11 @@ export default function UsersPage() {
}

async function UsersTable() {
const users = await getUsers();
const currentUser = await getCurrentUserFromSession();

if (!currentUser?.userId) redirect("/sign-in");

const users = await getUsers(currentUser?.userId as string);

if (users.length === 0) return <p>No customers found</p>;

Expand All @@ -48,6 +58,7 @@ async function UsersTable() {
<TableHeader>
<TableRow>
<TableHead>Email</TableHead>
<TableHead>Products selling</TableHead>
<TableHead>Orders</TableHead>
<TableHead>Value</TableHead>
<TableHead className="w-0">
Expand All @@ -59,6 +70,7 @@ async function UsersTable() {
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.email}</TableCell>
<TableCell>{formatNumber(user.products.length)}</TableCell>
<TableCell>{formatNumber(user.orders.length)}</TableCell>
<TableCell>
{formatCurrency(
Expand Down
Loading

0 comments on commit b9488ca

Please sign in to comment.