bolt.diy/app/routes/problem.$id.tsx
2025-02-21 09:00:23 -08:00

206 lines
6.8 KiB
TypeScript

import { ClientOnly } from 'remix-utils/client-only';
import { Header } from '~/components/header/Header';
import { Menu } from '~/components/sidebar/Menu.client';
import BackgroundRays from '~/components/ui/BackgroundRays';
import { TooltipProvider } from '@radix-ui/react-tooltip';
import { ToastContainerWrapper, Status, Keywords } from './problems';
import { toast } from 'react-toastify';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { useParams } from '@remix-run/react';
import { getProblem, updateProblem as backendUpdateProblem, getProblemsUsername, BoltProblemStatus, getNutIsAdmin } from '~/lib/replay/Problems';
import type { BoltProblem, BoltProblemComment } from '~/lib/replay/Problems';
function Comments({ comments }: { comments: BoltProblemComment[] }) {
return (
<div className="comments">
{comments.map((comment, index) => (
<div key={index} className="comment">
<div className="comment-header">
<span className="comment-username">{comment.username ?? ""}</span>
<span className="comment-date">
{new Date(comment.timestamp).toLocaleString()}
</span>
</div>
<div className="comment-text">{comment.content}</div>
</div>
))}
</div>
);
}
function ProblemViewer({ problem }: { problem: BoltProblem }) {
const { problemId, title, description, status = BoltProblemStatus.Pending, keywords = [], comments = [] } = problem;
return (
<div className="benchmark">
<h1 className="text-xl4 font-semibold mb-2">{title}</h1>
<p>{description}</p>
<a
href={`/load-problem/${problemId}`}
className="load-button inline-block px-4 py-2 mt-3 mb-3 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors duration-200 font-medium"
>
Load Problem
</a>
<Status status={status} />
<Keywords keywords={keywords} />
<Comments comments={comments} />
</div>
)
}
interface UpdateProblemFormProps {
handleSubmit: (content: string) => void;
updateText: string;
placeholder: string;
}
function UpdateProblemForm(props: UpdateProblemFormProps) {
const { handleSubmit, updateText, placeholder } = props;
const [value, setValue] = useState("");
const onSubmitClicked = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (value.trim()) {
handleSubmit(value)
setValue('')
}
}
return (
<form onSubmit={onSubmitClicked} className="mb-6 p-4 bg-bolt-elements-background-depth-2 rounded-lg">
<textarea
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={placeholder}
rows={4}
className="w-full p-3 mb-3 bg-bolt-elements-background-depth-3 rounded-md border border-bolt-elements-background-depth-4 text-black placeholder-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y min-h-[100px]"
required
/>
<button
type="submit"
disabled={!value.trim()}
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
>
{updateText}
</button>
</form>
)
}
type DoUpdateCallback = (problem: BoltProblem) => BoltProblem;
type UpdateProblemCallback = (doUpdate: DoUpdateCallback) => void;
function UpdateProblemForms({ updateProblem }: { updateProblem: UpdateProblemCallback }) {
const handleAddComment = (content: string) => {
const newComment: BoltProblemComment = {
timestamp: Date.now(),
username: getProblemsUsername(),
content,
}
updateProblem(problem => {
const comments = [...(problem.comments || []), newComment];
return {
...problem,
comments,
};
});
}
const handleSetTitle = (title: string) => {
updateProblem(problem => ({
...problem,
title,
}));
}
const handleSetDescription = (description: string) => {
updateProblem(problem => ({
...problem,
description,
}));
}
const handleSetStatus = (status: string) => {
const statusEnum = BoltProblemStatus[status as keyof typeof BoltProblemStatus];
if (!statusEnum) {
toast.error('Invalid status');
return;
}
updateProblem(problem => ({
...problem,
status: statusEnum,
}));
}
const handleSetKeywords = (keywordString: string) => {
const keywords = keywordString.split(' ').map(keyword => keyword.trim()).filter(keyword => keyword.length > 0);
updateProblem(problem => ({
...problem,
keywords,
}));
}
return (
<>
<UpdateProblemForm handleSubmit={handleAddComment} updateText="Add Comment" placeholder="Add a comment..." />
<UpdateProblemForm handleSubmit={handleSetTitle} updateText="Set Title" placeholder="Set the title of the problem..." />
<UpdateProblemForm handleSubmit={handleSetDescription} updateText="Set Description" placeholder="Set the description of the problem..." />
<UpdateProblemForm handleSubmit={handleSetStatus} updateText="Set Status" placeholder="Set the status of the problem..." />
<UpdateProblemForm handleSubmit={handleSetKeywords} updateText="Set Keywords" placeholder="Set the keywords of the problem..." />
</>
)
}
const Nothing = () => null;
function ViewProblemPage() {
const params = useParams();
const problemId = params.id;
if (typeof problemId !== 'string') {
throw new Error('Problem ID is required');
}
const [problemData, setProblemData] = useState<BoltProblem | null>(null);
const updateProblem = useCallback(async (callback: DoUpdateCallback) => {
if (!problemData) {
toast.error('Problem data missing');
return;
}
const newProblem = callback(problemData);
setProblemData(newProblem);
console.log("BackendUpdateProblem", problemId, newProblem);
await backendUpdateProblem(problemId, newProblem);
}, [problemData]);
useEffect(() => {
getProblem(problemId).then(setProblemData);
}, [problemId]);
return (
<Suspense fallback={<Nothing />}>
<TooltipProvider>
<div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1">
<BackgroundRays />
<Header />
<ClientOnly>{() => <Menu />}</ClientOnly>
<div className="p-6">
{problemData === null
? (<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
</div>)
: <ProblemViewer problem={problemData} />}
</div>
{getNutIsAdmin() && problemData && (
<UpdateProblemForms updateProblem={updateProblem} />
)}
<ToastContainerWrapper />
</div>
</TooltipProvider>
</Suspense>
);
}
export default ViewProblemPage;