@crestbase/web-shared-ui 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/core/Calendar.d.ts +8 -0
- package/dist/components/core/Calendar.js +397 -55
- package/dist/components/core/CalendarExtended.d.ts +10 -1
- package/dist/components/core/CalendarExtended.js +17 -10
- package/dist/components/core/CustomDropdown.js +2 -2
- package/dist/components/core/OptionsDropdown.d.ts +19 -1
- package/dist/components/core/OptionsDropdown.js +10 -4
- package/dist/components/core/PromptModal.d.ts +13 -0
- package/dist/components/core/PromptModal.js +20 -0
- package/dist/components/core/ShareModal.d.ts +8 -0
- package/dist/components/core/ShareModal.js +22 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.js +8 -0
- package/dist/components/property-details/MediaItem.d.ts +13 -0
- package/dist/components/property-details/MediaItem.js +12 -0
- package/dist/components/property-details/MediaPreviewModal.d.ts +13 -0
- package/dist/components/property-details/MediaPreviewModal.js +24 -0
- package/dist/components/property-details/PropertyImageGallery.d.ts +7 -0
- package/dist/components/property-details/PropertyImageGallery.js +77 -0
- package/dist/components/property-details/mediaUtils.d.ts +11 -0
- package/dist/components/property-details/mediaUtils.js +64 -0
- package/dist/components/svg/ChevronIcon.d.ts +7 -0
- package/dist/components/svg/ChevronIcon.js +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/styles.css +1 -1
- package/dist/types/assets.d.ts +6 -0
- package/dist/types/assets.js +2 -0
- package/package.json +1 -1
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
import React, { ReactNode } from "react";
|
|
2
|
+
export type DropdownRenderHelpers = {
|
|
3
|
+
/** Close the dropdown programmatically */
|
|
4
|
+
close: () => void;
|
|
5
|
+
};
|
|
2
6
|
export interface OptionItem {
|
|
3
7
|
id: string;
|
|
4
8
|
label: string;
|
|
5
9
|
onClick: () => void;
|
|
6
10
|
disabled?: boolean;
|
|
11
|
+
/** Optional icon node shown before the label */
|
|
12
|
+
icon?: ReactNode;
|
|
7
13
|
}
|
|
8
14
|
export interface OptionsDropdownProps {
|
|
9
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Array of options to render as the dropdown menu. When `dropdownContent` is provided,
|
|
17
|
+
* this array is optional and ignored. Kept for backward compatibility.
|
|
18
|
+
*/
|
|
19
|
+
options?: OptionItem[];
|
|
10
20
|
trigger?: ReactNode;
|
|
11
21
|
listClassName?: string;
|
|
12
22
|
optionClassName?: string;
|
|
13
23
|
containerClassName?: string;
|
|
14
24
|
disabled?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Custom content to render inside the dropdown. Accepts either a React node
|
|
27
|
+
* or a render function. When a function is provided, it will receive a
|
|
28
|
+
* helpers object with a `close()` method that can be used to close the
|
|
29
|
+
* dropdown programmatically (e.g., from an internal button).
|
|
30
|
+
* Backward compatible with previous ReactNode usage.
|
|
31
|
+
*/
|
|
32
|
+
dropdownContent?: ReactNode | ((helpers: DropdownRenderHelpers) => ReactNode);
|
|
15
33
|
}
|
|
16
34
|
declare const _default: (props: OptionsDropdownProps) => React.JSX.Element;
|
|
17
35
|
export default _default;
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { MoreVertical } from "lucide-react";
|
|
4
4
|
import { useEffect, useRef, useState } from "react";
|
|
5
|
-
const OptionsDropdown = ({ options, trigger, listClassName = "", optionClassName = "", containerClassName = "", disabled = false, }) => {
|
|
5
|
+
const OptionsDropdown = ({ options = [], trigger, listClassName = "", optionClassName = "", containerClassName = "", disabled = false, dropdownContent, }) => {
|
|
6
6
|
const [isOpen, setIsOpen] = useState(false);
|
|
7
7
|
const dropdownRef = useRef(null);
|
|
8
8
|
// Close dropdown when clicking outside
|
|
9
9
|
useEffect(() => {
|
|
10
10
|
const handleClickOutside = (event) => {
|
|
11
|
-
if (dropdownRef.current &&
|
|
11
|
+
if (dropdownRef.current &&
|
|
12
|
+
!dropdownRef.current.contains(event.target)) {
|
|
12
13
|
setIsOpen(false);
|
|
13
14
|
}
|
|
14
15
|
};
|
|
@@ -35,10 +36,15 @@ const OptionsDropdown = ({ options, trigger, listClassName = "", optionClassName
|
|
|
35
36
|
e.preventDefault();
|
|
36
37
|
e.stopPropagation();
|
|
37
38
|
handleToggle();
|
|
38
|
-
}, className: "cursor-pointer", children: trigger || defaultTrigger }), isOpen && (_jsx("div", { className: `${listClassName} absolute right-0 top-full mt-2 bg-white border border-gray-100 rounded-xl divide-y divide-gray-100 shadow-lg z-50 min-w-48 overflow-hidden`, children:
|
|
39
|
+
}, className: "cursor-pointer", children: trigger || defaultTrigger }), isOpen && (_jsx("div", { className: `${listClassName} absolute right-0 top-full mt-2 bg-white border border-gray-100 rounded-xl divide-y divide-gray-100 shadow-lg z-50 min-w-48 overflow-hidden flex flex-col`, children: dropdownContent ? (_jsx("div", { onClick: (e) => {
|
|
40
|
+
// allow interactive content without closing automatically
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
}, children: typeof dropdownContent === "function"
|
|
43
|
+
? dropdownContent({ close: () => setIsOpen(false) })
|
|
44
|
+
: dropdownContent })) : (options.map((option) => (_jsxs("button", { onClick: (e) => {
|
|
39
45
|
e.preventDefault();
|
|
40
46
|
e.stopPropagation();
|
|
41
47
|
handleOptionClick(option);
|
|
42
|
-
}, disabled: option.disabled, className: `w-full text-left px-3 py-2.5 cursor-pointer text-xs text-gray-500 hover:text-black hover:bg-gray-100 transition-colors duration-200 ${optionClassName}`, children: option.label }, option.id))) }))] }));
|
|
48
|
+
}, disabled: option.disabled, className: `w-full text-left px-3 py-2.5 cursor-pointer text-xs text-gray-500 hover:text-black hover:bg-gray-100 transition-colors duration-200 flex items-center gap-3 ${optionClassName}`, children: [option.icon ? (_jsx("span", { className: "inline-flex items-center", children: option.icon })) : null, _jsx("span", { children: option.label })] }, option.id)))) }))] }));
|
|
43
49
|
};
|
|
44
50
|
export default OptionsDropdown;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface PromptModalProps {
|
|
3
|
+
isOpen: boolean;
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
bgClass?: string;
|
|
6
|
+
onSubmit: () => Promise<void>;
|
|
7
|
+
submitText?: string;
|
|
8
|
+
cancelText?: string;
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
isLoading?: boolean;
|
|
11
|
+
}
|
|
12
|
+
declare const PromptModal: React.FC<PromptModalProps>;
|
|
13
|
+
export default PromptModal;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import Modal from "../core/Modal";
|
|
4
|
+
const PromptModal = ({ isOpen, onClose, bgClass = "bg-secondaryblue", onSubmit, submitText = "Submit", cancelText = "Cancel", children, isLoading = false, }) => {
|
|
5
|
+
const handleSubmit = async () => {
|
|
6
|
+
if (isLoading)
|
|
7
|
+
return;
|
|
8
|
+
await onSubmit();
|
|
9
|
+
if (!isLoading) {
|
|
10
|
+
onClose();
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const handleClose = () => {
|
|
14
|
+
if (isLoading)
|
|
15
|
+
return;
|
|
16
|
+
onClose();
|
|
17
|
+
};
|
|
18
|
+
return (_jsx(Modal, { isOpen: isOpen, onClose: handleClose, children: _jsxs("div", { className: "w-full max-w-[400px] bg-white rounded-lg shadow-xl overflow-hidden", children: [_jsx("div", { className: "flex items-center justify-end border-b border-gray-200 p-4", children: _jsx("button", { onClick: handleClose, disabled: isLoading, className: `p-2 rounded-full bg-gray-200 hover:bg-gray-100 transition-colors ${isLoading ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`, children: _jsxs("svg", { viewBox: "0 0 10 10", className: "text-gray-500 w-3 h-3", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [_jsx("path", { d: "M1.23438 1.23633L9.38847 9.39042", stroke: "currentColor", strokeWidth: "1.2" }), _jsx("path", { d: "M9.39062 1.23633L1.23653 9.39042", stroke: "currentColor", strokeWidth: "1.2" })] }) }) }), _jsx("div", { className: `text-center text-black flex flex-col items-center p-8 pb-4 px-20`, children: children }), _jsxs("div", { className: "flex justify-center text-xs gap-3 p-5 px-8 border-t border-gray-200 bg-gray-50", children: [_jsx("button", { onClick: handleClose, disabled: isLoading, className: `px-8 py-3 text-gray-500 border-gray-500 bg-transparent border hover:text-black rounded-md transition-colors ${isLoading ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`, children: cancelText }), _jsx("button", { onClick: handleSubmit, disabled: isLoading, className: `px-8 py-3 ${bgClass} text-white rounded-md hover:opacity-80 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors ${isLoading ? "cursor-not-allowed" : "cursor-pointer"}`, children: isLoading ? "Loading..." : submitText })] })] }) }));
|
|
19
|
+
};
|
|
20
|
+
export default PromptModal;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import Modal from "../core/Modal";
|
|
5
|
+
import Button from "../button";
|
|
6
|
+
const ShareModal = ({ isOpen, onClose, shareUrl, }) => {
|
|
7
|
+
const [copied, setCopied] = useState(false);
|
|
8
|
+
if (!isOpen)
|
|
9
|
+
return null;
|
|
10
|
+
const handleCopyLink = async () => {
|
|
11
|
+
try {
|
|
12
|
+
await navigator.clipboard.writeText(shareUrl);
|
|
13
|
+
setCopied(true);
|
|
14
|
+
setTimeout(() => setCopied(false), 2000);
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
console.error("Failed to copy link:", err);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, children: _jsxs("div", { className: "relative bg-white rounded-4xl shadow-xl max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto", children: [_jsxs("div", { className: "flex items-center p-8 pb-0 gap-4", children: [_jsx("button", { onClick: onClose, className: "p-2 rounded-full bg-gray-200 hover:bg-gray-100 transition-colors cursor-pointer", children: _jsxs("svg", { width: "9", height: "9", viewBox: "0 0 9 9", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [_jsx("path", { d: "M0.425781 0.423828L8.57987 8.57792", stroke: "#262C55", strokeWidth: "1.2" }), _jsx("path", { d: "M8.58203 0.423828L0.427939 8.57792", stroke: "#262C55", strokeWidth: "1.2" })] }) }), _jsx("h2", { className: "text-sm font-semibold text-secondaryblue", children: "Share" })] }), _jsxs("div", { className: "p-8 pt-6", children: [_jsx("div", { className: "mb-6 w-full text-sm py-3 border-b border-gray-300 text-gray-700 overflow-x-auto whitespace-nowrap", children: shareUrl }), _jsx(Button, { onClick: handleCopyLink, className: "w-full", children: _jsx("div", { className: "flex items-center justify-center gap-2", children: copied ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "\u2714" }), "Link copied!"] })) : (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "\uD83D\uDCCB" }), "Copy link"] })) }) })] })] }) }));
|
|
21
|
+
};
|
|
22
|
+
export default ShareModal;
|
|
@@ -21,10 +21,17 @@ export { default as CustomDropdown } from "./core/CustomDropdown";
|
|
|
21
21
|
export { default as Logo } from "./navigation/Logo";
|
|
22
22
|
export { default as Crestbase } from "./svg/Crestbase";
|
|
23
23
|
export { default as CrestbaseColored } from "./svg/CrestbaseColored";
|
|
24
|
+
export { default as ChevronIcon } from "./svg/ChevronIcon";
|
|
24
25
|
export { default as GlobalNotificationProvider } from "./notification/GlobalNotificationProvider";
|
|
25
26
|
export { useNotificationStore } from "./notification/notificationStore";
|
|
26
27
|
export { default as Upload } from "./core/Upload";
|
|
27
28
|
export type { UploadProps, UploadRef } from "./core/Upload";
|
|
29
|
+
export { default as PropertyImageGallery } from "./property-details/PropertyImageGallery";
|
|
30
|
+
export { default as MediaPreviewModal } from "./property-details/MediaPreviewModal";
|
|
31
|
+
export { default as MediaItem } from "./property-details/MediaItem";
|
|
32
|
+
export { default as ShareModal } from "./core/ShareModal";
|
|
33
|
+
export { default as PromptModal } from "./core/PromptModal";
|
|
34
|
+
export { isVideo, organizeViews, getDisplayViews, getLayoutConfig, fallbackImage, } from "./property-details/mediaUtils";
|
|
28
35
|
export type { AccordionProps } from "./core/Accordion";
|
|
29
36
|
export type { ToggleProps } from "./core/Toggle";
|
|
30
37
|
export type { CheckboxProps } from "./core/Checkbox";
|
package/dist/components/index.js
CHANGED
|
@@ -22,6 +22,14 @@ export { default as CustomDropdown } from "./core/CustomDropdown";
|
|
|
22
22
|
export { default as Logo } from "./navigation/Logo";
|
|
23
23
|
export { default as Crestbase } from "./svg/Crestbase";
|
|
24
24
|
export { default as CrestbaseColored } from "./svg/CrestbaseColored";
|
|
25
|
+
export { default as ChevronIcon } from "./svg/ChevronIcon";
|
|
25
26
|
export { default as GlobalNotificationProvider } from "./notification/GlobalNotificationProvider";
|
|
26
27
|
export { useNotificationStore } from "./notification/notificationStore";
|
|
27
28
|
export { default as Upload } from "./core/Upload";
|
|
29
|
+
// Property details
|
|
30
|
+
export { default as PropertyImageGallery } from "./property-details/PropertyImageGallery";
|
|
31
|
+
export { default as MediaPreviewModal } from "./property-details/MediaPreviewModal";
|
|
32
|
+
export { default as MediaItem } from "./property-details/MediaItem";
|
|
33
|
+
export { default as ShareModal } from "./core/ShareModal";
|
|
34
|
+
export { default as PromptModal } from "./core/PromptModal";
|
|
35
|
+
export { isVideo, organizeViews, getDisplayViews, getLayoutConfig, fallbackImage, } from "./property-details/mediaUtils";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { View } from "../../types/assets";
|
|
2
|
+
interface MediaItemProps {
|
|
3
|
+
view: View;
|
|
4
|
+
alt: string;
|
|
5
|
+
className: string;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
priority?: boolean;
|
|
9
|
+
onClick: () => void;
|
|
10
|
+
showVideoLabel?: boolean;
|
|
11
|
+
}
|
|
12
|
+
declare const MediaItem: ({ view, alt, className, width, height, priority, onClick, showVideoLabel, }: MediaItemProps) => import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export default MediaItem;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
import { fallbackImage, isVideo } from "./mediaUtils";
|
|
5
|
+
const MediaItem = ({ view, alt, className, width, height, priority = false, onClick, showVideoLabel = true, }) => {
|
|
6
|
+
const isVideoFile = isVideo(view);
|
|
7
|
+
if (isVideoFile) {
|
|
8
|
+
return (_jsxs("div", { className: `relative ${className} cursor-pointer group`, onClick: onClick, children: [_jsx("video", { src: view.url, className: "w-full h-full object-cover", muted: true, preload: "metadata", poster: view.url + "#t=0.1" }), _jsx("div", { className: "absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center group-hover:bg-opacity-40 transition-all", children: _jsx("div", { className: "w-12 h-12 bg-white bg-opacity-95 rounded-full flex items-center justify-center shadow-lg group-hover:bg-opacity-100 group-hover:scale-105 transition-all duration-200", children: _jsx("svg", { className: "w-6 h-6 text-gray-800 ml-0.5", fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M8 5v14l11-7z" }) }) }) }), showVideoLabel && (_jsx("div", { className: "absolute top-2 left-2 bg-black bg-opacity-80 text-white text-xs px-2 py-1 rounded font-medium", children: "Video" }))] }));
|
|
9
|
+
}
|
|
10
|
+
return (_jsx("div", { className: `${className} cursor-pointer`, onClick: onClick, children: _jsx(Image, { src: view.url || fallbackImage, alt: alt, priority: priority, width: width, height: height, className: "w-full h-full object-cover hover:opacity-95 transition-opacity" }) }));
|
|
11
|
+
};
|
|
12
|
+
export default MediaItem;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { View } from "../../types/assets";
|
|
2
|
+
interface MediaPreviewModalProps {
|
|
3
|
+
isOpen: boolean;
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
views: View[];
|
|
6
|
+
selectedIndex: number;
|
|
7
|
+
propertyName: string;
|
|
8
|
+
isVideoPlaying: boolean;
|
|
9
|
+
onVideoPlayingChange: (playing: boolean) => void;
|
|
10
|
+
onNavigate: (direction: "prev" | "next") => void;
|
|
11
|
+
}
|
|
12
|
+
declare const MediaPreviewModal: ({ isOpen, onClose, views, selectedIndex, propertyName, isVideoPlaying, onVideoPlayingChange, onNavigate, }: MediaPreviewModalProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
13
|
+
export default MediaPreviewModal;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
import Modal from "../core/Modal";
|
|
5
|
+
import { XIcon } from "lucide-react";
|
|
6
|
+
import ChevronIcon from "../svg/ChevronIcon";
|
|
7
|
+
const MediaPreviewModal = ({ isOpen, onClose, views, selectedIndex, propertyName, isVideoPlaying, onVideoPlayingChange, onNavigate, }) => {
|
|
8
|
+
const fallbackImage = "https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80";
|
|
9
|
+
const isVideo = (view) => {
|
|
10
|
+
return (view.name === "video" ||
|
|
11
|
+
view.url.toLowerCase().includes(".mp4") ||
|
|
12
|
+
view.url.toLowerCase().includes(".mov") ||
|
|
13
|
+
view.url.toLowerCase().includes(".avi") ||
|
|
14
|
+
view.url.toLowerCase().includes(".webm"));
|
|
15
|
+
};
|
|
16
|
+
if (!isOpen || selectedIndex < 0 || selectedIndex >= views.length) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const currentView = views[selectedIndex];
|
|
20
|
+
const isCurrentVideo = isVideo(currentView);
|
|
21
|
+
const totalCount = views.length;
|
|
22
|
+
return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, allowBackdropClose: true, containerClassName: "z-50 mx-auto w-full max-w-6xl flex justify-center items-center text-white px-4", className: "py-8", children: _jsxs("div", { className: "relative w-full bg-black rounded-4xl overflow-hidden", children: [_jsxs("div", { className: "absolute top-4 left-0 right-0 z-10 flex justify-between items-center px-6", children: [_jsxs("div", { className: "text-white text-sm font-medium", children: [selectedIndex + 1, " / ", totalCount] }), _jsx("button", { onClick: onClose, className: "p-2 rounded-full bg-gray-200 hover:bg-gray-100 transition-colors cursor-pointer", children: _jsx(XIcon, { className: "w-4 h-4 text-gray-500" }) })] }), totalCount > 1 && (_jsxs(_Fragment, { children: [_jsx("button", { onClick: () => onNavigate("prev"), className: "absolute left-4 top-1/2 transform -translate-y-1/2 bg-white/50 size-12 rounded-full flex items-center justify-center z-10 transition-colors p-3.5 cursor-pointer hover:bg-white", children: _jsx(ChevronIcon, { className: "size-full rotate-180" }) }), _jsx("button", { onClick: () => onNavigate("next"), className: "absolute right-4 top-1/2 transform -translate-y-1/2 bg-white/50 size-12 rounded-full flex items-center justify-center z-10 transition-colors p-3.5 cursor-pointer hover:bg-white", children: _jsx(ChevronIcon, { className: "size-full" }) })] })), _jsx("div", { className: "flex items-center justify-center pt-16 min-h-[60vh] max-h-[80vh]", children: isCurrentVideo ? (_jsx("div", { className: "relative w-full h-full flex items-center justify-center", children: isVideoPlaying ? (_jsx("video", { src: currentView.url, width: 1200, height: 800, className: "max-w-full max-h-full object-contain", controls: true, autoPlay: true, onEnded: () => onVideoPlayingChange(false), children: _jsx("track", { kind: "captions", srcLang: "en", label: "English" }) })) : (_jsxs("div", { className: "relative cursor-pointer", onClick: () => onVideoPlayingChange(true), children: [_jsx("video", { src: currentView.url, width: 1200, height: 800, className: "max-w-full max-h-full object-contain", muted: true, preload: "metadata", poster: currentView.url + "#t=0.1", children: _jsx("track", { kind: "captions", srcLang: "en", label: "English" }) }), _jsx("div", { className: "absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center hover:bg-opacity-50 transition-all", children: _jsx("div", { className: "w-20 h-20 bg-white bg-opacity-95 rounded-full flex items-center justify-center shadow-xl hover:bg-opacity-100 hover:scale-105 transition-all duration-200", children: _jsx("svg", { className: "w-10 h-10 text-gray-800 ml-1", fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M8 5v14l11-7z" }) }) }) })] })) })) : (_jsx(Image, { src: currentView.url || fallbackImage, alt: `${propertyName} - View ${selectedIndex + 1}`, width: 1200, height: 800, className: "max-w-full max-h-full object-contain" })) })] }) }));
|
|
23
|
+
};
|
|
24
|
+
export default MediaPreviewModal;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { View } from "../../types/assets";
|
|
2
|
+
interface PropertyImageGalleryProps {
|
|
3
|
+
views: View[];
|
|
4
|
+
propertyName: string;
|
|
5
|
+
}
|
|
6
|
+
declare const PropertyImageGallery: ({ views, propertyName }: PropertyImageGalleryProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export default PropertyImageGallery;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import MediaPreviewModal from "./MediaPreviewModal";
|
|
5
|
+
import MediaItem from "./MediaItem";
|
|
6
|
+
import { getLayoutConfig, organizeViews } from "./mediaUtils";
|
|
7
|
+
import ChevronIcon from "../svg/ChevronIcon";
|
|
8
|
+
const PropertyImageGallery = ({ views, propertyName }) => {
|
|
9
|
+
const [selectedIndex, setSelectedIndex] = useState(null);
|
|
10
|
+
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
|
|
11
|
+
const [mainViewIndex, setMainViewIndex] = useState(0);
|
|
12
|
+
const allViews = organizeViews(views);
|
|
13
|
+
const getDisplayViewsFromIndex = (startIndex) => {
|
|
14
|
+
const maxDisplay = 5;
|
|
15
|
+
const result = [];
|
|
16
|
+
for (let i = 0; i < Math.min(maxDisplay, allViews.length); i++) {
|
|
17
|
+
const index = (startIndex + i) % allViews.length;
|
|
18
|
+
result.push(allViews[index]);
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
};
|
|
22
|
+
const displayViews = getDisplayViewsFromIndex(mainViewIndex);
|
|
23
|
+
const displayCount = displayViews.length;
|
|
24
|
+
const totalCount = allViews.length;
|
|
25
|
+
const layoutConfig = getLayoutConfig(displayCount);
|
|
26
|
+
const handleViewClick = (displayIndex) => {
|
|
27
|
+
const actualIndex = (mainViewIndex + displayIndex) % totalCount;
|
|
28
|
+
setSelectedIndex(actualIndex);
|
|
29
|
+
setIsVideoPlaying(false);
|
|
30
|
+
};
|
|
31
|
+
const handleNavigate = (direction) => {
|
|
32
|
+
if (selectedIndex !== null) {
|
|
33
|
+
let newIndex;
|
|
34
|
+
if (direction === "prev") {
|
|
35
|
+
newIndex = selectedIndex > 0 ? selectedIndex - 1 : totalCount - 1;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
newIndex = selectedIndex < totalCount - 1 ? selectedIndex + 1 : 0;
|
|
39
|
+
}
|
|
40
|
+
setSelectedIndex(newIndex);
|
|
41
|
+
setIsVideoPlaying(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const closePreview = () => {
|
|
45
|
+
setSelectedIndex(null);
|
|
46
|
+
setIsVideoPlaying(false);
|
|
47
|
+
};
|
|
48
|
+
const renderSideItems = () => {
|
|
49
|
+
const sideViews = displayViews.slice(1);
|
|
50
|
+
if (displayCount === 2) {
|
|
51
|
+
return (_jsx(MediaItem, { view: sideViews[0], alt: `${propertyName} - View 2`, className: "w-full h-full", width: 320, height: 350, onClick: () => handleViewClick(1) }));
|
|
52
|
+
}
|
|
53
|
+
if (displayCount === 3) {
|
|
54
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "h-1/2", children: _jsx(MediaItem, { view: sideViews[0], alt: `${propertyName} - View 2`, className: "w-full h-full", width: 320, height: 225, onClick: () => handleViewClick(1) }) }), _jsx("div", { className: "h-1/2", children: _jsx(MediaItem, { view: sideViews[1], alt: `${propertyName} - View 3`, className: "w-full h-full", width: 320, height: 225, onClick: () => handleViewClick(2) }) })] }));
|
|
55
|
+
}
|
|
56
|
+
if (displayCount === 4) {
|
|
57
|
+
return sideViews.map((view, index) => (_jsx("div", { className: "row-span-1", children: _jsx(MediaItem, { view: view, alt: `${propertyName} - View ${index + 2}`, className: "w-full h-full", width: 320, height: 150, onClick: () => handleViewClick(index + 1) }) }, view.id)));
|
|
58
|
+
}
|
|
59
|
+
return sideViews.slice(0, 4).map((view, index) => (_jsxs("div", { className: "w-full h-full relative", children: [_jsx(MediaItem, { view: view, alt: `${propertyName} - View ${index + 2}`, className: "w-full h-full", width: 160, height: 150, onClick: () => handleViewClick(index + 1) }), index === 3 && totalCount > 5 && (_jsxs("button", { className: "absolute inset-0 bg-black bg-opacity-60 flex items-center justify-center text-white text-xl font-semibold cursor-pointer border-none", onClick: () => handleViewClick(index + 1), "aria-label": `View ${totalCount - 5} more images`, children: ["+", totalCount - 5] }))] }, view.id)));
|
|
60
|
+
};
|
|
61
|
+
if (displayCount === 0) {
|
|
62
|
+
return (_jsx("div", { className: "gap-2 m-4 mb-6 mt-0 mx-8 bg-white h-[350px] rounded-3xl overflow-hidden flex items-center justify-center", children: _jsx("div", { className: "text-gray-500", children: "No media available" }) }));
|
|
63
|
+
}
|
|
64
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex gap-2 m-4 mb-6 mt-0 mx-12 bg-white h-[350px] rounded-3xl overflow-hidden", children: [_jsxs("div", { className: layoutConfig.mainWidth + " h-full relative", children: [_jsx(MediaItem, { view: allViews[mainViewIndex], alt: `${propertyName} - Main view`, className: "w-full h-full", width: displayCount === 1 ? 800 : 480, height: 350, priority: true, onClick: () => {
|
|
65
|
+
setSelectedIndex(mainViewIndex);
|
|
66
|
+
setIsVideoPlaying(false);
|
|
67
|
+
} }), totalCount > 1 && (_jsxs(_Fragment, { children: [_jsx("button", { onClick: (e) => {
|
|
68
|
+
e.stopPropagation();
|
|
69
|
+
const prevIndex = mainViewIndex > 0 ? mainViewIndex - 1 : totalCount - 1;
|
|
70
|
+
setMainViewIndex(prevIndex);
|
|
71
|
+
}, className: "absolute left-4 top-1/2 transform -translate-y-1/2 bg-white/70 hover:bg-white size-10 rounded-full flex items-center justify-center z-10 transition-colors p-2.5 cursor-pointer shadow-lg", children: _jsx(ChevronIcon, { className: "size-full rotate-180 text-gray-700" }) }), _jsx("button", { onClick: (e) => {
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
const nextIndex = mainViewIndex < totalCount - 1 ? mainViewIndex + 1 : 0;
|
|
74
|
+
setMainViewIndex(nextIndex);
|
|
75
|
+
}, className: "absolute right-4 top-1/2 transform -translate-y-1/2 bg-white/70 hover:bg-white size-10 rounded-full flex items-center justify-center z-10 transition-colors p-2.5 cursor-pointer shadow-lg", children: _jsx(ChevronIcon, { className: "size-full text-gray-700" }) })] }))] }), layoutConfig.showSide && (_jsx("div", { className: `${layoutConfig.sideWidth} ${layoutConfig.sideLayout} h-full`, children: renderSideItems() }))] }), _jsx(MediaPreviewModal, { isOpen: selectedIndex !== null, onClose: closePreview, views: allViews, selectedIndex: selectedIndex || 0, propertyName: propertyName, isVideoPlaying: isVideoPlaying, onVideoPlayingChange: setIsVideoPlaying, onNavigate: handleNavigate })] }));
|
|
76
|
+
};
|
|
77
|
+
export default PropertyImageGallery;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { View } from "../../types/assets";
|
|
2
|
+
export declare const isVideo: (view: View) => boolean;
|
|
3
|
+
export declare const organizeViews: (views: View[]) => View[];
|
|
4
|
+
export declare const getDisplayViews: (views: View[]) => View[];
|
|
5
|
+
export declare const getLayoutConfig: (count: number) => {
|
|
6
|
+
mainWidth: string;
|
|
7
|
+
sideWidth: string;
|
|
8
|
+
sideLayout: string;
|
|
9
|
+
showSide: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare const fallbackImage = "https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Helper function to check if a view is a video
|
|
2
|
+
export const isVideo = (view) => {
|
|
3
|
+
return (view.name === "video" ||
|
|
4
|
+
view.url.toLowerCase().includes(".mp4") ||
|
|
5
|
+
view.url.toLowerCase().includes(".mov") ||
|
|
6
|
+
view.url.toLowerCase().includes(".avi") ||
|
|
7
|
+
view.url.toLowerCase().includes(".webm"));
|
|
8
|
+
};
|
|
9
|
+
// Separate and organize views: images first, then videos
|
|
10
|
+
export const organizeViews = (views) => {
|
|
11
|
+
if (!views || views.length === 0)
|
|
12
|
+
return [];
|
|
13
|
+
const images = views.filter((view) => !isVideo(view));
|
|
14
|
+
const videos = views.filter((view) => isVideo(view));
|
|
15
|
+
return [...images, ...videos];
|
|
16
|
+
};
|
|
17
|
+
// Get display views (max 5 for gallery display)
|
|
18
|
+
export const getDisplayViews = (views) => {
|
|
19
|
+
const organized = organizeViews(views);
|
|
20
|
+
return organized.slice(0, 5);
|
|
21
|
+
};
|
|
22
|
+
// Get layout configuration based on view count
|
|
23
|
+
export const getLayoutConfig = (count) => {
|
|
24
|
+
if (count === 1) {
|
|
25
|
+
return {
|
|
26
|
+
mainWidth: "w-full",
|
|
27
|
+
sideWidth: "",
|
|
28
|
+
sideLayout: "",
|
|
29
|
+
showSide: false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (count === 2) {
|
|
33
|
+
return {
|
|
34
|
+
mainWidth: "w-3/5",
|
|
35
|
+
sideWidth: "w-2/5",
|
|
36
|
+
sideLayout: "flex flex-col",
|
|
37
|
+
showSide: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (count === 3) {
|
|
41
|
+
return {
|
|
42
|
+
mainWidth: "w-3/5",
|
|
43
|
+
sideWidth: "w-2/5",
|
|
44
|
+
sideLayout: "flex flex-col gap-2",
|
|
45
|
+
showSide: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (count === 4) {
|
|
49
|
+
return {
|
|
50
|
+
mainWidth: "w-3/5",
|
|
51
|
+
sideWidth: "w-2/5",
|
|
52
|
+
sideLayout: "grid grid-cols-1 grid-rows-3 gap-2",
|
|
53
|
+
showSide: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// 5 or more
|
|
57
|
+
return {
|
|
58
|
+
mainWidth: "w-3/5",
|
|
59
|
+
sideWidth: "w-2/5",
|
|
60
|
+
sideLayout: "grid grid-cols-2 grid-rows-2 gap-2",
|
|
61
|
+
showSide: true,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
export const fallbackImage = "https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
const ChevronIcon = ({ className = "", ...props }) => {
|
|
4
|
+
return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: className, ...props, children: _jsx("path", { d: "M8 4l8 8-8 8" }) }));
|
|
5
|
+
};
|
|
6
|
+
export default ChevronIcon;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED