feat: update challenges

This commit is contained in:
jinhojang6 2023-11-14 00:31:04 +09:00
parent b6ece16698
commit 6bc894506c
9 changed files with 558 additions and 37 deletions

View File

@ -0,0 +1,116 @@
import { breakpoints, uiConfigs } from '@/configs/ui.configs'
import styled from '@emotion/styled'
import { calculatElementCount } from '../../../utils/count'
import { FilterTitle } from '../Filter'
type Props = {
data: any
activeBUs: string[]
setActiveBUs: React.Dispatch<React.SetStateAction<string[]>>
}
const ChallengeFilter = ({ data, activeBUs, setActiveBUs }: Props) => {
if (data == null) {
return <div>Something went wrong</div>
}
const businessUnits = Object.keys(data)
const toggleBU = (bu: string) => {
if (activeBUs.includes(bu)) {
setActiveBUs((prevBUs) => prevBUs.filter((item) => item !== bu))
} else {
setActiveBUs((prevBUs) => [...prevBUs, bu])
}
}
return (
<Container>
<FilterTitle title="Open Vacancies" length={calculatElementCount(data)} />
<Border />
<BUs>
<Tag active={activeBUs.length === 0} onClick={() => setActiveBUs([])}>
All Challenges
</Tag>
{businessUnits?.length ? (
businessUnits.map((bu: string) => (
<Tag
active={activeBUs.includes(bu)}
key={bu + '-tag'}
onClick={() => toggleBU(bu)}
>
{bu}
</Tag>
))
) : (
<NoChallenges>No Open Positions</NoChallenges>
)}
</BUs>
<Border />
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
width: 100%;
margin-top: calc(${uiConfigs.navbarHeight}px + 24px);
margin-bottom: 20px;
`
const BUs = styled.div`
display: flex;
overflow-x: auto;
gap: 16px;
padding: 16px 0;
&::-webkit-scrollbar {
display: none;
}
@media (max-width: ${breakpoints.md}px) {
width: calc(100vw - 32px);
margin-left: -16px;
padding: 16px;
}
`
const Border = styled.hr`
background: rgba(0, 0, 0, 0.18);
border: 0;
height: 1px;
width: 100%;
margin: 0;
`
const Tag = styled.div<{ active: boolean }>`
display: flex;
align-items: center;
justify-content: center;
background-color: ${({ active }) => (active ? 'black' : 'white')};
color: ${({ active }) => (active ? 'white' : 'black')};
font-size: 14px;
line-height: 20px;
height: 28px;
border-radius: 14px;
padding: 4px 14px;
box-sizing: border-box;
text-transform: capitalize;
cursor: pointer;
border: 1px solid black;
white-space: nowrap;
@media (max-width: ${breakpoints.md}px) {
padding: 4px 10px;
}
`
const NoChallenges = styled.p`
padding-top: 24px;
font-size: 36px;
color: black;
text-decoration: none;
`
export default ChallengeFilter

View File

@ -0,0 +1,293 @@
import { breakpoints } from '@/configs/ui.configs'
import styled from '@emotion/styled'
import { useState } from 'react'
import ArrowUpRight from '../Icons/ArrowUpRight'
type GitHubUser = {
login: string
avatarUrl: string
}
type GitHubLabel = {
name: string
}
type GitHubComment = {
id: string
author: GitHubUser
body: string
createdAt: string
}
type Challenge = {
id: string
title: string
url: string
author: GitHubUser
labels: {
nodes: GitHubLabel[]
}
commentCount: {
totalCount: number
}
commentsDetailed: {
nodes: GitHubComment[]
}
assignees: {
nodes: GitHubUser[]
}
milestone: any
createdAt: string
updatedAt: string
projectCards: {
nodes: any[] // Specify the type if project card structure is known
}
}
const ChallengeItem = ({ challenge }: { challenge: Challenge }) => {
const [open, setOpen] = useState<boolean>(false)
const handleClick = () => {
setOpen(!open)
}
return (
<ChallengeContainer>
<ChallengeHeader onClick={handleClick}>
<ChallengeTitle>{challenge.title}</ChallengeTitle>
<Toggle>
<ToggleButtonImage
src={open ? '/icons/minus.svg' : '/icons/plus.svg'}
alt={open ? 'minus' : 'plus'}
/>
</Toggle>
</ChallengeHeader>
{open && (
<Content>
<table>
<thead>
<tr>
<th>Participants</th>
<th>Assignees</th>
<th>Labels</th>
</tr>
</thead>
<tbody>
<tr>
<Participants>
{challenge.commentCount.totalCount}+
{challenge.assignees.nodes.map((assignee) => (
<img
key={assignee.login}
src={assignee.avatarUrl}
alt={assignee.login}
className="avatar"
/>
))}
</Participants>
<td>
{challenge.assignees.nodes
.map((assignee) => assignee.login)
.join(', ')}
</td>
<td>
{challenge.labels.nodes.map((label) => (
<span key={label.name} className="label">
{label.name}
</span>
))}
</td>
</tr>
<tr>
<th>Title</th>
<th>Milestone</th>
<th>Projects</th>
</tr>
<tr>
<td></td>
<td>
{challenge.milestone
? challenge.milestone.title
: 'No Milestone'}
</td>
<td>
{challenge.projectCards.nodes.length > 0
? challenge.projectCards.nodes
.map((card) => card.name)
.join(', ')
: 'None Yet'}
</td>
</tr>
</tbody>
</table>
<RewardContainer>
<div>Reward:</div>
<GithubButton href={challenge.url} target="_blank">
See on Github
<IconContainer>
<ArrowUpRight />
</IconContainer>
</GithubButton>
</RewardContainer>
</Content>
)}
</ChallengeContainer>
)
}
const ChallengeContainer = styled.div`
display: flex;
flex-direction: column;
border-bottom: 1px solid rgba(0, 0, 0, 0.18);
&:last-child {
border-bottom: none;
}
`
const ChallengeHeader = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 0;
cursor: pointer;
@media (max-width: ${breakpoints.md}px) {
margin-bottom: 24px;
}
`
const ChallengeTitle = styled.div`
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
flex: 1 0 0;
overflow: hidden;
color: #000;
text-overflow: ellipsis;
font-size: 36px;
font-weight: 400;
line-height: 42px;
overflow: hidden;
@media (max-width: ${breakpoints.md}px) {
font-size: 16px;
line-height: 130%;
}
`
const Toggle = styled.button`
cursor: pointer;
background-color: transparent;
border: none;
`
const ToggleButtonImage = styled.img`
width: 48px;
height: 48px;
@media (max-width: ${breakpoints.md}px) {
width: 18px;
height: 18px;
}
`
const Content = styled.div`
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
th {
color: rgba(0, 0, 0, 0.35);
text-transform: capitalize;
padding-top: 20px;
padding-bottom: 8px;
border-top: 1px solid rgba(0, 0, 0, 0.18) !important;
}
tr {
display: flex;
align-items: center;
}
table td,
table th {
width: calc(100% / 3);
font-size: 18px;
line-height: 22px;
}
table > tbody > tr:nth-child(2) {
margin-top: 20px;
}
table th,
table tr {
border: none;
font-weight: 400;
text-align: left;
font-size: 18px;
line-height: 130%;
}
.avatar {
border-radius: 50%;
width: 28px;
height: 28px;
}
.label {
font-size: 14px;
line-height: 20px;
padding: 4px 14px;
border-radius: 999px;
border: 1px solid rgba(0, 0, 0, 0.18);
}
`
const Participants = styled.td`
display: flex;
flex-direction: column;
gap: 8px;
`
const GithubButton = styled.a`
width: 160px;
height: 42px;
font-size: 14px;
display: flex;
align-items: center;
box-sizing: border-box;
border-radius: 2px;
background: rgba(0, 0, 0, 0.05);
position: relative;
color: black;
padding: 0 18px;
cursor: pointer;
text-decoration: none;
`
const IconContainer = styled.span`
position: absolute;
top: 7px;
right: 7px;
`
const RewardContainer = styled.div`
display: flex;
padding: 24px;
justify-content: space-between;
align-items: flex-end;
align-self: stretch;
border: 1px solid rgba(0, 0, 0, 0.18);
margin-bottom: 20px;
`
export default ChallengeItem

View File

@ -0,0 +1,117 @@
import { breakpoints } from '@/configs/ui.configs'
import styled from '@emotion/styled'
import { Box } from '../Box'
import ChallengeItem from './ChallengeItem'
interface BoardChallenges {
[key: string]: any[]
}
type Props = {
challenges: BoardChallenges
activeBUs: string[]
}
function extractOrgName(repoIdentifier: string): string {
const orgPart = repoIdentifier.split('/')[0]
return orgPart.replace(/-.*/, '')
}
const ChallengeList = ({ challenges, activeBUs }: Props) => {
if (challenges == null) {
return <div>Something went wrong</div>
}
return (
<CustomBox>
{Object.entries(challenges)
.filter(([businessUnit, _]) =>
!activeBUs?.length ? true : activeBUs.includes(businessUnit),
)
.map(([businessUnit, challengeList]) => (
<Container key={businessUnit + '-challenges'}>
<TitleContainer>
<Title>{extractOrgName(businessUnit)}</Title>
</TitleContainer>
<Challenges>
{challengeList?.length ? (
challengeList.map((challenge: any) => (
<ChallengeItem key={challenge.id} challenge={challenge} />
))
) : (
<NoChallenges>No Open Positions</NoChallenges>
)}
</Challenges>
</Container>
))}
</CustomBox>
)
}
const Container = styled.div`
display: flex;
width: 100%;
justify-content: space-between;
margin-top: 180px;
border-top: 1px solid rgba(0, 0, 0, 0.18);
@media (max-width: ${breakpoints.md}px) {
margin-top: 60px;
flex-direction: column;
}
`
const Challenges = styled.div`
width: 100%;
`
const TitleContainer = styled.div`
display: flex;
align-items: center;
gap: 16px;
width: 100%;
height: fit-content;
padding-top: 24px;
@media (max-width: ${breakpoints.md}px) {
padding-block: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.18);
}
`
const Title = styled.h3`
color: #000;
font-size: 52px;
font-weight: 400;
line-height: 60px;
text-transform: capitalize;
@media (max-width: ${breakpoints.md}px) {
font-size: 22px;
line-height: 122%;
}
`
const NoChallenges = styled.p`
padding-top: 24px;
font-size: 36px;
color: black;
text-decoration: none;
`
// const Mark = styled(Image)`
// @media (max-width: ${breakpoints.md}px) {
// display: none;
// }
// `
const CustomBox = styled(Box)`
margin-bottom: 238px;
@media (max-width: ${breakpoints.md}px) {
margin-bottom: 195px;
}
`
export default ChallengeList

View File

@ -0,0 +1,3 @@
export { default as ChallengeFilter } from './ChallengeFilter'
export { default as ChallengeItem } from './ChallengeItem'
export { default as ChallengeList } from './ChallengeList'

View File

@ -1,6 +1,6 @@
import { breakpoints, uiConfigs } from '@/configs/ui.configs'
import styled from '@emotion/styled'
import { calculateTotalJobCount } from '../../../utils/jobs'
import { calculatElementCount } from '../../../utils/count'
import { FilterTitle } from '../Filter'
import { Job } from './JobItem' // adjust path accordingly
@ -31,10 +31,7 @@ const JobFilter = ({ data, activeBUs, setActiveBUs }: Props) => {
return (
<Container>
<FilterTitle
title="Open Vacancies"
length={calculateTotalJobCount(data)}
/>
<FilterTitle title="Open Vacancies" length={calculatElementCount(data)} />
<Border />
<BUs>
<Tag active={activeBUs.length === 0} onClick={() => setActiveBUs([])}>

View File

@ -16,6 +16,7 @@ export const PortfolioItem = ({ title, mark, est, children }: Props) => {
const handleClick = () => {
setOpen(!open)
}
return (
<Container onClick={handleClick}>
<Header>

View File

@ -1,35 +1,24 @@
import { Box } from '@/components/Box'
import { ChallengeFilter, ChallengeList } from '@/components/Challenges'
import { SEO } from '@/components/SEO'
import { useState } from 'react'
import { SubPageLayout } from '../layouts/SubPageLayout'
const Page = ({ issues }: any) => {
const [activeBUs, setActiveBUs] = useState<string[]>([])
return (
<>
<SEO />
<div>
<h1>Open Issues</h1>
{Object.entries(issues).map(([repoFullName, issuesList]: any) => (
<div key={repoFullName}>
<h2>Repository: {repoFullName}</h2>
<ul>
{issuesList.map((issue: any) => (
<li key={issue.id}>
<a href={issue.url} target="_blank" rel="noopener noreferrer">
{issue.title}
</a>{' '}
- by {issue.author.login}
<div>
Labels:{' '}
{issue.labels.nodes
.map((label: any) => label.name)
.join(', ')}
</div>
<div>Comments: {issue.commentCount.totalCount}</div>
{/* Add any other issue details you want to render */}
</li>
))}
</ul>
</div>
))}
<Box>
<ChallengeFilter
data={issues}
activeBUs={activeBUs}
setActiveBUs={setActiveBUs}
/>
</Box>
<ChallengeList challenges={issues} activeBUs={activeBUs} />
</div>
</>
)

13
utils/count.ts Normal file
View File

@ -0,0 +1,13 @@
type DataObject = {
[key: string]: Array<any>
}
export const calculatElementCount = (data: DataObject): number => {
if (!data) {
return 0
}
return Object.keys(data).reduce(
(sum, element) => sum + data[element].length,
0,
)
}

View File

@ -1,8 +0,0 @@
import { BoardJobs } from '@/components/Jobs/JobFilter'
export const calculateTotalJobCount = (units: BoardJobs): number => {
if (!units) {
return 0
}
return Object.keys(units).reduce((sum, unit) => sum + units[unit].length, 0)
}