Update carousels, card hover, and IranBay branding.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* Install session from the URL hash BEFORE React AuthProvider runs.
|
||||
* Cross-subdomain localStorage is empty until this runs; without it,
|
||||
* ProtectedRoute saw ?businessId= and bounced admins back to /businesses.
|
||||
*/
|
||||
const HANDOFF_BOOT = `
|
||||
(function () {
|
||||
try {
|
||||
var raw = location.hash ? location.hash.replace(/^#/, "") : "";
|
||||
if (!raw) return;
|
||||
var p = new URLSearchParams(raw);
|
||||
var access = p.get("access");
|
||||
var refresh = p.get("refresh");
|
||||
var businessId = p.get("businessId");
|
||||
var clearManaged = p.get("clearManaged") === "1";
|
||||
var next = p.get("next") || "/profile";
|
||||
if (!access || !refresh) return;
|
||||
|
||||
localStorage.setItem("marketplace_access_token", access);
|
||||
localStorage.setItem("marketplace_refresh_token", refresh);
|
||||
|
||||
var root = "localhost";
|
||||
try {
|
||||
// Best-effort shared cookie for *.localhost / prod root
|
||||
document.cookie =
|
||||
"marketplace_access_token=" +
|
||||
encodeURIComponent(access) +
|
||||
"; Path=/; Max-Age=1209600; SameSite=Lax; Domain=" +
|
||||
root;
|
||||
document.cookie =
|
||||
"marketplace_refresh_token=" +
|
||||
encodeURIComponent(refresh) +
|
||||
"; Path=/; Max-Age=1209600; SameSite=Lax; Domain=" +
|
||||
root;
|
||||
} catch (e) {}
|
||||
|
||||
if (businessId) {
|
||||
localStorage.setItem("marketplace_managed_business_id", businessId);
|
||||
sessionStorage.setItem("marketplace_managed_business_id", businessId);
|
||||
} else if (clearManaged) {
|
||||
localStorage.removeItem("marketplace_managed_business_id");
|
||||
sessionStorage.removeItem("marketplace_managed_business_id");
|
||||
}
|
||||
|
||||
var target = next.charAt(0) === "/" ? next : "/" + next;
|
||||
location.replace(target);
|
||||
} catch (e) {}
|
||||
})();
|
||||
`;
|
||||
|
||||
export default function AuthHandoffLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<script dangerouslySetInnerHTML={{ __html: HANDOFF_BOOT }} />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Fallback UI if the inline boot script did not redirect
|
||||
* (e.g. missing hash). Prefer the layout script for session install.
|
||||
*/
|
||||
export default function AuthHandoffPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: "100vh",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
background: "#f6f6f6",
|
||||
fontSize: 12,
|
||||
letterSpacing: "0.2em",
|
||||
textTransform: "uppercase",
|
||||
color: "#8e8e8e",
|
||||
padding: 24,
|
||||
textAlign: "center",
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
Opening business dashboard…
|
||||
<br />
|
||||
<span style={{ letterSpacing: "0.06em", textTransform: "none", marginTop: 12 }}>
|
||||
If this page stays here, go back and click Manage business again.
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { BusinessMeshkeeGate } from "@/components/business/BusinessPageChrome";
|
||||
import AddBlogForm from "@/components/catalog/AddBlogForm";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
|
||||
export default function BusinessAddBlogPage() {
|
||||
return (
|
||||
<BusinessMeshkeeGate>
|
||||
{(meshkeeId) => (
|
||||
<AddBlogForm
|
||||
meshkeeId={meshkeeId}
|
||||
listHref="/blog"
|
||||
title={en.blog.addTitle}
|
||||
lead={en.blog.addLead}
|
||||
/>
|
||||
)}
|
||||
</BusinessMeshkeeGate>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import {
|
||||
BusinessListHeader,
|
||||
BusinessMeshkeeGate,
|
||||
} from "@/components/business/BusinessPageChrome";
|
||||
import BlogPanel from "@/components/catalog/BlogPanel";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
import styles from "@/components/BusinessDetailsPage.module.css";
|
||||
|
||||
export default function BusinessBlogPage() {
|
||||
return (
|
||||
<BusinessMeshkeeGate>
|
||||
{(meshkeeId) => (
|
||||
<>
|
||||
<BusinessListHeader
|
||||
title={en.blog.title}
|
||||
lead={en.blog.lead}
|
||||
action={
|
||||
<Link href="/blog/new" className={`outline-btn ${styles.actionBtn}`}>
|
||||
{en.blog.add}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<BlogPanel
|
||||
meshkeeId={meshkeeId}
|
||||
emptyLabel={en.blog.empty}
|
||||
addHref="/blog/new"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</BusinessMeshkeeGate>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BusinessListHeader,
|
||||
BusinessMeshkeeGate,
|
||||
} from "@/components/business/BusinessPageChrome";
|
||||
import GalleryPanel from "@/components/catalog/GalleryPanel";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
|
||||
export default function BusinessGalleryPage() {
|
||||
return (
|
||||
<BusinessMeshkeeGate>
|
||||
{(meshkeeId) => (
|
||||
<>
|
||||
<BusinessListHeader
|
||||
title={en.gallery.title}
|
||||
lead={en.gallery.lead}
|
||||
/>
|
||||
<GalleryPanel
|
||||
meshkeeId={meshkeeId}
|
||||
emptyLabel={en.gallery.empty}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</BusinessMeshkeeGate>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ProtectedRoute } from "@/components/AuthGuards";
|
||||
import BusinessShell from "@/components/BusinessShell";
|
||||
|
||||
export default function BusinessLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<ProtectedRoute requiredApp="business">
|
||||
<BusinessShell>{children}</BusinessShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function BusinessIndexPage() {
|
||||
redirect("/profile");
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { BusinessMeshkeeGate } from "@/components/business/BusinessPageChrome";
|
||||
import AddPortfolioForm from "@/components/catalog/AddPortfolioForm";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
|
||||
export default function BusinessAddPortfolioPage() {
|
||||
return (
|
||||
<BusinessMeshkeeGate>
|
||||
{(meshkeeId) => (
|
||||
<AddPortfolioForm
|
||||
meshkeeId={meshkeeId}
|
||||
listHref="/portfolios"
|
||||
title={en.portfolios.addTitle}
|
||||
lead={en.portfolios.addLead}
|
||||
/>
|
||||
)}
|
||||
</BusinessMeshkeeGate>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import {
|
||||
BusinessListHeader,
|
||||
BusinessMeshkeeGate,
|
||||
} from "@/components/business/BusinessPageChrome";
|
||||
import PortfoliosPanel from "@/components/catalog/PortfoliosPanel";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
import styles from "@/components/BusinessDetailsPage.module.css";
|
||||
|
||||
export default function BusinessPortfoliosPage() {
|
||||
return (
|
||||
<BusinessMeshkeeGate>
|
||||
{(meshkeeId) => (
|
||||
<>
|
||||
<BusinessListHeader
|
||||
title={en.portfolios.title}
|
||||
lead={en.portfolios.lead}
|
||||
action={
|
||||
<Link
|
||||
href="/portfolios/new"
|
||||
className={`outline-btn ${styles.actionBtn}`}
|
||||
>
|
||||
{en.portfolios.add}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<PortfoliosPanel
|
||||
meshkeeId={meshkeeId}
|
||||
emptyLabel={en.portfolios.empty}
|
||||
addHref="/portfolios/new"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</BusinessMeshkeeGate>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BusinessMeshkeeGate,
|
||||
} from "@/components/business/BusinessPageChrome";
|
||||
import ManageCategoriesPage from "@/components/catalog/ManageCategoriesPage";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
|
||||
export default function BusinessProductCategoriesPage() {
|
||||
return (
|
||||
<BusinessMeshkeeGate>
|
||||
{(meshkeeId) => (
|
||||
<ManageCategoriesPage
|
||||
meshkeeId={meshkeeId}
|
||||
entityType="product"
|
||||
title={en.products.categoriesTitle}
|
||||
lead={en.products.categoriesLead}
|
||||
listHref="/products"
|
||||
listLabel={en.products.title}
|
||||
/>
|
||||
)}
|
||||
</BusinessMeshkeeGate>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BusinessMeshkeeGate,
|
||||
} from "@/components/business/BusinessPageChrome";
|
||||
import AddProductForm from "@/components/catalog/AddProductForm";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
|
||||
export default function BusinessAddProductPage() {
|
||||
return (
|
||||
<BusinessMeshkeeGate>
|
||||
{(meshkeeId) => (
|
||||
<AddProductForm
|
||||
meshkeeId={meshkeeId}
|
||||
listHref="/products"
|
||||
title={en.products.addTitle}
|
||||
lead={en.products.addLead}
|
||||
/>
|
||||
)}
|
||||
</BusinessMeshkeeGate>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import {
|
||||
BusinessListHeader,
|
||||
BusinessMeshkeeGate,
|
||||
} from "@/components/business/BusinessPageChrome";
|
||||
import ProductsPanel from "@/components/catalog/ProductsPanel";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
import styles from "@/components/BusinessDetailsPage.module.css";
|
||||
|
||||
export default function BusinessProductsPage() {
|
||||
return (
|
||||
<BusinessMeshkeeGate>
|
||||
{(meshkeeId) => (
|
||||
<>
|
||||
<BusinessListHeader
|
||||
title={en.products.title}
|
||||
lead={en.products.lead}
|
||||
action={
|
||||
<>
|
||||
<Link
|
||||
href="/products/categories"
|
||||
className={`outline-btn ${styles.actionBtn}`}
|
||||
>
|
||||
{en.products.manageCategories}
|
||||
</Link>
|
||||
<Link
|
||||
href="/products/new"
|
||||
className={`outline-btn ${styles.actionBtn}`}
|
||||
>
|
||||
{en.products.add}
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ProductsPanel
|
||||
meshkeeId={meshkeeId}
|
||||
emptyLabel={en.products.empty}
|
||||
addHref="/products/new"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</BusinessMeshkeeGate>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { BusinessMeshkeeGate } from "@/components/business/BusinessPageChrome";
|
||||
import EditBusinessPage from "@/components/EditBusinessPage";
|
||||
|
||||
export default function BusinessProfilePage() {
|
||||
return (
|
||||
<BusinessMeshkeeGate>
|
||||
{(meshkeeId) => (
|
||||
<EditBusinessPage meshkeeBusinessId={meshkeeId} variant="business" />
|
||||
)}
|
||||
</BusinessMeshkeeGate>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ProtectedRoute } from "@/components/AuthGuards";
|
||||
import DashboardShell from "@/components/DashboardShell";
|
||||
import EditBusinessPage from "@/components/EditBusinessPage";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Edit business | IranBay Dashboard",
|
||||
};
|
||||
|
||||
export default async function EditBusinessRoute({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
|
||||
return (
|
||||
<ProtectedRoute requiredApp="admin">
|
||||
<DashboardShell>
|
||||
<EditBusinessPage listingId={id} />
|
||||
</DashboardShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ProtectedRoute } from "@/components/AuthGuards";
|
||||
import BusinessDetailsPage from "@/components/BusinessDetailsPage";
|
||||
import DashboardShell from "@/components/DashboardShell";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Business details | IranBay Dashboard",
|
||||
};
|
||||
|
||||
export default async function BusinessDetailsRoute({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
|
||||
return (
|
||||
<ProtectedRoute requiredApp="admin">
|
||||
<DashboardShell>
|
||||
<BusinessDetailsPage listingId={id} />
|
||||
</DashboardShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ProtectedRoute } from "@/components/AuthGuards";
|
||||
import BusinessCategoriesPage from "@/components/BusinessCategoriesPage";
|
||||
import DashboardShell from "@/components/DashboardShell";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Business categories | IranBay Dashboard",
|
||||
};
|
||||
|
||||
export default function BusinessCategoriesRoute() {
|
||||
return (
|
||||
<ProtectedRoute requiredApp="admin">
|
||||
<DashboardShell>
|
||||
<BusinessCategoriesPage />
|
||||
</DashboardShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Suspense } from "react";
|
||||
import { ProtectedRoute } from "@/components/AuthGuards";
|
||||
import BusinessesPage from "@/components/BusinessesPage";
|
||||
import DashboardShell from "@/components/DashboardShell";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Businesses | IranBay Dashboard",
|
||||
};
|
||||
|
||||
export default function BusinessesRoute() {
|
||||
return (
|
||||
<ProtectedRoute requiredApp="admin">
|
||||
<DashboardShell>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div
|
||||
style={{
|
||||
minHeight: 220,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
fontSize: 12,
|
||||
letterSpacing: "0.2em",
|
||||
textTransform: "uppercase",
|
||||
color: "#8e8e8e",
|
||||
}}
|
||||
>
|
||||
Loading businesses…
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<BusinessesPage />
|
||||
</Suspense>
|
||||
</DashboardShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ProtectedRoute } from "@/components/AuthGuards";
|
||||
import CustomerShell from "@/components/CustomerShell";
|
||||
|
||||
export default function CustomerLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<ProtectedRoute requiredApp="customer">
|
||||
<CustomerShell>{children}</CustomerShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
import CustomerHome from "@/components/CustomerHome";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Home | IranBay Account",
|
||||
};
|
||||
|
||||
export default function CustomerHomePage() {
|
||||
return <CustomerHome />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
import CustomerProfile from "@/components/CustomerProfile";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Profile | IranBay Account",
|
||||
};
|
||||
|
||||
export default function CustomerProfilePage() {
|
||||
return <CustomerProfile />;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/* Self-hosted Iran Yekan — public/fonts/iranyekan/ */
|
||||
@font-face {
|
||||
font-family: "IRANYekan";
|
||||
src: url("/fonts/iranyekan/IRANYekanWebLight.ttf") format("truetype");
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekan";
|
||||
src: url("/fonts/iranyekan/IRANYekanWebRegular.ttf") format("truetype");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
import ForgotPasswordForm from "@/components/ForgotPasswordForm";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Forgot Password | IranBay",
|
||||
};
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
return <ForgotPasswordForm />;
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
:root {
|
||||
--font-montserrat: "Montserrat", sans-serif;
|
||||
--font-farsi: "IRANYekan", Tahoma, "Segoe UI", sans-serif;
|
||||
--font-ui: var(--font-montserrat), var(--font-farsi);
|
||||
--color-black: #000;
|
||||
--color-white: #fff;
|
||||
--color-bg: #ffffff;
|
||||
--color-card: #eeeeee;
|
||||
--color-muted: #4a4a4a;
|
||||
--color-soft: #8e8e8e;
|
||||
--color-body: #aaaaaa;
|
||||
--color-dark: #1e1e1e;
|
||||
--color-explore: #f9f9f9;
|
||||
--color-footer: #070707;
|
||||
--color-footer-text: #bababa;
|
||||
--color-footer-line: #232323;
|
||||
--page-pad: 40px;
|
||||
--max-width: 1920px;
|
||||
--container-width: 1080px;
|
||||
--sidebar-width: 248px;
|
||||
--page-title-pattern: radial-gradient(
|
||||
rgba(0, 0, 0, 0.12) 0.75px,
|
||||
transparent 0.85px
|
||||
);
|
||||
--page-title-pattern-size: 14px 14px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
color: var(--color-black);
|
||||
background: var(--color-bg);
|
||||
font-family: var(--font-ui);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
button,
|
||||
a {
|
||||
font-family: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.outline-btn {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0.5px solid var(--color-black);
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
color: var(--color-black);
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2em;
|
||||
white-space: nowrap;
|
||||
transition: color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.outline-btn:hover,
|
||||
.outline-btn:focus-visible {
|
||||
color: var(--color-white);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.outline-btn:hover :is(span, strong, em, p, a, label, button),
|
||||
.outline-btn:focus-visible :is(span, strong, em, p, a, label, button) {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.tile-hover,
|
||||
.outline-btn {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.tile-hover::before,
|
||||
.outline-btn::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--color-black);
|
||||
clip-path: inset(100% 0 0 0);
|
||||
transition: clip-path 0.45s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.tile-hover::before {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.outline-btn::before {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.tile-hover:hover::before,
|
||||
.tile-hover:focus-within::before,
|
||||
.outline-btn:hover::before,
|
||||
.outline-btn:focus-visible::before {
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
|
||||
.tile-hover > *:not(img),
|
||||
.outline-btn > * {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.tile-hover:hover,
|
||||
.tile-hover:focus-within,
|
||||
.tile-hover:hover :is(h1, h2, h3, p, span, strong, a),
|
||||
.tile-hover:focus-within :is(h1, h2, h3, p, span, strong, a) {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.tile-hover :is(h1, h2, h3, p, span, strong, a) {
|
||||
transition: color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.outline-btn:hover img,
|
||||
.outline-btn:focus-visible img {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.tile-hover::before,
|
||||
.outline-btn::before {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.fa,
|
||||
[lang="fa"],
|
||||
:lang(fa) {
|
||||
font-family: var(--font-farsi);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
[dir="ltr"] {
|
||||
unicode-bidi: isolate;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
:root {
|
||||
--page-pad: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
:root {
|
||||
--page-pad: 20px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Montserrat } from "next/font/google";
|
||||
import { AuthProvider } from "@/components/AuthProvider";
|
||||
import "./fonts/iranyekan.css";
|
||||
import "./globals.css";
|
||||
|
||||
const montserrat = Montserrat({
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600"],
|
||||
variable: "--font-montserrat",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "IranBay Dashboard",
|
||||
description: "Admin dashboard for IranBay marketplace gate.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" dir="ltr" className={montserrat.variable}>
|
||||
<body>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Suspense } from "react";
|
||||
import { GuestRoute } from "@/components/AuthGuards";
|
||||
import LoginForm from "@/components/LoginForm";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Login | IranBay",
|
||||
};
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<GuestRoute>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div
|
||||
style={{
|
||||
minHeight: "100vh",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
background: "#f6f6f6",
|
||||
fontSize: 12,
|
||||
letterSpacing: "0.2em",
|
||||
textTransform: "uppercase",
|
||||
color: "#8e8e8e",
|
||||
}}
|
||||
>
|
||||
Loading
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LoginForm />
|
||||
</Suspense>
|
||||
</GuestRoute>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import AddBlogForm from "@/components/catalog/AddBlogForm";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
|
||||
export default function ManageAddBlogPage() {
|
||||
const params = useParams<{ meshkeeId: string }>();
|
||||
const meshkeeId = String(params.meshkeeId ?? "");
|
||||
const listHref = `/manage/${encodeURIComponent(meshkeeId)}/blog`;
|
||||
|
||||
return (
|
||||
<AddBlogForm
|
||||
meshkeeId={meshkeeId}
|
||||
listHref={listHref}
|
||||
title={en.blog.addTitle}
|
||||
lead={en.blog.addLead}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { BusinessListHeader } from "@/components/business/BusinessPageChrome";
|
||||
import BlogPanel from "@/components/catalog/BlogPanel";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
import styles from "@/components/BusinessDetailsPage.module.css";
|
||||
|
||||
export default function ManageBusinessBlogPage() {
|
||||
const params = useParams<{ meshkeeId: string }>();
|
||||
const meshkeeId = String(params.meshkeeId ?? "");
|
||||
const addHref = `/manage/${encodeURIComponent(meshkeeId)}/blog/new`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<BusinessListHeader
|
||||
title={en.blog.title}
|
||||
lead={en.blog.lead}
|
||||
action={
|
||||
<Link href={addHref} className={`outline-btn ${styles.actionBtn}`}>
|
||||
{en.blog.add}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<BlogPanel
|
||||
meshkeeId={meshkeeId}
|
||||
emptyLabel={en.blog.empty}
|
||||
addHref={addHref}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import { BusinessListHeader } from "@/components/business/BusinessPageChrome";
|
||||
import GalleryPanel from "@/components/catalog/GalleryPanel";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
|
||||
export default function ManageBusinessGalleryPage() {
|
||||
const params = useParams<{ meshkeeId: string }>();
|
||||
const meshkeeId = String(params.meshkeeId ?? "");
|
||||
|
||||
return (
|
||||
<>
|
||||
<BusinessListHeader title={en.gallery.title} lead={en.gallery.lead} />
|
||||
<GalleryPanel meshkeeId={meshkeeId} emptyLabel={en.gallery.empty} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { ProtectedRoute } from "@/components/AuthGuards";
|
||||
import BusinessShell from "@/components/BusinessShell";
|
||||
import { setManagedBusinessId } from "@/lib/managed-business";
|
||||
import { useAuth } from "@/components/AuthProvider";
|
||||
|
||||
function ManageBusinessBootstrap({ children }: { children: React.ReactNode }) {
|
||||
const params = useParams<{ meshkeeId: string }>();
|
||||
const meshkeeId = String(params.meshkeeId ?? "");
|
||||
const { isLoading } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (meshkeeId) setManagedBusinessId(meshkeeId);
|
||||
}, [meshkeeId]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: "100vh",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
background: "#f6f6f6",
|
||||
fontSize: 12,
|
||||
letterSpacing: "0.2em",
|
||||
textTransform: "uppercase",
|
||||
color: "#8e8e8e",
|
||||
}}
|
||||
>
|
||||
Loading
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BusinessShell manageMeshkeeId={meshkeeId}>{children}</BusinessShell>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ManageBusinessLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<ProtectedRoute requiredApp="admin">
|
||||
<ManageBusinessBootstrap>{children}</ManageBusinessBootstrap>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function ManageBusinessIndex({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ meshkeeId: string }>;
|
||||
}) {
|
||||
const { meshkeeId } = await params;
|
||||
redirect(`/manage/${meshkeeId}/profile`);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import AddPortfolioForm from "@/components/catalog/AddPortfolioForm";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
|
||||
export default function ManageAddPortfolioPage() {
|
||||
const params = useParams<{ meshkeeId: string }>();
|
||||
const meshkeeId = String(params.meshkeeId ?? "");
|
||||
const listHref = `/manage/${encodeURIComponent(meshkeeId)}/portfolios`;
|
||||
|
||||
return (
|
||||
<AddPortfolioForm
|
||||
meshkeeId={meshkeeId}
|
||||
listHref={listHref}
|
||||
title={en.portfolios.addTitle}
|
||||
lead={en.portfolios.addLead}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { BusinessListHeader } from "@/components/business/BusinessPageChrome";
|
||||
import PortfoliosPanel from "@/components/catalog/PortfoliosPanel";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
import styles from "@/components/BusinessDetailsPage.module.css";
|
||||
|
||||
export default function ManageBusinessPortfoliosPage() {
|
||||
const params = useParams<{ meshkeeId: string }>();
|
||||
const meshkeeId = String(params.meshkeeId ?? "");
|
||||
const addHref = `/manage/${encodeURIComponent(meshkeeId)}/portfolios/new`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<BusinessListHeader
|
||||
title={en.portfolios.title}
|
||||
lead={en.portfolios.lead}
|
||||
action={
|
||||
<Link href={addHref} className={`outline-btn ${styles.actionBtn}`}>
|
||||
{en.portfolios.add}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<PortfoliosPanel
|
||||
meshkeeId={meshkeeId}
|
||||
emptyLabel={en.portfolios.empty}
|
||||
addHref={addHref}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import ManageCategoriesPage from "@/components/catalog/ManageCategoriesPage";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
|
||||
export default function ManageProductCategoriesPage() {
|
||||
const params = useParams<{ meshkeeId: string }>();
|
||||
const meshkeeId = String(params.meshkeeId ?? "");
|
||||
const listHref = `/manage/${encodeURIComponent(meshkeeId)}/products`;
|
||||
|
||||
return (
|
||||
<ManageCategoriesPage
|
||||
meshkeeId={meshkeeId}
|
||||
entityType="product"
|
||||
title={en.products.categoriesTitle}
|
||||
lead={en.products.categoriesLead}
|
||||
homeHref={`/manage/${encodeURIComponent(meshkeeId)}`}
|
||||
listHref={listHref}
|
||||
listLabel={en.products.title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import AddProductForm from "@/components/catalog/AddProductForm";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
|
||||
export default function ManageAddProductPage() {
|
||||
const params = useParams<{ meshkeeId: string }>();
|
||||
const meshkeeId = String(params.meshkeeId ?? "");
|
||||
const listHref = `/manage/${encodeURIComponent(meshkeeId)}/products`;
|
||||
|
||||
return (
|
||||
<AddProductForm
|
||||
meshkeeId={meshkeeId}
|
||||
listHref={listHref}
|
||||
title={en.products.addTitle}
|
||||
lead={en.products.addLead}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { BusinessListHeader } from "@/components/business/BusinessPageChrome";
|
||||
import ProductsPanel from "@/components/catalog/ProductsPanel";
|
||||
import { en } from "@/lib/i18n/en";
|
||||
import styles from "@/components/BusinessDetailsPage.module.css";
|
||||
|
||||
export default function ManageBusinessProductsPage() {
|
||||
const params = useParams<{ meshkeeId: string }>();
|
||||
const meshkeeId = String(params.meshkeeId ?? "");
|
||||
const base = `/manage/${encodeURIComponent(meshkeeId)}/products`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<BusinessListHeader
|
||||
title={en.products.title}
|
||||
lead={en.products.lead}
|
||||
action={
|
||||
<>
|
||||
<Link
|
||||
href={`${base}/categories`}
|
||||
className={`outline-btn ${styles.actionBtn}`}
|
||||
>
|
||||
{en.products.manageCategories}
|
||||
</Link>
|
||||
<Link href={`${base}/new`} className={`outline-btn ${styles.actionBtn}`}>
|
||||
{en.products.add}
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ProductsPanel
|
||||
meshkeeId={meshkeeId}
|
||||
emptyLabel={en.products.empty}
|
||||
addHref={`${base}/new`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import EditBusinessPage from "@/components/EditBusinessPage";
|
||||
|
||||
export default function ManageBusinessProfilePage() {
|
||||
const params = useParams<{ meshkeeId: string }>();
|
||||
const meshkeeId = String(params.meshkeeId ?? "");
|
||||
|
||||
return (
|
||||
<EditBusinessPage meshkeeBusinessId={meshkeeId} variant="business" />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ProtectedRoute } from "@/components/AuthGuards";
|
||||
import DashboardShell from "@/components/DashboardShell";
|
||||
import HomeDashboard from "@/components/HomeDashboard";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Overview | IranBay Dashboard",
|
||||
};
|
||||
|
||||
export default function DashboardHomePage() {
|
||||
return (
|
||||
<ProtectedRoute requiredApp="admin">
|
||||
<DashboardShell>
|
||||
<HomeDashboard />
|
||||
</DashboardShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function WebsiteImagesRedirect() {
|
||||
redirect("/website/images");
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ProtectedRoute } from "@/components/AuthGuards";
|
||||
import BlogCategoriesPage from "@/components/BlogCategoriesPage";
|
||||
import DashboardShell from "@/components/DashboardShell";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Blog categories | IranBay Dashboard",
|
||||
};
|
||||
|
||||
export default function BlogCategoriesRoute() {
|
||||
return (
|
||||
<ProtectedRoute requiredApp="admin">
|
||||
<DashboardShell>
|
||||
<BlogCategoriesPage />
|
||||
</DashboardShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ProtectedRoute } from "@/components/AuthGuards";
|
||||
import DashboardShell from "@/components/DashboardShell";
|
||||
import WebsiteBlogForm from "@/components/WebsiteBlogForm";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Add blog post | IranBay Dashboard",
|
||||
};
|
||||
|
||||
export default function WebsiteBlogNewRoute() {
|
||||
return (
|
||||
<ProtectedRoute requiredApp="admin">
|
||||
<DashboardShell>
|
||||
<WebsiteBlogForm />
|
||||
</DashboardShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ProtectedRoute } from "@/components/AuthGuards";
|
||||
import DashboardShell from "@/components/DashboardShell";
|
||||
import WebsiteBlogListPage from "@/components/WebsiteBlogListPage";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Blog | IranBay Dashboard",
|
||||
};
|
||||
|
||||
export default function WebsiteBlogRoute() {
|
||||
return (
|
||||
<ProtectedRoute requiredApp="admin">
|
||||
<DashboardShell>
|
||||
<WebsiteBlogListPage />
|
||||
</DashboardShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ProtectedRoute } from "@/components/AuthGuards";
|
||||
import DashboardShell from "@/components/DashboardShell";
|
||||
import SpecialBusinessesPage from "@/components/SpecialBusinessesPage";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Carousels | IranBay Dashboard",
|
||||
};
|
||||
|
||||
export default function CarouselsRoute() {
|
||||
return (
|
||||
<ProtectedRoute requiredApp="admin">
|
||||
<DashboardShell>
|
||||
<SpecialBusinessesPage />
|
||||
</DashboardShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ProtectedRoute } from "@/components/AuthGuards";
|
||||
import DashboardShell from "@/components/DashboardShell";
|
||||
import WebsiteImagesPage from "@/components/WebsiteImagesPage";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Website images | IranBay Dashboard",
|
||||
};
|
||||
|
||||
export default function WebsiteImagesRoute() {
|
||||
return (
|
||||
<ProtectedRoute requiredApp="admin">
|
||||
<DashboardShell>
|
||||
<WebsiteImagesPage />
|
||||
</DashboardShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function SpecialBusinessesRedirect() {
|
||||
redirect("/website/carousels");
|
||||
}
|
||||
Reference in New Issue
Block a user