bug fixes
This commit is contained in:
parent
d339276bd2
commit
dc9b5d4f32
@ -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';
|
||||
|
||||
@ -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 ?
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -99,6 +99,7 @@ export const Cities = gql`
|
||||
}
|
||||
country {
|
||||
id
|
||||
Initials
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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,
|
||||
|
||||
@ -365,6 +365,9 @@ export const billboardFieldsQuery = `
|
||||
Initials
|
||||
name
|
||||
English_name
|
||||
country {
|
||||
Initials
|
||||
}
|
||||
}
|
||||
Cities (filter: { status: { _eq: "published" } }, limit: -1) {
|
||||
id
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>([]);
|
||||
|
||||
@ -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: [
|
||||
{
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
@ -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="" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -344,4 +344,6 @@ export type ApiResponse<T = any> = {
|
||||
data: T | undefined;
|
||||
error: any;
|
||||
status?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type CountryCode = 'IR' | 'CA' | 'US';
|
||||
@ -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(),
|
||||
}));
|
||||
|
||||
|
||||
@ -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
31
src/lib/general/cors.ts
Normal 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;
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
12
src/lib/general/general.ts
Normal file
12
src/lib/general/general.ts
Normal 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';
|
||||
};
|
||||
20
src/lib/general/get-country-static-paths.ts
Normal file
20
src/lib/general/get-country-static-paths.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
@ -29,7 +29,9 @@ interface WalletTopUpData {
|
||||
|
||||
}
|
||||
interface PurchaseOrderData {
|
||||
|
||||
billboard_orders: {
|
||||
billboard_orders_id: string;
|
||||
}[];
|
||||
}
|
||||
interface ServiceOrderData {
|
||||
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -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' });
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
|
||||
108
src/pages/api/general/orders/auto-confirm-orders.ts
Normal file
108
src/pages/api/general/orders/auto-confirm-orders.ts
Normal 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" });
|
||||
}
|
||||
}
|
||||
125
src/pages/api/general/orders/cancel-pending-order.ts
Normal file
125
src/pages/api/general/orders/cancel-pending-order.ts
Normal 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 24–48 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." });
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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[]) => `
|
||||
|
||||
@ -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"} />
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -147,6 +147,9 @@ const CreateProduct: React.FunctionComponent<CreateProduct> = ({ }) => {
|
||||
name
|
||||
English_name
|
||||
Initials
|
||||
country {
|
||||
Initials
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -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}`,
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -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"
|
||||
]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user