diff --git a/apps/business/public/fonts/iranyekan/IRANYekanWebLight.ttf b/apps/business/public/fonts/iranyekan/IRANYekanWebLight.ttf new file mode 100755 index 0000000..4431c6f Binary files /dev/null and b/apps/business/public/fonts/iranyekan/IRANYekanWebLight.ttf differ diff --git a/apps/business/public/fonts/iranyekan/IRANYekanWebRegular.ttf b/apps/business/public/fonts/iranyekan/IRANYekanWebRegular.ttf new file mode 100755 index 0000000..77dec5f Binary files /dev/null and b/apps/business/public/fonts/iranyekan/IRANYekanWebRegular.ttf differ diff --git a/apps/business/src/components/AddCustomerModal.tsx b/apps/business/src/components/AddCustomerModal.tsx index 0b68894..a32b409 100644 --- a/apps/business/src/components/AddCustomerModal.tsx +++ b/apps/business/src/components/AddCustomerModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { ApiError } from '../lib/api' import { toE164CellNumber } from '../lib/cellNumber' @@ -96,7 +97,7 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP } } - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/AddProductByAiModal.tsx b/apps/business/src/components/AddProductByAiModal.tsx index 30ea18b..823c318 100644 --- a/apps/business/src/components/AddProductByAiModal.tsx +++ b/apps/business/src/components/AddProductByAiModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react' +import { createPortal } from 'react-dom' import { Sparkles, X } from 'lucide-react' import { useToast } from '../context/ToastContext' import { ApiError } from '../lib/api' @@ -123,7 +124,7 @@ export function AddProductByAiModal({ if (!mounted) return null - return ( + return createPortal(
e.target === e.currentTarget && !isSubmitting && onClose()} @@ -230,5 +231,7 @@ export function AddProductByAiModal({
+ , + document.body, ) } diff --git a/apps/business/src/components/AddVariationModal.tsx b/apps/business/src/components/AddVariationModal.tsx index 19323e4..3f51c2b 100644 --- a/apps/business/src/components/AddVariationModal.tsx +++ b/apps/business/src/components/AddVariationModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react' +import { createPortal } from 'react-dom' import { Plus, X } from 'lucide-react' import type { Variation, VariationType } from '../types/variation' import { COLOR_OPTIONS, SIZE_OPTIONS } from '../types/variation' @@ -114,7 +115,7 @@ export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModal ? selectedSizes.length > 0 : customValues.some((v) => v.trim()) - return ( + return createPortal(
- + , + document.body, ) } diff --git a/apps/business/src/components/AddWebsiteSliderSlideModal.tsx b/apps/business/src/components/AddWebsiteSliderSlideModal.tsx index d1761d3..fbfc751 100644 --- a/apps/business/src/components/AddWebsiteSliderSlideModal.tsx +++ b/apps/business/src/components/AddWebsiteSliderSlideModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { ImageCropper } from './ImageCropper' import modalStyles from './CategoryModal.module.css' @@ -70,7 +71,7 @@ export function AddWebsiteSliderSlideModal({ }) } - return ( + return createPortal(
e.target === e.currentTarget && onClose()} @@ -149,5 +150,7 @@ export function AddWebsiteSliderSlideModal({
+ , + document.body, ) } diff --git a/apps/business/src/components/BlogCard.module.css b/apps/business/src/components/BlogCard.module.css index 7cbea6a..a686af7 100644 --- a/apps/business/src/components/BlogCard.module.css +++ b/apps/business/src/components/BlogCard.module.css @@ -91,6 +91,7 @@ } .title { + margin: 0; font-size: 15px; font-weight: 600; line-height: 1.4; @@ -102,6 +103,14 @@ overflow: hidden; } +.title[dir='rtl'] { + text-align: right; +} + +.title[dir='ltr'] { + text-align: left; +} + .abstract { margin: 0; font-size: 13px; @@ -113,6 +122,14 @@ overflow: hidden; } +.abstract[dir='rtl'] { + text-align: right; +} + +.abstract[dir='ltr'] { + text-align: left; +} + .meta { margin: 0; font-size: 12px; diff --git a/apps/business/src/components/BlogCard.tsx b/apps/business/src/components/BlogCard.tsx index a98e36a..b5db0d8 100644 --- a/apps/business/src/components/BlogCard.tsx +++ b/apps/business/src/components/BlogCard.tsx @@ -6,6 +6,7 @@ import { formatBlogCardDate, isBlogVerified, } from '../services/blogService' +import { textLocaleAttrs } from '../utils/textLocale' import { Tooltip } from './Tooltip' import cardStyles from './BlogCard.module.css' import controlStyles from './ProductCard.module.css' @@ -31,6 +32,9 @@ export function BlogCard({ }: BlogCardProps) { const verified = isBlogVerified(blog) const publishDate = formatBlogCardDate(blog.publishedAt ?? blog.createdAt) + const titleLocale = textLocaleAttrs(blog.title) + const abstractLocale = textLocaleAttrs(blog.abstract) + const hasAbstract = Boolean(blog.abstract?.trim()) return (
@@ -60,11 +64,27 @@ export function BlogCard({
-

{blog.title}

- {blog.abstract ? ( -

{blog.abstract}

+

+ {blog.title} +

+ {hasAbstract ? ( +

+ {blog.abstract} +

) : ( -

No summary yet.

+

+ No summary yet. +

)}

{formatBlogAuthor(blog.author)} diff --git a/apps/business/src/components/BlogCategoryRow.tsx b/apps/business/src/components/BlogCategoryRow.tsx index 528b190..3c76c85 100644 --- a/apps/business/src/components/BlogCategoryRow.tsx +++ b/apps/business/src/components/BlogCategoryRow.tsx @@ -1,4 +1,4 @@ -import { FolderPlus, Trash2, ChevronRight } from 'lucide-react' +import { FolderPlus, Pencil, Trash2, ChevronRight } from 'lucide-react' import type { Category } from '../types/category' import { Tooltip } from './Tooltip' import styles from './CategoryRow.module.css' @@ -10,6 +10,7 @@ interface BlogCategoryRowProps { expanded: boolean onToggle: () => void onAddSub: (id: string) => void + onEdit: (id: string) => void onRemove: (id: string) => void } @@ -20,10 +21,11 @@ export function BlogCategoryRow({ expanded, onToggle, onAddSub, + onEdit, onRemove, }: BlogCategoryRowProps) { return ( -

+
+ + +
+ , + document.body, ) } diff --git a/apps/business/src/components/BrandModal.tsx b/apps/business/src/components/BrandModal.tsx index 54a4e2e..7897fa6 100644 --- a/apps/business/src/components/BrandModal.tsx +++ b/apps/business/src/components/BrandModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { ImageCropper } from './ImageCropper' import type { Brand, BrandFormData } from '../types/brand' @@ -73,7 +74,7 @@ export function BrandModal({ }) } - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/CategoryAiPromptModal.tsx b/apps/business/src/components/CategoryAiPromptModal.tsx index 7917bff..f505840 100644 --- a/apps/business/src/components/CategoryAiPromptModal.tsx +++ b/apps/business/src/components/CategoryAiPromptModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { Sparkles, X } from 'lucide-react' import { ApiError } from '../lib/api' import modalStyles from './CategoryModal.module.css' @@ -79,7 +80,7 @@ export function CategoryAiPromptModal({ if (!mounted) return null - return ( + return createPortal(
e.target === e.currentTarget && !isRunning && onClose()} @@ -157,5 +158,7 @@ export function CategoryAiPromptModal({
+ , + document.body, ) } diff --git a/apps/business/src/components/CategoryModal.tsx b/apps/business/src/components/CategoryModal.tsx index ad4ee03..6df3597 100644 --- a/apps/business/src/components/CategoryModal.tsx +++ b/apps/business/src/components/CategoryModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import type { Category, CategoryFormData } from '../types/category' import { flattenCategories } from '../utils/categories' @@ -79,7 +80,7 @@ export function CategoryModal({ categories.filter((category) => category.id !== editingCategory?.id), ) - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/CategoryRow.tsx b/apps/business/src/components/CategoryRow.tsx index 6783507..27d6aa4 100644 --- a/apps/business/src/components/CategoryRow.tsx +++ b/apps/business/src/components/CategoryRow.tsx @@ -35,7 +35,7 @@ export function CategoryRow({ return (
+ , + document.body, ) } diff --git a/apps/business/src/components/ContactSubmissionDetailModal.tsx b/apps/business/src/components/ContactSubmissionDetailModal.tsx index 42345e8..a2ebcc5 100644 --- a/apps/business/src/components/ContactSubmissionDetailModal.tsx +++ b/apps/business/src/components/ContactSubmissionDetailModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import type { ContactSubmission } from '../types/contactSubmission' import { formatCellForDisplay } from '../lib/cellNumber' @@ -57,7 +58,7 @@ export function ContactSubmissionDetailModal({ const { date, time } = formatDateTime(submission.createdAt) - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/CreateProductVariantModal.tsx b/apps/business/src/components/CreateProductVariantModal.tsx index 0a201a7..f23fe83 100644 --- a/apps/business/src/components/CreateProductVariantModal.tsx +++ b/apps/business/src/components/CreateProductVariantModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import type { ProductVariant } from '../types/productVariant' import { buildVariantLabel } from '../types/productVariant' @@ -96,7 +97,7 @@ export function CreateProductVariantModal({ onSubmit({ variationId, optionId }) } - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/CreateStoreItemsModal.tsx b/apps/business/src/components/CreateStoreItemsModal.tsx index 465306d..9db8613 100644 --- a/apps/business/src/components/CreateStoreItemsModal.tsx +++ b/apps/business/src/components/CreateStoreItemsModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react' +import { createPortal } from 'react-dom' import { Plus, X } from 'lucide-react' import { ApiError } from '../lib/api' import { listAllProducts } from '../services/productService' @@ -241,7 +242,7 @@ export function CreateStoreItemsModal({ } } - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/EditCustomerModal.tsx b/apps/business/src/components/EditCustomerModal.tsx index c3483f4..2589ba6 100644 --- a/apps/business/src/components/EditCustomerModal.tsx +++ b/apps/business/src/components/EditCustomerModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { ApiError } from '../lib/api' import { formatCellForDisplay, toE164CellNumber } from '../lib/cellNumber' @@ -96,7 +97,7 @@ export function EditCustomerModal({ } } - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/EditStoreItemsModal.tsx b/apps/business/src/components/EditStoreItemsModal.tsx index 16f3a9f..b35aec5 100644 --- a/apps/business/src/components/EditStoreItemsModal.tsx +++ b/apps/business/src/components/EditStoreItemsModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react' +import { createPortal } from 'react-dom' import { Plus, X } from 'lucide-react' import { ApiError } from '../lib/api' import { @@ -209,7 +210,7 @@ export function EditStoreItemsModal({ } } - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/ImageLightbox.tsx b/apps/business/src/components/ImageLightbox.tsx index 7238882..a8df1d6 100644 --- a/apps/business/src/components/ImageLightbox.tsx +++ b/apps/business/src/components/ImageLightbox.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { ChevronLeft, ChevronRight, X } from 'lucide-react' import styles from './ImageLightbox.module.css' @@ -59,7 +60,7 @@ export function ImageLightbox({ const hasPrev = index > 0 const hasNext = index < images.length - 1 - return ( + return createPortal(
)}
+ , + document.body, ) } diff --git a/apps/business/src/components/OrderItemsModal.tsx b/apps/business/src/components/OrderItemsModal.tsx index 3be1d72..3a62474 100644 --- a/apps/business/src/components/OrderItemsModal.tsx +++ b/apps/business/src/components/OrderItemsModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import type { Order } from '../services/orderService' import { formatCellForDisplay } from '../lib/cellNumber' @@ -59,7 +60,7 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps) const itemCount = totalQuantity(order) - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/OrderStepModal.tsx b/apps/business/src/components/OrderStepModal.tsx index ce3d015..0bbe855 100644 --- a/apps/business/src/components/OrderStepModal.tsx +++ b/apps/business/src/components/OrderStepModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { useToast } from '../context/ToastContext' import { ApiError } from '../lib/api' @@ -86,7 +87,7 @@ export function OrderStepModal({ steps.find((step) => step.id === order.processStepId)?.label || order.processStepId - return ( + return createPortal(
{ @@ -161,5 +162,7 @@ export function OrderStepModal({
+ , + document.body, ) } diff --git a/apps/business/src/components/OrderTransactionsModal.tsx b/apps/business/src/components/OrderTransactionsModal.tsx index 3a0fd15..ecd2e5d 100644 --- a/apps/business/src/components/OrderTransactionsModal.tsx +++ b/apps/business/src/components/OrderTransactionsModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import type { Order, OrderTransaction, TransactionStatus, TransactionType } from '../services/orderService' import { formatIrtPrice } from '../utils/irtPrice' @@ -116,7 +117,7 @@ export function OrderTransactionsModal({ open, order, onClose }: OrderTransactio if (!mounted || !order) return null - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/PageContent.module.css b/apps/business/src/components/PageContent.module.css index 1e89473..eade76d 100644 --- a/apps/business/src/components/PageContent.module.css +++ b/apps/business/src/components/PageContent.module.css @@ -52,6 +52,12 @@ gap: 24px; } +.gridCols4 { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; +} + .gridFour { display: grid; grid-template-columns: repeat(2, 1fr); @@ -76,6 +82,12 @@ } } +@media (max-width: 1400px) { + .gridCols4 { + grid-template-columns: repeat(3, 1fr); + } +} + @media (max-width: 1200px) { .gridHome { grid-template-columns: repeat(3, 1fr); @@ -84,7 +96,8 @@ @media (max-width: 900px) { .gridHome, - .grid { + .grid, + .gridCols4 { grid-template-columns: repeat(2, 1fr); } } @@ -109,7 +122,8 @@ .grid, .gridHome, - .gridFour { + .gridFour, + .gridCols4 { grid-template-columns: 1fr; gap: 16px; } diff --git a/apps/business/src/components/Pagination.module.css b/apps/business/src/components/Pagination.module.css deleted file mode 100644 index f9d5ee0..0000000 --- a/apps/business/src/components/Pagination.module.css +++ /dev/null @@ -1,61 +0,0 @@ -.pagination { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - margin-top: 32px; - padding-top: 24px; -} - -.navBtn { - width: 38px; - height: 38px; - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-sm); - color: var(--text-secondary); - background: var(--glass-bg); - border: 1px solid var(--glass-border); - transition: background 0.2s, color 0.2s, opacity 0.2s; -} - -.navBtn:hover:not(:disabled) { - background: rgba(var(--primary-rgb) / 0.1); - color: var(--primary); -} - -.navBtn:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.pages { - display: flex; - align-items: center; - gap: 4px; -} - -.pageBtn { - min-width: 38px; - height: 38px; - padding: 0 10px; - font-size: 14px; - font-weight: 500; - color: var(--text-secondary); - background: var(--glass-bg); - border: 1px solid var(--glass-border); - border-radius: var(--radius-sm); - transition: background 0.2s, color 0.2s, border-color 0.2s; -} - -.pageBtn:hover { - background: rgba(var(--primary-rgb) / 0.08); - color: var(--primary); -} - -.pageBtn.active { - background: var(--primary); - border-color: var(--primary); - color: white; -} diff --git a/apps/business/src/components/Pagination.tsx b/apps/business/src/components/Pagination.tsx index 551a570..704bd65 100644 --- a/apps/business/src/components/Pagination.tsx +++ b/apps/business/src/components/Pagination.tsx @@ -1,53 +1 @@ -import { ChevronLeft, ChevronRight } from 'lucide-react' -import styles from './Pagination.module.css' - -interface PaginationProps { - currentPage: number - totalPages: number - onPageChange: (page: number) => void -} - -export function Pagination({ currentPage, totalPages, onPageChange }: PaginationProps) { - if (totalPages <= 1) return null - - const pages = Array.from({ length: totalPages }, (_, i) => i + 1) - - return ( - - ) -} +export { Pagination, type PaginationProps } from '@meshkee/dashboard-ui' diff --git a/apps/business/src/components/PickStoreSpecialItemsModal.tsx b/apps/business/src/components/PickStoreSpecialItemsModal.tsx index 756f53a..b694752 100644 --- a/apps/business/src/components/PickStoreSpecialItemsModal.tsx +++ b/apps/business/src/components/PickStoreSpecialItemsModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { ApiError } from '../lib/api' import { listAllStoreItems } from '../services/storeItemService' @@ -118,7 +119,7 @@ export function PickStoreSpecialItemsModal({ if (!mounted) return null - return ( + return createPortal(
e.target === e.currentTarget && onClose()} @@ -175,5 +176,7 @@ export function PickStoreSpecialItemsModal({
+ , + document.body, ) } diff --git a/apps/business/src/components/PickStoreVariantModal.tsx b/apps/business/src/components/PickStoreVariantModal.tsx index 6b706be..412c2d7 100644 --- a/apps/business/src/components/PickStoreVariantModal.tsx +++ b/apps/business/src/components/PickStoreVariantModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import type { StoreItem } from '../services/storeItemService' import { StoreItemPrice } from './StoreItemPrice' @@ -52,7 +53,7 @@ export function PickStoreVariantModal({ if (!mounted) return null - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/PickWebsiteBrandsModal.tsx b/apps/business/src/components/PickWebsiteBrandsModal.tsx index 7c846bf..e59f018 100644 --- a/apps/business/src/components/PickWebsiteBrandsModal.tsx +++ b/apps/business/src/components/PickWebsiteBrandsModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { ApiError } from '../lib/api' import { listAllBrands } from '../services/brandService' @@ -111,7 +112,7 @@ export function PickWebsiteBrandsModal({ if (!mounted) return null - return ( + return createPortal(
e.target === e.currentTarget && onClose()} @@ -170,5 +171,7 @@ export function PickWebsiteBrandsModal({
+ , + document.body, ) } diff --git a/apps/business/src/components/PickWebsiteCategoriesModal.tsx b/apps/business/src/components/PickWebsiteCategoriesModal.tsx index 40677a0..12ca1be 100644 --- a/apps/business/src/components/PickWebsiteCategoriesModal.tsx +++ b/apps/business/src/components/PickWebsiteCategoriesModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { ApiError } from '../lib/api' import { listProductCategories, mapProductCategoryToUi } from '../services/productCategoryService' @@ -112,7 +113,7 @@ export function PickWebsiteCategoriesModal({ if (!mounted) return null - return ( + return createPortal(
e.target === e.currentTarget && onClose()} @@ -171,5 +172,7 @@ export function PickWebsiteCategoriesModal({
+ , + document.body, ) } diff --git a/apps/business/src/components/PortfolioCard.module.css b/apps/business/src/components/PortfolioCard.module.css index 9a44ea6..82e5732 100644 --- a/apps/business/src/components/PortfolioCard.module.css +++ b/apps/business/src/components/PortfolioCard.module.css @@ -25,31 +25,28 @@ cursor: pointer; } -.clickable:hover .title { +.clickable:hover .titleFa, +.clickable:hover .titleEn { color: var(--primary); } .imageWrap { position: relative; width: 100%; - aspect-ratio: 3 / 2; overflow: hidden; - background: transparent; + background: rgba(148, 163, 184, 0.08); border-bottom: 1px solid rgba(148, 163, 184, 0.12); } .image { - position: relative; - z-index: 1; - width: 100%; - height: 100%; - object-fit: contain; display: block; + width: 100%; + height: auto; } .imagePlaceholder { width: 100%; - height: 100%; + aspect-ratio: 3 / 2; background: linear-gradient( 135deg, rgba(148, 163, 184, 0.12) 0%, @@ -65,11 +62,15 @@ flex: 1; } -.title { +.titleFa { + margin: 0; + font-family: var(--font-fa), var(--font-en); font-size: 15px; - font-weight: 600; + font-weight: 500; line-height: 1.4; color: var(--text-primary); + direction: rtl; + text-align: right; transition: color 0.2s; display: -webkit-box; -webkit-line-clamp: 2; @@ -77,6 +78,20 @@ overflow: hidden; } +.titleEn { + margin: 0; + font-size: 13px; + font-weight: 500; + line-height: 1.35; + color: var(--text-secondary); + text-align: left; + transition: color 0.2s; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; +} + .abstract { margin: 0; font-size: 13px; @@ -88,6 +103,14 @@ overflow: hidden; } +.abstract[dir='rtl'] { + text-align: right; +} + +.abstract[dir='ltr'] { + text-align: left; +} + .meta { margin: 0; font-size: 12px; diff --git a/apps/business/src/components/PortfolioCard.tsx b/apps/business/src/components/PortfolioCard.tsx index c90fa01..eb6fbf2 100644 --- a/apps/business/src/components/PortfolioCard.tsx +++ b/apps/business/src/components/PortfolioCard.tsx @@ -2,6 +2,7 @@ import { Link } from 'react-router-dom' import { Pencil, MessageSquare, Trash2 } from 'lucide-react' import type { Portfolio } from '../types/portfolio' import { formatPortfolioCardDate } from '../services/portfolioService' +import { textLocaleAttrs } from '../utils/textLocale' import { Tooltip } from './Tooltip' import cardStyles from './PortfolioCard.module.css' import controlStyles from './ProductCard.module.css' @@ -22,6 +23,12 @@ export function PortfolioCard({ onRemove, }: PortfolioCardProps) { const publishDate = formatPortfolioCardDate(portfolio.publishedAt ?? portfolio.createdAt) + const titleFa = (portfolio.titleFa || portfolio.title || '').trim() + const titleEn = (portfolio.titleEn || '').trim() + const showBothTitles = Boolean(titleFa && titleEn) + const showAbstract = !showBothTitles + const abstractLocale = textLocaleAttrs(portfolio.abstract) + const hasAbstract = Boolean(portfolio.abstract?.trim()) return (
@@ -30,7 +37,7 @@ export function PortfolioCard({ {portfolio.titleImageUrl ? ( {portfolio.title} @@ -40,12 +47,33 @@ export function PortfolioCard({
-

{portfolio.title}

- {portfolio.abstract ? ( -

{portfolio.abstract}

- ) : ( -

No summary yet.

- )} + {titleFa ? ( +

+ {titleFa} +

+ ) : null} + {titleEn ? ( +

+ {titleEn} +

+ ) : null} + {showAbstract ? ( + hasAbstract ? ( +

+ {portfolio.abstract} +

+ ) : ( +

+ No summary yet. +

+ ) + ) : null}

{portfolio.categoryName ? ( <> diff --git a/apps/business/src/components/PortfolioCommentsModal.tsx b/apps/business/src/components/PortfolioCommentsModal.tsx index fc428ab..d088926 100644 --- a/apps/business/src/components/PortfolioCommentsModal.tsx +++ b/apps/business/src/components/PortfolioCommentsModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { X, ThumbsUp, Check, XCircle, Trash2 } from 'lucide-react' import type { ProductComment } from '../types/comment' import { formatCommentDate } from '../data/productComments' @@ -147,7 +148,7 @@ export function PortfolioCommentsModal({ } } - return ( + return createPortal(

+ , + document.body, ) } diff --git a/apps/business/src/components/ProductCard.module.css b/apps/business/src/components/ProductCard.module.css index 1a5089c..9fa6f2f 100644 --- a/apps/business/src/components/ProductCard.module.css +++ b/apps/business/src/components/ProductCard.module.css @@ -35,6 +35,7 @@ overflow: hidden; background: #ffffff; border-bottom: 1px solid rgba(148, 163, 184, 0.12); + border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0; } .image { @@ -80,6 +81,7 @@ .nameFa { font-family: var(--font-fa), var(--font-en); font-size: 12px; + font-weight: 500; color: var(--text-secondary); direction: rtl; text-align: left; diff --git a/apps/business/src/components/ProductCommentsModal.tsx b/apps/business/src/components/ProductCommentsModal.tsx index 931d8c5..59ff09f 100644 --- a/apps/business/src/components/ProductCommentsModal.tsx +++ b/apps/business/src/components/ProductCommentsModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { X, ThumbsUp, Check, XCircle, Trash2 } from 'lucide-react' import type { ProductComment } from '../types/comment' import { formatCommentDate } from '../data/productComments' @@ -147,7 +148,7 @@ export function ProductCommentsModal({ } } - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/ProductTechnicalInfoModal.tsx b/apps/business/src/components/ProductTechnicalInfoModal.tsx index 5f4deb2..fe1acb8 100644 --- a/apps/business/src/components/ProductTechnicalInfoModal.tsx +++ b/apps/business/src/components/ProductTechnicalInfoModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { ApiError } from '../lib/api' import { @@ -210,7 +211,7 @@ export function ProductTechnicalInfoModal({ ) } - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/ProductVariantsModal.tsx b/apps/business/src/components/ProductVariantsModal.tsx index 0dc6791..e032acb 100644 --- a/apps/business/src/components/ProductVariantsModal.tsx +++ b/apps/business/src/components/ProductVariantsModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { ApiError } from '../lib/api' import { @@ -119,7 +120,7 @@ export function ProductVariantsModal({ } } - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/SearchableSelect.module.css b/apps/business/src/components/SearchableSelect.module.css index 970e686..1c0c1db 100644 --- a/apps/business/src/components/SearchableSelect.module.css +++ b/apps/business/src/components/SearchableSelect.module.css @@ -131,6 +131,7 @@ .optionFa { font-family: var(--font-fa), var(--font-en); + font-weight: 500; direction: rtl; } diff --git a/apps/business/src/components/ShoppingCartModal.tsx b/apps/business/src/components/ShoppingCartModal.tsx index 7a435d8..02e3e0a 100644 --- a/apps/business/src/components/ShoppingCartModal.tsx +++ b/apps/business/src/components/ShoppingCartModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react' +import { createPortal } from 'react-dom' import { ArrowLeft, Minus, Plus, Search, Trash2, User, X } from 'lucide-react' import { useDraftCart } from '../context/DraftCartContext' import { useToast } from '../context/ToastContext' @@ -341,7 +342,7 @@ export function ShoppingCartModal({ open, onClose, onOrderCreated }: ShoppingCar const canAddPayment = payments.length < PAYMENT_METHOD_OPTIONS.length - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/StoreItemCard.module.css b/apps/business/src/components/StoreItemCard.module.css index 48a8763..ebe8db2 100644 --- a/apps/business/src/components/StoreItemCard.module.css +++ b/apps/business/src/components/StoreItemCard.module.css @@ -103,6 +103,7 @@ .nameFa { font-family: var(--font-fa), var(--font-en); font-size: 12px; + font-weight: 500; color: var(--text-secondary); direction: rtl; text-align: left; diff --git a/apps/business/src/components/StoreItemDiscountModal.tsx b/apps/business/src/components/StoreItemDiscountModal.tsx index f60fc35..7a3e1d7 100644 --- a/apps/business/src/components/StoreItemDiscountModal.tsx +++ b/apps/business/src/components/StoreItemDiscountModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { ApiError } from '../lib/api' import { @@ -119,7 +120,7 @@ export function StoreItemDiscountModal({ } } - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/StoreItemFestivalModal.tsx b/apps/business/src/components/StoreItemFestivalModal.tsx index b094686..81b4267 100644 --- a/apps/business/src/components/StoreItemFestivalModal.tsx +++ b/apps/business/src/components/StoreItemFestivalModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { ApiError } from '../lib/api' import { @@ -114,7 +115,7 @@ export function StoreItemFestivalModal({ } } - return ( + return createPortal(
+ , + document.body, ) } diff --git a/apps/business/src/components/StoreSpecialModal.tsx b/apps/business/src/components/StoreSpecialModal.tsx index bcd999d..d8df61b 100644 --- a/apps/business/src/components/StoreSpecialModal.tsx +++ b/apps/business/src/components/StoreSpecialModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react' +import { createPortal } from 'react-dom' import { X } from 'lucide-react' import styles from './CategoryModal.module.css' @@ -63,13 +64,18 @@ export function StoreSpecialModal({ onSubmit(trimmed) } - return ( + return createPortal(
e.target === e.currentTarget && onClose()} role="presentation" > -
+

{title} @@ -107,5 +113,7 @@ export function StoreSpecialModal({

+ , + document.body, ) } diff --git a/apps/business/src/components/TechnicalFormAiPromptModal.tsx b/apps/business/src/components/TechnicalFormAiPromptModal.tsx new file mode 100644 index 0000000..998ee99 --- /dev/null +++ b/apps/business/src/components/TechnicalFormAiPromptModal.tsx @@ -0,0 +1,160 @@ +import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' +import { Sparkles, X } from 'lucide-react' +import modalStyles from './CategoryModal.module.css' +import aiStyles from '../styles/ai.module.css' +import styles from './CategoryAiPromptModal.module.css' + +export function buildTechnicalFormAiPrompt(categoryName: string) { + return `Suggest technical data form fields for the "${categoryName}" product category. + +For each field include: +- A short English label (e.g. "Screen Size", "Material", "Color") +- The input type: text, textarea, select, or multi_select +- Whether it is required +- For select / multi_select: the list of options + +Focus on specs and attributes that shoppers typically compare when buying this type of product.` +} + +interface TechnicalFormAiPromptModalProps { + open: boolean + categoryName: string + onClose: () => void + onRun: (prompt: string) => Promise + isRunning: boolean +} + +const ANIMATION_MS = 220 + +export function TechnicalFormAiPromptModal({ + open, + categoryName, + onClose, + onRun, + isRunning, +}: TechnicalFormAiPromptModalProps) { + const [mounted, setMounted] = useState(open) + const [closing, setClosing] = useState(false) + const [prompt, setPrompt] = useState(() => buildTechnicalFormAiPrompt(categoryName)) + const [error, setError] = useState('') + + useEffect(() => { + if (open) { + setMounted(true) + setClosing(false) + setPrompt(buildTechnicalFormAiPrompt(categoryName)) + setError('') + } else if (mounted) { + setClosing(true) + const timer = setTimeout(() => { + setMounted(false) + setClosing(false) + }, ANIMATION_MS) + return () => clearTimeout(timer) + } + }, [open, mounted, categoryName]) + + useEffect(() => { + if (!mounted || closing) return + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape' && !isRunning) onClose() + } + document.addEventListener('keydown', onKey) + return () => document.removeEventListener('keydown', onKey) + }, [mounted, closing, isRunning, onClose]) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + const trimmed = prompt.trim() + if (trimmed.length < 10) { + setError('Prompt must be at least 10 characters.') + return + } + setError('') + try { + await onRun(trimmed) + } catch (err) { + if (err instanceof Error) { + setError(err.message) + } else { + setError('Unable to generate technical form with AI.') + } + } + } + + if (!mounted) return null + + return createPortal( +
e.target === e.currentTarget && !isRunning && onClose()} + role="presentation" + > +
+
+
+

+ Generate Technical Form with AI +

+

+ Edit the prompt below, then run it to fill the form fields. +

+
+ +
+ +
void handleSubmit(e)}> +
+ +