mirror of
https://github.com/acid-info/free.technology.git
synced 2025-02-20 13:38:33 +00:00
feat: update challenges
This commit is contained in:
parent
b6ece16698
commit
6bc894506c
116
src/components/Challenges/ChallengeFilter.tsx
Normal file
116
src/components/Challenges/ChallengeFilter.tsx
Normal 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
|
293
src/components/Challenges/ChallengeItem.tsx
Normal file
293
src/components/Challenges/ChallengeItem.tsx
Normal 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
|
117
src/components/Challenges/ChallengeList.tsx
Normal file
117
src/components/Challenges/ChallengeList.tsx
Normal 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
|
3
src/components/Challenges/index.ts
Normal file
3
src/components/Challenges/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as ChallengeFilter } from './ChallengeFilter'
|
||||
export { default as ChallengeItem } from './ChallengeItem'
|
||||
export { default as ChallengeList } from './ChallengeList'
|
@ -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([])}>
|
||||
|
@ -16,6 +16,7 @@ export const PortfolioItem = ({ title, mark, est, children }: Props) => {
|
||||
const handleClick = () => {
|
||||
setOpen(!open)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container onClick={handleClick}>
|
||||
<Header>
|
||||
|
@ -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
13
utils/count.ts
Normal 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,
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user