openreplay/frontend/app/components/Spots/SpotsList/SpotListItem.tsx
Delirium 2cd96b0df0
Highlight UI (#2951)
* ui: start highlight ui

* ui: tag items

* ui: connecting highlights to notes api...

* Highlight feature refinements (#2948)

* ui: move clips player to foss, connect notes api to hl

* ui: tune note/hl editing, prevent zoom slider body from jumping around

* ui: safe check for tag

* ui: fix thumbnail gen

* ui: fix thumbnail gen

* ui: make player modal wider, add shadow

* ui: custom warn barge for clips

* ui: swap icon for note event wrapper

* ui: rm other, fix cancel

* ui: moving around creation modal

* ui: bg tint

* ui: rm disabled for text btn

* ui: fix ownership sorting

* ui: close player on bg click

* ui: fix query, fix min distance for default range

* ui: move hl list header out of list comp

* ui: spot list header segmented size

* Various improvements in highlights (#2955)

* ui: update hl in hlPanel comp

* ui: rm debug

* ui: fix icons file

---------

Co-authored-by: Sudheer Salavadi <connect.uxmaster@gmail.com>
2025-01-24 09:59:54 +01:00

319 lines
8.9 KiB
TypeScript

import {
ClockCircleOutlined,
DeleteOutlined,
DownloadOutlined,
EditOutlined,
MoreOutlined,
PlayCircleOutlined,
SlackOutlined,
UserOutlined,
} from '@ant-design/icons';
import { Button, Checkbox, Dropdown, Tooltip } from 'antd';
import copy from 'copy-to-clipboard';
import { Link2 } from 'lucide-react';
import React, { useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { toast } from 'react-toastify';
import { TextEllipsis } from "UI";
import { Spot } from 'App/mstore/types/spot';
import { spot as spotUrl, withSiteId } from 'App/routes';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import EditItemModal from './EditItemModal';
const backgroundUrl = '/assets/img/spotThumbBg.svg';
interface ISpotListItem {
spot: Spot;
onRename: (id: string, title: string) => void;
onDelete: () => void;
onVideo: (id: string) => Promise<{ url: string }>;
onSelect: (selected: boolean) => void;
isSelected: boolean;
}
function SpotListItem({
spot,
onRename,
onDelete,
onVideo,
onSelect,
isSelected,
}: ISpotListItem) {
const [isEdit, setIsEdit] = useState(false);
const [loading, setLoading] = useState(true);
const [tooltipText, setTooltipText] = useState('Copy link to clipboard');
const history = useHistory();
const { siteId } = useParams<{ siteId: string }>();
const menuItems = [
{
key: 'rename',
icon: <EditOutlined />,
label: 'Rename',
},
{
key: 'download',
label: 'Download Video',
icon: <DownloadOutlined />,
},
{
key: 'delete',
icon: <DeleteOutlined />,
label: 'Delete',
},
];
React.useEffect(() => {
menuItems.splice(1, 0, {
key: 'slack',
icon: <SlackOutlined />,
label: 'Share via Slack',
});
}, []);
const onMenuClick = async ({ key }: any) => {
switch (key) {
case 'rename':
return setIsEdit(true);
case 'download':
const { url } = await onVideo(spot.spotId);
await downloadFile(url, `${spot.title}.webm`);
return;
case 'copy':
copy(
`${window.location.origin}${withSiteId(
spotUrl(spot.spotId.toString()),
siteId
)}`
);
return toast.success('Spot URL copied to clipboard');
case 'delete':
return onDelete();
case 'slack':
break;
default:
break;
}
};
const onSpotClick = (e: any) => {
if (e.shiftKey || e.ctrlKey || e.metaKey) {
const spotLink = withSiteId(spotUrl(spot.spotId.toString()), siteId);
const fullLink = `${window.location.origin}${spotLink}`;
window.open(fullLink, '_blank');
} else {
history.push(withSiteId(spotUrl(spot.spotId.toString()), siteId));
}
};
const copyToClipboard = () => {
const spotLink = withSiteId(spotUrl(spot.spotId.toString()), siteId);
const fullLink = `${window.location.origin}${spotLink}`;
navigator.clipboard
.writeText(fullLink)
.then(() => {
setTooltipText('Link copied to clipboard!');
setTimeout(() => setTooltipText('Copy link to clipboard'), 2000); // Reset tooltip text after 2 seconds
})
.catch(() => {
setTooltipText('Failed to copy URL');
setTimeout(() => setTooltipText('Copy link to clipboard'), 2000); // Reset tooltip text after 2 seconds
});
};
const onSave = (newName: string) => {
onRename(spot.spotId, newName);
setIsEdit(false);
};
return (
<>
{isEdit ? (
<EditItemModal
onSave={onSave}
onClose={() => setIsEdit(false)}
itemName={spot.title}
/>
) : null}
<GridItem
modifier={
<div className="absolute left-0 bottom-8 flex relative gap-2 justify-end pe-2 pb-2 ">
<Tooltip title={tooltipText} className='capitalize'>
<div
className={
'bg-black/70 text-white p-1 px-2 text-xs rounded-lg transition-transform transform translate-y-14 group-hover:translate-y-0 '
}
onClick={copyToClipboard}
style={{ cursor: 'pointer' }}
>
<Link2 size={16} strokeWidth={1} />
</div>
</Tooltip>
<div
className={
'bg-black/70 text-white p-1 px-2 text-xs rounded-lg flex items-center cursor-normal'
}
>
{spot.duration}
</div>
</div>
}
onSave={onSave}
setIsEdit={setIsEdit}
isEdit={isEdit}
title={spot.title}
onItemClick={onSpotClick}
thumbnail={spot.thumbnail}
setLoading={setLoading}
loading={loading}
isSelected={isSelected}
tooltipText={tooltipText}
copyToClipboard={copyToClipboard}
duration={spot.duration}
onSelect={onSelect}
user={spot.user}
createdAt={spot.createdAt}
menuItems={menuItems}
onMenuClick={onMenuClick}
/>
</>
);
}
export function GridItem({
title,
onItemClick,
thumbnail,
setLoading,
loading,
isSelected,
onSelect,
user,
createdAt,
menuItems,
onMenuClick,
modifier,
}: {
title: string;
onItemClick: () => void;
thumbnail: string;
setLoading: (loading: boolean) => void;
loading?: boolean;
isSelected?: boolean;
copyToClipboard: () => void;
onSelect?: (selected: boolean) => void;
user: string;
createdAt: string;
menuItems: any[];
onMenuClick: (key: any) => void;
modifier: React.ReactNode;
}) {
return (
<div
className={`bg-white rounded-lg overflow-hidden shadow-sm border ${
isSelected ? 'border-teal/30' : 'border-transparent'
} transition flex flex-col items-start hover:border-teal`}
>
<div
className="relative group overflow-hidden"
style={{
width: '100%',
height: 180,
backgroundImage: `url(${backgroundUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
{loading && (
<div className="absolute inset-0 flex items-center justify-center">
<AnimatedSVG name={ICONS.LOADER} size={32} />
</div>
)}
<div
className="block w-full h-full cursor-pointer transition hover:bg-teal/70 relative"
onClick={onItemClick}
>
<img
src={thumbnail}
alt={title}
className={'w-full h-full object-cover opacity-80'}
onLoad={() => setLoading(false)}
onError={() => setLoading(false)}
style={{ display: loading ? 'none' : 'block' }}
/>
<div className="absolute inset-0 flex items-center justify-center opacity-0 scale-75 transition-all hover:scale-100 hover:transition-all group-hover:opacity-100 transition-opacity ">
<PlayCircleOutlined
style={{ fontSize: '48px', color: 'white' }}
className="bg-teal/50 rounded-full"
/>
</div>
</div>
{modifier}
</div>
<div className={'w-full border-t'}>
<div className={'flex items-center gap-2'}>
{onSelect ? (
<div className='px-3 pt-2'>
<Checkbox
checked={isSelected}
onChange={({ target: { checked } }) => onSelect(checked)}
className={`flex cursor-pointer w-full hover:text-teal ${
isSelected ? 'text-teal' : ''
}`}
>
<TextEllipsis text={title} className='w-full'/>
</Checkbox>
</div>
) : (
<div className='bg-yellow/50 mx-2 mt-2 px-2 w-full rounded '><TextEllipsis text={title} className='capitalize' /></div>
)}
</div>
<div className={'flex items-center gap-1 leading-4 text-xs opacity-50 p-3'}>
<div>
<UserOutlined />
</div>
<TextEllipsis text={user} className='capitalize' />
<div className="ml-auto">
<ClockCircleOutlined />
</div>
<div>{createdAt}</div>
<div>
<Dropdown
menu={{ items: menuItems, onClick: onMenuClick }}
trigger={['click']}
>
<Button type="text" icon={<MoreOutlined />} size={'small'} />
</Dropdown>
</div>
</div>
</div>
</div>
);
}
async function downloadFile(url: string, fileName: string) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
} catch (error) {
console.error('Error downloading file:', error);
}
}
export default SpotListItem;