Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
48490ea
fix(auth): remove process.env.JWT_SECRET_KEY check causing login failure
haroldcastillo Nov 21, 2025
20fd42e
feat(config): add /admin route matcher
haroldcastillo Nov 21, 2025
cb600d2
fix(theme-toggle): prevent hydration mismatch by delaying mount
haroldcastillo Nov 21, 2025
8e00a32
refactor(api): rename /email/try to /email/login for clarity
haroldcastillo Nov 21, 2025
f147a90
feat(filter):improve and update the filtering for products and instal…
haroldcastillo Nov 22, 2025
0bb8ae5
feat(install):added shadcn pagination component
haroldcastillo Nov 22, 2025
df5bf7b
feat(init):setup for react query
haroldcastillo Nov 22, 2025
fae96b0
fix(remove): uninstall the react query library
haroldcastillo Nov 22, 2025
3cb78b9
feat(pagination):support page and page size params
haroldcastillo Nov 22, 2025
8b414c6
fix(filter):adjust it to auto update params after input change
haroldcastillo Nov 22, 2025
2c5dff9
feat(product):code clean up
haroldcastillo Nov 22, 2025
e92aac8
feature(report):implementation of report
haroldcastillo Nov 23, 2025
28a6a00
feat(report): ui improvement
haroldcastillo Dec 2, 2025
ce7d755
fix(config):add seed location seed
haroldcastillo Dec 3, 2025
882b34d
feat(prisma config):added crosssell in product schema
haroldcastillo Dec 3, 2025
37ad327
feat(utils): added the assigning of cross-sell products
haroldcastillo Dec 3, 2025
3955c40
fix(product form): import react
haroldcastillo Dec 3, 2025
e189cf4
fix(cleanup): remove un used codes
haroldcastillo Dec 3, 2025
5d349f8
feat(config): update carousel version
haroldcastillo Dec 3, 2025
0eddf23
feat(prisma config) : added crosssell in product
haroldcastillo Dec 3, 2025
f487209
feat(carousel): created carousel for crosssell products
haroldcastillo Dec 3, 2025
3ca254d
fix(cart quantity input) : improve quantity input flow
haroldcastillo Dec 3, 2025
e6e7b85
feat(cart popup): created context provider for added product popup
haroldcastillo Dec 3, 2025
28a9957
feat(utils):added debounce function
haroldcastillo Dec 3, 2025
22ab90f
feat(crosssell api): added api for getting crosssell products
haroldcastillo Dec 3, 2025
23f4f01
feat(doc): update document
haroldcastillo Dec 3, 2025
c5479f1
fix(docs): added author
haroldcastillo Dec 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 108 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<div align="center"><h3>Full-Stack E-Commerce Platform</h3><p>Built using Typescript with Next.js, Prisma ORM and TailwindCSS.</p></div>
<div align="center">
<a href="https://pasargad.vercel.app">Storefront</a>
<a href="https://pasargad.vercel.app">Storefront</a>
<span> · </span>
<a href="https://pardis.vercel.app">Admin Panel</a>
</div>
Expand Down Expand Up @@ -100,3 +100,110 @@ Follow the deployment guides for [Vercel](https://create.t3.gg/en/deployment/ver
This project is MIT-licensed and is free to use and modify for your own projects. Check the [LICENSE](./LICENSE) file for details.

Created by [Amirhossein Mohammadi](https://github.com/sesto-dev).




## 📘 Developer Assessment Additions (Harold)

### Introduction
This project was developed as part of my Developer Assessment Exam. The goal of the assessment is to extend the existing next-prisma-tailwind-ecommerce application by implementing new features that enhance both the storefront and the admin panel.

The assessment focuses on three major enhancements:

### 1. Rebuilt Product Page Filter

#### Objective
Allow users to filter products by text, price, category, brand, and order.

#### Implementation Details
1) The filtering interface was redesigned to improve usability. Instead of placing all filters directly on the page—which can appear cluttered—secondary filters such as categories, brand, and price range were moved into a popover component. Users typically arrive intending to browse products immediately, so the search bar remains the primary, always-visible control. To address the drawback of hidden filters, a number badge was added to the filter button showing how many filters are currently active.

2) New filtering inputs were implemented for Sort By, Category, Price Range, and Brand. Dispatch was used to batch state updates to prevent excessive re-fetches, particularly during text search.

3) Pagination was added to avoid loading all products at once, improving performance and reducing layout shifts.

#### Testing
- Filtering across multiple categories, search queries, brand selection, and minimum/maximum price was validated.
- Responsiveness was checked to ensure proper layout behavior across different screen sizes.
- The page was tested on multiple devices to confirm consistent performance and functionality.
- Product listings now update dynamically based on user selected filters without requiring a full page refresh.

### 2. Admin Reporting Page

#### Objective
The reporting page provides administrators with tools to view and analyze order data, including:
- Order summaries grouped by date to track sales over time.
- Top‑selling products based on overall sales volume.
- Filter options for date range, product categories, and brand.

#### Implementation Details
1. Added filter inputs for Category, Brand, Date From, and Date To, including validation that prevents Date From being later than Date To, and vice versa.
2. Implemented a Prisma query that fetches orders based on active filters and groups them by date.
3. Added pagination to improve performance and ensure efficient retrieval of large datasets.
4. Developed a flexible top‑selling product function. The current configuration displays the top 5 products, but the limit can be easily changed.
5. Added a summary card showing total revenue and total order count, dynamically updated based on filters.
6. Designed the UI in Figma and implemented it in the Admin panel, including a copy‑to‑clipboard feature for quick access to order IDs.

#### Testing
1. **Filter Functionality**
- Verified that filtering by Category limits results to products within the selected category.
- Confirmed that Brand filtering shows only orders containing matching products.
- Validated date range logic so Date From cannot exceed Date To.
- Confirmed combined filters (e.g., Category + Date Range) produce accurate results.

2. **Order Grouping**
- Ensured orders are grouped correctly by date.
- Cross‑checked totals against sample seed data.

3. **Pagination**
- Verified smooth navigation between pages.

4. **Top‑Selling Products**
- Confirmed the Top 5 products display accurately.
- Adjusted limits (Top 3, Top 10) to validate flexibility.

5. **Summary Cards**
- Confirmed Total Revenue calculations are accurate.
- Verified Total Order Count updates with filters.

6. **UI & Responsiveness**
- Matched the final UI to the Figma design.
- Tested responsiveness across desktop, tablet, and mobile.
- Confirmed the order ID copy feature works reliably.

### 3. Extended Prisma Database Model & Cross‑Sell Functionality
- Updated the Prisma `Product` model to include an optional `crossSellProducts` field enabling product‑to‑product relationships.
- Storefront now displays cross‑sell product suggestions on product detail pages.
- The cart UI has been enhanced to show dynamic cross‑sell suggestions along with improved, user‑friendly feedback and notifications.



### Extend Prisma DB Model & Cross-Sell Functionality

#### Objective
Extended the Prisma Product model by adding an optional `crossSellProducts` field to support linking related products. This allows the storefront to display cross-sell suggestions on product detail and cart pages, paired with improved cart feedback to strengthen product discovery.

#### Implementation Details
1. **Schema Update**
- Modified the Product model to include a self-relation many-to-many field.
- Added two relational fields, `crossSellProducts` and `crossSellOf`, enabling bidirectional product linking.

2. **Seed Update**
- Updated the seed script to include products with cross-sell relationships.
- Ensured multiple products can be linked without affecting existing product data.

3. **API Integration**
- Implemented an API endpoint that retrieves all `crossSellProducts` for the provided product IDs.

4. **Displaying Suggested Products**
- Added a product card carousel on the product page to show cross-sell items.
- Added another carousel in the cart to highlight related suggestions for items currently in the cart.

5. **Frontend Enhancements**
- Displayed each product’s stock count; items marked `isAvailable = false` are shown as Out of Stock and cannot be added to the cart.
- Improved Add to Cart flow by allowing users to enter quantity before adding, reducing repeated requests.
- Added a popup on the cart icon to acknowledge successful additions.
- After adding a product, a redirect button with a badge appears, showing how many items were added.
- Added quantity validation based on available stock.
- Enhanced the cart page to allow quantity edits in a single update action rather than incremental changes.
Binary file modified apps/admin/bun.lockb
Binary file not shown.
8 changes: 6 additions & 2 deletions apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"db:seed": "npx prisma db seed",
"db:studio": "npx prisma studio"
},
"prisma": {
"seed": "bun run prisma/seed.ts"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@persepolis/mail": "^1.0.9",
Expand All @@ -32,10 +35,10 @@
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2",
Expand All @@ -51,6 +54,7 @@
"next-themes": "^0.3.0",
"nodemailer": "^6.9.15",
"react": "18.3.1",
"react-day-picker": "^9.11.1",
"react-dom": "18.3.1",
"react-hook-form": "^7.53.0",
"react-hot-toast": "^2.4.1",
Expand Down
4 changes: 4 additions & 0 deletions apps/admin/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ model Product {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

crossSellProducts Product[] @relation("CrossSell")
crossSellOf Product[] @relation("CrossSell")


@@index([brandId])
}

Expand Down
33 changes: 33 additions & 0 deletions apps/admin/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,39 @@ async function main() {
console.error('Could not create products...')
}

try {
console.log('Assigning random cross-sell products...')

for (const product of createdProducts) {
// filter out the product itself
const otherProducts = createdProducts.filter(
(p) => p.id !== product.id
)

// choose a random number from 1 to 5 (or less if fewer products exist)
const maxCrossSells = Math.min(5, otherProducts.length)
const randomCount = Math.floor(Math.random() * maxCrossSells) + 1

// shuffle + slice based on randomCount
const randomCrossSells = otherProducts
.sort(() => 0.5 - Math.random())
.slice(0, randomCount)

await prisma.product.update({
where: { id: product.id },
data: {
crossSellProducts: {
connect: randomCrossSells.map((p) => ({ id: p.id })),
},
},
})
}

console.log('Random cross-sell assignment done!')
} catch (error) {
console.error('Could not assign cross-sell products...', error)
}

try {
await prisma.author.create({
data: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { zodResolver } from '@hookform/resolvers/zod'
import { Category } from '@prisma/client'
import { Trash } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { useState } from 'react'
import React, { useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'react-hot-toast'
import * as z from 'zod'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { format } from 'date-fns'
import { ChevronsRight, Coins, ScrollText } from 'lucide-react'
import React from 'react'

export default function ChartComponent({
orderCount = 0,
startDate,
endDate,
totalRevenue,
}: {
orderCount: number
startDate: string
endDate: string
totalRevenue: number
}) {
return (
<Card className="p-4 ">
{(startDate || endDate) && (
<>
<CardHeader className="p-0 opacity-50 flex flex-row justify-between items-center text-sm gap-1 ">
<div className="flex flex-col">
<p className="opacity-80">Start Date:</p>
<p>
{startDate
? format(new Date(startDate), 'MMM dd, yyyy')
: 'All Time'}
</p>
</div>
<ChevronsRight className="flex-shrink-0" />
<div className="flex flex-col translate-y-[-2px]">
<p className="opacity-80">End Date:</p>
<p>
{endDate
? format(new Date(endDate), 'MMM dd, yyyy')
: 'Present'}
</p>
</div>
</CardHeader>
<Separator className="my-2" />
</>
)}
<CardContent className="p-0 grid grid-cols-2 gap-5">
<TitleValueCard
title="Total Revenue"
value={`$${totalRevenue.toFixed(2)}`}
icon={<Coins className="w-4 h-4 mr-2 opacity-70" />}
/>
<TitleValueCard
title="Total Orders"
value={orderCount.toString()}
icon={<ScrollText className="w-4 h-4 mr-2 opacity-70" />}
/>
</CardContent>
</Card>
)
}

function TitleValueCard({
title,
value,
icon,
}: {
title: string
value: string
icon: React.ReactNode
}) {
return (
<div>
<div className="flex items-center mb-2 ">
{icon}
<p>{title}</p>
</div>
<h2 className="text-2xl font-bold">{value}</h2>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client'

import { Copy } from 'lucide-react'
import React from 'react'
import toast from 'react-hot-toast'

export default function CopyToClipboardComponent({ text }: { text: string }) {
const handleCopy = () => {
navigator.clipboard.writeText(text)
toast.success('Copied to clipboard!')
}
return (
<Copy
className="w-4 h-4 cursor-pointer opacity-60 hover:opacity-100 shrink-0 translate-y-[-7px]"
onClick={handleCopy}
/>
)
}
Loading