Skip to content
This repository was archived by the owner on Dec 4, 2025. It is now read-only.
43 changes: 37 additions & 6 deletions src/components/Judging/Admin/SuperlativeSubmissions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CardLike } from '../../Common'
import { Button } from '../../Input'
import { H1 } from '../../Typography'
import { useHackathon } from '../../../utility/HackathonProvider'
import { JUDGING_RUBRIC, calculateGrade } from '../../../utility/Constants'

const SuperlativePrize = styled.div`
${CardLike};
Expand Down Expand Up @@ -34,6 +35,37 @@ const SuperlativeSubmissions = ({ superlativePrizes }) => {
'Devpost': project.links.devpost,
'Charity choice': project.charityChoice,
}
// compute average scores per rubric and overall grade if grades exist
if (project.grades && Object.keys(project.grades).length > 0) {
const totals = {}
const gradeEntries = Object.values(project.grades)
const count = gradeEntries.length

gradeEntries.forEach(grade => {
Object.entries(grade).forEach(([key, value]) => {
if (typeof value !== 'number') return
totals[key] = (totals[key] || 0) + value
})
})

// Attach averages for each rubric item
const avgForCalc = {}
JUDGING_RUBRIC.forEach(item => {
const avg = totals[item.id] ? totals[item.id] / count : 0
projectInfo[item.id] = Number.isFinite(avg) ? avg.toFixed(2) : ''
avgForCalc[item.id] = avg
})

// Overall grade based on averaged rubric values
projectInfo['Overall grade'] = calculateGrade(avgForCalc)
} else {
// ensure rubric headers exist even if no grades present
JUDGING_RUBRIC.forEach(item => {
projectInfo[item.id] = ''
})
projectInfo['Overall grade'] = ''
}

project.teamMembers.forEach((member, index) => {
projectInfo[`Member ${index + 1} Name`] = member.name
projectInfo[`Member ${index + 1} Email`] = member.email
Expand All @@ -53,12 +85,11 @@ const SuperlativeSubmissions = ({ superlativePrizes }) => {
<SuperlativePrize key={i}>
<Accordion cursor="default" heading={prize} key={prize}>
<EntriesList>
{superlativePrizes[prize].map((submission, i) => (
<li key={i}>
{submission.title}{' '}
{submission.draftStatus === 'public' ? 'Published (Submitted)' : 'Draft Only'}
</li>
))}
{superlativePrizes[prize].map((submission, i) =>
submission && submission.draftStatus === 'public' ? (
<li key={submission.id || idx}>{submission.title}</li>
) : null
)}
</EntriesList>
<Button color="secondary" width="medium">
<LinkContainer>
Expand Down
7 changes: 5 additions & 2 deletions src/components/Judging/Admin/Table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import styled from 'styled-components'
import { JUDGING_RUBRIC } from '../../../utility/Constants'
import { Button } from '../../Input'
import { A } from '../../Typography'
import { useHackathon } from '../../../utility/HackathonProvider'

const StyledTable = styled.table`
width: 100%;
Expand Down Expand Up @@ -88,8 +89,9 @@ const ProjectGradeTitles = [
]

export const ProjectGradeTable = ({ data, onDisqualify }) => {
const { activeHackathon } = useHackathon()
const formattedData = data?.map(row => {
const projectLink = `/projects/${row.id}`
const projectLink = `/app/${activeHackathon}/projects/${row.id}`
return [
row.title,
projectLink,
Expand Down Expand Up @@ -128,8 +130,9 @@ const RemoveButton = ({ onRemove }) => {
}

export const GradeTable = ({ data, onRemove }) => {
const { activeHackathon } = useHackathon()
const formattedData = data?.map(row => {
const projectLink = `/projects/${row.id}`
const projectLink = `/app/${activeHackathon}/projects/${row.id}`
return [
row.title,
projectLink,
Expand Down
35 changes: 26 additions & 9 deletions src/components/Judging/SubmissionForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ const SubmissionForm = ({
newErrors.sourceCode = 'Please enter a valid source code URL'
}

if (links.figma && !validateURL(links.figma)) {
newErrors.figma = 'Please enter a valid URL for Figma'
}

// Validate charity selection
if (!charityChoice) {
newErrors.charity = 'Please select a charity'
Expand Down Expand Up @@ -351,6 +355,13 @@ const SubmissionForm = ({
errorMsg={errors?.youtube}
onChange={e => setLinks({ ...links, youtube: e.target.value })}
/>
<TextInputWithField
fieldName="Figma URL"
value={links?.figma}
invalid={errors?.figma}
errorMsg={errors?.figma}
onChange={e => setLinks({ ...links, figma: e.target.value })}
/>
<TextInputWithField
fieldName="Other"
value={links?.other}
Expand Down Expand Up @@ -425,18 +436,24 @@ const SubmissionForm = ({
{superlativePrizes && (
<FormSection>
<HeadingLabel>Superlative Prizes</HeadingLabel>
<P style={{ marginTop: '8px' }}>
NOTE: To be considered for "Most Accessible Design", you must include a Figma link
above.
</P>
<div>
{superlativePrizes.map(prize => {
return (
<Select
key={prize}
type="radio"
checked={superlativeSelectedPrizes.includes(prize)}
label={prize}
onChange={() =>
setSuperlativeSelectedPrizes(prev => (prev.includes(prize) ? [] : [prize]))
}
/>
<>
<Select
key={prize}
type="radio"
checked={superlativeSelectedPrizes.includes(prize)}
label={prize}
onChange={() =>
setSuperlativeSelectedPrizes(prev => (prev.includes(prize) ? [] : [prize]))
}
/>
</>
)
})}
</div>
Expand Down
14 changes: 13 additions & 1 deletion src/components/Judging/ViewProject.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ const ViewProject = ({ project, score, error, success, isSubmitting, onChange, o
const cleanedUpDevpostLink = project.links.devpost
? project.links.devpost.replace(/https?:\/\//, '')
: ''
const cleanedUpFigmaLink = project.links.figma
? project.links.figma.replace(/https?:\/\//, '')
: ''
return (
<Container>
<JudgingColumn>
Expand All @@ -128,6 +131,15 @@ const ViewProject = ({ project, score, error, success, isSubmitting, onChange, o
View source code
</StyledA>
</StyledP>
<StyledP>
{cleanedUpFigmaLink ? (
<StyledA target="_blank" rel="noreferrer noopener" href={`//${cleanedUpFigmaLink}`}>
View Figma
</StyledA>
) : (
''
)}
</StyledP>
</Card>
</JudgingColumn>
<Column>
Expand All @@ -137,7 +149,7 @@ const ViewProject = ({ project, score, error, success, isSubmitting, onChange, o
<ExternalLink
target="_blank"
rel="noreferrer noopener"
href="https://nwplus.notion.site/PUBLIC-HackCamp-2024-Peer-Judging-Rubric-23ecd01e56b04442a7dfe0c7cbabbc62"
href="https://nwplus.notion.site/PUBLIC-HackCamp-2025-Peer-Judging-Rubric-1ed14d529faa811ba7f2c17e5fa454df?pvs=74"
>
Hacker Package
</ExternalLink>
Expand Down
118 changes: 95 additions & 23 deletions src/components/Schedule/Event.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react'
import React, { useState, useEffect, useLayoutEffect, useRef } from 'react'
import styled from 'styled-components'
import { P, H3 } from '../Typography'
import { Card, ScrollbarLike } from '../Common'
Expand All @@ -15,10 +15,10 @@ const EventDescription = styled(P)`
display: -webkit-box;
-webkit-line-clamp: ${props => (props.expanded ? 'unset' : '3')};
-webkit-box-orient: vertical;
max-height: ${props => (props.expanded ? 'none' : '4.5em')};
max-height: ${props => (props.expanded ? `${props.maxHeight}px` : '4.5em')};
transition: max-height 0.3s ease;
${p => p.theme.mediaQueries.mobile} {
overflow-y: scroll;
overflow-y: auto;
${ScrollbarLike}
}
`
Expand All @@ -35,19 +35,25 @@ const Points = styled(P)`
`

const ToggleButton = styled.button`
background-image: url(${expandButton});
background-color: transparent;
background: transparent;
border: none;
position: absolute;
cursor: pointer;
width: 15px;
height: 15px;
background-size: contain;
background-repeat: no-repeat;
transform: ${props => (props.expanded ? 'rotate(180deg)' : 'rotate(0deg)')};
transition: transform 0.3s ease;
width: 28px;
height: 28px;
right: 15px;
bottom: 15px;
z-index: 3;
display: flex;
align-items: center;
justify-content: center;
& > img {
width: 15px;
height: 15px;
transform: ${props => (props.expanded ? 'rotate(180deg)' : 'rotate(0deg)')};
transition: transform 0.3s ease;
display: block;
}
${p => p.theme.mediaQueries.mobile} {
display: none;
}
Expand Down Expand Up @@ -121,19 +127,81 @@ const Event = ({ event }) => {
const descriptionRef = useRef(null)
const theme = useTheme()

useEffect(() => {
if (descriptionRef.current) {
const isOverflowing =
descriptionRef.current.scrollHeight > descriptionRef.current.clientHeight
setShowToggleButton(isOverflowing)
useLayoutEffect(() => {
const el = descriptionRef.current
if (!el) return

const clampLines = 3

const measureFullHeight = node => {
const clone = node.cloneNode(true)
// keep same wrapping so measurement matches on-card layout
clone.style.width = `${node.clientWidth}px`
clone.style.position = 'absolute'
clone.style.visibility = 'hidden'
clone.style.pointerEvents = 'none'
clone.style.maxHeight = 'none'
clone.style.webkitLineClamp = 'unset'
clone.style.display = 'block'
clone.style.boxSizing = 'border-box'
document.body.appendChild(clone)
const h = clone.scrollHeight
document.body.removeChild(clone)
return h
}
}, [descriptionRef, event.description])

const toggleExpanded = () => {
setExpanded(!expanded)
if (!expanded) {
setMaxHeight(descriptionRef.current.scrollHeight)
const checkOverflow = () => {
const style = getComputedStyle(el)
const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.2
const allowedHeight = lineHeight * clampLines
const fullHeight = measureFullHeight(el)
const isOverflowed = fullHeight >= Math.ceil(allowedHeight)
setShowToggleButton(isOverflowed)
}

// initial check on next paint
const rafId = requestAnimationFrame(checkOverflow)

// re-check when the element or its parent resizes (fonts, layout, CSS)
let ro
if (typeof ResizeObserver !== 'undefined') {
ro = new ResizeObserver(checkOverflow)
ro.observe(el)
if (el.parentElement) ro.observe(el.parentElement)
}

// re-check after fonts load (if supported)
if (document?.fonts && document.fonts.ready) {
document.fonts.ready.then(checkOverflow).catch(() => {})
}

// when expanded, measure and set the full pixel height (with a small buffer)
// to ensure the animated max-height is never slightly short of the content.
const HEIGHT_BUFFER = 6 // extra pixels to avoid 1-2px clipping on some browsers
const setMeasuredHeightWhenExpanded = () => {
if (!expanded) return
// measure via clone to avoid mutating the live node
const fullHeight = measureFullHeight(el)
// add a small buffer so rounding doesn't clip the last line
setMaxHeight(fullHeight + HEIGHT_BUFFER)
}

// set measured height initially if already expanded
const rafSetHeight = requestAnimationFrame(setMeasuredHeightWhenExpanded)

const onResize = () => checkOverflow()
window.addEventListener('resize', onResize)

return () => {
cancelAnimationFrame(rafId)
cancelAnimationFrame(rafSetHeight)
if (ro) ro.disconnect()
window.removeEventListener('resize', onResize)
}
}, [event.description, expanded])

const toggleExpanded = () => {
setExpanded(prev => !prev)
}

return (
Expand All @@ -155,11 +223,15 @@ const Event = ({ event }) => {
{formatTime(event.startTime)} - {formatTime(event.endTime)}
</TimeStamp>
<EventLocation>{event.location}</EventLocation>
<Points>{event.points && `Points: ${event.points}`}</Points>
{event.points > 0 && <Points>Points: ${event.points}</Points>}
<EventDescription ref={descriptionRef} expanded={expanded} maxHeight={maxHeight}>
{event.description}
</EventDescription>
{showToggleButton && <ToggleButton onClick={toggleExpanded} expanded={expanded} />}
{showToggleButton && (
<ToggleButton onClick={toggleExpanded} expanded={expanded} aria-label="Toggle description">
<img src={expandButton} alt="expand" />
</ToggleButton>
)}
</EventCard>
)
}
Expand Down
Loading