feat: project without submission (#8530)
parent
05db236a3c
commit
d36af2d3fa
10 changed files with 364 additions and 65 deletions
@ -0,0 +1,74 @@ |
||||
import { useMutation } from '@tanstack/react-query'; |
||||
import { queryClient } from '../../stores/query-client'; |
||||
import { Modal } from '../Modal'; |
||||
import { httpPost } from '../../lib/query-http'; |
||||
import { useToast } from '../../hooks/use-toast'; |
||||
import { Loader2Icon } from 'lucide-react'; |
||||
import { projectStatusOptions } from '../../queries/project'; |
||||
|
||||
type CompleteProjectConfirmationProps = { |
||||
projectId: string; |
||||
onClose: () => void; |
||||
}; |
||||
|
||||
export function CompleteProjectConfirmation( |
||||
props: CompleteProjectConfirmationProps, |
||||
) { |
||||
const { onClose, projectId } = props; |
||||
|
||||
const toast = useToast(); |
||||
|
||||
const { mutate: completeProject, isPending: isCompletingProject } = |
||||
useMutation( |
||||
{ |
||||
mutationFn: () => { |
||||
return httpPost<{ |
||||
startedAt: Date; |
||||
}>( |
||||
`${import.meta.env.PUBLIC_API_URL}/v1-mark-as-done-project/${projectId}`, |
||||
{}, |
||||
); |
||||
}, |
||||
onSettled: () => { |
||||
queryClient.invalidateQueries(projectStatusOptions(projectId)); |
||||
}, |
||||
onError: (error) => { |
||||
toast.error(error?.message || 'Failed to start project'); |
||||
}, |
||||
onSuccess: () => { |
||||
onClose(); |
||||
}, |
||||
}, |
||||
queryClient, |
||||
); |
||||
|
||||
return ( |
||||
<Modal onClose={onClose} bodyClassName="h-auto p-4"> |
||||
<h2 className="mb-2 flex items-center gap-2.5 text-xl font-semibold"> |
||||
Complete Project |
||||
</h2> |
||||
<p className="text-sm text-gray-500"> |
||||
Are you sure you want to mark this project as completed? |
||||
</p> |
||||
|
||||
<div className="mt-4 grid grid-cols-2 gap-2"> |
||||
<button |
||||
onClick={onClose} |
||||
className="rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-500 hover:bg-gray-200" |
||||
> |
||||
Cancel |
||||
</button> |
||||
<button |
||||
onClick={() => completeProject()} |
||||
className="flex h-9 items-center justify-center gap-2 rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700" |
||||
> |
||||
{isCompletingProject ? ( |
||||
<Loader2Icon className="h-4 w-4 animate-spin stroke-[2.5]" /> |
||||
) : ( |
||||
'Complete Project' |
||||
)} |
||||
</button> |
||||
</div> |
||||
</Modal> |
||||
); |
||||
} |
@ -0,0 +1,71 @@ |
||||
import { useMutation } from '@tanstack/react-query'; |
||||
import { queryClient } from '../../stores/query-client'; |
||||
import { Modal } from '../Modal'; |
||||
import { httpPost } from '../../lib/query-http'; |
||||
import { useToast } from '../../hooks/use-toast'; |
||||
import { Loader2Icon } from 'lucide-react'; |
||||
import { projectStatusOptions } from '../../queries/project'; |
||||
|
||||
type StartProjectConfirmationProps = { |
||||
projectId: string; |
||||
onClose: () => void; |
||||
}; |
||||
|
||||
export function StartProjectConfirmation(props: StartProjectConfirmationProps) { |
||||
const { onClose, projectId } = props; |
||||
|
||||
const toast = useToast(); |
||||
|
||||
const { mutate: startProject, isPending: isStartingProject } = useMutation( |
||||
{ |
||||
mutationFn: () => { |
||||
return httpPost<{ |
||||
startedAt: Date; |
||||
}>( |
||||
`${import.meta.env.PUBLIC_API_URL}/v1-start-project/${projectId}`, |
||||
{}, |
||||
); |
||||
}, |
||||
onSettled: () => { |
||||
queryClient.invalidateQueries(projectStatusOptions(projectId)); |
||||
}, |
||||
onSuccess: () => { |
||||
onClose(); |
||||
}, |
||||
onError: (error) => { |
||||
toast.error(error?.message || 'Failed to start project'); |
||||
}, |
||||
}, |
||||
queryClient, |
||||
); |
||||
|
||||
return ( |
||||
<Modal onClose={onClose} bodyClassName="h-auto p-4"> |
||||
<h2 className="mb-2 flex items-center gap-2.5 text-xl font-semibold"> |
||||
Start Project |
||||
</h2> |
||||
<p className="text-sm text-gray-500"> |
||||
Are you sure you want to start this project? |
||||
</p> |
||||
|
||||
<div className="mt-4 grid grid-cols-2 gap-2"> |
||||
<button |
||||
onClick={onClose} |
||||
className="rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-500 hover:bg-gray-200" |
||||
> |
||||
Cancel |
||||
</button> |
||||
<button |
||||
onClick={() => startProject()} |
||||
className="flex h-9 items-center justify-center gap-2 rounded-lg bg-purple-600 px-4 py-2 text-sm font-medium text-white hover:bg-purple-700" |
||||
> |
||||
{isStartingProject ? ( |
||||
<Loader2Icon className="h-4 w-4 animate-spin stroke-[2.5]" /> |
||||
) : ( |
||||
'Start Project' |
||||
)} |
||||
</button> |
||||
</div> |
||||
</Modal> |
||||
); |
||||
} |
@ -0,0 +1,123 @@ |
||||
import { CheckIcon, PlayIcon } from 'lucide-react'; |
||||
import { useEffect, useState } from 'react'; |
||||
import { StartProjectConfirmation } from '../StartProjectConfirmation'; |
||||
import { projectStatusOptions } from '../../../queries/project'; |
||||
import { queryClient } from '../../../stores/query-client'; |
||||
import { useQuery } from '@tanstack/react-query'; |
||||
import { cn } from '../../../lib/classname'; |
||||
import { isLoggedIn } from '../../../lib/jwt'; |
||||
import { showLoginPopup } from '../../../lib/popup'; |
||||
import { getRelativeTimeString } from '../../../lib/date'; |
||||
import { CompleteProjectConfirmation } from '../CompleteProjectConfirmation'; |
||||
|
||||
type ProjectTrackingActionsProps = { |
||||
projectId: string; |
||||
}; |
||||
|
||||
export function ProjectTrackingActions(props: ProjectTrackingActionsProps) { |
||||
const { projectId } = props; |
||||
|
||||
const { data: projectStatus } = useQuery( |
||||
projectStatusOptions(projectId), |
||||
queryClient, |
||||
); |
||||
|
||||
const [isLoading, setIsLoading] = useState(true); |
||||
const [isStartingProject, setIsStartingProject] = useState(false); |
||||
const [isCompletingProject, setIsCompletingProject] = useState(false); |
||||
|
||||
useEffect(() => { |
||||
if (!projectStatus) { |
||||
return; |
||||
} |
||||
|
||||
setIsLoading(false); |
||||
}, [projectStatus]); |
||||
|
||||
const { startedAt, submittedAt } = projectStatus || {}; |
||||
const formattedStartedAt = startedAt ? getRelativeTimeString(startedAt) : ''; |
||||
const formattedSubmittedAt = submittedAt |
||||
? getRelativeTimeString(submittedAt) |
||||
: ''; |
||||
const isCompleted = !!submittedAt; |
||||
|
||||
return ( |
||||
<> |
||||
{isStartingProject && ( |
||||
<StartProjectConfirmation |
||||
onClose={() => setIsStartingProject(false)} |
||||
projectId={projectId} |
||||
/> |
||||
)} |
||||
|
||||
{isCompletingProject && ( |
||||
<CompleteProjectConfirmation |
||||
onClose={() => setIsCompletingProject(false)} |
||||
projectId={projectId} |
||||
/> |
||||
)} |
||||
|
||||
{!startedAt && ( |
||||
<button |
||||
onClick={() => { |
||||
if (!isLoggedIn()) { |
||||
showLoginPopup(); |
||||
return; |
||||
} |
||||
|
||||
setIsStartingProject(true); |
||||
}} |
||||
className={cn( |
||||
'relative flex items-center gap-1.5 overflow-hidden rounded-full bg-purple-600 py-1 pr-2.5 pl-2 text-sm text-white hover:bg-purple-700', |
||||
isLoading && 'bg-white text-gray-500', |
||||
)} |
||||
disabled={isLoading} |
||||
> |
||||
<PlayIcon size={13} /> |
||||
<span>Start Working</span> |
||||
|
||||
{isLoading && ( |
||||
<div |
||||
className={cn('striped-loader absolute inset-0 z-10 bg-white')} |
||||
/> |
||||
)} |
||||
</button> |
||||
)} |
||||
|
||||
{startedAt && !isLoading && ( |
||||
<div className="flex flex-col gap-1"> |
||||
<button |
||||
onClick={() => setIsCompletingProject(true)} |
||||
className={cn( |
||||
'relative flex items-center gap-1.5 overflow-hidden rounded-full bg-green-600 py-1 pr-2.5 pl-2 text-sm text-white hover:bg-green-700', |
||||
isCompleted && |
||||
'cursor-default bg-gray-200 text-gray-500 hover:bg-gray-200', |
||||
)} |
||||
disabled={isCompleted} |
||||
> |
||||
<CheckIcon size={13} className="stroke-[2.5]" /> |
||||
{isCompleted ? ( |
||||
<span>Completed</span> |
||||
) : ( |
||||
<span>Mark as Completed</span> |
||||
)} |
||||
</button> |
||||
|
||||
<div className="text-end text-xs text-gray-500"> |
||||
{isCompleted ? ( |
||||
<> |
||||
Completed{' '} |
||||
<span className="font-medium">{formattedSubmittedAt}</span> |
||||
</> |
||||
) : ( |
||||
<> |
||||
Started working{' '} |
||||
<span className="font-medium">{formattedStartedAt}</span> |
||||
</> |
||||
)} |
||||
</div> |
||||
</div> |
||||
)} |
||||
</> |
||||
); |
||||
} |
@ -0,0 +1,26 @@ |
||||
import { queryOptions } from '@tanstack/react-query'; |
||||
import { httpGet } from '../lib/query-http'; |
||||
import { isLoggedIn } from '../lib/jwt'; |
||||
|
||||
type ProjectStatusResponse = { |
||||
id?: string; |
||||
|
||||
startedAt?: Date; |
||||
submittedAt?: Date; |
||||
repositoryUrl?: string; |
||||
|
||||
upvotes: number; |
||||
downvotes: number; |
||||
}; |
||||
|
||||
export function projectStatusOptions(projectId: string) { |
||||
return queryOptions({ |
||||
queryKey: ['project-status', projectId], |
||||
queryFn: () => { |
||||
return httpGet<ProjectStatusResponse>( |
||||
`${import.meta.env.PUBLIC_API_URL}/v1-project-status/${projectId}`, |
||||
{}, |
||||
); |
||||
}, |
||||
}); |
||||
} |
Loading…
Reference in new issue