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