Update carousels, card hover, and IranBay branding.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
/** Normalize Iranian/local input to E.164 (e.g. +989121111111). */
|
||||
export function toE164CellNumber(input: string): string {
|
||||
const digits = input.replace(/\D/g, "");
|
||||
|
||||
if (!digits) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (digits.startsWith("98")) {
|
||||
return `+${digits}`;
|
||||
}
|
||||
|
||||
if (digits.startsWith("0")) {
|
||||
return `+98${digits.slice(1)}`;
|
||||
}
|
||||
|
||||
if (digits.length === 10 && digits.startsWith("9")) {
|
||||
return `+98${digits}`;
|
||||
}
|
||||
|
||||
return `+${digits}`;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
export type Area = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.addEventListener("load", () => resolve(image));
|
||||
image.addEventListener("error", (error) => reject(error));
|
||||
image.crossOrigin = "anonymous";
|
||||
image.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Crop that may extend outside the source image.
|
||||
* Outside pixels stay transparent (PNG) or fillColor (JPEG).
|
||||
*/
|
||||
export async function getCroppedImageFile(
|
||||
imageSrc: string,
|
||||
pixelCrop: Area,
|
||||
fileName: string,
|
||||
mimeType = "image/jpeg",
|
||||
quality = 0.92,
|
||||
fillColor = "#ffffff",
|
||||
): Promise<File> {
|
||||
const image = await loadImage(imageSrc);
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) throw new Error("Could not crop image.");
|
||||
|
||||
const outputWidth = Math.max(1, Math.round(pixelCrop.width));
|
||||
const outputHeight = Math.max(1, Math.round(pixelCrop.height));
|
||||
canvas.width = outputWidth;
|
||||
canvas.height = outputHeight;
|
||||
|
||||
if (mimeType !== "image/png") {
|
||||
ctx.fillStyle = fillColor;
|
||||
ctx.fillRect(0, 0, outputWidth, outputHeight);
|
||||
} else {
|
||||
ctx.clearRect(0, 0, outputWidth, outputHeight);
|
||||
}
|
||||
|
||||
const srcX = Math.max(0, pixelCrop.x);
|
||||
const srcY = Math.max(0, pixelCrop.y);
|
||||
const srcRight = Math.min(image.naturalWidth, pixelCrop.x + pixelCrop.width);
|
||||
const srcBottom = Math.min(
|
||||
image.naturalHeight,
|
||||
pixelCrop.y + pixelCrop.height,
|
||||
);
|
||||
const srcWidth = srcRight - srcX;
|
||||
const srcHeight = srcBottom - srcY;
|
||||
|
||||
if (srcWidth > 0 && srcHeight > 0) {
|
||||
const destX = Math.round(srcX - pixelCrop.x);
|
||||
const destY = Math.round(srcY - pixelCrop.y);
|
||||
ctx.drawImage(
|
||||
image,
|
||||
srcX,
|
||||
srcY,
|
||||
srcWidth,
|
||||
srcHeight,
|
||||
destX,
|
||||
destY,
|
||||
srcWidth,
|
||||
srcHeight,
|
||||
);
|
||||
}
|
||||
|
||||
const blob = await new Promise<Blob>((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
(result) => {
|
||||
if (result) resolve(result);
|
||||
else reject(new Error("Could not export cropped image."));
|
||||
},
|
||||
mimeType,
|
||||
quality,
|
||||
);
|
||||
});
|
||||
|
||||
const base = fileName.replace(/\.[^.]+$/, "") || "image";
|
||||
const extension = mimeType === "image/png" ? "png" : "jpg";
|
||||
return new File([blob], `${base}-cropped.${extension}`, { type: mimeType });
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
const ARABIC_SCRIPT = /[\u0600-\u06FF]/;
|
||||
|
||||
/** Wrap Persian/Arabic runs in IRANYekan; leave Latin as-is. */
|
||||
export function withFaSpans(value: string): ReactNode {
|
||||
if (!value || !ARABIC_SCRIPT.test(value)) return value;
|
||||
|
||||
const parts = value.split(/([\u0600-\u06FF][\u0600-\u06FF\s\u200c]*)/g);
|
||||
return parts.map((part, index) =>
|
||||
ARABIC_SCRIPT.test(part) ? (
|
||||
<span key={index} className="fa" lang="fa" dir="rtl">
|
||||
{part}
|
||||
</span>
|
||||
) : (
|
||||
part
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function hasArabicScript(value: string | null | undefined) {
|
||||
return Boolean(value && ARABIC_SCRIPT.test(value));
|
||||
}
|
||||
|
||||
/** Keep phone / E.164 numbers LTR even next to Persian text. */
|
||||
export function PhoneLtr({
|
||||
value,
|
||||
className,
|
||||
}: {
|
||||
value: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<span className={className} dir="ltr" lang="en">
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import type { DashboardType } from "@/types/auth";
|
||||
|
||||
export type HostApp = "admin" | "customer" | "business";
|
||||
|
||||
const BUSINESS_PREFIX = "business.";
|
||||
const CUSTOMER_PREFIX = "customer.";
|
||||
|
||||
/** Host without port, lowercased. */
|
||||
export function normalizeHost(hostHeader: string | null | undefined): string {
|
||||
if (!hostHeader) return "";
|
||||
return hostHeader.split(":")[0]?.toLowerCase() ?? "";
|
||||
}
|
||||
|
||||
export function isBusinessHost(hostHeader: string | null | undefined): boolean {
|
||||
const host = normalizeHost(hostHeader);
|
||||
if (!host) return false;
|
||||
if (host === "business.localhost") return true;
|
||||
return host.startsWith(BUSINESS_PREFIX);
|
||||
}
|
||||
|
||||
export function isCustomerHost(hostHeader: string | null | undefined): boolean {
|
||||
const host = normalizeHost(hostHeader);
|
||||
if (!host) return false;
|
||||
if (host === "customer.localhost") return true;
|
||||
return host.startsWith(CUSTOMER_PREFIX);
|
||||
}
|
||||
|
||||
export function hostAppFor(hostHeader: string | null | undefined): HostApp {
|
||||
if (isBusinessHost(hostHeader)) return "business";
|
||||
if (isCustomerHost(hostHeader)) return "customer";
|
||||
return "admin";
|
||||
}
|
||||
|
||||
/** @deprecated Prefer hostAppFor + access checks. */
|
||||
export function expectedDashboardForHost(
|
||||
hostHeader: string | null | undefined,
|
||||
): DashboardType {
|
||||
const app = hostAppFor(hostHeader);
|
||||
if (app === "business") return "business";
|
||||
if (app === "customer") return "customer";
|
||||
return "super_admin";
|
||||
}
|
||||
|
||||
function originFromEnv(
|
||||
envValue: string | undefined,
|
||||
fallbackLocal: string,
|
||||
fallbackProd: (root: string) => string,
|
||||
): string {
|
||||
const fromEnv = envValue?.replace(/\/$/, "");
|
||||
if (fromEnv) {
|
||||
if (fromEnv.startsWith("http://") || fromEnv.startsWith("https://")) {
|
||||
return fromEnv;
|
||||
}
|
||||
const protocol =
|
||||
fromEnv.includes("localhost") || fromEnv.startsWith("127.")
|
||||
? "http"
|
||||
: "https";
|
||||
return `${protocol}://${fromEnv}`;
|
||||
}
|
||||
|
||||
const root = process.env.NEXT_PUBLIC_ROOT_DOMAIN || "localhost";
|
||||
if (root === "localhost" || root.startsWith("127.")) {
|
||||
return fallbackLocal;
|
||||
}
|
||||
return fallbackProd(root);
|
||||
}
|
||||
|
||||
/** Absolute origin for the business dashboard (no trailing slash). */
|
||||
export function getBusinessDashboardOrigin(): string {
|
||||
return originFromEnv(
|
||||
process.env.NEXT_PUBLIC_BUSINESS_DASHBOARD_HOST,
|
||||
"http://business.localhost:3002",
|
||||
(root) => `https://business.${root}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Absolute origin for the customer dashboard. */
|
||||
export function getCustomerDashboardOrigin(): string {
|
||||
return originFromEnv(
|
||||
process.env.NEXT_PUBLIC_CUSTOMER_DASHBOARD_HOST,
|
||||
"http://customer.localhost:3002",
|
||||
(root) => `https://customer.${root}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Absolute origin for the superadmin dashboard. */
|
||||
export function getAdminDashboardOrigin(): string {
|
||||
return originFromEnv(
|
||||
process.env.NEXT_PUBLIC_ADMIN_DASHBOARD_HOST,
|
||||
"http://localhost:3002",
|
||||
(root) => `https://admin.${root}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Map public business URL path → internal /business/* path. */
|
||||
export function rewriteBusinessPath(pathname: string): string | null {
|
||||
if (pathname === "/") return "/business";
|
||||
if (pathname === "/login") return "/login";
|
||||
if (pathname === "/forgot-password") return "/forgot-password";
|
||||
if (pathname === "/auth/handoff") return "/auth/handoff";
|
||||
|
||||
const known = [
|
||||
"/profile",
|
||||
"/products",
|
||||
"/blog",
|
||||
"/portfolios",
|
||||
"/gallery",
|
||||
] as const;
|
||||
|
||||
for (const path of known) {
|
||||
if (pathname === path || pathname.startsWith(`${path}/`)) {
|
||||
return `/business${pathname}`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Map public customer URL path → internal /customer/* path. */
|
||||
export function rewriteCustomerPath(pathname: string): string | null {
|
||||
if (pathname === "/") return "/customer";
|
||||
if (pathname === "/login") return "/login";
|
||||
if (pathname === "/forgot-password") return "/forgot-password";
|
||||
if (pathname === "/auth/handoff") return "/auth/handoff";
|
||||
|
||||
if (pathname === "/profile" || pathname.startsWith("/profile/")) {
|
||||
return `/customer${pathname}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
export const en = {
|
||||
brandName: "IranBay",
|
||||
brandSubtitle: "Business",
|
||||
customerBrandSubtitle: "Account",
|
||||
workspaceEyebrow: "Business workspace",
|
||||
workspaceHint: "Manage your catalog",
|
||||
customerEyebrow: "Customer workspace",
|
||||
customerHint: "Your account",
|
||||
viewWebsite: "View website",
|
||||
openBusinessDashboard: "Business dashboard",
|
||||
openCustomerDashboard: "Customer dashboard",
|
||||
openAdminDashboard: "Admin dashboard",
|
||||
logOut: "Log out",
|
||||
loading: "Loading",
|
||||
nav: {
|
||||
home: "Home",
|
||||
profile: "Profile",
|
||||
products: "Products",
|
||||
blog: "Blog",
|
||||
portfolios: "Portfolios",
|
||||
gallery: "Gallery",
|
||||
},
|
||||
customerHome: {
|
||||
title: "Welcome",
|
||||
lead: "Manage your account from here.",
|
||||
businessCta: "Open business dashboard",
|
||||
businessLead: "You own a business on IranBay.",
|
||||
adminCta: "Open admin dashboard",
|
||||
adminLead: "Review marketplace listings and approve new shops.",
|
||||
},
|
||||
customerProfile: {
|
||||
title: "Profile",
|
||||
lead: "Your account details.",
|
||||
phone: "Phone",
|
||||
email: "Email",
|
||||
name: "Name",
|
||||
emptyEmail: "Not set",
|
||||
},
|
||||
profile: {
|
||||
title: "Profile",
|
||||
lead: "Update how your business appears on Meshkee.",
|
||||
},
|
||||
products: {
|
||||
title: "Products",
|
||||
lead: "Your product catalog.",
|
||||
empty: "No products yet.",
|
||||
emptyLead: "Add your first product to start building the catalog.",
|
||||
emptyFiltered: "Nothing in this category.",
|
||||
emptyFilteredLead: "Try another category, or add a product here.",
|
||||
add: "Add product",
|
||||
addTitle: "Add product",
|
||||
addLead: "Create a product like in Meshkee CMS.",
|
||||
manageCategories: "Manage categories",
|
||||
categoriesTitle: "Product categories",
|
||||
categoriesLead: "Organize products the same way as Meshkee CMS.",
|
||||
},
|
||||
blog: {
|
||||
title: "Blog",
|
||||
lead: "Articles and updates.",
|
||||
empty: "No blog posts yet.",
|
||||
emptyLead: "Publish your first article, news piece, or blog post.",
|
||||
add: "Add post",
|
||||
addTitle: "Add blog post",
|
||||
addLead: "Create a post like in Meshkee CMS.",
|
||||
manageCategories: "Manage categories",
|
||||
categoriesTitle: "Blog categories",
|
||||
categoriesLead: "Organize posts the same way as Meshkee CMS.",
|
||||
},
|
||||
portfolios: {
|
||||
title: "Portfolios",
|
||||
lead: "Project showcases.",
|
||||
empty: "No portfolios yet.",
|
||||
emptyLead: "Showcase a project with images, story, and links.",
|
||||
add: "Add portfolio",
|
||||
addTitle: "Add portfolio",
|
||||
addLead: "Create a portfolio like in Meshkee CMS.",
|
||||
manageCategories: "Manage categories",
|
||||
categoriesTitle: "Portfolio categories",
|
||||
categoriesLead: "Organize portfolios the same way as Meshkee CMS.",
|
||||
},
|
||||
gallery: {
|
||||
title: "Gallery",
|
||||
lead: "Upload and arrange images for your business.",
|
||||
empty: "No gallery images yet.",
|
||||
emptyLead: "Drop images above, or browse files to fill the gallery.",
|
||||
dropTitle: "Drop images here",
|
||||
dropHint: "or click to browse — JPG, PNG, WebP, GIF",
|
||||
uploading: "Uploading…",
|
||||
add: "Add images",
|
||||
},
|
||||
common: {
|
||||
edit: "Edit",
|
||||
remove: "Remove",
|
||||
all: "All",
|
||||
allInCategory: "All in category",
|
||||
prev: "Prev",
|
||||
next: "Next",
|
||||
comingSoon: "Edit coming soon",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type EnMessages = typeof en;
|
||||
|
||||
export function t(): EnMessages {
|
||||
return en;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
const MANAGED_BUSINESS_KEY = "marketplace_managed_business_id";
|
||||
|
||||
/**
|
||||
* Superadmin-only: which Meshkee business is open on the shared business.* host.
|
||||
* Regular business users ignore this and use `user.businesses` instead.
|
||||
*/
|
||||
export function getManagedBusinessId(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return (
|
||||
sessionStorage.getItem(MANAGED_BUSINESS_KEY) ||
|
||||
localStorage.getItem(MANAGED_BUSINESS_KEY)
|
||||
);
|
||||
}
|
||||
|
||||
export function setManagedBusinessId(businessId: string | null) {
|
||||
if (typeof window === "undefined") return;
|
||||
if (!businessId) {
|
||||
sessionStorage.removeItem(MANAGED_BUSINESS_KEY);
|
||||
localStorage.removeItem(MANAGED_BUSINESS_KEY);
|
||||
return;
|
||||
}
|
||||
const id = String(businessId);
|
||||
sessionStorage.setItem(MANAGED_BUSINESS_KEY, id);
|
||||
localStorage.setItem(MANAGED_BUSINESS_KEY, id);
|
||||
}
|
||||
|
||||
export function clearManagedBusinessId() {
|
||||
setManagedBusinessId(null);
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_MESHKEE_API_URL?.replace(/\/$/, "") ||
|
||||
"http://localhost:3010/api/v1";
|
||||
|
||||
const ACCESS_TOKEN_KEY = "marketplace_access_token";
|
||||
const REFRESH_TOKEN_KEY = "marketplace_refresh_token";
|
||||
const TOKEN_MAX_AGE_SEC = 60 * 60 * 24 * 14; // 14 days
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
|
||||
constructor(message: string, status: number) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
export function isAbortError(err: unknown): boolean {
|
||||
return err instanceof DOMException && err.name === "AbortError";
|
||||
}
|
||||
|
||||
/** Share cookies across admin / customer / business hosts on the same root. */
|
||||
function cookieDomain(): string | undefined {
|
||||
const root = (process.env.NEXT_PUBLIC_ROOT_DOMAIN || "localhost").trim();
|
||||
if (!root || root === "localhost" || root.startsWith("127.")) {
|
||||
return "localhost";
|
||||
}
|
||||
return root.startsWith(".") ? root : `.${root}`;
|
||||
}
|
||||
|
||||
function readCookie(name: string): string | null {
|
||||
if (typeof document === "undefined") return null;
|
||||
const prefix = `${name}=`;
|
||||
for (const part of document.cookie.split(";")) {
|
||||
const trimmed = part.trim();
|
||||
if (trimmed.startsWith(prefix)) {
|
||||
return decodeURIComponent(trimmed.slice(prefix.length));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function writeCookie(name: string, value: string) {
|
||||
if (typeof document === "undefined") return;
|
||||
const domain = cookieDomain();
|
||||
const secure =
|
||||
typeof window !== "undefined" && window.location.protocol === "https:"
|
||||
? "; Secure"
|
||||
: "";
|
||||
document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=${TOKEN_MAX_AGE_SEC}; SameSite=Lax; Domain=${domain}${secure}`;
|
||||
}
|
||||
|
||||
function deleteCookie(name: string) {
|
||||
if (typeof document === "undefined") return;
|
||||
const domain = cookieDomain();
|
||||
document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax; Domain=${domain}`;
|
||||
}
|
||||
|
||||
export function getAccessToken() {
|
||||
if (typeof window === "undefined") return null;
|
||||
return (
|
||||
readCookie(ACCESS_TOKEN_KEY) || localStorage.getItem(ACCESS_TOKEN_KEY)
|
||||
);
|
||||
}
|
||||
|
||||
export function getRefreshToken() {
|
||||
if (typeof window === "undefined") return null;
|
||||
return (
|
||||
readCookie(REFRESH_TOKEN_KEY) || localStorage.getItem(REFRESH_TOKEN_KEY)
|
||||
);
|
||||
}
|
||||
|
||||
export function setTokens(accessToken: string, refreshToken: string) {
|
||||
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
|
||||
writeCookie(ACCESS_TOKEN_KEY, accessToken);
|
||||
writeCookie(REFRESH_TOKEN_KEY, refreshToken);
|
||||
}
|
||||
|
||||
export function clearTokens() {
|
||||
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
deleteCookie(ACCESS_TOKEN_KEY);
|
||||
deleteCookie(REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
/** Promote existing localStorage sessions into shared cookies (once per load). */
|
||||
export function migrateTokensToSharedCookies() {
|
||||
if (typeof window === "undefined") return;
|
||||
const access = localStorage.getItem(ACCESS_TOKEN_KEY);
|
||||
const refresh = localStorage.getItem(REFRESH_TOKEN_KEY);
|
||||
if (access && refresh && !readCookie(ACCESS_TOKEN_KEY)) {
|
||||
writeCookie(ACCESS_TOKEN_KEY, access);
|
||||
writeCookie(REFRESH_TOKEN_KEY, refresh);
|
||||
}
|
||||
}
|
||||
|
||||
function parseErrorMessage(payload: unknown, fallback: string) {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const message = (payload as { message?: string | string[] }).message;
|
||||
|
||||
if (Array.isArray(message)) {
|
||||
return message.join(", ");
|
||||
}
|
||||
|
||||
if (typeof message === "string") {
|
||||
return message;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
async function refreshAccessToken() {
|
||||
const refreshToken = getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/auth/refresh`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
clearTokens();
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setTokens(data.accessToken, data.refreshToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
interface RequestOptions extends Omit<RequestInit, "body"> {
|
||||
body?: unknown;
|
||||
auth?: boolean;
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(
|
||||
path: string,
|
||||
options: RequestOptions = {},
|
||||
): Promise<T> {
|
||||
const { body, auth = false, headers, ...rest } = options;
|
||||
const requestHeaders = new Headers(headers);
|
||||
|
||||
if (body !== undefined) {
|
||||
requestHeaders.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
if (auth) {
|
||||
const accessToken = getAccessToken();
|
||||
if (accessToken) {
|
||||
requestHeaders.set("Authorization", `Bearer ${accessToken}`);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
...rest,
|
||||
headers: requestHeaders,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (response.status === 401 && auth) {
|
||||
const refreshed = await refreshAccessToken();
|
||||
if (refreshed) {
|
||||
return apiRequest<T>(path, options);
|
||||
}
|
||||
}
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(
|
||||
parseErrorMessage(payload, `Request failed with status ${response.status}`),
|
||||
response.status,
|
||||
);
|
||||
}
|
||||
|
||||
return payload as T;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
getAccessToken,
|
||||
getRefreshToken,
|
||||
setTokens,
|
||||
} from "@/lib/meshkee-api";
|
||||
import {
|
||||
getBusinessDashboardOrigin,
|
||||
getCustomerDashboardOrigin,
|
||||
} from "@/lib/host";
|
||||
import {
|
||||
clearManagedBusinessId,
|
||||
setManagedBusinessId,
|
||||
} from "@/lib/managed-business";
|
||||
|
||||
/**
|
||||
* Superadmin “Manage business” — same origin as admin (no subdomain bounce).
|
||||
* Example: /manage/{meshkeeBusinessId}/profile
|
||||
*/
|
||||
export function openBusinessDashboardAsAdmin(
|
||||
meshkeeBusinessId: string,
|
||||
nextPath = "/profile",
|
||||
) {
|
||||
const id = meshkeeBusinessId.trim();
|
||||
if (!id) return;
|
||||
|
||||
setManagedBusinessId(id);
|
||||
|
||||
const path = nextPath.startsWith("/") ? nextPath : `/${nextPath}`;
|
||||
const suffix = path === "/" ? "/profile" : path;
|
||||
window.location.assign(`/manage/${encodeURIComponent(id)}${suffix}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Business owner / staff → business.* host from their account.
|
||||
*/
|
||||
export function openBusinessDashboardAsUser(nextPath = "/profile") {
|
||||
const access = getAccessToken();
|
||||
const refresh = getRefreshToken();
|
||||
if (!access || !refresh) {
|
||||
window.location.assign(`${getCustomerDashboardOrigin()}/login`);
|
||||
return;
|
||||
}
|
||||
|
||||
setTokens(access, refresh);
|
||||
clearManagedBusinessId();
|
||||
|
||||
const next = nextPath.startsWith("/") ? nextPath : `/${nextPath}`;
|
||||
const hash = new URLSearchParams({
|
||||
access,
|
||||
refresh,
|
||||
next,
|
||||
clearManaged: "1",
|
||||
}).toString();
|
||||
|
||||
window.location.assign(
|
||||
`${getBusinessDashboardOrigin()}/auth/handoff#${hash}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function businessDashboardHrefForUser(nextPath = "/profile"): string {
|
||||
const next = nextPath.startsWith("/") ? nextPath : `/${nextPath}`;
|
||||
const access = getAccessToken();
|
||||
const refresh = getRefreshToken();
|
||||
if (!access || !refresh) {
|
||||
return `${getCustomerDashboardOrigin()}/login`;
|
||||
}
|
||||
const hash = new URLSearchParams({
|
||||
access,
|
||||
refresh,
|
||||
next,
|
||||
clearManaged: "1",
|
||||
}).toString();
|
||||
return `${getBusinessDashboardOrigin()}/auth/handoff#${hash}`;
|
||||
}
|
||||
|
||||
export function businessDashboardHrefForAdmin(
|
||||
meshkeeBusinessId: string,
|
||||
nextPath = "/profile",
|
||||
): string {
|
||||
const id = meshkeeBusinessId.trim();
|
||||
if (!id) return "/businesses";
|
||||
const path = nextPath.startsWith("/") ? nextPath : `/${nextPath}`;
|
||||
const suffix = path === "/" ? "/profile" : path;
|
||||
return `/manage/${encodeURIComponent(id)}${suffix}`;
|
||||
}
|
||||
|
||||
export function customerDashboardHref(nextPath = "/"): string {
|
||||
const next = nextPath.startsWith("/") ? nextPath : `/${nextPath}`;
|
||||
const access = getAccessToken();
|
||||
const refresh = getRefreshToken();
|
||||
if (!access || !refresh) {
|
||||
return `${getCustomerDashboardOrigin()}${next}`;
|
||||
}
|
||||
const hash = new URLSearchParams({
|
||||
access,
|
||||
refresh,
|
||||
next,
|
||||
}).toString();
|
||||
return `${getCustomerDashboardOrigin()}/auth/handoff#${hash}`;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { AuthUser } from "@/types/auth";
|
||||
|
||||
/** Prefer Meshkee English name fields; fall back to cell. */
|
||||
export function getDisplayName(user: AuthUser | null | undefined): string {
|
||||
if (!user) return "Admin";
|
||||
|
||||
const english = [user.firstNameEn, user.lastNameEn]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.trim();
|
||||
if (english) return english;
|
||||
|
||||
const local = [user.firstName, user.lastName].filter(Boolean).join(" ").trim();
|
||||
if (local && !/[\u0600-\u06FF]/.test(local)) return local;
|
||||
|
||||
return user.cellNumber || "User";
|
||||
}
|
||||
|
||||
export function getFirstName(user: AuthUser | null | undefined): string {
|
||||
if (!user) return "there";
|
||||
const raw =
|
||||
user.firstNameEn?.trim() ||
|
||||
(user.firstName?.trim() && !/[\u0600-\u06FF]/.test(user.firstName)
|
||||
? user.firstName.trim()
|
||||
: "");
|
||||
if (!raw) return "there";
|
||||
return raw.split(/\s+/)[0] || raw;
|
||||
}
|
||||
Reference in New Issue
Block a user