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
123 changes: 123 additions & 0 deletions components/SpotlightCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { formatDate } from 'pliny/utils/formatDate'
import { CoreContent } from 'pliny/utils/contentlayer'
import type { Blog } from 'contentlayer/generated'
import Image from '@/components/Image'
import Link from '@/components/Link'
import siteMetadata from '@/data/siteMetadata'
import {
getSpotlightOgImage,
getSpotlightHeroImage,
} from '@/components/post/postShared'

interface Props {
post: CoreContent<Blog>
featured?: boolean
}

const cardClasses =
'group block overflow-hidden rounded-lg border-2 border-gray-200 border-opacity-60 transition-colors hover:border-primary-500 dark:border-gray-700 dark:hover:border-primary-400'

function CompactCard({ post }: { post: CoreContent<Blog> }) {
const { path, date, title, summary, images } = post
const ogImage = getSpotlightOgImage(images)

return (
<Link
href={`/${path}`}
aria-label={`Read the spotlight: ${title}`}
className={`flex h-full flex-col ${cardClasses}`}
>
<div className="relative aspect-[16/9] w-full overflow-hidden">
{ogImage && (
<Image
src={ogImage}
alt=""
fill
sizes="(min-width: 1280px) 33vw, (min-width: 768px) 50vw, 100vw"
className="object-cover transition-transform duration-300 motion-safe:group-hover:scale-105"
/>
)}
</div>
<div className="p-5">
<time
dateTime={date}
className="text-sm font-medium text-gray-500 dark:text-gray-400"
>
{formatDate(date, siteMetadata.locale)}
</time>
<h2 className="mt-2 text-xl font-bold leading-tight tracking-tight text-gray-900 group-hover:text-primary-600 dark:text-gray-100 dark:group-hover:text-primary-400">
{title}
</h2>
{summary && (
<p className="mt-3 text-gray-500 dark:text-gray-400">{summary}</p>
)}
</div>
</Link>
)
}

function HeroCard({ post }: { post: CoreContent<Blog> }) {
const { path, date, title, summary, images } = post
const heroImage = getSpotlightHeroImage(images) ?? getSpotlightOgImage(images)

return (
<Link
href={`/${path}`}
aria-label={`Read the spotlight: ${title}`}
className={`relative ${cardClasses}`}
>
<div className="relative aspect-[2/1] w-full lg:aspect-[21/9]">
{heroImage && (
<Image
src={heroImage}
alt=""
fill
priority
sizes="100vw"
className="object-cover object-[70%_center] transition-transform duration-300 motion-safe:group-hover:scale-105 lg:object-center"
/>
)}
<div
className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/45 to-black/10"
aria-hidden
/>
<div className="absolute inset-x-0 bottom-0 p-6 sm:p-8">
<p className="text-xs font-semibold uppercase tracking-widest text-white/80">
Developer Spotlight
</p>
<time
dateTime={date}
className="mt-2 block text-sm font-medium text-white/90"
>
{formatDate(date, siteMetadata.locale)}
</time>
<h2 className="mt-2 max-w-3xl text-2xl font-extrabold leading-tight tracking-tight text-white sm:text-3xl md:text-4xl">
{title}
</h2>
{summary && (
<p className="mt-3 line-clamp-2 max-w-2xl text-white/90 sm:line-clamp-3">
{summary}
</p>
)}
</div>
</div>
</Link>
)
}

export default function SpotlightCard({ post, featured = false }: Props) {
if (!featured) {
return <CompactCard post={post} />
}

return (
<>
<div className="md:hidden">
<CompactCard post={post} />
</div>
<div className="hidden md:block">
<HeroCard post={post} />
</div>
</>
)
}
3 changes: 3 additions & 0 deletions components/post/postShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export const discussUrl = () =>
/** Spotlight posts: images[0] = OG/social, images[1] = cover hero. */
export const getSpotlightHeroImage = (images?: string[]) => images?.[1]

/** Spotlight posts: images[0] = OG/social preview. */
export const getSpotlightOgImage = (images?: string[]) => images?.[0]

/** Keep in sync with SectionContainer horizontal layout. */
export const postSectionClasses =
'mx-2 max-w-3xl px-4 sm:px-6 md:mx-auto lg:mx-auto xl:max-w-5xl xl:px-0'
Expand Down
5 changes: 0 additions & 5 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,6 @@ module.exports = () => {
destination: '/faq/grantee',
permanent: true,
},
{
source: '/spotlight',
destination: '/tags/spotlight',
permanent: true,
},
{
source: '/heartbeat',
destination: 'https://heartbeat.opensats.org/',
Expand Down
107 changes: 107 additions & 0 deletions pages/spotlight/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import Head from 'next/head'
import { PageSEO } from '@/components/SEO'
import siteMetadata from '@/data/siteMetadata'
import Link from '@/components/Link'
import SpotlightCard from '@/components/SpotlightCard'
import { kebabCase } from 'pliny/utils/kebabCase'
import { sortedBlogPost, allCoreContent } from 'pliny/utils/contentlayer'
import { InferGetStaticPropsType } from 'next'
import { allBlogs } from 'contentlayer/generated'
import type { Blog } from 'contentlayer/generated'

const PAGE_DESCRIPTION =
'Meet the developers OpenSats supports and the open-source work they are building.'

export const getStaticProps = async () => {
const posts = allCoreContent(
sortedBlogPost(
allBlogs.filter(
(post) =>
post.draft !== true &&
post.tags.map((tag) => kebabCase(tag)).includes('spotlight')
)
) as Blog[]
)
return { props: { posts } }
}

export default function Spotlight({
posts,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const [featured, ...rest] = posts

const structuredData = {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'Developer Spotlights',
description: PAGE_DESCRIPTION,
url: `${siteMetadata.siteUrl}/spotlight`,
mainEntity: {
'@type': 'ItemList',
itemListElement: posts.map((post, index) => ({
'@type': 'ListItem',
position: index + 1,
url: `${siteMetadata.siteUrl}/${post.path}`,
name: post.title,
})),
},
}

return (
<>
<PageSEO
title={`Developer Spotlights - ${siteMetadata.title}`}
description={PAGE_DESCRIPTION}
slug="spotlight"
/>
<Head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(structuredData),
}}
/>
</Head>
<div className="space-y-2 pb-8 pt-6 md:space-y-5">
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-5xl md:leading-14">
Developer Spotlights
</h1>
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
Get to know the developers we support and the open-source projects
they pour their days into.
</p>
</div>

{featured && (
<div className="pb-10">
<SpotlightCard post={featured} featured />
</div>
)}

{rest.length > 0 && (
<div className="grid gap-8 pb-10 md:grid-cols-2 xl:grid-cols-3">
{rest.map((post) => (
<SpotlightCard key={post.path} post={post} />
))}
</div>
)}

<div className="flex flex-col items-end gap-4 pt-8 text-base font-medium leading-6">
<Link
href="/apply"
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
aria-label="Apply for funding"
>
Could you be next? Apply for funding &rarr;
</Link>
<Link
href="/funds"
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
aria-label="Donate to OpenSats"
>
Help fund developers like these &rarr;
</Link>
</div>
</>
)
}
6 changes: 6 additions & 0 deletions scripts/generate-page-og.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ const EXTRA_PAGES = [
title: 'Map',
summary: 'Countries where OpenSats has awarded grants.',
},
{
slug: 'spotlight',
title: 'Developer Spotlights',
summary:
'Meet the developers OpenSats supports and the open-source work they are building.',
},
]

// Utility / legal / form-result pages that don't benefit from a custom
Expand Down
Loading