Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified public/assets/papers/maia1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/papers/maia2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/papers/maia3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/team/arthur.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/team/ashton.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/team/daniel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/team/dmitriy.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/team/george.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/team/isaac.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/team/jon.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/team/joseph.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/team/kevin.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/team/reid.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/team/sid.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 83 additions & 0 deletions scripts/optimize-images.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env node
/**
* Resize oversized homepage source images in place.
*
* These assets (team avatars, paper-card previews) are only rendered on the
* homepage at small sizes via next/image, but the source files ship at up to
* 2673px / 1.6MB. We downscale them to ~2x their largest display size so the
* repo stays lean and the Next image optimizer has less work to do. Final
* delivery format/size is still handled by next/image at request time.
*
* Usage: node scripts/optimize-images.mjs [--dry]
*/
import { readdir, stat } from 'node:fs/promises'
import path from 'node:path'
import sharp from 'sharp'

const DRY = process.argv.includes('--dry')
const ROOT = path.resolve(import.meta.dirname, '..')

// dir -> max edge (px). 2x the largest rendered size.
const TARGETS = [
{ dir: 'public/assets/team', maxEdge: 320 },
{ dir: 'public/assets/papers', maxEdge: 900 },
]

const EXEC = { '.jpg': 'jpeg', '.jpeg': 'jpeg', '.png': 'png' }

const kb = (n) => `${(n / 1024).toFixed(0)}KB`

let beforeTotal = 0
let afterTotal = 0

for (const { dir, maxEdge } of TARGETS) {
const abs = path.join(ROOT, dir)
let files
try {
files = await readdir(abs)
} catch {
console.warn(`skip missing dir ${dir}`)
continue
}
for (const file of files) {
const ext = path.extname(file).toLowerCase()
const fmt = EXEC[ext]
if (!fmt) continue
const fp = path.join(abs, file)
const before = (await stat(fp)).size
const img = sharp(fp)
const meta = await img.metadata()
const longest = Math.max(meta.width || 0, meta.height || 0)

let pipeline = sharp(fp).rotate() // respect EXIF orientation
if (longest > maxEdge) {
pipeline = pipeline.resize({
width: meta.width >= meta.height ? maxEdge : null,
height: meta.height > meta.width ? maxEdge : null,
withoutEnlargement: true,
})
}
pipeline =
fmt === 'jpeg'
? pipeline.jpeg({ quality: 82, mozjpeg: true })
: pipeline.png({ compressionLevel: 9, palette: true })

const buf = await pipeline.toBuffer()
beforeTotal += before
// Only rewrite if we actually save bytes.
if (buf.length < before) {
afterTotal += buf.length
if (!DRY) await sharp(buf).toFile(fp)
console.log(
`${file.padEnd(16)} ${String(longest).padStart(4)}px ${kb(before).padStart(7)} -> ${kb(buf.length).padStart(7)} ${DRY ? '(dry)' : ''}`,
)
} else {
afterTotal += before
console.log(`${file.padEnd(16)} already optimal (${kb(before)})`)
}
}
}

console.log(
`\nTotal: ${kb(beforeTotal)} -> ${kb(afterTotal)} (${(((beforeTotal - afterTotal) / beforeTotal) * 100).toFixed(0)}% smaller)`,
)
9 changes: 7 additions & 2 deletions src/components/Common/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import Link from 'next/link'
import Head from 'next/head'
import dynamic from 'next/dynamic'
import { Component } from 'react'
import Chessground from '@react-chess/chessground'
import { Header } from './Header'
import { Footer } from './Footer'
import { trackErrorEncountered } from 'src/lib/analytics'

const ErrorChessground = dynamic(() => import('@react-chess/chessground'), {
ssr: false,
loading: () => <div className="h-full w-full rounded bg-glass" />,
})

interface Props {
children: React.ReactNode
}
Expand Down Expand Up @@ -109,7 +114,7 @@ export class ErrorBoundary extends Component<Props, State> {
<div className="flex flex-1 flex-col items-center justify-center gap-8 p-6">
<div className="flex max-w-2xl flex-col items-center gap-6 text-center">
<div className="h-[200px] w-[200px] opacity-75">
<Chessground
<ErrorChessground
contained
config={{
fen: 'rn1qkb1r/ppp1pBpp/5n2/4N3/8/2N5/PPPP1PPP/R1BbK2R b KQkq - 0 7',
Expand Down
2 changes: 1 addition & 1 deletion src/components/Common/FeedbackButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useContext } from 'react'
import { AuthContext } from 'src/contexts'
import { AuthContext } from 'src/contexts/AuthContext'

export const FeedbackButton: React.FC = () => {
const { user } = useContext(AuthContext)
Expand Down
8 changes: 5 additions & 3 deletions src/components/Common/PlaySetupModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
TimeControlOptionNames,
TimeControlOptions,
} from 'src/types'
import { ModalContext } from 'src/contexts'
import { ModalContext } from 'src/contexts/ModalContext'
import { ModalContainer } from './ModalContainer'

const maiaOptions = [
Expand Down Expand Up @@ -72,7 +72,7 @@ function OptionSelect<T>({
)
}

interface Props {
export interface PlaySetupModalProps {
playType: PlayType
player?: Color
timeControl?: TimeControl
Expand All @@ -89,7 +89,9 @@ interface Props {
modalSubtitle?: string
}

export const PlaySetupModal: React.FC<Props> = (props: Props) => {
export const PlaySetupModal: React.FC<PlaySetupModalProps> = (
props: PlaySetupModalProps,
) => {
const { setPlaySetupModalProps } = useContext(ModalContext)
const router = useRouter()
const { push } = router
Expand Down
9 changes: 6 additions & 3 deletions src/components/Home/AboutMaia.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Image from 'next/image'
import { motion } from 'framer-motion'
import { TeamMember } from './TeamMember'
import { useInView } from 'react-intersection-observer'
Expand Down Expand Up @@ -177,11 +178,13 @@ const PaperCard = ({
</span>
</div>
{featured && (
<div className="aspect-[4/3] w-full overflow-hidden">
<img
<div className="relative aspect-[4/3] w-full overflow-hidden">
<Image
src={`/assets/papers/${paper.image}`}
alt={`${paper.title} paper preview`}
className="h-full w-full object-cover object-top"
fill
sizes="(min-width: 768px) 33vw, 95vw"
className="object-cover object-top"
/>
</div>
)}
Expand Down
74 changes: 68 additions & 6 deletions src/components/Home/GameCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,10 @@ export const GameCarousel: React.FC = () => {
const router = useRouter()
const [games, setGames] = useState<GameData[]>(SAMPLE_GAMES)
const [isPaused, setIsPaused] = useState(false)
const [isCarouselVisible, setIsCarouselVisible] = useState(false)
const [shouldLoadLiveGames, setShouldLoadLiveGames] = useState(false)
const abortController = useRef<AbortController | null>(null)
const sectionRef = useRef<HTMLElement>(null)
const carouselRef = useRef<HTMLDivElement>(null)

const handleGameStart = useCallback((gameData: StreamedGame) => {
Expand Down Expand Up @@ -300,7 +303,7 @@ export const GameCarousel: React.FC = () => {
}, [])

useEffect(() => {
if (isPaused || !carouselRef.current) return
if (isPaused || !isCarouselVisible || !carouselRef.current) return

const scroll = () => {
if (carouselRef.current) {
Expand All @@ -317,18 +320,74 @@ export const GameCarousel: React.FC = () => {

const interval = setInterval(scroll, 20)
return () => clearInterval(interval)
}, [isPaused])
}, [isPaused, isCarouselVisible])

useEffect(() => {
fetchNewGame()
fetchBroadcast()
const section = sectionRef.current

if (!section || typeof IntersectionObserver === 'undefined') {
setIsCarouselVisible(true)
setShouldLoadLiveGames(true)
return
}

const observer = new IntersectionObserver(
([entry]) => {
setIsCarouselVisible(entry.isIntersecting)

if (entry.isIntersecting) {
setShouldLoadLiveGames(true)
}
},
{ rootMargin: '300px 0px' },
)

observer.observe(section)

return () => observer.disconnect()
}, [])

useEffect(() => {
if (!shouldLoadLiveGames) return

const startLiveFeeds = () => {
fetchNewGame()
fetchBroadcast()
}

const windowWithIdleCallback = window as Window & {
requestIdleCallback?: (
callback: IdleRequestCallback,
options?: IdleRequestOptions,
) => number
cancelIdleCallback?: (handle: number) => void
}

if (windowWithIdleCallback.requestIdleCallback) {
const idleHandle = windowWithIdleCallback.requestIdleCallback(
startLiveFeeds,
{
timeout: 2500,
},
)

return () => {
windowWithIdleCallback.cancelIdleCallback?.(idleHandle)
if (abortController.current) {
abortController.current.abort()
}
}
}

const timeoutId = window.setTimeout(startLiveFeeds, 1000)

return () => {
window.clearTimeout(timeoutId)
if (abortController.current) {
abortController.current.abort()
}
}
}, [fetchNewGame, fetchBroadcast])
}, [fetchNewGame, fetchBroadcast, shouldLoadLiveGames])

const handleGameClick = useCallback(
(game: GameData) => {
Expand All @@ -340,7 +399,10 @@ export const GameCarousel: React.FC = () => {
)

return (
<section className="relative w-full overflow-y-visible py-6">
<section
ref={sectionRef}
className="relative w-full overflow-y-visible py-6"
>
<div className="relative mb-10 w-full overflow-y-visible">
<motion.div
ref={carouselRef}
Expand Down
41 changes: 35 additions & 6 deletions src/components/Home/HomeHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {

import { PlayType } from 'src/types'
import { getGlobalStats } from 'src/api'
import { AuthContext, ModalContext } from 'src/contexts'
import { AuthContext } from 'src/contexts/AuthContext'
import { ModalContext } from 'src/contexts/ModalContext'
import { AnimatedNumber } from 'src/components/Common/AnimatedNumber'

interface Props {
Expand Down Expand Up @@ -125,17 +126,45 @@ export const HomeHero: React.FC<Props> = ({ scrollHandler }: Props) => {
// Fetch global stats and set up periodic updates
useEffect(() => {
const fetchGlobalStats = async () => {
const data = await getGlobalStats()
setGlobalStats(data)
try {
const data = await getGlobalStats()
setGlobalStats(data)
} catch (error) {
console.error('Error fetching global stats:', error)
}
}

const windowWithIdleCallback = window as Window & {
requestIdleCallback?: (
callback: IdleRequestCallback,
options?: IdleRequestOptions,
) => number
cancelIdleCallback?: (handle: number) => void
}

// Fetch immediately
fetchGlobalStats()
const idleHandle = windowWithIdleCallback.requestIdleCallback?.(
fetchGlobalStats,
{
timeout: 2000,
},
)
const timeoutId =
idleHandle === undefined
? window.setTimeout(fetchGlobalStats, 800)
: undefined

// Update every 5 minutes
const interval = setInterval(fetchGlobalStats, 5 * 60 * 1000)

return () => clearInterval(interval)
return () => {
if (idleHandle !== undefined) {
windowWithIdleCallback.cancelIdleCallback?.(idleHandle)
}
if (timeoutId !== undefined) {
window.clearTimeout(timeoutId)
}
clearInterval(interval)
}
}, [])

return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/Home/Sections/AdditionalFeaturesSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useContext, ReactNode } from 'react'
import { motion } from 'framer-motion'
import { useInView } from 'react-intersection-observer'

import { ModalContext } from 'src/contexts'
import { ModalContext } from 'src/contexts/ModalContext'
import { PlayType } from 'src/types'
import { StarIcon, BrainIcon, BotOrNotIcon } from 'src/components/Common/Icons'

Expand Down
3 changes: 2 additions & 1 deletion src/components/Home/Sections/PlaySection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import type { DrawShape } from 'chessground/draw'
import Chessground from '@react-chess/chessground'
import { useInView } from 'react-intersection-observer'
import { useContext, useState, useEffect } from 'react'
import { ModalContext, AuthContext } from 'src/contexts'
import { AuthContext } from 'src/contexts/AuthContext'
import { ModalContext } from 'src/contexts/ModalContext'

const DEMO_POSITION = {
fen: 'r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/3P1N2/PPP2PPP/RNBQK2R b KQkq - 0 1',
Expand Down
8 changes: 6 additions & 2 deletions src/components/Home/TeamMember.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Image from 'next/image'
import { motion } from 'framer-motion'
import { GithubIcon } from 'src/components/Common/Icons'
interface TeamMemberProps {
Expand Down Expand Up @@ -31,9 +32,12 @@ export const TeamMember = ({
ease: 'easeOut',
}}
>
<img
<Image
src={image}
className="h-24 w-24 rounded-full md:h-40 md:w-40"
width={160}
height={160}
sizes="(min-width: 768px) 160px, 96px"
className="h-24 w-24 rounded-full object-cover md:h-40 md:w-40"
alt={name}
/>
<div className="flex flex-col">
Expand Down
Loading
Loading