bug fixes

This commit is contained in:
Deltora72 2026-01-27 16:24:05 +03:30
parent d339276bd2
commit dc9b5d4f32
56 changed files with 988 additions and 377 deletions

View File

@ -1,13 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
// const runtimeCaching = require('next-pwa/cache')
// const withPWA = require('next-pwa')({
// dest: 'public',
// // disable: process.env.NODE_ENV === 'development',
// register: true,
// runtimeCaching,
// });
const ContentSecurityPolicy = `
object-src 'none';

View File

@ -28,7 +28,7 @@ const Button: React.FunctionComponent<Button> = ({ text, type = "button", simple
form={formId}
name={name}
value={value}
className={`inline-block min-h-8 text-center rounded select-none active:scale-[0.98] transition-transform cursor-none lg:cursor-pointer duration-[75ms] shadow ${disabled ? "pointer-events-none !bg-gray-300" + " " + disabledClass : ""} ${className}`}
className={`inline-block min-h-8 text-center rounded select-none active:scale-[0.98] transition-transform cursor-none lg:cursor-pointer duration-[75ms] shadow ${loading ? "pointer-events-none" : ""} ${disabled ? "pointer-events-none !bg-gray-300" + " " + disabledClass : ""} ${className}`}
onClick={onClick}
>
{loading ?

View File

@ -1,6 +1,6 @@
import ClickOutside from "components/click-outside/click-outside";
import { SortSolid } from "components/icons";
import { useState } from "react";
import { useEffect, useState } from "react";
export type SelectDataFormat = {
label: React.ReactNode;
@ -61,6 +61,10 @@ const Select: React.FunctionComponent<Select> = ({
handleSelectOpen();
}
useEffect(() => {
setSelectedOption(value);
}, [value])
// return
return (
<div className={`flex items-center relative min-w-48 ${wrapperClass} ${disabled ? "pointer-events-none bg-gray-100" : ""}`} onClick={handleSelectOpen}>
@ -89,7 +93,7 @@ const Select: React.FunctionComponent<Select> = ({
>
{allValue && <option value={allValue} defaultValue={allValue}>{allValue}</option>}
{options.map(x => (
<option key={String(x.label)} className={`${optionClass}`} value={x.value} disabled={x.disabled}>{x.label}</option>
<option key={String(x.value)} className={`${optionClass}`} value={x.value} disabled={x.disabled}>{x.label}</option>
))}
</select>
</div>

View File

@ -99,6 +99,7 @@ export const Cities = gql`
}
country {
id
Initials
}
}
}

View File

@ -1,7 +1,8 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { Dictionary } from 'common/types/general';
import { CountryCode, Dictionary } from 'common/types/general';
import { RootState } from '../../redux/store/store';
import { ShoppingCartItem } from 'common/types/billboard';
import { getInitialCountry } from 'lib/general/general';
export interface GlobalState {
dictionary: Dictionary[],
@ -36,6 +37,7 @@ export interface GlobalState {
pixelDensity: number;
shoppingCart: ShoppingCartItem[];
isInstallable: boolean;
country: CountryCode;
}
const initialState = {
@ -48,7 +50,8 @@ const initialState = {
measures: "imperial",
pixelDensity: 1,
shoppingCart: [],
isInstallable: false
isInstallable: false,
country: getInitialCountry(),
}
export const globalSlice = createSlice({
@ -70,10 +73,13 @@ export const globalSlice = createSlice({
setIsInstallable: (state, action: PayloadAction<any>) => {
state.isInstallable = action.payload
},
setCountry: (state, action: PayloadAction<CountryCode>) => {
state.country = action.payload
},
},
})
export const { setDictionary, setMenu, setMeasures, setShoppingCart, setIsInstallable } = globalSlice.actions
export const { setDictionary, setMenu, setMeasures, setShoppingCart, setIsInstallable, setCountry } = globalSlice.actions
export const getDictionary = (state: RootState) => state.global.dictionary;
export const getGlobalMenu = (state: RootState) => state.global.menu;
@ -81,5 +87,6 @@ export const getUnitSystem = (state: RootState) => state.global.measures;
export const getPixelDensity = (state: RootState) => state.global.pixelDensity;
export const getShoppingCart = (state: RootState) => state.global.shoppingCart;
export const getIsInstallable = (state: RootState) => state.global.isInstallable;
export const getCountry = (state: RootState) => state.global.country;
export default globalSlice.reducer

View File

@ -1,5 +1,5 @@
import { configureStore, ThunkAction, Action, createListenerMiddleware } from '@reduxjs/toolkit'
import globalReducer, { setShoppingCart } from '../slices/global'
import globalReducer, { setCountry, setShoppingCart } from '../slices/global'
import marketReducer from '../slices/market'
import userReducer, { setUserData } from '../slices/user'
import { ShoppingCartItem } from 'common/types/billboard'
@ -36,6 +36,13 @@ listenerMiddleware.startListening({
},
})
listenerMiddleware.startListening({
actionCreator: setCountry,
effect: async (action) => {
localStorage.setItem('country', action.payload);
},
});
export const store = configureStore({
reducer: {
global: globalReducer,

View File

@ -365,6 +365,9 @@ export const billboardFieldsQuery = `
Initials
name
English_name
country {
Initials
}
}
Cities (filter: { status: { _eq: "published" } }, limit: -1) {
id

View File

@ -1,103 +1,106 @@
import { clearCsrfToken, getCsrfToken, setCsrfToken } from 'lib/general/csrf-helper';
import { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext();
interface AuthContextType {
isAuthenticated: boolean;
userData: any; // or a proper User type
loading: boolean;
signIn: (params: { email: string; password: string }) => Promise<any>;
signOut: () => Promise<void>;
}
export const AuthProvider = ({ children }) => {
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);
// Check session and populate CSRF token only if logged in
useEffect(() => {
const checkSession = async () => {
try {
const res = await fetch('/api/auth/check-session', { credentials: 'include' });
const data = await res.json();
if (data.success) {
setIsAuthenticated(true);
setUserData(data.user);
await refreshCsrfToken();
} else {
setIsAuthenticated(false);
setUserData(null);
}
} catch (err) {
console.error('Error checking session:', err);
setIsAuthenticated(false);
setUserData(null);
} finally {
setLoading(false);
}
};
checkSession();
}, [isAuthenticated]);
// 🔐 Always fetch CSRF after session exists
const refreshCsrfToken = async () => {
try {
// ✅ Step 1: Check if a CSRF token already exists in localStorage
const existingToken = getCsrfToken();
if (existingToken) {
// Optional: You can verify it's still valid by checking cookie presence if you want
return existingToken;
}
const res = await fetch('/api/auth/csrf', {
credentials: 'include',
cache: 'no-store',
});
// ✅ Step 2: If not in localStorage, request a new one
const res = await fetch('/api/auth/csrf', { credentials: 'include' });
const data = await res.json();
if (!res.ok) throw new Error('CSRF failed');
if (res.ok && data.csrfToken) {
setCsrfToken(data.csrfToken);
return data.csrfToken;
} else {
clearCsrfToken();
return null;
}
const { csrfToken } = await res.json();
setCsrfToken(csrfToken);
return csrfToken;
} catch (err) {
console.error('Failed to fetch CSRF token:', err);
clearCsrfToken();
return null;
}
};
const signIn = async ({ email, password }) => {
try {
const res = await fetch('/api/auth/sign-in', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email, password }),
});
const data = await res.json();
// ✅ Check session once on mount
useEffect(() => {
const checkSession = async () => {
try {
const res = await fetch('/api/auth/check-session', {
credentials: 'include',
});
if (res.ok && data.success) {
setIsAuthenticated(true);
setUserData(data.user || null);
await refreshCsrfToken();
} else {
const data = await res.json();
if (data.success) {
setIsAuthenticated(true);
setUserData(data.user ?? null);
await refreshCsrfToken();
} else {
setIsAuthenticated(false);
setUserData(null);
clearCsrfToken();
}
} catch {
setIsAuthenticated(false);
setUserData(null);
clearCsrfToken();
} finally {
setLoading(false);
}
};
checkSession();
}, [isAuthenticated]);
// 🔑 Sign in
const signIn = async ({ email, password }: { email: string; password: string }) => {
try {
const res = await fetch('/api/auth/sign-in', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
if (!res.ok) throw data;
setIsAuthenticated(true);
setUserData(data.user ?? null);
// 🔐 MUST happen after session cookie exists
await refreshCsrfToken();
return data;
} catch (err) {
console.error('Error signing in:', err);
setIsAuthenticated(false);
setUserData(null);
clearCsrfToken();
return { success: false, message: 'Sign-in failed' };
return { success: false };
}
};
// 🚪 Sign out
const signOut = async () => {
try {
await fetch('/api/auth/sign-out', {
method: 'POST',
credentials: 'include',
});
} catch (err) {
console.error('Error signing out:', err);
} finally {
setIsAuthenticated(false);
setUserData(null);
@ -106,10 +109,20 @@ export const AuthProvider = ({ children }) => {
};
return (
<AuthContext.Provider value={{ isAuthenticated, userData, loading, signIn, signOut }}>
<AuthContext.Provider
value={{ isAuthenticated, userData, loading, signIn, signOut }}
>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@ -15,6 +15,8 @@ import NotificationSign from "components/notification-sign/notification-sign"
import CatSelectionButton from "./cat-selection-button"
import Button from "components/button/button"
import BillboardCatPlaceHolder from "./placeholder"
import { useSelector } from "react-redux"
import { getCountry } from "common/redux/slices/global"
interface BillboardCatFilters {
initialBillboards: Billboard[];
@ -55,6 +57,8 @@ const BillboardCatFilters: React.FunctionComponent<BillboardCatFilters> = ({ ini
const updateQueryParams = useUpdateQueryParams();
const totalBillboardsCount = initialBillboards.length;
const itemsPerPage = 12;
const country = useSelector(getCountry);
const cities = filtersData.cities.filter(x => x.country.Initials === country);
const itemsQuery = `
Billboards_aggregated
@ -242,7 +246,7 @@ const BillboardCatFilters: React.FunctionComponent<BillboardCatFilters> = ({ ini
const applyFilters = (city: string) => {
let cityId = -1;
if (city) {
cityId = Number(filtersData.cities.filter(x => `${locale === "en" ? "" : x.name + " - "}${x.English_name}` == city)[0]?.id);
cityId = Number(cities.filter(x => `${locale === "en" ? "" : x.name + " - "}${x.English_name}` == city)[0]?.id);
setCurrentCity(Number(cityId));
} else {
setCurrentCity(-1);
@ -281,7 +285,7 @@ const BillboardCatFilters: React.FunctionComponent<BillboardCatFilters> = ({ ini
</div>
<div className={`hidden lg:flex items-center`}>
{/* {data.cat && data.cats && <MobileCats cat={data.cat} cats={data.cats} onChange={applyFilters} />} */}
<BillboardCity id="billboard-page-desktop-cities" data={filtersData.cities} currentValue={currentCity} onChange={applyFilters} />
<BillboardCity id="billboard-page-desktop-cities" data={cities} currentValue={currentCity} onChange={applyFilters} />
{/* sorting filters */}
<BillboardSort id="billboard-page-desktop-sort" currentValue={sortFactor === "-date_created" ? -1 : 1} onChange={applySort} />
<CatSelectionButton id="billboard-page-desktop-cat-selection" wrapperClass={`hidden lg:block`} billboardCats={filtersData.billboardCats} currentCat={filtersData.billboardCats.filter(x => Number(x.id) === catId)[0]} />
@ -320,7 +324,7 @@ const BillboardCatFilters: React.FunctionComponent<BillboardCatFilters> = ({ ini
>
<div className={`flex flex-col items-center justify-center gap-4`}>
{/* {data.cat && data.cats && <MobileCats cat={data.cat} cats={data.cats} onChange={applyFilters} />} */}
<BillboardCity id="billboard-page-mobile-cities" data={filtersData.cities} currentValue={currentCity} onChange={applyFilters} wrapperClass="py-2" />
<BillboardCity id="billboard-page-mobile-cities" data={cities} currentValue={currentCity} onChange={applyFilters} wrapperClass="py-2" />
{/* sorting filters */}
<BillboardSort id="billboard-page-mobile-order" currentValue={sortFactor === "-date_created" ? -1 : 1} onChange={applySort} wrapperClass="py-2" />
<Button

View File

@ -168,6 +168,7 @@ const BillboardProducts: React.FunctionComponent<BillboardProductsProps> = ({ bi
id
}
}
billboard_products
(
filter: {
@ -346,55 +347,55 @@ const BillboardProducts: React.FunctionComponent<BillboardProductsProps> = ({ bi
return (
<div className="block w-full">
{/* product categories */}
<div className="block lg:py-[10px] lg:mt-4 border-b border-gray-200/50 pb-4">
{cats ?
<ProductCatsFilter
cats={cats}
billboard={{
id: billboard.id,
title: billboardTitle,
brand_color: billboard.brand_color
}}
onChange={setActiveCats}
/>
:
<InnerLoading loadingText={""} width={"200"} height={"100"} />
}
</div>
{/* search & count */}
<div className="flex items-center justify-between w-full pt-4 pb-2">
{/* number of products */}
<div className={`${searchVisible ? "hidden" : "flex"} md:flex shrink-0 items-center space-x-1 rtl:space-x-reverse py-2 px-3 rounded-lg bg-[var(--light-brand-color)]`}>
<span className="inline-block text-xs first-letter:capitalize">{`${translate("number-of")} ${translate("products")}`} :</span>
<span className="inline-block text-sm font-extrabold text-[var(--brand-color)]">{currentProductsCount}</span>
</div>
{/* products search */}
<div className={`flex items-center ${searchVisible ? "max-md:w-full" : ""}`}>
<Input
type="search"
value={searchValue}
placeholder={translate("billboard-products-search-placeholder")}
stripeHtml
onInput={handleproductSearch}
className={`${searchVisible ? "block" : "hidden"} md:block w-full py-2 px-4 text-sm placeholder:text-xs outline-none rounded-lg`}
wrapperClass="w-full md:w-64"
inputDelay={350}
/>
{searchVisible ?
<XMark className="inline-block md:hidden shrink-0 size-9 lg:size-3 fill-secondary-light p-2 rounded-lg bg-white rtl:mr-2 ltr:ml-2 lg:rtl:mr-2 lg:ltr:ml-2" onClick={() => setSearchVisible(false)} />
:
<MagnifyingGlass className="inline-block md:hidden shrink-0 size-9 lg:size-3 fill-secondary-light p-2 rounded-lg bg-white rtl:mr-2 ltr:ml-2 lg:rtl:mr-2 lg:ltr:ml-2" onClick={() => setSearchVisible(true)} />
}
</div>
</div>
{currentProducts ?
<>
{/* <HighlightedProducts
billboardId={billboard.id}
billboardTitle={billboardTitle}
/> */}
{totalProductsCount > 0 ?
<div className="block w-full max-lg:px-4 max-lg:pt-2">
<div className="block lg:py-[10px] lg:mt-4 border-b border-gray-200/50 pb-4">
{cats ?
<ProductCatsFilter
cats={cats}
billboard={{
id: billboard.id,
title: billboardTitle,
brand_color: billboard.brand_color
}}
onChange={setActiveCats}
/>
:
<InnerLoading loadingText={""} width={"200"} height={"100"} />
}
</div>
<div className="flex items-center justify-between w-full pt-4 pb-2">
{/* number of products */}
<div className={`${searchVisible ? "hidden" : "flex"} md:flex shrink-0 items-center space-x-1 rtl:space-x-reverse py-2 px-3 rounded-lg bg-[var(--light-brand-color)]`}>
<span className="inline-block text-xs first-letter:capitalize">{`${translate("number-of")} ${translate("products")}`} :</span>
<span className="inline-block text-sm font-extrabold text-[var(--brand-color)]">{totalProductsCount}</span>
</div>
{/* products search */}
<div className={`flex items-center ${searchVisible ? "max-md:w-full" : ""}`}>
<Input
type="search"
value={searchValue}
placeholder={translate("billboard-products-search-placeholder")}
stripeHtml
onInput={handleproductSearch}
className={`${searchVisible ? "block" : "hidden"} md:block w-full py-2 px-4 text-sm placeholder:text-xs outline-none rounded-lg`}
wrapperClass="w-full md:w-64"
inputDelay={350}
/>
{searchVisible ?
<XMark className="inline-block md:hidden shrink-0 size-9 lg:size-3 fill-secondary-light p-2 rounded-lg bg-white rtl:mr-2 ltr:ml-2 lg:rtl:mr-2 lg:ltr:ml-2" onClick={() => setSearchVisible(false)} />
:
<MagnifyingGlass className="inline-block md:hidden shrink-0 size-9 lg:size-3 fill-secondary-light p-2 rounded-lg bg-white rtl:mr-2 ltr:ml-2 lg:rtl:mr-2 lg:ltr:ml-2" onClick={() => setSearchVisible(true)} />
}
</div>
</div>
{/* items */}
{productRatings && priceData && priceData.length > 0 ?
<div className="block w-full mb-8">

View File

@ -11,6 +11,8 @@ import { privateDataFetch } from "common/data/apollo-client";
import { updateUserDetails } from "./payloads";
import { useRouter } from "next/router";
import { UserDetails } from "./data-types";
import { restBulkUpdateRequest } from "services/queries/directus/billboard";
import EventAlert from "components/popover/event-alert";
interface EditDetails {
className?: string,
@ -27,6 +29,10 @@ const EditDetails: React.FunctionComponent<EditDetails> = ({ userDetails, cities
// states
const [selectionOpen, setSelectionOpen] = useState(false);
const [city, setCity] = useState<string[]>([`${userDetails.city.name} - ${userDetails.city.English_name}`]);
const [alertOpen, setAlertOpen] = useState(false);
const [updating, setUpdating] = useState(false);
// variables
const translate = useTranslate();
const router = useRouter();
@ -52,8 +58,6 @@ const EditDetails: React.FunctionComponent<EditDetails> = ({ userDetails, cities
postal_code: false,
});
// variables
const topCities = ["Vancouver", "North Vancouver", "West Vancouver", "Toronto", "Montreal", "Calgary", "Ottawa", "Victoria",
"Edmonton", "Quebec", "Mississauga", "Winnipeg", "Brampton", "Hamilton", "Surrey", "Halifax", "London", "Laval", "Markham",
"Vaughan", "Gatineau", "Saskatoon"
@ -137,9 +141,35 @@ const EditDetails: React.FunctionComponent<EditDetails> = ({ userDetails, cities
}
}
const [submitForm, { data: mutationData, error: mutationErrors, loading: mutating }] = useMutation(updateUserDetails(formData.current, userDetails.id), { client: privateDataFetch("system") });
// const [submitForm, { data: mutationData, error: mutationErrors, loading: mutating }] = useMutation(updateUserDetails(formData.current, userDetails.id), { client: privateDataFetch("system") });
// console.log(accessToken);
const submitForm = async () => {
try {
await restBulkUpdateRequest({
collection: 'directus_users',
updates: [
{
id: String(userDetails.id),
data: {
first_name: formData.current.first_name,
last_name: formData.current.last_name,
description: formData.current.description,
email: formData.current.email,
phone_number: formData.current.phone_number,
city: formData.current.city.ids[0],
address: formData.current.address,
postal_code: formData.current.postal_code,
},
},
]
});
setAlertOpen(true);
} catch (error) {
console.error("User details update failed:", error);
setUpdating(false);
}
}
const sendChanges = () => {
validate()
@ -147,7 +177,10 @@ const EditDetails: React.FunctionComponent<EditDetails> = ({ userDetails, cities
submitForm();
}
}
mutationData && router.push("/dashboard/account");
const onAlertClose = () => {
setAlertOpen(false);
alertOpen && router.reload();
}
// useEffects
useEffect(() => {
@ -159,6 +192,14 @@ const EditDetails: React.FunctionComponent<EditDetails> = ({ userDetails, cities
// return
return (
<div className="">
<EventAlert
text={translate("dashboard-changes-successfully-applied")}
open={alertOpen}
type="success"
timeOut={3000}
onClose={onAlertClose}
hasTime
/>
<form className="w-full lg:w-full block bg-white rounded-none p-gi lg:p-6 lg:rounded-md">
<FormGroup title={translate("edit-user-details")}>
{/* <FormHelper text={translate("profile-ad-form-shared-helper-text")} className="col-span-2" /> */}
@ -179,9 +220,9 @@ const EditDetails: React.FunctionComponent<EditDetails> = ({ userDetails, cities
<Input id="email" name="email" type="text" className={`profile-form placeholder:text-sm w-full ${fieldErrors.email ? "profile-form-error" : ""}`} value={formData.current.email} onInput={data => handleFormUpdate("email", data)} />
</FormItem>
{/* city */}
<FormItem forId="ad-city" title={formTitles.city} className="col-span-2 md:col-span-1" required errorText={formErrors.necessaryFields} errorVisible={fieldErrors.city}>
<FormItem forId="user-city" title={formTitles.city} className="col-span-2 md:col-span-1" required errorText={formErrors.necessaryFields} errorVisible={fieldErrors.city}>
<DataList
id={"ad-city"}
id={"user-city"}
label={formTitles.city}
options={[...citiesList.map((x: any) => `${router.locale === "en" ? "" : x.name + " - "}${x.English_name}`), formTitles.otherCities]}
onChange={(items) => handleDetailsSelection("city", items)}

View File

@ -74,6 +74,7 @@ const CoverPhotoUpdate: React.FunctionComponent<CoverPhotoUpdateProps> = ({ cove
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "cover",
fileIds: galleryData.current.picsToRemove,
billboardId: query.id,
}),

View File

@ -246,6 +246,7 @@ const DeliveryMethodForm = ({ data, country, open = false, onClose, onChange, th
working_days: weekDays.filter(x => selectedWorkingDays.some(y => translate(x) === y)),
calculation_method: selectedCalculationMethod,
shipping_cost_currency: selectedCurrency,
delivery_proof_type: "none",
};
onChange(finalData as any);
onClose(false);

View File

@ -73,6 +73,7 @@ const LogoUpdate: React.FunctionComponent<LogoUpdateProps> = ({ logo, open = fal
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "logo",
fileIds: galleryData.current.picsToRemove,
billboardId: query.id,
}),

View File

@ -13,6 +13,8 @@ import StepButtons from './step-buttons';
import { City, Currencies, State } from 'common/types/general';
import { Editor } from '@tinymce/tinymce-react';
import { UnfinishedBillboardProps } from 'pages/dashboard/billboards/create-new';
import { useSelector } from 'react-redux';
import { getCountry } from 'common/redux/slices/global';
interface BillboardBasicDataFormProps {
themeStyles: React.CSSProperties;
@ -36,6 +38,7 @@ const BillboardBasicDataForm: React.FunctionComponent<BillboardBasicDataFormProp
const { locale, router } = useGetRouter();
const translate = useTranslate();
const country = useSelector(getCountry);
// states
const [fieldsTr, setFieldsTr] = useState(locale);
@ -64,6 +67,7 @@ const BillboardBasicDataForm: React.FunctionComponent<BillboardBasicDataFormProp
checkboxTitleClass: "text-text-gray text-xs sm:text-sm",
ctaClass: "!bg-[var(--brand-color)] rounded-lg"
}
const states: State[] = fieldsData.states.filter((x: State) => x.country.Initials === country);
const editorRef: any = useRef(null);
const formData = useRef({
@ -387,7 +391,7 @@ const BillboardBasicDataForm: React.FunctionComponent<BillboardBasicDataFormProp
<DataList
id={"state"}
label={translate("state")}
options={fieldsData.states.map(x => locale === "fa" ? x.name : x.English_name)}
options={states.map(x => locale === "fa" ? x.name : x.English_name)}
onChange={(value) => handleFeatureSelection("state", value)}
isOpen={selectionOpen}
onClose={() => setSelectionOpen(false)}

View File

@ -188,7 +188,7 @@ const BillboardReview: React.FunctionComponent<BillboardReviewProps> = ({ themeS
if (error) {
console.log("Billboard creation failed:", error);
}
return billboardData.data[0].data;
return billboardData;
} catch (err) {
console.error("Billboard creation failed:", err);
@ -221,12 +221,13 @@ const BillboardReview: React.FunctionComponent<BillboardReviewProps> = ({ themeS
if (data.gallery.length > 0) {
const uploadedPhotos = await uploadFiles(data.gallery, "Billboard");
const billoboard = await handleCreateBillboard(uploadedPhotos!);
bId = billoboard.id;
bId = billoboard.data[0].id;
} else {
// step-2 => create the billboard
const billoboard = await handleCreateBillboard(null);
bId = billoboard.id;
bId = billoboard.data[0].id;
}
// step-3 => create price unit
await handleCreatePriceUnit(bId);
// step-4 => publish the billboard

View File

@ -16,6 +16,9 @@ import UnsavedChangesGuard from 'components/general/unsaved-changes-guard';
import Modal from 'components/modal/modal';
import EventAlert from 'components/popover/event-alert';
import { TranslatableFieldsInfo } from 'components/general/form-tools';
import { State } from 'common/types/general';
import { useSelector } from 'react-redux';
import { getCountry } from 'common/redux/slices/global';
interface GeneralDetailsProps {
themeStyles: React.CSSProperties;
@ -73,6 +76,7 @@ const GeneralDetails: React.FunctionComponent<GeneralDetailsProps> = ({ themeSty
// variables
const globalData = useGetGlobalData();
const parentCats = globalData.billboard.filter((x:any) => x.has_children);
const country = useSelector(getCountry);
let currentSubCats: string[] = mainCat.names.length > 0 ?
globalData.billboard.filter((x: any) => !x.has_children).filter((x: any) => getLocaleTr(x.parent[0].related_billboard_categories_id, locale).name === mainCat.names[0]).map((x: any) => getLocaleTr(x, locale).name)
:
@ -111,7 +115,7 @@ const GeneralDetails: React.FunctionComponent<GeneralDetailsProps> = ({ themeSty
const { data: fieldsData, error: fieldsError } = useSWRImmutable(["", billboardFieldsQuery], ([route, query]) => fetchPublicData(route, query));
fieldsError && console.log(fieldsError);
const states = fieldsData?.data.States;
const states: State[] = fieldsData?.data.States.filter((x: State) => x.country.Initials === country);
const topCities = ["Vancouver", "North Vancouver", "West Vancouver", "Toronto", "Montreal", "Calgary", "Ottawa", "Victoria",
"Edmonton", "Quebec", "Mississauga", "Winnipeg", "Brampton", "Hamilton", "Surrey", "Halifax", "London", "Laval", "Markham",
"Vaughan", "Gatineau", "Saskatoon"

View File

@ -56,6 +56,7 @@ const PhotosGallery: React.FunctionComponent<PhotosGallery> = ({ pics }) => {
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "gallery",
fileIds: galleryData.current.picsToRemove,
billboardId: query.id,
}),

View File

@ -201,7 +201,7 @@ const ProductReview: React.FunctionComponent<ProductReviewProps> = ({ extraData,
if (error) {
console.log("Gallery thumb creation failed:", error);
}
return variantGalleryData.data.map((x:any) => x.data);
return variantGalleryData.data;
} catch (err) {
console.error("Gallery thumb creation failed:", err);
setUpdating(false);
@ -254,7 +254,7 @@ const ProductReview: React.FunctionComponent<ProductReviewProps> = ({ extraData,
console.log("Product price entry creation failed:", error);
}
return variantsData.data.map((x:any) => x.data);
return variantsData.data;
} catch (err) {
console.error("Product price entry creation failed:", err);
setUpdating(false);
@ -277,6 +277,7 @@ const ProductReview: React.FunctionComponent<ProductReviewProps> = ({ extraData,
]
});
const { data: productData, error } = results;
if (error) {
console.log("Product price entry creation failed:", error);
}
@ -330,7 +331,7 @@ const ProductReview: React.FunctionComponent<ProductReviewProps> = ({ extraData,
if (error) {
console.log("Product creation failed:", error);
}
return productData.data[0].data;
return productData.data[0];
} catch (err) {
console.error("Product creation failed:", err);

View File

@ -72,8 +72,8 @@ const GeneralLayout: React.FunctionComponent<GeneralLayout> = ({ children, bodyC
const { data: messagesData, error: messageFetchError, mutate } = useSWR(isUserDataAvailable ? [`${messagesDataQuery + contactListQuery}`, ""] : null, ([query, route]) => fetchPrivateData(route, query));
const { data: globalData, error: globalDataError } = useCachedGlobalData();
// const { data: globalData, error: globalDataError } = process.env.NODE_ENV === 'production' ? useCachedGlobalData() : useSWRImmutable([globalDataQuery, ""], ([query, route]) => fetchPublicData(route, query));
// const { data: globalData, error: globalDataError } = useCachedGlobalData();
const { data: globalData, error: globalDataError } = process.env.NODE_ENV === 'production' ? useCachedGlobalData() : useSWRImmutable([globalDataQuery, ""], ([query, route]) => fetchPublicData(route, query));
const loadingText = translate("general-loading");
if (messageFetchError) {

View File

@ -2,10 +2,12 @@ import useTranslate from "services/translation/translation";
import OrderSection from "./order-section";
import { BillboardOrdersItem } from "common/types/billboard";
import { getOrderStatusColors } from "services/billboard/orders";
import { hasValue, mtd } from "services/general/general";
import { hasValue, mtd, useGetRouter } from "services/general/general";
import OrderDetailItem from "./details-item";
import { BoxTapedSolid, CalendarLinesSolid, CircleQuestionSolid, ClockSolid, CreditCardSolid, PrintSolid, XmarkSolid } from "components/icons";
import { BoxTapedSolid, CalendarLinesSolid, CircleQuestionSolid, CreditCardSolid, PrintSolid, XmarkSolid } from "components/icons";
import Button from "components/button/button";
import { useState } from "react";
import EventAlert from "components/popover/event-alert";
interface OrderSummary {
order: BillboardOrdersItem;
@ -15,25 +17,67 @@ interface OrderSummary {
const OrderSummary: React.FunctionComponent<OrderSummary> = ({ order, onPayment, onPrint }) => {
// states
const [updating, setUpdating] = useState(false);
const [alertOpen, setAlertOpen] = useState(false);
// variables
const translate = useTranslate();
const { router } = useGetRouter();
const { orderStatusBg, orderStatusText } = getOrderStatusColors(order.order_status);
const isPaid = hasValue(order.payment_id);
const paymentStatus = order.payment_id ? translate("order-is-paid") : translate("order-is-unpaid");
const orderStatus = translate(`billboard-orders-status-${order.order_status}`);
const orderDate = mtd(new Date(order.date_created));
const iconClass = "inline-block size-4 fill-cool-gray me-2";
const showCancelButton = ["awaiting-payment", "pending"].includes(order.order_status);
const showCancelButton = ["pending"].includes(order.order_status) && order.cancel_period !== 0;
// methods
const openPaymentMethods = () => {
onPayment(true);
}
const handleCancelOrder = async () => {
setUpdating(true);
try {
const response = await fetch("/api/general/orders/cancel-pending-order", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
orderId: Number(order.id),
paymentReceiptId: order.payment_id
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to cancel order");
}
setUpdating(false);
setAlertOpen(true);
} catch (error) {
console.error("payment failed:", error);
setUpdating(false);
}
}
const onAlertClose = () => {
if (alertOpen) {
setAlertOpen(false);
alertOpen && router.reload();
}
}
return <OrderSection title={translate("dashboard-order-page-order-details-title")} hideTitle wrapperClass="col-span-2 relative">
<EventAlert
text={translate("checkout-page-payment-success-text")}
open={alertOpen}
type="success"
timeOut={5000}
onClose={onAlertClose}
hasTime
/>
<span className="flex items-center absolute left-4 top-4 sm:left-6 sm:top-6 text-lg sm:text-xl font-semibold p-[10px] bg-gray-100 print:hidden rounded-full lg:cursor-pointer" onClick={() => onPrint()}>
<PrintSolid className="inline-block size-5 sm:size-6 fill-text-gray" />
</span>

View File

@ -39,9 +39,10 @@ const CheckoutCore = ({ stores, shoppingCart, latestPrices, stockData, policiesD
return storeTotal;
}
const getStoreDeliveryMethods = (store_id: number, methods: DeliveryMethodsDataProps[]) => {
const methodsData = getOrderDeliveryMethods({ customer_city_id: customer_city_id, order_price: getOrdertotalPerStore(store_id), delivery_Methods: methods.filter(x => Number(x.business.id) === store_id) });
const methodsData = getOrderDeliveryMethods({ customer_city_id: customer_city_id, order_price: getOrdertotalPerStore(store_id), delivery_Methods: methods.filter(x => Number(x.business.id) === Number(store_id)) });
return methodsData;
}
// states
const [stockErrorItems, setStockErrorItems] = useState<number[]>([]);
const [orderTotal, setOrderTotal] = useState<any>([]);

View File

@ -34,7 +34,6 @@ const PaymentMethods = ({ isOpen = false, orderId, orderTotalAmount, cancelPerio
// variables
const translate = useTranslate();
const { locale, router } = useGetRouter();
const dispatch = useAppDispatch();
const methodIconClass = "inline-block size-6 fill-current me-4";
const paymentMethods = [
{ id: 1, title: translate("checkout-page-payment-method-card"), icon: <CreditCardSolid className={`${methodIconClass}`} /> },
@ -72,6 +71,7 @@ const PaymentMethods = ({ isOpen = false, orderId, orderTotalAmount, cancelPerio
const handleCardPayment = async (orderId: string, totalCents: number) => {
const paymentSession = await createStripePaymentSession({
mode: "payment",
capture_method: "manual",
payment_method_types: ["card"],
line_items: [
{

View File

@ -0,0 +1,68 @@
import { getCountry, setCountry } from "common/redux/slices/global";
import { CountryCode } from "common/types/general";
import { useEffect, useState } from "react"
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import useTranslate from "services/translation/translation";
interface CountrySelectorProps {
className?: string;
}
const CountrySelector = ({ className }: CountrySelectorProps) => {
// states
const [isOpen, setIsOpen] = useState(false);
// variables
const translate = useTranslate();
const dispatch = useDispatch();
const country = useSelector(getCountry);
const COUNTRIES: { code: CountryCode; label: string }[] = [
{ code: "IR", label: translate("iran") },
{ code: "CA", label: translate("canada") },
{ code: "US", label: translate("united-states") },
];
// 🔑 Decide visibility once, on mount
useEffect(() => {
const storedCountry = localStorage.getItem("country");
if (!storedCountry) {
setIsOpen(true);
}
}, []);
const handleSelect = (code: CountryCode) => {
dispatch(setCountry(code)); // persists via listener
setIsOpen(false);
};
if (!isOpen) return null;
return (
<div
className={`fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm ${className}`}
>
<div className="w-full max-w-lg rounded-xl bg-market-input p-gi shadow-lg">
<h2 className="mb-4 text-lg font-semibold text-secondary-light">{translate("country-selection-modal-title")}</h2>
<p className="mb-6 text-sm/7">{translate("country-selection-modal-text")}</p>
<div className="grid gap-3">
{COUNTRIES.map(({ code, label }) => (
<button
key={code}
onClick={() => handleSelect(code)}
className="flex items-center justify-between rounded-lg border border-market-border bg-white px-4 py-3 text-sm font-medium hover:bg-market-hover transition"
>
<span>{label}</span>
<span className="text-xs text-market-muted">{code}</span>
</button>
))}
</div>
</div>
</div>
);
};
export default CountrySelector;

View File

@ -3,62 +3,148 @@ import useTranslate from "services/translation/translation"
import { switchLocale } from "services/locales/locale-switch"
import ClickOutside from "components/click-outside/click-outside"
import { useGetRouter } from "services/general/general";
import { BillboardFormItem } from "common/templates/dashboard/billboards/general/fields";
import Select from "components/select/select";
import { useSelector } from "react-redux";
import { getCountry, setCountry } from "common/redux/slices/global";
import { useDispatch } from "react-redux";
import { CountryCode } from "common/types/general";
interface LanguageSelectProps {
trData?: any;
className?: string;
}
interface ListItemProps {
router: any;
targetLocale: string;
queryData: any;
}
const ListItem = ({ router, targetLocale, queryData }: ListItemProps) => {
const translate = useTranslate();
return <li className="flex items-center py-2 px-2 hover:bg-market-input" onClick={() => switchLocale({ router: router, targetLocale: targetLocale, queryData: queryData })}>
<img
className="w-6 rounded-full rtl:ml-1 ltr:mr-1 object-contain bg-gray-200"
src={targetLocale === "en" ? "/pics/flags/canada-square.png" : "/pics/flags/white-flag.svg"}
alt={targetLocale === "en" ? "English" : "Persian"}
/>
<span className={`text-sm capitalize px-2`}>{targetLocale === "en" ? translate("english") : translate("persian")}</span>
</li>
}
const LanguageSelect = ({ trData, className }: LanguageSelectProps) => {
// states
const [open, setOpen] = useState(false);
// variables
const { router } = useGetRouter();
// variables
const dispatch = useDispatch();
const translate = useTranslate();
const { router, locale } = useGetRouter();
const country = useSelector(getCountry);
const COUNTRIES = [
{ value: "IR", label: translate("iran"), flag: "/pics/flags/white-flag.svg" },
{ value: "CA", label: translate("canada"), flag: "/pics/flags/canada-square.png" },
{ value: "US", label: translate("united-states"), flag: "/pics/flags/usa-square.png" },
];
const LANGUAGES = [
{ value: "fa", label: translate("persian") },
{ value: "en", label: translate("english") },
];
const selectedCountry = COUNTRIES.find(c => c.value === country)!;
// methods
const handleCountrySelection = (value: string) => {
dispatch(setCountry(value as CountryCode));
};
const handleLanguageSelection = (targetLocale: "fa" | "en") => {
switchLocale({ router: router, targetLocale: targetLocale, queryData: trData })
};
const toggleLanguageMenu = () => {
setOpen(!open);
setOpen(prev => !prev);
};
return (
<ClickOutside className={`${className}`} onClickOutside={() => setOpen(false)}>
<div className={`select-none lg:cursor-pointer ml-6 relative ltr:[direction:ltr] z-20`} onClick={toggleLanguageMenu}>
<div className={`flex items-center ${router.asPath === "/" ? "bg-secondary-light/80" : "bg-light-gray"} py-[3px] px-1 rounded-full`}>
<img
className="w-6 h-6 rtl:rounded-r-full ltr:rounded-l-full object-contain bg-white"
src={router.locale === "en" ? "/pics/flags/canada-square.png" : "/pics/flags/white-flag.svg"}
alt={router.locale === "en" ? "English" : "Persian"}
<ClickOutside className={className} onClickOutside={() => setOpen(false)}>
<div className="relative ml-6 z-20 select-none">
<div className={`flex items-center cursor-pointer py-[3px] px-1 rounded-full ${router.asPath === "/" ? "bg-secondary-light/80" : "bg-light-gray"}`} onClick={toggleLanguageMenu}>
<img
className="w-6 h-6 rounded-full object-contain bg-white"
src={router.locale === "en"
? "/pics/flags/canada-square.png"
: "/pics/flags/white-flag.svg"}
alt={router.locale === "en" ? "English" : "Persian"}
/>
<span className={`text-sm uppercase px-2 ${router.asPath === "/" ? "text-white" : "text-secondary-light"}`}>{router.locale === "en" ? "en" : "fa"}</span>
<span className={`text-sm uppercase px-2 ${router.asPath === "/" ? "text-white" : "text-secondary-light"}`}>
{router.locale === "en" ? "en" : "fa"}
</span>
</div>
<div className={`${open ? "block" : "hidden"} bg-white w-64 pb-4 absolute left-1/2 -translate-x-1/2 mt-1 rounded-lg shadow`}>
{/* 🌍 Country Select */}
<BillboardFormItem
label={{
forId: "select-country",
title: translate("country"),
className: "!text-secondary-light font-semibold px-1"
}}
wrapperClass="flex flex-col justify-start pt-3 pb-4 px-4"
>
<Select
id="country-selection"
value={{
label: <div className="flex items-center">
<img
className="w-6 rounded-full me-2 object-contain bg-gray-200"
src={selectedCountry.flag}
alt={selectedCountry.label}
/>
{selectedCountry.label}
</div>,
value: selectedCountry.value,
disabled: false,
}}
options={COUNTRIES.map(c => ({
label: <div className="flex items-center">
<img
className="w-6 rounded-full me-3 object-contain bg-gray-200"
src={c.flag}
alt={c.label}
/>
{c.label}
</div>,
value: c.value,
disabled: false,
}))}
onChange={(data: any) => handleCountrySelection(data.value)}
optionsWrapper="inline-block w-full px-2 rounded-lg bg-white capitalize text-sm py-2"
optionClass="!text-sm"
buttonWrapper="!ring-0 bg-white"
buttonText="!text-sm"
wrapperClass="block bg-market-input rounded-xl py-1 px-1 !min-w-36"
/>
</BillboardFormItem>
{/* Language Selection */}
<BillboardFormItem
label={{
forId: "select-language",
title: translate("language"),
className: "!text-secondary-light font-semibold px-1"
}}
wrapperClass="flex flex-col justify-start pb-3 px-4"
>
<Select
id="language-selection"
value={{
label: locale === "fa" ? translate("persian") : translate("english"),
value: locale === "fa" ? "fa" : "en",
disabled: false,
}}
options={LANGUAGES.map(c => ({
label: c.label,
value: c.value,
disabled: false,
}))}
onChange={(data: any) => handleLanguageSelection(data.value)}
optionsWrapper="inline-block w-full px-2 rounded-lg bg-white capitalize text-sm py-2"
optionClass="!text-sm"
buttonWrapper="!ring-0 bg-white"
buttonText="!text-sm"
wrapperClass="block bg-market-input rounded-xl py-1 px-1 !min-w-36"
/>
</BillboardFormItem>
</div>
<ul className={`${open ? "block" : "hidden"} bg-white w-32 absolute left-1/2 -translate-x-1/2 mt-1 rounded-lg shadow overflow-hidden divide-y divide-gray-100`}>
<ListItem router={router} targetLocale="fa" queryData={trData} />
<ListItem router={router} targetLocale="en" queryData={trData} />
</ul>
</div>
</ClickOutside>
)
}
export default React.memo(LanguageSelect)
);
};
export default LanguageSelect;

View File

@ -12,6 +12,7 @@ import { setDictionary, setMenu } from "common/redux/slices/global";
import { useAuth } from "services/user/AuthContext";
import { globalDataQuery, useCachedGlobalData } from "services/general/global-data";
import { MessagesRecipient, NotificationsContactList } from "common/types/general";
import CountrySelector from "../country-selection/country-selection";
interface LayoutProps {
children: React.ReactNode;
@ -70,9 +71,9 @@ const Layout = ({ children, fullScreen = false, bgColor = "#fff", trData, header
([query, route]) => fetchPrivateData(route, query)
);
const { data: globalData, error: globalDataError } = useCachedGlobalData();
// const { data: globalData, error: globalDataError } = process.env.NODE_ENV === 'production' ? useCachedGlobalData() : useSWRImmutable([globalDataQuery, ""], ([query, route]) => fetchPublicData(route, query));
// const { data: globalData, error: globalDataError } = useCachedGlobalData();
const { data: globalData, error: globalDataError } = process.env.NODE_ENV === 'production' ? useCachedGlobalData() : useSWRImmutable([globalDataQuery, ""], ([query, route]) => fetchPublicData(route, query));
// methods
const setGlobalData = () => {
dispatch(setDictionary(globalData?.data.dictionary));
@ -82,7 +83,7 @@ const Layout = ({ children, fullScreen = false, bgColor = "#fff", trData, header
magazine: globalData?.data.magazine_categories,
}));
}
if (messageFetchError) {
console.log(messageFetchError);
}
@ -91,7 +92,7 @@ const Layout = ({ children, fullScreen = false, bgColor = "#fff", trData, header
const systemMessagesLength = newMessages?.filter(x => x.message.is_system).length;
const busniessMessagesLength: number = newMessages?.filter(x => !x.message.is_system).filter(x => contactList.some(y => y.billboard.id === x.sender.id && y.subscribed_at !== null && !hasValue(y.muted_at))).length;
const finalMessagesLength: number = systemMessagesLength + busniessMessagesLength;
// useEffects
useEffect(() => {
const localState = JSON.parse(localStorage.getItem('user') || "null");
@ -105,42 +106,38 @@ const Layout = ({ children, fullScreen = false, bgColor = "#fff", trData, header
dispatch(resetUserData(""))
}
}, []);
// useEffects
useEffect(() => {
globalData && setGlobalData();
}, [globalData]);
useEffect(() => {
if (uData && messagesData && finalMessagesLength !== user.inbox.new_messages) {
dispatch(setUserData(userObject(uData.data, finalMessagesLength)));
}
}, [messagesData]);
useEffect(() => {
mutate(messagesData);
}, [user]);
if (uData && (JSON.stringify(user) !== JSON.stringify(userObject(uData.data, (hasValue(messagesData) ? finalMessagesLength : 0))))) {
dispatch(setUserData(userObject(uData.data, (hasValue(messagesData) ? finalMessagesLength : 0))));
}
return (
<>
{/* <Head>
<link rel="preload" href={`${basePath()}/pics/g-data.json`} as="fetch" crossOrigin="anonymous" key="globalData" />
</Head> */}
<>
<div className={`block h-full relative ${headerFixed ? "top-[var(--menu-height)]" : ""}`} style={{'direction': 'rtl'}}>
<Header trData={trData} isFixed={headerFixed} />
<main style={{ "--bg": bgColor } as React.CSSProperties } className={`block w-full bg-[var(--bg)] ${fullScreen ? "lg:px-0" : "px-[var(--gi)] lg:px-[calc((100%_-_var(--max-width))/2)]"}`}>
{children}
</main>
<Footer />
</div>
<Cookies className="" />
</>
<div className={`block h-full relative ${headerFixed ? "top-[var(--menu-height)]" : ""}`} style={{ 'direction': 'rtl' }}>
<Header trData={trData} isFixed={headerFixed} />
<main style={{ "--bg": bgColor } as React.CSSProperties} className={`block w-full bg-[var(--bg)] ${fullScreen ? "lg:px-0" : "px-[var(--gi)] lg:px-[calc((100%_-_var(--max-width))/2)]"}`}>
{children}
</main>
<Footer />
</div>
<CountrySelector className="" />
<Cookies className="" />
</>
)
}

View File

@ -820,6 +820,7 @@ export type BillboardOrdersItem = {
billboard_order_id: number;
user_id: UUID;
payment_id: UUID;
stripe_payment_id: string | any;
payment_method: "wallet" | "payment_gateway" | "cash";
payment_date: string;
currency: Currencies;

View File

@ -344,4 +344,6 @@ export type ApiResponse<T = any> = {
data: T | undefined;
error: any;
status?: number;
};
};
export type CountryCode = 'IR' | 'CA' | 'US';

View File

@ -85,6 +85,7 @@ export const CreateOrderSchema = z.array(z.object({
estimated_delivery_date: z.string().nullable(),
desired_delivery_date: z.string().nullable(),
delivery_code: z.string(),
delivery_proof_type: z.string().nullable(),
delivery_proof_token: z.string().nullable(),
}));

View File

@ -41,6 +41,11 @@ const WalletTopUpPaymentSchema = BasePaymentDataSchema.extend({
});
const PurchaseOrderPaymentSchema = BasePaymentDataSchema.extend({
receipt_type: z.literal("purchase-order"),
billboard_orders: z.array(
z.object({
billboard_orders_id: z.string()
}),
),
// Add purchase-order-specific fields here
});
const ServiceOrderPaymentSchema = BasePaymentDataSchema.extend({

31
src/lib/general/cors.ts Normal file
View File

@ -0,0 +1,31 @@
import type { NextApiRequest, NextApiResponse } from 'next';
const trustedOrigins = [
'http://localhost:3000',
'http://192.168.1.105:3000',
'https://flierland.ca',
];
export function applyCors(req: NextApiRequest, res: NextApiResponse) {
const origin = req.headers.origin;
if (origin && trustedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader(
'Access-Control-Allow-Headers',
'Content-Type, X-CSRF-Token'
);
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, PUT, PATCH, DELETE, OPTIONS'
);
}
if (req.method === 'OPTIONS') {
res.status(200).end();
return true;
}
return false;
}

View File

@ -2,22 +2,12 @@ let csrfToken: string | null = null;
export function setCsrfToken(token: string) {
csrfToken = token;
if (typeof window !== 'undefined') {
localStorage.setItem('csrfToken', token);
}
}
export function getCsrfToken(): string | null {
if (csrfToken) return csrfToken;
if (typeof window !== 'undefined') {
return localStorage.getItem('csrfToken');
}
return null;
return csrfToken;
}
export function clearCsrfToken() {
csrfToken = null;
if (typeof window !== 'undefined') {
localStorage.removeItem('csrfToken');
}
}

View File

@ -0,0 +1,12 @@
import { CountryCode } from "common/types/general";
export const getInitialCountry = (): CountryCode => {
if (typeof window === 'undefined') return 'IR';
const stored = localStorage.getItem('country');
if (stored === 'IR' || stored === 'CA' || stored === 'US') {
return stored;
}
return 'IR';
};

View File

@ -0,0 +1,20 @@
export const COUNTRIES = ['ir', 'ca', 'us'] as const;
export const LOCALES = ['fa', 'en'] as const;
export function getCountryStaticPaths() {
const paths = [];
for (const country of COUNTRIES) {
for (const locale of LOCALES) {
paths.push({
params: { country },
locale,
});
}
}
return {
paths,
fallback: false,
};
}

View File

@ -66,15 +66,15 @@ export const getOrderDeliveryMethods = ({ customer_city_id, order_price, deliver
return (item.free_shipping_threshold !== -1 && order_price >= item.free_shipping_threshold) ? 0 : shippingCost;
}
delivery_Methods.filter(x =>
x.active &&
x.active &&
(x.type === "store_pickup" ? true : (isCitySupported(x, customer_city_id) && isInArea(x, customer_city_id)))
)
.map(x => availableDeliveryMethods.push(
{
price: x.type === "store_pickup" ? 0 : getPrice(x, customer_city_id),
data: x
}
));
.map(x => availableDeliveryMethods.push(
{
price: x.type === "store_pickup" ? 0 : getPrice(x, customer_city_id),
data: x
}
));
return availableDeliveryMethods;
};

View File

@ -29,7 +29,9 @@ interface WalletTopUpData {
}
interface PurchaseOrderData {
billboard_orders: {
billboard_orders_id: string;
}[];
}
interface ServiceOrderData {

View File

@ -13,10 +13,13 @@ export interface CheckoutSessionParams {
metadata?: Record<string, any>;
success_url?: string;
cancel_url?: string;
capture_method?: 'manual' | 'automatic';
}
export const createCheckoutSession = async (params: CheckoutSessionParams) => {
// Create the Stripe checkout session with a success_url that includes session_id
const { capture_method, ...restParams } = params;
const session = await stripe.checkout.sessions.create({
...params,
success_url: `${params.success_url}?success=true`,

View File

@ -10,9 +10,10 @@ interface CreateStripePaymentSessionProps {
customer: string;
success_url?: string;
cancel_url?: string;
capture_method?: 'manual' | 'automatic';
}
export const createStripePaymentSession = async ({ mode, payment_method_types, line_items, metadata, customer, success_url, cancel_url }: CreateStripePaymentSessionProps) => {
export const createStripePaymentSession = async ({ mode, payment_method_types, line_items, metadata, customer, capture_method, success_url, cancel_url }: CreateStripePaymentSessionProps) => {
const response = await fetch(`${basePath()}/api/stripe/create-checkout-session`, {
method: "POST",
@ -25,6 +26,9 @@ export const createStripePaymentSession = async ({ mode, payment_method_types, l
line_items: line_items,
metadata: metadata,
customer: customer,
payment_intent_data: {
capture_method: capture_method || 'automatic',
},
success_url: success_url,
cancel_url: cancel_url,
}),

View File

@ -13,8 +13,12 @@ export async function storePurchaseOrderPaid(session: Stripe.Checkout.Session, s
}
try {
// 1. Determine if this was a "Hold" or a "Charge"
// If you aren't passing capture_method in metadata,
// you can fetch the PaymentIntent or just rely on your cancel_period logic.
const isManualHold = Number(metaData.cancel_period) > 0;
// call payment receipt creation api
// 2. Create Payment Receipt
const results = await CreatePaymentReceipt({
status: "published",
receipt_type: metaData.receipt_type,
@ -22,8 +26,9 @@ export async function storePurchaseOrderPaid(session: Stripe.Checkout.Session, s
stripe_customer_id: metaData.stripe_customer_id,
stripe_payment_id: session.payment_intent,
payment_method: "payment_gateway",
payment_status: "succeeded",
payment_status: isManualHold ? "requires_capture" : "succeeded",
order_amount: metaData.order_amount,
billboard_orders: [{ billboard_orders_id: metaData.order_id }],
payed_amount: String(metaData.order_amount),
discount_used: false,
currency: String(getCurrencyId({ abbr: session.currency as string })),
@ -31,15 +36,16 @@ export async function storePurchaseOrderPaid(session: Stripe.Checkout.Session, s
const { data: receiptData, error: receiptError } = await results.data;
// update order status & payment details
// 3. Update order status & payment details
await restBulkUpdateRequest({
collection: 'billboard_orders',
updates: [
{
id: metaData.order_id,
data: {
order_status: Number(metaData.cancel_period) === 0 ? "confirmed" : "pending",
order_status: isManualHold ? "pending" : "confirmed",
payment_id: receiptData.data[0].id,
stripe_payment_id: session.payment_intent,
payment_method: "payment_gateway",
payment_date: receiptData.data[0].date_created,
cancel_deadline: new Date(getCancelDeadline(receiptData.data[0].date_created, Number(metaData.cancel_period))).toISOString(),
@ -49,9 +55,7 @@ export async function storePurchaseOrderPaid(session: Stripe.Checkout.Session, s
systemSecret: secret
});
} catch (error) {
console.error('Error updating user wallet:', error);
console.error('Error updating purchase order:', error);
}
}

View File

@ -1,41 +1,25 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { randomBytes } from 'crypto';
import { applyCors } from 'lib/general/cors';
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const origin = req.headers.origin;
const trustedOrigins = [
"http://localhost:3000",
"http://192.168.1.105:3000",
"https://flierland.ca",
];
if (applyCors(req, res)) return;
if (origin && trustedOrigins.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
if (req.method !== 'GET') {
return res.status(405).json({ message: 'Method not allowed' });
}
if (req.method !== "GET") {
return res.status(405).json({ message: "Method not allowed" });
const sessionToken = req.cookies['directus_session_token'];
if (!sessionToken) {
return res.status(401).json({ message: 'Not authenticated' });
}
try {
const sessionToken = req.cookies['directus_session_token'];
if (!sessionToken) {
return res.status(401).json({ message: 'Not authenticated: No session token' });
}
const csrfToken = randomBytes(32).toString('hex');
// Generate a random CSRF token
const csrfToken = randomBytes(32).toString('hex');
res.setHeader('Set-Cookie', [
`_csrf=${csrfToken}; Path=/; HttpOnly; SameSite=${process.env.NODE_ENV === 'production' ? 'Strict' : 'Lax'
}; ${process.env.NODE_ENV === 'production' ? 'Secure' : ''}`,
]);
// Set CSRF token in a cookie
res.setHeader('Set-Cookie', [
`_csrf=${csrfToken}; Path=/; SameSite=Lax; HttpOnly; ${process.env.NODE_ENV === 'production' ? 'Secure' : ''
}`,
]);
res.status(200).json({ csrfToken });
} catch (error) {
console.error('Error generating CSRF token:', error);
res.status(500).json({ message: 'Internal server error' });
}
}
res.status(200).json({ csrfToken });
}

View File

@ -2,6 +2,7 @@ import { z } from 'zod';
import type { NextApiRequest, NextApiResponse } from 'next';
import { cmsAddress } from 'services/general/general';
import { passwordValidationRegex } from 'common/templates/authentication/sign-up';
import { applyCors } from 'lib/general/cors';
const LoginSchema = z.object({
email: z.string().email('Invalid email'),
@ -9,22 +10,7 @@ const LoginSchema = z.object({
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const origin = req.headers.origin;
const trustedOrigins = [
'http://192.168.1.105:3000',
'http://localhost:3000',
'https://flierland.ca',
];
// Always handle CORS
if (origin && trustedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-CSRF-Token');
} else {
console.warn(`Blocked CORS request from origin: ${origin}`);
return res.status(403).json({ success: false, message: 'CORS origin not allowed' });
}
if (applyCors(req, res)) return;
if (req.method !== 'POST') {
return res.status(405).json({ success: false, message: 'Method not allowed' });

View File

@ -4,22 +4,20 @@ import { cmsAddress } from 'services/general/general';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await axios.post(`${cmsAddress()}/auth/logout`,
await axios.post(
`${cmsAddress()}/auth/logout`,
{ mode: 'session' },
{
"mode": "session"
},
{
headers: {
Cookie: req.headers.cookie // Pass the client's cookies to Directus
},
withCredentials: true // Ensure credentials (cookies) are included in requests
headers: { Cookie: req.headers.cookie },
withCredentials: true,
}
);
} catch { }
res.setHeader('Set-Cookie', 'directus_session_token=; Path=/; Max-Age=0;');
res.status(200).end();
res.setHeader('Set-Cookie', [
'directus_session_token=; Path=/; Max-Age=0;',
'_csrf=; Path=/; Max-Age=0;',
]);
} catch (error) {
res.status(500).json({ message: 'Failed to sign out' });
}
res.status(200).end();
}

View File

@ -4,6 +4,14 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { adminAccessToken, cmsAddress } from "services/general/general";
import { restBulkDeleteRequest } from "services/queries/directus/billboard";
import { getUserFromRequest } from "services/user/get-user-from-request";
import z from "zod";
const BillboardFileDeleteSchema = z.object({
type: z.enum(["logo", "cover", "gallery"]),
billboardId: z.string(),
fileIds: z.array(z.string()).min(1, "File(s) ID required"),
});
// DELETE multiple files (logo / cover_photo , etc.) if owned by the user
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@ -17,11 +25,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(401).json({ error: "Unauthorized" });
}
const { fileIds, billboardId } = req.body as { fileIds?: string[]; billboardId: string };
if (!fileIds || !Array.isArray(fileIds) || fileIds.length === 0) {
return res.status(400).json({ error: "No file IDs provided" });
const parsed = BillboardFileDeleteSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({
error: "Invalid payload",
detail: parsed.error.errors,
});
}
// safe, type validated body data
const { type, fileIds, billboardId } = parsed.data;
// --- Check ownership of billboards ---
const query = `
query BillboardWithFiles {
@ -70,9 +84,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const billboardFileIds = new Set<string>();
const allowedFileIds = new Set<string>();
billboardFileIds.add(billboard.logo.id);
billboardFileIds.add(billboard.cover_photo.id);
billboard.gallery.map(x => billboardFileIds.add(x.directus_files_id.id));
switch (type) {
case "logo":
billboardFileIds.add(billboard.logo.id);
break;
case "cover":
billboardFileIds.add(billboard.cover_photo.id);
break;
case "gallery":
billboard.gallery.map(x => billboardFileIds.add(x.directus_files_id.id));
break;
default:
break;
}
fileIds.filter(x => Array.from(billboardFileIds).includes(x)).map(x => allowedFileIds.add(x));

View File

@ -0,0 +1,108 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { adminAccessToken, cmsAddress } from "services/general/general";
import { safeJson } from "lib/general/safe-response-json";
import { restBulkUpdateRequest } from "services/queries/directus/billboard";
import Stripe from "stripe";
import { getStripeSecretKey } from "services/general/stripe";
import { BillboardOrdersItem } from "common/types/billboard";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") return res.status(405).json({ error: "Method Not Allowed" });
if (req.headers["x-system-secret"] !== process.env.SYSTEM_API_SECRET) {
return res.status(401).json({ message: "Invalid security token" });
}
const stripe = new Stripe(getStripeSecretKey(), { apiVersion: '2025-08-27.basil' });
try {
// 1. Get current time in ISO format for GraphQL
const now = new Date().toISOString();
const query = `
query GetExpiredOrders {
billboard_orders (
filter: {
status: { _eq: "published" },
order_status: { _eq: "pending" },
cancel_deadline: { _lte: "${now}" }
},
limit: 100,
sort: ["cancel_deadline"]
) {
id
payment_id
stripe_payment_id
}
}
`;
const response = await fetch(`${cmsAddress()}/graphql`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${adminAccessToken}`,
},
body: JSON.stringify({
query,
}),
});
const { data: verificationData } = await safeJson(response);
const orders: BillboardOrdersItem[] = verificationData?.data?.billboard_orders || [];
if (orders.length === 0) {
return res.status(200).json({ message: "No orders to capture" });
}
let capturedOrders: BillboardOrdersItem[] = [];
// 2. Capture funds one by one
for (const order of orders) {
try {
if (!order.stripe_payment_id) continue;
const intent = await stripe.paymentIntents.capture(order.stripe_payment_id);
if (intent.status === 'succeeded') {
capturedOrders.push(order);
}
} catch (stripeErr) {
console.error(`Stripe capture failed for order ${order.id}:`, stripeErr);
// NEW: Mark this specific order as failed so it doesn't clog your cron job
await restBulkUpdateRequest({
collection: 'billboard_orders',
updates: [{ id: order.id, data: { order_status: 'failed' } }],
systemSecret: process.env.SYSTEM_API_SECRET
});
}
}
if (capturedOrders.length > 0) {
// 3. Update Order Status
await restBulkUpdateRequest({
collection: 'billboard_orders',
updates: capturedOrders.map(x => ({ id: x.id, data: { order_status: 'confirmed' } })),
systemSecret: process.env.SYSTEM_API_SECRET
});
// 4. Update Payment Status (Note: payment_id might be an object or string depending on your SDK)
await restBulkUpdateRequest({
collection: 'Payments',
updates: capturedOrders.map(x => ({
id: x.payment_id,
data: { payment_status: 'succeeded' }
})),
systemSecret: process.env.SYSTEM_API_SECRET
});
}
return res.status(200).json({
message: `Successfully captured ${capturedOrders.length} of ${orders.length} orders.`
});
} catch (error: any) {
console.error("Cron Job Error:", error);
return res.status(500).json({ error: "Internal Server Error" });
}
}

View File

@ -0,0 +1,125 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getUserFromRequest } from "services/user/get-user-from-request";
import { adminAccessToken, cmsAddress } from "services/general/general";
import { safeJson } from "lib/general/safe-response-json";
import { restBulkUpdateRequest } from "services/queries/directus/billboard";
import Stripe from "stripe";
import { getStripeSecretKey } from "services/general/stripe";
import { BillboardOrdersItem } from "common/types/billboard";
import { PaymentData } from "common/types/payments";
import z from "zod";
// ✅ Inline Zod schema for validation
const OrderCancellationSchema = z.object({
orderId: z.number(),
paymentReceiptId: z.string().uuid(),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method Not Allowed" });
}
// get user data from the session token
const user = await getUserFromRequest(req);
if (!user) {
return res.status(401).json({ error: "Unauthorized" });
}
// Parse & validate early — replaces manual checks
const parsedInput = OrderCancellationSchema.safeParse(req.body);
if (!parsedInput.success) {
return res.status(400).json({
error: `Invalid payload: ${parsedInput.error}`
});
}
const { orderId, paymentReceiptId } = parsedInput.data;
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2025-08-27.basil',
});
try {
// 1. Check if provided orderId actually exists & if the user is actually the owner of this order (not just some random user)
const query = `
query VerifyOrderDetails {
billboard_orders (
filter: {
id: { _eq: "${orderId}" },
status: { _eq: "published" },
order_status: { _eq: "pending" },
user_id: { _eq: "${user.id}" }
}
) {
id
}
Payments (
filter: {
id: { _eq: "${paymentReceiptId}" },
status: { _eq: "published" },
owner_user_id: { _eq: "${user.id}" },
billboard_orders: { billboard_orders_id: { id: { _eq: "${orderId}" } } }
}
) {
id
stripe_payment_id
}
}
`;
const response = await fetch(`${cmsAddress()}/graphql`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${adminAccessToken}`,
},
body: JSON.stringify({
query,
}),
});
const { data: verificationData, error: verificationError } = await safeJson(response);
if (!response.ok) {
throw new Error(`GraphQL request failed: ${verificationError}`);
}
const order: BillboardOrdersItem = verificationData.data.billboard_orders[0];
const payment: PaymentData = verificationData.data.Payments[0];
const stripePaymentId = payment?.stripe_payment_id;
if (!order || !payment || !stripePaymentId) {
return res.status(400).json({ error: "Invalid order details" });
}
// 2. Tell Stripe to release the hold (Instant & Free)
try {
await stripe.paymentIntents.cancel(stripePaymentId);
} catch (err: any) {
// If it's already canceled, we can ignore the error and proceed to update the DB
if (err.code !== 'payment_intent_unexpected_state') {
throw err;
}
}
// 3. Update order status to cancelled
await restBulkUpdateRequest({
collection: 'billboard_orders',
updates: [{ id: String(orderId), data: { order_status: 'canceled' } }],
systemSecret: process.env.SYSTEM_API_SECRET
});
// 4. Update payment status to cancelled
await restBulkUpdateRequest({
collection: 'Payments',
updates: [{ id: paymentReceiptId, data: { payment_status: 'canceled' } }],
systemSecret: process.env.SYSTEM_API_SECRET
});
return res.status(200).json({ message: "Order canceled. The temporary hold on your card will be released within 2448 hours (times vary by bank)." });
} catch (error: any) {
console.error("Failed to release funds:", error);
return res.status(500).json({ error: "Failed to release funds." });
}
}

View File

@ -41,17 +41,29 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
try {
switch (event.type) {
case "checkout.session.completed":
// 1. Get the payment_intent ID from the session
// 2. Mark your order as "Pending" in your DB
// 3. Store the payment_intent ID (pi_...) so your Cron can find it
await handleCheckoutSessionCompleted(event.data.object as Stripe.Checkout.Session, process.env.SYSTEM_API_SECRET!);
break;
case "payment_intent.amount_capturable_updated":
// Optional but recommended: This event confirms the funds are officially held.
// You could use this to safely move the order from "Awaiting Payment" to "Pending (In Window)"
const intent = event.data.object as Stripe.PaymentIntent;
// console.log(`Funds held for ${intent.id}. Ready for capture later.`);
break;
case "payment_intent.succeeded":
// This will now only fire AFTER your Cron job runs the capture command.
// Use this to send the final "Payment Confirmed" email to the customer.
await handlePaymentIntentSucceeded(event.data.object as Stripe.PaymentIntent);
break;
case "invoice.payment_succeeded":
await handleInvoicePaid(event.data.object as Stripe.Invoice);
break;
case "payment_intent.succeeded":
await handlePaymentIntentSucceeded(event.data.object as Stripe.PaymentIntent);
break;
case "customer.created":
await handleCustomerCreated(event.data.object as Stripe.Customer);
break;

View File

@ -26,7 +26,7 @@ const BillboardProductPage: React.FunctionComponent<any> = ({ data, params }) =>
const serviceTypes: BillboardServiceTypes[] = data.service_types;
const productCats: BillboardProductsCategory[] = data.billboard_product_categories;
const pageTitle = getLocaleTr(product, locale).title;
const metaDescription = stripHtml(getLocaleTr(product, locale).description);
const metaDescription = stripHtml(getLocaleTr(product, locale).description ?? "");
const pageUrl = url(`${basePath()}/${locale === 'en' ? 'en/' : ''}billboard/${billboardId}/${url(billboardTitle)}/products/${product.product_id}`);
const productTumb = `${serverAddress()}${thumbs[0].gallery[0].directus_files_id.id}`;
const productBrand = product.brand?.english_name ?? "";
@ -41,7 +41,7 @@ const BillboardProductPage: React.FunctionComponent<any> = ({ data, params }) =>
type: `website`,
}
}
return (
<>
<Meta data={metaData} />
@ -115,26 +115,31 @@ export async function getStaticPaths() {
`
});
const hasFaRoute = data.billboard_products.some((x: any) =>
x.billboard[0].Billboards_id.translations.some((y: any) => y.languages_code.code.slice(0, 2) === "fa") &&
const hasFaRoute = data.billboard_products.some((x: any) =>
x.billboard[0].Billboards_id.translations.some((y: any) => y.languages_code.code.slice(0, 2) === "fa") &&
x.translations.some((y: any) => y.languages_code.code.slice(0, 2) === "fa")
);
const hasEnRoute = data.billboard_products.some((x: any) =>
const hasEnRoute = data.billboard_products.some((x: any) =>
x.billboard[0].Billboards_id.translations.some((y: any) => y.languages_code.code.slice(0, 2) === "en") &&
x.translations.some((y: any) => y.languages_code.code.slice(0, 2) === "en")
);
const faPath = hasFaRoute ? data.billboard_products.map((x: any) => (
{ params: { id: x.billboard[0].Billboards_id.id, billboard: url(x.billboard[0].Billboards_id.translations.filter((y: any) => y.languages_code.code.slice(0, 2) === "fa")[0].title), pid: String(x.product_id) }, locale: 'fa' }
)) : [];
const enPath = hasEnRoute ? data.billboard_products.map((x: any) => (
{ params: { id: x.billboard[0].Billboards_id.id, billboard: url(x.billboard[0].Billboards_id.translations.filter((y: any) => y.languages_code.code.slice(0, 2) === "en")[0].title), pid: String(x.product_id) }, locale: 'en' }
const faPath = hasFaRoute ? data.billboard_products
.filter((x: any) => x.translations.some((y: any) => y.languages_code.code.slice(0, 2) === "fa"))
.map((x: any) => (
{ params: { id: x.billboard[0].Billboards_id.id, billboard: url(x.billboard[0].Billboards_id.translations.filter((y: any) => y.languages_code.code.slice(0, 2) === "fa")[0].title), pid: String(x.product_id) }, locale: 'fa' }
)) : [];
const enPath = hasEnRoute ? data.billboard_products
.filter((x: any) => x.translations.some((y: any) => y.languages_code.code.slice(0, 2) === "en"))
.map((x: any) => (
{ params: { id: x.billboard[0].Billboards_id.id, billboard: url(x.billboard[0].Billboards_id.translations.filter((y: any) => y.languages_code.code.slice(0, 2) === "en")[0].title), pid: String(x.product_id) }, locale: 'en' }
)) : [];
return { paths: [...faPath, ...enPath], fallback: 'blocking' }
}
export async function getStaticProps({ locale, params } : any) {
export async function getStaticProps({ locale, params }: any) {
const billboardId = params.id;
const productId = Number(params.pid);
const langCode = locale === "fa" ? "fa-IR" : "en-US";

View File

@ -80,8 +80,8 @@ const BillboardCheckOut = () => {
const shoppingCart: ShoppingCartItem[] = useAppSelector(getShoppingCart);
const infoTextIconClass = "inline-block size-[14px] -mt-[2px] sm:size-5 text-secondary-light fill-current me-2 sm:me-3"
const { data: globalData, error: globalDataError } = useCachedGlobalData();
// const { data: globalData, error: globalDataError } = process.env.NODE_ENV === 'production' ? useCachedGlobalData() : useSWRImmutable([globalDataQuery, ""], ([query, route]) => fetchPublicData(route, query));
// const { data: globalData, error: globalDataError } = useCachedGlobalData();
const { data: globalData, error: globalDataError } = process.env.NODE_ENV === 'production' ? useCachedGlobalData() : useSWRImmutable([globalDataQuery, ""], ([query, route]) => fetchPublicData(route, query));
// get store's orders data
const billboardOrdersQuery = (ids: number[]) => `

View File

@ -5,6 +5,9 @@ import InnerLoading from "components/loading/inner-loading";
import useTranslate from "services/translation/translation";
import EditDetails from "common/templates/dashboard/account/edit-details";
import { useAuth } from "services/user/AuthContext";
import { useSelector } from "react-redux";
import { getCountry } from "common/redux/slices/global";
import { City } from "common/types/general";
interface NewAdFrom {
@ -17,6 +20,7 @@ const NewAdFrom: React.FunctionComponent<NewAdFrom> = ({ }) => {
// variables
const translate = useTranslate();
const { isAuthenticated } = useAuth();
const country = useSelector(getCountry);
const GetUserDetails = `
users_me {
@ -51,7 +55,7 @@ const NewAdFrom: React.FunctionComponent<NewAdFrom> = ({ }) => {
}
`
const fieldsQuery = `
Cities (filter: { status: { _eq: "published" } }, limit: -1) {
Cities (filter: { status: { _eq: "published" }, country: { Initials: { _eq: "${country}" } } }, limit: -1) {
id
name
English_name
@ -65,6 +69,8 @@ const NewAdFrom: React.FunctionComponent<NewAdFrom> = ({ }) => {
userDetailsError && console.log("something went wrong:" + userDetailsError);
const cities: City[] = fieldsData?.data.Cities;
// methods
// useEffects
@ -72,7 +78,7 @@ const NewAdFrom: React.FunctionComponent<NewAdFrom> = ({ }) => {
// return
return <GeneralLayout>
{userDetails && fieldsData ?
<EditDetails userDetails={userDetails.data.users_me} cities={fieldsData.data.Cities} />
<EditDetails userDetails={userDetails.data.users_me} cities={cities} />
:
<InnerLoading loadingText={""} width={"100"} height={"195"} />
}

View File

@ -5,7 +5,7 @@ import ThemeColor, { inputColors } from "common/templates/dashboard/billboards/b
import GeneralLayout from "common/templates/dashboard/navigation/general-layout";
import { Billboard } from "common/types/billboard";
import InnerLoading from "components/loading/inner-loading";
import { deepClone, fetchPublicData, getLocaleTr, hasValue, useGetRouter } from "services/general/general";
import { deepClone, fetchPrivateData, fetchPublicData, getLocaleTr, hasValue, useGetRouter } from "services/general/general";
import useTranslate from "services/translation/translation";
import useSWR from "swr";
import { PlusSolid } from "components/icons";
@ -51,9 +51,9 @@ const FaqPage: React.FunctionComponent<FaqPageProps> = ({ }) => {
ownership: { directus_users_id: { id: { _eq: "${user.generalDetails.id}" }}},
id : { _eq: "${query.id}" }
},
sort: ["sort", "-date_created"],
sort: ["sort", "-date_created"],
limit: -1
)
)
{
id
translations {
@ -75,7 +75,7 @@ const FaqPage: React.FunctionComponent<FaqPageProps> = ({ }) => {
[`${
billboardQuery
}`, ""],
([query, route]) => fetchPublicData(route, query)
([query, route]) => fetchPrivateData(route, query)
);
staticDataFetchError && console.log(staticDataFetchError);

View File

@ -147,6 +147,9 @@ const CreateProduct: React.FunctionComponent<CreateProduct> = ({ }) => {
name
English_name
Initials
country {
Initials
}
}
`

View File

@ -175,6 +175,7 @@ const OrderDetailsPage: React.FunctionComponent<OrderDetailsPageProps> = ({ }) =
const openPaymentMethods = async () => {
setShowPaymentMethod(true);
}
const handlePrint = useReactToPrint({
contentRef: receiptRef,
documentTitle: `${translate("dashboard-user-orders-table-titles-order")} : # ${order?.id}`,

View File

@ -1,7 +1,6 @@
import React, { useEffect } from "react"
import React from "react"
import { gql } from "@apollo/client";
import client from "../common/data/apollo-client";
import { useAppDispatch, useAppSelector } from '../common/redux/hooks';
import client from "common/data/apollo-client";
import useTranslate from "services/translation/translation";
import { basePath, stripHtml, useGetRouter } from 'services/general/general'
import Meta from "components/meta/meta"

View File

@ -29,8 +29,8 @@ const InstallPage: React.FC = () => {
const dispatch = useAppDispatch();
const isInstallable = useAppSelector(state => state.global.isInstallable);
const { data: globalData, error: globalDataError } = useCachedGlobalData();
// const { data: globalData, error: globalDataError } = process.env.NODE_ENV === 'production' ? useCachedGlobalData() : useSWRImmutable([globalDataQuery, ""], ([query, route]) => fetchPublicData(route, query));
// const { data: globalData, error: globalDataError } = useCachedGlobalData();
const { data: globalData, error: globalDataError } = process.env.NODE_ENV === 'production' ? useCachedGlobalData() : useSWRImmutable([globalDataQuery, ""], ([query, route]) => fetchPublicData(route, query));
// methods
const setGlobalData = () => {

View File

@ -1,8 +1,6 @@
import React, { useEffect, useState } from "react"
import { gql } from "@apollo/client";
import { useAppDispatch } from '../common/redux/hooks';
import client from "../common/data/apollo-client";
import { setDictionary, setMenu } from '../common/redux/slices/global'
import { getCountry, setDictionary, setMenu } from '../common/redux/slices/global'
import useTranslate from "services/translation/translation"
import Link from "components/link/link"
import Tabs from "components/tabs/tabs";
@ -11,24 +9,41 @@ import Login from "common/templates/authentication/login";
import SignUp from "common/templates/authentication/sign-up";
import { globalDataQuery, useCachedGlobalData } from "services/general/global-data";
import useSWRImmutable from "swr";
import { fetchPublicData, useGetRouter } from "services/general/general";
import { fetchPublicData } from "services/general/general";
import { useSelector } from "react-redux";
import { City } from "common/types/general";
import InnerLoading from "components/loading/inner-loading";
interface SignIn {
data: any,
}
const SignIn: React.FunctionComponent<SignIn> = ({ data }) => {
const SignIn: React.FunctionComponent<SignIn> = ({ }) => {
// states
const [currentTab, setCurrentTab] = useState(1);
// variables
// variables
const dispatch = useAppDispatch();
const translate = useTranslate();
const { router } = useGetRouter();
const country = useSelector(getCountry);
const { data: globalData, error: globalDataError } = useCachedGlobalData();
// const { data: globalData, error: globalDataError } = process.env.NODE_ENV === 'production' ? useCachedGlobalData() : useSWRImmutable([globalDataQuery, ""], ([query, route]) => fetchPublicData(route, query));
// const { data: globalData, error: globalDataError } = useCachedGlobalData();
const { data: globalData, error: globalDataError } = process.env.NODE_ENV === 'production' ? useCachedGlobalData() : useSWRImmutable([globalDataQuery, ""], ([query, route]) => fetchPublicData(route, query));
const citiesDataQuery = `
Cities (filter: { status: { _eq: "published" }, country: { Initials: { _eq: "${country}" } } }, limit: -1) {
id
name
English_name
state {
name
}
}
`
// get cities data
const { data: citiesData, error: citiesError } = useSWRImmutable(country ? [`${citiesDataQuery}`, ""] : null, ([query, route]) => fetchPublicData(route, query));
const cities: City[] = citiesData?.data.Cities;
// methods
const setGlobalData = () => {
@ -39,7 +54,7 @@ const SignIn: React.FunctionComponent<SignIn> = ({ data }) => {
magazine: globalData?.data.magazine_categories,
}));
}
// useEffects
useEffect(() => {
globalData && setGlobalData();
@ -47,47 +62,32 @@ const SignIn: React.FunctionComponent<SignIn> = ({ data }) => {
// return
return (
<div className="block w-screen relative h-screen">
< div className="block w-screen relative h-screen" >
<div className="block w-screen h-screen bg-login-page-mobile lg:bg-login-page-desktop bg-repeat md:relative max-md:[background-size:250px_250px]"></div>
<div className="absolute lg:backdrop-blur-none w-full md:w-[500px] min-h-[100%] right-1/2 translate-x-1/2 overflow-y-scroll max-h-screen hide-scrollbar top-0 px-6 md:px-8 pt-1 pb-5 bg-gradient-to-b from-white/[1.0] via-white/[1.0] to-white/[0.95] md:from-white/100 md:via-white/[1.0] md:to-white/[0.95] shadow-xl">
<Link href="/" className="relative select-none">
<img src="pics/flierland-new-logo-colored.png" alt={translate('slogan')} className="block w-8/12 md:w-8/12 mx-auto mt-6" />
</Link>
<Tabs
activeClass="text-white bg-secondary-light"
tabClass="text-center py-3 md:py-4 px-3 uppercase text-base md:text-lg border-b-[4px] border-secondary-light rounded-t-md select-none"
className="login-tabs block w-full md:mt-12 md:mb-8"
onChange={(x) => setCurrentTab(x)}
>
<Tab index={1} title={translate('sign-in')}><Login /></Tab>
<Tab index={2} title={translate('user-register-text')}>
<SignUp cities={data.Cities} />
</Tab>
</Tabs>
{cities ?
<>
<Tabs
activeClass="text-white bg-secondary-light"
tabClass="text-center py-3 md:py-4 px-3 uppercase text-base md:text-lg border-b-[4px] border-secondary-light rounded-t-md select-none"
className="login-tabs block w-full md:mt-12 md:mb-8"
onChange={(x) => setCurrentTab(x)}
>
<Tab index={1} title={translate('sign-in')}><Login /></Tab>
<Tab index={2} title={translate('user-register-text')}>
<SignUp cities={cities} />
</Tab>
</Tabs>
</>
:
<InnerLoading loadingText={""} width={"200"} height={"100"} />
}
</div>
</div>
</div >
)
}
export default SignIn;
export async function getStaticProps() {
const { data } = await client.query({
query: gql`
query GetGeneralData {
Cities (filter: { status: { _eq: "published" } }, limit: -1) {
id
name
English_name
state {
name
}
}
}
`
});
return {
props: {
data,
},
};
}

View File

@ -43,7 +43,7 @@
"src/common/services/analytics/analytics.ts",
"src/pages/sitemap.js",
".next/types/**/*.ts"
, "src/common/services/general/ghj", "src/common/services/user/AuthContext.js" ],
, "src/common/services/general/ghj", "src/common/services/user/AuthContext.tsx" ],
"exclude": [
"node_modules"
]