login , register api / category provider api

This commit is contained in:
haniyeroozmand
2026-03-30 10:06:30 +03:30
parent 4d6e9a6857
commit d9902b7d99
7 changed files with 322 additions and 128 deletions

View File

@@ -5,6 +5,7 @@ import Footer from "@/components/footer";
import localFont from 'next/font/local'; import localFont from 'next/font/local';
import { CartProvider } from "@/components/context/cartcontext"; import { CartProvider } from "@/components/context/cartcontext";
import { categoryService } from "@/public/src/services/categories/api"; import { categoryService } from "@/public/src/services/categories/api";
import { CategoryProvider } from "@/components/context/categoryprovider";
const Yekanbakh = localFont({ const Yekanbakh = localFont({
src: [ src: [
@@ -35,8 +36,12 @@ export default async function RootLayout({
<html lang="fa" dir="rtl" className={Yekanbakh.variable}> <html lang="fa" dir="rtl" className={Yekanbakh.variable}>
<body> <body>
<CartProvider> <CartProvider>
<Header categories={categories}/> <CategoryProvider>
<Header />
{children} {children}
</CategoryProvider>
</CartProvider> </CartProvider>
<Footer/> <Footer/>

View File

@@ -1,13 +1,20 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { Headphones, FileText, Truck, ShieldCheck, Square, MoreVertical, Circle, Target, CookingPot, Settings, CircleDashed, Disc, Hexagon, Droplets, Wrench, Minus, MessageCircleCheckIcon, } from "lucide-react"; import {
Headphones, FileText, Truck, ShieldCheck, CircleDashed,
Disc,
Hexagon,
Settings,
Wrench,
Droplets, Square, MoreVertical, Circle, Target, CookingPot, Minus, MessageCircleCheckIcon,
} from "lucide-react";
import ProductCard from "@/components/productcard"; import ProductCard from "@/components/productcard";
import ArticleCard from "@/components/articlecard"; import ArticleCard from "@/components/articlecard";
import FAQItem from "@/components/faq"; import FAQItem from "@/components/faq";
import { products } from "@/lib/data"; import { products } from "@/lib/data";
import { articles } from "@/lib/data"; import { articles } from "@/lib/data";
import Link from "next/link"; import Link from "next/link";
import { useCategories } from "@/components/context/categoryprovider";
const features = [ const features = [
@@ -60,28 +67,35 @@ export default function Home() {
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
const brands = ["NTN", "KOYO", "NACHI", "TIMKEN", "FAG", "SKF"]; const brands = ["NTN", "KOYO", "NACHI", "TIMKEN", "FAG", "SKF"];
const latestArticles = articles.slice(-4); const latestArticles = articles.slice(-4);
const { rootCategories } = useCategories();
const uniqueCategories = Array.from(new Set(products.map((p) => p.category))); // const uniqueCategories = Array.from(new Set(products.map((p) => p.category)));
const getCategoryIcon = (categoryName: string) => { // const getCategoryIcon = (categoryName: string) => {
switch (categoryName) { // switch (categoryName) {
case "شیار عمیق": // case "شیار عمیق":
return CircleDashed; // return CircleDashed;
case "مخروطی": // case "مخروطی":
return Disc; // return Disc;
case "شبکه ای": // case "شبکه ای":
return Hexagon; // return Hexagon;
case "سوزنی": // case "سوزنی":
return Settings; // return Settings;
case "یاتاقان": // case "یاتاقان":
return Wrench; // return Wrench;
case "گریس و روانکار": // case "گریس و روانکار":
return Droplets; // return Droplets;
default: // default:
return Settings; // آیکون پیش‌فرض // return Settings; // آیکون پیش‌فرض
} // }
}; // };
const categoryIcons = [
CircleDashed,
Disc,
Hexagon,
Wrench,
Droplets
];
return ( return (
<main className=" bg-gray-50"> <main className=" bg-gray-50">
@@ -225,26 +239,26 @@ export default function Home() {
{/* cards */} {/* cards */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6"> <div className="grid justify-center grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-6">
{uniqueCategories.map((category, index) => {
const Icon = getCategoryIcon(category);
// فقط فاصله‌ها را به خط تیره تبدیل می‌کنیم
const categorySlug = category.replace(/\s+/g, "-");
{rootCategories.map((cat, index) => {
const Icon = categoryIcons[index % categoryIcons.length];
return ( return (
<Link <Link
href={`/category/${categorySlug}`} key={cat.id}
key={index} href={`/category/${cat.slug}`}
className="bg-white border border-gray-200 rounded-xl shadow-sm hover:shadow-md hover:border-blue-400 transition-all duration-300 flex flex-col items-center justify-center py-8 cursor-pointer group" className="bg-white border gap-2 border-gray-200 rounded-xl shadow-sm hover:shadow-md hover:border-blue-400 transition-all duration-300 flex flex-col items-center justify-center py-8 cursor-pointer group"
> >
<Icon size={34} className="text-gray-400 mb-3 group-hover:text-blue-500 transition-colors" /> <Icon size={34} strokeWidth={1} className="text-gray-400 mb-3 group-hover:text-blue-500 transition-colors" />
<p className="text-sm font-medium text-gray-700 group-hover:text-blue-600"> <span className="text-sm font-medium text-gray-700 group-hover:text-blue-600">{cat.name}</span>
{category}
</p>
</Link> </Link>
); );
})} })}
</div> </div>
</div> </div>
</section> </section>

View File

@@ -0,0 +1,34 @@
"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { getAllCategories } from "@/public/src/services/categories/store";
import { Category } from "@/public/src/types/categories";
interface CategoryContextType {
categories: Category[];
rootCategories: Category[];
}
const CategoryContext = createContext<CategoryContextType>({
categories: [],
rootCategories: [],
});
export function CategoryProvider({ children }: { children: React.ReactNode }) {
const [categories, setCategories] = useState<Category[]>([]);
useEffect(() => {
getAllCategories().then((data) => setCategories(data));
}, []);
const rootCategories = categories.filter((c) => !c.parent);
return (
<CategoryContext.Provider value={{ categories, rootCategories }}>
{children}
</CategoryContext.Provider>
);
}
export function useCategories() {
return useContext(CategoryContext);
}

View File

@@ -3,13 +3,13 @@ import React, { useState, useEffect, useRef } from 'react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
import { registerUser } from '@/public/src/services/auth/api';
import { Category } from '@/public/src/types/categories';
import { ShoppingCart, Trash2, Search, X, ChevronDown } from "lucide-react"; import { ShoppingCart, Trash2, Search, X, ChevronDown } from "lucide-react";
import { useCart } from './context/cartcontext'; import { useCart } from './context/cartcontext';
import { products } from '@/lib/data'; import { products } from '@/lib/data';
import '@/public/src/css/header.css'; import '@/public/src/css/header.css';
import { registerUser } from '@/public/src/services/auth/api';
import { loginUser } from '@/public/src/services/auth/api';
import { useCategories } from './context/categoryprovider';
const topBarLinks = [ const topBarLinks = [
{ label: "بخش صنعتی", href: "/" }, { label: "بخش صنعتی", href: "/" },
@@ -25,7 +25,7 @@ const mainNavLinks = [
]; ];
export function Header({ categories }: { categories: Category[] }) { export function Header() {
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const pathname = usePathname(); const pathname = usePathname();
const { cart, removeFromCart } = useCart(); const { cart, removeFromCart } = useCart();
@@ -34,54 +34,143 @@ export function Header({ categories }: { categories: Category[] }) {
const [filteredProducts, setFilteredProducts] = useState<any[]>([]); const [filteredProducts, setFilteredProducts] = useState<any[]>([]);
const searchRef = useRef<HTMLDivElement>(null); const searchRef = useRef<HTMLDivElement>(null);
const router = useRouter(); const router = useRouter();
const safeCategories = categories || [];
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [formType, setFormType] = useState("mobile"); const [formType, setFormType] = useState("mobile");
const [activeTab, setActiveTab] = useState("login"); const [activeTab, setActiveTab] = useState("login");
const [registerForm, setRegisterForm] = useState({ const [registerError, setRegisterError] = useState("");
phone: "", const [loginError, setLoginError] = useState("");
const { rootCategories } = useCategories();
const [loginForm, setLoginForm] = useState({
username: "", username: "",
firstName: "",
lastName: "",
password: "", password: "",
confirmPassword: "",
}); });
const handleRegisterChange = (e: any) => { const [registerForm, setRegisterForm] = useState({
const { name, value } = e.target; phone: "",
fullName: "",
username: "",
password: "",
});
setRegisterForm((prev) => ({ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
...prev, setRegisterForm({
[name]: value, ...registerForm,
})); [e.target.name]: e.target.value,
});
}; };
const handleRegister = async () => { const handleRegister = async () => {
setRegisterError("");
const phone = registerForm.phone.trim();
const username = registerForm.username.trim();
const password = registerForm.password.trim();
const fullName = registerForm.fullName.trim();
// ✅ بررسی خالی بودن فیلدها
if (!phone || !username || !password || !fullName) {
setRegisterError("لطفاً فیلدها را پر کنید");
return;
}
try { try {
const payload = { const payload = {
phone: registerForm.phone, phone,
username: registerForm.username, username,
password: registerForm.password, password,
fullName,
}; };
const res = await registerUser(payload); const res = await registerUser(payload);
console.log("ثبت نام موفق", res); localStorage.setItem("accessToken", res.data.accessToken);
localStorage.setItem("refreshToken", res.data.refreshToken);
} catch (err) { setIsOpen(false);
console.log("خطا:", err); router.push("/dashboard");
} catch (error: any) {
const message = error?.message?.toLowerCase() || "";
const usernameDuplicate = message.includes("username");
const phoneDuplicate = message.includes("phone");
if (usernameDuplicate && phoneDuplicate) {
setRegisterError("نام کاربری و شماره موبایل قبلاً ثبت شده‌اند");
}
else if (usernameDuplicate) {
setRegisterError("این نام کاربری قبلاً ثبت شده است");
}
else if (phoneDuplicate) {
setRegisterError("این شماره موبایل قبلاً ثبت شده است");
}
else {
setRegisterError("خطا در ثبت نام");
}
} }
}; };
const rootCategories = safeCategories.filter(cat => cat.parent === null); const handleLogin = async () => {
setLoginError("");
const username = loginForm.username.trim();
const password = loginForm.password.trim();
if (!username || !password) {
setLoginError("لطفاً نام کاربری و رمز عبور را وارد کنید");
return;
}
try {
const res = await loginUser({ username, password });
localStorage.setItem("accessToken", res.data.accessToken);
localStorage.setItem("refreshToken", res.data.refreshToken);
setIsOpen(false);
router.push("/dashboard");
} catch (error: any) {
const msg = error?.message?.toLowerCase() || "";
console.log("LOGIN ERROR RAW:", msg); // این خیلی مهمه
// 1) اول بررسی کنیم که نام کاربری وجود ندارد
if (
msg.includes("not found") ||
msg.includes("user") && msg.includes("not") ||
msg.includes("username") && msg.includes("not")
) {
setLoginError("این نام کاربری وجود ندارد");
return;
}
// 2) سپس رمز عبور اشتباه
if (
msg.includes("password") ||
msg.includes("invalid") ||
msg.includes("incorrect")
) {
setLoginError("رمز عبور اشتباه است");
return;
}
// 3) خطای عمومی
setLoginError("خطا در ورود. لطفاً دوباره تلاش کنید.");
}
};
const parsePrice = (priceStr?: string | null) => { const parsePrice = (priceStr?: string | null) => {
if (!priceStr) return 0; if (!priceStr) return 0;
return Number(priceStr.toString().replace(/,/g, '')); return Number(priceStr.toString().replace(/,/g, ''));
}; };
const totalPrice = cart.reduce((total, item) => total + (parsePrice(item.price) * item.quantity), 0); const totalPrice = cart.reduce((total, item) => total + (parsePrice(item.price) * item.quantity), 0);
useEffect(() => { useEffect(() => {
@@ -422,7 +511,7 @@ export function Header({ categories }: { categories: Category[] }) {
<button <button
onClick={() => setActiveTab("register")} onClick={() => setActiveTab("register")}
className={`flex-1 py-2 text-sm rounded-lg transition ${activeTab === "register" className={`flex-1 cursor-pointer py-2 text-sm rounded-lg transition ${activeTab === "register"
? "bg-white shadow text-[#1A2332] font-semibold" ? "bg-white shadow text-[#1A2332] font-semibold"
: "text-gray-500" : "text-gray-500"
}`} }`}
@@ -467,19 +556,38 @@ export function Header({ categories }: { categories: Category[] }) {
<input <input
type="text" type="text"
value={loginForm.username}
onChange={(e) =>
setLoginForm((prev) => ({
...prev,
username: e.target.value,
}))
}
placeholder="نام کاربری" placeholder="نام کاربری"
className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]" className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]"
/> />
<input <input
type="password" type="password"
value={loginForm.password}
onChange={(e) =>
setLoginForm((prev) => ({
...prev,
password: e.target.value,
}))
}
placeholder="رمز عبور" placeholder="رمز عبور"
className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]" className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]"
/> />
<button className="w-full cursor-pointer py-3.5 bg-[#ffb900] hover:bg-[#e5a600] text-[#1A2332] font-semibold rounded-2xl text-sm"> <button onClick={handleLogin} className="w-full cursor-pointer py-3.5 bg-[#ffb900] hover:bg-[#e5a600] text-[#1A2332] font-semibold rounded-2xl text-sm">
ورود ورود
</button> </button>
{loginError && (
<div className="text-red-500 text-sm text-center mt-2">
{loginError}
</div>
)}
<button <button
onClick={() => setFormType("mobile")} onClick={() => setFormType("mobile")}
@@ -495,40 +603,30 @@ export function Header({ categories }: { categories: Category[] }) {
)} )}
{/* ---------------- REGISTER ---------------- */} {/* ---------------- REGISTER ---------------- */}
{activeTab === "register" && ( {activeTab === "register" && (
<div className="w-full space-y-4"> <div className="w-full space-y-4">
<input <input
name="phone" name="phone"
value={registerForm.phone} value={registerForm.phone}
onChange={handleRegisterChange} onChange={handleChange}
type="tel" type="tel"
placeholder="شماره موبایل" placeholder="شماره موبایل"
className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]" className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]"
/> />
<input <input
name="firstName" name="fullName"
value={registerForm.firstName} value={registerForm.fullName}
onChange={handleRegisterChange} onChange={handleChange}
type="text" type="text"
placeholder="نام" placeholder="نام"
className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]" className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]"
/> />
<input
name="lastName"
value={registerForm.lastName}
onChange={handleRegisterChange}
type="text"
placeholder="نام خانوادگی"
className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]"
/>
<input <input
name="username" name="username"
value={registerForm.username} value={registerForm.username}
onChange={handleRegisterChange} onChange={handleChange}
type="text" type="text"
placeholder="نام کاربری" placeholder="نام کاربری"
className='w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]' className='w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]'
@@ -536,20 +634,19 @@ export function Header({ categories }: { categories: Category[] }) {
<input <input
name="password" name="password"
value={registerForm.password} value={registerForm.password}
onChange={handleRegisterChange} onChange={handleChange} type="password"
type="password"
placeholder="رمز عبور" placeholder="رمز عبور"
className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]" className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]"
/> />
<input {/* <input
name="confirmPassword" name="confirmPassword"
value={registerForm.confirmPassword} value={registerForm.confirmPassword}
onChange={handleRegisterChange} onChange={handleChange}
type="password" type="password"
placeholder="تکرار رمز عبور" placeholder="تکرار رمز عبور"
className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]" className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]"
/> /> */}
<button <button
onClick={handleRegister} onClick={handleRegister}
@@ -558,6 +655,11 @@ export function Header({ categories }: { categories: Category[] }) {
ثبت نام ثبت نام
</button> </button>
{registerError && (
<div className="text-red-500 text-sm text-center mt-2">
{registerError}
</div>
)}
</div> </div>
)} )}
@@ -590,21 +692,13 @@ export function Header({ categories }: { categories: Category[] }) {
<div className="absolute top-[calc(100%+16px)] right-0 w-56 bg-white border border-gray-200 rounded-xl shadow-lg p-2 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 flex flex-col gap-1 z-50"> <div className="absolute top-[calc(100%+16px)] right-0 w-56 bg-white border border-gray-200 rounded-xl shadow-lg p-2 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 flex flex-col gap-1 z-50">
{rootCategories.length > 0 ? (
rootCategories.map((category) => ( {rootCategories.map(cat => (
<Link <Link className='className="block w-full text-right px-3 py-2 text-xs font-medium text-gray-600 hover:bg-gray-100 hover:text-blue-600 rounded-md transition-colors'
key={category.id} href={`/category/${cat.slug}`} key={cat.id}>
href={`/category/${category.slug}`} {cat.name}
className="block w-full text-right px-3 py-2 text-xs font-medium text-gray-600 hover:bg-gray-100 hover:text-blue-600 rounded-md transition-colors"
>
{category.name}
</Link> </Link>
)) ))}
) : (
<div className="text-center text-xs text-gray-500 py-2">
در حال بارگذاری...
</div>
)}
</div> </div>
</div> </div>

View File

@@ -1,13 +1,55 @@
import { API_BASE_URL } from "../config"; import { API_BASE_URL } from "../config";
export async function registerUser(data: any) { export interface RegisterPayload {
const res = await fetch(`${API_BASE_URL}/auth/register/password`, { phone: string;
username: string;
password: string;
fullName: string;
}
export interface LoginPayload {
username: string;
password: string;
}
export async function registerUser(data: RegisterPayload) {
const response = await fetch(`${API_BASE_URL}/auth/register/password`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
console.log( API_BASE_URL);
return res.json(); const result = await response.json();
}
if (!response.ok) {
throw new Error(result.message || "Register failed");
}
return result;
}
export async function loginUser(data: LoginPayload) {
const response = await fetch(
`${API_BASE_URL}/auth/login/password`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
}
);
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || "Login failed");
}
return result;
}

View File

@@ -1,32 +1,18 @@
// src/services/categories/categories.service.ts import { API_BASE_URL } from "../config";
import { ApiResponse } from '../../types/global'; import { Category } from "../../types/categories";
import { Category } from '../../types/categories';
const BASE_URL = 'https://parsshop-back.mugit.ir';
export const categoryService = { export const categoryService = {
/**
* دریافت تمام دسته‌بندی‌ها با استفاده از کش Next.js
*/
getCategories: async (): Promise<Category[]> => { getCategories: async (): Promise<Category[]> => {
try { try {
const response = await fetch(`${BASE_URL}/api/categories`, { const response = await fetch(`${API_BASE_URL}/categories`, {
// بهینه‌ترین حالت: داده‌ها تا ۱ ساعت کش می‌شوند (3600 ثانیه)
// اگر تغییرات شما لحظه‌ای است، این عدد را کمتر کنید یا از تگ‌های On-Demand Revalidation استفاده کنید
next: { revalidate: 3600 }, next: { revalidate: 3600 },
}); });
if (!response.ok) { const json = await response.json();
throw new Error(`HTTP error! status: ${response.status}`); return json.data || [];
}
const result: ApiResponse<Category[]> = await response.json();
// ما فقط به آرایه data نیاز داریم
return result.data;
} catch (error) { } catch (error) {
console.error('Error fetching categories:', error); console.error("Category API Error:", error);
return []; // در صورت خطا، یک آرایه خالی برمی‌گردانیم تا صفحه کرش نکند return [];
} }
}, },
}; };

View File

@@ -0,0 +1,19 @@
import { Category } from "../../types/categories";
import { categoryService } from "./api";
let cachedCategories: Category[] | null = null;
export async function getAllCategories(): Promise<Category[]> {
if (cachedCategories) {
return cachedCategories;
}
const categories = await categoryService.getCategories();
cachedCategories = categories;
return categories;
}
export function clearCategoryCache() {
cachedCategories = null;
}