@ -13,7 +13,8 @@ import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup' ;
import { showLoginPopup } from '../../lib/popup' ;
import { VoteButton } from './VoteButton.tsx' ;
import { VoteButton } from './VoteButton.tsx' ;
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx' ;
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx' ;
import { cn } from '../../lib/classname.ts' ;
import { SelectLanguages } from './SelectLanguages.tsx' ;
import type { ProjectFrontmatter } from '../../lib/project.ts' ;
export interface ProjectStatusDocument {
export interface ProjectStatusDocument {
_id? : string ;
_id? : string ;
@ -24,6 +25,7 @@ export interface ProjectStatusDocument {
startedAt? : Date ;
startedAt? : Date ;
submittedAt? : Date ;
submittedAt? : Date ;
repositoryUrl? : string ;
repositoryUrl? : string ;
languages? : string [ ] ;
upvotes : number ;
upvotes : number ;
downvotes : number ;
downvotes : number ;
@ -53,15 +55,16 @@ type ListProjectSolutionsResponse = {
type QueryParams = {
type QueryParams = {
p? : string ;
p? : string ;
l? : string ;
} ;
} ;
type PageState = {
type PageState = {
currentPage : number ;
currentPage : number ;
language : string ;
} ;
} ;
const VISITED_SOLUTIONS_KEY = 'visited-project-solutions' ;
type ListProjectSolutionsProps = {
type ListProjectSolutionsProps = {
project : ProjectFrontmatter ;
projectId : string ;
projectId : string ;
} ;
} ;
@ -90,27 +93,26 @@ const submittedAlternatives = [
] ;
] ;
export function ListProjectSolutions ( props : ListProjectSolutionsProps ) {
export function ListProjectSolutions ( props : ListProjectSolutionsProps ) {
const { projectId } = props ;
const { projectId , project : projectData } = props ;
const toast = useToast ( ) ;
const toast = useToast ( ) ;
const [ pageState , setPageState ] = useState < PageState > ( {
const [ pageState , setPageState ] = useState < PageState > ( {
currentPage : 0 ,
currentPage : 0 ,
language : '' ,
} ) ;
} ) ;
const [ isLoading , setIsLoading ] = useState ( true ) ;
const [ isLoading , setIsLoading ] = useState ( true ) ;
const [ solutions , setSolutions ] = useState < ListProjectSolutionsResponse > ( ) ;
const [ solutions , setSolutions ] = useState < ListProjectSolutionsResponse > ( ) ;
const [ alreadyVisitedSolutions , setAlreadyVisitedSolutions ] = useState <
Record < string , boolean >
> ( { } ) ;
const [ showLeavingRoadmapModal , setShowLeavingRoadmapModal ] = useState <
const [ showLeavingRoadmapModal , setShowLeavingRoadmapModal ] = useState <
ListProjectSolutionsResponse [ 'data' ] [ number ] | null
ListProjectSolutionsResponse [ 'data' ] [ number ] | null
> ( null ) ;
> ( null ) ;
const loadSolutions = async ( page = 1 ) = > {
const loadSolutions = async ( page = 1 , language : string = '' ) = > {
const { response , error } = await httpGet < ListProjectSolutionsResponse > (
const { response , error } = await httpGet < ListProjectSolutionsResponse > (
` ${ import . meta . env . PUBLIC_API_URL } /v1-list-project-solutions/ ${ projectId } ` ,
` ${ import . meta . env . PUBLIC_API_URL } /v1-list-project-solutions/ ${ projectId } ` ,
{
{
currPage : page ,
currPage : page ,
. . . ( language ? { languages : language } : { } ) ,
} ,
} ,
) ;
) ;
@ -132,7 +134,7 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
return ;
return ;
}
}
pageProgressMessage . set ( 'Submitting vote... ' ) ;
pageProgressMessage . set ( 'Submitting vote' ) ;
const { response , error } = await httpPost (
const { response , error } = await httpPost (
` ${ import . meta . env . PUBLIC_API_URL } /v1-vote-project/ ${ solutionId } ` ,
` ${ import . meta . env . PUBLIC_API_URL } /v1-vote-project/ ${ solutionId } ` ,
{
{
@ -172,13 +174,9 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
useEffect ( ( ) = > {
useEffect ( ( ) = > {
const queryParams = getUrlParams ( ) as QueryParams ;
const queryParams = getUrlParams ( ) as QueryParams ;
const alreadyVisitedSolutions = JSON . parse (
localStorage . getItem ( VISITED_SOLUTIONS_KEY ) || '{}' ,
) ;
setAlreadyVisitedSolutions ( alreadyVisitedSolutions ) ;
setPageState ( {
setPageState ( {
currentPage : + ( queryParams . p || '1' ) ,
currentPage : + ( queryParams . p || '1' ) ,
language : queryParams.l || '' ,
} ) ;
} ) ;
} , [ ] ) ;
} , [ ] ) ;
@ -188,23 +186,21 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
return ;
return ;
}
}
if ( pageState . currentPage !== 1 ) {
if ( pageState . currentPage !== 1 || pageState . language !== '' ) {
setUrlParams ( {
setUrlParams ( {
p : String ( pageState . currentPage ) ,
p : String ( pageState . currentPage ) ,
l : pageState.language ,
} ) ;
} ) ;
} else {
} else {
deleteUrlParam ( 'p' ) ;
deleteUrlParam ( 'p' ) ;
deleteUrlParam ( 'l' ) ;
}
}
loadSolutions ( pageState . currentPage ) . finally ( ( ) = > {
loadSolutions ( pageState . currentPage , pageState . language ) . finally ( ( ) = > {
setIsLoading ( false ) ;
setIsLoading ( false ) ;
} ) ;
} ) ;
} , [ pageState ] ) ;
} , [ pageState ] ) ;
if ( isLoading ) {
return < LoadingSolutions / > ;
}
const isEmpty = solutions ? . data . length === 0 ;
const isEmpty = solutions ? . data . length === 0 ;
if ( isEmpty ) {
if ( isEmpty ) {
return < EmptySolutions projectId = { projectId } / > ;
return < EmptySolutions projectId = { projectId } / > ;
@ -213,116 +209,128 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
const leavingRoadmapModal = showLeavingRoadmapModal ? (
const leavingRoadmapModal = showLeavingRoadmapModal ? (
< LeavingRoadmapWarningModal
< LeavingRoadmapWarningModal
onClose = { ( ) = > setShowLeavingRoadmapModal ( null ) }
onClose = { ( ) = > setShowLeavingRoadmapModal ( null ) }
onContinue = { ( ) = > {
repositoryUrl = { showLeavingRoadmapModal ? . repositoryUrl ! }
const visitedSolutions = {
. . . alreadyVisitedSolutions ,
[ showLeavingRoadmapModal . _id ! ] : true ,
} ;
localStorage . setItem (
VISITED_SOLUTIONS_KEY ,
JSON . stringify ( visitedSolutions ) ,
) ;
window . open ( showLeavingRoadmapModal . repositoryUrl , '_blank' ) ;
} }
/ >
/ >
) : null ;
) : null ;
const selectedLanguage = pageState . language ;
return (
return (
< section >
< div className = "mb-4 overflow-hidden rounded-lg border bg-white p-3 sm:p-5" >
{ leavingRoadmapModal }
{ leavingRoadmapModal }
< div className = "relative mb-5 hidden items-center justify-between sm:flex" >
< div >
< h1 className = "mb-1 text-xl font-semibold" >
{ projectData . title } Solutions
< / h1 >
< p className = "text-sm text-gray-500" > { projectData . description } < / p >
< / div >
{ ! isLoading && (
< SelectLanguages
projectId = { projectId }
selectedLanguage = { selectedLanguage }
onSelectLanguage = { ( language ) = > {
setPageState ( ( prev ) = > ( {
. . . prev ,
language : prev.language === language ? '' : language ,
} ) ) ;
} }
/ >
) }
< / div >
< div className = "flex min-h-[500px] flex-col divide-y divide-gray-100" >
{ isLoading ? (
{ solutions ? . data . map ( ( solution , counter ) = > {
< LoadingSolutions / >
const isVisited = alreadyVisitedSolutions [ solution . _id ! ] ;
) : (
const avatar = solution . user . avatar || '' ;
< >
< div className = "flex min-h-[500px] flex-col divide-y divide-gray-100" >
return (
{ solutions ? . data . map ( ( solution , counter ) = > {
< div
const avatar = solution . user . avatar || '' ;
key = { solution . _id }
return (
className = {
< div
'flex flex-col justify-between gap-2 py-2 text-sm text-gray-500 sm:flex-row sm:items-center sm:gap-0'
key = { solution . _id }
}
className = "flex flex-col gap-2 py-2 text-sm text-gray-500"
>
< 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 [
counter % 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 = "flex items-center justify-end gap-1" >
< span className = "flex 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' ) ;
} }
/ >
< / span >
< a
className = "ml-1 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"
onClick = { ( e ) = > {
e . preventDefault ( ) ;
setShowLeavingRoadmapModal ( solution ) ;
} }
target = "_blank"
href = { solution . repositoryUrl }
>
>
< GitHubIcon className = "h-4 w-4 text-current" / >
< div className = "flex flex-col justify-between gap-2 text-sm text-gray-500 sm:flex-row sm:items-center sm:gap-0" >
Visit Solution
< div className = "flex items-center gap-1.5" >
< / a >
< img
< / div >
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 [
counter % 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 = "flex items-center justify-end gap-1" >
< span 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' ) ;
} }
/ >
< / span >
< button
className = "ml-1 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"
onClick = { ( ) = > {
setShowLeavingRoadmapModal ( solution ) ;
} }
>
< GitHubIcon className = "h-4 w-4 text-current" / >
Visit Solution
< / button >
< / div >
< / div >
< / div >
) ;
} ) }
< / div >
{ ( solutions ? . totalPages || 0 ) > 1 && (
< div className = "mt-4" >
< Pagination
totalPages = { solutions ? . totalPages || 1 }
currPage = { solutions ? . currPage || 1 }
perPage = { solutions ? . perPage || 21 }
totalCount = { solutions ? . totalCount || 0 }
onPageChange = { ( page ) = > {
setPageState ( {
. . . pageState ,
currentPage : page ,
} ) ;
} }
/ >
< / div >
< / div >
) ;
) }
} ) }
< / >
< / div >
{ ( solutions ? . totalPages || 0 ) > 1 && (
< div className = "mt-4" >
< Pagination
totalPages = { solutions ? . totalPages || 1 }
currPage = { solutions ? . currPage || 1 }
perPage = { solutions ? . perPage || 21 }
totalCount = { solutions ? . totalCount || 0 }
onPageChange = { ( page ) = > {
setPageState ( {
. . . pageState ,
currentPage : page ,
} ) ;
} }
/ >
< / div >
) }
) }
< / section >
< / div >
) ;
) ;
}
}