parent
e39fadb032
commit
3ceab552f6
6 changed files with 206 additions and 5 deletions
@ -0,0 +1,50 @@ |
||||
import { getUser } from '../../lib/jwt'; |
||||
import { getPercentage } from '../../helper/number'; |
||||
import { ProjectProgressActions } from './ProjectProgressActions'; |
||||
import { cn } from '../../lib/classname'; |
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions'; |
||||
import { ProjectStatus } from './ProjectStatus'; |
||||
import { ThumbsUp } from 'lucide-react'; |
||||
|
||||
type ProjectProgressType = { |
||||
projectStatus: ProjectStatusDocument & { |
||||
title: string; |
||||
}; |
||||
}; |
||||
|
||||
export function ProjectProgress(props: ProjectProgressType) { |
||||
const { projectStatus } = props; |
||||
const userId = getUser()?.id; |
||||
|
||||
const shouldShowActions = |
||||
projectStatus.submittedAt && projectStatus.submittedAt !== null; |
||||
|
||||
return ( |
||||
<div className="relative"> |
||||
<a |
||||
className={cn( |
||||
'group relative flex w-full items-center justify-between overflow-hidden rounded-md border border-gray-300 bg-white px-3 py-2 pr-7 text-left text-sm transition-all hover:border-gray-400', |
||||
shouldShowActions ? '' : 'pr-3', |
||||
)} |
||||
href={`/projects/${projectStatus.projectId}`} |
||||
target="_blank" |
||||
> |
||||
<ProjectStatus projectStatus={projectStatus} /> |
||||
<span className="ml-2 flex-grow truncate">{projectStatus?.title}</span> |
||||
<span className="inline-flex items-center gap-1 text-xs text-gray-400"> |
||||
{projectStatus.upvotes} |
||||
<ThumbsUp className="size-2.5 stroke-[2.5px]" /> |
||||
</span> |
||||
</a> |
||||
|
||||
{shouldShowActions && ( |
||||
<div className="absolute right-2 top-0 flex h-full items-center"> |
||||
<ProjectProgressActions |
||||
userId={userId!} |
||||
projectId={projectStatus.projectId} |
||||
/> |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,68 @@ |
||||
import { MoreVertical, X } from 'lucide-react'; |
||||
import { useRef, useState } from 'react'; |
||||
import { useOutsideClick } from '../../hooks/use-outside-click'; |
||||
import { useKeydown } from '../../hooks/use-keydown'; |
||||
import { cn } from '../../lib/classname'; |
||||
import { useCopyText } from '../../hooks/use-copy-text'; |
||||
import { CheckIcon } from '../ReactIcons/CheckIcon'; |
||||
import { ShareIcon } from '../ReactIcons/ShareIcon'; |
||||
|
||||
type ProjectProgressActionsType = { |
||||
userId: string; |
||||
projectId: string; |
||||
}; |
||||
|
||||
export function ProjectProgressActions(props: ProjectProgressActionsType) { |
||||
const { userId, projectId } = props; |
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>(null); |
||||
const [isOpen, setIsOpen] = useState(false); |
||||
|
||||
const { copyText, isCopied } = useCopyText(); |
||||
|
||||
const projectSolutionUrl = `${import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh'}/projects/${projectId}/solutions?u=${userId}`; |
||||
|
||||
useOutsideClick(dropdownRef, () => { |
||||
setIsOpen(false); |
||||
}); |
||||
|
||||
useKeydown('Escape', () => { |
||||
setIsOpen(false); |
||||
}); |
||||
|
||||
return ( |
||||
<div className="relative h-full" ref={dropdownRef}> |
||||
<button |
||||
className="h-full text-gray-400 hover:text-gray-700" |
||||
onClick={() => setIsOpen(!isOpen)} |
||||
> |
||||
<MoreVertical size={16} /> |
||||
</button> |
||||
|
||||
{isOpen && ( |
||||
<div className="absolute right-0 top-8 z-10 w-48 overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg"> |
||||
<button |
||||
className={cn( |
||||
'flex w-full items-center gap-1.5 p-2 text-xs font-medium hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-70 sm:text-sm', |
||||
isCopied ? 'text-green-500' : 'text-gray-500 hover:text-black', |
||||
)} |
||||
onClick={() => { |
||||
copyText(projectSolutionUrl); |
||||
}} |
||||
> |
||||
{isCopied ? ( |
||||
<> |
||||
<CheckIcon additionalClasses="h-3.5 w-3.5" /> Link Copied |
||||
</> |
||||
) : ( |
||||
<> |
||||
<ShareIcon className="h-3.5 w-3.5 stroke-[2.5px]" /> Share |
||||
Solution |
||||
</> |
||||
)} |
||||
</button> |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,24 @@ |
||||
import { CircleDashed } from 'lucide-react'; |
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions'; |
||||
import { CheckIcon } from '../ReactIcons/CheckIcon'; |
||||
|
||||
type ProjectStatusType = { |
||||
projectStatus: ProjectStatusDocument & { |
||||
title: string; |
||||
}; |
||||
}; |
||||
|
||||
export function ProjectStatus(props: ProjectStatusType) { |
||||
const { projectStatus } = props; |
||||
|
||||
const { submittedAt, repositoryUrl } = projectStatus; |
||||
const status = submittedAt && repositoryUrl ? 'submitted' : 'started'; |
||||
|
||||
if (status === 'submitted') { |
||||
return <CheckIcon additionalClasses="size-3 text-gray-500 shrink-0" />; |
||||
} |
||||
|
||||
return ( |
||||
<CircleDashed className="size-3 shrink-0 stroke-[2.5px] text-gray-400" /> |
||||
); |
||||
} |
Loading…
Reference in new issue