diff options
Diffstat (limited to 'src/client/util/reportManager/ReportManagerComponents.tsx')
-rw-r--r-- | src/client/util/reportManager/ReportManagerComponents.tsx | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/src/client/util/reportManager/ReportManagerComponents.tsx b/src/client/util/reportManager/ReportManagerComponents.tsx new file mode 100644 index 000000000..1a4ddb3a3 --- /dev/null +++ b/src/client/util/reportManager/ReportManagerComponents.tsx @@ -0,0 +1,153 @@ +import * as React from 'react'; +import { Issue } from './reportManagerSchema'; +import { getLabelColors } from './reportManagerUtils'; +import ReactMarkdown from 'react-markdown'; +import rehypeRaw from 'rehype-raw'; +import remarkGfm from 'remark-gfm'; + +// Mini components to render issues + +interface IssueCardProps { + issue: Issue; + onSelect: () => void; +} +export const IssueCard = ({ issue, onSelect }: IssueCardProps) => { + return ( + <div className="issue-card" onClick={onSelect}> + <div className="issue-top"> + <label className="issue-label">#{issue.number}</label> + <div className="issue-tags"> + {issue.labels.map(label => { + const labelString = typeof label === 'string' ? label : label.name ?? ''; + const colors = getLabelColors(labelString); + return <Tag key={labelString} text={labelString} backgroundColor={colors[0]} color={colors[1]} />; + })} + </div> + </div> + <h3 className="issue-title">{issue.title}</h3> + </div> + ); +}; + +interface IssueViewProps { + issue: Issue; +} + +export const IssueView = ({ issue }: IssueViewProps) => { + const [issueBody, setIssueBody] = React.useState(''); + + const isVideoValid = (src: string) => { + const videoElement = document.createElement('video'); + const validPromise: Promise<boolean> = new Promise(resolve => { + videoElement.addEventListener('loadeddata', () => resolve(true)); + videoElement.addEventListener('error', () => resolve(false)); + }); + videoElement.src = src; + return validPromise; + }; + + const getLinkFromTag = async (tag: string) => { + const regex = /src="([^"]+)"/; + let url = ''; + const match = tag.match(regex); + if (match) { + url = match[1]; + } + + if (url.startsWith('https://github.com/brown-dash/Dash-Web/assets')) { + return `\n${url} (Not authorized to display image here)`; + } + return await getTagFromUrl(url); + }; + + const getTagFromUrl = async (url: string) => { + const imgRegex = /https:\/\/browndash\.com\/files[/\\]images/; + const videoRegex = /https:\/\/browndash\.com\/files[/\\]videos/; + const audioRegex = /https:\/\/browndash\.com\/files[/\\]audio/; + + if (imgRegex.test(url) || url.includes('user-images.githubusercontent.com')) { + return `\n${url}\n<img width="100%" alt="Issue asset" src=${url} />\n`; + } else if (videoRegex.test(url)) { + const videoValid = await isVideoValid(url); + if (!videoValid) return `\n${url} (This video could not be loaded)\n`; + return `\n${url}\n<video class="report-default-video" width="100%" controls alt="Issue asset" src=${url} />\n`; + } else if (audioRegex.test(url)) { + return `\n${url}\n<audio src=${url} controls />\n`; + } else { + return url; + } + }; + + const parseBody = async (body: string) => { + const imgTagRegex = /<img\b[^>]*\/?>/; + const fileRegex = /https:\/\/browndash\.com\/files/; + const parts = body.split('\n'); + + const modifiedParts = await Promise.all( + parts.map(async part => { + if (imgTagRegex.test(part)) { + return `\n${await getLinkFromTag(part)}\n`; + } else if (fileRegex.test(part)) { + const tag = await getTagFromUrl(part); + return tag; + } else { + return part; + } + }) + ); + + setIssueBody(modifiedParts.join('\n')); + }; + + React.useEffect(() => { + parseBody((issue.body as string) ?? ''); + }, [issue]); + + return ( + <div className="issue-view"> + <span className="issue-label"> + Issue{' '} + <a className="issue-link" href={issue.html_url} target="_blank"> + #{issue.number} + </a> + </span> + <h2 className="issue-title">{issue.title}</h2> + <div className="issue-date"> + Opened on {new Date(issue.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} {issue.user?.login && `by ${issue.user?.login}`} + </div> + {issue.labels.length > 0 && ( + <div> + <div className="issue-tags"> + {issue.labels.map(label => { + const labelString = typeof label === 'string' ? label : label.name ?? ''; + const colors = getLabelColors(labelString); + return <Tag key={labelString} text={labelString} backgroundColor={colors[0]} color={colors[1]} fontSize="12px" />; + })} + </div> + </div> + )} + <ReactMarkdown children={issueBody} className="issue-content" linkTarget={'_blank'} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} /> + </div> + ); +}; + +interface TagProps { + text: string; + fontSize?: string; + color?: string; + backgroundColor?: string; + borderColor?: string; + border?: boolean; + onClick?: () => void; +} + +export const Tag = ({ text, color, backgroundColor, fontSize, border, borderColor, onClick }: TagProps) => { + return ( + <div + onClick={onClick ?? (() => {})} + className="report-tag" + style={{ color: color ?? '#ffffff', backgroundColor: backgroundColor ?? '#347bff', cursor: onClick ? 'pointer' : 'auto', fontSize: fontSize ?? '10px', border: border ? '1px solid' : 'none', borderColor: borderColor ?? '#94a3b8' }}> + {text} + </div> + ); +}; |