-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ff6cd3f
commit fbac2bc
Showing
102 changed files
with
3,776 additions
and
567 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
export type Styles = { | ||
subtitle: string; | ||
title: string; | ||
}; | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import type { PropsWithChildren } from 'react'; | ||
import styles from './style.module.scss'; | ||
|
||
const Carousel = ({ children }: PropsWithChildren) => { | ||
return <div className={styles.slider}>{children}</div>; | ||
}; | ||
|
||
export default Carousel; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
@use 'src/styles/vars.scss' as vars; | ||
|
||
.slider { | ||
display: flex; | ||
gap: 2rem; | ||
margin: 0 -1rem; | ||
overflow-x: auto; | ||
padding: 1rem; | ||
scrollbar-color: var(--theme-primary-2) var(--theme-primary-1); | ||
|
||
&::-webkit-scrollbar { | ||
background-color: var(--theme-primary-1); | ||
border: 1rem solid var(--theme-background); | ||
border-radius: 3rem; | ||
height: 2.75rem; | ||
} | ||
|
||
&::-webkit-scrollbar-thumb { | ||
background-color: var(--theme-primary-2); | ||
border: 1rem solid var(--theme-background); | ||
border-radius: 3rem; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export type Styles = { | ||
slider: string; | ||
}; | ||
|
||
export type ClassNames = keyof Styles; | ||
|
||
declare const styles: Styles; | ||
|
||
export default styles; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,29 +1,22 @@ | ||
import { communityLogos } from '@/lib/constants/communityLogos'; | ||
import { Community } from '@/lib/types/enums'; | ||
import { capitalize } from '@/lib/utils'; | ||
import Image from 'next/image'; | ||
|
||
import AILogo from '@/public/assets/acm-logos/communities/ai.png'; | ||
import CyberLogo from '@/public/assets/acm-logos/communities/cyber.png'; | ||
import DesignLogo from '@/public/assets/acm-logos/communities/design.png'; | ||
import HackLogo from '@/public/assets/acm-logos/communities/hack.png'; | ||
import ACMLogo from '@/public/assets/acm-logos/general/light-mode.png'; | ||
|
||
interface CommunityLogoProps { | ||
community: string; | ||
size: number; | ||
} | ||
|
||
const CommunityLogo = ({ community, size }: CommunityLogoProps) => { | ||
switch (community.toLowerCase()) { | ||
case 'hack': | ||
return <Image src={HackLogo} width={size} alt="ACM Hack Logo" />; | ||
case 'ai': | ||
return <Image src={AILogo} width={size} alt="ACM AI Logo" />; | ||
case 'cyber': | ||
return <Image src={CyberLogo} width={size} alt="ACM Cyber Logo" />; | ||
case 'design': | ||
return <Image src={DesignLogo} width={size} alt="ACM Design Logo" />; | ||
default: | ||
return <Image src={ACMLogo} width={size} alt="ACM General Logo" />; | ||
} | ||
const formattedName = capitalize(community) as Community; | ||
|
||
if (!Object.values(Community).includes(formattedName)) | ||
return <Image src={communityLogos.General} width={size} alt="ACM General Logo" />; | ||
|
||
return ( | ||
<Image src={communityLogos[formattedName]} width={size} alt={`ACM ${formattedName} Logo`} /> | ||
); | ||
}; | ||
|
||
export default CommunityLogo; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
import Modal from '@/components/common/Modal'; | ||
import { useObjectUrl } from '@/lib/utils'; | ||
import Image from 'next/image'; | ||
import { PointerEvent, useCallback, useRef, useState } from 'react'; | ||
import styles from './style.module.scss'; | ||
|
||
/** Height of the preview square. */ | ||
const HEIGHT = 200; | ||
|
||
/** Promisified version of `HTMLCanvasElement.toBlob` */ | ||
function toBlob(canvas: HTMLCanvasElement, type?: string, quality?: number): Promise<Blob | null> { | ||
return new Promise(resolve => { | ||
canvas.toBlob(resolve, type, quality); | ||
}); | ||
} | ||
|
||
type DragState = { | ||
pointerId: number; | ||
offsetX: number; | ||
offsetY: number; | ||
initLeft: number; | ||
initTop: number; | ||
}; | ||
|
||
interface CropperProps { | ||
file: Blob | null; | ||
aspectRatio: number; | ||
circle?: boolean; | ||
maxFileHeight: number; | ||
maxSize?: number; | ||
onCrop: (file: Blob) => void; | ||
onClose: (reason: 'invalid-image' | 'cannot-compress' | null) => void; | ||
} | ||
|
||
const Cropper = ({ | ||
file, | ||
aspectRatio, | ||
circle, | ||
maxFileHeight, | ||
maxSize = Infinity, | ||
onCrop, | ||
onClose, | ||
}: CropperProps) => { | ||
const WIDTH = HEIGHT * aspectRatio; | ||
|
||
const url = useObjectUrl(file); | ||
const image = useRef<HTMLImageElement>(null); | ||
const [loaded, setLoaded] = useState(file); | ||
const [left, setLeft] = useState(0); | ||
const [top, setTop] = useState(0); | ||
const [width, setWidth] = useState(0); | ||
const [height, setHeight] = useState(0); | ||
const [scale, setScale] = useState(1); | ||
const dragState = useRef<DragState | null>(null); | ||
|
||
const handlePointerEnd = (e: PointerEvent<HTMLElement>) => { | ||
if (dragState.current?.pointerId === e.pointerId) { | ||
dragState.current = null; | ||
} | ||
}; | ||
// onError callback needs to be memoized lest next/image reload the image | ||
const handleImageError = useCallback(() => { | ||
if (file !== null) { | ||
onClose('invalid-image'); | ||
} | ||
}, [file, onClose]); | ||
|
||
return ( | ||
<Modal title="Edit image" open={loaded === file && file !== null} onClose={() => onClose(null)}> | ||
<div | ||
className={styles.cropWrapper} | ||
onPointerDown={e => { | ||
if (!dragState.current) { | ||
dragState.current = { | ||
pointerId: e.pointerId, | ||
offsetX: e.clientX, | ||
offsetY: e.clientY, | ||
initLeft: left, | ||
initTop: top, | ||
}; | ||
e.currentTarget.setPointerCapture(e.pointerId); | ||
} | ||
}} | ||
onPointerMove={e => { | ||
const state = dragState.current; | ||
if (state?.pointerId === e.pointerId) { | ||
const left = state.initLeft + e.clientX - state.offsetX; | ||
const top = state.initTop + e.clientY - state.offsetY; | ||
setLeft(Math.max(Math.min(left, 0), WIDTH - width * scale)); | ||
setTop(Math.max(Math.min(top, 0), HEIGHT - height * scale)); | ||
} | ||
}} | ||
onPointerUp={handlePointerEnd} | ||
onPointerCancel={handlePointerEnd} | ||
> | ||
{url && ( | ||
<Image | ||
src={url} | ||
alt="Selected file" | ||
className={styles.image} | ||
style={{ transform: `translate(${left}px, ${top}px)` }} | ||
width={width * scale} | ||
height={height * scale} | ||
onLoad={e => { | ||
if (e.currentTarget.naturalHeight > 0) { | ||
const ratio = e.currentTarget.naturalWidth / e.currentTarget.naturalHeight; | ||
const height = Math.max(HEIGHT, WIDTH / ratio); | ||
const width = height * ratio; | ||
setWidth(width); | ||
setHeight(height); | ||
// Default to centering the image | ||
setLeft((WIDTH - width) / 2); | ||
setTop((HEIGHT - height) / 2); | ||
setScale(1); | ||
setLoaded(file); | ||
} | ||
}} | ||
onError={handleImageError} | ||
draggable={false} | ||
ref={image} | ||
/> | ||
)} | ||
<div | ||
className={`${styles.frame} ${circle ? styles.circle : ''}`} | ||
style={{ aspectRatio: `${aspectRatio}` }} | ||
/> | ||
</div> | ||
<div className={styles.controls}> | ||
<label className={styles.zoomWrapper}> | ||
Zoom: | ||
<input | ||
type="range" | ||
className={styles.zoom} | ||
min={1} | ||
max={2} | ||
step="any" | ||
value={scale} | ||
onChange={e => { | ||
const newScale = +e.currentTarget.value; | ||
const newLeft = WIDTH / 2 - ((WIDTH / 2 - left) / scale) * newScale; | ||
const newTop = HEIGHT / 2 - ((HEIGHT / 2 - top) / scale) * newScale; | ||
setScale(newScale); | ||
setLeft(Math.max(Math.min(newLeft, 0), WIDTH - width * newScale)); | ||
setTop(Math.max(Math.min(newTop, 0), HEIGHT - height * newScale)); | ||
}} | ||
/> | ||
</label> | ||
<button | ||
type="submit" | ||
className={styles.upload} | ||
onClick={async () => { | ||
const canvas = document.createElement('canvas'); | ||
const context = canvas.getContext('2d'); | ||
if (!context || !image.current) { | ||
return; | ||
} | ||
const sourceHeight = image.current.naturalHeight * (HEIGHT / (height * scale)); | ||
const fileHeight = Math.min(maxFileHeight, sourceHeight); | ||
canvas.width = fileHeight * aspectRatio; | ||
canvas.height = fileHeight; | ||
context.drawImage( | ||
image.current, | ||
image.current.naturalWidth * (-left / (width * scale)), | ||
image.current.naturalHeight * (-top / (height * scale)), | ||
sourceHeight * aspectRatio, | ||
sourceHeight, | ||
0, | ||
0, | ||
fileHeight * aspectRatio, | ||
fileHeight | ||
); | ||
const blob = await toBlob(canvas); | ||
if (blob && blob.size <= maxSize) { | ||
onCrop(blob); | ||
return; | ||
} | ||
// Try compressing as JPG with various qualities, in parallel due to | ||
// eslint(no-await-in-loop) | ||
const blobs = await Promise.all( | ||
[1, 0.9, 0.7, 0.4, 0].map(quality => toBlob(canvas, 'image/jpeg', quality)) | ||
); | ||
const firstSmallEnough = blobs.find(blob => blob && blob.size <= maxSize); | ||
if (firstSmallEnough) { | ||
onCrop(firstSmallEnough); | ||
} else { | ||
onClose('cannot-compress'); | ||
} | ||
}} | ||
> | ||
Apply | ||
</button> | ||
</div> | ||
</Modal> | ||
); | ||
}; | ||
|
||
export default Cropper; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
@use 'sass:math'; | ||
@use 'src/styles/vars.scss' as vars; | ||
|
||
$margin: 40px; | ||
$height: 200px; | ||
|
||
.cropWrapper { | ||
border-radius: 1rem; | ||
cursor: grab; | ||
overflow: hidden; | ||
position: relative; | ||
touch-action: none; | ||
user-select: none; | ||
z-index: 0; | ||
|
||
.image { | ||
margin: $margin; | ||
position: absolute; | ||
z-index: -1; | ||
} | ||
|
||
.frame { | ||
border-radius: 0.5rem; | ||
box-shadow: 0 0 0 3px var(--theme-text-on-background-1), | ||
0 0 0 (($height + $margin) * sqrt(0.5)) var(--theme-accent-line-1-transparent); | ||
height: $height; | ||
margin: $margin; | ||
|
||
&.circle { | ||
border-radius: 50%; | ||
} | ||
} | ||
} | ||
|
||
.controls { | ||
align-items: center; | ||
display: flex; | ||
gap: 1rem; | ||
margin-top: 1rem; | ||
|
||
.zoomWrapper { | ||
align-items: center; | ||
display: flex; | ||
flex: auto; | ||
|
||
.zoom { | ||
flex: auto; | ||
width: 0; | ||
} | ||
} | ||
|
||
.upload { | ||
background-color: var(--theme-primary-2); | ||
border-radius: 0.5rem; | ||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25); | ||
color: var(--theme-background); | ||
font-size: 1rem; | ||
font-weight: bold; | ||
height: 2.5rem; | ||
width: 6.5rem; | ||
} | ||
} |
Oops, something went wrong.