Files
OpenFrame/components/video-page/comment-rich-text.tsx
T
Yusuf İpek 975ff603e1 feat: add audio asset support with recording and upload functionality
- Implemented audio asset handling in the API, including download and deletion routes.
- Added audio content type mappings and extraction functions for audio file names and keys.
- Enhanced the asset management UI to support audio uploads, including recording capabilities.
- Updated the database schema to accommodate audio assets with new enum values.
- Integrated audio playback features in the asset list and comment sections.
- Improved user experience with drag-and-drop support for audio files.
2026-02-28 11:01:10 +03:00

92 lines
3.0 KiB
TypeScript

'use client';
import React from 'react';
import { Image as ImageIcon, Video, Volume2 } from 'lucide-react';
import type { VideoAsset } from '@/components/video-page/types';
const URL_REGEX = /(https?:\/\/[^\s]+)/g;
const ASSET_MENTION_REGEX = /@\[(.+?)\]\(asset:([a-z0-9]+)\)/gi;
interface CommentRichTextProps {
text: string;
onAssetMentionClick?: (assetId: string) => void;
assets?: VideoAsset[];
}
function renderUrls(text: string): React.ReactNode[] {
const parts = text.split(URL_REGEX);
return parts.map((part, index) => {
if (/^https?:\/\/[^\s]+$/.test(part)) {
return (
<a
key={`url-${index}`}
href={part}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline break-all"
onClick={(event) => event.stopPropagation()}
>
{part}
</a>
);
}
return <React.Fragment key={`txt-${index}`}>{part}</React.Fragment>;
});
}
export function CommentRichText({ text, onAssetMentionClick, assets = [] }: CommentRichTextProps) {
const nodes: React.ReactNode[] = [];
let lastIndex = 0;
for (const match of text.matchAll(ASSET_MENTION_REGEX)) {
const mentionIndex = match.index ?? -1;
if (mentionIndex < 0) continue;
if (mentionIndex > lastIndex) {
nodes.push(...renderUrls(text.slice(lastIndex, mentionIndex)));
}
const fallbackLabel = match[1] || 'asset';
const assetId = match[2] || '';
const matchedAsset = assets.find((asset) => asset.id === assetId);
const label = matchedAsset?.displayName || fallbackLabel;
const assetKind = matchedAsset?.kind;
nodes.push(
<button
key={`mention-${assetId}-${mentionIndex}`}
type="button"
className="inline-flex max-w-full items-center gap-1 rounded bg-primary/10 px-1.5 py-0.5 text-primary hover:bg-primary/20 transition-colors align-middle"
onClick={(event) => {
event.stopPropagation();
if (assetId && onAssetMentionClick) onAssetMentionClick(assetId);
}}
title={label}
>
<span
className={
assetKind === 'VIDEO'
? 'inline-flex h-4 w-4 shrink-0 items-center justify-center rounded bg-violet-500/25 text-violet-200'
: assetKind === 'AUDIO'
? 'inline-flex h-4 w-4 shrink-0 items-center justify-center rounded bg-blue-500/25 text-blue-200'
: 'inline-flex h-4 w-4 shrink-0 items-center justify-center rounded bg-emerald-500/25 text-emerald-200'
}
>
{assetKind === 'VIDEO' ? <Video className="h-2.5 w-2.5" /> : assetKind === 'AUDIO' ? <Volume2 className="h-2.5 w-2.5" /> : <ImageIcon className="h-2.5 w-2.5" />}
</span>
<span className="truncate max-w-[190px] sm:max-w-[240px]">
@{label}
</span>
</button>
);
lastIndex = mentionIndex + match[0].length;
}
if (lastIndex < text.length) {
nodes.push(...renderUrls(text.slice(lastIndex)));
}
return <>{nodes}</>;
}