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
2 changes: 1 addition & 1 deletion app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export default async function LangLayout({
}) {
const { lang } = await params

if (!locales.includes(lang as any)) {
if (!(locales as readonly string[]).includes(lang)) {
notFound()
}

Expand Down
2 changes: 1 addition & 1 deletion app/api/revalidate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function POST(request: NextRequest) {
if (Array.isArray(body.tags)) {
for (const tag of body.tags) {
if (typeof tag === 'string') {
revalidateTag(tag)
revalidateTag(tag, { expire: 0 })
revalidated.tags.push(tag)
}
}
Expand Down
2 changes: 1 addition & 1 deletion app/api/stats/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ export async function GET() {
}

export async function POST() {
revalidateTag('site-stats')
revalidateTag('site-stats', { expire: 0 })
return NextResponse.json({ revalidated: true })
}
16 changes: 7 additions & 9 deletions components/blog/ShareButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useEffect, useState } from 'react'
import { useSyncExternalStore } from 'react'
import { usePathname } from '@/src/i18n/navigation'
import { useTranslations } from 'next-intl'

Expand All @@ -11,17 +11,15 @@ interface ShareButtonsProps {

const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://catholicdigitalcommons.org'

const subscribeNoop = () => () => {}
const getCanShare = () => typeof navigator.share === 'function'
const getCanShareServer = () => false

export default function ShareButtons({ title, namespace = 'blog' }: ShareButtonsProps) {
const t = useTranslations(namespace)
const pathname = usePathname()
const [canShare, setCanShare] = useState(false)
const [url, setUrl] = useState('')

useEffect(() => {
const fullUrl = `${SITE_URL}${pathname}`
setUrl(fullUrl)
setCanShare(typeof navigator.share === 'function')
}, [pathname])
const canShare = useSyncExternalStore(subscribeNoop, getCanShare, getCanShareServer)
const url = `${SITE_URL}${pathname}`

const encodedUrl = encodeURIComponent(url)
const encodedTitle = encodeURIComponent(title)
Expand Down
30 changes: 23 additions & 7 deletions components/projects/RepoLanguages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type LanguageData = Record<string, Record<string, number>>

export default function RepoLanguages({ repos, label }: RepoLanguagesProps) {
const [languages, setLanguages] = useState<LanguageData | null>(null)
const [loading, setLoading] = useState(true)
const [fetchedKey, setFetchedKey] = useState<string | null>(null)

// Extract owner/repo from GitHub URLs
const repoIds = repos
Expand All @@ -55,19 +55,35 @@ export default function RepoLanguages({ repos, label }: RepoLanguagesProps) {
})
.filter((id): id is string => id !== null)

const repoKey = repoIds.join(',')
const loading = repoIds.length > 0 && fetchedKey !== repoKey

useEffect(() => {
if (repoIds.length === 0) {
setLoading(false)
// Clear stale data via microtask to satisfy react-hooks/set-state-in-effect
void Promise.resolve().then(() => {
setLanguages(null)
setFetchedKey(null)
})
return
}

fetch(`/api/github/languages?repos=${repoIds.join(',')}`)
const controller = new AbortController()
fetch(`/api/github/languages?repos=${repoIds.join(',')}`, { signal: controller.signal })
.then((res) => (res.ok ? res.json() : null))
.then((data) => setLanguages(data))
.catch(() => setLanguages(null))
.finally(() => setLoading(false))
.then((data) => {
setLanguages(data)
setFetchedKey(repoKey)
})
.catch((err) => {
if (err.name !== 'AbortError') {
setLanguages(null)
setFetchedKey(repoKey)
}
})
return () => { controller.abort() }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [repos.join(',')])
}, [repoKey])

if (loading) {
return (
Expand Down
71 changes: 37 additions & 34 deletions components/sections/FishBackground.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
'use client'

import { useEffect, useState } from 'react'

/**
* Inline SVG path data for stylized fish in various poses.
Expand Down Expand Up @@ -52,44 +49,50 @@ interface PlacedFish {
opacity: number
}

function randomBetween(min: number, max: number) {
return min + Math.random() * (max - min)
/** Simple deterministic PRNG (mulberry32) seeded from count */
function createSeededRng(seed: number) {
let s = seed | 0
return () => {
s = (s + 0x6d2b79f5) | 0
let t = Math.imul(s ^ (s >>> 15), 1 | s)
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}

export default function FishBackground({ count = 5 }: { count?: number }) {
const [fish, setFish] = useState<PlacedFish[]>([])
function seededBetween(rng: () => number, min: number, max: number) {
return min + rng() * (max - min)
}

useEffect(() => {
const placed: PlacedFish[] = []
const minDistance = 8 // minimum % distance between fish centers
export default function FishBackground({ count = 5, seed }: { count?: number; seed?: number }) {
const rng = createSeededRng(seed ?? count * 7919)
const fish: PlacedFish[] = []
const minDistance = 8 // minimum % distance between fish centers

for (let i = 0; i < count; i++) {
let candidate: PlacedFish | null = null
for (let attempt = 0; attempt < 50; attempt++) {
const x = randomBetween(5, 90)
const y = randomBetween(5, 90)
const tooClose = placed.some((p) => {
const dx = p.x - x
const dy = p.y - y
return Math.sqrt(dx * dx + dy * dy) < minDistance
})
if (!tooClose) {
candidate = {
pathIndex: Math.floor(Math.random() * fishPaths.length),
x,
y,
rotation: randomBetween(-35, 35),
size: randomBetween(80, 140),
opacity: randomBetween(0.1, 0.18),
}
break
for (let i = 0; i < count; i++) {
let candidate: PlacedFish | null = null
for (let attempt = 0; attempt < 50; attempt++) {
const x = seededBetween(rng, 5, 90)
const y = seededBetween(rng, 5, 90)
const tooClose = fish.some((p) => {
const dx = p.x - x
const dy = p.y - y
return Math.sqrt(dx * dx + dy * dy) < minDistance
})
if (!tooClose) {
candidate = {
pathIndex: Math.floor(rng() * fishPaths.length),
x,
y,
rotation: seededBetween(rng, -35, 35),
size: seededBetween(rng, 80, 140),
opacity: seededBetween(rng, 0.1, 0.18),
}
break
}
if (candidate) placed.push(candidate)
}

setFish(placed)
}, [count])
if (candidate) fish.push(candidate)
}

if (fish.length === 0) return null

Expand Down
5 changes: 4 additions & 1 deletion components/sections/HeroBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,13 @@ export default function HeroBanner({ hero }: HeroBannerProps) {

<div className={clsx('cdcf-section relative flex flex-col', alignClass)}>
{hero.heroShowLogo && (
<img
<Image
src="/logo.svg"
alt=""
width={96}
height={96}
className="mb-8 h-20 w-20 sm:h-24 sm:w-24"
unoptimized
/>
)}

Expand Down
37 changes: 21 additions & 16 deletions components/sections/ReferCommunityProjectModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export default function ReferCommunityProjectModal({ buttonLabel }: ReferCommuni
const t = useTranslations('communityProjects')
const dialogRef = useRef<HTMLDialogElement>(null)
const openedAtRef = useRef<number>(0)
const formDataRef = useRef<{ fields: Record<string, string>; tags: string[] }>({ fields: {}, tags: [] })
const [formData, setFormData] = useState<{ fields: Record<string, string>; tags: string[] }>({ fields: {}, tags: [] })
const [formKey, setFormKey] = useState(0)
const [status, setStatus] = useState<Status>('idle')
const [verificationCode, setVerificationCode] = useState('')
const [codeError, setCodeError] = useState('')
Expand All @@ -26,8 +27,9 @@ export default function ReferCommunityProjectModal({ buttonLabel }: ReferCommuni
setCodeError('')
setTags([])
setTagInput('')
setFormKey((k) => k + 1)
openedAtRef.current = Date.now()
formDataRef.current = { fields: {}, tags: [] }
setFormData({ fields: {}, tags: [] })
dialogRef.current?.showModal()
}, [])

Expand All @@ -53,7 +55,7 @@ export default function ReferCommunityProjectModal({ buttonLabel }: ReferCommuni
website: data.get('website') as string,
}

formDataRef.current = { fields, tags }
setFormData({ fields, tags })

const payload = {
...fields,
Expand Down Expand Up @@ -88,8 +90,8 @@ export default function ReferCommunityProjectModal({ buttonLabel }: ReferCommuni
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...formDataRef.current.fields,
tags: formDataRef.current.tags,
...formData.fields,
tags: formData.tags,
verification_code: verificationCode,
elapsed_ms: Date.now() - openedAtRef.current,
}),
Expand Down Expand Up @@ -127,8 +129,8 @@ export default function ReferCommunityProjectModal({ buttonLabel }: ReferCommuni
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...formDataRef.current.fields,
tags: formDataRef.current.tags,
...formData.fields,
tags: formData.tags,
elapsed_ms: Date.now() - openedAtRef.current,
}),
})
Expand All @@ -147,6 +149,9 @@ export default function ReferCommunityProjectModal({ buttonLabel }: ReferCommuni
setStatus('idle')
setVerificationCode('')
setCodeError('')
setTags(formData.tags)
setTagInput('')
setFormKey((k) => k + 1)
}

const isCodeView = status === 'awaiting_code' || status === 'submitting'
Expand Down Expand Up @@ -209,7 +214,7 @@ export default function ReferCommunityProjectModal({ buttonLabel }: ReferCommuni
</div>
<h3 className="text-lg font-semibold text-cdcf-navy">{t('checkEmailTitle')}</h3>
<p className="mt-2 text-sm text-gray-600">
{t('checkEmailMessage', { email: formDataRef.current.fields.submitter_email })}
{t('checkEmailMessage', { email: formData.fields.submitter_email })}
</p>
</div>

Expand Down Expand Up @@ -272,7 +277,7 @@ export default function ReferCommunityProjectModal({ buttonLabel }: ReferCommuni
</div>
)}

<form onSubmit={handleSendCode} className="space-y-4">
<form key={formKey} onSubmit={(e) => { void handleSendCode(e) }} className="space-y-4">
{/* Honeypot — hidden from real users */}
<div className="absolute -left-[9999px]" aria-hidden="true">
<label htmlFor="cp_website">Website</label>
Expand All @@ -294,7 +299,7 @@ export default function ReferCommunityProjectModal({ buttonLabel }: ReferCommuni
id="cp_project_name"
name="project_name"
required
defaultValue={formDataRef.current.fields.project_name}
defaultValue={formData.fields.project_name}
placeholder={t('fieldProjectNamePlaceholder')}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-cdcf-gold focus:ring-1 focus:ring-cdcf-gold focus:outline-none"
/>
Expand All @@ -309,7 +314,7 @@ export default function ReferCommunityProjectModal({ buttonLabel }: ReferCommuni
id="cp_category"
name="category"
list="cp-categories"
defaultValue={formDataRef.current.fields.category}
defaultValue={formData.fields.category}
placeholder={t('fieldCategoryPlaceholder')}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-cdcf-gold focus:ring-1 focus:ring-cdcf-gold focus:outline-none"
/>
Expand Down Expand Up @@ -373,7 +378,7 @@ export default function ReferCommunityProjectModal({ buttonLabel }: ReferCommuni
name="description"
required
rows={3}
defaultValue={formDataRef.current.fields.description}
defaultValue={formData.fields.description}
placeholder={t('fieldDescriptionPlaceholder')}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-cdcf-gold focus:ring-1 focus:ring-cdcf-gold focus:outline-none"
/>
Expand All @@ -387,7 +392,7 @@ export default function ReferCommunityProjectModal({ buttonLabel }: ReferCommuni
type="url"
id="cp_project_url"
name="project_url"
defaultValue={formDataRef.current.fields.project_url}
defaultValue={formData.fields.project_url}
placeholder={t('fieldProjectUrlPlaceholder')}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-cdcf-gold focus:ring-1 focus:ring-cdcf-gold focus:outline-none"
/>
Expand All @@ -401,7 +406,7 @@ export default function ReferCommunityProjectModal({ buttonLabel }: ReferCommuni
type="url"
id="cp_github_url"
name="github_url"
defaultValue={formDataRef.current.fields.github_url}
defaultValue={formData.fields.github_url}
placeholder={t('fieldGithubUrlPlaceholder')}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-cdcf-gold focus:ring-1 focus:ring-cdcf-gold focus:outline-none"
/>
Expand All @@ -418,7 +423,7 @@ export default function ReferCommunityProjectModal({ buttonLabel }: ReferCommuni
id="cp_submitter_name"
name="submitter_name"
required
defaultValue={formDataRef.current.fields.submitter_name}
defaultValue={formData.fields.submitter_name}
placeholder={t('fieldYourNamePlaceholder')}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-cdcf-gold focus:ring-1 focus:ring-cdcf-gold focus:outline-none"
/>
Expand All @@ -433,7 +438,7 @@ export default function ReferCommunityProjectModal({ buttonLabel }: ReferCommuni
id="cp_submitter_email"
name="submitter_email"
required
defaultValue={formDataRef.current.fields.submitter_email}
defaultValue={formData.fields.submitter_email}
placeholder={t('fieldYourEmailPlaceholder')}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-cdcf-gold focus:ring-1 focus:ring-cdcf-gold focus:outline-none"
/>
Expand Down
Loading
Loading