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
17 changes: 16 additions & 1 deletion frontend/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
images: {
// Allow specific remote hosts for Next/Image
domains: ["fakestoreapi.com", "i.pravatar.cc"],
remotePatterns: [
{
protocol: "https",
hostname: "fakestoreapi.com",
pathname: "/img/**",
},
{
protocol: "https",
hostname: "i.pravatar.cc",
pathname: "/**",
},
],
},
};

export default nextConfig;
12 changes: 7 additions & 5 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@
"lint": "next lint"
},
"dependencies": {
"framer-motion": "^12.23.24",
"next": "15.2.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.2.4"
"react-icons": "^5.5.0"
},
"devDependencies": {
"typescript": "^5",
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9",
"eslint-config-next": "15.2.4",
"@eslint/eslintrc": "^3"
"tailwindcss": "^4",
"typescript": "^5"
}
}
32 changes: 12 additions & 20 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,25 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import type { ReactNode } from "react";
import "./globals.css";
import Nav from "@/components/Nav";
import Footer from "@/components/Footer";

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export const metadata = {
title: "Codedability Store",
description: "A modern store demo showcasing products with search and details.",
} as const;

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
children: ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<body className={`antialiased`}>
<link rel="preconnect" href="https://fakestoreapi.com" />
<Nav />
{children}
<Footer />
</body>
</html>
);
Expand Down
135 changes: 40 additions & 95 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,103 +1,48 @@
import Image from "next/image";
"use client";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { motion } from "framer-motion";
import { fadeIn, slideUp } from "@/lib/motion";

export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="min-h-screen">
<section className="relative mx-auto max-w-7xl w-full px-6 pt-24 pb-24 md:pt-32 md:pb-32 bg-[linear-gradient(to_bottom,rgba(0,0,0,0.75),rgba(0,0,0,0.6))] text-white rounded-3xl mt-6">
<motion.div
initial="hidden"
animate="visible"
variants={fadeIn}
className="mx-auto max-w-3xl text-center"
>
<h1 className="text-4xl md:text-6xl font-extrabold tracking-tight">
Discover Our Store
</h1>
<p className="mt-4 text-base md:text-lg text-white/80">
Browse our curated collection of contemporary pieces crafted for the discerning individual.
</p>
<motion.div variants={slideUp} initial="hidden" animate="visible" className="mt-8 flex justify-center">
<Link href="/products" className="cursor-pointer">
<Button size="lg">View All Products</Button>
</Link>
</motion.div>
</motion.div>
<div className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(60%_40%_at_50%_0%,rgba(255,255,255,.08),transparent)]" />
</section>

<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
<section className="mx-auto max-w-7xl w-full px-6 mt-12 grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="rounded-xl border border-black/10 dark:border-white/10 p-6">
<div className="text-lg font-semibold mb-2">Curated Quality</div>
<p className="text-foreground/70">Every piece is handpicked for craftsmanship and timeless appeal.</p>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
<div className="rounded-xl border border-black/10 dark:border-white/10 p-6">
<div className="text-lg font-semibold mb-2">Modern Aesthetics</div>
<p className="text-foreground/70">Clean lines and neutral tones designed for a refined look.</p>
</div>
<div className="rounded-xl border border-black/10 dark:border-white/10 p-6">
<div className="text-lg font-semibold mb-2">Fast Shipping</div>
<p className="text-foreground/70">Reliable delivery so you can enjoy your purchase sooner.</p>
</div>
</section>
</div>
);
}
29 changes: 29 additions & 0 deletions frontend/src/app/products/[id]/MotionDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";
import { motion } from "framer-motion";
import { fadeIn, slideUp } from "@/lib/motion";

export function Title({ children }: { children: React.ReactNode }) {
return (
<motion.div variants={fadeIn} initial="hidden" animate="visible" className="text-xl font-semibold">
{children}
</motion.div>
);
}

export function ImageWrapper({ children }: { children: React.ReactNode }) {
return (
<motion.div variants={fadeIn} initial="hidden" animate="visible" className="relative aspect-square w-full">
{children}
</motion.div>
);
}

export function Content({ children }: { children: React.ReactNode }) {
return (
<motion.div variants={slideUp} initial="hidden" animate="visible" className="space-y-4">
{children}
</motion.div>
);
}


31 changes: 31 additions & 0 deletions frontend/src/app/products/[id]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";

export default function LoadingProduct() {
return (
<div className="mx-auto max-w-4xl w-full px-4 py-8">
<Card>
<CardHeader>
<Skeleton className="h-7 w-2/3" />
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Skeleton className="aspect-square w-full rounded-md" />
<div className="space-y-4">
<Skeleton className="h-8 w-1/3" />
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-16 w-full" />
<div className="flex gap-3">
<Skeleton className="h-10 w-28" />
<Skeleton className="h-10 w-24" />
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}


65 changes: 65 additions & 0 deletions frontend/src/app/products/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Image from "next/image";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import type { Product } from "@/types/product";
import { Title, ImageWrapper, Content } from "./MotionDetail";

async function fetchProduct(id: string): Promise<Product | null> {
const res = await fetch(`https://fakestoreapi.com/products/${id}`, { cache: "no-store" });
if (!res.ok) {
return null;
}
return res.json();
}

export default async function ProductDetail({ params }: { params: { id: string } }) {
const { id } = params;
const product = await fetchProduct(id);

if (!product) {
return (
<div className="mx-auto max-w-4xl w-full px-4 py-8">
<div className="mb-6">Product not found.</div>
<Link href="/products"><Button>Back to products</Button></Link>
</div>
);
}

return (
<div className="mx-auto max-w-4xl w-full px-4 py-8">
<Card>
<CardHeader>
<Title>{product.title}</Title>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<ImageWrapper>
<Image
src={product.image}
alt={product.title}
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-contain rounded-md bg-black/5 dark:bg-white/10"
/>
</ImageWrapper>
<Content>
<div className="text-2xl font-bold">${product.price.toFixed(2)}</div>
<div className="text-sm text-foreground/80">Category: {product.category}</div>
{product.rating && (
<div className="text-sm text-foreground/80">Rating: {product.rating.rate} ({product.rating.count})</div>
)}
<p className="leading-relaxed">{product.description}</p>
<div className="pt-2 flex gap-3">
<Button className="cursor-pointer">Add to cart</Button>
<Link href="/products" className="cursor-pointer"><Button variant="outline">Back</Button></Link>
</div>
</Content>
</div>
</CardContent>
</Card>
</div>
);
}


25 changes: 25 additions & 0 deletions frontend/src/app/products/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";

export default function Loading() {
return (
<div className="mx-auto max-w-6xl w-full px-4 py-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 9 }).map((_, i) => (
<Card key={i}>
<CardHeader>
<Skeleton className="h-6 w-1/2" />
</CardHeader>
<CardContent>
<Skeleton className="aspect-square w-full rounded-md mb-3" />
<Skeleton className="h-4 w-3/4 mb-2" />
<Skeleton className="h-4 w-1/3" />
</CardContent>
</Card>
))}
</div>
</div>
);
}


Loading