Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e27611b867 |
+3
-1
@@ -5,7 +5,9 @@ export type TIssueAttachment = {
|
||||
attributes: {
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
};
|
||||
asset: string;
|
||||
asset_url: string;
|
||||
issue_id: string;
|
||||
// required
|
||||
@@ -14,7 +16,7 @@ export type TIssueAttachment = {
|
||||
};
|
||||
|
||||
export type TIssueAttachmentUploadResponse = TFileSignedURLResponse & {
|
||||
attachment: TIssueAttachment
|
||||
attachment: TIssueAttachment;
|
||||
};
|
||||
|
||||
export type TIssueAttachmentMap = {
|
||||
|
||||
@@ -17,6 +17,8 @@ import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useMember } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// local-components
|
||||
import { MusicPlayer } from "./music-player";
|
||||
|
||||
type TIssueAttachmentsListItem = {
|
||||
attachmentId: string;
|
||||
@@ -36,7 +38,7 @@ export const IssueAttachmentsListItem: FC<TIssueAttachmentsListItem> = observer(
|
||||
// derived values
|
||||
const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined;
|
||||
const fileName = getFileName(attachment?.attributes.name ?? "");
|
||||
const fileExtension = getFileExtension(attachment?.asset_url ?? "");
|
||||
const fileExtension = getFileExtension(attachment?.asset ?? "");
|
||||
const fileIcon = getFileIcon(fileExtension, 18);
|
||||
const fileURL = getFileURL(attachment?.asset_url ?? "");
|
||||
// hooks
|
||||
@@ -52,48 +54,56 @@ export const IssueAttachmentsListItem: FC<TIssueAttachmentsListItem> = observer(
|
||||
e.stopPropagation();
|
||||
window.open(fileURL, "_blank");
|
||||
}}
|
||||
className="group flex items-center justify-between gap-3 h-11 hover:bg-custom-background-90 pl-9 pr-2 w-full border-red-500"
|
||||
>
|
||||
<div className="group flex items-center justify-between gap-3 h-11 hover:bg-custom-background-90 pl-9 pr-2">
|
||||
<div className="flex items-center gap-3 text-sm truncate">
|
||||
<div className="flex items-center gap-3">{fileIcon}</div>
|
||||
<div className="flex items-center gap-4 truncate">
|
||||
<div className="flex items-center gap-3 text-sm truncate border-red-500">
|
||||
<div className="flex items-center gap-3 flex-shrink-0">{fileIcon}</div>
|
||||
<Tooltip tooltipContent={`${fileName}.${fileExtension}`} isMobile={isMobile}>
|
||||
<p className="text-custom-text-200 font-medium truncate">{`${fileName}.${fileExtension}`}</p>
|
||||
</Tooltip>
|
||||
<span className="flex size-1.5 bg-custom-background-80 rounded-full" />
|
||||
<span className="flex size-1.5 bg-custom-background-80 rounded-full flex-shrink-0" />
|
||||
<span className="flex-shrink-0 text-custom-text-400">{convertBytesToSize(attachment.attributes.size)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{attachment?.updated_by && (
|
||||
<>
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`${
|
||||
getUserDetails(attachment.updated_by)?.display_name ?? ""
|
||||
} uploaded on ${renderFormattedDate(attachment.updated_at)}`}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<ButtonAvatars showTooltip userIds={attachment?.updated_by} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
{(getFileExtension(attachment.attributes.name) === "wav" ||
|
||||
getFileExtension(attachment.attributes.name) == "mp3") &&
|
||||
fileURL && (
|
||||
<div className="w-min max-w-52 flex-shrink-0">
|
||||
<MusicPlayer src={fileURL} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CustomMenu ellipsis closeOnSelect placement="bottom-end" disabled={disabled}>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleDeleteAttachmentModal(attachmentId);
|
||||
}}
|
||||
<div className="flex items-center gap-3 flex-shrink-0 border-orange-500">
|
||||
{attachment?.updated_by && (
|
||||
<>
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`${
|
||||
getUserDetails(attachment.updated_by)?.display_name ?? ""
|
||||
} uploaded on ${renderFormattedDate(attachment.updated_at)}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
<span>Delete</span>
|
||||
<div className="flex items-center justify-center">
|
||||
<ButtonAvatars showTooltip userIds={attachment?.updated_by} />
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<CustomMenu ellipsis closeOnSelect placement="bottom-end" disabled={disabled}>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleDeleteAttachmentModal(attachmentId);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
|
||||
@@ -6,4 +6,5 @@ export * from "./attachment-upload-details";
|
||||
export * from "./attachment-upload";
|
||||
export * from "./attachments-list";
|
||||
export * from "./delete-attachment-modal";
|
||||
export * from "./music-player";
|
||||
export * from "./root";
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
import React, { FC, useRef, useState } from "react";
|
||||
import { Pause, Play, RotateCw } from "lucide-react";
|
||||
|
||||
type Props = { src: string };
|
||||
|
||||
export const MusicPlayer: FC<Props> = (props) => {
|
||||
const { src } = props;
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
const timelineRef = useRef<HTMLDivElement>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [isEnded, setIsEnded] = useState(false);
|
||||
const [showControls, setShowControls] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const formatTime = (time: number) => {
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const handleReplay = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = 0;
|
||||
audioRef.current.play();
|
||||
setIsPlaying(true);
|
||||
setIsEnded(false);
|
||||
}
|
||||
};
|
||||
|
||||
const togglePlay = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (audioRef.current) {
|
||||
if (isPlaying) {
|
||||
audioRef.current.pause();
|
||||
} else {
|
||||
audioRef.current.play();
|
||||
setIsEnded(false);
|
||||
setShowControls(true);
|
||||
}
|
||||
setIsPlaying(!isPlaying);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTimeUpdate = () => {
|
||||
if (audioRef.current) {
|
||||
setCurrentTime(audioRef.current.currentTime);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadedMetadata = () => {
|
||||
if (audioRef.current) {
|
||||
setDuration(audioRef.current.duration);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnded = () => {
|
||||
setIsPlaying(false);
|
||||
setIsEnded(true);
|
||||
};
|
||||
|
||||
const handleTimelineClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (timelineRef.current && audioRef.current) {
|
||||
const rect = timelineRef.current.getBoundingClientRect();
|
||||
const pos = (e.clientX - rect.left) / rect.width;
|
||||
const newTime = pos * duration;
|
||||
audioRef.current.currentTime = newTime;
|
||||
setCurrentTime(newTime);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center bg-custom-background-90 group-hover:bg-custom-background-100 rounded py-1 px-2 transition-all ease-linear ${
|
||||
showControls || isHovered ? "min-w-[200px]" : "w-fit"
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<audio
|
||||
ref={audioRef}
|
||||
src={src}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
onLoadedMetadata={handleLoadedMetadata}
|
||||
onEnded={handleEnded}
|
||||
/>
|
||||
{isEnded ? (
|
||||
<button onClick={handleReplay}>
|
||||
<RotateCw className="size-3.5" />
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={togglePlay}>
|
||||
{isPlaying ? (
|
||||
<Pause className="size-3.5 text-custom-text-300 group-hover:text-custom-text-200 fill-current cursor-pointer" />
|
||||
) : (
|
||||
<Play className="size-3.5 text-custom-text-300 group-hover:text-custom-text-200 fill-current cursor-pointer" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className={`flex items-center gap-2 flex-1 overflow-hidden transition-all ease-linear origin-left ${
|
||||
showControls || isHovered
|
||||
? "opacity-100 translate-x-0 max-w-full ml-2 "
|
||||
: "opacity-0 -translate-x-2 max-w-0 pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
<>
|
||||
<span className="text-xs text-custom-text-300 group-hover:text-custom-text-200 whitespace-nowrap">
|
||||
{formatTime(currentTime)}/{formatTime(duration)}
|
||||
</span>
|
||||
<div
|
||||
ref={timelineRef}
|
||||
className="flex-1 relative h-1 bg-custom-border-200 rounded cursor-pointer"
|
||||
onClick={handleTimelineClick}
|
||||
>
|
||||
<div
|
||||
className="absolute h-full bg-custom-text-300 rounded-full"
|
||||
style={{ width: `${(currentTime / duration) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user