@medalsocial/meda
Version:
Shared Meda UI shell and runtime package.
105 lines (104 loc) • 15.8 kB
JavaScript
'use client';
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { Bookmark, ChevronLeft, ChevronRight, Heart, MessageCircle, MoreHorizontal, Music, Send, } from 'lucide-react';
import { useCallback, useState } from 'react';
import { cn } from '../../lib/utils.js';
import { Avatar } from '../internal/avatar.js';
import { Textarea } from '../internal/textarea.js';
import { useSwipe } from '../internal/use-swipe.js';
export const DEFAULT_INSTAGRAM_LABELS = {
placeholder: 'Write a caption...',
justNow: 'Just now',
likes: (count) => `${count} likes`,
addImage: 'Add an image to preview',
moreOptions: 'More options',
like: 'Like',
comment: 'Comment',
share: 'Share',
save: 'Save',
carouselPrev: 'Previous slide',
carouselNext: 'Next slide',
carouselCounter: (current, total) => `${current} / ${total}`,
reelLabel: 'Reels',
reelAudio: 'Original audio',
storyLabel: 'Story',
storySendMessage: 'Send message',
};
/**
* Instagram preview supporting Feed / Carousel / Reel / Story layouts.
* Auto-detects `carousel` when more than one media URL is present.
*/
export function InstagramPreview({ displayName, username, avatarUrl, content, mediaUrls, editable = false, onContentChange, className, labels, instagramPostType, }) {
const l = { ...DEFAULT_INSTAGRAM_LABELS, ...labels };
const formattedUsername = username.startsWith('@') ? username.slice(1) : username;
const hasMedia = mediaUrls !== undefined && mediaUrls.length > 0;
const postType = instagramPostType ?? (mediaUrls && mediaUrls.length > 1 ? 'carousel' : 'feed');
if (postType === 'reel') {
return (_jsx(ReelPreview, { displayName: displayName, username: formattedUsername, avatarUrl: avatarUrl, content: content, mediaUrl: hasMedia ? mediaUrls[0] : undefined, labels: l, className: className }));
}
if (postType === 'story') {
return (_jsx(StoryPreview, { displayName: displayName, username: formattedUsername, avatarUrl: avatarUrl, mediaUrl: hasMedia ? mediaUrls[0] : undefined, labels: l, className: className }));
}
return (_jsx(FeedPreview, { displayName: displayName, username: formattedUsername, avatarUrl: avatarUrl, content: content, mediaUrls: mediaUrls, editable: editable, onContentChange: onContentChange, isCarousel: postType === 'carousel', labels: l, className: className }));
}
// ---------------------------------------------------------------------------
// Feed / Carousel layout
// ---------------------------------------------------------------------------
function FeedPreview({ displayName, username, avatarUrl, content, mediaUrls, editable, onContentChange, isCarousel, labels, className, }) {
const hasMedia = mediaUrls !== undefined && mediaUrls.length > 0;
const [carouselIndex, setCarouselIndex] = useState(0);
const goToPrev = useCallback(() => {
setCarouselIndex((prev) => Math.max(0, prev - 1));
}, []);
const goToNext = useCallback(() => {
/* v8 ignore next — mediaUrls is always defined when goToNext is reachable (carousel requires >1 url) */
if (!mediaUrls)
return;
setCarouselIndex((prev) => Math.min(mediaUrls.length - 1, prev + 1));
}, [mediaUrls]);
const swipe = useSwipe({ onSwipeLeft: goToNext, onSwipeRight: goToPrev });
return (_jsxs("div", { "data-slot": "post-preview", "data-platform": "instagram", "data-instagram-post-type": isCarousel ? 'carousel' : 'feed', className: cn('@container bg-white text-gray-900 @[420px]:text-[15px] dark:bg-black dark:text-white', className), children: [_jsxs("div", { className: "flex items-center justify-between p-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Avatar, { src: avatarUrl, displayName: displayName, className: "h-8 w-8 ring-2 ring-pink-500 ring-offset-2" }), _jsx("p", { className: "font-semibold text-[14px]", children: username })] }), _jsx("button", { type: "button", "aria-label": labels.moreOptions, className: "-m-2 inline-flex min-h-11 min-w-11 items-center justify-center p-2", children: _jsx(MoreHorizontal, { className: "h-5 w-5" }) })] }), hasMedia ? (_jsxs("div", { "data-slot": "instagram-carousel-track", className: "relative aspect-square bg-gray-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500", style: { touchAction: 'pan-y' }, ...(isCarousel && mediaUrls.length > 1
? {
role: 'region',
'aria-label': labels.carouselCounter(carouselIndex + 1, mediaUrls.length),
tabIndex: 0,
onKeyDown: (event) => {
if (event.key === 'ArrowLeft') {
event.preventDefault();
event.stopPropagation();
goToPrev();
}
else if (event.key === 'ArrowRight') {
event.preventDefault();
event.stopPropagation();
goToNext();
}
},
}
: {}), ...swipe, children: [_jsx("img", { src: mediaUrls[isCarousel ? carouselIndex : 0], alt: "Post", loading: "lazy", decoding: "async", className: "h-full w-full object-cover" }), isCarousel && mediaUrls.length > 1 && (_jsxs(_Fragment, { children: [carouselIndex > 0 && (_jsx("button", { type: "button", onClick: goToPrev, "aria-label": labels.carouselPrev, className: "-translate-y-1/2 absolute top-1/2 left-2 inline-flex h-7 w-7 items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70", children: _jsx(ChevronLeft, { className: "h-4 w-4" }) })), carouselIndex < mediaUrls.length - 1 && (_jsx("button", { type: "button", onClick: goToNext, "aria-label": labels.carouselNext, className: "-translate-y-1/2 absolute top-1/2 right-2 inline-flex h-7 w-7 items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70", children: _jsx(ChevronRight, { className: "h-4 w-4" }) })), _jsx("div", { className: "absolute top-3 right-3 rounded-full bg-black/60 px-2.5 py-0.5 font-medium text-white text-xs", children: labels.carouselCounter(carouselIndex + 1, mediaUrls.length) }), _jsxs("div", { className: "-translate-x-1/2 absolute bottom-4 left-1/2 flex gap-1", children: [mediaUrls.slice(0, 5).map((url, i) => (_jsx("button", { type: "button", "aria-label": `Go to slide ${i + 1}`, ...(i === carouselIndex ? { 'aria-current': 'true' } : {}), onClick: () => setCarouselIndex(i), className: cn(
// Visible dot stays small; expand the touch target via padding so the
// hit area is at least 44x44px without breaking the IG visual.
'inline-flex min-h-11 min-w-11 items-center justify-center p-2'), children: _jsx("span", { "aria-hidden": "true", className: cn('h-1.5 w-1.5 rounded-full transition-colors', i === carouselIndex ? 'bg-blue-500' : 'bg-white/60') }) }, url))), mediaUrls.length > 5 && (_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-white/40", "aria-hidden": "true" }))] })] }))] })) : (_jsx(MediaPlaceholder, { label: labels.addImage })), _jsxs("div", { className: "p-3", children: [_jsxs("div", { className: "mb-3 flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx("button", { type: "button", "aria-label": labels.like, className: "-m-1 inline-flex min-h-11 min-w-11 items-center justify-center p-1 hover:opacity-60", children: _jsx(Heart, { className: "h-6 w-6" }) }), _jsx("button", { type: "button", "aria-label": labels.comment, className: "-m-1 inline-flex min-h-11 min-w-11 items-center justify-center p-1 hover:opacity-60", children: _jsx(MessageCircle, { className: "-scale-x-100 h-6 w-6" }) }), _jsx("button", { type: "button", "aria-label": labels.share, className: "-m-1 inline-flex min-h-11 min-w-11 items-center justify-center p-1 hover:opacity-60", children: _jsx(Send, { className: "-rotate-12 h-6 w-6" }) })] }), _jsx("button", { type: "button", "aria-label": labels.save, className: "-m-1 inline-flex min-h-11 min-w-11 items-center justify-center p-1 hover:opacity-60", children: _jsx(Bookmark, { className: "h-6 w-6" }) })] }), _jsx("p", { className: "mb-1 font-semibold text-[14px]", children: labels.likes(0) }), _jsxs("div", { className: "text-[14px]", children: [_jsx("span", { className: "mr-1 font-semibold", children: username }), editable ? (_jsx(Textarea, { value: content, onChange: (e) => onContentChange?.(e.target.value), placeholder: labels.placeholder, className: "mt-1 inline min-h-[40px] w-full text-[14px] text-gray-900 placeholder:text-gray-500" })) : (_jsx("span", { className: "whitespace-pre-wrap break-words", children: content }))] }), _jsx("p", { className: "mt-2 text-[10px] text-gray-400 uppercase tracking-wide", children: labels.justNow })] })] }));
}
// ---------------------------------------------------------------------------
// Reel layout
// ---------------------------------------------------------------------------
/* v8 ignore next 17 — v8 phantom duplicate function record */
function ReelPreview({ displayName, username, avatarUrl, content, mediaUrl, labels, className, }) {
/* v8 ignore next — v8 phantom duplicate function body record */
return (_jsxs("div", { "data-slot": "post-preview", "data-platform": "instagram", "data-instagram-post-type": "reel", className: cn('@container relative aspect-[9/16] overflow-hidden rounded-lg bg-black @[420px]:rounded-xl', className), children: [mediaUrl ? (_jsx("img", { src: mediaUrl, alt: "Reel", loading: "lazy", decoding: "async", className: "h-full w-full object-cover" })) : (_jsx("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-b from-gray-800 to-gray-900", children: _jsxs("div", { className: "text-center text-gray-500", children: [_jsx("div", { className: "mx-auto mb-2 h-16 w-16 rounded-full border-2 border-gray-600 border-dashed" }), _jsx("p", { className: "text-sm", children: labels.reelLabel })] }) })), _jsx("div", { className: "absolute top-4 right-3 font-semibold text-sm text-white drop-shadow-lg", children: labels.reelLabel }), _jsxs("div", { className: "absolute right-3 bottom-20 flex flex-col items-center gap-5", children: [_jsxs("button", { type: "button", "aria-label": labels.like, className: "inline-flex min-h-11 min-w-11 flex-col items-center gap-0.5", children: [_jsx(Heart, { className: "h-7 w-7 text-white drop-shadow-lg" }), _jsx("span", { className: "text-[11px] text-white drop-shadow-lg", children: "0" })] }), _jsxs("button", { type: "button", "aria-label": labels.comment, className: "inline-flex min-h-11 min-w-11 flex-col items-center gap-0.5", children: [_jsx(MessageCircle, { className: "-scale-x-100 h-7 w-7 text-white drop-shadow-lg" }), _jsx("span", { className: "text-[11px] text-white drop-shadow-lg", children: "0" })] }), _jsx("button", { type: "button", "aria-label": labels.share, className: "inline-flex min-h-11 min-w-11 flex-col items-center gap-0.5", children: _jsx(Send, { className: "-rotate-12 h-7 w-7 text-white drop-shadow-lg" }) }), _jsx("button", { type: "button", "aria-label": labels.moreOptions, className: "inline-flex min-h-11 min-w-11 flex-col items-center gap-0.5", children: _jsx(MoreHorizontal, { className: "h-7 w-7 text-white drop-shadow-lg" }) }), _jsx(Avatar, { src: avatarUrl, displayName: displayName, className: "h-8 w-8 rounded-md border-2 border-white/40" })] }), _jsxs("div", { className: "absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/70 via-black/30 to-transparent p-4 pb-5", children: [_jsxs("div", { className: "mb-2 flex items-center gap-2", children: [_jsx(Avatar, { src: avatarUrl, displayName: displayName, className: "h-8 w-8" }), _jsx("span", { className: "font-semibold text-sm text-white", children: username })] }), content ? _jsx("p", { className: "line-clamp-2 text-sm text-white/90", children: content }) : null, _jsxs("div", { className: "mt-3 flex items-center gap-2", children: [_jsx(Music, { className: "h-3 w-3 text-white/80", "aria-hidden": "true" }), _jsxs("p", { className: "flex-1 truncate text-white/80 text-xs", children: [labels.reelAudio, " \u00B7 ", username] })] })] })] }));
}
// ---------------------------------------------------------------------------
// Story layout
// ---------------------------------------------------------------------------
/* v8 ignore next 15 — v8 phantom duplicate function record */
function StoryPreview({ displayName, username, avatarUrl, mediaUrl, labels, className, }) {
/* v8 ignore next — v8 phantom duplicate function body record */
return (_jsxs("div", { "data-slot": "post-preview", "data-platform": "instagram", "data-instagram-post-type": "story", className: cn('@container relative aspect-[9/16] overflow-hidden rounded-lg bg-black @[420px]:rounded-xl', className), children: [mediaUrl ? (_jsx("img", { src: mediaUrl, alt: "Story", loading: "lazy", decoding: "async", className: "h-full w-full object-cover" })) : (_jsx("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-b from-gray-800 to-gray-900", children: _jsxs("div", { className: "text-center text-gray-500", children: [_jsx("div", { className: "mx-auto mb-2 h-16 w-16 rounded-full border-2 border-gray-600 border-dashed" }), _jsx("p", { className: "text-sm", children: labels.storyLabel })] }) })), _jsx("div", { className: "absolute inset-x-0 top-0 px-2 pt-2", children: _jsx("div", { className: "h-0.5 w-full overflow-hidden rounded-full bg-white/30", children: _jsx("div", { className: "h-full w-1/3 rounded-full bg-white" }) }) }), _jsxs("div", { className: "absolute inset-x-0 top-3 flex items-center gap-2 px-4 pt-1", children: [_jsx(Avatar, { src: avatarUrl, displayName: displayName, className: "h-8 w-8 ring-2 ring-white/60" }), _jsx("span", { className: "font-semibold text-sm text-white drop-shadow-lg", children: username }), _jsx("span", { className: "text-white/70 text-xs drop-shadow-lg", children: labels.justNow }), _jsx("div", { className: "ml-auto", children: _jsx(MoreHorizontal, { className: "h-5 w-5 text-white drop-shadow-lg", "aria-hidden": "true" }) })] }), _jsx("div", { className: "absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/50 to-transparent p-4 pt-8", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex-1 rounded-full border border-white/40 px-4 py-2", children: _jsx("span", { className: "text-sm text-white/60", children: labels.storySendMessage }) }), _jsx(Heart, { className: "h-6 w-6 text-white", "aria-hidden": "true" }), _jsx(Send, { className: "-rotate-12 h-6 w-6 text-white", "aria-hidden": "true" })] }) })] }));
}
// ---------------------------------------------------------------------------
// Shared
// ---------------------------------------------------------------------------
function MediaPlaceholder({ label }) {
return (_jsx("div", { className: "flex aspect-square items-center justify-center bg-gradient-to-br from-gray-100 to-gray-200", children: _jsxs("div", { className: "text-center text-gray-400", children: [_jsx("svg", { className: "mx-auto mb-2 h-16 w-16", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1, d: "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" }) }), _jsx("p", { className: "text-sm", children: label })] }) }));
}