diff --git a/apps/business/index.html b/apps/business/index.html index b71258d..715411a 100644 --- a/apps/business/index.html +++ b/apps/business/index.html @@ -2,6 +2,7 @@ + diff --git a/apps/business/public/favicon.png b/apps/business/public/favicon.png new file mode 100755 index 0000000..f80989a Binary files /dev/null and b/apps/business/public/favicon.png differ diff --git a/apps/business/public/favicon.svg b/apps/business/public/favicon.svg deleted file mode 100644 index 6893eb1..0000000 --- a/apps/business/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/business/public/meshkee-logo.png b/apps/business/public/meshkee-logo.png old mode 100644 new mode 100755 index 9d12a5b..9b7eaf1 Binary files a/apps/business/public/meshkee-logo.png and b/apps/business/public/meshkee-logo.png differ diff --git a/apps/business/src/assets/meshkee-logo.png b/apps/business/src/assets/meshkee-logo.png old mode 100644 new mode 100755 index 9d12a5b..9b7eaf1 Binary files a/apps/business/src/assets/meshkee-logo.png and b/apps/business/src/assets/meshkee-logo.png differ diff --git a/apps/business/src/components/ImageCropper.module.css b/apps/business/src/components/ImageCropper.module.css index 9bdb967..acbda89 100644 --- a/apps/business/src/components/ImageCropper.module.css +++ b/apps/business/src/components/ImageCropper.module.css @@ -55,6 +55,12 @@ object-fit: cover; } +.previewImgNatural { + width: 100%; + height: auto; + display: block; +} + .removeBtn { position: absolute; top: 8px; @@ -85,9 +91,15 @@ position: relative; width: 100%; min-height: 160px; + max-height: 420px; background: #1e293b; } +.cropAreaPortrait { + max-height: none; + min-height: 0; +} + .cropControls { padding: 14px 16px; display: flex; @@ -141,6 +153,11 @@ background: var(--primary-dark); } +.applyBtn:disabled { + opacity: 0.55; + cursor: not-allowed; +} + .changeBtn { display: inline-flex; align-self: flex-start; diff --git a/apps/business/src/components/ImageCropper.tsx b/apps/business/src/components/ImageCropper.tsx index 940b999..1a6dbb2 100644 --- a/apps/business/src/components/ImageCropper.tsx +++ b/apps/business/src/components/ImageCropper.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import Cropper, { type Area } from 'react-easy-crop' import { ImagePlus, X } from 'lucide-react' import { getCroppedImage } from '../utils/cropImage' @@ -7,6 +7,7 @@ import styles from './ImageCropper.module.css' interface ImageCropperProps { value: string | null onChange: (value: string | null) => void + /** Crop aspect ratio. Defaults to square (1). */ aspect?: number outputFormat?: 'jpeg' | 'png' accept?: string @@ -30,6 +31,12 @@ export function ImageCropper({ const [zoom, setZoom] = useState(1) const [croppedArea, setCroppedArea] = useState(null) + useEffect(() => { + setCrop({ x: 0, y: 0 }) + setZoom(1) + setCroppedArea(null) + }, [aspect]) + const onCropComplete = useCallback((_: Area, pixels: Area) => { setCroppedArea(pixels) }, []) @@ -38,7 +45,12 @@ export function ImageCropper({ const file = e.target.files?.[0] if (!file) return const reader = new FileReader() - reader.onload = () => setImageSrc(reader.result as string) + reader.onload = () => { + setImageSrc(reader.result as string) + setCrop({ x: 0, y: 0 }) + setZoom(1) + setCroppedArea(null) + } reader.readAsDataURL(file) e.target.value = '' } @@ -62,13 +74,26 @@ export function ImageCropper({ onChange(null) } - const frameStyle = { aspectRatio: `${aspect}` as const } + const isPortrait = aspect < 1 + const frameStyle: React.CSSProperties = isPortrait + ? { + aspectRatio: `${aspect}`, + height: 'min(480px, 65vh)', + width: 'auto', + maxWidth: '100%', + marginInline: 'auto', + } + : { + aspectRatio: `${aspect}`, + width: '100%', + maxHeight: 'min(420px, 55vh)', + } return (
{value && !imageSrc && ( -
- Thumbnail preview +
+ Thumbnail preview @@ -86,8 +111,9 @@ export function ImageCropper({ {imageSrc && (
-
+
Cancel -
diff --git a/apps/business/src/components/PortfolioCard.module.css b/apps/business/src/components/PortfolioCard.module.css index 5d2fda5..9a44ea6 100644 --- a/apps/business/src/components/PortfolioCard.module.css +++ b/apps/business/src/components/PortfolioCard.module.css @@ -34,14 +34,17 @@ width: 100%; aspect-ratio: 3 / 2; overflow: hidden; - background: rgba(148, 163, 184, 0.08); + background: transparent; border-bottom: 1px solid rgba(148, 163, 184, 0.12); } .image { + position: relative; + z-index: 1; width: 100%; height: 100%; - object-fit: cover; + object-fit: contain; + display: block; } .imagePlaceholder { diff --git a/apps/business/src/context/TenantBrandingContext.tsx b/apps/business/src/context/TenantBrandingContext.tsx index 922b900..63e8d0e 100644 --- a/apps/business/src/context/TenantBrandingContext.tsx +++ b/apps/business/src/context/TenantBrandingContext.tsx @@ -16,6 +16,7 @@ import { resolveTenantByDomain } from '../services/tenantService' interface TenantBrandingContextValue { businessName: string + logoUrl: string | null faviconUrl: string | null refreshBranding: () => void } @@ -34,6 +35,7 @@ function pickBusinessName( export function TenantBrandingProvider({ children }: { children: ReactNode }) { const [businessName, setBusinessName] = useState('') + const [logoUrl, setLogoUrl] = useState(null) const [faviconUrl, setFaviconUrl] = useState(null) const [refreshToken, setRefreshToken] = useState(0) @@ -51,6 +53,7 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) { if (controller.signal.aborted) return let name = pickBusinessName(tenant.name, tenant.nameFa, domain) + let nextLogo = tenant.logoUrl?.trim() || null let nextFavicon = tenant.faviconUrl?.trim() || tenant.logoUrl?.trim() || null @@ -63,6 +66,7 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) { name, domain, ) + nextLogo = profile.profile.logoUrl?.trim() || nextLogo nextFavicon = profile.profile.faviconUrl?.trim() || profile.profile.logoUrl?.trim() || @@ -76,12 +80,14 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) { if (!controller.signal.aborted) { setBusinessName(name || domain) + setLogoUrl(nextLogo) setFaviconUrl(nextFavicon) applyDocumentFavicon(nextFavicon) } } catch (err) { if (isAbortError(err) || controller.signal.aborted) return setBusinessName(domain) + setLogoUrl(null) setFaviconUrl(null) applyDocumentFavicon(null) } @@ -101,8 +107,8 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) { }, [refreshToken, refreshBranding]) const value = useMemo( - () => ({ businessName, faviconUrl, refreshBranding }), - [businessName, faviconUrl, refreshBranding], + () => ({ businessName, logoUrl, faviconUrl, refreshBranding }), + [businessName, logoUrl, faviconUrl, refreshBranding], ) return ( diff --git a/apps/business/src/pages/AddNewPortfolioPage.tsx b/apps/business/src/pages/AddNewPortfolioPage.tsx index 2373a26..a598692 100644 --- a/apps/business/src/pages/AddNewPortfolioPage.tsx +++ b/apps/business/src/pages/AddNewPortfolioPage.tsx @@ -28,6 +28,19 @@ import { flattenCategories } from '../utils/categories' import pageStyles from '../components/PageContent.module.css' import styles from './AddNewProductPage.module.css' +type TitleImageAspectId = 'square' | '3:2' | '16:9' | '9:16' + +const TITLE_IMAGE_ASPECTS: { + id: TitleImageAspectId + label: string + aspect: number +}[] = [ + { id: 'square', label: 'Square', aspect: 1 }, + { id: '3:2', label: '3:2', aspect: 3 / 2 }, + { id: '16:9', label: '16:9', aspect: 16 / 9 }, + { id: '9:16', label: '9:16 (Reel)', aspect: 9 / 16 }, +] + export function AddNewPortfolioPage() { const { id } = useParams() const navigate = useNavigate() @@ -40,6 +53,7 @@ export function AddNewPortfolioPage() { const [abstract, setAbstract] = useState('') const [mainTextHtml, setMainTextHtml] = useState('') const [titleImage, setTitleImage] = useState(null) + const [titleImageAspect, setTitleImageAspect] = useState('3:2') const [images, setImages] = useState([]) const [tags, setTags] = useState([]) const [featuredMediaId, setFeaturedMediaId] = useState(null) @@ -49,6 +63,8 @@ export function AddNewPortfolioPage() { const [error, setError] = useState('') const categoryOptions = flattenCategories(categories) + const selectedAspect = + TITLE_IMAGE_ASPECTS.find((option) => option.id === titleImageAspect)?.aspect ?? 3 / 2 useEffect(() => { const controller = new AbortController() @@ -199,65 +215,96 @@ export function AddNewPortfolioPage() {
-
+
+
+ {TITLE_IMAGE_ASPECTS.map((option) => ( + + ))} +
-
- - -
+
+
+ + +
-
- - setTitle(e.target.value)} - required - disabled={isSubmitting} - /> -
+
+ + setTitle(e.target.value)} + required + disabled={isSubmitting} + /> +
-
- -