commit 64af77101fac70e6611f4d7bda4c0c467349c283 Author: Husanjonazamov Date: Tue Feb 24 12:52:49 2026 +0500 classify web diff --git a/.env b/.env new file mode 100644 index 0000000..886ac39 --- /dev/null +++ b/.env @@ -0,0 +1,39 @@ +# Web Version (Do not Change) +NEXT_PUBLIC_WEB_VERSION="2.10.1" + + +# Admin panle url +NEXT_PUBLIC_API_URL="https://eclassify.wrteam.me" + + +# Website URL +NEXT_PUBLIC_WEB_URL="https://eclassifyweb.wrteam.me" + +# API ENDPOINT (Do not change) +NEXT_PUBLIC_END_POINT="/api/" + +# Firebase config +NEXT_PUBLIC_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +NEXT_PUBLIC_AUTH_DOMAIN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +NEXT_PUBLIC_PROJECT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +NEXT_PUBLIC_STORAGE_BUCKET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +NEXT_PUBLIC_MESSAGING_SENDER_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +NEXT_PUBLIC_APP_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +NEXT_PUBLIC_MEASUREMENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + + +# Vapid api key +NEXT_PUBLIC_VAPID_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +# here you get default country list (https://developers.google.com/hotels/hotel-prices/dev-guide/country-codes) +# enter in small case like (india :- in) +#DEFAULT COUNTRY +NEXT_PUBLIC_DEFAULT_COUNTRY=in +NEXT_PUBLIC_SEO = true + +NEXT_PUBLIC_SEO_REVALIDATE_MINUTES = 60 + +NEXT_PUBLIC_META_TITLE="eClassify - Buy & Sell Marketplace | Find Everything You Need" +NEXT_PUBLIC_META_DESCRIPTION="Discover eClassify, your ultimate online marketplace for buying and selling a wide range of products. Enjoy a seamless shopping experience with diverse listings, secure transactions, and excellent customer support. Join eClassify today and start exploring!" +NEXT_PUBLIC_META_kEYWORDS="buy and sell marketplace, online marketplace, eClassify, buy and sell online, online shopping, sell products online, buy products online, secure transactions, online listings, customer support, e-commerce, digital marketplace, shop online, sell online, online buying and selling" + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be724ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ +/public/sitemap.xml + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..ecf77b7 --- /dev/null +++ b/.htaccess @@ -0,0 +1,15 @@ + + RewriteEngine On + RewriteBase / + + RewriteRule ^/.well-known/acme-challenge/(.*) /.well-known/acme-challenge/$1 [L] + + RewriteRule ^_next/(.*) /.next/$1 [L] + # Allow access to static files with specific extensions + RewriteCond %{REQUEST_URI} \.(js|css|svg|jpg|jpeg|png|gif|ico|woff2)$ + RewriteRule ^ - [L] + + RewriteRule ^index.html http://127.0.0.1:8004/$1 [P] + RewriteRule ^index.php http://127.0.0.1:8004/$1 [P] + RewriteRule ^/?(.*)$ http://127.0.0.1:8004/$1 [P] + \ No newline at end of file diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association new file mode 100644 index 0000000..43a771e --- /dev/null +++ b/.well-known/apple-app-site-association @@ -0,0 +1,14 @@ +{ + "applinks": { + "apps": [], + "details": [ + { + "appID": "QA5UX5B4B6.com.eclassify.wrteam", + "paths": [ + "/ad-details/*", + "/seller/*" + ] + } + ] + } +} \ No newline at end of file diff --git a/.well-known/assetlinks.json b/.well-known/assetlinks.json new file mode 100644 index 0000000..146c2a3 --- /dev/null +++ b/.well-known/assetlinks.json @@ -0,0 +1,26 @@ +[ + { + "relation": [ + "delegate_permission/common.handle_all_urls" + ], + "target": { + "namespace": "android_app", + "package_name": "com.eclassify.wrteam", + "sha256_cert_fingerprints": [ + "B4:B3:AD:26:FA:94:07:B0:EA:CC:30:7E:65:B3:AB:14:B9:98:BB:AB:2F:2C:FF:81:73:9A:6C:22:DF:7C:8C:42" + ] + } + }, + { + "relation": [ + "delegate_permission/common.handle_all_urls" + ], + "target": { + "namespace": "android_app", + "package_name": "com.eclassify.wrteam", + "sha256_cert_fingerprints": [ + "C8:7B:75:BD:A2:AF:6B:E2:93:50:AA:20:43:83:68:DE:AE:87:4B:1C:7A:B3:17:9E:CA:53:F9:BC:A7:9B:F2:39" + ] + } + } +] \ No newline at end of file diff --git a/HOC/Checkauth.jsx b/HOC/Checkauth.jsx new file mode 100644 index 0000000..5e250e4 --- /dev/null +++ b/HOC/Checkauth.jsx @@ -0,0 +1,59 @@ +"use client"; +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import Loader from "@/components/Common/Loader"; +import { usePathname } from "next/navigation"; +import { useNavigate } from "@/components/Common/useNavigate"; + +const Checkauth = (WrappedComponent) => { + const Wrapper = (props) => { + const pathname = usePathname(); + const { navigate } = useNavigate(); + const user = useSelector((state) => state.UserSignup.data); + const [isAuthorized, setIsAuthorized] = useState(false); + const [authChecked, setAuthChecked] = useState(false); + + useEffect(() => { + // List of routes that require authentication + const privateRoutes = [ + "/profile", + "/ad-listing", + "/notifications", + "/chat", + "/user-subscription", + "/my-ads", + "/favorites", + "/transactions", + "/reviews", + "/edit-listing", + "/user-verification", + "/job-applications", + ]; + const isPrivateRoute = privateRoutes.some( + (route) => pathname === route || pathname.startsWith(`${route}/`) + ); + + // If it's a private route and user is not authenticated + if (isPrivateRoute && !user) { + navigate("/"); + return; + } + + // If user is authenticated or it's not a private route + setIsAuthorized(true); + setAuthChecked(true); + }, [user, pathname]); + + // Show loader until auth check completes + if (!authChecked) { + return ; + } + + // Only render the component if user is authorized + return isAuthorized ? : null; + }; + + return Wrapper; +}; + +export default Checkauth; diff --git a/api/AxiosInterceptors.jsx b/api/AxiosInterceptors.jsx new file mode 100644 index 0000000..120d087 --- /dev/null +++ b/api/AxiosInterceptors.jsx @@ -0,0 +1,50 @@ +import { logoutSuccess } from "@/redux/reducer/authSlice"; +import { setIsUnauthorized } from "@/redux/reducer/globalStateSlice"; +import { store } from "@/redux/store"; +import axios from "axios"; + +const Api = axios.create({ + baseURL: `${process.env.NEXT_PUBLIC_API_URL}${process.env.NEXT_PUBLIC_END_POINT}`, +}); + +let isUnauthorizedToastShown = false; + +Api.interceptors.request.use(function (config) { + let token = undefined; + let langCode = undefined; + + if (typeof window !== "undefined") { + const state = store.getState(); + token = state?.UserSignup?.data?.token; + langCode = state?.CurrentLanguage?.language?.code; + } + + if (token) config.headers.authorization = `Bearer ${token}`; + if (langCode) config.headers["Content-Language"] = langCode; + + return config; +}); + +// Add a response interceptor +Api.interceptors.response.use( + function (response) { + return response; + }, + function (error) { + if (error.response && error.response.status === 401) { + // Call the logout function if the status code is 401 + logoutSuccess(); + if (!isUnauthorizedToastShown) { + store.dispatch(setIsUnauthorized(true)); + isUnauthorizedToastShown = true; + // Reset the flag after a certain period + setTimeout(() => { + isUnauthorizedToastShown = false; + }, 3000); // 3 seconds delay before allowing another toast + } + } + return Promise.reject(error); + } +); + +export default Api; diff --git a/app/about-us/page.jsx b/app/about-us/page.jsx new file mode 100644 index 0000000..b09f3d8 --- /dev/null +++ b/app/about-us/page.jsx @@ -0,0 +1,48 @@ +import AboutUs from "@/components/PagesComponent/StaticPages/AboutUs"; +import { SEO_REVALIDATE_SECONDS } from "@/lib/constants"; + +export const dynamic = "force-dynamic"; + + +export const generateMetadata = async ({ searchParams }) => { + try { + if (process.env.NEXT_PUBLIC_SEO === "false") return; + const params = await searchParams; + const langCode = params?.lang || "en"; + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}${process.env.NEXT_PUBLIC_END_POINT}seo-settings?page=about-us`, + { + headers: { + "Content-Language": langCode || "en", + }, + next: { + revalidate: SEO_REVALIDATE_SECONDS, + }, + } + ); + + const data = await res.json(); + const aboutUs = data?.data?.[0]; + + return { + title: aboutUs?.translated_title || process.env.NEXT_PUBLIC_META_TITLE, + description: + aboutUs?.translated_description || + process.env.NEXT_PUBLIC_META_DESCRIPTION, + openGraph: { + images: aboutUs?.image ? [aboutUs?.image] : [], + }, + keywords: + aboutUs?.translated_keywords || process.env.NEXT_PUBLIC_META_kEYWORDS, + }; + } catch (error) { + console.error("Error fetching MetaData:", error); + return null; + } +}; + +const AboutUsPage = () => { + return ; +}; + +export default AboutUsPage; diff --git a/app/ad-details/[slug]/page.jsx b/app/ad-details/[slug]/page.jsx new file mode 100644 index 0000000..f4b9b2e --- /dev/null +++ b/app/ad-details/[slug]/page.jsx @@ -0,0 +1,104 @@ +import StructuredData from "@/components/Layout/StructuredData"; +import ProductDetail from "@/components/PagesComponent/ProductDetail/ProductDetails"; +import { SEO_REVALIDATE_SECONDS } from "@/lib/constants"; +import { generateKeywords } from "@/utils/generateKeywords"; + +export const generateMetadata = async ({ params, searchParams }) => { + if (process.env.NEXT_PUBLIC_SEO === "false") return; + try { + const { slug } = await params; + const langCode = (await searchParams)?.lang || "en"; + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}${process.env.NEXT_PUBLIC_END_POINT}get-item?slug=${slug}`, + { + headers: { + "Content-Language": langCode || "en", + }, + next: { + revalidate: SEO_REVALIDATE_SECONDS, + }, + } + ); + + const data = await res.json(); + const item = data?.data?.data?.[0]; + const title = item?.translated_item?.name; + const description = item?.translated_item?.description; + const keywords = generateKeywords(item?.translated_item?.description); + const image = item?.image; + + return { + title: title || process.env.NEXT_PUBLIC_META_TITLE, + description: description || process.env.NEXT_PUBLIC_META_DESCRIPTION, + openGraph: { + images: image ? [image] : [], + }, + keywords: keywords, + }; + } catch (error) { + console.error("Error fetching MetaData:", error); + return null; + } +}; + +const getItemData = async (slug, langCode) => { + if (process.env.NEXT_PUBLIC_SEO === "false") return; + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}${process.env.NEXT_PUBLIC_END_POINT}get-item?slug=${slug}`, + { + headers: { + "Content-Language": langCode || "en", + }, + next: { + revalidate: SEO_REVALIDATE_SECONDS, + }, + } + ); + + const data = await res.json(); + const item = data?.data?.data?.[0]; + return item; + } catch (error) { + console.error("Error fetching item data:", error); + return null; + } +}; + +const ProductDetailPage = async ({ params, searchParams }) => { + const { slug } = await params; + const langCode = (await searchParams).lang || "en"; + const product = await getItemData(slug, langCode); + const jsonLd = product + ? { + "@context": "https://schema.org", + "@type": "Product", + productID: product?.id, + name: product?.translated_item?.name, + description: product?.translated_item?.description, + image: product?.image, + url: `${process.env.NEXT_PUBLIC_WEB_URL}/ad-details/${product?.slug}`, + category: { + "@type": "Thing", + name: product?.category?.translated_name || "General Category", // Default category name + }, + ...(product?.price && { + offers: { + "@type": "Offer", + price: product.price, + priceCurrency: "USD", + }, + }), + countryOfOrigin: product?.translated_item?.country, + } + : null; + + return ( + <> + + + + ); +}; + +export default ProductDetailPage; diff --git a/app/ad-listing/page.jsx b/app/ad-listing/page.jsx new file mode 100644 index 0000000..d31b559 --- /dev/null +++ b/app/ad-listing/page.jsx @@ -0,0 +1,5 @@ +import AdsListing from "@/components/PagesComponent/AdsListing/AdsListing"; +const AdListingPage = () => { + return ; +}; +export default AdListingPage; diff --git a/app/ads/page.jsx b/app/ads/page.jsx new file mode 100644 index 0000000..fb95673 --- /dev/null +++ b/app/ads/page.jsx @@ -0,0 +1,215 @@ +import StructuredData from "@/components/Layout/StructuredData"; +import Products from "@/components/PagesComponent/Ads/Ads"; +import { SEO_REVALIDATE_SECONDS } from "@/lib/constants"; +import { generateKeywords } from "@/utils/generateKeywords"; + +export const dynamic = "force-dynamic"; + + +const buildIndexableParams = (searchParams) => { + const indexableFilters = [ + "category", + "query", + "country", + "state", + "city", + "areaId", + "sortBy", + "min_price", + "max_price", + "date_posted", + ]; + + const params = new URLSearchParams(); + + const { country, state, city, areaId } = searchParams || {}; + + const locationPriority = areaId + ? "areaId" + : city + ? "city" + : state + ? "state" + : country + ? "country" + : null; + + indexableFilters.forEach((key) => { + let value = searchParams[key]; + if (value === undefined || value === "") return; + + // 🧹 Skip non-selected location levels + if (["country", "state", "city", "areaId"].includes(key)) { + if (key !== locationPriority) return; + } + + if (["areaId", "min_price", "max_price"].includes(key)) + value = Number(value); + if (key === "category") params.append("category_slug", value); + else if (key === "query") params.append("search", value); + else if (key === "date_posted") params.append("posted_since", value); + else params.append(key, value); + }); + + return params.toString(); +}; + +const buildCanonicalParams = (searchParams) => { + const params = new URLSearchParams(); + + const { category, query, lang } = searchParams || {}; + + if (category) params.append("category", category); + if (query) params.append("search", query); + // Add lang to canonical params for consistency with sitemap + if (lang) params.append("lang", lang); + + return params.toString(); +}; + +export const generateMetadata = async ({ searchParams }) => { + if (process.env.NEXT_PUBLIC_SEO === "false") return; + + try { + const originalSearchParams = await searchParams; + const langCode = originalSearchParams?.lang || "en"; + const slug = originalSearchParams?.category || ""; // change to your param name if needed + + let title = process.env.NEXT_PUBLIC_META_TITLE; + let description = process.env.NEXT_PUBLIC_META_DESCRIPTION; + let keywords = process.env.NEXT_PUBLIC_META_kEYWORDS; + let image = ""; + + if (slug) { + // Fetch category-specific SEO + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}${process.env.NEXT_PUBLIC_END_POINT}get-categories?slug=${slug}`, + { + headers: { "Content-Language": langCode || "en" }, + next: { + revalidate: SEO_REVALIDATE_SECONDS, + }, + } + ); + + const data = await response.json(); + const selfCategory = data?.self_category; + + title = selfCategory?.translated_name || title; + description = selfCategory?.translated_description || description; + keywords = + generateKeywords(selfCategory?.translated_description) || keywords; + image = selfCategory?.image || image; + } else { + // Fetch default ad listing SEO + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}${process.env.NEXT_PUBLIC_END_POINT}seo-settings?page=ad-listing`, + { + headers: { "Content-Language": langCode || "en" }, + next: { + revalidate: SEO_REVALIDATE_SECONDS, + }, + } + ); + const data = await res.json(); + const adListing = data?.data?.[0]; + + title = adListing?.translated_title || title; + description = adListing?.translated_description || description; + keywords = adListing?.translated_keywords || keywords; + image = adListing?.image || image; + } + + const baseUrl = process.env.NEXT_PUBLIC_WEB_URL; + const paramsStr = buildCanonicalParams(originalSearchParams); + const canonicalUrl = `${baseUrl}/ads${paramsStr ? `?${paramsStr}` : ""}`; + + return { + title, + description, + openGraph: { + images: image ? [image] : [], + }, + keywords, + alternates: { + canonical: canonicalUrl, + }, + }; + } catch (error) { + console.error("Error fetching MetaData:", error); + return null; + } +}; + +const getAllItems = async (langCode, searchParams) => { + if (process.env.NEXT_PUBLIC_SEO === "false") return; + + try { + const queryString = buildIndexableParams(searchParams) + ? buildIndexableParams(searchParams) + : ""; + const url = `${process.env.NEXT_PUBLIC_API_URL}${process.env.NEXT_PUBLIC_END_POINT + }get-item?page=1${queryString ? `&${queryString}` : ""}`; + + const res = await fetch(url, { + headers: { + "Content-Language": langCode || "en", + }, + next: { + revalidate: SEO_REVALIDATE_SECONDS, + }, + }); + + const data = await res.json(); + return data?.data?.data || []; + } catch (error) { + console.error("Error fetching Product Items Data:", error); + return []; + } +}; + +const AdsPage = async ({ searchParams }) => { + const originalSearchParams = await searchParams; + const langCode = originalSearchParams?.lang || "en"; + const AllItems = await getAllItems(langCode, originalSearchParams); + + const jsonLd = AllItems + ? { + "@context": "https://schema.org", + "@type": "ItemList", + itemListElement: AllItems.map((product, index) => ({ + "@type": "ListItem", + position: index + 1, // Position starts at 1 + item: { + "@type": "Product", + productID: product?.id, + name: product?.translated_item?.name || "", + description: product?.translated_item?.description || "", + image: product?.image || "", + url: `${process.env.NEXT_PUBLIC_WEB_URL}/ad-details/${product?.slug}`, + category: { + "@type": "Thing", + name: product?.category?.translated_name || "", + }, + offers: { + "@type": "Offer", + price: product.price || undefined, + priceCurrency: product?.price ? "USD" : undefined, + availability: product?.price + ? "https://schema.org/InStock" + : "https://schema.org/PreOrder", + }, + countryOfOrigin: product?.translated_item?.country || "", + }, + })), + } + : null; + + return ( + <> + + + + ); +}; +export default AdsPage; diff --git a/app/blogs/[slug]/page.jsx b/app/blogs/[slug]/page.jsx new file mode 100644 index 0000000..5e4a0ed --- /dev/null +++ b/app/blogs/[slug]/page.jsx @@ -0,0 +1,119 @@ +import StructuredData from "@/components/Layout/StructuredData"; +import BlogDetailPage from "@/components/PagesComponent/BlogDetail/BlogDetailPage"; +import { SEO_REVALIDATE_SECONDS } from "@/lib/constants"; + +const stripHtml = (html) => { + return html.replace(/<[^>]*>/g, ""); // Regular expression to remove HTML tags +}; + +// Function to format the date correctly (ISO 8601) +const formatDate = (dateString) => { + // Remove microseconds and ensure it follows ISO 8601 format + const validDateString = dateString.slice(0, 19) + "Z"; // Remove microseconds and add 'Z' for UTC + return validDateString; +}; + +export const generateMetadata = async ({ params, searchParams }) => { + try { + if (process.env.NEXT_PUBLIC_SEO === "false") return; + const slugParams = await params; + const langParams = await searchParams; + const langCode = langParams?.lang || "en"; + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}${process.env.NEXT_PUBLIC_END_POINT}blogs?slug=${slugParams?.slug}`, + { + headers: { + "Content-Language": langCode || "en", + }, + next: { + revalidate: SEO_REVALIDATE_SECONDS, + }, + } + ); + + if (!response.ok) { + throw new Error("Failed to fetch metadata"); + } + + const responseData = await response.json(); + const data = responseData?.data?.data[0]; + + const plainTextDescription = data?.translated_description?.replace( + /<\/?[^>]+(>|$)/g, + "" + ); + + return { + title: data?.translated_title || process.env.NEXT_PUBLIC_META_TITLE, + description: plainTextDescription + ? plainTextDescription + : process.env.NEXT_PUBLIC_META_DESCRIPTION, + openGraph: { + images: data?.image ? [data?.image] : [], + }, + keywords: data?.translated_tags || process.env.NEXT_PUBLIC_META_kEYWORDS, + }; + } catch (error) { + console.error("Error fetching MetaData:", error); + return null; + } +}; + +const fetchSingleBlogItem = async (slug, langCode) => { + try { + if (process.env.NEXT_PUBLIC_SEO === "false") return; + const url = `${process.env.NEXT_PUBLIC_API_URL}${process.env.NEXT_PUBLIC_END_POINT}blogs?slug=${slug}`; + const response = await fetch(url, { + headers: { + "Content-Language": langCode || "en", + }, + next: { + revalidate: SEO_REVALIDATE_SECONDS, + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch blog data"); + } + + const responseData = await response.json(); + return responseData?.data?.data[0] || []; + } catch (error) { + console.error("Error fetching Blog Items Data:", error); + return []; + } +}; + +const BlogPage = async ({ params, searchParams }) => { + const { slug } = await params; + const langCode = (await searchParams).lang || "en"; + const singleBlog = await fetchSingleBlogItem(slug, langCode); + + const jsonLd = singleBlog + ? { + "@context": "https://schema.org", + "@type": "BlogPosting", + headline: singleBlog?.translated_title, + description: singleBlog?.translated_description + ? stripHtml(singleBlog.translated_description) + : "No description available", // Strip HTML from description + url: `${process.env.NEXT_PUBLIC_WEB_URL}/blogs/${singleBlog?.slug}`, + image: singleBlog?.image, + datePublished: singleBlog?.created_at + ? formatDate(singleBlog.created_at) + : "", // Format date to ISO 8601 + keywords: singleBlog?.translated_tags + ? singleBlog.translated_tags.join(", ") + : "", // Adding tags as keywords + } + : null; + + return ( + <> + + + + ); +}; + +export default BlogPage; diff --git a/app/blogs/page.jsx b/app/blogs/page.jsx new file mode 100644 index 0000000..7f582f3 --- /dev/null +++ b/app/blogs/page.jsx @@ -0,0 +1,123 @@ +import StructuredData from "@/components/Layout/StructuredData"; +import Blogs from "@/components/PagesComponent/Blogs/Blogs"; +import { SEO_REVALIDATE_SECONDS } from "@/lib/constants"; + +export const dynamic = "force-dynamic"; + +export const generateMetadata = async ({ searchParams }) => { + try { + if (process.env.NEXT_PUBLIC_SEO === "false") return; + const params = await searchParams; + const langCode = params?.lang || "en"; + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}${process.env.NEXT_PUBLIC_END_POINT}seo-settings?page=blogs`, + { + headers: { + "Content-Language": langCode || "en", + }, + next: { + revalidate: SEO_REVALIDATE_SECONDS, + }, + } + ); + + if (!response.ok) { + throw new Error("Failed to fetch blogs metadata"); + } + const data = await response.json(); + const blogs = data?.data[0]; + return { + title: blogs?.translated_title || process.env.NEXT_PUBLIC_META_TITLE, + description: + blogs?.translated_description || + process.env.NEXT_PUBLIC_META_DESCRIPTION, + openGraph: { + images: blogs?.image ? [blogs?.image] : [], + }, + keywords: + blogs?.translated_keywords || process.env.NEXT_PUBLIC_META_kEYWORDS, + }; + } catch (error) { + console.error("Error fetching MetaData:", error); + return null; + } +}; + +const stripHtml = (html) => { + return html.replace(/<[^>]*>/g, ""); // Regular expression to remove HTML tags +}; + +// Function to format the date correctly (ISO 8601) +const formatDate = (dateString) => { + // Remove microseconds and ensure it follows ISO 8601 format + const validDateString = dateString.slice(0, 19) + "Z"; // Remove microseconds and add 'Z' for UTC + return validDateString; +}; + +const fetchBlogItems = async (langCode, tag) => { + try { + if (process.env.NEXT_PUBLIC_SEO === "false") return; + let url = `${process.env.NEXT_PUBLIC_API_URL}${process.env.NEXT_PUBLIC_END_POINT}blogs`; + + if (tag) { + url += `?tag=${encodeURIComponent(tag)}`; + } + + const response = await fetch(url, { + headers: { + "Content-Language": langCode || "en", + }, + next: { + revalidate: SEO_REVALIDATE_SECONDS, + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch blogs json-ld data"); + } + const data = await response.json(); + return data?.data?.data || []; + } catch (error) { + console.error("Error fetching Blog Items Data:", error); + return []; + } +}; + +const BlogsPage = async ({ searchParams }) => { + const params = await searchParams; + const langCode = params?.lang || "en"; + const tag = params?.tag || null; + const blogItems = await fetchBlogItems(langCode, tag); + + const jsonLd = blogItems + ? { + "@context": "https://schema.org", + "@type": "ItemList", + itemListElement: blogItems.map((blog, index) => ({ + "@type": "ListItem", + position: index + 1, + item: { + "@type": "BlogPosting", + headline: blog?.translated_title, + description: blog?.translated_description + ? stripHtml(blog.translated_description) + : "No description available", // Strip HTML from description + url: `${process.env.NEXT_PUBLIC_WEB_URL}/blogs/${blog?.slug}`, + image: blog?.image, + datePublished: blog?.created_at ? formatDate(blog.created_at) : "", // Format date to ISO 8601 + keywords: blog?.translated_tags + ? blog.translated_tags.join(", ") + : "", // Adding tags as keywords + }, + })), + } + : null; + return ( + <> + + + + ); +}; + +export default BlogsPage; diff --git a/app/chat/page.jsx b/app/chat/page.jsx new file mode 100644 index 0000000..79fdca2 --- /dev/null +++ b/app/chat/page.jsx @@ -0,0 +1,7 @@ +import ProfileDashboard from "@/components/PagesComponent/ProfileDashboard/ProfileDashboard"; + +const ChatPage = () => { + return ; +}; + +export default ChatPage; diff --git a/app/contact-us/page.jsx b/app/contact-us/page.jsx new file mode 100644 index 0000000..53600f3 --- /dev/null +++ b/app/contact-us/page.jsx @@ -0,0 +1,46 @@ +import ContactUs from "@/components/PagesComponent/Contact/ContactUs"; +import { SEO_REVALIDATE_SECONDS } from "@/lib/constants"; + +export const dynamic = "force-dynamic"; + + +export const generateMetadata = async ({ searchParams }) => { + try { + if (process.env.NEXT_PUBLIC_SEO === "false") return; + const params = await searchParams; + const langCode = params?.lang || "en"; + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}${process.env.NEXT_PUBLIC_END_POINT}seo-settings?page=contact-us`, + { + headers: { + "Content-Language": langCode || "en", + }, + next: { + revalidate: SEO_REVALIDATE_SECONDS, + }, + } + ); + const data = await res.json(); + const contactUs = data?.data?.[0]; + return { + title: contactUs?.translated_title || process.env.NEXT_PUBLIC_META_TITLE, + description: + contactUs?.translated_description || + process.env.NEXT_PUBLIC_META_DESCRIPTION, + openGraph: { + images: contactUs?.image ? [contactUs?.image] : [], + }, + keywords: + contactUs?.translated_keywords || process.env.NEXT_PUBLIC_META_kEYWORDS, + }; + } catch (error) { + console.error("Error fetching MetaData:", error); + return null; + } +}; + +const ContactUsPage = () => { + return ; +}; + +export default ContactUsPage; diff --git a/app/edit-listing/[id]/page.jsx b/app/edit-listing/[id]/page.jsx new file mode 100644 index 0000000..f52c8a4 --- /dev/null +++ b/app/edit-listing/[id]/page.jsx @@ -0,0 +1,9 @@ +import EditListing from "@/components/PagesComponent/EditListing/EditListing"; + +const EditListingPage = async (props) => { + const params = await props.params; + const id = await params.id; + return ; +}; + +export default EditListingPage; diff --git a/app/error.jsx b/app/error.jsx new file mode 100644 index 0000000..166e4eb --- /dev/null +++ b/app/error.jsx @@ -0,0 +1,37 @@ +"use client"; // Error components must be Client Components +import { useEffect } from "react"; +import somthingWrong from "../public/assets/something_went_wrong.svg"; +import { t } from "@/utils"; +import { Button } from "@/components/ui/button"; +import CustomImage from "@/components/Common/CustomImage"; +import { useNavigate } from "@/components/Common/useNavigate"; + +export default function Error({ error }) { + const { navigate } = useNavigate(); + useEffect(() => { + // Log the error to an error reporting service + console.error(error); + }, [error]); + const navigateHome = () => { + navigate("/"); + }; + return ( +
+ +

+ {t("somthingWentWrong")} +

+
+ {t("tryLater")} + +
+
+ ); +} diff --git a/app/faqs/page.jsx b/app/faqs/page.jsx new file mode 100644 index 0000000..8b8ae60 --- /dev/null +++ b/app/faqs/page.jsx @@ -0,0 +1,45 @@ +import FaqsPage from "@/components/PagesComponent/Faq/FaqsPage"; +import { SEO_REVALIDATE_SECONDS } from "@/lib/constants"; + +export const dynamic = "force-dynamic"; + +export const generateMetadata = async ({ searchParams }) => { + try { + if (process.env.NEXT_PUBLIC_SEO === "false") return; + const params = await searchParams; + const langCode = params?.lang || "en"; + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}${process.env.NEXT_PUBLIC_END_POINT}seo-settings?page=faqs`, + { + headers: { + "Content-Language": langCode || "en", + }, + next: { + revalidate: SEO_REVALIDATE_SECONDS, + }, + } + ); + const data = await response.json(); + const faqs = data?.data?.[0]; + return { + title: faqs?.translated_title || process.env.NEXT_PUBLIC_META_TITLE, + description: + faqs?.translated_description || + process.env.NEXT_PUBLIC_META_DESCRIPTION, + openGraph: { + images: faqs?.image ? [faqs?.image] : [], + }, + keywords: + faqs?.translated_keywords || process.env.NEXT_PUBLIC_META_kEYWORDS, + }; + } catch (error) { + console.error("Error fetching MetaData:", error); + return null; + } +}; + +const page = () => { + return ; +}; + +export default page; diff --git a/app/favorites/page.jsx b/app/favorites/page.jsx new file mode 100644 index 0000000..b57c863 --- /dev/null +++ b/app/favorites/page.jsx @@ -0,0 +1,9 @@ +import ProfileDashboard from "@/components/PagesComponent/ProfileDashboard/ProfileDashboard" + +export default function FavoritesPage() { + return ( + <> + + + ) +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..2626168 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,274 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} + +@layer base { + :root { + --background: #ffffff; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: #00b2ca; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: #f6f5fa; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: #dc3545; + --destructive-foreground: 0 0% 98%; + --border: #d3d3d3; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: #00b2ca; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --font-color: #ffffff; + --light-font-color: #595b6c; + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-[background] text-foreground; + } +} + +@layer utilities { + + + .landingSecHeader { + @apply text-center text-4xl lg:text-5xl font-light; + } + + .outlinedSecHead { + @apply p-3 px-4 border border-primary text-primary rounded-md font-semibold text-center; + } + + .storeIcons { + @apply max-w-max w-full sm:w-[170px] md:w-[215px] lg:w-[235px] h-auto; + } + + .footerSocialLinks { + @apply flex items-center justify-center w-[40px] h-[40px] rounded-md p-2 transition-all duration-500 bg-white/15; + } + + .footerContactIcons { + @apply flex items-center justify-center min-w-[48px] w-[48px] h-[48px] rounded-md p-[10px] transition-all duration-500 bg-white/15; + } + + + .footerSocialLinks:hover, + .footerContactIcons:hover { + background-color: var(--primary-color); + box-shadow: 0px 8px 28px 0px var(--primary-color); + } + + .footerLabel { + @apply text-white opacity-65 text-sm transition-colors; + } + + .footerLabel:hover { + color: var(--primary-color); + } + + .space-between { + @apply flex items-center justify-between; + } + + .labelInputCont { + @apply flex flex-col gap-2; + } + + .requiredInputLabel { + @apply after:content-['*'] after:text-destructive; + } + + .loader-container-otp { + @apply flex items-center justify-center h-7 py-1 px-2; + } + + .loader-otp { + @apply border-4 border-t-[var(--primary-color)] border-[#f3f3f3] rounded-full w-5 h-5 animate-spin; + } + + .profileActiveTab { + @apply py-2 px-4 bg-primary rounded-full text-white w-max + } + + .sectionTitle { + @apply text-xl sm:text-2xl font-medium capitalize + } + + .loader { + @apply w-full h-full relative flex items-center justify-center m-auto + } + + .text_ellipsis { + @apply overflow-hidden text-ellipsis whitespace-nowrap + } + + .scrollbar-none::-webkit-scrollbar { + display: none; + } + + /* Hide scrollbar for IE, Edge, and Firefox */ + .scrollbar-none { + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ + } +} + +/* .place_search div { + width: 100%; +} */ + +.formWrapper .react-tel-input .special-label { + display: none !important; +} + +.react-tel-input .form-control { + width: 100% !important; + height: 40px !important; + border: 1px solid lightgray !important; + outline: none !important; + border-radius: 4px !important; +} + +.react-tel-input .flag-dropdown { + background-color: transparent !important; +} + +/* Update focus styles to match shadcn Input */ +.react-tel-input .form-control:focus, +.react-tel-input .form-control:focus-visible { + outline: none !important; + box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--primary) !important; + border-color: transparent !important; +} + + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + margin: 0; +} + + +::-webkit-scrollbar { + width: 5px !important; + width: 5px !important; +} + +/* Track */ +::-webkit-scrollbar-track { + border-radius: 10px !important; + background-color: lightgray; + border-radius: 10px !important; + background-color: lightgray; +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background: var(--primary) !important; + border-radius: 10px !important; + background: var(--primary) !important; + border-radius: 10px !important; +} + + +/* RTL overrides for react-phone-input-2*/ +[dir='rtl'] .react-tel-input .form-control { + padding-left: inherit; + padding-right: 48px; +} + +[dir='rtl'] .react-tel-input .selected-flag { + padding: 0 8px 0 0; +} + +[dir='rtl'] .react-tel-input .selected-flag .arrow { + left: auto; + right: 20px; +} + +[dir=rtl] input[type=tel i] { + direction: rtl; +} + +.perspective-1000 { + perspective: 1000px; +} + +.transform-style-preserve-3d { + transform-style: preserve-3d; +} + +.backface-hidden { + backface-visibility: hidden; +} + +.rotate-y-180 { + transform: rotateY(180deg); +} \ No newline at end of file diff --git a/app/job-applications/page.jsx b/app/job-applications/page.jsx new file mode 100644 index 0000000..c2fd759 --- /dev/null +++ b/app/job-applications/page.jsx @@ -0,0 +1,7 @@ +import ProfileDashboard from "@/components/PagesComponent/ProfileDashboard/ProfileDashboard"; + +const JobApplicationsPage = () => { + return ; +}; + +export default JobApplicationsPage; diff --git a/app/landing/page.jsx b/app/landing/page.jsx new file mode 100644 index 0000000..63f60ad --- /dev/null +++ b/app/landing/page.jsx @@ -0,0 +1,57 @@ +import Layout from "@/components/Layout/Layout"; +import AnythingYouWant from "@/components/PagesComponent/LandingPage/AnythingYouWant"; +import OurBlogs from "@/components/PagesComponent/LandingPage/OurBlogs"; +import QuickAnswers from "@/components/PagesComponent/LandingPage/QuickAnswers"; +import WorkProcess from "@/components/PagesComponent/LandingPage/WorkProcess"; +import { SEO_REVALIDATE_SECONDS } from "@/lib/constants"; + +export const dynamic = "force-dynamic"; + +export const generateMetadata = async ({ searchParams }) => { + try { + if (process.env.NEXT_PUBLIC_SEO === "false") return; + const params = await searchParams; + const langCode = params?.lang || "en"; + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}${process.env.NEXT_PUBLIC_END_POINT}seo-settings?page=landing`, + { + headers: { + "Content-Language": langCode || "en", + }, + next: { + revalidate: SEO_REVALIDATE_SECONDS, + }, + } + ); + const data = await res.json(); + const landing = data?.data?.[0]; + + return { + title: landing?.translated_title || process.env.NEXT_PUBLIC_META_TITLE, + description: + landing?.translated_description || + process.env.NEXT_PUBLIC_META_DESCRIPTION, + openGraph: { + images: landing?.image ? [landing?.image] : [], + }, + keywords: + landing?.translated_keywords || process.env.NEXT_PUBLIC_META_kEYWORDS, + }; + } catch (error) { + console.error("Error fetching MetaData:", error); + return null; + } +}; + +const LandingPage = () => { + return ( + + + + + + + ); +}; + +export default LandingPage; diff --git a/app/layout.js b/app/layout.js new file mode 100644 index 0000000..8d83ea7 --- /dev/null +++ b/app/layout.js @@ -0,0 +1,46 @@ +import { Manrope } from "next/font/google"; +import "./globals.css"; +import { Providers } from "@/redux/store/providers"; +import { Toaster } from "@/components/ui/sonner"; +// import Script from "next/script"; + +const manrope = Manrope({ + weight: ["200", "300", "400", "500", "600", "700", "800"], + subsets: ["latin"], + display: "swap", +}); + +export const generateMetadata = () => { + return { + title: process.env.NEXT_PUBLIC_META_TITLE, + description: process.env.NEXT_PUBLIC_META_DESCRIPTION, + keywords: process.env.NEXT_PUBLIC_META_kEYWORDS, + openGraph: { + title: process.env.NEXT_PUBLIC_META_TITLE, + description: process.env.NEXT_PUBLIC_META_DESCRIPTION, + keywords: process.env.NEXT_PUBLIC_META_kEYWORDS, + }, + }; +}; + +export default function RootLayout({ children }) { + return ( + + + {/*