文章
Next js components
1、顶部导航栏
Navbar.tsx
import Link from "next/link";
import Image from "next/image";
import SearchBar from "@/components/SearchBar";
import {Bell, Home, ShoppingCart} from "lucide-react";
export default function Navbar () {
return (
// w-full 填满容器 元素左右贴边对齐,不加w-full则是根据内容左右两边对齐,一般导航栏、页眉、页脚用的多
<nav className={"w-full flex items-center justify-between border-b border-gray-200 pb-4"}>
{/*LEFT*/}
{/*LOGO*/}
<Link href="/" className={"flex items-center"}>
<Image src={"/logo.png"} alt={"TrendLama"} width={36} height={36} className={"w-6 h-6 md:w-9 md:h-9"}/>
{/*设置更宽的字间距*/}
<p className={"hidden md:block text-md font-medium tracking-wider"}>TRENDLAMA.</p>
</Link>
{/*RIGHT*/}
<div className={"flex items-center gap-6"}>
<SearchBar />
<Link href={"/"}>
<Home className={"w-4 h-4 text-gray-600"} />
</Link>
<Bell className={"w-4 h-4 text-gray-600"}/>
<ShoppingCart className={"w-4 h-4 text-gray-600"}/>
<Link href={"/login"}>Sign in</Link>
</div>
</nav>
)
}
SearchBar.tsx
import {Search} from "lucide-react";
export default function SearchBar() {
return (
// 因为 sm:block 和 flex 同时存在时,flex 会覆盖 block,导致 sm:block 完全失效 —— 写了等于白写,还误导阅读者
<div className={"hidden sm:flex items-center gap-2 rounded-md ring-1 ring-gray-200 px-2 py-1 shadow-md"}>
{/*icon*/}
<Search className={"w-4 h-4 text-gray-500"} />
{/*input*/}
<input type="text" id={"search"} placeholder={"Search..."} className={"text-sm outline-0"} />
</div>
)
}
2、Footer 页脚
import Image from "next/image";
import Link from "next/link";
export default function Footer () {
return (
<div className="mt-16 flex flex-col items-center gap-8 md:flex-row md:items-start md:justify-between md:gap-0 bg-gray-800 p-8 rounded-lg">
<div className="flex flex-col gap-4 items-center md:items-start">
<Link href="/" className="flex items-center">
<Image src="/logo.png" alt="TrendLama" width={36} height={36} />
<p className="hidden md:block text-md font-medium tracking-wider text-white">
TRENDLAMA.
</p>
</Link>
<p className="text-sm text-gray-400">© 2025 Trendlama.</p>
<p className="text-sm text-gray-400">All rights reserved.</p>
</div>
<div className="flex flex-col gap-4 text-sm text-gray-400 items-center md:items-start">
<p className="text-sm text-amber-50">Links</p>
<Link href="/">Homepage</Link>
<Link href="/">Contact</Link>
<Link href="/">Terms of Service</Link>
<Link href="/">Privacy Policy</Link>
</div>
<div className="flex flex-col gap-4 text-sm text-gray-400 items-center md:items-start">
<p className="text-sm text-amber-50">Links</p>
<Link href="/">All Products</Link>
<Link href="/">New Arrivals</Link>
<Link href="/">Best Sellers</Link>
<Link href="/">Sale</Link>
</div>
<div className="flex flex-col gap-4 text-sm text-gray-400 items-center md:items-start">
<p className="text-sm text-amber-50">Links</p>
<Link href="/">About</Link>
<Link href="/">Contact</Link>
<Link href="/">Blog</Link>
<Link href="/">Affiliate Program</Link>
</div>
</div>
)
}
3、分类导航与交互
"use client"
import {
Footprints,
Glasses,
Briefcase,
Shirt,
ShoppingBasket,
Hand,
Venus,
} from "lucide-react";
import {usePathname, useRouter, useSearchParams} from "next/navigation";
const categories = [
{
name: "All",
icon: <ShoppingBasket className="w-4 h-4"/>,
slug: "all",
},
{
name: "T-shirts",
icon: <Shirt className="w-4 h-4"/>,
slug: "t-shirts",
},
{
name: "Shoes",
icon: <Footprints className="w-4 h-4"/>,
slug: "shoes",
},
{
name: "Accessories",
icon: <Glasses className="w-4 h-4"/>,
slug: "accessories",
},
{
name: "Bags",
icon: <Briefcase className="w-4 h-4"/>,
slug: "bags",
},
{
name: "Dresses",
icon: <Venus className="w-4 h-4"/>,
slug: "dresses",
},
{
name: "Jackets",
icon: <Shirt className="w-4 h-4"/>,
slug: "jackets",
},
{
name: "Gloves",
icon: <Hand className="w-4 h-4"/>,
slug: "gloves",
},
];
export function Categories() {
const searchParams = useSearchParams();
const router = useRouter();
// 获取URL中的PATH路径
const pathname = usePathname();
const selectedCategory = searchParams.get("category") || "all";
console.log(selectedCategory);
const handleChange = (value: string | null) => {
// 根据当前页面的searchParams创建URLSearchParams
const params = new URLSearchParams(searchParams);
// 修改当前searchParams中category参数,其他参数保持不变
params.set("category", value || "all");
// router.push(`/?category=${value}`);
// 使用searchParams生成URL查询链接
router.push(`${pathname}?${params.toString()}`, {scroll: false});
}
return (
<div
className={"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-2 bg-gray-100 p-2 rounded-lg mb-4 text-sm"}>
{categories.map((category) => (
<div
key={category.name}
className={`flex items-center justify-center gap-2 cursor-pointer px-2 py-1 rounded-md
${category.slug === selectedCategory ? "bg-white" : "text-gray-500"}`}
onClick={() => handleChange(category.slug)}
>
{category.icon}
{category.name}
</div>
))}
</div>
)
}
4、ProductCard
"use client"
import {ProductType} from "@/types";
import Image from "next/image";
import Link from "next/link";
import {ShoppingCart} from "lucide-react";
import {useState} from "react";
export default function ProductCard({product}: { product: ProductType }) {
// 会提交product到购物车 需要存储 当前product选择的color、size参数
const [productTypes, setProductTypes] = useState({ // 这里商品状态存储的是一个对象,默认使用第一个size以及第一个color
color: product.colors[0],
size: product.sizes[0],
});
// 上面定义了当前商品的color、size的默认值
// 下面定义 修改商品的其中一个参数
const handleProductType = ({type, value}: { type: "color" | "size"; value: string }) => {
// 上面定义当前商品有 两个属性 color 和 size
// 使用prev传参 意思是保留之前的 参数对应的值 然后修改 当前type对应的值
// 比如选择一个尺寸的时候 再去选一个颜色,正确的是应该尺寸会发生改变 ,并记录当前的颜色,而不是尺寸变为默认值
setProductTypes((prev) => // prev 只是你给函数参数起的一个有意义的名字,React 会自动将当前状态值传递给它。
// prev 确实是"变化之前的值",但它是"当前最新的变化之前的值" [type]: value 是最新的变化
// ...prev - 展开操作符 相当于:{ color: prev.color, size: prev.size }
// [type]: value - 计算属性名,这里的方括号[]不是数组,而是计算属性名的语法
// [type]: value 意思是:使用变量type的值作为属性名
({...prev, [type]: value}));
}
return (
<div className={"shadow-lg rounded-lg overflow-hidden"}>
{/*IMAGE*/}
<Link href={`/products/${product.id}`}>
<div className="relative aspect-[2/3]">
{/*根据选择的颜色来显示对应的图片*/}
<Image src={product.images[productTypes.color]} alt={product.name} fill
className={"object-cover hover:scale-105 transition-all duration-300"}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
</Link>
{/*PRODUCT DETAIL*/}
<div className="flex flex-col gap-4 p-4">
<h1 className={"font-medium"}>{product.name}</h1>
<p className={"text-sm text-gray-500"}>{product.shortDescription}</p>
{/*PRODUCT TYPES 左右结构 左边尺寸 右边颜色*/}
<div className="flex items-center gap-4 text-xs">
{/*SIZES 上下结构 上面文字 下面尺寸选择*/}
<div className="flex flex-col gap-1">
<span className={"text-gray-500"}>Size</span>
{/*下拉列表 遍历商品尺寸数组*/}
<select
name="size"
id={`${product.id}-size`}
className={"ring ring-gray-300 rounded-md px-2 py-1"}
// 点击size下拉框改变下拉框的值的时候 触发handleProductType函数 并传参
onChange={(e) => handleProductType({type: "size", value: e.target.value})}
>
{product.sizes.map((size) => (
<option key={size} value={size}>{size.toUpperCase()}</option>
))}
</select>
</div>
{/*COLORS 上下结构 上面文字 下面颜色选择*/}
<div className="flex flex-col gap-1">
<span className={"text-gray-500"}>Color</span>
{/*横向展示所有的颜色*/}
<div className={"flex items-center gap-2"}>
{product.colors.map((color) => (
// 点击商品的颜色圆圈 触发handleProductType函数并传参
<div key={color} className={`cursor-pointer border-1 ${productTypes.color === color ? "border-gray-400" : "border-gray-200"} rounded-full p-[1.2px]`}
onClick={() => handleProductType({type: "color", value: color})}>
{/*画个带颜色的实心圆*/}
<div
className={"w-[14px] h-[14px] rounded-full"}
style={{backgroundColor: color}}
/>
</div>
))}
</div>
</div>
</div>
{/*PRICE AND ADD TO CART BUTTON 左右结构*/}
<div className={"flex items-center justify-between"}>
{/*PRICE*/}
<p className={"font-medium"}>${product.price.toFixed(2)}</p>
{/*Add to Cart Button 左右结构*/}
<button
className={"ring-1 ring-gray-200 shadow-lg rounded-md px-2 py-1 text-sm cursor-pointer hover:text-white hover:bg-black transition-all duration-300 flex items-center gap-2"}>
<ShoppingCart className={"w-4 h-4"}/>
Add to Cart
</button>
</div>
</div>
</div>
)
}
5、购物车右上角显示数字
"use client"
import Link from "next/link";
import {ShoppingCart} from "lucide-react";
export default function ShoppingCartIcon() {
return (
<Link href="/cart" className={"relative"}>
<ShoppingCart className={"w-4 h-4 text-gray-600"}/>
<span className={"absolute -top-3 -right-3 bg-amber-400 text-gray-600 rounded-full w-4 h-4 flex items-center justify-center text-xs font-medium"}>20</span>
</Link>
)
}