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: 2 additions & 0 deletions common/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ export const SHIPS = makePath('/ships')

export const PARTNER_PACK = makePath('/partner-pack')

export const LOVE_LETTERS = makePath('/love-letters')

export const SECURITY_CHALLENGE = makePath('/security-challenge')
15 changes: 15 additions & 0 deletions components/header/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import GitHubLogo from '../../public/icons/github-logo'
import IconCalendar from '../../public/icons/calendar'
import IconBooks from '../../public/icons/books'
import BoxGift from '../../public/icons/box-gift'
import Heart from '../../public/icons/heart'
import Rocket from '../../public/icons/rocket'
import { BREAKPOINTS } from '../../common/constants'

Expand Down Expand Up @@ -122,6 +123,20 @@ const Header = () => {
</span>
</Link>
</li>
<li>
<Link
href={ROUTES.LOVE_LETTERS.path}
aria-label="Love Letters"
className={clsx('header__link', {
['is-active']: pathname === ROUTES.LOVE_LETTERS.path,
})}
>
<Heart />
<span className="header__link-text">
Love Letters
</span>
</Link>
</li>
</ul>
</nav>
</div>
Expand Down
47 changes: 47 additions & 0 deletions components/postcard/Postcard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* eslint-disable @next/next/no-img-element */
import { useMemo } from 'react'
import md from 'markdown-it'

const Postcard = ({ postcards }) => {
const items = useMemo(() => postcards || [], [postcards])

if (items.length === 0) {
return (
<section className="postcards">
<p className="postcards__empty">No postcards yet.</p>
</section>
)
}

return (
<section className="postcards">
<div className="postcards__intro">
<h2 className="postcards__heading">Love Letters</h2>
<p className="postcards__subtitle">
Postcards of appreciation for the maintainers who keep open source thriving.
</p>
</div>
<div className="postcards__grid">
{items.map((postcard, index) => (
<article key={index} className="postcards__card">
<div className="postcards__image-wrapper">
<img
className="postcards__image"
src={postcard.image}
alt={postcard.quote}
/>
</div>
Comment thread
samus-aran marked this conversation as resolved.
<blockquote
className="postcards__quote"
dangerouslySetInnerHTML={{
__html: md({ linkify: true }).render(postcard.quote || ''),
}}
/>
</article>
))}
</div>
</section>
)
}

export default Postcard
89 changes: 89 additions & 0 deletions components/postcard/postcard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
.postcards {
padding: spacing(7) spacing(2);
padding-bottom: spacing(4);
max-width: 1440px;
margin: 0 auto;
box-sizing: border-box;

@media (min-width: $lg) {
padding-left: spacing(4);
padding-right: spacing(4);
}

&__intro {
margin-bottom: spacing(5);
max-width: 640px;
}

&__heading {
@extend %header-2;
color: $white;
margin-bottom: spacing(2);
overflow-wrap: break-word;
}

&__subtitle {
@extend %body-1;
color: $white-50;
}

&__empty {
text-align: center;
padding: spacing(8) spacing(2);
@extend %body-1;
color: $white-50;
}

&__grid {
display: grid;
grid-template-columns: 1fr;
gap: spacing(3);

@media (min-width: $md) {
grid-template-columns: repeat(2, 1fr);
}

@media (min-width: $lg) {
grid-template-columns: repeat(3, 1fr);
}
}

&__card {
background: $white-20;
border-radius: 12px;
overflow: hidden;

@extend %background-filter;
}

&__image-wrapper {
aspect-ratio: 4 / 3;
overflow: hidden;
}

&__image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}

&__quote {
padding: spacing(3);
@extend %body-1;
color: $white-80;
font-style: italic;
margin: 0;

a {
color: $white;
text-decoration: underline;
text-underline-offset: 3px;
transition: opacity $simple-fade;

&:hover {
opacity: 0.8;
}
}
}
}
13 changes: 13 additions & 0 deletions content/love-letters/postcards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
postcards:
- image: https://github.com/user-attachments/assets/86057fb3-ff13-4548-b6e8-d379c73a80bc
quote: "Thank you for maintaining this project — it powers our entire workflow."
- image: https://github.com/user-attachments/assets/4a35b200-dd04-44d9-816e-b828e403b0b4
quote: "Your dedication to open source inspires me every day."
- image: https://github.com/user-attachments/assets/2d6f0283-d9aa-4eee-9294-40ff2797bb17
quote: "This library saved me hundreds of hours. You are appreciated!"
- image: https://github.com/user-attachments/assets/003a97f3-2866-419a-8464-29727d312d3d
quote: "This library saved me hundreds of hours. You are appreciated!"
- image: https://github.com/user-attachments/assets/fa44bc57-e10f-453e-a8c9-2bc5d6312327
quote: "This library saved me hundreds of hours. You are appreciated! ~ [@samus-aran](https://github.com/samus-aran)"
---
48 changes: 48 additions & 0 deletions pages/love-letters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useEffect } from 'react'
import Head from 'next/head'
import path from 'path'

import { getLiteral } from '../common/i18n'
import { getDataFromMD } from '../common/api'
import Postcard from '../components/postcard/Postcard'

import { useBackground } from '../contexts/BackgroundContext'

export default function LoveLettersPage({ postcards }) {
const { setAnimationStep } = useBackground()

useEffect(() => {
setAnimationStep(6)
}, [setAnimationStep])

return (
<div>
<Head>
<title>{`Love Letters - ${getLiteral('meta:title')}`}</title>
<meta name="description" content="Postcards of appreciation for open source maintainers." />

<meta property="og:title" content="Love Letters" />
<meta property="og:description" content="Postcards of appreciation for open source maintainers." />
<meta property="og:image" content="https://maintainermonth.github.com/images/og/generic.png" />

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Love Letters" />
<meta name="twitter:description" content="Postcards of appreciation for open source maintainers." />
<meta name="twitter:image" content="https://maintainermonth.github.com/images/og/generic.png" />
</Head>

<Postcard postcards={postcards} />
</div>
)
}

export async function getStaticProps() {
const filePath = path.join(process.cwd(), 'content', 'love-letters', 'postcards.md')
const data = getDataFromMD(filePath)

return {
props: {
postcards: data.postcards || [],
},
}
}
13 changes: 13 additions & 0 deletions public/icons/heart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const Heart = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
width="16"
height="16"
fill="currentColor"
>
<path d="m8 14.25.345.666a.75.75 0 0 1-.69 0l-.008-.004-.018-.01a7.152 7.152 0 0 1-.31-.17 22.055 22.055 0 0 1-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.066 22.066 0 0 1-3.744 2.584l-.018.01-.006.003h-.002ZM4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.58 20.58 0 0 0 8 13.393a20.58 20.58 0 0 0 3.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.749.749 0 0 1-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5Z" />
</svg>
)

export default Heart
2 changes: 2 additions & 0 deletions styles/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
@import '../components/ships/ships';
@import '../components/ships/ships-filter';

@import '../components/postcard/postcard';

@import '../components/not-found/not-found';

@import '../components/partner-pack/offers/offers';
Expand Down
Loading