aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/reportManager/ReportManagerComponents.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util/reportManager/ReportManagerComponents.tsx')
-rw-r--r--src/client/util/reportManager/ReportManagerComponents.tsx153
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>
+ );
+};