全栈开发

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>
    )
}