Initial commit: Hoomat storefront with Meshkee API and static-image slots.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-04 15:00:41 +03:30
co-authored by Cursor
commit cfabf3e327
95 changed files with 12651 additions and 0 deletions
+335
View File
@@ -0,0 +1,335 @@
import {
MESHKEE_API_BASE,
MESHKEE_DOMAIN,
} from "@/lib/meshkee/config";
import type {
AuthUser,
BlogPost,
Category,
Comment,
FaviconInfo,
Paginated,
Product,
StaticImageSlot,
StoreItemVariant,
TechnicalInfo,
Tenant,
} from "@/lib/meshkee/types";
export class MeshkeeApiError extends Error {
status: number;
payload: unknown;
constructor(message: string, status: number, payload?: unknown) {
super(message);
this.name = "MeshkeeApiError";
this.status = status;
this.payload = payload;
}
}
type FetchOptions = {
method?: string;
body?: unknown;
token?: string | null;
signal?: AbortSignal;
cache?: RequestCache;
next?: NextFetchRequestConfig;
};
async function request<T>(path: string, options: FetchOptions = {}): Promise<T> {
const headers: Record<string, string> = {
Accept: "application/json",
};
if (options.body !== undefined) {
headers["Content-Type"] = "application/json";
}
if (options.token) {
headers.Authorization = `Bearer ${options.token}`;
}
const init: RequestInit = {
method: options.method ?? "GET",
headers,
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
signal: options.signal,
};
// `next` / `cache` are server-only fetch extensions
if (typeof window === "undefined") {
if (options.cache) init.cache = options.cache;
if (options.next) {
(init as RequestInit & { next?: NextFetchRequestConfig }).next = options.next;
}
}
const response = await fetch(`${MESHKEE_API_BASE}${path}`, init);
const payload = await response.json().catch(() => null);
if (!response.ok) {
const record =
payload && typeof payload === "object"
? (payload as Record<string, unknown>)
: null;
const errorValue = record?.error;
const messageValue = record?.message;
const message =
(typeof errorValue === "string" && errorValue) ||
(typeof messageValue === "string" && messageValue) ||
"خطا در ارتباط با سرور";
throw new MeshkeeApiError(message, response.status, payload);
}
return payload as T;
}
function tenantPath(suffix = "") {
return `/tenants/${encodeURIComponent(MESHKEE_DOMAIN)}${suffix}`;
}
export async function getTenant(): Promise<Tenant> {
return request<Tenant>(tenantPath(), { next: { revalidate: 300 } });
}
export async function getFavicon(): Promise<FaviconInfo> {
return request<FaviconInfo>(tenantPath("/website/favicon"), {
next: { revalidate: 300 },
});
}
export async function getStaticImages(
pageKey?: string,
): Promise<StaticImageSlot[]> {
const query = pageKey ? `?pageKey=${encodeURIComponent(pageKey)}` : "";
const data = await request<{ items: StaticImageSlot[] }>(
`${tenantPath("/website/static-images")}${query}`,
{ next: { revalidate: 60 } },
);
return data.items ?? [];
}
export async function getStaticImageSlot(
key: string,
): Promise<StaticImageSlot | null> {
try {
return await request<StaticImageSlot>(
tenantPath(`/website/static-images/${encodeURIComponent(key)}`),
{ next: { revalidate: 60 } },
);
} catch {
return null;
}
}
export async function getCategories(
entityType: "product" | "blog" = "product",
): Promise<Category[]> {
const data = await request<{ items: Category[] }>(
`${tenantPath("/categories")}?entityType=${entityType}`,
{ next: { revalidate: 120 } },
);
return data.items ?? [];
}
export async function listStoreItems(params: {
page?: number;
pageSize?: number;
categoryId?: string;
name?: string;
productId?: string;
} = {}): Promise<Paginated<StoreItemVariant>> {
const query = new URLSearchParams();
if (params.page) query.set("page", String(params.page));
if (params.pageSize) query.set("pageSize", String(params.pageSize));
if (params.categoryId) query.set("categoryId", params.categoryId);
if (params.name) query.set("name", params.name);
if (params.productId) query.set("productId", params.productId);
const qs = query.toString();
return request<Paginated<StoreItemVariant>>(
`${tenantPath("/store-items")}${qs ? `?${qs}` : ""}`,
{ next: { revalidate: 60 } },
);
}
export async function getStoreItemVariant(
variantId: string,
): Promise<StoreItemVariant> {
const data = await request<{ variant: StoreItemVariant }>(
tenantPath(`/store-items/${encodeURIComponent(variantId)}`),
{ next: { revalidate: 60 } },
);
return data.variant;
}
export async function getProductById(productId: string): Promise<{
product: Product;
relatedProducts: Product[];
}> {
const data = await request<{
product: Product;
relatedProducts?: Product[] | null;
}>(tenantPath(`/products/by-id/${encodeURIComponent(productId)}`), {
next: { revalidate: 60 },
});
return {
product: data.product,
relatedProducts: data.relatedProducts ?? [],
};
}
export async function getProductTechnicalInfo(
productId: string,
): Promise<TechnicalInfo> {
return request<TechnicalInfo>(
tenantPath(`/products/by-id/${encodeURIComponent(productId)}/technical-info`),
{ next: { revalidate: 120 } },
);
}
export async function listBlogs(params: {
page?: number;
pageSize?: number;
} = {}): Promise<Paginated<BlogPost>> {
const query = new URLSearchParams();
if (params.page) query.set("page", String(params.page));
if (params.pageSize) query.set("pageSize", String(params.pageSize));
const qs = query.toString();
return request<Paginated<BlogPost>>(
`${tenantPath("/blogs")}${qs ? `?${qs}` : ""}`,
{ next: { revalidate: 60 } },
);
}
export async function getBlogById(blogId: string): Promise<BlogPost> {
const data = await request<{ blog?: BlogPost } & BlogPost>(
tenantPath(`/blogs/by-id/${encodeURIComponent(blogId)}`),
{ next: { revalidate: 60 } },
);
return data.blog ?? data;
}
export async function listComments(params: {
entityType: "product" | "blog";
entityId: string;
}): Promise<Comment[]> {
const query = new URLSearchParams({
entityType: params.entityType,
entityId: params.entityId,
});
const data = await request<{ items: Comment[] }>(
`${tenantPath("/comments")}?${query.toString()}`,
{ cache: "no-store" },
);
return data.items ?? [];
}
export async function submitComment(
body: {
entityType: "product" | "blog";
entityId: string;
authorName: string;
authorEmail?: string;
text: string;
},
token?: string | null,
) {
return request(tenantPath("/comments"), {
method: "POST",
body,
token,
cache: "no-store",
});
}
export async function submitContact(body: {
title: string;
name: string;
email?: string;
cellNumber?: string;
text: string;
}) {
return request(tenantPath("/contact-submissions"), {
method: "POST",
body,
cache: "no-store",
});
}
export async function getMe(token: string, signal?: AbortSignal): Promise<AuthUser> {
const data = await request<{ user?: AuthUser } & AuthUser>("/auth/me", {
token,
signal,
cache: "no-store",
});
return data.user ?? data;
}
export async function refreshTokens(refreshToken: string) {
return request<{ accessToken: string; refreshToken: string }>("/auth/refresh", {
method: "POST",
body: { refreshToken },
cache: "no-store",
});
}
export async function createAuthHandoff(token: string) {
return request<{ ticket?: string }>("/auth/handoff", {
method: "POST",
token,
cache: "no-store",
});
}
export async function getCart(businessId: string, token: string) {
return request(`/businesses/${encodeURIComponent(businessId)}/cart`, {
token,
cache: "no-store",
});
}
export async function addCartItem(
businessId: string,
token: string,
body: { storeItemVariantId: string; quantity: number },
) {
return request(`/businesses/${encodeURIComponent(businessId)}/cart/items`, {
method: "POST",
token,
body,
cache: "no-store",
});
}
export async function updateCartItem(
businessId: string,
token: string,
itemId: string,
quantity: number,
) {
return request(
`/businesses/${encodeURIComponent(businessId)}/cart/items/${encodeURIComponent(itemId)}`,
{
method: "PATCH",
token,
body: { quantity },
cache: "no-store",
},
);
}
export async function removeCartItem(
businessId: string,
token: string,
itemId: string,
) {
return request(
`/businesses/${encodeURIComponent(businessId)}/cart/items/${encodeURIComponent(itemId)}`,
{
method: "DELETE",
token,
cache: "no-store",
},
);
}
+117
View File
@@ -0,0 +1,117 @@
import type { CartLine, CartState } from "@/lib/meshkee/types";
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
}
function asString(value: unknown): string {
return typeof value === "string" ? value : value == null ? "" : String(value);
}
function asNumber(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim() && !Number.isNaN(Number(value))) {
return Number(value);
}
return null;
}
export function emptyCart(): CartState {
return { items: [], subtotal: 0, total: 0 };
}
export function cartFromGuest(
items: Array<{
id: string;
name: string;
slug?: string;
price: number;
image?: string | null;
quantity: number;
}>,
): CartState {
const lines: CartLine[] = items.map((item) => ({
id: item.id,
storeItemVariantId: item.id,
name: item.name,
slug: item.slug,
image: item.image ?? null,
price: item.price,
quantity: item.quantity,
}));
const subtotal = lines.reduce((sum, line) => sum + line.price * line.quantity, 0);
return { items: lines, subtotal, total: subtotal };
}
export function normalizeCartResponse(payload: unknown): CartState {
const root = asRecord(payload);
const cart = asRecord(root.cart ?? root);
const source = Array.isArray(cart.items)
? cart.items
: Array.isArray(root.items)
? root.items
: [];
const items: CartLine[] = source.map((raw) => {
const item = asRecord(raw);
const product = asRecord(item.product);
const variant = asRecord(item.variant);
const storeItem = asRecord(item.storeItem);
const quantity = asNumber(item.quantity) ?? 1;
const price =
asNumber(item.unitPrice) ??
asNumber(item.price) ??
asNumber(item.discountedPrice) ??
asNumber(variant.discountedPrice) ??
asNumber(variant.price) ??
asNumber(storeItem.discountedPrice) ??
asNumber(storeItem.price) ??
0;
const selections = Array.isArray(item.selections)
? item.selections
: Array.isArray(variant.selections)
? variant.selections
: [];
const optionLabel = selections
.map((entry) => {
const row = asRecord(entry);
return asString(row.label || row.optionLabel || row.value);
})
.filter(Boolean)
.join(" / ");
return {
id: asString(item.id || item.itemId),
storeItemVariantId: asString(
item.storeItemVariantId || variant.id || storeItem.id || item.variantId,
),
name: asString(
item.productTitle ||
item.productNameFa ||
product.nameFa ||
product.name ||
product.title ||
storeItem.productTitle ||
storeItem.productNameFa ||
"محصول",
),
slug: asString(product.slug || item.productSlug || storeItem.productSlug) || undefined,
image:
asString(
item.productImage ||
item.image ||
product.image ||
product.thumbnailUrl ||
storeItem.productImage,
) || null,
price,
quantity,
optionLabel: optionLabel || undefined,
};
});
const subtotal =
asNumber(cart.subtotal) ?? items.reduce((sum, line) => sum + line.price * line.quantity, 0);
const total = asNumber(cart.total) ?? subtotal;
return { items, subtotal, total };
}
+9
View File
@@ -0,0 +1,9 @@
export const MESHKEE_DOMAIN = "hoomatstore.com";
export const MESHKEE_API_BASE = "https://api.meshkee.com/api/v1";
export const CUSTOMER_APP_URL = `https://customer.${MESHKEE_DOMAIN}`;
export const BUSINESS_APP_URL = `https://business.${MESHKEE_DOMAIN}`;
export const ACCESS_TOKEN_KEY = "meshkee_customer_access_token";
export const REFRESH_TOKEN_KEY = "meshkee_customer_refresh_token";
export const GUEST_CART_KEY = "meshkee-guest-cart";
export const GUEST_CART_URL_PARAM = "guestCart";
+102
View File
@@ -0,0 +1,102 @@
import type { Category, CategoryTreeNode, GuestCartLine } from "@/lib/meshkee/types";
export function slugifyFa(value: string | null | undefined): string {
const raw = (value ?? "").trim().toLowerCase();
if (!raw) return "item";
return raw
.replace(/\s+/g, "-")
.replace(/[^\u0600-\u06FFa-z0-9-]+/g, "")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "") || "item";
}
export function formatPrice(value: number | null | undefined): string {
const amount = Number(value ?? 0);
return `${new Intl.NumberFormat("fa-IR").format(amount)} تومان`;
}
export function formatDate(value: string | null | undefined): string {
if (!value) return "";
try {
return new Intl.DateTimeFormat("fa-IR", {
year: "numeric",
month: "long",
day: "numeric",
}).format(new Date(value));
} catch {
return value;
}
}
export function displayName(user: {
firstName?: string | null;
lastName?: string | null;
}): string {
const first = user.firstName?.trim();
if (first) return first;
const last = user.lastName?.trim();
if (last) return last;
return "کاربر";
}
export function buildCategoryTree(categories: Category[]): CategoryTreeNode[] {
const map = new Map<string, CategoryTreeNode>();
for (const category of categories) {
if (category.isActive === false) continue;
map.set(category.id, { ...category, children: [] });
}
const roots: CategoryTreeNode[] = [];
for (const node of map.values()) {
if (node.parentId && map.has(node.parentId)) {
map.get(node.parentId)!.children.push(node);
} else {
roots.push(node);
}
}
const sortNodes = (nodes: CategoryTreeNode[]) => {
nodes.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
nodes.forEach((node) => sortNodes(node.children));
};
sortNodes(roots);
return roots;
}
export function categoryLabel(category: Category): string {
return category.nameFa?.trim() || category.name;
}
export function storeItemName(item: {
productNameFa?: string | null;
productTitle?: string | null;
label?: string | null;
}): string {
return item.productNameFa?.trim() || item.productTitle?.trim() || "محصول";
}
export function storeItemPrice(item: {
price: number;
discountedPrice?: number | null;
}): number {
return item.discountedPrice != null && item.discountedPrice > 0
? item.discountedPrice
: item.price;
}
export function normalizeGuestLines(items: GuestCartLine[]): GuestCartLine[] {
const result: GuestCartLine[] = [];
for (const item of items) {
const id = (item.id || "").trim();
if (!id) continue;
result.push({
id,
name: item.name || "محصول",
slug: item.slug,
price: Number(item.price) || 0,
image: item.image ?? null,
quantity: Math.max(1, Math.floor(item.quantity) || 1),
});
}
return result;
}
+49
View File
@@ -0,0 +1,49 @@
import {
GUEST_CART_KEY,
GUEST_CART_URL_PARAM,
} from "@/lib/meshkee/config";
import { normalizeGuestLines } from "@/lib/meshkee/format";
import type { GuestCartLine } from "@/lib/meshkee/types";
function parseLines(raw: string | null): GuestCartLine[] {
if (!raw) return [];
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? normalizeGuestLines(parsed) : [];
} catch {
return [];
}
}
export function readGuestCart(): GuestCartLine[] {
if (typeof window === "undefined") return [];
return parseLines(localStorage.getItem(GUEST_CART_KEY));
}
export function writeGuestCart(items: GuestCartLine[]) {
if (typeof window === "undefined") return;
const normalized = normalizeGuestLines(items);
localStorage.setItem(GUEST_CART_KEY, JSON.stringify(normalized));
}
export function clearGuestCart() {
if (typeof window === "undefined") return;
localStorage.removeItem(GUEST_CART_KEY);
}
export function encodeGuestCartForUrl(items: GuestCartLine[]): string {
const payload = normalizeGuestLines(items).map((item) => ({
id: item.id,
name: item.name,
...(item.slug ? { slug: item.slug } : {}),
price: item.price,
image: item.image ?? null,
quantity: item.quantity,
}));
return btoa(unescape(encodeURIComponent(JSON.stringify(payload))))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/g, "");
}
export { GUEST_CART_URL_PARAM };
+127
View File
@@ -0,0 +1,127 @@
import type { StaticImageItem, StaticImageSlot } from "@/lib/meshkee/types";
/**
* Slot catalog for Meshkee dashboard import.
* If UI image size/aspect changes during development, update these values.
*/
export const STATIC_IMAGE_SLOT_CATALOG = [
{
key: "slider",
label: "Homepage slider",
kind: "list" as const,
pageKey: "home",
aspectRatio: "16:9",
itemCount: null,
recommendedWidth: 1440,
},
{
key: "categories-icon",
label: "Quick category icons",
kind: "list" as const,
pageKey: "home",
aspectRatio: "1:1",
itemCount: 6,
recommendedWidth: 192,
},
{
key: "special-sale-banner",
label: "Special sale banner",
kind: "single" as const,
pageKey: "home",
aspectRatio: "3:5",
itemCount: 1,
recommendedWidth: 520,
},
{
key: "product-features-image",
label: "Product features sofa image",
kind: "single" as const,
pageKey: "home",
aspectRatio: "2:1",
itemCount: 1,
recommendedWidth: 900,
},
{
key: "contactus-image",
label: "Homepage contact guarantee image",
kind: "single" as const,
pageKey: "home",
aspectRatio: "4:5",
itemCount: 1,
recommendedWidth: 720,
},
{
key: "two-banner-row",
label: "Two banner row",
kind: "list" as const,
pageKey: "home",
aspectRatio: "12:5",
itemCount: 2,
recommendedWidth: 1200,
},
{
key: "aboutus-image",
label: "Homepage about image",
kind: "single" as const,
pageKey: "home",
aspectRatio: "6:5",
itemCount: 1,
recommendedWidth: 720,
},
{
key: "three-banner-row",
label: "Three banner row",
kind: "list" as const,
pageKey: "home",
aspectRatio: "12:5",
itemCount: 3,
recommendedWidth: 1200,
},
{
key: "about-us-image",
label: "About page images",
kind: "list" as const,
pageKey: "about",
aspectRatio: "6:5",
itemCount: 2,
recommendedWidth: 720,
},
];
export function findSlot(
slots: StaticImageSlot[],
key: string,
): StaticImageSlot | undefined {
return slots.find((slot) => slot.key === key);
}
export function slotImages(
slots: StaticImageSlot[],
key: string,
): StaticImageItem[] {
const slot = findSlot(slots, key);
if (!slot?.images?.length) return [];
if (slot.kind === "single") {
return slot.images[0]?.url ? [slot.images[0]] : [];
}
return slot.images.filter((image) => Boolean(image?.url));
}
export function firstSlotImage(
slots: StaticImageSlot[],
key: string,
): StaticImageItem | null {
return slotImages(slots, key)[0] ?? null;
}
export function imageTitleFa(image: StaticImageItem | null | undefined): string {
return image?.titleFa?.trim() || image?.title?.trim() || "";
}
export function imageTitleEn(image: StaticImageItem | null | undefined): string {
return image?.titleEn?.trim() || "";
}
export function imageSubtext(image: StaticImageItem | null | undefined): string {
return image?.subtext?.trim() || "";
}
+187
View File
@@ -0,0 +1,187 @@
export type Tenant = {
id: string;
name: string;
nameFa?: string | null;
slug?: string;
domain: string;
logoUrl?: string | null;
logoDarkUrl?: string | null;
faviconUrl?: string | null;
specialProductsSource?: "product" | "store_item";
};
export type FaviconInfo = {
faviconUrl: string | null;
logoUrl: string | null;
logoDarkUrl: string | null;
hasDedicatedFavicon: boolean;
};
export type Category = {
id: string;
parentId: string | null;
name: string;
nameFa?: string | null;
slug: string;
entityType?: string;
sortOrder?: number;
isActive?: boolean;
};
export type CategoryTreeNode = Category & { children: CategoryTreeNode[] };
export type StoreItemVariant = {
id: string;
storeItemId?: string;
productId: string;
productTitle?: string | null;
productNameFa?: string | null;
productImage?: string | null;
productTotalStock?: number | null;
selections?: Array<{
variationId?: string;
variationName?: string;
optionId?: string;
value?: string;
}>;
label?: string | null;
price: number;
discountedPrice?: number | null;
stockQuantity?: number | null;
isFestival?: boolean;
sortOrder?: number;
updatedAt?: string;
};
export type Paginated<T> = {
items: T[];
total: number;
page: number;
pageSize: number;
};
export type ProductImage = {
mediaId?: string;
url: string;
};
export type Product = {
id: string;
title?: string | null;
nameFa?: string | null;
summary?: string | null;
descriptionHtml?: string | null;
slug?: string | null;
categoryId?: string | null;
categoryName?: string | null;
categoryNameFa?: string | null;
thumbnailUrl?: string | null;
image?: string | null;
images?: ProductImage[];
commentCount?: number;
};
export type TechnicalInfo = {
form: {
fields: Array<{
id: string;
label?: string;
labelFa?: string;
key?: string;
type?: string;
sortOrder?: number;
options?: Array<{ id: string; label?: string; labelFa?: string }>;
}>;
} | null;
values: Array<{
fieldId: string;
textValue?: string | null;
optionId?: string | null;
optionIds?: string[] | null;
}>;
};
export type BlogPost = {
id: string;
title: string;
slug: string;
type?: string;
abstract?: string | null;
mainTextHtml?: string | null;
categoryId?: string | null;
categoryName?: string | null;
titleImageUrl?: string | null;
publishedAt?: string | null;
createdAt?: string | null;
commentCount?: number;
};
export type Comment = {
id: string;
authorName?: string;
text: string;
createdAt?: string;
};
export type AuthUser = {
id: string;
firstName?: string | null;
lastName?: string | null;
cellNumber?: string | null;
email?: string | null;
roles?: string[];
isSuperAdmin?: boolean;
businesses?: Array<{
id: string;
name?: string;
isOwner?: boolean;
teamRole?: string | null;
}>;
};
export type CartLine = {
id: string;
storeItemVariantId: string;
name: string;
slug?: string;
image?: string | null;
price: number;
quantity: number;
optionLabel?: string;
};
export type CartState = {
items: CartLine[];
subtotal: number;
total: number;
};
export type GuestCartLine = {
id: string;
name: string;
slug?: string;
price: number;
image?: string | null;
quantity: number;
};
export type StaticImageItem = {
id?: string;
url: string;
title?: string | null;
titleFa?: string | null;
titleEn?: string | null;
subtext?: string | null;
linkUrl?: string | null;
};
export type StaticImageSlot = {
key: string;
pageKey?: string;
kind: "single" | "list";
label?: string;
aspectRatio?: string;
itemCount?: number | null;
recommendedWidth?: number | null;
images: StaticImageItem[];
};
+55
View File
@@ -0,0 +1,55 @@
import {
BUSINESS_APP_URL,
CUSTOMER_APP_URL,
} from "@/lib/meshkee/config";
import {
encodeGuestCartForUrl,
GUEST_CART_URL_PARAM,
} from "@/lib/meshkee/guest-cart";
import { slugifyFa } from "@/lib/meshkee/format";
import type { GuestCartLine } from "@/lib/meshkee/types";
export function customerLoginUrl(redirectPath = "/"): string {
const url = new URL("/login", CUSTOMER_APP_URL);
url.searchParams.set("redirect", redirectPath.startsWith("/") ? redirectPath : `/${redirectPath}`);
return url.toString();
}
export function customerProfileUrl(): string {
return `${CUSTOMER_APP_URL}/`;
}
export function businessDashboardUrl(): string {
return `${BUSINESS_APP_URL}/`;
}
export function customerContinueCheckoutUrl(
isAuthenticated: boolean,
guestItems: GuestCartLine[] = [],
): string {
if (isAuthenticated) {
const url = new URL("/checkout/cart", CUSTOMER_APP_URL);
if (guestItems.length > 0) {
url.hash = `${GUEST_CART_URL_PARAM}=${encodeGuestCartForUrl(guestItems)}`;
}
return url.toString();
}
let redirect = "/checkout/cart";
if (guestItems.length > 0) {
redirect = `${redirect}#${GUEST_CART_URL_PARAM}=${encodeGuestCartForUrl(guestItems)}`;
}
return customerLoginUrl(redirect);
}
export function storeItemHref(variantId: string, name?: string | null): string {
return `/store/${encodeURIComponent(variantId)}/${slugifyFa(name)}`;
}
export function storeCategoryHref(categoryId: string, name?: string | null): string {
return `/store?categoryId=${encodeURIComponent(categoryId)}&name=${encodeURIComponent(slugifyFa(name))}`;
}
export function blogHref(id: string, title?: string | null): string {
return `/blog/${encodeURIComponent(id)}/${slugifyFa(title)}`;
}
+209
View File
@@ -0,0 +1,209 @@
export const site = {
name: "مبلمان هومت",
title: "فروشگاه اینترنتی مبلمان و دکوراسیون | مبلمان هومت",
description: "خرید آنلاین مبلمان راحتی، مدرن و کلاسیک با قیمت‌های ویژه و کیفیت عالی",
phone: "0992-3964060",
mobile: "0902-0462518",
whatsapp: "09223964060",
palette: {
primary: "#00234B",
secondary: "#CC9966",
tertiary: "#883F3F",
cream: "#F8F3EC",
ink: "#12151C",
},
logo: "/assets/hoomat-logo.png",
heroImages: [
"/assets/sample/spacejoy-c0JoR_-2x3E-unsplash.jpg",
"/assets/sample/premium_photo-1682259448839-2ea3bc7f9e73.avif",
"/assets/sample/charlesdeluvio-CdTH3UwWlR4-unsplash.jpg",
],
addresses: [
"دفتر مرکزی: تهران _ شهرک غرب _ مجتمع تجاری پلاتین _ طبقه چهارم _ مبلمان هومت",
"نماینده فروش: قم _ بازار مبل گنجوی _ بلوار نظامی گنجوی _ بین کوی 5 و 7 _ مبلمان هومت",
"کارخانه: قم _ بلوار حضرت معصومه _ کوی 35 _ جنب قالیشویی امیرکبیر _ کارخانه مبلمان هومت",
],
};
export const navItems = [
{ label: "محصولات", href: "/store" },
{ label: "درباره ما", href: "/about-us" },
{ label: "تماس با ما", href: "/contactus" },
{ label: "مجله دکوراسیون", href: "/blog" },
];
export const heroSlides = [
{
eyebrow: "ONLINE FURNITURE SHOP",
title: "خرید اینترنتی مبلمان و دکوراسیون منزل",
description:
"تولید کننده انواع مبلمان راحتی و کلاسیک با امکان خرید آنلاین، مشاوره مستقیم و قیمت‌های ویژه برای خانه‌های امروزی.",
image: site.heroImages[0],
},
{
eyebrow: "FURNITURE PRODUCER",
title: "تولید کننده انواع مبلمان راحتی و کلاسیک",
description:
"مدل‌های مینیمال، مدرن و کلاسیک را با راهنمایی مستقیم انتخاب کنید و متناسب با چیدمان خانه سفارش دهید.",
image: site.heroImages[1],
},
{
eyebrow: "SPECIAL SALE",
title: "جشنواره مبلمان با قیمت‌های ویژه",
description:
"برای انتخاب پارچه، رنگ و مدل دلخواه با مشاوران هومت در تماس باشید و خریدی مطمئن‌تر تجربه کنید.",
image: site.heroImages[2],
},
];
export const categories = [
{
title: "مبل راحتی",
subtitle: "Sofa Set",
image: "/assets/sample/products/sam-moghadam-kvmdsTrGOBM-unsplash.jpg",
},
{
title: "مبل کلاسیک",
subtitle: "Royal Sofa Set",
image: "/assets/sample/products/scopic-ltd-NLlWwR4d3qU-unsplash.jpg",
},
{
title: "سرویس خواب",
subtitle: "Bed Set",
image: "/assets/sample/jean-philippe-delberghe-feijc-nqWKM-unsplash.jpg",
},
{
title: "میز ناهارخوری",
subtitle: "Dinning Set Table",
image: "/assets/sample/lucas-kepner-o_-snE5cDTI-unsplash.jpg",
},
];
export const categoryQuickLinks = [
{ title: "مبل راحتی", icon: "sofa", iconImage: "/assets/sample/categories/sofa.png" },
{ title: "مبل کلاسیک", icon: "chair", iconImage: "/assets/sample/categories/classic.png" },
{ title: "مبل مینیمال", icon: "minimal", iconImage: "/assets/sample/categories/console.png" },
{ title: "مبل تخت خواب شو", icon: "sleep", iconImage: "/assets/sample/categories/chair.png" },
{ title: "میز ناهارخوری", icon: "table", iconImage: "/assets/sample/categories/dining-table.png" },
{ title: "سرویس خواب", icon: "bed", iconImage: "/assets/sample/categories/bed.png" },
];
export const features = [
{
title: "تولید کننده انواع مبلمان",
subtitle: "FURNITURE PRODUCER",
subtext: "خرید مستقیم از مجموعه هومت",
},
{
title: "فروشگاه اینترنتی مبلمان",
subtitle: "ONLINE FURNITURE SHOP",
subtext: "انتخاب ساده و سفارش آنلاین",
},
{
title: "ضمانت نامه کتبی مبلمان",
subtitle: "GUARANTEE",
subtext: "اطمینان بیشتر هنگام خرید",
},
{
title: "ارسال مبل راحتی و کلاسیک",
subtitle: "DELIVERY",
subtext: "هماهنگی ارسال تا محل شما",
},
];
export const products = [
{
title: "مبل راحتی مینیمال 7 نفره دسته چوبی مدل سون",
enName: "MINIMAL SOFA SEVEN",
price: "85,900,000 تومان",
badge: "جدیدترین مبل",
image: "/assets/sample/products/ambo-ampeng-KEGGJPKxcSc-unsplash.jpg",
},
{
title: "مبل راحتی مدرن 8 نفره مدل النا",
enName: "ELENA SOFA FURNITURE",
price: "209,250,000 تومان",
badge: "Newest sofa",
image: "/assets/sample/products/kevin-shek-kcxZ-YUZ8VE-unsplash.jpg",
},
{
title: "مبل راحتی مینیمال مدل سارینا 8 نفره",
enName: "SARINA SOFA FURNITURE",
price: "تماس بگیرید",
badge: "فروش ویژه",
image: "/assets/sample/products/premium_photo-1682410454254-809412bf04bd.avif",
},
{
title: "مبل راحتی مینیمال جدید مدل نوید 8 نفره",
enName: "NAVID MINIMAL SOFA",
price: "212,500,000 تومان",
badge: "special sale",
image: "/assets/sample/products/premium_photo-1683120739323-0a920bdb15a4.avif",
},
];
export const diningProducts = [
{
title: "ست میز ناهارخوری کلاسیک",
enName: "CLASSIC DINING SET",
price: "تماس بگیرید",
image: "/assets/sample/products/sam-moghadam-xej3GOtAQ-o-unsplash.jpg",
},
{
title: "ست میز ناهارخوری مدرن",
enName: "MODERN DINING SET",
price: "تماس بگیرید",
image: "/assets/sample/premium_photo-1670950411934-e21abf39191e.avif",
},
{
title: "صندلی ناهارخوری",
enName: "DINING CHAIR",
price: "تماس بگیرید",
image: "/assets/sample/premium_photo-1682410455752-1007e8cea553.avif",
},
];
export const articles = [
{
title: "راهنمای خرید مبل راحتی با قیمت مناسب ارزشمند تر است تا تعمیر کردن مبل؟",
date: "۱۲ مرداد ۱۴۰۴",
image: "/assets/sample/spacejoy-c0JoR_-2x3E-unsplash.jpg",
},
{
title: "خرید مبل اقساطی: خرید انواع مبلمان با شرایط ویژه",
date: "۵ مرداد ۱۴۰۴",
image: "/assets/sample/charlesdeluvio-CKbBn7ZhmyM-unsplash.jpg",
},
{
title: "بازار مبل قم کجاست؟",
date: "۲۸ تیر ۱۴۰۴",
image: "/assets/sample/charlesdeluvio-CdTH3UwWlR4-unsplash.jpg",
},
{
title: "خرید مبل راحتی آنلاین | راهنمای جامع انتخاب بهترین مدل‌ها",
date: "۲۰ تیر ۱۴۰۴",
image: "/assets/sample/premium_photo-1682259448839-2ea3bc7f9e73.avif",
},
{
title: "نکات کلیدی برای ست کردن مبلمان راحتی و میز ناهارخوری",
date: "۱۴ تیر ۱۴۰۴",
image: "/assets/sample/lucas-kepner-o_-snE5cDTI-unsplash.jpg",
},
{
title: "رنگ سال مبل ۱۴۰۴: ایده‌هایی برای دکوراسیون داخلی",
date: "۸ تیر ۱۴۰۴",
image: "/assets/sample/jean-philippe-delberghe-feijc-nqWKM-unsplash.jpg",
},
];
export const aboutCopy = {
eyebrow: "About Us",
title: "با مبلمان هومت بیشتر آشنا شویم",
quote: "زیبایی را لمس کنید",
storyTitle: "داستان مبلمان هومت",
story:
"مبلمان هومت فروشگاه آنلاین خرید مبلمان منزل با تنوع بالا و قیمت ویژه است. تمرکز ما تولید و عرضه انواع مبلمان راحتی، کلاسیک، مینیمال، سرویس خواب و میز ناهارخوری با کیفیت قابل اعتماد و تجربه خرید ساده است.",
visionTitle: "دیدگاه ما",
vision:
"هدف هومت ساختن فضایی گرم‌تر برای خانه‌های ایرانی است؛ جایی که انتخاب مبلمان با اطلاعات کامل، قیمت شفاف، مشاوره مستقیم و پشتیبانی واقعی همراه باشد.",
};