Skip to content

Commit 17af983

Browse files
committed
add problem for exercise 5
1 parent c953421 commit 17af983

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+3474
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DATABASE_URL="file:./prisma/data.db"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## Infinite Scrolling with `useFetcher` hook
2+
3+
One more exercise to go! You have done an amazing job so far.
4+
5+
In this one we tackled infinite scrolling using the `useFetcher` hook provided by React Router.
6+
We learned how to load data without navigating away from the current page, and how to use an intersection observer
7+
to detect when the user has scrolled to the bottom of the page.
8+
9+
Now all that is left to do for us it to fetch the data on the singular product page using loaders and we're done!
10+
11+
12+
You're doing great! 🚀
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@import 'tailwindcss';
2+
3+
@theme {
4+
--font-sans:
5+
'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
6+
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
7+
}
8+
9+
html,
10+
body {
11+
@apply bg-white dark:bg-gray-950;
12+
13+
@media (prefers-color-scheme: dark) {
14+
color-scheme: dark;
15+
}
16+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { ShoppingBag } from 'lucide-react'
2+
import { Link } from 'react-router'
3+
4+
export const Footer = () => {
5+
return (
6+
<footer className="border-t border-stone-200 bg-white dark:border-gray-700 dark:bg-gray-900">
7+
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
8+
<div className="grid grid-cols-1 gap-12 md:grid-cols-4">
9+
<div className="col-span-1 md:col-span-2">
10+
<div className="mb-6 flex items-center space-x-3">
11+
<ShoppingBag className="h-7 w-7 text-amber-600 dark:text-amber-500" />
12+
<span className="text-xl font-light tracking-wide text-gray-900 dark:text-white">
13+
EpicStore
14+
</span>
15+
</div>
16+
<p className="max-w-md leading-relaxed font-light text-gray-600 dark:text-gray-400">
17+
Your premier destination for premium epic products. Discover the
18+
perfect product that combines style, comfort, and quality
19+
craftsmanship.
20+
</p>
21+
</div>
22+
<div>
23+
<h3 className="mb-6 text-sm font-medium tracking-wider text-gray-900 uppercase dark:text-white">
24+
Shop
25+
</h3>
26+
<ul className="space-y-4">
27+
<li>
28+
<Link
29+
to="/products"
30+
className="font-light text-gray-600 transition-colors duration-300 hover:text-amber-600 dark:text-gray-400 dark:hover:text-amber-500"
31+
>
32+
All Products
33+
</Link>
34+
</li>
35+
<li>
36+
<Link
37+
to="/products?category=Running"
38+
className="font-light text-gray-600 transition-colors duration-300 hover:text-amber-600 dark:text-gray-400 dark:hover:text-amber-500"
39+
>
40+
Running
41+
</Link>
42+
</li>
43+
<li>
44+
<Link
45+
to="/products?category=Casual"
46+
className="font-light text-gray-600 transition-colors duration-300 hover:text-amber-600 dark:text-gray-400 dark:hover:text-amber-500"
47+
>
48+
Casual
49+
</Link>
50+
</li>
51+
<li>
52+
<Link
53+
to="/products?category=Formal"
54+
className="font-light text-gray-600 transition-colors duration-300 hover:text-amber-600 dark:text-gray-400 dark:hover:text-amber-500"
55+
>
56+
Formal
57+
</Link>
58+
</li>
59+
</ul>
60+
</div>
61+
<div>
62+
<h3 className="mb-6 text-sm font-medium tracking-wider text-gray-900 uppercase dark:text-white">
63+
Company
64+
</h3>
65+
<ul className="space-y-4">
66+
<li>
67+
<Link
68+
to="/about"
69+
className="font-light text-gray-600 transition-colors duration-300 hover:text-amber-600 dark:text-gray-400 dark:hover:text-amber-500"
70+
>
71+
About
72+
</Link>
73+
</li>
74+
<li>
75+
<Link
76+
to="/contact"
77+
className="font-light text-gray-600 transition-colors duration-300 hover:text-amber-600 dark:text-gray-400 dark:hover:text-amber-500"
78+
>
79+
Contact
80+
</Link>
81+
</li>
82+
<li>
83+
<Link
84+
to="/terms-of-service"
85+
className="font-light text-gray-600 transition-colors duration-300 hover:text-amber-600 dark:text-gray-400 dark:hover:text-amber-500"
86+
>
87+
Terms of Service
88+
</Link>
89+
</li>
90+
<li>
91+
<Link
92+
to="/terms-of-use"
93+
className="font-light text-gray-600 transition-colors duration-300 hover:text-amber-600 dark:text-gray-400 dark:hover:text-amber-500"
94+
>
95+
Terms of Use
96+
</Link>
97+
</li>
98+
</ul>
99+
</div>
100+
</div>
101+
<div className="mt-12 border-t border-stone-200 pt-8 dark:border-gray-700">
102+
<p className="text-center font-light text-gray-500 dark:text-gray-400">
103+
© 2025 EpicStore. All rights reserved.
104+
</p>
105+
</div>
106+
</div>
107+
</footer>
108+
)
109+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { ShoppingBag, Search, Menu, X } from 'lucide-react'
2+
import { useState } from 'react'
3+
import { Link, useLocation, Form, useSearchParams } from 'react-router'
4+
5+
export const Header = () => {
6+
const [isMenuOpen, setIsMenuOpen] = useState(false)
7+
const navigation = [
8+
{ name: 'Home', href: '/' },
9+
{ name: 'Products', href: '/products' },
10+
{ name: 'About', href: '/about' },
11+
{ name: 'Contact', href: '/contact' },
12+
]
13+
const [searchParams] = useSearchParams()
14+
const location = useLocation()
15+
const isActive = (href: string) => location.pathname === href
16+
const q = searchParams.get('q') || ''
17+
return (
18+
<nav className="sticky top-0 z-50 border-b border-stone-200 bg-white/95 backdrop-blur-md dark:border-gray-700 dark:bg-gray-900/95">
19+
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
20+
<div className="flex h-20 items-center justify-between">
21+
<div className="flex items-center">
22+
<Link to="/" className="flex items-center space-x-3">
23+
<ShoppingBag className="h-8 w-8 text-amber-600 dark:text-amber-500" />
24+
<span className="text-2xl font-light tracking-wide text-gray-900 dark:text-white">
25+
EpicStore
26+
</span>
27+
</Link>
28+
</div>
29+
30+
<div className="mx-8 hidden max-w-lg flex-1 md:block">
31+
<div className="relative">
32+
<Form method="GET" action="/products" className="relative">
33+
<Search className="absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 transform text-gray-400" />
34+
<input
35+
type="text"
36+
defaultValue={q}
37+
name="q"
38+
onKeyDown={(e) => {
39+
if (e.key === 'Enter') {
40+
const form = e.currentTarget.form
41+
if (form) {
42+
form.submit()
43+
}
44+
}
45+
}}
46+
placeholder="Search for products..."
47+
className="w-full rounded-full border border-gray-200 bg-gray-50 py-3 pr-24 pl-10 text-gray-900 focus:border-transparent focus:ring-2 focus:ring-amber-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
48+
/>
49+
<button
50+
type="submit"
51+
className="absolute top-1/2 right-2 flex -translate-y-1/2 items-center space-x-1 rounded-full bg-amber-600 px-4 py-2 text-sm font-medium text-white transition-colors duration-300 hover:bg-amber-700 dark:bg-amber-500 dark:hover:bg-amber-600"
52+
>
53+
<Search className="h-4 w-4" />
54+
<span>Search</span>
55+
</button>
56+
</Form>
57+
</div>
58+
</div>
59+
60+
<div className="hidden md:block">
61+
<div className="flex items-center space-x-8">
62+
{navigation.map((item) => (
63+
<Link
64+
key={item.name}
65+
to={item.href}
66+
className={`text-sm font-medium tracking-wide transition-colors duration-300 ${
67+
isActive(item.href)
68+
? 'text-amber-600 dark:text-amber-500'
69+
: 'text-gray-700 hover:text-amber-600 dark:text-gray-300 dark:hover:text-amber-500'
70+
}`}
71+
>
72+
{item.name}
73+
</Link>
74+
))}
75+
<div className="flex items-center space-x-4">
76+
{/* <button className="p-2 text-gray-700 dark:text-gray-300 hover:text-amber-600 dark:hover:text-amber-500 transition-colors duration-300">
77+
<Heart className="w-5 h-5" />
78+
</button>
79+
<button className="p-2 text-gray-700 dark:text-gray-300 hover:text-amber-600 dark:hover:text-amber-500 transition-colors duration-300">
80+
<User className="w-5 h-5" />
81+
</button>
82+
<ShoppingCart /> */}
83+
</div>
84+
</div>
85+
</div>
86+
87+
<div className="flex items-center space-x-4 md:hidden">
88+
{/* <ShoppingCart /> */}
89+
<button
90+
onClick={() => setIsMenuOpen(!isMenuOpen)}
91+
className="p-2 text-gray-700 hover:text-amber-600 dark:text-gray-300 dark:hover:text-amber-500"
92+
>
93+
{isMenuOpen ? (
94+
<X className="h-6 w-6" />
95+
) : (
96+
<Menu className="h-6 w-6" />
97+
)}
98+
</button>
99+
</div>
100+
</div>
101+
</div>
102+
103+
{isMenuOpen && (
104+
<div className="md:hidden">
105+
<div className="space-y-4 border-t border-stone-200 bg-white px-4 pt-2 pb-6 dark:border-gray-700 dark:bg-gray-900">
106+
<div className="relative mb-4">
107+
<Search className="absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 transform text-gray-400" />
108+
<input
109+
type="text"
110+
placeholder="Search for products..."
111+
className="w-full rounded-full border border-gray-200 bg-gray-50 py-3 pr-4 pl-10 text-gray-900 focus:border-transparent focus:ring-2 focus:ring-amber-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
112+
/>
113+
</div>
114+
{navigation.map((item) => (
115+
<Link
116+
key={item.name}
117+
to={item.href}
118+
onClick={() => setIsMenuOpen(false)}
119+
className={`block text-base font-medium tracking-wide transition-colors duration-300 ${
120+
isActive(item.href)
121+
? 'text-amber-600 dark:text-amber-500'
122+
: 'text-gray-700 hover:text-amber-600 dark:text-gray-300 dark:hover:text-amber-500'
123+
}`}
124+
>
125+
{item.name}
126+
</Link>
127+
))}
128+
</div>
129+
</div>
130+
)}
131+
</nav>
132+
)
133+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Heart, Star } from 'lucide-react'
2+
import { Link } from 'react-router'
3+
import { type ProductCardInfo } from '#app/domain/products.server.js'
4+
5+
export function ProductCard({ product }: { product: ProductCardInfo }) {
6+
return (
7+
<div
8+
key={product.id}
9+
className="group overflow-hidden rounded-lg bg-white transition-all duration-300 hover:scale-105 hover:transform hover:shadow-xl dark:bg-gray-800"
10+
>
11+
<div className="relative overflow-hidden">
12+
<Link to={`/products/${product.id}`}>
13+
<img
14+
src={product.imageUrl}
15+
alt={product.name}
16+
className="h-64 w-full object-cover transition-transform duration-500 group-hover:scale-110"
17+
/>
18+
</Link>
19+
<button className="absolute top-4 right-4 rounded-full bg-white p-2 shadow-lg transition-colors duration-200 hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800">
20+
<Heart className="h-4 w-4 text-gray-600 dark:text-gray-400" />
21+
</button>
22+
<div className="absolute top-4 left-4 rounded-full bg-white px-3 py-1 dark:bg-gray-900">
23+
<div className="flex items-center space-x-1">
24+
<Star className="h-4 w-4 fill-current text-amber-500" />
25+
<span className="text-sm font-medium text-gray-900 dark:text-white">
26+
{product.reviewScore.toFixed(1)}
27+
</span>
28+
</div>
29+
</div>
30+
</div>
31+
<div className="p-6">
32+
<div className="mb-2 text-sm font-medium text-amber-600 dark:text-amber-500">
33+
{product.brand.name}
34+
</div>
35+
<Link to={`/products/${product.id}`}>
36+
<h3 className="mb-2 text-lg font-medium text-gray-900 transition-colors duration-300 group-hover:text-amber-600 dark:text-white dark:group-hover:text-amber-500">
37+
{product.name}
38+
</h3>
39+
</Link>
40+
<p className="mb-4 line-clamp-2 text-sm text-gray-600 dark:text-gray-300">
41+
{product.description}
42+
</p>
43+
<div className="flex items-center justify-between">
44+
<span className="text-xl font-bold text-gray-900 dark:text-white">
45+
${product.price}
46+
</span>
47+
<span className="text-sm text-gray-500 dark:text-gray-400">
48+
{product._count.reviews} reviews
49+
</span>
50+
</div>
51+
</div>
52+
</div>
53+
)
54+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { PrismaBetterSQLite3 } from '@prisma/adapter-better-sqlite3';
2+
import { PrismaClient } from "./generated/prisma/client";
3+
4+
const adapter = new PrismaBetterSQLite3({
5+
url: process.env.DATABASE_URL,
6+
});
7+
const prisma = new PrismaClient({ adapter });
8+
await prisma.$connect();
9+
export const db = prisma;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { db } from "#app/db.server.js"
2+
3+
export const getAllBrands = async () => {
4+
const brands = await db.brand.findMany({
5+
orderBy: { name: "asc" },
6+
select: { id: true, name: true },
7+
});
8+
return { brands };
9+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { db } from "#app/db.server.js";
2+
3+
export const getAllCategories = async ({ perPage }: { perPage?: number } = {}) => {
4+
const categories = await db.category.findMany({
5+
orderBy: { name: "asc" },
6+
select: { id: true, name: true, imageUrl: true },
7+
take: perPage,
8+
});
9+
return { categories };
10+
}
11+
12+
export type Category = Awaited<ReturnType<typeof getAllCategories>>["categories"][number];

0 commit comments

Comments
 (0)