parent
7da86f173b
commit
018a8d6f0f
5 changed files with 257 additions and 20 deletions
@ -0,0 +1,198 @@ |
|||||||
|
import { useEffect, useState } from 'react'; |
||||||
|
import { deleteUrlParam, getUrlParams } from '../../lib/browser'; |
||||||
|
import { ModalLoader } from '../UserProgress/ModalLoader'; |
||||||
|
import { Modal } from '../Modal'; |
||||||
|
import { httpGet, httpPost } from '../../lib/http'; |
||||||
|
import { |
||||||
|
submittedAlternatives, |
||||||
|
type AllowedVoteType, |
||||||
|
} from './ListProjectSolutions'; |
||||||
|
import { getRelativeTimeString } from '../../lib/date'; |
||||||
|
import { ArrowUpRight, ThumbsDown, ThumbsUp } from 'lucide-react'; |
||||||
|
import { VoteButton } from './VoteButton'; |
||||||
|
import { GitHubIcon } from '../ReactIcons/GitHubIcon'; |
||||||
|
import { isLoggedIn } from '../../lib/jwt'; |
||||||
|
import { showLoginPopup } from '../../lib/popup'; |
||||||
|
import { pageProgressMessage } from '../../stores/page'; |
||||||
|
import { useToast } from '../../hooks/use-toast'; |
||||||
|
|
||||||
|
type UserProjectSolutionResponse = { |
||||||
|
id?: string; |
||||||
|
|
||||||
|
startedAt?: Date; |
||||||
|
submittedAt?: Date; |
||||||
|
repositoryUrl?: string; |
||||||
|
|
||||||
|
upvotes?: number; |
||||||
|
downvotes?: number; |
||||||
|
|
||||||
|
voteType?: AllowedVoteType | 'none'; |
||||||
|
user: { |
||||||
|
id: string; |
||||||
|
name: string; |
||||||
|
avatar: string; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
type ProjectSolutionModalProps = { |
||||||
|
projectId: string; |
||||||
|
projectTitle: string; |
||||||
|
projectDescription: string; |
||||||
|
}; |
||||||
|
|
||||||
|
export function ProjectSolutionModal(props: ProjectSolutionModalProps) { |
||||||
|
const { projectId, projectTitle, projectDescription } = props; |
||||||
|
|
||||||
|
const { u: userId } = getUrlParams(); |
||||||
|
if (!userId) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
const toast = useToast(); |
||||||
|
const [isLoading, setIsLoading] = useState(true); |
||||||
|
const [error, setError] = useState(''); |
||||||
|
const [solution, setSolution] = useState<UserProjectSolutionResponse>(); |
||||||
|
|
||||||
|
const loadUserProjectSolution = async () => { |
||||||
|
setIsLoading(true); |
||||||
|
setError(''); |
||||||
|
|
||||||
|
const { response, error } = await httpGet<UserProjectSolutionResponse>( |
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-project-solution/${projectId}/${userId}`, |
||||||
|
); |
||||||
|
|
||||||
|
if (error || !response) { |
||||||
|
setError(error?.message || 'Something went wrong'); |
||||||
|
setIsLoading(false); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
setSolution(response); |
||||||
|
setIsLoading(false); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleSubmitVote = async ( |
||||||
|
solutionId: string, |
||||||
|
voteType: AllowedVoteType, |
||||||
|
) => { |
||||||
|
if (!isLoggedIn()) { |
||||||
|
showLoginPopup(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
pageProgressMessage.set('Submitting vote'); |
||||||
|
const { response, error } = await httpPost( |
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-vote-project/${solutionId}`, |
||||||
|
{ |
||||||
|
voteType, |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
if (error || !response) { |
||||||
|
toast.error(error?.message || 'Failed to submit vote'); |
||||||
|
pageProgressMessage.set(''); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
pageProgressMessage.set(''); |
||||||
|
setSolution((prev) => { |
||||||
|
if (!prev) { |
||||||
|
return prev; |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
...prev, |
||||||
|
upvotes: response?.upvotes || 0, |
||||||
|
downvotes: response?.downvotes || 0, |
||||||
|
voteType, |
||||||
|
}; |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
loadUserProjectSolution().finally(); |
||||||
|
}, []); |
||||||
|
|
||||||
|
if (isLoading || error) { |
||||||
|
return ( |
||||||
|
<ModalLoader |
||||||
|
text="Loading project solution..." |
||||||
|
isLoading={isLoading} |
||||||
|
error={error} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
const avatar = solution?.user.avatar; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Modal |
||||||
|
onClose={() => { |
||||||
|
deleteUrlParam('u'); |
||||||
|
window.location.reload(); |
||||||
|
}} |
||||||
|
> |
||||||
|
<div className="relative p-4"> |
||||||
|
<h1 className="text-xl font-semibold">{projectTitle}</h1> |
||||||
|
<p className="mt-1 max-w-xs text-sm text-gray-500"> |
||||||
|
{projectDescription} |
||||||
|
</p> |
||||||
|
|
||||||
|
<hr className="-mx-4 my-4 border-gray-300" /> |
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5"> |
||||||
|
<img |
||||||
|
src={ |
||||||
|
avatar |
||||||
|
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}` |
||||||
|
: '/images/default-avatar.png' |
||||||
|
} |
||||||
|
alt={solution?.user?.name} |
||||||
|
className="mr-0.5 h-7 w-7 rounded-full" |
||||||
|
/> |
||||||
|
<span className="font-medium text-black">{solution?.user.name}</span> |
||||||
|
<span className="hidden sm:inline"> |
||||||
|
{submittedAlternatives[ |
||||||
|
Math.floor(Math.random() * submittedAlternatives.length) |
||||||
|
] || 'submitted their solution'} |
||||||
|
</span>{' '} |
||||||
|
<span className="flex-grow text-right text-gray-400 sm:flex-grow-0 sm:text-left sm:font-medium sm:text-black"> |
||||||
|
{getRelativeTimeString(solution?.submittedAt!)} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="mt-4 flex items-center justify-between gap-2"> |
||||||
|
<a |
||||||
|
className="flex items-center gap-1 rounded-full border px-2 py-1 text-xs text-black transition-colors hover:border-black hover:bg-black hover:text-white" |
||||||
|
href={solution?.repositoryUrl} |
||||||
|
target="_blank" |
||||||
|
> |
||||||
|
<GitHubIcon className="h-4 w-4 text-current" /> |
||||||
|
View Solution |
||||||
|
</a> |
||||||
|
|
||||||
|
<div className="flex shrink-0 overflow-hidden rounded-full border"> |
||||||
|
<VoteButton |
||||||
|
icon={ThumbsUp} |
||||||
|
isActive={solution?.voteType === 'upvote'} |
||||||
|
count={solution?.upvotes || 0} |
||||||
|
onClick={() => { |
||||||
|
handleSubmitVote(solution?.id!, 'upvote'); |
||||||
|
}} |
||||||
|
/> |
||||||
|
|
||||||
|
<VoteButton |
||||||
|
icon={ThumbsDown} |
||||||
|
isActive={solution?.voteType === 'downvote'} |
||||||
|
count={solution?.downvotes || 0} |
||||||
|
hideCount={true} |
||||||
|
onClick={() => { |
||||||
|
handleSubmitVote(solution?.id!, 'downvote'); |
||||||
|
}} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Modal> |
||||||
|
); |
||||||
|
} |
Loading…
Reference in new issue