Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 40 additions & 12 deletions AI_LOG.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,53 @@
# AI Usage Log

Please be honest and detailed about your AI usage. Using AI is perfectly fine and expected, but we want to understand your workflow!

## Tools Used
*(e.g., GitHub Copilot, ChatGPT, Claude, Cursor, etc.)*
*GitHub Copilot, Opencode, ChatGPT*

- *Github Copilot - For understanding the challenge, Logic errors and missing features*
- *ChatGPT - For taking assistance in handling the errors in git*
- *Opencode - For completing the coding part in each task*

-
-

## How did you use AI for this assessment?
## Logs:

**Task 1: Pricing Logic Bug**
- *Did you use AI to find the bug? If so, what prompt did you use?*
- *Did you use AI to write the fix?*
- *Did you use AI to find the bug? If so, what prompt did you use?* <br>
Yes, I use Github Copilot to find the bug, the bug was on the calculator.ts file.
- *Did you use AI to write the fix?* <br>
Yes, With Opencode I changed the logic, when a Pro user also has a stackable free-shipping code, the shipping rate gets added into discountCents, inflating the discount. Free shipping is already handled by setting shippingCents = 0. I'll remove the offending block so shipping stays a separate value.

**Task 2: Dashboard Performance**
- *How did you use AI here? Did you use it to identify the N+1 issue, or just to write the optimized query?*
- *How did you use AI here? Did you use it to identify the N+1 issue, or just to write the optimized query?* <br>
I used github copilot to identify the problem and use opencode to write the code.
I found the N+1 query issue. The listOrders function fetches all orders, then makes a separate query for each order's items. I fix this by using Prisma's include to fetch everything in one query.

**Task 3: Duplicate Order Feature**
- *Did you use AI to write the API route, the client component, or both?*
- *Did AI make any mistakes you had to fix manually?*
- *Did you use AI to write the API route, the client component, or both?* <br>
Yes, for both i use opencode what i done is
1. Add a POST endpoint to /api/orders/[id]/route.ts that validates stock and returns order items
2. Add a "Duplicate Order" button to OrderCard.tsx
3. Add a function to cart.ts to add multiple items from an order (replacing the cart)
- *Did AI make any mistakes you had to fix manually?* <br>
Yes, Added "use client" directive at the top of OrderCard.tsx.

## General Comments
*(Any other thoughts on how AI helped or hindered you during this assessment?)*

**Comment 1 — Docker / Database Setup Confusion**<br>

- *When I first saw `docker-compose.yml`, I thought of installing something in Docker. When I opened it, I saw the PostgreSQL username, password, and database, so I thought I needed to initialize Docker. For confirmation, I asked GitHub Copilot, "Do I need to initialize Docker for PostgreSQL?" But it reminded me of Prisma, so I left it and copied the DB URL from `env.example`.*

- *But when I clicked the "Shop Now" button, it triggered an error: "Prisma couldn't connect to the database." I was shocked and asked OpenCode for a fix. It told me to start a local PostgreSQL 16 instance matching the repo's `docker-compose.yml` credentials using rootless Podman.*

**Comment 2 — Navigation Buttons**<br>

Using Opencode I added a back button and logout button

- *Logout — `components/dashboard/LogoutButton.tsx` added to the Order History page.*
- *Back arrow — `components/layout/BackButton.tsx` rendered in the root layout on the Orders and Shop pages.*

**Comment 3 — Clear Cart on Logout**<br>

Using Opencode I also added a Clear Cart feature after every logout in the dashboard

- *Cart cleared — `actions/cart.ts` modified to clear the cart after logout.*
- *Confirmation — `components/dashboard/LogoutButton.tsx` added a confirmation dialog before logout.*
38 changes: 22 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,33 @@ Welcome to OrbitStack! This is a modern, Next.js-based e-commerce platform built
npm run dev
```

## The Challenge
## The Challenge [Finished]

There are three tasks to complete. Please review the codebase and implement the fixes and features described below.

### Task 1: Pricing Logic Bug
### Task 1: Pricing Logic Bug [100% completed]
A user reported an issue where applying a free shipping discount code to a Pro account results in a negative discount calculation (instead of ) for the shipping cost.
- You need to locate the pricing computation logic and fix the bug so that shipping is calculated correctly when multiple shipping discounts/free tiers stack.
- The pricing computation logic was fixed so that shipping is calculated correctly when multiple shipping discounts/free tiers stack.

### Task 2: Dashboard Performance
### Task 2: Dashboard Performance [100% completed]
The /dashboard page is loading extremely slowly for users who have a large order history.
- Identify the performance bottleneck when loading the order history.
- Optimize the data fetching so the dashboard loads quickly regardless of how many orders the user has.
- The performance bottleneck when loading the order history was identified.
- The data fetching was optimized so the dashboard loads quickly regardless of how many orders the user has.

### Task 3: "Duplicate Order" Feature [100% completed]
A "Duplicate Order" button was added to the past orders displayed on the dashboard.
- Clicking the button adds the same items from the past order directly into the user's cart (provided they are still in stock).
- The API endpoint and the client-side logic to update the cart store was built.



## Additional Features Added
1. I added a back button and logout button feature

- *Logout — `components/dashboard/LogoutButton.tsx` added to the Order History page.*
- *Back arrow — `components/layout/BackButton.tsx` rendered in the root layout on the Orders and Shop pages.*

### Task 3: "Duplicate Order" Feature
Add a new "Duplicate Order" button to the past orders displayed on the dashboard.
- Clicking the button should add the same items from the past order directly into the user's cart (provided they are still in stock).
- You will need to build the API endpoint and the client-side logic to update the cart store.
2. I also added a Clear Cart feature after every logout in the dashboard

Good luck!
- *Cart cleared — `actions/cart.ts` modified to clear the cart after logout.*
- *Confirmation — `components/dashboard/LogoutButton.tsx` added a confirmation dialog before logout.*

## Submission

When you are finished, please submit a pull request with your changes.
**IMPORTANT:** You must fill out the AI_LOG.md file included in this repository. Please describe which AI tools you used and how you used them to complete the tasks.
130 changes: 130 additions & 0 deletions app/api/cart/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db/client';
import { getSession } from '@/lib/auth/session';
import type { ApiResponse } from '@/types';

export async function GET() {
try {
const session = await getSession();
if (!session) {
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Unauthorized' },
{ status: 401 }
);
}

const cart = await prisma.cart.findUnique({
where: { userId: session.id },
include: {
items: { include: { product: true } },
},
});

const items = (cart?.items ?? []).map((item) => ({
product: {
id: item.product.id,
name: item.product.name,
description: item.product.description,
priceCents: item.product.priceCents,
stock: item.product.stock,
category: item.product.category,
imageUrl: item.product.imageUrl,
active: item.product.active,
createdAt: item.product.createdAt.toISOString(),
updatedAt: item.product.updatedAt.toISOString(),
},
quantity: item.quantity,
}));

return NextResponse.json<ApiResponse<typeof items>>({
data: items,
error: null,
});
} catch (err) {
console.error('[GET /api/cart]', err);
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Failed to load cart' },
{ status: 500 }
);
}
}

export async function POST(request: NextRequest) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Unauthorized' },
{ status: 401 }
);
}

const body = await request.json();
const { items } = body as {
items: { productId: string; quantity: number }[];
};

if (!Array.isArray(items)) {
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Invalid cart data' },
{ status: 400 }
);
}

await prisma.$transaction(async (tx) => {
const cart = await tx.cart.upsert({
where: { userId: session.id },
create: { userId: session.id },
update: {},
});

await tx.cartItem.deleteMany({ where: { cartId: cart.id } });

if (items.length > 0) {
await tx.cartItem.createMany({
data: items.map((item) => ({
cartId: cart.id,
productId: item.productId,
quantity: item.quantity,
})),
});
}
});

return NextResponse.json<ApiResponse<null>>({
data: null,
error: null,
});
} catch (err) {
console.error('[POST /api/cart]', err);
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Failed to save cart' },
{ status: 500 }
);
}
}

export async function DELETE() {
try {
const session = await getSession();
if (!session) {
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Unauthorized' },
{ status: 401 }
);
}

await prisma.cart.deleteMany({ where: { userId: session.id } });

return NextResponse.json<ApiResponse<null>>({
data: null,
error: null,
});
} catch (err) {
console.error('[DELETE /api/cart]', err);
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Failed to clear cart' },
{ status: 500 }
);
}
}
94 changes: 94 additions & 0 deletions app/api/orders/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db/client';
import { getSession } from '@/lib/auth/session';
import { getOrderById } from '@/lib/orders/queries';
import type { ApiResponse } from '@/types';
Expand Down Expand Up @@ -45,3 +46,96 @@ export async function GET(
);
}
}

export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Unauthorized' },
{ status: 401 }
);
}

const { id } = await params;
const order = await getOrderById(id);

if (!order) {
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Order not found' },
{ status: 404 }
);
}

if (order.userId !== session.id) {
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Forbidden' },
{ status: 403 }
);
}

if (!order.items || order.items.length === 0) {
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Order has no items' },
{ status: 400 }
);
}

const productIds = order.items.map((item) => item.productId);
const products = await prisma.product.findMany({
where: { id: { in: productIds }, active: true },
});

if (products.length !== productIds.length) {
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'One or more products are no longer available' },
{ status: 400 }
);
}

const outOfStock = order.items.filter((item) => {
const product = products.find((p: typeof products[0]) => p.id === item.productId)!;
return product.stock < item.quantity;
});

if (outOfStock.length > 0) {
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: `Insufficient stock for ${outOfStock.map((i) => i.product.name).join(', ')}` },
{ status: 400 }
);
}

const itemsForCart = order.items.map((item) => {
const product = products.find((p: typeof products[0]) => p.id === item.productId)!;
return {
product: {
id: product.id,
name: product.name,
description: product.description,
priceCents: product.priceCents,
stock: product.stock,
category: product.category,
imageUrl: product.imageUrl,
active: product.active,
createdAt: product.createdAt.toISOString(),
updatedAt: product.updatedAt.toISOString(),
},
quantity: item.quantity,
};
});

return NextResponse.json<ApiResponse<typeof itemsForCart>>({
data: itemsForCart,
error: null,
});
} catch (err) {
console.error('[POST /api/orders/[id]/duplicate]', err);
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Failed to duplicate order' },
{ status: 500 }
);
}
}
16 changes: 10 additions & 6 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { redirect } from 'next/navigation';
import { getSession } from '@/lib/auth/session';
import { listOrders } from '@/lib/orders/queries';
import { OrderCard } from '@/components/dashboard/OrderCard';
import { LogoutButton } from '@/components/dashboard/LogoutButton';
import type { Order, OrderItem, Product } from '@/types';

export const metadata: Metadata = {
Expand Down Expand Up @@ -31,12 +32,15 @@ export default async function DashboardPage() {

return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-lunar-100">Order History</h1>
<p className="mt-1 text-sm text-lunar-400">
Welcome back, {session.name} —{' '}
<span className="capitalize text-moon-gold">{session.tier}</span> member
</p>
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-lunar-100">Order History</h1>
<p className="mt-1 text-sm text-lunar-400">
Welcome back, {session.name} —{' '}
<span className="capitalize text-moon-gold">{session.tier}</span> member
</p>
</div>
<LogoutButton />
</div>

{serialized.length === 0 ? (
Expand Down
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Oswald, Space_Mono } from 'next/font/google';
import './globals.css';
import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
import { BackButton } from '@/components/layout/BackButton';

const oswald = Oswald({ subsets: ['latin'], variable: '--font-oswald' });
const spaceMono = Space_Mono({ weight: ['400', '700'], subsets: ['latin'], variable: '--font-space-mono' });
Expand All @@ -21,6 +22,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<body className="min-h-screen bg-black font-mono text-white antialiased">
<Header />
<main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 relative z-10">
<BackButton />
{children}
</main>
<Footer />
Expand Down
Loading