├── src ├── assets │ ├── js │ │ ├── customize.js │ │ ├── previewing.js │ │ └── init.js │ ├── img │ │ ├── oss-og.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── valentines │ │ │ ├── open-source-open-hearts.zip │ │ │ ├── open-source-open-hearts │ │ │ │ ├── PNG │ │ │ │ │ ├── lgtm.png │ │ │ │ │ ├── hooked.png │ │ │ │ │ ├── git-push.png │ │ │ │ │ ├── jamstack.png │ │ │ │ │ ├── slice-it.png │ │ │ │ │ ├── thank-you.png │ │ │ │ │ ├── gui-u-and-i.png │ │ │ │ │ ├── hello-world.png │ │ │ │ │ ├── main-squeeze.png │ │ │ │ │ ├── easy-to-commit.png │ │ │ │ │ ├── every-version.png │ │ │ │ │ ├── outta-my-head.png │ │ │ │ │ ├── passing-props.png │ │ │ │ │ ├── the-best-style.png │ │ │ │ │ ├── trick-or-treat.png │ │ │ │ │ ├── unicode-u-n-i.png │ │ │ │ │ ├── a-time-before-you.png │ │ │ │ │ ├── applet-of-my-eye.png │ │ │ │ │ ├── div-without-you.png │ │ │ │ │ ├── fave-dependency.png │ │ │ │ │ ├── part-of-my-stack.png │ │ │ │ │ ├── promise-to-await.png │ │ │ │ │ ├── run-array-with-me.png │ │ │ │ │ ├── align-self-with-you.png │ │ │ │ │ ├── function-without-you.png │ │ │ │ │ └── your-code-has-what-i-need.png │ │ │ │ └── SVG │ │ │ │ │ └── your-code-has-what-i-need.svg │ │ │ ├── your-code-has-what-i-need.svg │ │ │ └── function-without-you.svg │ │ ├── heart.svg │ │ ├── illo-valentines.svg │ │ └── xoxo-lynn.svg │ └── _fonts │ │ ├── MulishVar-subset.woff2 │ │ ├── Pacaembu-Variable.woff2 │ │ ├── PacaembuVar-subset.woff2 │ │ ├── rubik-v11-latin-500.woff │ │ ├── rubik-v11-latin-500.woff2 │ │ └── Mulish-VariableFont_wght.ttf └── site │ ├── styles.scss │ ├── _includes │ ├── type.scss │ ├── fonts.scss │ ├── footer.scss │ ├── buttons.scss │ ├── global.scss │ ├── header.scss │ ├── variables.scss │ ├── view-choose.scss │ ├── layouts │ │ └── base.11ty.js │ └── view-card.scss │ ├── customize-logged-out.md │ ├── customize-logged-in.md │ ├── donate.md │ ├── index.liquid │ ├── customize.liquid │ └── _data │ └── cards.json ├── .prettierrc ├── .gitignore ├── netlify └── functions │ ├── partials │ ├── 404.js │ └── card.js │ ├── auth-logout.ts │ ├── auth-login.ts │ ├── user.mts │ ├── auth-callback.mts │ ├── auth-check.ts │ ├── save-card.js │ ├── view-card.js │ └── view-og.js ├── netlify.toml ├── package.json ├── .eleventy.js └── README.md /src/assets/js/customize.js: -------------------------------------------------------------------------------- 1 | (async function () { 2 | toggleCardUI(); 3 | })(); 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/img/oss-og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/oss-og.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | .env 5 | 6 | # Local Netlify folder 7 | .netlify 8 | .env.local 9 | -------------------------------------------------------------------------------- /src/assets/img/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/img/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/_fonts/MulishVar-subset.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/_fonts/MulishVar-subset.woff2 -------------------------------------------------------------------------------- /src/assets/_fonts/Pacaembu-Variable.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/_fonts/Pacaembu-Variable.woff2 -------------------------------------------------------------------------------- /src/assets/_fonts/PacaembuVar-subset.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/_fonts/PacaembuVar-subset.woff2 -------------------------------------------------------------------------------- /src/assets/_fonts/rubik-v11-latin-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/_fonts/rubik-v11-latin-500.woff -------------------------------------------------------------------------------- /src/assets/_fonts/rubik-v11-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/_fonts/rubik-v11-latin-500.woff2 -------------------------------------------------------------------------------- /src/assets/_fonts/Mulish-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/_fonts/Mulish-VariableFont_wght.ttf -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts.zip -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/lgtm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/lgtm.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/hooked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/hooked.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/git-push.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/git-push.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/jamstack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/jamstack.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/slice-it.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/slice-it.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/thank-you.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/thank-you.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/gui-u-and-i.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/gui-u-and-i.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/hello-world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/hello-world.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/main-squeeze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/main-squeeze.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/easy-to-commit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/easy-to-commit.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/every-version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/every-version.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/outta-my-head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/outta-my-head.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/passing-props.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/passing-props.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/the-best-style.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/the-best-style.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/trick-or-treat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/trick-or-treat.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/unicode-u-n-i.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/unicode-u-n-i.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/a-time-before-you.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/a-time-before-you.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/applet-of-my-eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/applet-of-my-eye.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/div-without-you.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/div-without-you.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/fave-dependency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/fave-dependency.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/part-of-my-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/part-of-my-stack.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/promise-to-await.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/promise-to-await.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/run-array-with-me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/run-array-with-me.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/align-self-with-you.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/align-self-with-you.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/function-without-you.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/function-without-you.png -------------------------------------------------------------------------------- /src/assets/img/valentines/open-source-open-hearts/PNG/your-code-has-what-i-need.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynnandtonic/oss-valentines/HEAD/src/assets/img/valentines/open-source-open-hearts/PNG/your-code-has-what-i-need.png -------------------------------------------------------------------------------- /src/site/styles.scss: -------------------------------------------------------------------------------- 1 | @import '_includes/variables.scss'; 2 | @import '_includes/fonts.scss'; 3 | @import '_includes/global.scss'; 4 | @import '_includes/type.scss'; 5 | @import '_includes/buttons.scss'; 6 | @import '_includes/header.scss'; 7 | @import '_includes/view-choose.scss'; 8 | @import '_includes/view-card.scss'; 9 | @import '_includes/footer.scss'; 10 | -------------------------------------------------------------------------------- /src/site/_includes/type.scss: -------------------------------------------------------------------------------- 1 | h1, h2, h3, h4 { 2 | font-family: var(--font-headline); 3 | font-weight: 500; 4 | } 5 | 6 | h1 { 7 | font-size: 2em; 8 | line-height: 1.3; 9 | } 10 | 11 | h2 { 12 | font-size: 2.5em; 13 | } 14 | 15 | h3 { 16 | font-size: 1.6em; 17 | } 18 | 19 | main p, 20 | main ol, 21 | main ul { 22 | margin-top: 1.2em; 23 | font-size: 1.2em; 24 | } 25 | -------------------------------------------------------------------------------- /src/site/customize-logged-out.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.11ty.js 3 | pageClass: view-card view-customize 4 | --- 5 | 6 |
7 |
8 |
9 |

Choose a recipient by connecting to GitHub

10 | 11 |
12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /src/site/customize-logged-in.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.11ty.js 3 | pageClass: view-card view-customize 4 | --- 5 | 6 |
7 |
8 |
9 | 10 | Add recipient 11 |
12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /src/site/donate.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.11ty.js 3 | pageClass: view-card view-donate 4 | --- 5 | 6 | 7 |
8 |
9 |
10 |

This project accepts sponsorship! Make a contribution?

11 | 12 | Heck yes! 13 | 14 | Not now 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /src/site/_includes/fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Rubik; 3 | src: url(../_fonts/rubik-v11-latin-500.woff2) format("woff2"); 4 | unicode-range: U+5,U+20,U+21,U+24,U+25,U+27,U+2B-2E,U+30-3A,U+3F,U+41-5A,U+61-7A,U+D7,U+2019; 5 | font-weight: 100 1000; 6 | font-display: swap; 7 | } 8 | 9 | @font-face { 10 | font-family: Mulish; 11 | src: url(../_fonts/MulishVar-subset.woff2) format("woff2"); 12 | unicode-range: U+5,U+20,U+21,U+24,U+25,U+27,U+2B-2E,U+30-3A,U+3F,U+41-5A,U+61-7A,U+D7,U+2019; 13 | font-weight: 200 900; 14 | font-display: swap; 15 | } 16 | -------------------------------------------------------------------------------- /netlify/functions/partials/404.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | 3 | 4 | return ` 5 |
6 |
7 |
8 | two smiling stamped envelopes surrounded by hearts 9 |

Where is the love?

10 |

11 | I’m sure somebody is thinking of you. Maybe sending some love of your own might give them a gentle nudge! 12 |

13 |
14 |
15 |
`; 16 | }; 17 | -------------------------------------------------------------------------------- /src/site/_includes/footer.scss: -------------------------------------------------------------------------------- 1 | .footer-main { 2 | margin-top: auto; 3 | display: grid; 4 | grid-template-columns: repeat(auto-fit,minmax(300px,1fr)); 5 | gap: 0 1px; 6 | background-color: var(--color-red-R400); 7 | } 8 | 9 | .footer-main a:hover { 10 | text-decoration: none; 11 | } 12 | 13 | .landing .footer-main { 14 | margin-top: 0; 15 | padding-top: 5em; 16 | } 17 | 18 | .footer-main .contributors { 19 | padding: calc(var(--grid-gutter) * 2) var(--grid-gutter); 20 | display: grid; 21 | gap: 1rem; 22 | background-color: white; 23 | border-top: 1px solid var(--color-red-R400); 24 | } 25 | 26 | .footer-main .contributors > * { 27 | max-width: 300px; 28 | } 29 | 30 | .footer-main p .footer-heart { 31 | height: 30px; 32 | margin-top: -3px; 33 | display: inline-block; 34 | vertical-align: top; 35 | } 36 | -------------------------------------------------------------------------------- /netlify/functions/auth-logout.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from '@netlify/functions'; 2 | import { parse } from 'cookie'; 3 | import { deleteToken } from '@octokit/oauth-methods'; 4 | 5 | export const handler: Handler = async (event) => { 6 | if (event.headers.cookie) { 7 | const cookies = parse(event.headers.cookie); 8 | if (cookies['nf-gh-session']) { 9 | const auth = JSON.parse(cookies['nf-gh-session']); 10 | await deleteToken({ 11 | clientType: 'oauth-app', 12 | clientId: process.env.GITHUB_APP_CLIENT_ID, 13 | clientSecret: process.env.GITHUB_APP_CLIENT_SECRET, 14 | token: auth.access_token, 15 | }); 16 | } 17 | } 18 | 19 | return { 20 | statusCode: 301, 21 | headers: { 22 | Location: '/', 23 | }, 24 | body: JSON.stringify({ status: 'redirecting' }), 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist" 3 | command = "npm run build" 4 | 5 | [dev] 6 | publish = "dist" 7 | command = "npm run dev" 8 | 9 | [[redirects]] 10 | from = "https://oss-valentine.netlify.app" 11 | to = "https://oss.cards" 12 | status = 301 13 | force = true 14 | 15 | [[redirects]] 16 | from = "/create" 17 | to = "/.netlify/functions/save-card" 18 | status = 200 19 | 20 | [[redirects]] 21 | from = "/card/*" 22 | to = "/.netlify/builders/view-card" 23 | status = 200 24 | 25 | [[redirects]] 26 | from = "/auth/*" 27 | to = "/.netlify/functions/auth-:splat" 28 | status = 200 29 | 30 | [[redirects]] 31 | from = "/api/user/:username" 32 | to = "/.netlify/functions/user" 33 | status = 200 34 | 35 | [[redirects]] 36 | from = "/api/*" 37 | to = "/.netlify/functions/:splat" 38 | status = 200 39 | 40 | [[redirects]] 41 | from = "/og/*" 42 | to = "/.netlify/functions/view-og" 43 | status = 200 44 | -------------------------------------------------------------------------------- /src/assets/img/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/js/previewing.js: -------------------------------------------------------------------------------- 1 | (async function () { 2 | const body = document.getElementsByTagName('body')[0]; 3 | if (document.referrer) { 4 | const referrer = new URL(document.referrer); 5 | if (referrer.pathname.indexOf('/customize/') !== -1) { 6 | console.log(`previewing`); 7 | body.classList.add('previewing'); 8 | } 9 | } else { 10 | console.log(`enjoying`); 11 | body.classList.add('gifted'); 12 | } 13 | 14 | btnHandler('.copy-url', function () { 15 | const copyFeedback = document.querySelector('.copy-success'); 16 | navigator.clipboard 17 | .writeText(document.querySelector('.share-link').innerText) 18 | .then( 19 | function () { 20 | console.log(`link copied`); 21 | copyFeedback.innerHTML = 'Copied!'; 22 | }, 23 | function () { 24 | console.log(`Couldn't copy link to clipboard`); 25 | copyFeedback.innerHTML = 'Try again!'; 26 | } 27 | ); 28 | }); 29 | })(); 30 | -------------------------------------------------------------------------------- /netlify/functions/auth-login.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from '@netlify/functions'; 2 | import { getWebFlowAuthorizationUrl } from '@octokit/oauth-methods'; 3 | import { serialize } from 'cookie'; 4 | 5 | export const handler: Handler = async (event) => { 6 | let returnRoute = '/'; 7 | if (event.headers.referer) { 8 | returnRoute = event.headers.referer; 9 | } 10 | 11 | const redirectCookie = serialize( 12 | 'nf-authed-path', 13 | JSON.stringify({ route: returnRoute }), 14 | { 15 | secure: true, 16 | httpOnly: true, 17 | sameSite: true, 18 | maxAge: 1000 * 60 * 60, // one hour 19 | } 20 | ); 21 | 22 | const { url } = await getWebFlowAuthorizationUrl({ 23 | clientType: 'oauth-app', 24 | clientId: process.env.GITHUB_APP_CLIENT_ID, 25 | scopes: ['read:user', 'read:org'], 26 | }); 27 | 28 | return { 29 | statusCode: 301, 30 | headers: { 31 | 'Set-Cookie': redirectCookie, 32 | Location: url, 33 | }, 34 | body: JSON.stringify({ status: 'Redirecting...' }), 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oss-valentines", 3 | "version": "1.0.0", 4 | "description": "The code that powers https://oss.cards", 5 | "scripts": { 6 | "build": "eleventy", 7 | "dev": "eleventy --serve" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/netlify/oss-valentines.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/netlify/oss-valentines/issues" 18 | }, 19 | "homepage": "https://github.com/netlify/oss-valentines#readme", 20 | "dependencies": { 21 | "@11ty/eleventy": "^1.0.0", 22 | "@11ty/eleventy-plugin-directory-output": "^1.0.1", 23 | "@netlify/functions": "^2.6.0", 24 | "@octokit/graphql": "^4.8.0", 25 | "@octokit/oauth-app": "^3.6.0", 26 | "@octokit/oauth-methods": "^1.2.6", 27 | "@supabase/supabase-js": "^1.30.0", 28 | "node-fetch": "^2.7.0", 29 | "sass": "^1.49.7", 30 | "shortid": "^2.2.16" 31 | }, 32 | "devDependencies": { 33 | "netlify-cli": "^17.23.1" 34 | }, 35 | "private": true 36 | } 37 | -------------------------------------------------------------------------------- /src/site/_includes/buttons.scss: -------------------------------------------------------------------------------- 1 | button, 2 | .button, 3 | .view-card input[type='submit'] { 4 | padding: 0.5em 1em 0.6em; 5 | display: inline-block; 6 | background-color: var(--color-teal-T500); 7 | border-radius: 4px; 8 | font-family: var(--font-secondary); 9 | font-size: 100%; 10 | font-weight: 700; 11 | line-height: 1; 12 | color: var(--color-teal-T900); 13 | text-decoration: none; 14 | border: none; 15 | transition: background-color 200ms; 16 | white-space: nowrap; 17 | } 18 | 19 | button:hover, 20 | .button:hover, 21 | .view-card input[type='submit']:hover { 22 | background-color: var(--color-teal-T200); 23 | cursor: pointer; 24 | } 25 | 26 | input[type='submit']:disabled, 27 | input[type='submit']:disabled:hover, 28 | button:disabled { 29 | cursor: not-allowed; 30 | filter: grayscale(100%); 31 | } 32 | 33 | button:focus-visible, 34 | .button:focus-visible { 35 | box-shadow: 0 0 0 1px white, 0 0 0 6px var(--color-focus-ring); 36 | } 37 | 38 | .button.blue { 39 | background-color: var(--color-blue-B500); 40 | color: var(--color-blue-B050); 41 | } 42 | 43 | .button.blue:hover { 44 | background-color: var(--color-blue-B700); 45 | color: var(--color-blue-B050); 46 | } 47 | -------------------------------------------------------------------------------- /.eleventy.js: -------------------------------------------------------------------------------- 1 | const directoryOutputPlugin = require('@11ty/eleventy-plugin-directory-output'); 2 | const sass = require('sass'); 3 | // const uglify = require("uglify-js"); 4 | 5 | module.exports = function (eleventyConfig) { 6 | eleventyConfig.setQuietMode(true); 7 | eleventyConfig.addPlugin(directoryOutputPlugin, { 8 | columns: { 9 | filesize: true, 10 | benchmark: true, 11 | }, 12 | warningFileSize: 50 * 1000, 13 | }); 14 | 15 | // Sass pipeline 16 | eleventyConfig.addTemplateFormats('scss'); 17 | eleventyConfig.addExtension('scss', { 18 | outputFileExtension: 'css', 19 | compile: function (contents, includePath) { 20 | let includePaths = [this.config.dir.includes]; 21 | return () => { 22 | let ret = sass.renderSync({ 23 | file: includePath, 24 | includePaths, 25 | data: contents, 26 | outputStyle: 'compressed', 27 | }); 28 | return ret.css.toString('utf8'); 29 | }; 30 | }, 31 | }); 32 | 33 | // Pass through assets 34 | eleventyConfig.addPassthroughCopy({ 'src/assets': '/' }); 35 | 36 | return { 37 | dir: { 38 | input: 'src/site', 39 | includes: '_includes', 40 | output: 'dist', 41 | }, 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/site/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.11ty.js 3 | pageClass: view-choose 4 | bannerTitle: Choose a card 5 | scripts: ["init.js"] 6 | --- 7 | 8 | 9 |
10 |
11 |
12 |
13 |

Open source, Open hearts

14 |

It’s a great day to send a note of appreciation to your fave open source developers and projects.

15 |
    16 |
  1. Choose a card
  2. 17 |
  3. Log in with GitHub
  4. 18 |
  5. Choose recipient
  6. 19 |
  7. Send your card
  8. 20 |
21 |

Thank you for sharing the love with the open source community!

22 | 25 |
26 |
27 | 28 | {% for card in cards %} 29 | 30 | {{ card.title }} 31 | 32 | {% endfor %} 33 | 34 |
35 |

Open mailboxes

36 |

Send ’em the old fashioned way.

37 | Download cards 38 |
39 | 40 | 41 | 42 |
43 |
44 | 45 | -------------------------------------------------------------------------------- /netlify/functions/user.mts: -------------------------------------------------------------------------------- 1 | import type { Context } from '@netlify/functions'; 2 | 3 | export default async (req: Request, context: Context) => { 4 | const [, , , username = ''] = new URL(req.url).pathname.split('/'); 5 | const auth = JSON.parse( 6 | decodeURIComponent(context.cookies.get('nf-gh-session')) 7 | ); 8 | 9 | const response = await fetch('https://api.github.com/graphql', { 10 | method: 'POST', 11 | headers: { 12 | Authorization: `Bearer ${auth.access_token}`, 13 | }, 14 | body: JSON.stringify({ 15 | query: ` 16 | query ($login: String!) { 17 | user: repositoryOwner(login: $login) { 18 | avatarUrl 19 | login 20 | 21 | ... on Organization { 22 | viewerCanSponsor 23 | name 24 | } 25 | ... on User { 26 | viewerCanSponsor 27 | name 28 | } 29 | } 30 | } 31 | `, 32 | variables: { 33 | login: username, 34 | }, 35 | }), 36 | }); 37 | 38 | if (!response.ok) { 39 | return new Response(response.statusText, { 40 | status: response.status, 41 | }); 42 | } 43 | 44 | const { data } = await response.json(); 45 | 46 | return new Response(JSON.stringify(data.user), { 47 | status: 200, 48 | headers: { 49 | 'content-type': 'application/json', 50 | }, 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /src/site/_includes/global.scss: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | ::selection { 10 | background: var(--color-teal-T300); 11 | color: black; 12 | } 13 | 14 | body { 15 | min-height: 100vh; 16 | padding: 0; 17 | display: flex; 18 | flex-direction: column; 19 | background-color: var(--color-red-R200); 20 | color: var(--color-gray-L800); 21 | line-height: 1.5; 22 | font-family: var(--font-secondary); 23 | } 24 | 25 | a { 26 | color: currentColor; 27 | } 28 | 29 | abbr[title] { 30 | text-decoration: none; 31 | } 32 | 33 | img { 34 | display: block; 35 | max-width: 100%; 36 | height: auto; 37 | } 38 | 39 | .center { 40 | text-align: center; 41 | } 42 | 43 | :focus:not(:focus-visible) { 44 | outline: none; 45 | } 46 | :focus-visible { 47 | outline-color: var(--color-focus-ring); 48 | z-index: 1; 49 | } 50 | @supports (box-shadow: none) { 51 | :focus-visible { 52 | outline: none; 53 | box-shadow: 0 0 1px 4px var(--color-focus-ring); 54 | } 55 | } 56 | 57 | .container { 58 | display: grid; 59 | grid-template-columns: [full-start] minmax(var(--grid-gutter),1fr) [main-start] minmax(0,1300px) [main-end] minmax(var(--grid-gutter),1fr) [full-end]; 60 | } 61 | 62 | .container > .content { 63 | grid-column: main; 64 | padding: 4em 0; 65 | } 66 | 67 | /* Putting this here for now? */ 68 | .four-oh-four { 69 | max-width: 500px; 70 | margin: 0 auto; 71 | text-align: center; 72 | } 73 | -------------------------------------------------------------------------------- /netlify/functions/auth-callback.mts: -------------------------------------------------------------------------------- 1 | import type { Context } from '@netlify/functions'; 2 | import { exchangeWebFlowCode } from '@octokit/oauth-methods'; 3 | 4 | export default async (req: Request, context: Context) => { 5 | const url = new URL(req.url); 6 | const code = url.searchParams.get('code') ?? ''; 7 | 8 | const { data } = await exchangeWebFlowCode({ 9 | clientType: 'oauth-app', 10 | clientId: process.env.GITHUB_APP_CLIENT_ID!, 11 | clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!, 12 | code, 13 | }); 14 | 15 | if (!data || !data.access_token) { 16 | console.log('no token found'); 17 | 18 | return new Response('redirecting...', { 19 | status: 301, 20 | headers: { 21 | Location: '/', 22 | }, 23 | }); 24 | } 25 | 26 | context.cookies.set({ 27 | name: 'nf-gh-session', 28 | value: encodeURIComponent(JSON.stringify(data)), 29 | secure: true, 30 | httpOnly: true, 31 | sameSite: 'Strict', 32 | path: '/', 33 | maxAge: 1000 * 60 * 60 * 24 * 14, // two weeks 34 | }); 35 | 36 | const authPath = decodeURIComponent(context.cookies.get('nf-authed-path')); 37 | let returnUrl = '/'; 38 | if (authPath) { 39 | returnUrl = JSON.parse(authPath).route; 40 | if (typeof returnUrl !== 'string') { 41 | returnUrl = '/'; 42 | } 43 | } 44 | 45 | return new Response('redirecting...', { 46 | status: 301, 47 | headers: { 48 | Location: returnUrl, 49 | }, 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /netlify/functions/auth-check.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from '@netlify/functions'; 2 | import { parse } from 'cookie'; 3 | import { checkToken } from '@octokit/oauth-methods'; 4 | 5 | type User = { 6 | login?: string; 7 | avatar_url?: string; 8 | }; 9 | 10 | export const handler: Handler = async (event) => { 11 | if (!event.headers.cookie) { 12 | return { 13 | statusCode: 401, 14 | body: JSON.stringify({ isLoggedIn: false, user: {} }), 15 | }; 16 | } 17 | const cookies = parse(event.headers?.cookie); 18 | 19 | if (!cookies['nf-gh-session']) { 20 | return { 21 | statusCode: 401, 22 | body: JSON.stringify({ isLoggedIn: false, user: {} }), 23 | }; 24 | } 25 | const auth = JSON.parse(cookies['nf-gh-session']); 26 | let isLoggedIn = false; 27 | let user: User = {}; 28 | 29 | try { 30 | const { data } = await checkToken({ 31 | clientType: 'oauth-app', 32 | clientId: process.env.GITHUB_APP_CLIENT_ID, 33 | clientSecret: process.env.GITHUB_APP_CLIENT_SECRET, 34 | token: auth.access_token, 35 | }); 36 | 37 | user.login = data.user.login; 38 | user.avatar_url = data.user.avatar_url; 39 | 40 | if (data.id) { 41 | isLoggedIn = true; 42 | } 43 | } catch (err) { 44 | console.log(err); 45 | return { 46 | statusCode: 401, 47 | body: JSON.stringify({ isLoggedIn: false, user: {} }), 48 | }; 49 | } 50 | 51 | return { 52 | statusCode: 200, 53 | body: JSON.stringify({ isLoggedIn, user }), 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /netlify/functions/save-card.js: -------------------------------------------------------------------------------- 1 | const shortid = require('shortid'); 2 | const querystring = require('querystring'); 3 | const { createClient } = require('@supabase/supabase-js'); 4 | 5 | // Environment variables managed as part of teh Netlify site. 6 | // To access these automatically during local development you 7 | // should run the site with `ntl dev` 8 | const { DATABASE_URL, SUPABASE_SERVICE_API_KEY } = process.env; 9 | 10 | exports.handler = async (event, context) => { 11 | // get the form data and add a unique path id 12 | const data = querystring.parse(event.body); 13 | const uniquePath = shortid.generate(); 14 | data.cardPath = uniquePath; 15 | 16 | // Connect to database and save our data 17 | const supabase = createClient(DATABASE_URL, SUPABASE_SERVICE_API_KEY); 18 | const { savedData, error } = await supabase.from('ValentineCards').upsert({ 19 | path: data.cardPath, 20 | cardVariant: data.cardVariant, 21 | senderName: data.senderName, 22 | senderAvatar: data.senderAvatar, 23 | recipientName: data.recipientName, 24 | recipientAvatar: data.recipientAvatar, 25 | recipientCanBeSponsored: data.recipientCanBeSponsored, 26 | }); 27 | 28 | if (error) { 29 | // TODO: make a better error experience 30 | console.log('Error saving card data:', error); 31 | return { 32 | statusCode: 400, 33 | body: JSON.stringify(error), 34 | }; 35 | } else { 36 | // send the user to their nice new page 37 | console.log(`Saved: ${JSON.stringify(savedData)}`); 38 | return { 39 | statusCode: 302, 40 | headers: { 41 | Location: `/card/${data.cardPath}`, 42 | }, 43 | }; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /netlify/functions/view-card.js: -------------------------------------------------------------------------------- 1 | const { createClient } = require('@supabase/supabase-js'); 2 | const { builder } = require('@netlify/functions'); 3 | const { render } = require('../../src/site/_includes/layouts/base.11ty.js'); 4 | const card = require('./partials/card.js'); 5 | const notFound = require('./partials/404.js'); 6 | 7 | // Environment variables managed as part of teh Netlify site. 8 | // To access these automatically during local development you 9 | // should run the site with `ntl dev` 10 | const { DATABASE_URL, SUPABASE_SERVICE_API_KEY } = process.env; 11 | 12 | const handler = async (event) => { 13 | // get the card ID from the request 14 | const cardPath = event.path.split('card/')[1]; 15 | 16 | // Connect to database and fetch data 17 | const supabase = createClient(DATABASE_URL, SUPABASE_SERVICE_API_KEY); 18 | const { data, error } = await supabase 19 | .from('ValentineCards') 20 | .select('*') 21 | .eq('path', cardPath); 22 | 23 | console.log(data, error); 24 | 25 | if (data[0]) { 26 | // if found return a view 27 | console.log(`Render and persist page: ${cardPath}`); 28 | 29 | let pageData = { 30 | content: card(data[0]), 31 | pageClass: 'view-card view-share', 32 | bannerTitle: 'For you', 33 | ogPath: cardPath, 34 | scripts: ['init.js', 'previewing.js'], 35 | }; 36 | 37 | return { 38 | statusCode: 200, 39 | headers: { 40 | 'Content-Type': 'text/html', 41 | }, 42 | body: render(pageData), 43 | }; 44 | } else { 45 | // not found or an error, send to the generic error page 46 | console.log('Error:', error); 47 | 48 | let pageData = { 49 | content: notFound(), 50 | pageClass: 'view-card', 51 | bannerTitle: 'ummmm...', 52 | scripts: ['init.js'], 53 | }; 54 | 55 | return { 56 | statusCode: 200, 57 | headers: { 58 | 'Content-Type': 'text/html', 59 | }, 60 | // body: JSON.stringify(error) 61 | body: render(pageData), 62 | }; 63 | } 64 | }; 65 | 66 | exports.handler = builder(handler); 67 | -------------------------------------------------------------------------------- /netlify/functions/view-og.js: -------------------------------------------------------------------------------- 1 | // Get the data for a given badge number and 2 | // generate an image for it with cloudinary 3 | 4 | const { createClient } = require('@supabase/supabase-js'); 5 | const fetch = require('node-fetch'); 6 | 7 | const { DATABASE_URL, SUPABASE_SERVICE_API_KEY } = process.env; 8 | 9 | const encodeURL = (url) => { 10 | let b64 = Buffer.from(url.split('?')[0]).toString('base64'); 11 | return b64; 12 | }; 13 | 14 | const handler = async (event) => { 15 | // get the card ID from the request 16 | const cardPath = event.path.split('og/')[1]; 17 | 18 | // Connect to database and fetch data 19 | const supabase = createClient(DATABASE_URL, SUPABASE_SERVICE_API_KEY); 20 | const { data, error } = await supabase 21 | .from('ValentineCards') 22 | .select('*') 23 | .eq('path', cardPath); 24 | 25 | // No custom card on this URL? 26 | // Display a generic OG image 27 | if (!data.length) { 28 | console.log(`no card found on ${cardPath}`); 29 | return; 30 | } 31 | 32 | let cardData = data[0]; 33 | 34 | // Fetch a generated image from Cloudinary 35 | const bgImageUrl = `oss-love/${cardData.cardVariant.replace('.svg', '.png')}`; 36 | const senderName = `c_fit,l_text:Roboto_32:@${encodeURI( 37 | cardData.senderName 38 | )},g_south_west,w_340,x_780,y_65`; 39 | const senderAvatar = `l_fetch:${encodeURL( 40 | cardData.senderAvatar 41 | )},r_max,w_70,g_south_west,x_700,y_48`; 42 | const recipientName = `c_fit,l_text:Roboto_32:@${encodeURI( 43 | cardData.recipientName 44 | )},g_south_west,w_340,x_155,y_65`; 45 | const recipientAvatar = `l_fetch:${encodeURL( 46 | cardData.recipientAvatar 47 | )},r_max,w_70,g_south_west,x_75,y_48`; 48 | 49 | const ogUrl = `https://res.cloudinary.com/dlb090tcz/image/upload/${senderName}/${senderAvatar}/${recipientName}/${recipientAvatar}/${bgImageUrl}`; 50 | console.log(ogUrl); 51 | 52 | let image; 53 | try { 54 | const result = await fetch(ogUrl); 55 | image = await result.buffer(); 56 | } catch (error) { 57 | console.log('error', error); 58 | return { 59 | statusCode: 500, 60 | body: JSON.stringify({ 61 | error: error.message, 62 | }), 63 | }; 64 | } 65 | 66 | // return the image directly 67 | return { 68 | statusCode: 200, 69 | headers: { 70 | 'Content-type': 'image/jpeg', 71 | }, 72 | body: image.toString('base64'), 73 | isBase64Encoded: true, 74 | }; 75 | }; 76 | 77 | exports.handler = handler; 78 | -------------------------------------------------------------------------------- /src/site/_includes/header.scss: -------------------------------------------------------------------------------- 1 | header { 2 | padding: .5em var(--grid-gutter); 3 | position: relative; 4 | display: flex; 5 | flex-wrap: wrap; 6 | justify-content: flex-end; 7 | gap: .6em; 8 | align-items: center; 9 | background-color: white; 10 | border-bottom: 1px solid var(--color-red-R400); 11 | } 12 | 13 | header .masthead { 14 | margin-right: auto; 15 | display: flex; 16 | align-items: center; 17 | font-family: var(--font-headline); 18 | font-weight: 500; 19 | text-decoration: none; 20 | } 21 | 22 | header .masthead-heart { 23 | height: 30px; 24 | display: inline-flex; 25 | margin-right: .3em; 26 | } 27 | 28 | header button, 29 | header .button { 30 | font-size: 80%; 31 | } 32 | 33 | header #header-auth { 34 | display: flex; 35 | align-items: center; 36 | gap: .5em; 37 | } 38 | 39 | header .logged-in-user { 40 | font-size: 90%; 41 | } 42 | 43 | :root { 44 | --instructions-bg: var(--color-gray-L200); 45 | } 46 | 47 | .instructions { 48 | background-color: var(--instructions-bg); 49 | font-family: var(--font-headline); 50 | font-weight: 680; 51 | font-size: 1.4em; 52 | } 53 | 54 | 55 | @media screen and (max-width: 800px) { 56 | header { 57 | padding-bottom: 0; 58 | } 59 | 60 | .instructions { 61 | margin: 0 calc(var(--grid-gutter) * -1); 62 | padding: .5rem 1rem; 63 | display: flex; 64 | align-items: center; 65 | justify-content: space-between; 66 | gap: .5em; 67 | flex-basis: calc(100% + 2em); 68 | order: 3; 69 | } 70 | 71 | .instructions svg { 72 | width: 50px; 73 | } 74 | } 75 | 76 | @media screen and (min-width: 801px) { 77 | .instructions { 78 | height: 100%; 79 | min-width: 250px; 80 | position: absolute; 81 | left: 50%; 82 | top: 0; 83 | transform: translateX(-50%); 84 | text-align: center; 85 | } 86 | 87 | .instructions { 88 | padding-top: .5em; 89 | } 90 | 91 | .instructions svg { 92 | width: 45px; 93 | display: block; 94 | margin: 0 auto; 95 | } 96 | 97 | .instructions::before { 98 | content: ''; 99 | width: 100%; 100 | height: 25px; 101 | position: absolute; 102 | left: 0; 103 | bottom: -25px; 104 | background-repeat: no-repeat; 105 | background-image: linear-gradient( 8deg, transparent 39.5%, var(--instructions-bg) 41%), 106 | linear-gradient(-8deg, transparent 39.5%, var(--instructions-bg) 41%); 107 | background-size: 51% 100%; 108 | background-position: 0 0, 100% 0; 109 | z-index: -1; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/site/customize.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.11ty.js 3 | pageClass: view-card view-customize 4 | bannerTitle: Make it yours 5 | pagination: 6 | data: cards 7 | alias: card 8 | size: 1 9 | permalink: "/customize/{{card.image}}/index.html" 10 | scripts: ["init.js","customize.js"] 11 | --- 12 | 13 | 14 |
15 |
16 |
17 |
18 |
19 | {{card.title}} 20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 |
31 | 32 | 33 | 34 | 58 | 62 | 63 | -------------------------------------------------------------------------------- /netlify/functions/partials/card.js: -------------------------------------------------------------------------------- 1 | module.exports = (data) => { 2 | 3 | 4 | const sponsorCTA = (data) => { 5 | 6 | 7 | console.log(data); 8 | 9 | if (data.recipientCanBeSponsored) { 10 | return `` 20 | } else { 21 | return ""; 22 | } 23 | }; 24 | 25 | 26 | 27 | return ` 28 |
29 |
30 |
31 |

@${data.senderName} sent you a card to say “Thank you!”

32 |
33 |
34 |

Copy and share this URL with @${data.recipientName}.

35 | 36 | 37 |
38 |
39 | 40 | 41 |
42 | Tweet 43 |
44 |
45 | 46 | 47 |
48 | 49 |
50 | 51 | ${data.recipientName} 52 |
53 |
54 | 55 | ${data.senderName} 56 |
57 |
58 | 59 | ${sponsorCTA(data)} 60 | 61 |
62 |
`; 63 | }; 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oss-valentines (https://oss.cards) 2 | 3 | ![oss.cards interface](https://github.com/user-attachments/assets/fa85ec2e-ca8d-4cc0-94bd-40e45341358a) 4 | 5 | Originally built by a small team at Netlify. 6 | Now maintained by [@lynnandtonic](https://github.com/lynnandtonic). 7 | 8 | ## Overview 9 | 10 | This site was built to allow people to express their appreciation of open source contributors and organisations by selecting a greeting card, adding a recipient and sender name, and generating a unique URL for their customised card which they could share. The site uses: 11 | 12 | - [Eleventy](https://11ty.dev) for site templating 13 | - [Netlify Functions](https://www.netlify.com/products/functions) and [on-demand builders](https://docs.netlify.com/configure-builds/on-demand-builders/) for creating unique pages on demand 14 | - [Supabase](https://supabase.com/) via Netlify Functions for data persistence and retrieval 15 | - [Cloudinary](https://cloudinary.com/) for generating composite open graph images 16 | - [GitHub oAuth](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps) for user authenticaion 17 | 18 | 19 | ## Development 20 | 21 | The site uses some templating via 11ty. 22 | 23 | ```bash 24 | # install dependencies 25 | npm i 26 | 27 | # build, serve and watch with 11ty during development 28 | # do this via Netlify Dev in order to get ODB goodies 29 | ntl dev 30 | ``` 31 | 32 | Access to the database (Supabase) is provided by environment variables. Authorised Netlifyers will be able to access these by linking their local site to the Netlfy project via `ntl link`. This will [populate your local development version with the centrally-managed environment variables](https://www.netlify.com/blog/2021/12/10/more-tips-for-environment-variables-and-netlify-cli/). 33 | 34 | Others will need to create their own Suapbase database and add their own environment variables to access it. 35 | 36 | 37 | ## Auth for local development 38 | 39 | To test the GitHub oAuth flow on a local development server you can expose your local deve server on a public URL using [Netlify Dev](https://www.netlify.com/products/cli). 40 | 41 | ```bash 42 | # Run a local dev server and serve it on a public URL 43 | ntl dev --live 44 | ``` 45 | 46 | This will generate a unique public URL which is valid for the duration of the local server's life. Each time you kill the server and run it again, you'll get a new unique URL. 47 | 48 | You can provide this URL to GitHub as the basis for an oAuth application. In production this is oAuth application is maintained under the Netlify account and associated with the https://oss.cards domain. 49 | 50 | To create your own GitHub oAuth application for local testing visit the [Developer Settings](https://github.com/settings/developers) section of [your profile](https://github.com/settings/profile) 51 | 52 | Here you can create a new oAuth Application and provide some details. Providing the public URL of your local dev server will enable you to test the athentication flow and authenticated features of the site. 53 | 54 | | Setting | Value | 55 | | ----- | -------- | 56 | | **Homepage URL** | `https://oss-valentine-XXXXX.netlify.live/` (provided by `ntl dev --live`) | 57 | | **Authorization callback URLL** | `https://oss-valentine-XXXXX.netlify.live/auth/callback` | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/site/_includes/variables.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-white: #ffffff; 3 | /* Bonus stage Color Palette */ 4 | --color-teal-T900: #054861; 5 | --color-teal-T800: #0F6A80; 6 | --color-teal-T700: #139CAB; 7 | --color-teal-T600: #30C8C9; 8 | --color-teal-T500: #5CEBDF; 9 | --color-teal-T400: #84F3DF; 10 | --color-teal-T300: #9EF9E0; 11 | --color-teal-T200: #BFFDE7; 12 | --color-teal-T100: #DFFEF0; 13 | --color-teal-T050: #F2FEF5; 14 | --color-blue-B900: #061475; 15 | --color-blue-B800: #0A1E8D; 16 | --color-blue-B700: #112CAF; 17 | --color-blue-B600: #183DD1; 18 | --color-blue-B500: #2250F4; 19 | --color-blue-B400: #587EF8; 20 | --color-blue-B300: #799CFB; 21 | --color-blue-B200: #A6BFFD; 22 | --color-blue-B100: #D2E0FE; 23 | --color-blue-B050: #E5EDFF; 24 | --color-gray-L800: #151A1E; 25 | --color-gray-L700: #33373B; 26 | --color-gray-L600: #676C70; 27 | --color-gray-L500: #80858A; 28 | --color-gray-L400: #A3A7AC; 29 | --color-gray-L300: #E7EAED; 30 | --color-gray-L200: #F2F5F8; 31 | --color-gray-L100: #FAFBFB; 32 | --color-gray-L000: #FFFFFF; 33 | --color-gray-D800: #151A1E; 34 | --color-gray-D700: #1C2126; 35 | --color-gray-D600: #22262A; 36 | --color-gray-D500: #2A2E32; 37 | --color-gray-D400: #4C5257; 38 | --color-gray-D300: #80858A; 39 | --color-gray-D200: #A3A8AC; 40 | --color-gray-D100: #E7EAED; 41 | --color-gray-D000: #FFFFFF; 42 | --color-yellow-Y900: #7A4804; 43 | --color-yellow-Y800: #935C07; 44 | --color-yellow-Y700: #B7790B; 45 | --color-yellow-Y600: #DB9710; 46 | --color-yellow-Y500: #FFB916; 47 | --color-yellow-Y400: #FFCF50; 48 | --color-yellow-Y300: #FFDD73; 49 | --color-yellow-Y200: #FFEBA1; 50 | --color-yellow-Y100: #FFF6D0; 51 | --color-yellow-Y050: #FFFCEC; 52 | --color-red-R900: #7A122D; 53 | --color-red-R800: #931E33; 54 | --color-red-R700: #B7303D; 55 | --color-red-R600: #DB4648; 56 | --color-red-R500: #FF6B60; 57 | --color-red-R400: #FF9987; 58 | --color-red-R300: #FFB59F; 59 | --color-red-R200: #FFD3BF; 60 | --color-red-R100: #FFEBDF; 61 | --color-red-R050: #FFF5EB; 62 | /* Primary colors */ 63 | --color-pink: #FF6969; 64 | --color-pink-dark: #FF1154; 65 | --color-pink-accessible: #E8114E; 66 | --color-orange: #F86816; 67 | --color-orange-dark: #B74808; 68 | /* Secondary colors */ 69 | --color-blue: #43B4D8; 70 | --color-blue-dark: #146396; 71 | --color-blue-dark-b: #133857; 72 | --color-violet: #C57AA2; 73 | --color-violet-dark: #7C0C64; 74 | --color-gold: #F6BC00; 75 | --color-yellow: #FFAD43; 76 | --color-yellow-dark: #CC801F; 77 | /* Plan colors */ 78 | --color-plan-starter: var(--color-teal-T900); 79 | --color-plan-pro: var(--color-pink-dark); 80 | --color-plan-pro-accessible: var(--color-pink-accessible); 81 | --color-plan-business: var(--color-blue-dark); 82 | --color-plan-enterprise: var(--color-gray-L700); 83 | /* Fonts */ 84 | --font-headline: Rubik, sans-serif; 85 | --font-headline-feature-settings: "salt" 1; 86 | --font-primary: Rubik, sans-serif; 87 | --font-primary-feature-settings: "salt" 1; 88 | --font-secondary: Mulish, sans-serif; 89 | --font-monospace: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 90 | /* Functional */ 91 | --border-radius: 6px; 92 | --border-radius-large: 12px; 93 | --color-focus-ring: var(--color-teal-T600); 94 | --grid-gutter: 1.1rem; 95 | /* 40px / 16 */ 96 | --shadow-light: 0px 2px 4px rgba(14, 30, 37, 0.12); 97 | --shadow-heavy: -10px 10px 80px rgba(0, 0, 0, 0.1); 98 | --shadow-hover: 0 4px 12px rgba(0, 0, 0, .25); 99 | --shadow-deep: 0px 16px 24px rgba(0, 0, 0, 0.07), 0px 6px 30px rgba(0, 0, 0, 0.06), 0px 8px 10px rgba(0, 0, 0, 0.1); 100 | --ui-border: 1px solid var(--color-gray-L800); 101 | } 102 | -------------------------------------------------------------------------------- /src/site/_includes/view-choose.scss: -------------------------------------------------------------------------------- 1 | .view-choose .nav-choose { 2 | display: flex; 3 | } 4 | 5 | .view-choose main .content { 6 | min-height: 400px; 7 | display: grid; 8 | gap: 1.5em; 9 | align-items: end; 10 | } 11 | 12 | @media screen and (max-width: 800px) { 13 | .view-choose main .content { 14 | padding: var(--grid-gutter) 0; 15 | } 16 | } 17 | 18 | @media screen and (min-width: 400px) { 19 | .view-choose main .content { 20 | grid-template-columns: repeat(auto-fit,minmax(350px,1fr)); 21 | } 22 | } 23 | 24 | .view-choose .intro { 25 | padding: 1.5em; 26 | grid-row: 1 / 4; 27 | align-self: start; 28 | justify-self: center; 29 | display: flex; 30 | flex-direction: column; 31 | justify-content: space-between; 32 | border: 1px solid var(--color-red-R400); 33 | align-self: stretch; 34 | } 35 | 36 | .view-choose .intro-steps li { 37 | margin-top: .7em; 38 | counter-increment: steps-counter; 39 | list-style: none; 40 | display: grid; 41 | grid-template-columns: auto 1fr; 42 | gap: .5em; 43 | align-items: center; 44 | } 45 | 46 | .view-choose .intro-steps li > * { 47 | grid-column: 2; 48 | } 49 | 50 | .view-choose .intro-steps li::before { 51 | width: 30px; 52 | height: 30px; 53 | grid-column: 1; 54 | border-radius: 50%; 55 | align-self: start; 56 | content: counter(steps-counter); 57 | background-color: var(--color-red-R500); 58 | display: grid; 59 | place-content: center; 60 | font-family: var(--font-headline); 61 | font-weight: 400; 62 | font-size: 80%; 63 | color: white; 64 | } 65 | 66 | .view-choose .byline { 67 | margin-top: 1.2em; 68 | max-width: 180px; 69 | } 70 | 71 | .view-choose .warning { 72 | padding: 10px 20px; 73 | display: grid; 74 | align-content: center; 75 | justify-items: center; 76 | background-color: var(--color-red-R700); 77 | align-self: stretch; 78 | color: white; 79 | } 80 | 81 | .view-choose .warning span { 82 | font-size: 2em; 83 | } 84 | 85 | .view-choose .warning p { 86 | font-size: 1rem; 87 | text-align: center; 88 | text-wrap: pretty; 89 | } 90 | 91 | :root { 92 | --shadow-color: 18deg 43% 62%; 93 | } 94 | 95 | .view-choose .valentine { 96 | display: block; 97 | box-shadow: 0px 0.7px 0.8px hsl(var(--shadow-color) / 0.25), 98 | 0px 1.1px 1.3px -1.2px hsl(var(--shadow-color) / 0.26), 99 | 0px 2.5px 2.9px -2.4px hsl(var(--shadow-color) / 0.26);; 100 | transform: scale(1,1) translateZ(0) translate3d(0,0,0); 101 | transition-duration: 100ms; 102 | transition-property: transform, box-shadow; 103 | transition-timing-function: ease-in-out; 104 | } 105 | 106 | .view-choose .valentine img { 107 | width: 100%; 108 | display: block; 109 | } 110 | 111 | .view-choose .valentine:hover { 112 | box-shadow: 0px 0.7px 0.8px hsl(var(--shadow-color) / 0.26), 113 | 0px 2.2px 2.5px -0.8px hsl(var(--shadow-color) / 0.27), 114 | -0.1px 5.2px 5.9px -1.6px hsl(var(--shadow-color) / 0.27), 115 | -0.2px 12.4px 14.1px -2.4px hsl(var(--shadow-color) / 0.28);; 116 | transform: scale(1.04,1.04) translateZ(0) translate3d(0,0,0); 117 | } 118 | 119 | .view-choose .download { 120 | padding: 1.5em; 121 | aspect-ratio: 120/63; 122 | display: grid; 123 | place-items: center; 124 | background-color: var(--color-red-R100); 125 | border: 1px solid var(--color-red-R400); 126 | text-align: center; 127 | } 128 | 129 | .view-choose .download p, 130 | .view-choose .download .button { 131 | margin-top: .5em; 132 | } 133 | 134 | .valentine:focus:not(:focus-visible) { 135 | outline: none; 136 | } 137 | 138 | .valentine:focus-visible { 139 | outline-color: var(--color-focus-ring); 140 | } 141 | 142 | @supports (box-shadow: none) { 143 | .valentine:focus-visible { 144 | outline: none; 145 | box-shadow: 0 0 0 2px white, 0 0 0 6px var(--color-focus-ring); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/site/_data/cards.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "image": "passing-props", 4 | "title": "three clapping emojis. Text reads “Passing all the props to you”" 5 | }, 6 | { 7 | "image": "slice-it", 8 | "title": "A happy slice of pepperoni pizza. Text reads “Your code is great no matter how you slice it”" 9 | }, 10 | { 11 | "image": "trick-or-treat", 12 | "title": "A jack-o-lantern surrounded by candy corn. JSON code reads “code: yours, trick: false, treat: true”" 13 | }, 14 | { 15 | "image": "hello-world", 16 | "title": "A smiling Earth says “Hi” with small hearts. Text reads “You had me at Hello World”" 17 | }, 18 | { 19 | "image": "fave-dependency", 20 | "title": "Isometric cubed pattern with some isolated and colored to look like hearts. Text reads “You’re my favorite dependency”" 21 | }, 22 | { 23 | "image": "a-time-before-you", 24 | "title": "A smiling hour glass. Text reads: “I wouldn’t go back to a time ::before you.”" 25 | }, 26 | { 27 | "image": "div-without-you", 28 | "title": "A pattern of diamonds outlined to demonstrate the CSS box model. Text reads “I just couldn’t
without you”" 29 | }, 30 | { 31 | "image": "run-array-with-me", 32 | "title": "Three suitcases each labeled 0, 1, and 2 respectively. Text reads “Run array with me”" 33 | }, 34 | { 35 | "image": "jamstack", 36 | "title": "A smiling toast with jam and a small jam jar nearby. Text reads “You’re my Jam(stack)”" 37 | }, 38 | { 39 | "image": "easy-to-commit", 40 | "title": "A CLI terminal prompts “Are you sure?” and the input says “Heck yes!”. The valentine text reads “You make it easy to commit”" 41 | }, 42 | { "image": "gui-u-and-i", "title": "You can’t spell GUI without U and I" }, 43 | { 44 | "image": "unicode-u-n-i", 45 | "title": "“U+1F60D” is written out three times next to text that reads “You can’t spell Unicode with U ‘n’ I”" 46 | }, 47 | { 48 | "image": "function-without-you", 49 | "title": "I couldn’t function {hearts} without you" 50 | }, 51 | { 52 | "image": "your-code-has-what-i-need", 53 | "title": "A rainbow in the clouds surrounded by birds and stars. Text reads: “#your-code:has(.what-i-need)”" 54 | }, 55 | { 56 | "image": "every-version", 57 | "title": "The text “Every version of you is great” surrounded by a background of software release versions like v0.0.1" 58 | }, 59 | { "image": "lgtm", "title": "Your code always LGTM" }, 60 | { 61 | "image": "outta-my-head", 62 | "title": "A smiling brain with small hearts. Text reads “I can’t get you out of my ”" 63 | }, 64 | { 65 | "image": "git-push", 66 | "title": "A CLI terminal sends rainbow arrows up to a smiling cloud in the sky. Text reads “You can’t git push without ‘us’”" 67 | }, 68 | { 69 | "image": "part-of-my-stack", 70 | "title": "Four tall stacks of pancakes each with a pat of butter. Text reads “I’m glad you’re part of my stack”" 71 | }, 72 | { 73 | "image": "hooked", 74 | "title": "A smiling worm sits on a fishing hook surrounded by hearts and bubbles. Text reads “Your code’s got me hooked”" 75 | }, 76 | { 77 | "image": "promise-to-await", 78 | "title": "A countdown clock counting down from a very high number. Text reads “I promise to await for you”" 79 | }, 80 | { 81 | "image": "the-best-style", 82 | "title": "A rainbow gradient of tshirts on hangers. Text reads “You’ve got the best