├── 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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 | - Choose a card
17 | - Log in with GitHub
18 | - Choose recipient
19 | - Send your card
20 |
21 |
Thank you for sharing the love with the open source community!
22 |
23 |
24 |
25 |
26 |
27 |
28 | {% for card in cards %}
29 |
30 |
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 |
18 |
19 |

20 |
21 |
![]()
22 |
23 |
24 |
25 |
![]()
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | @
38 |
39 |
40 |
57 |
58 |
59 | Choose a recipient by connecting to GitHub
60 | Log in with GitHub
61 |
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 |
33 |
34 | Copy and share this URL with @${data.recipientName}.
35 |
36 | https://oss.cards/card/${data.path}
37 |
44 |
45 |
46 |
47 |
48 |

49 |
53 |
57 |
58 |
59 | ${sponsorCTA(data)}
60 |
61 |
62 | `;
63 | };
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # oss-valentines (https://oss.cards)
2 |
3 | 
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