chore: apply automatic linting

This commit is contained in:
Corbin Crutchley
2022-09-25 05:51:04 -07:00
parent 5d628203d3
commit 3900a13481
92 changed files with 4787 additions and 4694 deletions

View File

@@ -1,46 +1,46 @@
const tsRules = { const tsRules = {
"@typescript-eslint/ban-types": "off", "@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-empty-interface": "off" "@typescript-eslint/no-empty-interface": "off",
} };
module.exports = { module.exports = {
env: { env: {
node: true, node: true,
browser: true, browser: true,
}, },
extends: ['eslint:recommended', 'plugin:astro/recommended'], extends: ["eslint:recommended", "plugin:astro/recommended"],
parserOptions: { parserOptions: {
ecmaVersion: 'latest', ecmaVersion: "latest",
sourceType: 'module', sourceType: "module",
}, },
rules: { rules: {
"no-unused-vars": "off" "no-unused-vars": "off",
}, },
overrides: [ overrides: [
{ {
files: ['*.astro'], files: ["*.astro"],
parser: 'astro-eslint-parser', parser: "astro-eslint-parser",
parserOptions: { parserOptions: {
parser: '@typescript-eslint/parser', parser: "@typescript-eslint/parser",
extraFileExtensions: ['.astro'], extraFileExtensions: [".astro"],
}, },
rules: { rules: {
...tsRules ...tsRules,
}, },
}, },
{ {
files: ['*.ts'], files: ["*.ts"],
parser: '@typescript-eslint/parser', parser: "@typescript-eslint/parser",
extends: ['plugin:@typescript-eslint/recommended'], extends: ["plugin:@typescript-eslint/recommended"],
rules: { rules: {
...tsRules ...tsRules,
}, },
}, },
{ {
// Define the configuration for `<script>` tag. // Define the configuration for `<script>` tag.
// Script in `<script>` is assigned a virtual file name with the `.js` extension. // Script in `<script>` is assigned a virtual file name with the `.js` extension.
files: ['**/*.astro/*.js', '*.astro/*.js'], files: ["**/*.astro/*.js", "*.astro/*.js"],
parser: '@typescript-eslint/parser', parser: "@typescript-eslint/parser",
}, },
], ],
}; };

View File

@@ -1,11 +1,11 @@
{ {
"useTabs": true, "useTabs": true,
"overrides": [ "overrides": [
{ {
"files": ["**/*.astro"], "files": ["**/*.astro"],
"options": { "options": {
"parser": "astro" "parser": "astro"
} }
} }
] ]
} }

View File

@@ -16,8 +16,8 @@ import { rehypeUnicornElementMap } from "./src/utils/markdown/rehype-unicorn-ele
import { rehypeExcerpt } from "./src/utils/markdown/rehype-excerpt"; import { rehypeExcerpt } from "./src/utils/markdown/rehype-excerpt";
import { rehypeUnicornPopulatePost } from "./src/utils/markdown/rehype-unicorn-populate-post"; import { rehypeUnicornPopulatePost } from "./src/utils/markdown/rehype-unicorn-populate-post";
import { rehypeWordCount } from "./src/utils/markdown/rehype-word-count"; import { rehypeWordCount } from "./src/utils/markdown/rehype-word-count";
import {rehypeUnicornGetSuggestedPosts} from "./src/utils/markdown/rehype-unicorn-get-suggested-posts"; import { rehypeUnicornGetSuggestedPosts } from "./src/utils/markdown/rehype-unicorn-get-suggested-posts";
import copy from 'rollup-plugin-copy' import copy from "rollup-plugin-copy";
// TODO: Create types // TODO: Create types
import behead from "remark-behead"; import behead from "remark-behead";
@@ -26,90 +26,93 @@ import rehypeRaw from "rehype-raw";
import image from "@astrojs/image"; import image from "@astrojs/image";
export default defineConfig({ export default defineConfig({
integrations: [image()], integrations: [image()],
vite: { vite: {
ssr: { ssr: {
external: ["svgo"], external: ["svgo"],
}, },
plugins: [ plugins: [
{ {
...copy({ ...copy({
hook: 'options', hook: "options",
flatten: false, flatten: false,
targets: [ targets: [
{ {
src: 'content/**/*', src: "content/**/*",
dest: 'public/content' dest: "public/content",
} },
] ],
}), }),
enforce: 'pre' enforce: "pre",
} },
] ],
}, },
markdown: { markdown: {
mode: "md", mode: "md",
syntaxHighlight: false, syntaxHighlight: false,
extendDefaultPlugins: false, extendDefaultPlugins: false,
remarkPlugins: [ remarkPlugins: [
remarkGfm, remarkGfm,
// Remove complaining about "div cannot be in p element" // Remove complaining about "div cannot be in p element"
remarkUnwrapImages, remarkUnwrapImages,
/* start remark plugins here */ /* start remark plugins here */
[behead, { depth: 1 }], [behead, { depth: 1 }],
[ [
remarkEmbedder as any, remarkEmbedder as any,
{ {
transformers: [oembedTransformer, [TwitchTransformer, { parent }]], transformers: [oembedTransformer, [TwitchTransformer, { parent }]],
} as RemarkEmbedderOptions, } as RemarkEmbedderOptions,
], ],
[ [
remarkTwoslash, remarkTwoslash,
{ {
themes: ["css-variables"], themes: ["css-variables"],
} as UserConfigSettings, } as UserConfigSettings,
], ],
], ],
rehypePlugins: [ rehypePlugins: [
rehypeUnicornPopulatePost, rehypeUnicornPopulatePost,
rehypeUnicornGetSuggestedPosts, rehypeUnicornGetSuggestedPosts,
// This is required to handle unsafe HTML embedded into Markdown // This is required to handle unsafe HTML embedded into Markdown
rehypeRaw, rehypeRaw,
// Do not add the tabs before the slug. We rely on some of the heading // Do not add the tabs before the slug. We rely on some of the heading
// logic in order to do some of the subheading logic // logic in order to do some of the subheading logic
[ [
rehypeSlug, rehypeSlug,
{ {
maintainCase: true, maintainCase: true,
removeAccents: true, removeAccents: true,
enableCustomId: true, enableCustomId: true,
}, },
], ],
[ [
rehypeTabs, rehypeTabs,
{ {
injectSubheaderProps: true, injectSubheaderProps: true,
tabSlugifyProps: { tabSlugifyProps: {
enableCustomId: true, enableCustomId: true,
}, },
}, },
], ],
rehypeHeaderText, rehypeHeaderText,
/** /**
* Insert custom HTML generation code here * Insert custom HTML generation code here
*/ */
[ [
rehypeAstroImageMd, rehypeAstroImageMd,
{ {
maxHeight: 768, maxHeight: 768,
maxWidth: 768, maxWidth: 768,
} },
], ],
rehypeUnicornElementMap, rehypeUnicornElementMap,
[rehypeExcerpt, { [
maxLength: 150 rehypeExcerpt,
}], {
rehypeWordCount maxLength: 150,
], },
} as AstroUserConfig["markdown"] as never, ],
rehypeWordCount,
],
} as AstroUserConfig["markdown"] as never,
}); });

View File

@@ -1,5 +1,5 @@
{ {
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français" "fr": "Français"
} }

View File

@@ -1,42 +1,42 @@
[ [
{ {
"id": "cc-by-nc-sa-4", "id": "cc-by-nc-sa-4",
"footerImg": "https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png", "footerImg": "https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png",
"licenceType": "Creative Commons License", "licenceType": "Creative Commons License",
"explainLink": "http://creativecommons.org/licenses/by-nc-sa/4.0/", "explainLink": "http://creativecommons.org/licenses/by-nc-sa/4.0/",
"name": "Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)", "name": "Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)",
"displayName": "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License" "displayName": "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License"
}, },
{ {
"id": "cc-by-4", "id": "cc-by-4",
"footerImg": "https://i.creativecommons.org/l/by/4.0/88x31.png", "footerImg": "https://i.creativecommons.org/l/by/4.0/88x31.png",
"licenceType": "Creative Commons License", "licenceType": "Creative Commons License",
"explainLink": "http://creativecommons.org/licenses/by/4.0/", "explainLink": "http://creativecommons.org/licenses/by/4.0/",
"name": "Attribution 4.0 International (CC BY 4.0)", "name": "Attribution 4.0 International (CC BY 4.0)",
"displayName": "Creative Commons Attribution 4.0 International License" "displayName": "Creative Commons Attribution 4.0 International License"
}, },
{ {
"id": "publicdomain-zero-1", "id": "publicdomain-zero-1",
"footerImg": "https://licensebuttons.net/p/zero/1.0/88x31.png", "footerImg": "https://licensebuttons.net/p/zero/1.0/88x31.png",
"licenceType": "Public Domain", "licenceType": "Public Domain",
"explainLink": "https://creativecommons.org/publicdomain/zero/1.0/", "explainLink": "https://creativecommons.org/publicdomain/zero/1.0/",
"name": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication", "name": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication",
"displayName": "Public Domain" "displayName": "Public Domain"
}, },
{ {
"id": "cc-by-nc-nd-4", "id": "cc-by-nc-nd-4",
"footerImg": "https://i.creativecommons.org/l/by-nc-nd/4.0/88x31.png", "footerImg": "https://i.creativecommons.org/l/by-nc-nd/4.0/88x31.png",
"licenceType": "Creative Commons License", "licenceType": "Creative Commons License",
"explainLink": "https://creativecommons.org/licenses/by-nc-nd/4.0/", "explainLink": "https://creativecommons.org/licenses/by-nc-nd/4.0/",
"name": "Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)", "name": "Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)",
"displayName": "Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0) License" "displayName": "Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0) License"
}, },
{ {
"id": "coderpad", "id": "coderpad",
"footerImg": "/sponsors/coderpad.svg", "footerImg": "/sponsors/coderpad.svg",
"licenceType": "Owned by CoderPad", "licenceType": "Owned by CoderPad",
"explainLink": "https://coderpad.io/about-us/", "explainLink": "https://coderpad.io/about-us/",
"name": "Written for CoderPad, reposted to Unicorn Utterances", "name": "Written for CoderPad, reposted to Unicorn Utterances",
"displayName": "Written for CoderPad" "displayName": "Written for CoderPad"
} }
] ]

View File

@@ -1,322 +1,322 @@
[ [
{ {
"id": "she", "id": "she",
"they": "she", "they": "she",
"them": "her", "them": "her",
"their": "her", "their": "her",
"theirs": "hers", "theirs": "hers",
"themselves": "herself" "themselves": "herself"
}, },
{ {
"id": "he", "id": "he",
"they": "he", "they": "he",
"them": "him", "them": "him",
"their": "his", "their": "his",
"theirs": "his", "theirs": "his",
"themselves": "himself" "themselves": "himself"
}, },
{ {
"id": "they/themselves", "id": "they/themselves",
"they": "they", "they": "they",
"them": "them", "them": "them",
"their": "their", "their": "their",
"theirs": "theirs", "theirs": "theirs",
"themselves": "themselves" "themselves": "themselves"
}, },
{ {
"id": "they/themself", "id": "they/themself",
"they": "they", "they": "they",
"them": "them", "them": "them",
"their": "their", "their": "their",
"theirs": "theirs", "theirs": "theirs",
"themselves": "themself" "themselves": "themself"
}, },
{ {
"id": "ze/hir", "id": "ze/hir",
"they": "ze", "they": "ze",
"them": "hir", "them": "hir",
"their": "hir", "their": "hir",
"theirs": "hirs", "theirs": "hirs",
"themselves": "hirself" "themselves": "hirself"
}, },
{ {
"id": "ze/zir", "id": "ze/zir",
"they": "ze", "they": "ze",
"them": "zir", "them": "zir",
"their": "zir", "their": "zir",
"theirs": "zirs", "theirs": "zirs",
"themselves": "zirself" "themselves": "zirself"
}, },
{ {
"id": "xey", "id": "xey",
"they": "xey", "they": "xey",
"them": "xem", "them": "xem",
"their": "xyr", "their": "xyr",
"theirs": "xyrs", "theirs": "xyrs",
"themselves": "xemself" "themselves": "xemself"
}, },
{ {
"id": "ae", "id": "ae",
"they": "ae", "they": "ae",
"them": "aer", "them": "aer",
"their": "aer", "their": "aer",
"theirs": "aers", "theirs": "aers",
"themselves": "aerself" "themselves": "aerself"
}, },
{ {
"id": "e", "id": "e",
"they": "e", "they": "e",
"them": "em", "them": "em",
"their": "eir", "their": "eir",
"theirs": "eirs", "theirs": "eirs",
"themselves": "emself" "themselves": "emself"
}, },
{ {
"id": "ey", "id": "ey",
"they": "ey", "they": "ey",
"them": "em", "them": "em",
"their": "eir", "their": "eir",
"theirs": "eirs", "theirs": "eirs",
"themselves": "eirself" "themselves": "eirself"
}, },
{ {
"id": "fae", "id": "fae",
"they": "fae", "they": "fae",
"them": "faer", "them": "faer",
"their": "faer", "their": "faer",
"theirs": "faers", "theirs": "faers",
"themselves": "faerself" "themselves": "faerself"
}, },
{ {
"id": "fey", "id": "fey",
"they": "fey", "they": "fey",
"them": "fem", "them": "fem",
"their": "feir", "their": "feir",
"theirs": "feirs", "theirs": "feirs",
"themselves": "feirself" "themselves": "feirself"
}, },
{ {
"id": "hu", "id": "hu",
"they": "hu", "they": "hu",
"them": "hum", "them": "hum",
"their": "hus", "their": "hus",
"theirs": "hus", "theirs": "hus",
"themselves": "humself" "themselves": "humself"
}, },
{ {
"id": "it", "id": "it",
"they": "it", "they": "it",
"them": "it", "them": "it",
"their": "its", "their": "its",
"theirs": "its", "theirs": "its",
"themselves": "itself" "themselves": "itself"
}, },
{ {
"id": "jee", "id": "jee",
"they": "jee", "they": "jee",
"them": "jem", "them": "jem",
"their": "jeir", "their": "jeir",
"theirs": "jeirs", "theirs": "jeirs",
"themselves": "jemself" "themselves": "jemself"
}, },
{ {
"id": "kit", "id": "kit",
"they": "kit", "they": "kit",
"them": "kit", "them": "kit",
"their": "kits", "their": "kits",
"theirs": "kits", "theirs": "kits",
"themselves": "kitself" "themselves": "kitself"
}, },
{ {
"id": "ne", "id": "ne",
"they": "ne", "they": "ne",
"them": "nem", "them": "nem",
"their": "nir", "their": "nir",
"theirs": "nirs", "theirs": "nirs",
"themselves": "nemself" "themselves": "nemself"
}, },
{ {
"id": "peh", "id": "peh",
"they": "peh", "they": "peh",
"them": "pehm", "them": "pehm",
"their": "peh's", "their": "peh's",
"theirs": "peh's", "theirs": "peh's",
"themselves": "pehself" "themselves": "pehself"
}, },
{ {
"id": "per", "id": "per",
"they": "per", "they": "per",
"them": "per", "them": "per",
"their": "per", "their": "per",
"theirs": "pers", "theirs": "pers",
"themselves": "perself" "themselves": "perself"
}, },
{ {
"id": "sie", "id": "sie",
"they": "sie", "they": "sie",
"them": "hir", "them": "hir",
"their": "hir", "their": "hir",
"theirs": "hirs", "theirs": "hirs",
"themselves": "hirself" "themselves": "hirself"
}, },
{ {
"id": "se", "id": "se",
"they": "se", "they": "se",
"them": "sim", "them": "sim",
"their": "ser", "their": "ser",
"theirs": "sers", "theirs": "sers",
"themselves": "serself" "themselves": "serself"
}, },
{ {
"id": "shi", "id": "shi",
"they": "shi", "they": "shi",
"them": "hir", "them": "hir",
"their": "hir", "their": "hir",
"theirs": "hirs", "theirs": "hirs",
"themselves": "hirself" "themselves": "hirself"
}, },
{ {
"id": "si", "id": "si",
"they": "si", "they": "si",
"them": "hyr", "them": "hyr",
"their": "hyr", "their": "hyr",
"theirs": "hyrs", "theirs": "hyrs",
"themselves": "hyrself" "themselves": "hyrself"
}, },
{ {
"id": "they/thonself", "id": "they/thonself",
"they": "thon", "they": "thon",
"them": "thon", "them": "thon",
"their": "thons", "their": "thons",
"theirs": "thons", "theirs": "thons",
"themselves": "thonself" "themselves": "thonself"
}, },
{ {
"id": "ve/vis/verself", "id": "ve/vis/verself",
"they": "ve", "they": "ve",
"them": "ver", "them": "ver",
"their": "vis", "their": "vis",
"theirs": "vis", "theirs": "vis",
"themselves": "verself" "themselves": "verself"
}, },
{ {
"id": "ve/vir/vemself", "id": "ve/vir/vemself",
"they": "ve", "they": "ve",
"them": "vem", "them": "vem",
"their": "vir", "their": "vir",
"theirs": "virs", "theirs": "virs",
"themselves": "vemself" "themselves": "vemself"
}, },
{ {
"id": "vi/ver/verself", "id": "vi/ver/verself",
"they": "vi", "they": "vi",
"them": "ver", "them": "ver",
"their": "ver", "their": "ver",
"theirs": "vers", "theirs": "vers",
"themselves": "verself" "themselves": "verself"
}, },
{ {
"id": "vi/vir/vimself", "id": "vi/vir/vimself",
"they": "vi", "they": "vi",
"them": "vim", "them": "vim",
"their": "vir", "their": "vir",
"theirs": "virs", "theirs": "virs",
"themselves": "vimself" "themselves": "vimself"
}, },
{ {
"id": "vi/vim/vimself", "id": "vi/vim/vimself",
"they": "vi", "they": "vi",
"them": "vim", "them": "vim",
"their": "vim", "their": "vim",
"theirs": "vims", "theirs": "vims",
"themselves": "vimself" "themselves": "vimself"
}, },
{ {
"id": "xie", "id": "xie",
"they": "xie", "they": "xie",
"them": "xer", "them": "xer",
"their": "xer", "their": "xer",
"theirs": "xers", "theirs": "xers",
"themselves": "xerself" "themselves": "xerself"
}, },
{ {
"id": "xe", "id": "xe",
"they": "xe", "they": "xe",
"them": "xem", "them": "xem",
"their": "xyr", "their": "xyr",
"theirs": "xyrs", "theirs": "xyrs",
"themselves": "xemself" "themselves": "xemself"
}, },
{ {
"id": "xey", "id": "xey",
"they": "xey", "they": "xey",
"them": "xem", "them": "xem",
"their": "xeir", "their": "xeir",
"theirs": "xeirs", "theirs": "xeirs",
"themselves": "xemself" "themselves": "xemself"
}, },
{ {
"id": "yo", "id": "yo",
"they": "yo", "they": "yo",
"them": "yo", "them": "yo",
"their": "yos", "their": "yos",
"theirs": "yos", "theirs": "yos",
"themselves": "yosself" "themselves": "yosself"
}, },
{ {
"id": "ze/zirself", "id": "ze/zirself",
"they": "ze", "they": "ze",
"them": "zem", "them": "zem",
"their": "zes", "their": "zes",
"theirs": "zes", "theirs": "zes",
"themselves": "zirself" "themselves": "zirself"
}, },
{ {
"id": "ze", "id": "ze",
"they": "ze", "they": "ze",
"them": "mer", "them": "mer",
"their": "zer", "their": "zer",
"theirs": "zers", "theirs": "zers",
"themselves": "zemself" "themselves": "zemself"
}, },
{ {
"id": "zee", "id": "zee",
"they": "zee", "they": "zee",
"them": "zed", "them": "zed",
"their": "zeta", "their": "zeta",
"theirs": "zetas", "theirs": "zetas",
"themselves": "zedself" "themselves": "zedself"
}, },
{ {
"id": "zie/zir", "id": "zie/zir",
"they": "zie", "they": "zie",
"them": "zir", "them": "zir",
"their": "zir", "their": "zir",
"theirs": "zirs", "theirs": "zirs",
"themselves": "zirself" "themselves": "zirself"
}, },
{ {
"id": "zie/zem", "id": "zie/zem",
"they": "zie", "they": "zie",
"them": "zem", "them": "zem",
"their": "zes", "their": "zes",
"theirs": "zes", "theirs": "zes",
"themselves": "zirself" "themselves": "zirself"
}, },
{ {
"id": "zie/hir", "id": "zie/hir",
"they": "zie", "they": "zie",
"them": "hir", "them": "hir",
"their": "hir", "their": "hir",
"theirs": "hirs", "theirs": "hirs",
"themselves": "hirself" "themselves": "hirself"
}, },
{ {
"id": "zme", "id": "zme",
"they": "zme", "they": "zme",
"them": "zmyr", "them": "zmyr",
"their": "zmyr", "their": "zmyr",
"theirs": "zmyrs", "theirs": "zmyrs",
"themselves": "zmyrself" "themselves": "zmyrself"
} }
] ]

View File

@@ -1,26 +1,26 @@
[ [
{ {
"id": "developer", "id": "developer",
"prettyname": "Developer" "prettyname": "Developer"
}, },
{ {
"id": "designer", "id": "designer",
"prettyname": "Designer" "prettyname": "Designer"
}, },
{ {
"id": "devops", "id": "devops",
"prettyname": "Dev-ops" "prettyname": "Dev-ops"
}, },
{ {
"id": "author", "id": "author",
"prettyname": "Author" "prettyname": "Author"
}, },
{ {
"id": "translator", "id": "translator",
"prettyname": "Translator" "prettyname": "Translator"
}, },
{ {
"id": "community", "id": "community",
"prettyname": "Community Leader" "prettyname": "Community Leader"
} }
] ]

View File

@@ -1,499 +1,427 @@
[ [
{ {
"id": "crutchcorn", "id": "crutchcorn",
"name": "Corbin Crutchley", "name": "Corbin Crutchley",
"firstName": "Corbin", "firstName": "Corbin",
"lastName": "Crutchley", "lastName": "Crutchley",
"description": "Corbin is a senior developer with a passion for helping others. 💜\nThey're focused on ensuring that learning is open and fun. 🦄\nThey blog, livestream, code, and more to reach those goals to help others! 💅", "description": "Corbin is a senior developer with a passion for helping others. 💜\nThey're focused on ensuring that learning is open and fun. 🦄\nThey blog, livestream, code, and more to reach those goals to help others! 💅",
"socials": { "socials": {
"twitter": "crutchcorn", "twitter": "crutchcorn",
"github": "crutchcorn", "github": "crutchcorn",
"twitch": "crutchcorn" "twitch": "crutchcorn"
}, },
"pronouns": "they/themselves", "pronouns": "they/themselves",
"profileImg": "./crutchcorn.png", "profileImg": "./crutchcorn.png",
"color": "#ba68c8", "color": "#ba68c8",
"roles": [ "roles": ["devops", "developer", "author", "community"]
"devops", },
"developer", {
"author", "id": "fennifith",
"community" "name": "James Fenn",
] "firstName": "James",
}, "lastName": "Fenn",
{ "description": "Enjoys writing software on loud keyboards. Starts too many projects. Consumes food.",
"id": "fennifith", "socials": {
"name": "James Fenn", "twitter": "fennifith",
"firstName": "James", "github": "fennifith"
"lastName": "Fenn", },
"description": "Enjoys writing software on loud keyboards. Starts too many projects. Consumes food.", "pronouns": "he",
"socials": { "profileImg": "./fennifith.jpg",
"twitter": "fennifith", "color": "#0091EA",
"github": "fennifith" "roles": ["developer", "author", "community"]
}, },
"pronouns": "he", {
"profileImg": "./fennifith.jpg", "id": "evelynhathaway",
"color": "#0091EA", "name": "Evelyn Hathaway",
"roles": [ "firstName": "Evelyn",
"developer", "lastName": "Hathaway",
"author", "description": "👩‍💻🌈 I'm a student and software developer with a strong passion for frontend and backend JavaScript and web accessibility.",
"community" "socials": {
] "twitter": "eeveedev",
}, "github": "evelynhathaway"
{ },
"id": "evelynhathaway", "pronouns": "she",
"name": "Evelyn Hathaway", "profileImg": "proud.png",
"firstName": "Evelyn", "color": "#ef5f17",
"lastName": "Hathaway", "roles": ["developer", "devops", "community"]
"description": "👩‍💻🌈 I'm a student and software developer with a strong passion for frontend and backend JavaScript and web accessibility.", },
"socials": { {
"twitter": "eeveedev", "id": "adueppen",
"github": "evelynhathaway" "name": "Alex Dueppen",
}, "firstName": "Alex",
"pronouns": "she", "lastName": "Dueppen",
"profileImg": "proud.png", "description": "I do stuff sometimes.",
"color": "#ef5f17", "socials": {
"roles": [ "twitter": "AlexDueppen",
"developer", "github": "adueppen",
"devops", "website": "https://ajd.sh/"
"community" },
] "pronouns": "he",
}, "profileImg": "./adueppen.png",
{ "color": "#69ffff",
"id": "adueppen", "roles": ["developer", "community"]
"name": "Alex Dueppen", },
"firstName": "Alex", {
"lastName": "Dueppen", "id": "zavukodlak",
"description": "I do stuff sometimes.", "name": "Vukašin Anđelković",
"socials": { "firstName": "Vukašin",
"twitter": "AlexDueppen", "lastName": "Anđelković",
"github": "adueppen", "description": "Always doing a little bit of everything. Although, mainly focusing on design, logos, illustrations and paintings.\nI created the logo and all of the unicorns you see peeking out in some of the profile pictures!",
"website": "https://ajd.sh/" "socials": {
}, "twitter": "vukkashin",
"pronouns": "he", "website": "https://vukash.in/",
"profileImg": "./adueppen.png", "dribbble": "vukashin"
"color": "#69ffff", },
"roles": [ "pronouns": "he",
"developer", "profileImg": "./vukashin.png",
"community" "color": "#3485FF",
] "roles": ["designer"]
}, },
{ {
"id": "zavukodlak", "id": "tommyemo",
"name": "Vukašin Anđelković", "name": "Tom Wellington",
"firstName": "Vukašin", "firstName": "Tom",
"lastName": "Anđelković", "lastName": "Wellington",
"description": "Always doing a little bit of everything. Although, mainly focusing on design, logos, illustrations and paintings.\nI created the logo and all of the unicorns you see peeking out in some of the profile pictures!", "description": "I design icons and user interfaces, among other things. he/him ✌️",
"socials": { "socials": {
"twitter": "vukkashin", "twitter": "tommy_emo_",
"website": "https://vukash.in/", "website": "https://www.tommyemo.net/"
"dribbble": "vukashin" },
}, "pronouns": "he",
"pronouns": "he", "profileImg": "./tommyemo.jpg",
"profileImg": "./vukashin.png", "color": "#8539EB",
"color": "#3485FF", "roles": ["designer"]
"roles": [ },
"designer" {
] "id": "edpratti",
}, "name": "Eduardo Pratti",
{ "firstName": "Eduardo",
"id": "tommyemo", "lastName": "Pratti",
"name": "Tom Wellington", "description": "UI designer and developer wannabe. Cares about negative space, layout grids and Bloodborne challenge runs.",
"firstName": "Tom", "socials": {
"lastName": "Wellington", "twitter": "edpratti",
"description": "I design icons and user interfaces, among other things. he/him ✌️", "website": "http://pratti.design"
"socials": { },
"twitter": "tommy_emo_", "pronouns": "he",
"website": "https://www.tommyemo.net/" "profileImg": "./edpratti.jpg",
}, "color": "#FF3300",
"pronouns": "he", "roles": ["designer", "author"]
"profileImg": "./tommyemo.jpg", },
"color": "#8539EB", {
"roles": [ "id": "sarsamurmu",
"designer" "name": "Sarsa Murmu",
] "firstName": "Sarsa",
}, "lastName": "Murmu",
{ "description": "A High School Web Dev. Likes Android Development too. On the way to be an expert in Front-end. Checkout GitHub for more.",
"id": "edpratti", "socials": {
"name": "Eduardo Pratti", "twitter": "sarsamurmu",
"firstName": "Eduardo", "github": "sarsamurmu",
"lastName": "Pratti", "website": "https://sarsamurmu.github.io"
"description": "UI designer and developer wannabe. Cares about negative space, layout grids and Bloodborne challenge runs.", },
"socials": { "pronouns": "he",
"twitter": "edpratti", "profileImg": "./sarsamurmu.png",
"website": "http://pratti.design" "color": "#7C4DFF",
}, "roles": ["developer"]
"pronouns": "he", },
"profileImg": "./edpratti.jpg", {
"color": "#FF3300", "id": "MDutro",
"roles": [ "name": "Micah Dutro",
"designer", "firstName": "Micah",
"author" "lastName": "Dutro",
] "description": "A non-profit lawyer turned budding web developer.",
}, "socials": {
{ "github": "MDutro"
"id": "sarsamurmu", },
"name": "Sarsa Murmu", "pronouns": "he",
"firstName": "Sarsa", "profileImg": "./mdutro.jpg",
"lastName": "Murmu", "color": "#7C4DFF",
"description": "A High School Web Dev. Likes Android Development too. On the way to be an expert in Front-end. Checkout GitHub for more.", "roles": ["developer", "author", "community"]
"socials": { },
"twitter": "sarsamurmu", {
"github": "sarsamurmu", "id": "reikaze",
"website": "https://sarsamurmu.github.io" "name": "Kevin Mai",
}, "firstName": "Kevin",
"pronouns": "he", "lastName": "Mai",
"profileImg": "./sarsamurmu.png", "description": "Hello! I'm Kevin Phong Mai, aka Reikaze or RockmanDash12, a Computer Engineering Student and Freelance Writer passionate about Tech, Anime, Visual Novels and much more. I'm the Owner of RockmanDash Reviews Blog, and I write for the AniTAY & FuwaNovel blogs.",
"color": "#7C4DFF", "socials": {
"roles": [ "twitter": "Reikaze0",
"developer" "github": "Reikaze"
] },
}, "pronouns": "he",
{ "profileImg": "./reikaze.jpg",
"id": "MDutro", "color": "#ba68c8",
"name": "Micah Dutro", "roles": ["author"]
"firstName": "Micah", },
"lastName": "Dutro", {
"description": "A non-profit lawyer turned budding web developer.", "id": "thodges314",
"socials": { "name": "Thomas Hodges",
"github": "MDutro" "firstName": "Thomas",
}, "lastName": "Hodges",
"pronouns": "he", "description": "A software engineer with a mathematical background, professional experience in frontend, primarily with Reactjs and D3js, and a strong interest in mathematical modeling and visualisations.",
"profileImg": "./mdutro.jpg", "socials": {
"color": "#7C4DFF", "github": "thodges314",
"roles": [ "linkedIn": "thomas-hodges"
"developer", },
"author", "pronouns": "he",
"community" "profileImg": "./thodges.png",
] "color": "#ba68c8",
}, "roles": ["author"]
{ },
"id": "reikaze", {
"name": "Kevin Mai", "id": "skatcat31",
"firstName": "Kevin", "name": "Robert Mennell",
"lastName": "Mai", "firstName": "Robert",
"description": "Hello! I'm Kevin Phong Mai, aka Reikaze or RockmanDash12, a Computer Engineering Student and Freelance Writer passionate about Tech, Anime, Visual Novels and much more. I'm the Owner of RockmanDash Reviews Blog, and I write for the AniTAY & FuwaNovel blogs.", "lastName": "Mennell",
"socials": { "description": "A fullstack engineer who loves learning new things, playing video games, and his wife.\nIf you can learn it, you can do it.\nIf you can do it well, you've learned it.",
"twitter": "Reikaze0", "socials": {
"github": "Reikaze" "github": "skatcat31",
}, "linkedIn": "rnmennell"
"pronouns": "he", },
"profileImg": "./reikaze.jpg", "color": "#ba68c8",
"color": "#ba68c8", "profileImg": "./hello.png",
"roles": [ "pronouns": "he",
"author" "roles": ["author", "community"]
] },
}, {
{ "id": "seanmiller",
"id": "thodges314", "name": "Sean Miller",
"name": "Thomas Hodges", "firstName": "Sean",
"firstName": "Thomas", "lastName": "Miller",
"lastName": "Hodges", "description": "Howdy! Computer Science major at Texas A&M University, with a minor in cybersecurity. Super passionate about all things software!",
"description": "A software engineer with a mathematical background, professional experience in frontend, primarily with Reactjs and D3js, and a strong interest in mathematical modeling and visualisations.", "socials": {
"socials": { "twitter": "beastosean",
"github": "thodges314", "github": "tamuseanmiller",
"linkedIn": "thomas-hodges" "website": "https://sean.millerfamily.tech",
}, "linkedIn": "tamuseanmiller"
"pronouns": "he", },
"profileImg": "./thodges.png", "pronouns": "he",
"color": "#ba68c8", "profileImg": "./seanmiller.jpg",
"roles": [ "color": "#551a8b",
"author" "roles": ["author"]
] },
}, {
{ "id": "pierremtb",
"id": "skatcat31", "name": "Pierre Jacquier",
"name": "Robert Mennell", "firstName": "Pierre",
"firstName": "Robert", "lastName": "Jacquier",
"lastName": "Mennell", "description": "Junior Hardware Engineer at Algolux. Computationally curious.",
"description": "A fullstack engineer who loves learning new things, playing video games, and his wife.\nIf you can learn it, you can do it.\nIf you can do it well, you've learned it.", "socials": {
"socials": { "twitter": "PierreJacquier",
"github": "skatcat31", "github": "pierremtb",
"linkedIn": "rnmennell" "website": "https://pierrejacquier.com",
}, "linkedIn": "pierrejacquier"
"color": "#ba68c8", },
"profileImg": "./hello.png", "pronouns": "he",
"pronouns": "he", "profileImg": "./pierremtb.jpg",
"roles": [ "color": "#FFEB3B",
"author", "roles": ["author"]
"community" },
] {
}, "id": "maisydino",
{ "name": "Maisy Dinosaur",
"id": "seanmiller", "firstName": "Maisy",
"name": "Sean Miller", "lastName": "Dinosaur",
"firstName": "Sean", "description": "I do a lot of stuff sometimes. Part-time fullstack, full time dog petter.",
"lastName": "Miller", "socials": {
"description": "Howdy! Computer Science major at Texas A&M University, with a minor in cybersecurity. Super passionate about all things software!", "twitter": "rodentman87",
"socials": { "github": "rodentman87",
"twitter": "beastosean", "website": "https://likesdinosaurs.com"
"github": "tamuseanmiller", },
"website": "https://sean.millerfamily.tech", "pronouns": "she",
"linkedIn": "tamuseanmiller" "profileImg": "./maisydino.jpg",
}, "color": "#FDF6E3",
"pronouns": "he", "roles": ["author", "community"]
"profileImg": "./seanmiller.jpg", },
"color": "#551a8b", {
"roles": [ "id": "bobrossrtx",
"author" "name": "Bobrossrtx",
] "firstName": "Owen",
}, "lastName": "Boreham",
{ "description": "I have over 1000 years of software development experience, do not underestimate me!",
"id": "pierremtb", "socials": {
"name": "Pierre Jacquier", "twitter": "bobrossrtx",
"firstName": "Pierre", "github": "bobrossrtx",
"lastName": "Jacquier", "website": "https://www.owenboreham.tech"
"description": "Junior Hardware Engineer at Algolux. Computationally curious.", },
"socials": { "pronouns": "he",
"twitter": "PierreJacquier", "profileImg": "./bobrossrtx.jpg",
"github": "pierremtb", "color": "#b7e11e",
"website": "https://pierrejacquier.com", "roles": ["developer", "author"]
"linkedIn": "pierrejacquier" },
}, {
"pronouns": "he", "id": "ljtech",
"profileImg": "./pierremtb.jpg", "name": "Landon Johnson",
"color": "#FFEB3B", "firstName": "Landon",
"roles": [ "lastName": "Johnson",
"author" "description": "Hello there, my name is Lj. I am a full stack developer.",
] "socials": {
}, "twitter": "ljtechdotca",
{ "github": "ljtechdotca",
"id": "maisydino", "twitch": "ljtechdotca",
"name": "Maisy Dinosaur", "website": "https://ljtech.ca"
"firstName": "Maisy", },
"lastName": "Dinosaur", "pronouns": "he",
"description": "I do a lot of stuff sometimes. Part-time fullstack, full time dog petter.", "profileImg": "./ljtechdotca.png",
"socials": { "color": "#7b61ff",
"twitter": "rodentman87", "roles": ["author"]
"github": "rodentman87", },
"website": "https://likesdinosaurs.com" {
}, "id": "SkyHawk_0",
"pronouns": "she", "name": "Joshua Hawkins",
"profileImg": "./maisydino.jpg", "firstName": "Joshua",
"color": "#FDF6E3", "lastName": "Hawkins",
"roles": [ "description": "I am a high school student who focuses on python. Most of my scripts I have made where just for fun or to work something out that I couldn't.",
"author", "socials": {},
"community" "pronouns": "he",
] "profileImg": "./goofy.png",
}, "color": "#18BBC9",
{ "roles": ["author"]
"id": "bobrossrtx", },
"name": "Bobrossrtx", {
"firstName": "Owen", "id": "splatkillwill",
"lastName": "Boreham", "name": "William (Will) Lohan",
"description": "I have over 1000 years of software development experience, do not underestimate me!", "firstName": "William",
"socials": { "lastName": "Lohan",
"twitter": "bobrossrtx", "description": "",
"github": "bobrossrtx", "socials": {
"website": "https://www.owenboreham.tech" "github": "william-lohan",
}, "twitch": "splat_killwill",
"pronouns": "he", "website": "https://gatimus.com/",
"profileImg": "./bobrossrtx.jpg", "linkedIn": "william-lohan-b202637a"
"color": "#b7e11e", },
"roles": [ "pronouns": "they/themselves",
"developer", "profileImg": "./splatkillwill.jpg",
"author" "color": "#BF00FF",
] "roles": ["author"]
}, },
{ {
"id": "ljtech", "id": "fmothe",
"name": "Landon Johnson", "name": "Federico Mothe",
"firstName": "Landon", "firstName": "Federico",
"lastName": "Johnson", "lastName": "Mothe",
"description": "Hello there, my name is Lj. I am a full stack developer.", "description": "Software Dev from Argentina learning the dark magic of frontend development and typescript.",
"socials": { "socials": {
"twitter": "ljtechdotca", "twitter": "FedericoMothe",
"github": "ljtechdotca", "github": "fmothe",
"twitch": "ljtechdotca", "twitch": "mothevv"
"website": "https://ljtech.ca" },
}, "pronouns": "he",
"pronouns": "he", "profileImg": "./fmothe.jpg",
"profileImg": "./ljtechdotca.png", "color": "#18BBC9",
"color": "#7b61ff", "roles": ["translator"]
"roles": [ },
"author" {
] "id": "jahirfiquitiva",
}, "name": "Jahir Fiquitiva",
{ "firstName": "Jahir",
"id": "SkyHawk_0", "lastName": "Fiquitiva",
"name": "Joshua Hawkins", "description": "Passionate and creative full-stack software engineer based in Colombia 🇨🇴.",
"firstName": "Joshua", "socials": {
"lastName": "Hawkins", "twitter": "jahirfiquitiva",
"description": "I am a high school student who focuses on python. Most of my scripts I have made where just for fun or to work something out that I couldn't.", "github": "jahirfiquitiva",
"socials": {}, "website": "https://jahir.dev",
"pronouns": "he", "linkedIn": "jahirfiquitiva"
"profileImg": "./goofy.png", },
"color": "#18BBC9", "pronouns": "he",
"roles": [ "profileImg": "./jahirfiquitiva.jpg",
"author" "color": "#3867d6",
] "roles": ["translator"]
}, },
{ {
"id": "splatkillwill", "id": "kaleem",
"name": "William (Will) Lohan", "name": "Kaleem",
"firstName": "William", "firstName": "Kaleem",
"lastName": "Lohan", "lastName": "",
"description": "", "description": "Software Engineer, Simplifying programming, writing about learnings and lessons learned.",
"socials": { "socials": {
"github": "william-lohan", "twitter": "kaleemniz",
"twitch": "splat_killwill", "github": "kaleem68",
"website": "https://gatimus.com/", "linkedIn": "nixamani5"
"linkedIn": "william-lohan-b202637a" },
}, "pronouns": "he",
"pronouns": "they/themselves", "profileImg": "./kaleem.jpeg",
"profileImg": "./splatkillwill.jpg", "color": "#a8b3ba",
"color": "#BF00FF", "roles": ["author"]
"roles": [ },
"author" {
] "id": "qarnax",
}, "name": "Qarnax",
{ "firstName": "",
"id": "fmothe", "lastName": "",
"name": "Federico Mothe", "description": "I'm a frontend developer and indie game enthusiast 👾 \n I enjoy learning new things and building my own stuff 🔧 and I love helping people get into coding 😊",
"firstName": "Federico", "socials": {
"lastName": "Mothe", "twitch": "qarnax_",
"description": "Software Dev from Argentina learning the dark magic of frontend development and typescript.", "twitter": "qarnax",
"socials": { "github": "qarnax801"
"twitter": "FedericoMothe", },
"github": "fmothe", "profileImg": "./qarnax.jpg",
"twitch": "mothevv" "color": "",
}, "roles": ["developer", "author", "community", "translator"]
"pronouns": "he", },
"profileImg": "./fmothe.jpg", {
"color": "#18BBC9", "id": "alexchadwick",
"roles": [ "name": "Alex Chadwick",
"translator" "firstName": "Alex",
] "lastName": "Chadwick",
}, "description": "I'm a full-stack web developer in the UK (but born in sunny Spain!) \n I spend too much time reading articles on clean code and not enough refactoring 🤣",
{ "socials": {
"id": "jahirfiquitiva", "twitch": "alexchadwicc",
"name": "Jahir Fiquitiva", "twitter": "TheAlexChadwick",
"firstName": "Jahir", "github": "AlexChadwickP",
"lastName": "Fiquitiva", "linkedin": "alexchadwickp",
"description": "Passionate and creative full-stack software engineer based in Colombia 🇨🇴.", "website": "https://alexchadwick.com"
"socials": { },
"twitter": "jahirfiquitiva", "pronouns": "he",
"github": "jahirfiquitiva", "profileImg": "./alexchadwick.jpg",
"website": "https://jahir.dev", "color": "",
"linkedIn": "jahirfiquitiva" "roles": ["author", "translator"]
}, },
"pronouns": "he", {
"profileImg": "./jahirfiquitiva.jpg", "id": "williamcook",
"color": "#3867d6", "name": "William George Cook",
"roles": [ "firstName": "William",
"translator" "lastName": "Cook",
] "description": "Full stack developer who loves to help! Find me in the garden, up a tree, or at Disney World ✨",
}, "socials": {
{ "twitter": "wgeorgecook",
"id": "kaleem", "github": "wgeorgecook",
"name": "Kaleem", "linkedin": "wgeorgecook",
"firstName": "Kaleem", "website": "https://williamgeorgecook.com"
"lastName": "", },
"description": "Software Engineer, Simplifying programming, writing about learnings and lessons learned.", "pronouns": "he",
"socials": { "profileImg": "./williamcook.jpg",
"twitter": "kaleemniz", "color": "#AF7AC5",
"github": "kaleem68", "roles": ["author"]
"linkedIn": "nixamani5" },
}, {
"pronouns": "he", "id": "rudy",
"profileImg": "./kaleem.jpeg", "name": "Rudolph Schmitz",
"color": "#a8b3ba", "firstName": "Rudolph",
"roles": [ "lastName": "Schmitz",
"author" "description": "Full stack dev with frontend emphasis. Enjoys all things Typescript, soccer, and my mom is the best.",
] "socials": {
}, "twitter": "rudolphschmitz",
{ "github": "rwschmitz"
"id": "qarnax", },
"name": "Qarnax", "pronouns": "he",
"firstName": "", "profileImg": "./rudy.jpg",
"lastName": "", "color": "#ba68c8",
"description": "I'm a frontend developer and indie game enthusiast 👾 \n I enjoy learning new things and building my own stuff 🔧 and I love helping people get into coding 😊", "roles": ["community"]
"socials": { },
"twitch": "qarnax_", {
"twitter": "qarnax", "id": "LayZee",
"github": "qarnax801" "name": "Lars Gyrup Brink Nielsen",
}, "firstName": "Lars",
"profileImg": "./qarnax.jpg", "lastName": "Gyrup Brink Nielsen",
"color": "", "description": "Hi there, I'm Lars 👋\n\nI am a public tech contributor. I write articles and books, I organize communities, and I maintain open source software.",
"roles": [ "socials": {
"developer", "twitter": "LayZeeDK",
"author", "github": "LayZeeDK",
"community", "twitch": "LayZeeDK"
"translator" },
] "pronouns": "he",
}, "profileImg": "./lars-gyrup-brink-nielsen.jpg",
{ "color": "#1b9bf0",
"id": "alexchadwick", "roles": ["author"]
"name": "Alex Chadwick", }
"firstName": "Alex",
"lastName": "Chadwick",
"description": "I'm a full-stack web developer in the UK (but born in sunny Spain!) \n I spend too much time reading articles on clean code and not enough refactoring 🤣",
"socials": {
"twitch": "alexchadwicc",
"twitter": "TheAlexChadwick",
"github": "AlexChadwickP",
"linkedin": "alexchadwickp",
"website": "https://alexchadwick.com"
},
"pronouns": "he",
"profileImg": "./alexchadwick.jpg",
"color": "",
"roles": [
"author",
"translator"
]
},
{
"id": "williamcook",
"name": "William George Cook",
"firstName": "William",
"lastName": "Cook",
"description": "Full stack developer who loves to help! Find me in the garden, up a tree, or at Disney World ✨",
"socials": {
"twitter": "wgeorgecook",
"github": "wgeorgecook",
"linkedin": "wgeorgecook",
"website": "https://williamgeorgecook.com"
},
"pronouns": "he",
"profileImg": "./williamcook.jpg",
"color": "#AF7AC5",
"roles": [
"author"
]
},
{
"id": "rudy",
"name": "Rudolph Schmitz",
"firstName": "Rudolph",
"lastName": "Schmitz",
"description": "Full stack dev with frontend emphasis. Enjoys all things Typescript, soccer, and my mom is the best.",
"socials": {
"twitter": "rudolphschmitz",
"github": "rwschmitz"
},
"pronouns": "he",
"profileImg": "./rudy.jpg",
"color": "#ba68c8",
"roles": [
"community"
]
},
{
"id": "LayZee",
"name": "Lars Gyrup Brink Nielsen",
"firstName": "Lars",
"lastName": "Gyrup Brink Nielsen",
"description": "Hi there, I'm Lars 👋\n\nI am a public tech contributor. I write articles and books, I organize communities, and I maintain open source software.",
"socials": {
"twitter": "LayZeeDK",
"github": "LayZeeDK",
"twitch": "LayZeeDK"
},
"pronouns": "he",
"profileImg": "./lars-gyrup-brink-nielsen.jpg",
"color": "#1b9bf0",
"roles": [
"author"
]
}
] ]

View File

@@ -1,71 +1,71 @@
{ {
"name": "unicorn-utterances-site", "name": "unicorn-utterances-site",
"private": true, "private": true,
"description": "Learning programming from magically majestic words", "description": "Learning programming from magically majestic words",
"version": "0.3.0-alpha.1", "version": "0.3.0-alpha.1",
"bugs": { "bugs": {
"url": "https://github.com/unicorn-utterances/unicorn-utterances/issues" "url": "https://github.com/unicorn-utterances/unicorn-utterances/issues"
}, },
"homepage": "https://unicorn-utterances.com", "homepage": "https://unicorn-utterances.com",
"keywords": [ "keywords": [
"blog", "blog",
"education", "education",
"programming" "programming"
], ],
"license": "MPL-2.0", "license": "MPL-2.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/unicorn-utterances/unicorn-utterances.git" "url": "git+https://github.com/unicorn-utterances/unicorn-utterances.git"
}, },
"scripts": { "scripts": {
"debug": "node --inspect-brk ./node_modules/astro/astro.js dev --experimental-integrations", "debug": "node --inspect-brk ./node_modules/astro/astro.js dev --experimental-integrations",
"dev": "astro dev --experimental-integrations", "dev": "astro dev --experimental-integrations",
"start": "astro dev --experimental-integrations", "start": "astro dev --experimental-integrations",
"build": "astro build --experimental-integrations", "build": "astro build --experimental-integrations",
"preview": "astro preview --experimental-integrations", "preview": "astro preview --experimental-integrations",
"host:local": "cd dist && ws --http2 --compress", "host:local": "cd dist && ws --http2 --compress",
"format": "prettier -w . --cache --plugin-search-dir=.", "format": "prettier -w . --cache --plugin-search-dir=.",
"lint": "eslint . --ext .js,.ts,.astro" "lint": "eslint . --ext .js,.ts,.astro"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/image": "^0.7.1", "@astrojs/image": "^0.7.1",
"@remark-embedder/core": "^3.0.1", "@remark-embedder/core": "^3.0.1",
"@remark-embedder/transformer-oembed": "^3.0.0", "@remark-embedder/transformer-oembed": "^3.0.0",
"@types/classnames": "^2.3.1", "@types/classnames": "^2.3.1",
"@types/node": "^18.7.18", "@types/node": "^18.7.18",
"@typescript-eslint/parser": "^4.33.0", "@typescript-eslint/parser": "^4.33.0",
"astro": "^1.2.8", "astro": "^1.2.8",
"astro-icon": "^0.7.3", "astro-icon": "^0.7.3",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-plugin-astro": "^0.19.0", "eslint-plugin-astro": "^0.19.0",
"eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-jsx-a11y": "^6.6.1",
"gatsby-remark-embedder": "^6.0.1", "gatsby-remark-embedder": "^6.0.1",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"hast": "^1.0.0", "hast": "^1.0.0",
"hast-util-from-html": "^1.0.0", "hast-util-from-html": "^1.0.0",
"hast-util-has-property": "^2.0.0", "hast-util-has-property": "^2.0.0",
"hast-util-heading-rank": "^2.1.0", "hast-util-heading-rank": "^2.1.0",
"hast-util-to-string": "^2.0.0", "hast-util-to-string": "^2.0.0",
"image-size": "^1.0.2", "image-size": "^1.0.2",
"junk": "^4.0.0", "junk": "^4.0.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"prettier-plugin-astro": "^0.5.4", "prettier-plugin-astro": "^0.5.4",
"rehype-raw": "^6.1.1", "rehype-raw": "^6.1.1",
"rehype-retext": "^3.0.2", "rehype-retext": "^3.0.2",
"rehype-slug-custom-id": "^1.1.0", "rehype-slug-custom-id": "^1.1.0",
"remark-behead": "^3.1.0", "remark-behead": "^3.1.0",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-shiki-twoslash": "^3.1.0", "remark-shiki-twoslash": "^3.1.0",
"remark-unwrap-images": "^3.0.1", "remark-unwrap-images": "^3.0.1",
"retext-english": "^4.1.0", "retext-english": "^4.1.0",
"rollup-plugin-copy": "^3.4.0", "rollup-plugin-copy": "^3.4.0",
"sass": "^1.54.9", "sass": "^1.54.9",
"slash": "^4.0.0", "slash": "^4.0.0",
"terser": "^5.15.0", "terser": "^5.15.0",
"unified": "^10.1.2", "unified": "^10.1.2",
"unist-util-replace-all-between": "^0.1.1", "unist-util-replace-all-between": "^0.1.1",
"unist-util-visit": "^4.1.1" "unist-util-visit": "^4.1.1"
} }
} }

View File

@@ -1,51 +1,51 @@
{ {
"name": "Unicorn Utterances", "name": "Unicorn Utterances",
"short_name": "Unicorn Utterances", "short_name": "Unicorn Utterances",
"start_url": "/", "start_url": "/",
"background_color": "#ffffff", "background_color": "#ffffff",
"theme_color": "#127db3", "theme_color": "#127db3",
"display": "minimal-ui", "display": "minimal-ui",
"cacheDigest": "2a", "cacheDigest": "2a",
"icons": [ "icons": [
{ {
"src": "icons/icon-48x48.png?v=2a", "src": "icons/icon-48x48.png?v=2a",
"sizes": "48x48", "sizes": "48x48",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "icons/icon-72x72.png?v=2a", "src": "icons/icon-72x72.png?v=2a",
"sizes": "72x72", "sizes": "72x72",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "icons/icon-96x96.png?v=2a", "src": "icons/icon-96x96.png?v=2a",
"sizes": "96x96", "sizes": "96x96",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "icons/icon-144x144.png?v=2a", "src": "icons/icon-144x144.png?v=2a",
"sizes": "144x144", "sizes": "144x144",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "icons/icon-192x192.png?v=2a", "src": "icons/icon-192x192.png?v=2a",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "icons/icon-256x256.png?v=2a", "src": "icons/icon-256x256.png?v=2a",
"sizes": "256x256", "sizes": "256x256",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "icons/icon-384x384.png?v=2a", "src": "icons/icon-384x384.png?v=2a",
"sizes": "384x384", "sizes": "384x384",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "icons/icon-512x512.png?v=2a", "src": "icons/icon-512x512.png?v=2a",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
} }
] ]
} }

View File

@@ -1,26 +1,26 @@
/* AFTER CHANGING THIS FILE, PLEASE MANUALLY MINIFY IT AND PUT INTO backbtn.min.js */ /* AFTER CHANGING THIS FILE, PLEASE MANUALLY MINIFY IT AND PUT INTO backbtn.min.js */
/* TODO: Add minifier to build script */ /* TODO: Add minifier to build script */
window.onload = () => { window.onload = () => {
const backBtn = document.querySelector('#backbtn'); const backBtn = document.querySelector("#backbtn");
let hasHistory = false; let hasHistory = false;
window.addEventListener('beforeunload', () => { window.addEventListener("beforeunload", () => {
hasHistory = true; hasHistory = true;
}) });
backBtn.addEventListener('click', () => { backBtn.addEventListener("click", () => {
if (!document.referrer) { if (!document.referrer) {
// This is the first page the user has visited on the site in this session // This is the first page the user has visited on the site in this session
window.location.href = '/'; window.location.href = "/";
return; return;
} }
history.back(); history.back();
// User cannot go back, meaning that we're at the first page of the site session // User cannot go back, meaning that we're at the first page of the site session
setTimeout(() => { setTimeout(() => {
if (!hasHistory){ if (!hasHistory) {
window.location.href = "/"; window.location.href = "/";
} }
}, 200); }, 200);
}) });
} };

View File

@@ -1,167 +1,170 @@
/* AFTER CHANGING THIS FILE, PLEASE MANUALLY MINIFY IT AND PUT INTO tabs.min.js */ /* AFTER CHANGING THIS FILE, PLEASE MANUALLY MINIFY IT AND PUT INTO tabs.min.js */
const LOCAL_STORAGE_KEY = "tabs-selection"; const LOCAL_STORAGE_KEY = "tabs-selection";
window.addEventListener('DOMContentLoaded', () => { window.addEventListener("DOMContentLoaded", () => {
const tabLists = document.querySelectorAll('[role="tablist"]'); const tabLists = document.querySelectorAll('[role="tablist"]');
tabLists.forEach(tabList => { tabLists.forEach((tabList) => {
/** /**
* @type {NodeListOf<HTMLElement>} * @type {NodeListOf<HTMLElement>}
*/ */
const tabs = tabList.querySelectorAll('[role="tab"]'); const tabs = tabList.querySelectorAll('[role="tab"]');
// Add a click event handler to each tab // Add a click event handler to each tab
tabs.forEach((tab) => { tabs.forEach((tab) => {
tab.addEventListener('click', e => { tab.addEventListener("click", (e) => {
/** /**
* @type {HTMLElement} * @type {HTMLElement}
*/ */
const target = e.target; const target = e.target;
// Scroll onto screen in order to avoid jumping page locations // Scroll onto screen in order to avoid jumping page locations
setTimeout(() => { setTimeout(() => {
target.scrollIntoView({ target.scrollIntoView({
behavior: "auto", behavior: "auto",
block: "center", block: "center",
inline: "center", inline: "center",
}); });
}, 0); }, 0);
changeTabs({ target }) changeTabs({ target });
}); });
}); });
// Enable arrow navigation between tabs in the tab list // Enable arrow navigation between tabs in the tab list
let tabFocus = 0; let tabFocus = 0;
tabList.addEventListener('keydown', (_e) => { tabList.addEventListener("keydown", (_e) => {
/** /**
* @type {KeyboardEvent} * @type {KeyboardEvent}
*/ */
const e = _e; const e = _e;
// Move right // Move right
if (e.keyCode === 39 || e.keyCode === 37) { if (e.keyCode === 39 || e.keyCode === 37) {
tabs[tabFocus].setAttribute('tabindex', `-1`); tabs[tabFocus].setAttribute("tabindex", `-1`);
if (e.keyCode === 39) { if (e.keyCode === 39) {
tabFocus++; tabFocus++;
// If we're at the end, go to the start // If we're at the end, go to the start
if (tabFocus >= tabs.length) { if (tabFocus >= tabs.length) {
tabFocus = 0; tabFocus = 0;
} }
// Move left // Move left
} else if (e.keyCode === 37) { } else if (e.keyCode === 37) {
tabFocus--; tabFocus--;
// If we're at the start, move to the end // If we're at the start, move to the end
if (tabFocus < 0) { if (tabFocus < 0) {
tabFocus = tabs.length - 1; tabFocus = tabs.length - 1;
} }
} }
tabs[tabFocus].setAttribute('tabindex', `0`); tabs[tabFocus].setAttribute("tabindex", `0`);
tabs[tabFocus].focus(); tabs[tabFocus].focus();
tabs[tabFocus].click(); tabs[tabFocus].click();
} }
}); });
}); });
const currentTab = localStorage.getItem(LOCAL_STORAGE_KEY); const currentTab = localStorage.getItem(LOCAL_STORAGE_KEY);
if (currentTab) { if (currentTab) {
/** /**
* @type {HTMLElement} * @type {HTMLElement}
*/ */
const el = document.querySelector(`[data-tabname="${currentTab}"]`); const el = document.querySelector(`[data-tabname="${currentTab}"]`);
if (el) changeTabs({ target: el }); if (el) changeTabs({ target: el });
} }
function changeTabs(_e) { function changeTabs(_e) {
/** /**
* @type {{ target: HTMLElement }} * @type {{ target: HTMLElement }}
*/ */
const e = _e; const e = _e;
const target = e.target; const target = e.target;
const parent = target.parentNode; const parent = target.parentNode;
const grandparent = parent.parentNode; const grandparent = parent.parentNode;
// Remove all current selected tabs // Remove all current selected tabs
parent parent
.querySelectorAll('[aria-selected="true"]') .querySelectorAll('[aria-selected="true"]')
.forEach((t) => t.setAttribute('aria-selected', `false`)); .forEach((t) => t.setAttribute("aria-selected", `false`));
// Set this tab as selected // Set this tab as selected
target.setAttribute('aria-selected', `true`); target.setAttribute("aria-selected", `true`);
const tabName = target.dataset.tabname; const tabName = target.dataset.tabname;
/** /**
* @type {NodeListOf<HTMLElement>} * @type {NodeListOf<HTMLElement>}
*/ */
const relatedTabs = document.querySelectorAll(`[role="tab"][data-tabname="${target.dataset.tabname}"]`); const relatedTabs = document.querySelectorAll(
`[role="tab"][data-tabname="${target.dataset.tabname}"]`
localStorage.setItem(LOCAL_STORAGE_KEY, tabName); );
for (let relatedTab of relatedTabs) { localStorage.setItem(LOCAL_STORAGE_KEY, tabName);
if (relatedTab === target) continue;
changeTabs({ target: relatedTab }); for (let relatedTab of relatedTabs) {
} if (relatedTab === target) continue;
changeTabs({ target: relatedTab });
// Hide all tab panels }
grandparent
.querySelectorAll('[role="tabpanel"]') // Hide all tab panels
.forEach((p) => p.setAttribute('hidden', `true`)); grandparent
.querySelectorAll('[role="tabpanel"]')
// Show the selected panel .forEach((p) => p.setAttribute("hidden", `true`));
grandparent.parentNode
.querySelector(`#${target.getAttribute('aria-controls')}`) // Show the selected panel
.removeAttribute('hidden'); grandparent.parentNode
} .querySelector(`#${target.getAttribute("aria-controls")}`)
.removeAttribute("hidden");
/* -------------------- */ }
/** /* -------------------- */
*
* @param {HTMLElement} el /**
* @param {(el: HTMLElement) => boolean} check *
* @returns {boolean} * @param {HTMLElement} el
*/ * @param {(el: HTMLElement) => boolean} check
function checkElementsParents(el, check) { * @returns {boolean}
if (el.parentElement) { */
if (!check(el.parentElement)) { function checkElementsParents(el, check) {
return checkElementsParents(el.parentElement, check); if (el.parentElement) {
} else { if (!check(el.parentElement)) {
return true; return checkElementsParents(el.parentElement, check);
} } else {
} else { return true;
return false; }
} } else {
} return false;
}
(() => { }
// If user has linked to a heading that's inside of a tab
const hash = window.location.hash; (() => {
if (!hash) return; // If user has linked to a heading that's inside of a tab
const heading = document.querySelector < HTMLElement > (hash); const hash = window.location.hash;
if (!heading) return; if (!hash) return;
const isHidden = checkElementsParents(heading, el => const heading = document.querySelector < HTMLElement > hash;
el.hasAttribute('hidden') && el.getAttribute('hidden') !== "false" if (!heading) return;
) const isHidden = checkElementsParents(
// If it's not hidden, then we can assume that the browser will auto-scroll to it heading,
if (!isHidden) return; (el) => el.hasAttribute("hidden") && el.getAttribute("hidden") !== "false"
const partialHash = hash.slice(1); );
try { // If it's not hidden, then we can assume that the browser will auto-scroll to it
const matchingTab = document.querySelector < HTMLElement > ( if (!isHidden) return;
`[data-headers*="${partialHash}"` const partialHash = hash.slice(1);
); try {
if (!matchingTab) return; const matchingTab =
// If header is not in a tab document.querySelector <
const tabName = matchingTab.getAttribute("data-tabname"); HTMLElement >
if (!tabName) return; `[data-headers*="${partialHash}"`;
matchingTab.click(); if (!matchingTab) return;
setTimeout(() => { // If header is not in a tab
const el = document.querySelector(hash); const tabName = matchingTab.getAttribute("data-tabname");
if (!el) return; if (!tabName) return;
el.scrollIntoView(true); matchingTab.click();
}, 0); setTimeout(() => {
} catch (e) { const el = document.querySelector(hash);
console.error("Error finding matching tab", e); if (!el) return;
} el.scrollIntoView(true);
})() }, 0);
} catch (e) {
}); console.error("Error finding matching tab", e);
}
})();
});

View File

@@ -1,28 +1,29 @@
/* AFTER CHANGING THIS FILE, PLEASE MANUALLY MINIFY IT AND PUT INTO tabs.min.js */ /* AFTER CHANGING THIS FILE, PLEASE MANUALLY MINIFY IT AND PUT INTO tabs.min.js */
const COLOR_MODE_STORAGE_KEY = "currentTheme"; const COLOR_MODE_STORAGE_KEY = "currentTheme";
const themeToggleBtn = document.querySelector('#theme-toggle-button'); const themeToggleBtn = document.querySelector("#theme-toggle-button");
const darkIconEl = document.querySelector('#dark-icon'); const darkIconEl = document.querySelector("#dark-icon");
const lightIconEl = document.querySelector('#light-icon'); const lightIconEl = document.querySelector("#light-icon");
function toggleButton(theme) { function toggleButton(theme) {
themeToggleBtn.ariaPressed = `${theme === 'dark'}`; themeToggleBtn.ariaPressed = `${theme === "dark"}`;
if (theme === 'light') { if (theme === "light") {
lightIconEl.style.display = null; lightIconEl.style.display = null;
darkIconEl.style.display = 'none'; darkIconEl.style.display = "none";
} else { } else {
lightIconEl.style.display = 'none'; lightIconEl.style.display = "none";
darkIconEl.style.display = null; darkIconEl.style.display = null;
} }
} }
// TODO: Migrate to `classList` // TODO: Migrate to `classList`
const initialTheme = document.documentElement.className; const initialTheme = document.documentElement.className;
toggleButton(initialTheme); toggleButton(initialTheme);
themeToggleBtn.addEventListener('click', () => { themeToggleBtn.addEventListener("click", () => {
const currentTheme = document.documentElement.className; const currentTheme = document.documentElement.className;
document.documentElement.className = currentTheme === 'light' ? 'dark' : 'light'; document.documentElement.className =
// TODO: Persist new setting currentTheme === "light" ? "dark" : "light";
const newTheme = document.documentElement.className; // TODO: Persist new setting
toggleButton(newTheme); const newTheme = document.documentElement.className;
localStorage.setItem(COLOR_MODE_STORAGE_KEY, newTheme) toggleButton(newTheme);
}) localStorage.setItem(COLOR_MODE_STORAGE_KEY, newTheme);
});

View File

@@ -1,14 +1,14 @@
self.addEventListener("install", function (e) { self.addEventListener("install", function (e) {
self.skipWaiting(); self.skipWaiting();
}); });
self.addEventListener("activate", function (e) { self.addEventListener("activate", function (e) {
self.registration self.registration
.unregister() .unregister()
.then(function () { .then(function () {
return self.clients.matchAll(); return self.clients.matchAll();
}) })
.then(function (clients) { .then(function (clients) {
clients.forEach((client) => client.navigate(client.url)); clients.forEach((client) => client.navigate(client.url));
}); });
}); });

View File

@@ -1,10 +1,10 @@
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker navigator.serviceWorker
.register("/sw.js") .register("/sw.js")
.then(serviceWorker => { .then((serviceWorker) => {
console.log("Service Worker registered: ", serviceWorker); console.log("Service Worker registered: ", serviceWorker);
}) })
.catch(error => { .catch((error) => {
console.error("Error registering the Service Worker: ", error); console.error("Error registering the Service Worker: ", error);
}); });
} }

View File

@@ -3,13 +3,13 @@ import layoutStyles from "./blog-post-layout.module.scss";
--- ---
<div class={layoutStyles.blogPostLayoutContainer}> <div class={layoutStyles.blogPostLayoutContainer}>
<div class={layoutStyles.leftContainer}> <div class={layoutStyles.leftContainer}>
<slot name="left"/> <slot name="left" />
</div> </div>
<div class={layoutStyles.centerContainer}> <div class={layoutStyles.centerContainer}>
<slot/> <slot />
</div> </div>
<div class={layoutStyles.rightContainer}> <div class={layoutStyles.rightContainer}>
<slot name="right"/> <slot name="right" />
</div> </div>
</div> </div>

View File

@@ -1,16 +1,28 @@
--- ---
import { Icon } from 'astro-icon'; import { Icon } from "astro-icon";
import btnStyles from "./dark-light-button.module.scss"; import btnStyles from "./dark-light-button.module.scss";
--- ---
<button <button
class={`${btnStyles.darkLightBtn} baseBtn`} class={`${btnStyles.darkLightBtn} baseBtn`}
id="theme-toggle-button" id="theme-toggle-button"
aria-pressed="false" aria-pressed="false"
aria-label={"Is dark mode enabled?"} aria-label={"Is dark mode enabled?"}
> >
<Icon name="dark" height="36" width="36" id="dark-icon" style="display: none;"/> <Icon
<Icon name="light" height="36" width="36" id="light-icon" style="display: none;"/> name="dark"
height="36"
width="36"
id="dark-icon"
style="display: none;"
/>
<Icon
name="light"
height="36"
width="36"
id="light-icon"
style="display: none;"
/>
</button> </button>
<script defer is:inline src="/scripts/themetoggle.min.js"/> <script defer is:inline src="/scripts/themetoggle.min.js"></script>

View File

@@ -1,39 +1,55 @@
--- ---
import layoutStyles from "./layout.module.scss"; import layoutStyles from "./layout.module.scss";
import DarkLightButton from "components/dark-light-button/dark-light-button.astro"; import DarkLightButton from "components/dark-light-button/dark-light-button.astro";
import { Icon } from 'astro-icon'; import { Icon } from "astro-icon";
// const { back } = useHistory(); // const { back } = useHistory();
const rootPath = `/`; const rootPath = `/`;
const isBase = Astro.url.pathname === rootPath; const isBase = Astro.url.pathname === rootPath;
const isBlogPost = Astro.url.pathname.startsWith(`${rootPath}posts`); const isBlogPost = Astro.url.pathname.startsWith(`${rootPath}posts`);
const isCollection = Astro.url.pathname.startsWith(`${rootPath}collections`); const isCollection = Astro.url.pathname.startsWith(`${rootPath}collections`);
---
---
<div class={layoutStyles.horizCenter}> <div class={layoutStyles.horizCenter}>
<header class={layoutStyles.header} aria-label={"Toolbar for primary action buttons"}> <header
<div class={layoutStyles.headerInsideContainer}> class={layoutStyles.header}
{!isBase ? (<> aria-label={"Toolbar for primary action buttons"}
<button id="backbtn" class={`${layoutStyles.backBtn} baseBtn`} aria-label="Go back"> >
<Icon height="36" width="36" name="back" /> <div class={layoutStyles.headerInsideContainer}>
</button> {
<script is:inline defer src="/scripts/backbtn.min.js"/> !isBase ? (
</> <>
) : ( <button
<div /> id="backbtn"
)} class={`${layoutStyles.backBtn} baseBtn`}
<div class={layoutStyles.iconList}> aria-label="Go back"
<!-- <AnalyticsLink category={"outbound"} href="https://discord.gg/FMcvc6T" className={"baseBtn"} >
aria-label={"Join the Discord"}> --> <Icon height="36" width="36" name="back" />
<Icon height="36" width="36" name="discord" /> </button>
<!-- </AnalyticsLink> --> <script is:inline defer src="/scripts/backbtn.min.js" />
<DarkLightButton /> </>
</div> ) : (
</div> <div />
</header> )
<div class={ isCollection ? "" : !isBlogPost ? "listViewContent" : "postViewContent" }> }
<slot /> <div class={layoutStyles.iconList}>
</div> <!-- <AnalyticsLink category={"outbound"} href="https://discord.gg/FMcvc6T" className={"baseBtn"}
</div> aria-label={"Join the Discord"}> -->
<Icon height="36" width="36" name="discord" />
<!-- </AnalyticsLink> -->
<DarkLightButton />
</div>
</div>
</header>
<div
class={isCollection
? ""
: !isBlogPost
? "listViewContent"
: "postViewContent"}
>
<slot />
</div>
</div>

View File

@@ -1,112 +1,112 @@
export interface Page { export interface Page {
display: string; display: string;
pageNumber: number; pageNumber: number;
ariaLabel?: string; ariaLabel?: string;
} }
export const DR = { export const DR = {
ariaLabel: "Go to the next set of pages", ariaLabel: "Go to the next set of pages",
display: '...' display: "...",
}; };
export const DL = { export const DL = {
ariaLabel: "Go to previous set of pages", ariaLabel: "Go to previous set of pages",
display: '...' display: "...",
}; };
const range = (start: number, end: number): Page[] => { const range = (start: number, end: number): Page[] => {
let length = end - start + 1; const length = end - start + 1;
return Array.from({ length }, (_, idx) => { return Array.from({ length }, (_, idx) => {
const page = idx + start; const page = idx + start;
return { return {
display: String(page), display: String(page),
pageNumber: page, pageNumber: page,
ariaLabel: `Goto page ${page}` ariaLabel: `Goto page ${page}`,
} };
}); });
}; };
interface GetPaginationRangeProps { interface GetPaginationRangeProps {
totalCount: number; totalCount: number;
pageSize: number; pageSize: number;
siblingCount?: number; siblingCount?: number;
currentPage: number; currentPage: number;
} }
export const getPaginationRange = ({ export const getPaginationRange = ({
totalCount, totalCount,
pageSize, pageSize,
siblingCount = 1, siblingCount = 1,
currentPage currentPage,
}: GetPaginationRangeProps): Page[] => { }: GetPaginationRangeProps): Page[] => {
const totalPageCount = Math.ceil(totalCount / pageSize); const totalPageCount = Math.ceil(totalCount / pageSize);
const totalPageCountPage: Page = { const totalPageCountPage: Page = {
display: `${totalPageCount}`, display: `${totalPageCount}`,
pageNumber: totalPageCount pageNumber: totalPageCount,
} };
const totalPageNumbers = siblingCount + 5; const totalPageNumbers = siblingCount + 5;
if (totalPageNumbers >= totalPageCount) { if (totalPageNumbers >= totalPageCount) {
return range(1, totalPageCount); return range(1, totalPageCount);
} }
const leftSiblingIndex = Math.max(currentPage - siblingCount, 1); const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
const rightSiblingIndex = Math.min( const rightSiblingIndex = Math.min(
currentPage + siblingCount, currentPage + siblingCount,
totalPageCount totalPageCount
); );
const shouldShowLeftDots = leftSiblingIndex > 2; const shouldShowLeftDots = leftSiblingIndex > 2;
const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2; const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2;
const firstPageIndex: Page = { const firstPageIndex: Page = {
display: "1", display: "1",
pageNumber: 1 pageNumber: 1,
}; };
const lastPageIndex: Page = { const lastPageIndex: Page = {
display: `${totalPageCount}`, display: `${totalPageCount}`,
pageNumber: totalPageCount pageNumber: totalPageCount,
}; };
if (!shouldShowLeftDots && shouldShowRightDots) { if (!shouldShowLeftDots && shouldShowRightDots) {
let leftItemCount = 3 + 2 * siblingCount; const leftItemCount = 3 + 2 * siblingCount;
let leftRange = range(1, leftItemCount); const leftRange = range(1, leftItemCount);
const lastPage = leftRange[leftRange.length - 1] const lastPage = leftRange[leftRange.length - 1];
const DR_Page: Page = { const DR_Page: Page = {
...DR, ...DR,
pageNumber: currentPage + 2 pageNumber: currentPage + 2,
} };
return [...leftRange, DR_Page, totalPageCountPage]; return [...leftRange, DR_Page, totalPageCountPage];
} }
if (shouldShowLeftDots && !shouldShowRightDots) { if (shouldShowLeftDots && !shouldShowRightDots) {
let rightItemCount = 3 + 2 * siblingCount; const rightItemCount = 3 + 2 * siblingCount;
let rightRange = range( const rightRange = range(
totalPageCount - rightItemCount + 1, totalPageCount - rightItemCount + 1,
totalPageCount totalPageCount
); );
const DL_Page: Page = { const DL_Page: Page = {
...DL, ...DL,
pageNumber: currentPage - 2 pageNumber: currentPage - 2,
} };
return [firstPageIndex, DL_Page, ...rightRange]; return [firstPageIndex, DL_Page, ...rightRange];
} }
if (shouldShowLeftDots && shouldShowRightDots) { if (shouldShowLeftDots && shouldShowRightDots) {
let middleRange = range(leftSiblingIndex, rightSiblingIndex); const middleRange = range(leftSiblingIndex, rightSiblingIndex);
const DL_Page: Page = { const DL_Page: Page = {
...DL, ...DL,
pageNumber: currentPage - 2 pageNumber: currentPage - 2,
} };
const DR_Page: Page = { const DR_Page: Page = {
...DR, ...DR,
pageNumber: currentPage + 2 pageNumber: currentPage + 2,
} };
return [firstPageIndex, DL_Page, ...middleRange, DR_Page, lastPageIndex]; return [firstPageIndex, DL_Page, ...middleRange, DR_Page, lastPageIndex];
} }
return []; return [];
}; };

View File

@@ -1,31 +1,31 @@
--- ---
import { getPaginationRange } from './pagination-logic'; import { getPaginationRange } from "./pagination-logic";
import { Page } from 'astro'; import { Page } from "astro";
import { PostInfo } from 'types/PostInfo'; import { PostInfo } from "types/PostInfo";
import styles from './pagination.module.scss'; import styles from "./pagination.module.scss";
interface PaginationProps { interface PaginationProps {
page: Pick<Page<PostInfo>, 'total' | 'currentPage' | 'size' | 'lastPage' | 'url'>; page: Pick<
class: string; Page<PostInfo>,
rootURL: string; "total" | "currentPage" | "size" | "lastPage" | "url"
>;
class: string;
rootURL: string;
} }
const { const { page, rootURL, class: className = "" } = Astro.props as PaginationProps;
page,
rootURL,
class: className = ""
} = Astro.props as PaginationProps;
const paginationRange = getPaginationRange({ const paginationRange = getPaginationRange({
currentPage: page.currentPage, currentPage: page.currentPage,
totalCount: page.total, totalCount: page.total,
siblingCount: 0, siblingCount: 0,
pageSize: page.size pageSize: page.size,
}); });
const dontShowAnything = page.currentPage === 0 || paginationRange.length < 2; const dontShowAnything = page.currentPage === 0 || paginationRange.length < 2;
const getPageHref = (pageNum: number) => pageNum === 0 || pageNum === 1 ? rootURL : `${rootURL}page/${pageNum}`; const getPageHref = (pageNum: number) =>
pageNum === 0 || pageNum === 1 ? rootURL : `${rootURL}page/${pageNum}`;
const lastPage = paginationRange[paginationRange.length - 1]; const lastPage = paginationRange[paginationRange.length - 1];
const firstPage = paginationRange[0]; const firstPage = paginationRange[0];
@@ -34,27 +34,47 @@ const disablePrevious = !firstPage || page.currentPage === firstPage.pageNumber;
const disableNext = !lastPage || page.currentPage === lastPage.pageNumber; const disableNext = !lastPage || page.currentPage === lastPage.pageNumber;
--- ---
{dontShowAnything ? null : <ul role="navigation" aria-label="Pagination Navigation" class={`${styles.pagination} ${className}`}> {
{!disablePrevious && <li class={`${styles.paginationItem} ${styles.previous}`}> dontShowAnything ? null : (
<a href={getPageHref(page.currentPage - 1)} aria-label="Previous"> <ul
{"<"} role="navigation"
</a> aria-label="Pagination Navigation"
</li>} class={`${styles.pagination} ${className}`}
>
{!disablePrevious && (
<li class={`${styles.paginationItem} ${styles.previous}`}>
<a href={getPageHref(page.currentPage - 1)} aria-label="Previous">
{"<"}
</a>
</li>
)}
{paginationRange.map(pageItem => { {paginationRange.map((pageItem) => {
const isSelected = pageItem.pageNumber === page.currentPage; const isSelected = pageItem.pageNumber === page.currentPage;
return ( return (
<li class={`${styles.paginationItem} ${ isSelected ? styles.active : '' }`}> <li
<a href={getPageHref(pageItem.pageNumber)} aria-label={pageItem.ariaLabel} aria-current={isSelected || undefined}> class={`${styles.paginationItem} ${
{pageItem.display} isSelected ? styles.active : ""
</a> }`}
</li> >
); <a
})} href={getPageHref(pageItem.pageNumber)}
aria-label={pageItem.ariaLabel}
aria-current={isSelected || undefined}
>
{pageItem.display}
</a>
</li>
);
})}
{!disableNext && <li class={`${styles.paginationItem} ${styles.next}`}> {!disableNext && (
<a href={getPageHref(page.currentPage + 1)} aria-label="Next"> <li class={`${styles.paginationItem} ${styles.next}`}>
{">"} <a href={getPageHref(page.currentPage + 1)} aria-label="Next">
</a> {">"}
</li>} </a>
</ul>} </li>
)}
</ul>
)
}

View File

@@ -5,12 +5,12 @@ import { PostInfo } from "types/PostInfo";
// import { PostListContext } from "constants/post-list-context"; // import { PostListContext } from "constants/post-list-context";
export interface PostListProps { export interface PostListProps {
showWordCount?: boolean; showWordCount?: boolean;
numberOfArticles?: number; numberOfArticles?: number;
wordCount?: number; wordCount?: number;
unicornData?: PostInfo['authorsMeta']; unicornData?: PostInfo["authorsMeta"];
listAriaLabel: string; listAriaLabel: string;
postsToDisplay: PostInfo[] postsToDisplay: PostInfo[];
} }
/** /**
* unicornData - The data with the associated post. If present - you're on profile page * unicornData - The data with the associated post. If present - you're on profile page
@@ -18,12 +18,7 @@ export interface PostListProps {
const { listAriaLabel, postsToDisplay } = Astro.props as PostListProps; const { listAriaLabel, postsToDisplay } = Astro.props as PostListProps;
--- ---
<ul
class={listStyle.postsListContainer} <ul class={listStyle.postsListContainer} aria-label={listAriaLabel} role="list">
aria-label={listAriaLabel} {postsToDisplay.map((post) => <PostCard post={post} />)}
role="list" </ul>
>
{postsToDisplay.map((post) => (
<PostCard post={post}/>
))}
</ul>

View File

@@ -7,61 +7,52 @@ import dayjs from "dayjs";
import { PostInfo } from "types/PostInfo"; import { PostInfo } from "types/PostInfo";
interface PostCardProps { interface PostCardProps {
post: PostInfo; // Info on the authors of the post post: PostInfo; // Info on the authors of the post
class?: string; // class to pass to the post card element class?: string; // class to pass to the post card element
} }
const { const { post, class: className = "" } = Astro.props as PostCardProps;
post,
class: className = ""
} = Astro.props as PostCardProps;
const { const { published, slug, title, authorsMeta, tags, description, excerpt } =
published, post;
slug,
title,
authorsMeta,
tags,
description,
excerpt
} = post;
const publishedStr = dayjs(published).format("MMMM D, YYYY"); const publishedStr = dayjs(published).format("MMMM D, YYYY");
--- ---
<li class={`${cardStyles.card} ${className}`} role="listitem"> <li class={`${cardStyles.card} ${className}`} role="listitem">
<div class={cardStyles.cardContents}> <div class={cardStyles.cardContents}>
<a href={`/posts/${slug}`} class="unlink"> <a href={`/posts/${slug}`} class="unlink">
<h2 class={cardStyles.header}> <h2 class={cardStyles.header}>
{title} {title}
</h2> </h2>
</a> </a>
<p class={cardStyles.authorName}> <p class={cardStyles.authorName}>
<span>by&nbsp;</span> <span>by&nbsp;</span>
<a class={cardStyles.authorLink} href={`/unicorns/${authorsMeta[0].id}`}> <a class={cardStyles.authorLink} href={`/unicorns/${authorsMeta[0].id}`}>
{authorsMeta[0].name} {authorsMeta[0].name}
</a> </a>
<!-- To avoid having commas on the first author name, we did this --> <!-- To avoid having commas on the first author name, we did this -->
{authorsMeta.slice(1).map((author, i) => { {
return ( authorsMeta.slice(1).map((author, i) => {
<> return (
<span>, </span> <>
<a href={`/unicorns/${author.id}`} class={cardStyles.authorLink}> <span>, </span>
{author.name} <a href={`/unicorns/${author.id}`} class={cardStyles.authorLink}>
</a> {author.name}
</> </a>
); </>
})} );
</p> })
<div class={cardStyles.dateTagSubheader}> }
<p class={cardStyles.date}>{publishedStr}</p> </p>
{tags.map((tag) => ( <div class={cardStyles.dateTagSubheader}>
<span class={cardStyles.tag}> <p class={cardStyles.date}>{publishedStr}</p>
{tag} {tags.map((tag) => <span class={cardStyles.tag}>{tag}</span>)}
</span> </div>
))} <p class={cardStyles.excerpt} set:html={description || excerpt}></p>
</div> </div>
<p class={cardStyles.excerpt} set:html={description || excerpt} /> <UserProfilePic
</div> authors={authorsMeta}
<UserProfilePic authors={authorsMeta} className={cardStyles.authorImagesContainer} /> className={cardStyles.authorImagesContainer}
</li> />
</li>

View File

@@ -1,3 +1,3 @@
{/* Google Analytics */} {/* Google Analytics */}
<link rel="preconnect" href="https://www.google.com" /> <link rel="preconnect" href="https://www.google.com" />
<link rel="preconnect" href="https://marketingplatform.google.com" /> <link rel="preconnect" href="https://marketingplatform.google.com" />

View File

@@ -1,19 +1,33 @@
--- ---
import {SEOProps} from './shared'; import { SEOProps } from "./shared";
type Props = Pick<SEOProps, 'editedTime' | 'publishedTime' | 'keywords' | 'unicornsData'>; type Props = Pick<
const { keywords, editedTime, publishedTime, unicornsData } = Astro.props as Props; SEOProps,
const author = unicornsData.map(uni => uni.name).join(","); "editedTime" | "publishedTime" | "keywords" | "unicornsData"
--- >;
const { keywords, editedTime, publishedTime, unicornsData } =
<>{keywords?.length ? keywords.map(keyword => Astro.props as Props;
<meta property="article:tag" content={keyword} />) : null} const author = unicornsData.map((uni) => uni.name).join(",");
</> ---
<meta property="article:section" content="Technology" />
<meta property="article:author" content={author}/> <>
<>{editedTime && {
<meta property="article:modified_time" content={editedTime} />} keywords?.length
</> ? keywords.map((keyword) => (
<>{publishedTime && <meta property="article:tag" content={keyword} />
<meta property="article:published_time" content={publishedTime} />} ))
</> : null
}
</>
<meta property="article:section" content="Technology" />
<meta property="article:author" content={author} />
<>
{editedTime && <meta property="article:modified_time" content={editedTime} />}
</>
<>
{
publishedTime && (
<meta property="article:published_time" content={publishedTime} />
)
}
</>

View File

@@ -1,17 +1,13 @@
--- ---
import {SEOProps} from './shared'; import { SEOProps } from "./shared";
type Props = Pick<SEOProps, 'publishedTime' | 'isbn' | 'unicornsData'>; type Props = Pick<SEOProps, "publishedTime" | "isbn" | "unicornsData">;
const { const { publishedTime, unicornsData, isbn } = Astro.props as Props;
publishedTime,
unicornsData, const author = unicornsData!.map((uni) => uni.name).join(",");
isbn ---
} = Astro.props as Props;
<meta property="book:release_date" content={publishedTime} />
const author = unicornsData!.map((uni)=> uni.name).join(","); <meta property="book:author" content={author} />
--- <>{isbn && <meta property="book:isbn" content={isbn} />}</>
<meta property="book:release_date" content={publishedTime} />
<meta property="book:author" content={author} />
<>{isbn && <meta property="book:isbn" content={isbn} />}</>

View File

@@ -1,34 +1,42 @@
--- ---
import { removePrefixLanguageFromPath } from "utils/translations";
import { removePrefixLanguageFromPath } from 'utils/translations'; import { SEOProps } from "./shared";
import {SEOProps} from './shared';
type Props = Pick<SEOProps, "langData"> & {
type Props = Pick<SEOProps, 'langData'> & { pathName: string;
pathName: string; siteMetadata: { siteUrl: string };
siteMetadata: {siteUrl: string} };
};
const { langData, siteMetadata, pathName } = Astro.props as Props;
const {langData, siteMetadata, pathName} = Astro.props as Props; ---
---
{langData?.currentLang && ( <>
<link {
rel="alternate" langData?.currentLang && (
href={ <link
siteMetadata.siteUrl + removePrefixLanguageFromPath(pathName || "") rel="alternate"
} href={
href-lang="x-default" siteMetadata.siteUrl + removePrefixLanguageFromPath(pathName || "")
/> }
)} href-lang="x-default"
{langData?.otherLangs?.length ? />
langData.otherLangs.map((lang) => ( )
<link }
rel="alternate" </>
href={ <>
siteMetadata.siteUrl + {
`${lang === "en" ? "" : "/"}${lang === "en" ? "" : lang}` + langData?.otherLangs?.length
removePrefixLanguageFromPath(pathName || "") ? langData.otherLangs.map((lang) => (
} <link
href-lang={lang} rel="alternate"
/> href={
)) : null} siteMetadata.siteUrl +
`${lang === "en" ? "" : "/"}${lang === "en" ? "" : lang}` +
removePrefixLanguageFromPath(pathName || "")
}
href-lang={lang}
/>
))
: null
}
</>

View File

@@ -1,44 +1,48 @@
--- ---
import { fileToOpenGraphConverter } from "utils/translations"; import { fileToOpenGraphConverter } from "utils/translations";
import {SEOProps} from './shared'; import { SEOProps } from "./shared";
type Props = Pick<SEOProps, 'title' | 'langData' | 'unicornsData'> & { type Props = Pick<SEOProps, "title" | "langData" | "unicornsData"> & {
currentPath: string; currentPath: string;
metaDescription: string; metaDescription: string;
metaImage: string; metaImage: string;
ogType: string; ogType: string;
siteMetadata: {title: string} siteMetadata: { title: string };
}; };
const { const {
currentPath, currentPath,
siteMetadata, siteMetadata,
title, title,
langData, langData,
metaDescription, metaDescription,
metaImage, metaImage,
ogType, ogType,
} = Astro.props as Props; } = Astro.props as Props;
--- ---
{/* Open Graph SEO */}
<meta property="og:url" content={currentPath} /> {/* Open Graph SEO */}
<meta property="og:site_name" content={siteMetadata.title} /> <meta property="og:url" content={currentPath} />
<meta property="og:title" content={title} /> <meta property="og:site_name" content={siteMetadata.title} />
<meta property="og:title" content={title} />
<meta
property="og:locale" <meta
content={ property="og:locale"
langData ? fileToOpenGraphConverter(langData.currentLang) : "en" content={langData ? fileToOpenGraphConverter(langData.currentLang) : "en"}
} />
/> <>
{langData?.otherLangs?.length ? {
langData.otherLangs.map((lang) => ( langData?.otherLangs?.length
<meta ? langData.otherLangs.map((lang) => (
property="og:locale:alternate" <meta
content={fileToOpenGraphConverter(lang)} property="og:locale:alternate"
/> content={fileToOpenGraphConverter(lang)}
)) : null} />
<meta property="og:description" content={metaDescription} /> ))
<meta property="og:image" content={metaImage} /> : null
<meta property="og:type" content={ogType} /> }
</>
<meta property="og:description" content={metaDescription} />
<meta property="og:image" content={metaImage} />
<meta property="og:type" content={ogType} />

View File

@@ -1,10 +1,10 @@
--- ---
import {SEOProps} from './shared'; import { SEOProps } from "./shared";
type Props = Pick<SEOProps, 'unicornsData'>; type Props = Pick<SEOProps, "unicornsData">;
const { unicornsData } = Astro.props as Props; const { unicornsData } = Astro.props as Props;
--- ---
<meta property="profile:firstName" content={unicornsData![0].firstName} /> <meta property="profile:firstName" content={unicornsData![0].firstName} />
<meta property="profile:lastName" content={unicornsData![0].lastName} /> <meta property="profile:lastName" content={unicornsData![0].lastName} />
<meta property="profile:username" content={unicornsData![0].id} /> <meta property="profile:username" content={unicornsData![0].id} />

View File

@@ -10,18 +10,18 @@ import { SEOProps } from "./shared";
import Twitter from "./twitter.astro"; import Twitter from "./twitter.astro";
const { const {
description = "", description = "",
title, title,
keywords, keywords,
canonical, canonical,
type, type,
unicornsData, unicornsData,
publishedTime, publishedTime,
editedTime, editedTime,
pathName, pathName,
isbn, isbn,
langData, langData,
shareImage, shareImage,
} = Astro.props as SEOProps; } = Astro.props as SEOProps;
const metaDescription = description || siteMetadata.description; const metaDescription = description || siteMetadata.description;
@@ -32,29 +32,62 @@ const ogType = type ?? "blog";
const socialUnicorn = unicornsData?.find((uni) => uni.socials); const socialUnicorn = unicornsData?.find((uni) => uni.socials);
const uniTwitter = const uniTwitter =
socialUnicorn && socialUnicorn.socials && socialUnicorn.socials.twitter; socialUnicorn && socialUnicorn.socials && socialUnicorn.socials.twitter;
const currentPath = siteMetadata.siteUrl + (pathName || ""); const currentPath = siteMetadata.siteUrl + (pathName || "");
--- ---
<title> <title>
{title ? `${title} | ${siteMetadata.title}` : siteMetadata.title} {title ? `${title} | ${siteMetadata.title}` : siteMetadata.title}
</title> </title>
{canonical ? <>{canonical ? <link rel="canonical" href={canonical} /> : null}</>
<link rel="canonical" href={canonical} /> : null}
<meta property="name" content={siteMetadata.title} /> <meta property="name" content={siteMetadata.title} />
<meta name="description" content={metaDescription} /> <meta name="description" content={metaDescription} />
<meta property="keywords" content={metaKeywords} /> <meta property="keywords" content={metaKeywords} />
<Analytics /> <Analytics />
<Twitter title={title} metaDescription={metaDescription} siteMetadata={siteMetadata} metaImage={metaImage} <Twitter
unicornsData={unicornsData} uniTwitter={uniTwitter} type={type} /> title={title}
<OpenGraph currentPath={currentPath} siteMetadata={siteMetadata} title={title} langData={langData} metaDescription={metaDescription}
metaDescription={metaDescription} metaImage={metaImage} ogType={ogType} /> siteMetadata={siteMetadata}
metaImage={metaImage}
unicornsData={unicornsData}
uniTwitter={uniTwitter}
type={type}
/>
<OpenGraph
currentPath={currentPath}
siteMetadata={siteMetadata}
title={title}
langData={langData}
metaDescription={metaDescription}
metaImage={metaImage}
ogType={ogType}
/>
<Locale langData={langData} siteMetadata={siteMetadata} pathName={pathName} /> <Locale langData={langData} siteMetadata={siteMetadata} pathName={pathName} />
{type === 'article' && <>
<Article keywords={keywords} editedTime={editedTime} publishedTime={publishedTime} unicornsData={unicornsData} />} {
{type === 'book' && type === "article" && (
<Book publishedTime={publishedTime} unicornsData={unicornsData} isbn={isbn} />} <Article
{type === 'profile' && keywords={keywords}
<Profile unicornsData={unicornsData} />} editedTime={editedTime}
<slot /> publishedTime={publishedTime}
unicornsData={unicornsData}
/>
)
}
</>
<>
{
type === "book" && (
<Book
publishedTime={publishedTime}
unicornsData={unicornsData}
isbn={isbn}
/>
)
}
</>
<>
{type === "profile" && <Profile unicornsData={unicornsData} />}
</>
<slot />

View File

@@ -1,19 +1,19 @@
import { Languages, UnicornInfo } from "../../types"; import { Languages, UnicornInfo } from "../../types";
export interface SEOProps { export interface SEOProps {
description?: string; description?: string;
langData?: { langData?: {
currentLang: Languages; currentLang: Languages;
otherLangs: Languages[]; otherLangs: Languages[];
}; };
title: string; title: string;
unicornsData?: UnicornInfo[]; unicornsData?: UnicornInfo[];
keywords?: string[]; keywords?: string[];
publishedTime?: string; publishedTime?: string;
editedTime?: string; editedTime?: string;
type?: "article" | "profile" | "book"; type?: "article" | "profile" | "book";
pathName?: string; pathName?: string;
canonical?: string; canonical?: string;
isbn?: string; isbn?: string;
shareImage?: string; shareImage?: string;
} }

View File

@@ -1,29 +1,34 @@
--- ---
import {SEOProps} from './shared'; import { SEOProps } from "./shared";
type Props = Pick<SEOProps, 'title' | 'unicornsData' | 'type'> & { type Props = Pick<SEOProps, "title" | "unicornsData" | "type"> & {
metaDescription: string; metaDescription: string;
metaImage: string; metaImage: string;
siteMetadata: {twitterHandle: string} siteMetadata: { twitterHandle: string };
uniTwitter?: string; uniTwitter?: string;
}; };
const { const {
title, title,
metaDescription, metaDescription,
siteMetadata, siteMetadata,
metaImage, metaImage,
unicornsData, unicornsData,
uniTwitter, uniTwitter,
type type,
} = Astro.props as Props; } = Astro.props as Props;
--- ---
{/* Twitter SEO */}
<meta name="twitter:title" content={title} /> {/* Twitter SEO */}
<meta name="twitter:description" content={metaDescription} /> <meta name="twitter:title" content={title} />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:description" content={metaDescription} />
<meta name="twitter:site" content={siteMetadata.twitterHandle} /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={metaImage} /> <meta name="twitter:site" content={siteMetadata.twitterHandle} />
{type === "article" && unicornsData?.length === 1 && uniTwitter ? ( <meta name="twitter:image" content={metaImage} />
<meta property="twitter:creator" content={`@${uniTwitter}`} /> <>
) : null} {
type === "article" && unicornsData?.length === 1 && uniTwitter ? (
<meta property="twitter:creator" content={`@${uniTwitter}`} />
) : null
}
</>

View File

@@ -1,68 +1,71 @@
--- ---
const {headingsToDisplaySlugs} = Astro.props as {headingsToDisplaySlugs: Array<string>} const { headingsToDisplaySlugs } = Astro.props as {
headingsToDisplaySlugs: Array<string>;
};
--- ---
<script define:vars={{headingsToDisplaySlugs}}> <script define:vars={{ headingsToDisplaySlugs }}>
window.onload = () => { window.onload = () => {
const tocListRef = document.querySelector('#tocList'); const tocListRef = document.querySelector("#tocList");
const linkRefs = [...document.querySelectorAll('[data-headingitem="true"]')] const linkRefs = [
...document.querySelectorAll('[data-headingitem="true"]'),
];
let previousSection = {current: ""} let previousSection = { current: "" };
const handleObserver = (entries) => { const handleObserver = (entries) => {
const highlightFirstActive = () => { const highlightFirstActive = () => {
if (!tocListRef) return; if (!tocListRef) return;
let firstVisibleLink = let firstVisibleLink = tocListRef.querySelector(".toc-is-visible");
tocListRef.querySelector(".toc-is-visible");
linkRefs.forEach((linkRef) => { linkRefs.forEach((linkRef) => {
linkRef.classList.remove("toc-is-active"); linkRef.classList.remove("toc-is-active");
}); });
if (firstVisibleLink) { if (firstVisibleLink) {
firstVisibleLink.classList.add("toc-is-active"); firstVisibleLink.classList.add("toc-is-active");
} }
if (!firstVisibleLink && previousSection.current) { if (!firstVisibleLink && previousSection.current) {
tocListRef tocListRef
.querySelector(`a[href="#${previousSection.current}"]`) .querySelector(`a[href="#${previousSection.current}"]`)
.parentElement.classList.add("toc-is-active"); .parentElement.classList.add("toc-is-active");
} }
}; };
entries.forEach((entry) => { entries.forEach((entry) => {
let href = `#${entry.target.getAttribute("id")}`, let href = `#${entry.target.getAttribute("id")}`,
link = linkRefs.find( link = linkRefs.find(
(l) => l.firstElementChild.getAttribute("href") === href (l) => l.firstElementChild.getAttribute("href") === href
); );
if (!link) return; if (!link) return;
if (entry.isIntersecting && entry.intersectionRatio >= 1) { if (entry.isIntersecting && entry.intersectionRatio >= 1) {
link.classList.add("toc-is-visible"); link.classList.add("toc-is-visible");
const newPreviousSection = entry.target.getAttribute("id"); const newPreviousSection = entry.target.getAttribute("id");
previousSection.current = newPreviousSection; previousSection.current = newPreviousSection;
} else { } else {
link.classList.remove("toc-is-visible"); link.classList.remove("toc-is-visible");
} }
highlightFirstActive(); highlightFirstActive();
}); });
}; };
const observer = new IntersectionObserver(handleObserver, { const observer = new IntersectionObserver(handleObserver, {
rootMargin: "0px", rootMargin: "0px",
threshold: 1, threshold: 1,
}); });
const headingsEls = headingsToDisplaySlugs.map((headingToDisplay) => { const headingsEls = headingsToDisplaySlugs.map((headingToDisplay) => {
return document.getElementById(headingToDisplay); return document.getElementById(headingToDisplay);
}); });
headingsEls headingsEls
.filter((a) => a) .filter((a) => a)
.forEach((heading) => { .forEach((heading) => {
observer.observe(heading); observer.observe(heading);
}); });
} };
</script> </script>

View File

@@ -6,7 +6,7 @@ import { PostInfo } from "types/PostInfo";
import HeadingIntersectionObserverScript from "./heading-intersection-observer-script.astro"; import HeadingIntersectionObserverScript from "./heading-intersection-observer-script.astro";
interface TableOfContentsProps { interface TableOfContentsProps {
headingsWithId: PostInfo["headingsWithId"]; headingsWithId: PostInfo["headingsWithId"];
} }
const { headingsWithId } = Astro.props as TableOfContentsProps; const { headingsWithId } = Astro.props as TableOfContentsProps;
@@ -18,33 +18,30 @@ const minDepth = Math.min(...headings.map((h) => h.depth));
// offset the heading depths by minDepth, so they always start at 1 // offset the heading depths by minDepth, so they always start at 1
const headingsToDisplay = headings const headingsToDisplay = headings
.map((h) => Object.assign({}, h, { depth: h.depth - minDepth + 1 })) .map((h) => Object.assign({}, h, { depth: h.depth - minDepth + 1 }))
.filter((headingInfo) => headingInfo.depth <= 3); .filter((headingInfo) => headingInfo.depth <= 3);
const headingsToDisplaySlugs = headingsToDisplay.map(item => item.slug); const headingsToDisplaySlugs = headingsToDisplay.map((item) => item.slug);
--- ---
<aside aria-label={"Table of Contents"}> <aside aria-label={"Table of Contents"}>
<ol <ol class={tableOfContentsStyle.tableList} role="list" id="tocList">
class={tableOfContentsStyle.tableList} {
role="list" headingsToDisplay.map((headingInfo, i) => {
id="tocList" const liClassNames = classnames(tableOfContentsStyle.tocLi, {
> [tableOfContentsStyle.tocH1]: headingInfo.depth === 1,
{headingsToDisplay.map((headingInfo, i) => { [tableOfContentsStyle.tocH2]: headingInfo.depth === 2,
const liClassNames = classnames(tableOfContentsStyle.tocLi, { [tableOfContentsStyle.tocH3]: headingInfo.depth === 3,
[tableOfContentsStyle.tocH1]: headingInfo.depth === 1, });
[tableOfContentsStyle.tocH2]: headingInfo.depth === 2, return (
[tableOfContentsStyle.tocH3]: headingInfo.depth === 3, <li class={liClassNames} data-headingitem="true">
}); <a href={`#${headingInfo.slug}`}>{headingInfo.value}</a>
return ( </li>
<li );
class={liClassNames} })
data-headingitem="true" }
> </ol>
<a href={`#${headingInfo.slug}`}>{headingInfo.value}</a>
</li>
);
})}
</ol>
</aside> </aside>
<HeadingIntersectionObserverScript headingsToDisplaySlugs={headingsToDisplaySlugs} /> <HeadingIntersectionObserverScript
headingsToDisplaySlugs={headingsToDisplaySlugs}
/>

View File

@@ -60,7 +60,9 @@
@extend %headline-uniwidth-2; @extend %headline-uniwidth-2;
} }
.tocH2, .tocH3, .tocH4 { .tocH2,
.tocH3,
.tocH4 {
a { a {
font-style: italic; font-style: italic;
} }

View File

@@ -1,39 +1,42 @@
--- ---
import styles from "./user-profile-pic.module.scss"; import styles from "./user-profile-pic.module.scss";
import { UnicornInfo } from "uu-types"; import { UnicornInfo } from "uu-types";
import {Image} from '@astrojs/image/components'; import { Image } from "@astrojs/image/components";
// TODO: Fix image loading and image 'onClick' // TODO: Fix image loading and image 'onClick'
interface UserProfilePicProps { interface UserProfilePicProps {
authors: Array<UnicornInfo>; authors: Array<UnicornInfo>;
className: string; className: string;
} }
const { authors, className } = Astro.props as UserProfilePicProps const { authors, className } = Astro.props as UserProfilePicProps;
const hasTwoAuthors = authors.length !== 1; const hasTwoAuthors = authors.length !== 1;
--- ---
<div class={`${styles.container} ${className || ""}`}>
{authors.map((unicorn, i) => {
const classesToApply = hasTwoAuthors ? styles.twoAuthor : "";
return ( <div class={`${styles.container} ${className || ""}`}>
<div {
class={`pointer ${styles.profilePicContainer} ${classesToApply}`} authors.map((unicorn, i) => {
style={`border-color: ${unicorn.color};`} const classesToApply = hasTwoAuthors ? styles.twoAuthor : "";
>
<Image return (
data-testid={`author-pic-${i}`} <div
src={unicorn.profileImgMeta.relativeServerPath} class={`pointer ${styles.profilePicContainer} ${classesToApply}`}
alt={unicorn.name} style={`border-color: ${unicorn.color};`}
sizes={"85px"} >
height={85} <Image
width={85} data-testid={`author-pic-${i}`}
format={'png'} src={unicorn.profileImgMeta.relativeServerPath}
class={`circleImg ${styles.profilePicImage} ${styles.width50} ${classesToApply}`} alt={unicorn.name}
/> sizes={"85px"}
</div> height={85}
); width={85}
})} format={"png"}
class={`circleImg ${styles.profilePicImage} ${styles.width50} ${classesToApply}`}
/>
</div>
);
})
}
</div> </div>

View File

@@ -6,32 +6,32 @@ let parent: string;
// Try & Catch to allow for hosts themselves to be passed // Try & Catch to allow for hosts themselves to be passed
// `new URL('domain.com')` will fail/throw, but is a valid host // `new URL('domain.com')` will fail/throw, but is a valid host
try { try {
const url = new URL(siteUrl); const url = new URL(siteUrl);
// URLs like 'localhost:3000' might not give host. // URLs like 'localhost:3000' might not give host.
// Throw in order to catch in wrapper handler // Throw in order to catch in wrapper handler
if (!url.host) throw new Error(); if (!url.host) throw new Error();
parent = url.host; parent = url.host;
} catch (_) { } catch (_) {
const url = new URL("https://" + siteUrl); const url = new URL("https://" + siteUrl);
parent = url.host; parent = url.host;
} }
// Twitch embed throws error with strings like 'localhost:3000', but // Twitch embed throws error with strings like 'localhost:3000', but
// those persist with `new URL().host` // those persist with `new URL().host`
if (parent.startsWith("localhost")) { if (parent.startsWith("localhost")) {
parent = "localhost"; parent = "localhost";
} }
const siteMetadata = { const siteMetadata = {
title: `Unicorn Utterances`, title: `Unicorn Utterances`,
description: `Learning programming from magically majestic words. A place to learn about all sorts of programming topics from entry-level concepts to advanced abstractions`, description: `Learning programming from magically majestic words. A place to learn about all sorts of programming topics from entry-level concepts to advanced abstractions`,
siteUrl, siteUrl,
disqusShortname: "unicorn-utterances", disqusShortname: "unicorn-utterances",
repoPath: "unicorn-utterances/unicorn-utterances", repoPath: "unicorn-utterances/unicorn-utterances",
relativeToPosts: "/content/blog", relativeToPosts: "/content/blog",
keywords: keywords:
"programming,development,mobile,web,game,utterances,software engineering,javascript,angular,react,computer science", "programming,development,mobile,web,game,utterances,software engineering,javascript,angular,react,computer science",
twitterHandle: "@unicornuttrncs", twitterHandle: "@unicornuttrncs",
}; };
export { parent, siteUrl, buildMode, siteMetadata }; export { parent, siteUrl, buildMode, siteMetadata };

View File

@@ -1,22 +1,22 @@
export default () => { export default () => {
const buildMode = process.env.BUILD_ENV || "production"; const buildMode = process.env.BUILD_ENV || "production";
let siteUrl = process.env.SITE_URL || process.env.VERCEL_URL || ""; let siteUrl = process.env.SITE_URL || process.env.VERCEL_URL || "";
if (!siteUrl) { if (!siteUrl) {
switch (buildMode) { switch (buildMode) {
case "production": case "production":
siteUrl = "https://unicorn-utterances.com"; siteUrl = "https://unicorn-utterances.com";
break; break;
case "development": case "development":
siteUrl = "http://localhost:9000"; siteUrl = "http://localhost:9000";
break; break;
default: default:
siteUrl = "https://beta.unicorn-utterances.com"; siteUrl = "https://beta.unicorn-utterances.com";
} }
} }
return { return {
buildMode, buildMode,
siteUrl, siteUrl,
}; };
}; };

View File

@@ -1,66 +1,66 @@
export const COLORS = { export const COLORS = {
//main styles //main styles
darkPrimary: { light: "#153E67", dark: "#E4F4FF" }, darkPrimary: { light: "#153E67", dark: "#E4F4FF" },
primary: { light: "#127DB3", dark: "#127DB3" }, primary: { light: "#127DB3", dark: "#127DB3" },
lightPrimary: { light: "#315e81", dark: "#bdd9e9" }, lightPrimary: { light: "#315e81", dark: "#bdd9e9" },
black: { light: "black", dark: "white" }, black: { light: "black", dark: "white" },
white: { light: "white", dark: "black" }, white: { light: "white", dark: "black" },
darkGrey: { light: "rgba(0, 0, 0, 0.64)", dark: "rgba(255, 255, 255, .64)" }, darkGrey: { light: "rgba(0, 0, 0, 0.64)", dark: "rgba(255, 255, 255, .64)" },
highImpactBlack: { highImpactBlack: {
light: "rgba(0, 0, 0, 0.87)", light: "rgba(0, 0, 0, 0.87)",
dark: "rgba(255, 255, 255, .87)", dark: "rgba(255, 255, 255, .87)",
}, },
midImpactBlack: { midImpactBlack: {
light: "rgba(0, 0, 0, 0.64)", light: "rgba(0, 0, 0, 0.64)",
dark: "rgba(255, 255, 255, .64)", dark: "rgba(255, 255, 255, .64)",
}, },
lowImpactBlack: { lowImpactBlack: {
light: "rgba(0, 0, 0, 0.58)", light: "rgba(0, 0, 0, 0.58)",
dark: "rgba(255, 255, 255, .58)", dark: "rgba(255, 255, 255, .58)",
}, },
minImpactBlack: { minImpactBlack: {
light: "rgba(0, 0, 0, 0.2)", light: "rgba(0, 0, 0, 0.2)",
dark: "rgba(255, 255, 255, .2)", dark: "rgba(255, 255, 255, .2)",
}, },
backgroundColor: { light: "#E4F4FF", dark: "#072a41" }, backgroundColor: { light: "#E4F4FF", dark: "#072a41" },
cardActiveBackground: { light: "#EBF6FC", dark: "#163954" }, cardActiveBackground: { light: "#EBF6FC", dark: "#163954" },
cardActiveBoxShadow: { cardActiveBoxShadow: {
light: "0px 2px 4px rgba(11, 37, 104, 0.27), inset 0px 1px 0px #FFFFFF", light: "0px 2px 4px rgba(11, 37, 104, 0.27), inset 0px 1px 0px #FFFFFF",
dark: "0px 2px 4px rgba(0, 0, 0, 0.27), inset 0px 1px 0px #435e75", dark: "0px 2px 4px rgba(0, 0, 0, 0.27), inset 0px 1px 0px #435e75",
}, },
codeBlockBackground: { light: "white", dark: "#202746" }, codeBlockBackground: { light: "white", dark: "#202746" },
codeInlineBackground: { light: "#cbe8fb", dark: "#1d495e" }, codeInlineBackground: { light: "#cbe8fb", dark: "#1d495e" },
//code styles //code styles
codeBackgroundColor: { light: "#fff", dark: "#161b1d" }, codeBackgroundColor: { light: "#fff", dark: "#161b1d" },
textColor: { light: "#5e6687", dark: "#7ea2b4" }, textColor: { light: "#5e6687", dark: "#7ea2b4" },
stringColor: { light: "#007396", dark: "#7ee2c4" }, stringColor: { light: "#007396", dark: "#7ee2c4" },
keywordColor: { light: "#846c00", dark: "#b5ea94" }, keywordColor: { light: "#846c00", dark: "#b5ea94" },
operatorColor: { light: "#b74c00", dark: "#935c25" }, operatorColor: { light: "#b74c00", dark: "#935c25" },
punctuationColor: { light: "#006fce", dark: "#7ea2b4" }, punctuationColor: { light: "#006fce", dark: "#7ea2b4" },
constantColor: { light: "#aa05d4", dark: "#cf8ae1" }, constantColor: { light: "#aa05d4", dark: "#cf8ae1" },
functionColor: { light: "#5357d2", dark: "#c1c3ff" }, functionColor: { light: "#5357d2", dark: "#c1c3ff" },
selectionColor: { light: "#dfe2f1", dark: "#cfe0ec" }, selectionColor: { light: "#dfe2f1", dark: "#cfe0ec" },
commentColor: { light: "#898ea4", dark: "#c7d5d7" }, commentColor: { light: "#898ea4", dark: "#c7d5d7" },
propColor: { light: "#c08b30", dark: "#8a8a0f" }, propColor: { light: "#c08b30", dark: "#8a8a0f" },
varColor: { light: "#3d8fd1", dark: "#88c1e2" }, varColor: { light: "#3d8fd1", dark: "#88c1e2" },
selectorColor: { light: "#6679cc", dark: "#bdbdff" }, selectorColor: { light: "#6679cc", dark: "#bdbdff" },
urlColor: { light: "#22a9c9", dark: "#9affde" }, urlColor: { light: "#22a9c9", dark: "#9affde" },
insertedUnderlineColor: { light: "#202746", dark: "#ebf8ff" }, insertedUnderlineColor: { light: "#202746", dark: "#ebf8ff" },
highlightColor: { light: "#c94922", dark: "#ff92c0" }, highlightColor: { light: "#c94922", dark: "#ff92c0" },
lineNumbersColor: { light: "#979db4", dark: "#abe1fa" }, lineNumbersColor: { light: "#979db4", dark: "#abe1fa" },
lineHighlightColor: { lineHighlightColor: {
light: "rgba(107, 115, 148, 0.2)", light: "rgba(107, 115, 148, 0.2)",
dark: "rgba(235, 248, 255, 0.2)", dark: "rgba(235, 248, 255, 0.2)",
}, },
lineHighlightFadeColor: { lineHighlightFadeColor: {
light: "rgba(107, 115, 148, 0)", light: "rgba(107, 115, 148, 0)",
dark: "rgba(235, 248, 255, 0)", dark: "rgba(235, 248, 255, 0)",
}, },
scrollBarBG: { scrollBarBG: {
light: "rgba(18, 125, 179, 0.3)", light: "rgba(18, 125, 179, 0.3)",
dark: "rgba(228, 244, 255, 0.3)", dark: "rgba(228, 244, 255, 0.3)",
}, },
scrollBarThumb: { light: "var(--primary)", dark: "var(--darkPrimary)" }, scrollBarThumb: { light: "var(--primary)", dark: "var(--darkPrimary)" },
}; };
export const COLOR_MODE_STORAGE_KEY = "currentTheme"; export const COLOR_MODE_STORAGE_KEY = "currentTheme";

File diff suppressed because one or more lines are too long

View File

@@ -7,421 +7,421 @@
@import "./convertkit"; @import "./convertkit";
:root { :root {
--animSpeed: 200ms; --animSpeed: 200ms;
--animStyle: ease-in-out; --animStyle: ease-in-out;
--cardOutlineStyle: 1px solid var(--primary); --cardOutlineStyle: 1px solid var(--primary);
--cardRadius: #{$baseUnit}px; --cardRadius: #{$baseUnit}px;
--filterBarIconSize: #{3 * $baseUnit}px; --filterBarIconSize: #{3 * $baseUnit}px;
--listViewPadding: #{1.5 * $baseUnit}px; --listViewPadding: #{1.5 * $baseUnit}px;
font-size: $rootFontSize; font-size: $rootFontSize;
line-height: 1.2; line-height: 1.2;
scrollbar-color: var(--darkPrimary) var(--backgroundColor); scrollbar-color: var(--darkPrimary) var(--backgroundColor);
transition: scrollbar-color var(--animStyle) var(--animSpeed); transition: scrollbar-color var(--animStyle) var(--animSpeed);
} }
@include from($startMediumScreenSize) { @include from($startMediumScreenSize) {
:root { :root {
--listViewPadding: #{$baseUnit * 2.5}px; --listViewPadding: #{$baseUnit * 2.5}px;
} }
} }
:focus { :focus {
outline-color: var(--darkPrimary); outline-color: var(--darkPrimary);
} }
//without this all <button>s have bad outline colors in firefox //without this all <button>s have bad outline colors in firefox
:focus::-moz-focus-inner { :focus::-moz-focus-inner {
padding: 0; //prevent weirdness just in case padding: 0; //prevent weirdness just in case
border-color: var(--darkPrimary); border-color: var(--darkPrimary);
} }
*::-webkit-scrollbar { *::-webkit-scrollbar {
width: 12px; width: 12px;
} }
*::-webkit-scrollbar-track { *::-webkit-scrollbar-track {
background: var(--scrollBarBG); background: var(--scrollBarBG);
border-radius: 10px; border-radius: 10px;
} }
*::-webkit-scrollbar-thumb { *::-webkit-scrollbar-thumb {
border-radius: 10px; border-radius: 10px;
background: var(--scrollBarThumb); background: var(--scrollBarThumb);
} }
.listViewContent { .listViewContent {
margin: 0 auto; margin: 0 auto;
max-width: #{160 * $baseUnit}px; // 1280px max-width: #{160 * $baseUnit}px; // 1280px
padding: 0 var(--listViewPadding); padding: 0 var(--listViewPadding);
} }
.postViewContent { .postViewContent {
padding: #{$baseUnit * 2.5}px; padding: #{$baseUnit * 2.5}px;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
.postViewContent > * { .postViewContent > * {
margin: 0 auto; margin: 0 auto;
} }
body { body {
background-color: var(--backgroundColor); background-color: var(--backgroundColor);
margin: 0; margin: 0;
padding: 0; padding: 0;
@extend %body-1; @extend %body-1;
color: var(--black); color: var(--black);
transition: color var(--animStyle) var(--animSpeed), transition: color var(--animStyle) var(--animSpeed),
background-color var(--animStyle) var(--animSpeed); background-color var(--animStyle) var(--animSpeed);
} }
.medium-zoom-overlay { .medium-zoom-overlay {
background: var(--backgroundColor) !important; background: var(--backgroundColor) !important;
} }
/* https://snook.ca/archives/html_and_css/hiding-content-for-accessibility */ /* https://snook.ca/archives/html_and_css/hiding-content-for-accessibility */
.visually-hidden { .visually-hidden {
position: absolute !important; position: absolute !important;
height: 1px; height: 1px;
width: 1px; width: 1px;
overflow: hidden; overflow: hidden;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
clip: rect(1px, 1px, 1px, 1px); clip: rect(1px, 1px, 1px, 1px);
} }
.visually-hidden a:focus, .visually-hidden a:focus,
.visually-hidden input:focus, .visually-hidden input:focus,
.visually-hidden button:focus { .visually-hidden button:focus {
position: static; position: static;
width: auto; width: auto;
height: auto; height: auto;
} }
.filterDropdown { .filterDropdown {
// This maps to the dropdown button. If the asset is changes, thus this must as well // This maps to the dropdown button. If the asset is changes, thus this must as well
transform-origin: 26px 26px; transform-origin: 26px 26px;
transition: transform 300ms var(--animStyle); transition: transform 300ms var(--animStyle);
} }
.expandedIcon .filterDropdown { .expandedIcon .filterDropdown {
transform: rotate(180deg); transform: rotate(180deg);
} }
.baseBtn { .baseBtn {
cursor: pointer; cursor: pointer;
} }
.baseBtn, .baseBtn,
.btnLike { .btnLike {
appearance: none; appearance: none;
text-decoration: none; text-decoration: none;
background: none; background: none;
border: none; border: none;
transition: background var(--animSpeed) var(--animStyle), transition: background var(--animSpeed) var(--animStyle),
box-shadow var(--animSpeed) var(--animStyle), box-shadow var(--animSpeed) var(--animStyle),
border-color var(--animSpeed) var(--animStyle), border-color var(--animSpeed) var(--animStyle),
color var(--animStyle) var(--animSpeed); color var(--animStyle) var(--animSpeed);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-shrink: 0; flex-shrink: 0;
border-radius: var(--cardRadius); border-radius: var(--cardRadius);
font-size: 1rem; font-size: 1rem;
padding: #{math.div($baseUnit, 2)}px #{$baseUnit}px; padding: #{math.div($baseUnit, 2)}px #{$baseUnit}px;
color: var(--darkPrimary); color: var(--darkPrimary);
} }
.baseBtn:hover { .baseBtn:hover {
background: var(--cardActiveBackground); background: var(--cardActiveBackground);
border-color: transparent; border-color: transparent;
box-shadow: var(--cardActiveBoxShadow); box-shadow: var(--cardActiveBoxShadow);
} }
.baseBtn svg, .baseBtn svg,
.btnLike svg { .btnLike svg {
$size: #{$baseUnit * 4}px; $size: #{$baseUnit * 4}px;
height: $size; height: $size;
width: $size; width: $size;
flex-shrink: 0; flex-shrink: 0;
} }
$pendIconMarg: #{$baseUnit}px; $pendIconMarg: #{$baseUnit}px;
.baseBtn.prependIcon svg, .baseBtn.prependIcon svg,
.btnLike.prependIcon svg { .btnLike.prependIcon svg {
margin-right: $pendIconMarg; margin-right: $pendIconMarg;
} }
.baseBtn.appendIcon svg, .baseBtn.appendIcon svg,
.btnLike.appendIcon svg { .btnLike.appendIcon svg {
margin-left: $pendIconMarg; margin-left: $pendIconMarg;
} }
.post-body { .post-body {
margin: 0 auto #{$baseUnit * 4}px; margin: 0 auto #{$baseUnit * 4}px;
max-width: 768px; max-width: 768px;
line-height: 1.7; line-height: 1.7;
// Fix autolink-headings anchors positioning // Fix autolink-headings anchors positioning
.anchor { .anchor {
line-height: 1; line-height: 1;
padding-right: 24px; padding-right: 24px;
svg { svg {
vertical-align: middle; vertical-align: middle;
} }
} }
.anchor.before { .anchor.before {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
transform: translateX(-100%); transform: translateX(-100%);
padding-right: 24px; padding-right: 24px;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
h1 .anchor svg, h1 .anchor svg,
h2 .anchor svg, h2 .anchor svg,
h3 .anchor svg, h3 .anchor svg,
h4 .anchor svg, h4 .anchor svg,
h5 .anchor svg, h5 .anchor svg,
h6 .anchor svg { h6 .anchor svg {
visibility: hidden; visibility: hidden;
} }
h1:hover .anchor svg, h1:hover .anchor svg,
h2:hover .anchor svg, h2:hover .anchor svg,
h3:hover .anchor svg, h3:hover .anchor svg,
h4:hover .anchor svg, h4:hover .anchor svg,
h5:hover .anchor svg, h5:hover .anchor svg,
h6:hover .anchor svg, h6:hover .anchor svg,
h1 .anchor:focus svg, h1 .anchor:focus svg,
h2 .anchor:focus svg, h2 .anchor:focus svg,
h3 .anchor:focus svg, h3 .anchor:focus svg,
h4 .anchor:focus svg, h4 .anchor:focus svg,
h5 .anchor:focus svg, h5 .anchor:focus svg,
h6 .anchor:focus svg { h6 .anchor:focus svg {
visibility: visible; visibility: visible;
} }
img { img {
margin: 0 auto; margin: 0 auto;
display: block; display: block;
max-width: 100%; max-width: 100%;
&[src$=".svg"] { &[src$=".svg"] {
width: 100%; width: 100%;
max-height: 50vh; max-height: 50vh;
} }
} }
h1, h1,
h2, h2,
h3, h3,
h4, h4,
h5, h5,
h6 { h6 {
margin-top: 1.25em; margin-top: 1.25em;
line-height: 1.5; line-height: 1.5;
margin-bottom: 0; margin-bottom: 0;
} }
h1 + h2, h1 + h2,
h2 + h3, h2 + h3,
h3 + h4, h3 + h4,
h4 + h5, h4 + h5,
h5 + h6 { h5 + h6 {
margin-top: 0.75em; margin-top: 0.75em;
} }
table tr:last-child th:first-child { table tr:last-child th:first-child {
border-bottom-left-radius: 10px; border-bottom-left-radius: 10px;
} }
table tr:last-child td:first-child { table tr:last-child td:first-child {
border-bottom-left-radius: 10px; border-bottom-left-radius: 10px;
} }
table tr:last-child td:last-child { table tr:last-child td:last-child {
border-bottom-right-radius: 10px; border-bottom-right-radius: 10px;
} }
.table-container { .table-container {
max-width: 100%; max-width: 100%;
overflow: auto; overflow: auto;
} }
table { table {
border: var(--cardOutlineStyle); border: var(--cardOutlineStyle);
border-radius: var(--cardRadius); border-radius: var(--cardRadius);
border-collapse: collapse; border-collapse: collapse;
// Border-collapse and border-radius don't mix. This is a workaround for that issue // Border-collapse and border-radius don't mix. This is a workaround for that issue
box-shadow: 0 0 0 1px var(--primary); box-shadow: 0 0 0 1px var(--primary);
overflow: hidden; overflow: hidden;
@include until($endSmallScreenSize) { @include until($endSmallScreenSize) {
ul { ul {
padding: 0; padding: 0;
list-style: none; list-style: none;
} }
} }
} }
tr, tr,
td, td,
th { th {
border: var(--cardOutlineStyle); border: var(--cardOutlineStyle);
} }
td, td,
th { th {
padding-left: 1rem; padding-left: 1rem;
padding-right: 1rem; padding-right: 1rem;
@include until($endSmallScreenSize) { @include until($endSmallScreenSize) {
padding-left: 5px; padding-left: 5px;
padding-right: 5px; padding-right: 5px;
} }
} }
iframe { iframe {
width: 100%; width: 100%;
min-height: 500px; min-height: 500px;
border: var(--cardOutlineStyle); border: var(--cardOutlineStyle);
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
} }
p > code { p > code {
display: inline; display: inline;
padding: 0 0.4em; padding: 0 0.4em;
font-size: 85%; font-size: 85%;
color: var(--black); color: var(--black);
background-color: var(--codeInlineBackground); background-color: var(--codeInlineBackground);
border-radius: 4px; border-radius: 4px;
} }
details { details {
border: 2px solid var(--darkPrimary); border: 2px solid var(--darkPrimary);
border-radius: 0.5rem; border-radius: 0.5rem;
margin-bottom: 1em; margin-bottom: 1em;
summary { summary {
padding: .5rem 1.5rem; padding: 0.5rem 1.5rem;
background: var(--darkPrimary); background: var(--darkPrimary);
color: var(--backgroundColor); color: var(--backgroundColor);
cursor: pointer; cursor: pointer;
} }
summary ~ * { summary ~ * {
margin-left: 1.5rem; margin-left: 1.5rem;
margin-right: 1.5rem; margin-right: 1.5rem;
} }
} }
} }
.post-lower-area { .post-lower-area {
margin: 0 auto; margin: 0 auto;
max-width: #{$baseUnit * 115}px; max-width: #{$baseUnit * 115}px;
.postBottom { .postBottom {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@include until($startMediumScreenSize) { @include until($startMediumScreenSize) {
flex-direction: column; flex-direction: column;
.btnLike { .btnLike {
order: 2; order: 2;
} }
.baseBtn { .baseBtn {
margin-top: 20px; margin-top: 20px;
margin-bottom: 5px; margin-bottom: 5px;
} }
} }
} }
} }
// Please use this sparingly. There's massive A11y concerns // Please use this sparingly. There's massive A11y concerns
.unlink { .unlink {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
pre { pre {
overflow: auto; overflow: auto;
border: var(--cardOutlineStyle); border: var(--cardOutlineStyle);
border-radius: 8px; border-radius: 8px;
background: white; background: white;
} }
.lowercase { .lowercase {
text-transform: lowercase; text-transform: lowercase;
} }
.pointer { .pointer {
cursor: pointer; cursor: pointer;
} }
.circleImg { .circleImg {
flex-shrink: 0; flex-shrink: 0;
flex-grow: 0; flex-grow: 0;
border-radius: 50%; border-radius: 50%;
} }
a { a {
color: var(--darkPrimary); color: var(--darkPrimary);
} }
svg.strokeicon { svg.strokeicon {
&, &,
* { * {
transition: stroke var(--animStyle) var(--animSpeed); transition: stroke var(--animStyle) var(--animSpeed);
stroke: var(--darkPrimary); stroke: var(--darkPrimary);
} }
} }
svg:not(.strokeicon) { svg:not(.strokeicon) {
&, &,
* { * {
transition: fill var(--animStyle) var(--animSpeed); transition: fill var(--animStyle) var(--animSpeed);
fill: var(--darkPrimary); fill: var(--darkPrimary);
} }
} }
.marginZeroAutoChild { .marginZeroAutoChild {
& > * { & > * {
margin: 0 auto; margin: 0 auto;
} }
} }
li > ul > li { li > ul > li {
margin: 1rem 0; margin: 1rem 0;
} }
.toc-is-active { .toc-is-active {
font-weight: bold; font-weight: bold;
a { a {
color: var(--darkPrimary) !important; color: var(--darkPrimary) !important;
} }
} }
kbd { kbd {
background-color: #eee; background-color: #eee;
border-radius: 3px; border-radius: 3px;
border: 1px solid #b4b4b4; border: 1px solid #b4b4b4;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 2px 0 0 rgba(255, 255, 255, 0.7) inset; 0 2px 0 0 rgba(255, 255, 255, 0.7) inset;
color: #333; color: #333;
display: inline-block; display: inline-block;
font-size: 0.85em; font-size: 0.85em;
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
padding: 2px 4px; padding: 2px 4px;
white-space: nowrap; white-space: nowrap;
} }

View File

@@ -1,29 +1,28 @@
--- ---
import {v4 as uuidV4} from 'uuid'; import { v4 as uuidV4 } from "uuid";
const id = uuidV4(); const id = uuidV4();
const clipId = `path-1-inside-1${id}`; const clipId = `path-1-inside-1${id}`;
const props = Astro.props; const props = Astro.props;
--- ---
<svg <svg
width="36" width="36"
height="36" height="36"
viewBox="0 0 36 36" viewBox="0 0 36 36"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
{...props} {...props}
> >
<g clip-path={`url(#${clipId})`}> <g clip-path={`url(#${clipId})`}>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"
d="M24.6893 7.31065C23.0659 5.6873 20.4339 5.6873 18.8106 7.31066L8.06059 18.0607C5.33267 20.7886 5.33267 25.2114 8.06059 27.9393C10.7885 30.6673 15.2113 30.6673 17.9393 27.9393L30.4393 15.4393C31.0251 14.8535 31.9748 14.8535 32.5606 15.4393C33.1464 16.0251 33.1464 16.9749 32.5606 17.5607L20.0606 30.0607C16.1611 33.9602 9.83876 33.9601 5.93927 30.0607C2.03978 26.1612 2.03977 19.8388 5.93927 15.9393L16.6893 5.18934C19.4842 2.39441 24.0157 2.39441 26.8106 5.18933C29.6055 7.98426 29.6055 12.5157 26.8106 15.3107L16.0606 26.0606C14.3702 27.751 11.6296 27.751 9.93927 26.0607C8.24891 24.3703 8.24891 21.6297 9.93927 19.9393L19.9393 9.93933C20.5251 9.35355 21.4748 9.35355 22.0606 9.93933C22.6464 10.5251 22.6464 11.4749 22.0606 12.0607L12.0606 22.0607C11.5418 22.5794 11.5418 23.4205 12.0606 23.9393C12.5794 24.4581 13.4205 24.4581 13.9393 23.9393L24.6893 13.1893C26.3126 11.566 26.3126 8.93401 24.6893 7.31065Z" d="M24.6893 7.31065C23.0659 5.6873 20.4339 5.6873 18.8106 7.31066L8.06059 18.0607C5.33267 20.7886 5.33267 25.2114 8.06059 27.9393C10.7885 30.6673 15.2113 30.6673 17.9393 27.9393L30.4393 15.4393C31.0251 14.8535 31.9748 14.8535 32.5606 15.4393C33.1464 16.0251 33.1464 16.9749 32.5606 17.5607L20.0606 30.0607C16.1611 33.9602 9.83876 33.9601 5.93927 30.0607C2.03978 26.1612 2.03977 19.8388 5.93927 15.9393L16.6893 5.18934C19.4842 2.39441 24.0157 2.39441 26.8106 5.18933C29.6055 7.98426 29.6055 12.5157 26.8106 15.3107L16.0606 26.0606C14.3702 27.751 11.6296 27.751 9.93927 26.0607C8.24891 24.3703 8.24891 21.6297 9.93927 19.9393L19.9393 9.93933C20.5251 9.35355 21.4748 9.35355 22.0606 9.93933C22.6464 10.5251 22.6464 11.4749 22.0606 12.0607L12.0606 22.0607C11.5418 22.5794 11.5418 23.4205 12.0606 23.9393C12.5794 24.4581 13.4205 24.4581 13.9393 23.9393L24.6893 13.1893C26.3126 11.566 26.3126 8.93401 24.6893 7.31065Z"
fill="#153E67" fill="#153E67"></path>
/> </g>
</g> <defs>
<defs> <clip-path id={clipId}>
<clipPath id={clipId}> <rect width="36" height="36" fill="white"></rect>
<rect width="36" height="36" fill="white" /> </clip-path>
</clipPath> </defs>
</defs> </svg>
</svg>

View File

@@ -1,27 +1,26 @@
--- ---
import {v4 as uuidV4} from 'uuid'; import { v4 as uuidV4 } from "uuid";
const id = uuidV4(); const id = uuidV4();
const clipId = `path-1-inside-1${id}`; const clipId = `path-1-inside-1${id}`;
const props = Astro.props; const props = Astro.props;
--- ---
<svg <svg
width="36" width="36"
height="36" height="36"
viewBox="0 0 36 36" viewBox="0 0 36 36"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
{...props} {...props}
> >
<g clipPath={`url(#${clipId.current})`}> <g clip-path={`url(#${clipId})`}>
<path <path
d="M1.43934 32.4393C0.853553 33.0251 0.853553 33.9749 1.43934 34.5607C2.02513 35.1464 2.97487 35.1464 3.56066 34.5607L1.43934 32.4393ZM24.0607 14.0607C24.6464 13.4749 24.6464 12.5251 24.0607 11.9393C23.4749 11.3536 22.5251 11.3536 21.9393 11.9393L24.0607 14.0607ZM8.67157 14.3284L7.61091 13.2678L8.67157 14.3284ZM14.5607 23.5607L24.0607 14.0607L21.9393 11.9393L12.4393 21.4393L14.5607 23.5607ZM26.5 21H13.5V24H26.5V21ZM3.56066 34.5607L8.56066 29.5607L6.43934 27.4393L1.43934 32.4393L3.56066 34.5607ZM8.56066 29.5607L14.5607 23.5607L12.4393 21.4393L6.43934 27.4393L8.56066 29.5607ZM7.5 30H18.8431V27H7.5V30ZM22.7322 28.3891L31.5607 19.5607L29.4393 17.4393L20.6109 26.2678L22.7322 28.3891ZM9 28.5V17.1569H6V28.5H9ZM9.73223 15.3891L18.5607 6.56066L16.4393 4.43934L7.61091 13.2678L9.73223 15.3891ZM18.5607 6.56066C21.5647 3.55659 26.4353 3.5566 29.4393 6.56066L31.5607 4.43934C27.385 0.263705 20.615 0.263698 16.4393 4.43934L18.5607 6.56066ZM9 17.1569C9 16.4938 9.26339 15.8579 9.73223 15.3891L7.61091 13.2678C6.57946 14.2992 6 15.6982 6 17.1569H9ZM31.5607 19.5607C35.7363 15.385 35.7363 8.61497 31.5607 4.43934L29.4393 6.56066C32.4434 9.56472 32.4434 14.4353 29.4393 17.4393L31.5607 19.5607ZM18.8431 30C20.3018 30 21.7008 29.4205 22.7322 28.3891L20.6109 26.2678C20.1421 26.7366 19.5062 27 18.8431 27V30Z" d="M1.43934 32.4393C0.853553 33.0251 0.853553 33.9749 1.43934 34.5607C2.02513 35.1464 2.97487 35.1464 3.56066 34.5607L1.43934 32.4393ZM24.0607 14.0607C24.6464 13.4749 24.6464 12.5251 24.0607 11.9393C23.4749 11.3536 22.5251 11.3536 21.9393 11.9393L24.0607 14.0607ZM8.67157 14.3284L7.61091 13.2678L8.67157 14.3284ZM14.5607 23.5607L24.0607 14.0607L21.9393 11.9393L12.4393 21.4393L14.5607 23.5607ZM26.5 21H13.5V24H26.5V21ZM3.56066 34.5607L8.56066 29.5607L6.43934 27.4393L1.43934 32.4393L3.56066 34.5607ZM8.56066 29.5607L14.5607 23.5607L12.4393 21.4393L6.43934 27.4393L8.56066 29.5607ZM7.5 30H18.8431V27H7.5V30ZM22.7322 28.3891L31.5607 19.5607L29.4393 17.4393L20.6109 26.2678L22.7322 28.3891ZM9 28.5V17.1569H6V28.5H9ZM9.73223 15.3891L18.5607 6.56066L16.4393 4.43934L7.61091 13.2678L9.73223 15.3891ZM18.5607 6.56066C21.5647 3.55659 26.4353 3.5566 29.4393 6.56066L31.5607 4.43934C27.385 0.263705 20.615 0.263698 16.4393 4.43934L18.5607 6.56066ZM9 17.1569C9 16.4938 9.26339 15.8579 9.73223 15.3891L7.61091 13.2678C6.57946 14.2992 6 15.6982 6 17.1569H9ZM31.5607 19.5607C35.7363 15.385 35.7363 8.61497 31.5607 4.43934L29.4393 6.56066C32.4434 9.56472 32.4434 14.4353 29.4393 17.4393L31.5607 19.5607ZM18.8431 30C20.3018 30 21.7008 29.4205 22.7322 28.3891L20.6109 26.2678C20.1421 26.7366 19.5062 27 18.8431 27V30Z"
fill="#153E67" fill="#153E67"></path>
/> </g>
</g> <defs>
<defs> <clip-path id={clipId}>
<clipPath id={clipId.current}> <rect width="36" height="36" fill="white"></rect>
<rect width="36" height="36" fill="white" /> </clip-path>
</clipPath> </defs>
</defs> </svg>
</svg>

View File

@@ -1,70 +1,70 @@
--- ---
import ThemeStyle from "../page-components/layouts/theme-style.astro"; import ThemeStyle from "../page-components/layouts/theme-style.astro";
import BlockingThemeChangerScript from "../page-components/layouts/blocking-theme-changer-script.astro"; import BlockingThemeChangerScript from "../page-components/layouts/blocking-theme-changer-script.astro";
import Layout from "../components/layout/layout.astro"; import Layout from "../components/layout/layout.astro";
import '../global.scss'; import "../global.scss";
--- ---
<html class="light"> <html class="light">
<head> <head>
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width" />
<meta charset="utf-8"/> <meta charset="utf-8" />
<link <link
rel="preload" rel="preload"
as="style" as="style"
href="https://fonts.googleapis.com/css?family=Asap:400,700%7CWork+Sans:400,500,600,700%7CArchivo:400,500,600%7CRoboto+Mono:400&display=swap" href="https://fonts.googleapis.com/css?family=Asap:400,700%7CWork+Sans:400,500,600,700%7CArchivo:400,500,600%7CRoboto+Mono:400&display=swap"
/> />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#127db3" /> <meta name="theme-color" content="#127db3" />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="48x48" sizes="48x48"
href="/icons/icon-48x48.png?v=2a" href="/icons/icon-48x48.png?v=2a"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="72x72" sizes="72x72"
href="/icons/icon-72x72.png?v=2a" href="/icons/icon-72x72.png?v=2a"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="96x96" sizes="96x96"
href="/icons/icon-96x96.png?v=2a" href="/icons/icon-96x96.png?v=2a"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="144x144" sizes="144x144"
href="/icons/icon-144x144.png?v=2a" href="/icons/icon-144x144.png?v=2a"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="192x192" sizes="192x192"
href="/icons/icon-192x192.png?v=2a" href="/icons/icon-192x192.png?v=2a"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="256x256" sizes="256x256"
href="/icons/icon-256x256.png?v=2a" href="/icons/icon-256x256.png?v=2a"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="384x384" sizes="384x384"
href="/icons/icon-384x384.png?v=2a" href="/icons/icon-384x384.png?v=2a"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="512x512" sizes="512x512"
href="/icons/icon-512x512.png?v=2a" href="/icons/icon-512x512.png?v=2a"
/> />
<script defer src="/uninstall-sw.js" is:inline/> <script defer src="/uninstall-sw.js" is:inline></script>
<ThemeStyle/> <ThemeStyle />
<slot name="head"/> <slot name="head" />
</head> </head>
<body> <body>
<BlockingThemeChangerScript/> <BlockingThemeChangerScript />
<Layout> <Layout>
<slot /> <slot />
</Layout> </Layout>
</body> </body>
</html> </html>

View File

@@ -5,7 +5,7 @@ import dayjs from "dayjs";
import { PostInfo } from "types/PostInfo"; import { PostInfo } from "types/PostInfo";
interface PostMetadataProps { interface PostMetadataProps {
post: PostInfo; post: PostInfo;
} }
const { post } = Astro.props as PostMetadataProps; const { post } = Astro.props as PostMetadataProps;
@@ -14,8 +14,8 @@ const { authorsMeta } = post;
let originalHost; let originalHost;
if (post.originalLink) { if (post.originalLink) {
const url = new URL(post.originalLink); const url = new URL(post.originalLink);
originalHost = url.host; originalHost = url.host;
} }
const publishedStr = dayjs(post.published).format("MMMM D, YYYY"); const publishedStr = dayjs(post.published).format("MMMM D, YYYY");
@@ -24,32 +24,43 @@ const publishedStr = dayjs(post.published).format("MMMM D, YYYY");
--- ---
<div class={styles.container}> <div class={styles.container}>
<UserProfilePic authors={authorsMeta} class={styles.postMetadataAuthorImagesContainer} /> <UserProfilePic
<div class={styles.textDiv}> authors={authorsMeta}
<h2 class={styles.authorName} data-testid="post-meta-author-name"> class={styles.postMetadataAuthorImagesContainer}
<span>by </span> />
{authorsMeta.map((author, i) => { <div class={styles.textDiv}>
return ( <h2 class={styles.authorName} data-testid="post-meta-author-name">
<> <span>by</span>
{i !== 0 && <span>{", "}</span>} {
<a href={`/unicorns/${author.id}`} class={styles.authorLink}> authorsMeta.map((author, i) => {
{author.name} return (
</a> <>
</> {i !== 0 && <span>{", "}</span>}
); <a href={`/unicorns/${author.id}`} class={styles.authorLink}>
})} {author.name}
</h2> </a>
<div class={styles.belowName}> </>
<p>{publishedStr}</p> );
<p>{post.wordCount} words</p> })
</div> }
</div> </h2>
{!!post.originalLink && ( <div class={styles.belowName}>
<p class={styles.originalLink}> <p>{publishedStr}</p>
Originally posted at&nbsp; <p>{post.wordCount} words</p>
<a href={post.originalLink} target="_blank" rel="nofollow noopener noreferrer"> </div>
{originalHost} </div>
</a> {
</p> !!post.originalLink && (
)} <p class={styles.originalLink}>
</div> Originally posted at&nbsp;
<a
href={post.originalLink}
target="_blank"
rel="nofollow noopener noreferrer"
>
{originalHost}
</a>
</p>
)
}
</div>

View File

@@ -3,19 +3,15 @@ import styles from "./post-title-header.module.scss";
import { PostInfo } from "types/index"; import { PostInfo } from "types/index";
interface PostTitleHeaderProps { interface PostTitleHeaderProps {
post: PostInfo; post: PostInfo;
} }
const {post} = Astro.props as PostTitleHeaderProps; const { post } = Astro.props as PostTitleHeaderProps;
const { title, tags } = post; const { title, tags } = post;
--- ---
<div class={styles.container}> <div class={styles.container}>
<h1 class={styles.title}>{title}</h1> <h1 class={styles.title}>{title}</h1>
<ul aria-label="Post tags" role="list" class={styles.tags}> <ul aria-label="Post tags" role="list" class={styles.tags}>
{tags.map((tag) => ( {tags.map((tag) => <li role="listitem">{tag}</li>)}
<li role="listitem"> </ul>
{tag} </div>
</li>
))}
</ul>
</div>

View File

@@ -3,44 +3,43 @@ import suggestedStyle from "./suggested-articles.module.scss";
import { Languages, PostInfo } from "types/index"; import { Languages, PostInfo } from "types/index";
interface TableOfContentsProps { interface TableOfContentsProps {
suggestedArticles: PostInfo[]; suggestedArticles: PostInfo[];
lang?: Languages; lang?: Languages;
} }
const { const { suggestedArticles, lang } = Astro.props as TableOfContentsProps;
suggestedArticles,
lang,
} = Astro.props as TableOfContentsProps;
--- ---
{!suggestedArticles.length ? null : <aside aria-label={"Suggested Articles"}> {
<ol role={"list"} class={suggestedStyle.list}> !suggestedArticles.length ? null : (
<h2 class={suggestedStyle.header}>Related posts</h2> <aside aria-label={"Suggested Articles"}>
{suggestedArticles.map((suggestedArticle, i) => { <ol role={"list"} class={suggestedStyle.list}>
const authorNames = suggestedArticle.authorsMeta <h2 class={suggestedStyle.header}>Related posts</h2>
.map((author) => author.name) {suggestedArticles.map((suggestedArticle, i) => {
.join(", "); const authorNames = suggestedArticle.authorsMeta
return ( .map((author) => author.name)
<li .join(", ");
class={`${suggestedStyle.card} ${suggestedStyle.localCard}`} return (
> <li class={`${suggestedStyle.card} ${suggestedStyle.localCard}`}>
<a <a
href={`/${lang !== "en" ? `${lang}/` : ""}posts/${ href={`/${lang !== "en" ? `${lang}/` : ""}posts/${
suggestedArticle.slug suggestedArticle.slug
}`} }`}
data-type="Analytics link" data-type="Analytics link"
data-category="suggested_article" data-category="suggested_article"
class={suggestedStyle.aTag} class={suggestedStyle.aTag}
> >
<span class={suggestedStyle.titleTag}> <span class={suggestedStyle.titleTag}>
{suggestedArticle.title} {suggestedArticle.title}
</span> </span>
<br /> <br />
<span class={suggestedStyle.srOnly}>by</span> <span class={suggestedStyle.srOnly}>by</span>
<span class={suggestedStyle.author}> {authorNames}</span> <span class={suggestedStyle.author}> {authorNames}</span>
</a> </a>
</li> </li>
); );
})} })}
</ol> </ol>
</aside>} </aside>
)
}

View File

@@ -1,50 +1,50 @@
@import "../../../styles/vars"; @import "../../../styles/vars";
@import "../../../styles/text_styles"; @import "../../../styles/text_styles";
@import "../../../components/post-card/post-card.module"; @import "../../../components/post-card/post-card.module";
.list { .list {
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.localCard { .localCard {
padding: 1rem !important; padding: 1rem !important;
margin-bottom: 1rem; margin-bottom: 1rem;
text-decoration: none; text-decoration: none;
} }
.localCard a { .localCard a {
text-decoration: none; text-decoration: none;
} }
.aTag { .aTag {
display: block; display: block;
} }
.localCard:hover .titleTag { .localCard:hover .titleTag {
text-decoration: underline; text-decoration: underline;
transition: text-decoration var(--animSpeed); transition: text-decoration var(--animSpeed);
} }
.header { .header {
@extend %headline-4; @extend %headline-4;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.author { .author {
color: var(--midImpactBlack) !important; color: var(--midImpactBlack) !important;
text-underline-style: none; text-underline-style: none;
} }
.srOnly { .srOnly {
position: absolute; position: absolute;
width: 1px; width: 1px;
height: 1px; height: 1px;
padding: 0; padding: 0;
margin: -1px; margin: -1px;
overflow: hidden; overflow: hidden;
clip: rect(0, 0, 0, 0); clip: rect(0, 0, 0, 0);
white-space: nowrap; white-space: nowrap;
border-width: 0; border-width: 0;
} }

View File

@@ -1,4 +1,4 @@
--- ---
--- ---
<script is:inline defer src="/scripts/tabs.min.js"/> <script is:inline defer src="/scripts/tabs.min.js"></script>

View File

@@ -1,56 +1,56 @@
--- ---
import * as Terser from "terser"; import * as Terser from "terser";
import { import { COLOR_MODE_STORAGE_KEY } from "../../constants";
COLOR_MODE_STORAGE_KEY,
} from "../../constants"; /**
* Much of this code deals with dark mode. It's ripped straight from:
/** * @see https://joshwcomeau.com/gatsby/dark-mode/
* Much of this code deals with dark mode. It's ripped straight from: *
* @see https://joshwcomeau.com/gatsby/dark-mode/ * Huge thanks to Josh for outlining how to do this
* */
* Huge thanks to Josh for outlining how to do this /**
*/ * DARK MODE CODE
/** *
* DARK MODE CODE * Prevents the "flash" of light mode
* */
* Prevents the "flash" of light mode /**
*/ * Trust me, I know that it looks like we're reading entries from an emoji
/** * but what's really happening is that this function is being converted to a
* Trust me, I know that it looks like we're reading entries from an emoji * string, then mutated by "MagicScriptTag" in order to add in dynamic code
* but what's really happening is that this function is being converted to a * into that string. This way, we're able to avoid duplicating
* string, then mutated by "MagicScriptTag" in order to add in dynamic code */
* into that string. This way, we're able to avoid duplicating function setColorsByTheme() {
*/ const STORAGE_KEY = "COLOR_MODE_STORAGE_KEY";
function setColorsByTheme() {
const STORAGE_KEY = "COLOR_MODE_STORAGE_KEY"; const mql = window.matchMedia("(prefers-color-scheme: dark)");
const prefersDarkFromMQ = mql.matches;
const mql = window.matchMedia("(prefers-color-scheme: dark)"); const prefersDarkFromLocalStorage = localStorage.getItem(STORAGE_KEY);
const prefersDarkFromMQ = mql.matches;
const prefersDarkFromLocalStorage = localStorage.getItem(STORAGE_KEY); let colorMode = "light";
let colorMode = "light"; const hasUsedToggle = typeof prefersDarkFromLocalStorage === "string";
const hasUsedToggle = typeof prefersDarkFromLocalStorage === "string"; if (hasUsedToggle) {
colorMode = prefersDarkFromLocalStorage;
if (hasUsedToggle) { } else {
colorMode = prefersDarkFromLocalStorage; colorMode = prefersDarkFromMQ ? "dark" : "light";
} else { }
colorMode = prefersDarkFromMQ ? "dark" : "light";
} let root = document.documentElement;
let root = document.documentElement; // TODO: migrate to `classList`
root.className = colorMode;
// TODO: migrate to `classList` }
root.className = colorMode;
} const boundFn = String(setColorsByTheme).replace(
"COLOR_MODE_STORAGE_KEY",
const boundFn = String(setColorsByTheme) COLOR_MODE_STORAGE_KEY
.replace("COLOR_MODE_STORAGE_KEY", COLOR_MODE_STORAGE_KEY); );
let calledFunction = `(${boundFn})()`; let calledFunction = `(${boundFn})()`;
calledFunction = (await Terser.minify(calledFunction)).code!; calledFunction = (await Terser.minify(calledFunction)).code!;
--- ---
<script set:html={calledFunction} /> <script set:html={calledFunction}></script>

View File

@@ -1,25 +1,25 @@
--- ---
import { import { COLORS } from "../../constants";
COLORS
} from "../../constants"; function getThemeStyling(theme: "light" | "dark") {
const CSS_THEME = Object.entries(COLORS).reduce((prev, [key, val]) => {
function getThemeStyling(theme: 'light' | 'dark') { prev += `\n--${key}: ${val[theme]};`;
const CSS_THEME = Object.entries(COLORS).reduce((prev, [key, val]) => { return prev;
prev += `\n--${key}: ${val[theme]};`; }, "");
return prev;
}, ""); return CSS_THEME;
}
return CSS_THEME;
} const rawStylesCSS = `
html.light, body.light {
const rawStylesCSS = ` ${getThemeStyling("light")}
html.light, body.light { }
${getThemeStyling('light')} html.dark, body.dark {
} ${getThemeStyling("dark")}
html.dark, body.dark { }
${getThemeStyling('dark')} `;
} ---
`
--- <style set:html={rawStylesCSS}>
<style set:html={rawStylesCSS} /> </style>

View File

@@ -1,26 +1,38 @@
--- ---
import styles from "./post-list-header.module.scss"; import styles from "./post-list-header.module.scss";
import {Image} from '@astrojs/image/components'; import { Image } from "@astrojs/image/components";
import unicornLogo from "../../../assets/unicorn_utterances_logo_512.png"; import unicornLogo from "../../../assets/unicorn_utterances_logo_512.png";
interface PostListHeaderProps { interface PostListHeaderProps {
siteDescription: string; siteDescription: string;
} }
const { siteDescription } = Astro.props as PostListHeaderProps; const { siteDescription } = Astro.props as PostListHeaderProps;
--- ---
<div class={styles.container} role="banner" aria-label={`Banner for Unicorn Utterances`}> <div
<div class={styles.headerPic}> class={styles.container}
<Image format={'png'} loading={"eager"} sizes={"300px"} alt={`Smiling cartoon unicorn with a role="banner"
bowtie`} src={unicornLogo} height={300} width={300} /> aria-label={`Banner for Unicorn Utterances`}
</div> >
<div class={styles.noMgContainer}> <div class={styles.headerPic}>
<h1 class={styles.title}>Unicorn Utterances</h1> <Image
<div class={styles.subheader}> format={"png"}
{siteDescription} loading={"eager"}
<br /> sizes={"300px"}
<a href={"/about"}>About Us</a> alt={`Smiling cartoon unicorn with a
</div> bowtie`}
</div> src={unicornLogo}
</div> height={300}
width={300}
/>
</div>
<div class={styles.noMgContainer}>
<h1 class={styles.title}>Unicorn Utterances</h1>
<div class={styles.subheader}>
{siteDescription}
<br />
<a href={"/about"}>About Us</a>
</div>
</div>
</div>

View File

@@ -12,31 +12,30 @@ import { Page } from "astro";
import Pagination from "components/pagination/pagination.astro"; import Pagination from "components/pagination/pagination.astro";
export interface PostListTemplateProps { export interface PostListTemplateProps {
posts: PostInfo[]; posts: PostInfo[];
rootURL: string; rootURL: string;
page: Pick<Page<PostInfo>, 'total' | 'currentPage' | 'size' | 'lastPage' | 'url'> page: Pick<
Page<PostInfo>,
"total" | "currentPage" | "size" | "lastPage" | "url"
>;
} }
const { const { posts, page, rootURL } = Astro.props as PostListTemplateProps;
posts,
page,
rootURL
} = Astro.props as PostListTemplateProps;
--- ---
<div> <div>
<!-- <PostListProvider <!-- <PostListProvider
posts={posts} posts={posts}
numberOfPages={numberOfPages} numberOfPages={numberOfPages}
limitNumber={limitNumber} limitNumber={limitNumber}
pageIndex={pageIndex} pageIndex={pageIndex}
> --> > -->
<PostListHeader siteDescription={siteMetadata.description} /> <PostListHeader siteDescription={siteMetadata.description} />
<main> <main>
<!-- <FilterSearchBar /> --> <!-- <FilterSearchBar /> -->
<PostList listAriaLabel="List of posts" postsToDisplay={posts} /> <PostList listAriaLabel="List of posts" postsToDisplay={posts} />
</main> </main>
<Pagination page={page} rootURL={rootURL} /> <Pagination page={page} rootURL={rootURL} />
<!-- <Pagination absolutePath={router.basePath} /> --> <!-- <Pagination absolutePath={router.basePath} /> -->
<!-- </PostListProvider> --> <!-- </PostListProvider> -->
</div> </div>

View File

@@ -1,117 +1,115 @@
--- ---
import styles from "./profile-header.module.scss"; import styles from "./profile-header.module.scss";
import { UnicornInfo } from "uu-types"; import { UnicornInfo } from "uu-types";
import {Image} from '@astrojs/image/components'; import { Image } from "@astrojs/image/components";
import { Icon } from 'astro-icon'; import { Icon } from "astro-icon";
import SocialBtn from "./social-button.astro"; import SocialBtn from "./social-button.astro";
const getNamePossessive = (name: string) => { const getNamePossessive = (name: string) => {
if (name.endsWith("s")) return `${name}'`; if (name.endsWith("s")) return `${name}'`;
return `${name}'s`; return `${name}'s`;
}; };
interface PicTitleHeaderProps { interface PicTitleHeaderProps {
unicornData: UnicornInfo; unicornData: UnicornInfo;
} }
const { unicornData } = Astro.props as PicTitleHeaderProps; const { unicornData } = Astro.props as PicTitleHeaderProps;
const possessiveName = getNamePossessive(unicornData.name); const possessiveName = getNamePossessive(unicornData.name);
---
---
<div
<div class={styles.container}
class={styles.container} role="banner"
role="banner" aria-label={`Banner for ${unicornData.name}`}
aria-label={`Banner for ${unicornData.name}`} >
> <div
<div class={styles.headerPic}
class={styles.headerPic} style={{
style={{ height: "300px",
height: "300px", width: "300px",
width: "300px", borderRadius: "50%",
borderRadius: "50%", overflow: "hidden",
overflow: "hidden", }}
}} >
> <Image
<Image src={unicornData.profileImgMeta.relativeServerPath}
src={unicornData.profileImgMeta.relativeServerPath} height={192}
height={192} width={192}
width={192} sizes={"192px"}
sizes={"192px"} format={"png"}
format={'png'} loading={"eager"}
loading={"eager"} alt={`${possessiveName} profile picture`}
alt={`${possessiveName} profile picture`} />
/> </div>
</div> <div class={styles.noMgContainer}>
<div class={styles.noMgContainer}> <h1 class={`${styles.title} ${styles.mobileTitle}`}>
<h1 class={`${styles.title} ${styles.mobileTitle}`}> {unicornData.name}
{unicornData.name} </h1>
</h1> <h1 class={`${styles.title} ${styles.desktopTitle}`}>
<h1 class={`${styles.title} ${styles.desktopTitle}`}> {unicornData.name}
{unicornData.name} </h1>
</h1> <div
<div class={styles.subheader}
class={styles.subheader} aria-label={`A description of ${unicornData.name}`}
aria-label={`A description of ${unicornData.name}`} >
> {unicornData.description}
{unicornData.description} </div>
</div> {
{unicornData.socials && ( unicornData.socials && (
<ul <ul
class={styles.socialsContainer} class={styles.socialsContainer}
aria-label={`${possessiveName} social media links`} aria-label={`${possessiveName} social media links`}
role="list" role="list"
> >
{unicornData.socials.twitter && ( {unicornData.socials.twitter && (
<SocialBtn <SocialBtn
text={"Twitter"} text={"Twitter"}
url={`https://twitter.com/${unicornData.socials.twitter}`} url={`https://twitter.com/${unicornData.socials.twitter}`}
> >
<Icon name="twitter" height="36" width="36" slot="icon"/> <Icon name="twitter" height="36" width="36" slot="icon" />
</SocialBtn> </SocialBtn>
)} )}
{unicornData.socials.github && ( {unicornData.socials.github && (
<SocialBtn <SocialBtn
text={"GitHub"} text={"GitHub"}
url={`https://github.com/${unicornData.socials.github}`} url={`https://github.com/${unicornData.socials.github}`}
> >
<Icon name="github" height="36" width="36" slot="icon"/> <Icon name="github" height="36" width="36" slot="icon" />
</SocialBtn> </SocialBtn>
)} )}
{unicornData.socials.linkedIn && ( {unicornData.socials.linkedIn && (
<SocialBtn <SocialBtn
text={"LinkedIn"} text={"LinkedIn"}
url={`https://www.linkedin.com/in/${unicornData.socials.linkedIn}`} url={`https://www.linkedin.com/in/${unicornData.socials.linkedIn}`}
> >
<Icon name="linkedin" height="36" width="36" slot="icon"/> <Icon name="linkedin" height="36" width="36" slot="icon" />
</SocialBtn> </SocialBtn>
)} )}
{unicornData.socials.twitch && ( {unicornData.socials.twitch && (
<SocialBtn <SocialBtn
text={"Twitch"} text={"Twitch"}
url={`https://twitch.tv/${unicornData.socials.twitch}`} url={`https://twitch.tv/${unicornData.socials.twitch}`}
> >
<Icon name="twitch" height="36" width="36" slot="icon"/> <Icon name="twitch" height="36" width="36" slot="icon" />
</SocialBtn> </SocialBtn>
)} )}
{unicornData.socials.dribbble && ( {unicornData.socials.dribbble && (
<SocialBtn <SocialBtn
text={"Dribbble"} text={"Dribbble"}
url={`https://dribbble.com/${unicornData.socials.dribbble}`} url={`https://dribbble.com/${unicornData.socials.dribbble}`}
> >
<Icon name="dribbble" height="36" width="36" slot="icon"/> <Icon name="dribbble" height="36" width="36" slot="icon" />
</SocialBtn> </SocialBtn>
)} )}
{unicornData.socials.website && ( {unicornData.socials.website && (
<SocialBtn <SocialBtn text={"Website"} url={unicornData.socials.website}>
text={"Website"} <Icon name="site" height="36" width="36" slot="icon" />
url={unicornData.socials.website} </SocialBtn>
> )}
<Icon name="site" height="36" width="36" slot="icon"/> </ul>
</SocialBtn> )
)} }
</ul> </div>
)} </div>
</div>
</div>

View File

@@ -1,100 +1,100 @@
@import "../../../styles/vars"; @import "../../../styles/vars";
@import "../../../styles/utils"; @import "../../../styles/utils";
@import "../../../styles/text_styles"; @import "../../../styles/text_styles";
.container { .container {
margin: 50px auto 58px auto; margin: 50px auto 58px auto;
max-width: 1032px; max-width: 1032px;
display: flex; display: flex;
position: relative; position: relative;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
@include from($endSmallScreenSize) { @include from($endSmallScreenSize) {
margin-bottom: 24px; margin-bottom: 24px;
flex-wrap: nowrap; flex-wrap: nowrap;
justify-content: flex-start; justify-content: flex-start;
} }
} }
.headerPic { .headerPic {
$mobileImgSize: 120px; $mobileImgSize: 120px;
max-width: $mobileImgSize; max-width: $mobileImgSize;
max-height: $mobileImgSize; max-height: $mobileImgSize;
margin-right: 0; margin-right: 0;
flex-shrink: 0; flex-shrink: 0;
& > img { & > img {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
@include from($endSmallScreenSize) { @include from($endSmallScreenSize) {
$desktopImgSize: 192px; $desktopImgSize: 192px;
max-width: $desktopImgSize; max-width: $desktopImgSize;
max-height: $desktopImgSize; max-height: $desktopImgSize;
margin-right: 48px; margin-right: 48px;
} }
} }
.title { .title {
transition: color var(--animStyle) var(--animSpeed); transition: color var(--animStyle) var(--animSpeed);
color: var(--highImpactBlack); color: var(--highImpactBlack);
margin: 0; margin: 0;
@include until($endSmallScreenSize) { @include until($endSmallScreenSize) {
text-align: center; text-align: center;
} }
} }
.mobileTitle { .mobileTitle {
@extend %headline-1; @extend %headline-1;
margin-top: 16px; margin-top: 16px;
@include from($endSmallScreenSize) { @include from($endSmallScreenSize) {
display: none; display: none;
} }
} }
.desktopTitle { .desktopTitle {
@extend %headline-2; @extend %headline-2;
@include until($endSmallScreenSize) { @include until($endSmallScreenSize) {
display: none; display: none;
} }
} }
.noMgContainer { .noMgContainer {
flex-grow: 1; flex-grow: 1;
} }
.subheader { .subheader {
@extend %subheader-3; @extend %subheader-3;
margin: 0; margin: 0;
transition: color var(--animStyle) var(--animSpeed); transition: color var(--animStyle) var(--animSpeed);
color: var(--midImpactBlack); color: var(--midImpactBlack);
white-space: pre-line; white-space: pre-line;
@include until($endSmallScreenSize) { @include until($endSmallScreenSize) {
margin: 16px 16px 0; margin: 16px 16px 0;
} }
} }
.socialsContainer { .socialsContainer {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
transition: color var(--animStyle) var(--animSpeed); transition: color var(--animStyle) var(--animSpeed);
color: var(--darkPrimary); color: var(--darkPrimary);
flex-wrap: wrap; flex-wrap: wrap;
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0 auto; margin: 0 auto;
justify-content: center; justify-content: center;
margin-top: 16px; margin-top: 16px;
@include from($endSmallScreenSize) { @include from($endSmallScreenSize) {
padding: 6px 0; padding: 6px 0;
margin: 0; margin: 0;
justify-content: flex-start; justify-content: flex-start;
} }
} }

View File

@@ -1,29 +1,24 @@
--- ---
interface SocialBtnProps { interface SocialBtnProps {
text: string; text: string;
url: string; url: string;
} }
const { text, url } = Astro.props as SocialBtnProps; const { text, url } = Astro.props as SocialBtnProps;
import styles from './social-button.module.scss'; import styles from "./social-button.module.scss";
--- ---
<li class={`baseBtn ${styles.socialBtnLink}`} role="listitem"> <li class={`baseBtn ${styles.socialBtnLink}`} role="listitem">
<!-- <AnalyticsLink <!-- <AnalyticsLink
category="outbound" category="outbound"
class="unlink" class="unlink"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
href={url} href={url}
> --> > -->
<a <a class="unlink" target="_blank" rel="noreferrer" href={url}>
class="unlink" <span class={styles.svgContainer} aria-hidden={true}>
target="_blank" <slot name="icon" />
rel="noreferrer" </span>
href={url} <span class={styles.socialText}>{text}</span>
> </a>
<span class={styles.svgContainer} aria-hidden={true}> </li>
<slot name="icon"/>
</span>
<span class={styles.socialText}>{text}</span>
</a>
</li>

View File

@@ -1,60 +1,59 @@
@import "../../../styles/vars"; @import "../../../styles/vars";
@import "../../../styles/utils"; @import "../../../styles/utils";
@import "../../../styles/text_styles"; @import "../../../styles/text_styles";
.svgContainer { .svgContainer {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.socialBtnLink {
.socialBtnLink { &:not(:last-of-type) {
&:not(:last-of-type) { margin-right: 32px;
margin-right: 32px;
@include from($endSmallScreenSize) {
@include from($endSmallScreenSize) { margin-right: 24px;
margin-right: 24px; }
} }
}
.socialText {
.socialText { margin-left: 0.5rem;
margin-left: 0.5rem; @extend %subheader-3;
@extend %subheader-3;
// Make it so that the next and prev label text is hidden on mobile
// Make it so that the next and prev label text is hidden on mobile // but not removed from the aria-reading role
// but not removed from the aria-reading role @include until($endSmallScreenSize) {
@include until($endSmallScreenSize) { width: 1px;
width: 1px; height: 1px;
height: 1px; overflow: hidden;
overflow: hidden; opacity: 0;
opacity: 0; display: inline-block;
display: inline-block; }
} }
}
& > a {
& > a { display: flex;
display: flex; flex-wrap: nowrap;
flex-wrap: nowrap; align-items: center;
align-items: center;
$mobileSvgSize: 32px;
$mobileSvgSize: 32px;
@include until($endSmallScreenSize) {
@include until($endSmallScreenSize) { height: $mobileSvgSize;
height: $mobileSvgSize; width: $mobileSvgSize;
width: $mobileSvgSize; }
}
svg {
svg { $desktopSvgSize: 36px;
$desktopSvgSize: 36px; height: $desktopSvgSize;
height: $desktopSvgSize; width: $desktopSvgSize;
width: $desktopSvgSize; fill: none;
fill: none;
@include until($endSmallScreenSize) {
@include until($endSmallScreenSize) { height: $mobileSvgSize;
height: $mobileSvgSize; width: $mobileSvgSize;
width: $mobileSvgSize; }
} }
} }
} }
}

View File

@@ -1,45 +1,42 @@
--- ---
import PostList from "components/post-card-list/post-card-list.astro"; import PostList from "components/post-card-list/post-card-list.astro";
import Pagination from "components/pagination/pagination.astro"; import Pagination from "components/pagination/pagination.astro";
import { Page } from "astro"; import { Page } from "astro";
import { PostInfo } from "types/PostInfo"; import { PostInfo } from "types/PostInfo";
import ProfileHeader from './profile-header/profile-header.astro'; import ProfileHeader from "./profile-header/profile-header.astro";
export interface UnicornTemplateProps { export interface UnicornTemplateProps {
unicorn: any; unicorn: any;
posts: PostInfo[]; posts: PostInfo[];
rootURL: string; rootURL: string;
page: Pick<Page<PostInfo>, 'total' | 'currentPage' | 'size' | 'lastPage' | 'url'> page: Pick<
} Page<PostInfo>,
"total" | "currentPage" | "size" | "lastPage" | "url"
const { >;
unicorn, }
page,
rootURL, const { unicorn, page, rootURL, posts } = Astro.props as UnicornTemplateProps;
posts ---
} = Astro.props as UnicornTemplateProps;
--- <!-- <PostListProvider
posts={authoredPosts}
numberOfPages={numberOfPages}
<!-- <PostListProvider limitNumber={postsPerPage}
posts={authoredPosts} pageIndex={pageNum}
numberOfPages={numberOfPages} > -->
limitNumber={postsPerPage} <ProfileHeader unicornData={unicorn} />
pageIndex={pageNum} <main>
> --> <!-- <FilterSearchBar>
<ProfileHeader unicornData={unicorn} /> <WordCount
<main> wordCount={wordCount}
<!-- <FilterSearchBar> numberOfArticles={authoredPosts.length}
<WordCount />
wordCount={wordCount} </FilterSearchBar> -->
numberOfArticles={authoredPosts.length} <PostList
/> listAriaLabel={`List of posts written by ${unicorn.name}`}
</FilterSearchBar> --> postsToDisplay={posts}
<PostList />
listAriaLabel={`List of posts written by ${unicorn.name}`} </main>
postsToDisplay={posts} <Pagination page={page} rootURL={rootURL} />
/> <!-- <Pagination absolutePath={basePath} />
</main> </PostListProvider> -->
<Pagination page={page} rootURL={rootURL} />
<!-- <Pagination absolutePath={basePath} />
</PostListProvider> -->

View File

@@ -1,13 +1,15 @@
--- ---
import Document from '../layouts/document.astro'; import Document from "../layouts/document.astro";
import PostListTemplate, { PostListTemplateProps } from '../page-components/post-list/post-list.astro'; import PostListTemplate, {
import { getAllPostsForListView } from 'utils/api'; PostListTemplateProps,
import {PostInfo} from 'types/PostInfo'; } from "../page-components/post-list/post-list.astro";
import { getAllPostsForListView } from "utils/api";
import { PostInfo } from "types/PostInfo";
import SEO from "components/seo/seo.astro"; import SEO from "components/seo/seo.astro";
const posts = await Astro.glob<PostInfo>('../../content/blog/**/*.md') const posts = await Astro.glob<PostInfo>("../../content/blog/**/*.md");
const enPosts = getAllPostsForListView(posts, 'en'); const enPosts = getAllPostsForListView(posts, "en");
const postsToDisplay = enPosts.slice(0, 8); const postsToDisplay = enPosts.slice(0, 8);
const page = { const page = {
@@ -17,12 +19,12 @@ const page = {
lastPage: Math.floor(enPosts.length / postsToDisplay.length), lastPage: Math.floor(enPosts.length / postsToDisplay.length),
url: { url: {
current: Astro.url.href, current: Astro.url.href,
next: '/page/2' next: "/page/2",
} },
} as PostListTemplateProps['page']; } as PostListTemplateProps["page"];
--- ---
<Document> <Document>
<SEO slot="head" title="Homepage" /> <SEO slot="head" title="Homepage" />
<PostListTemplate posts={postsToDisplay} page={page} rootURL={'/'}/> <PostListTemplate posts={postsToDisplay} page={page} rootURL={"/"} />
</Document> </Document>

View File

@@ -1,27 +1,27 @@
--- ---
import Document from '../../layouts/document.astro'; import Document from "../../layouts/document.astro";
import PostListTemplate from '../../page-components/post-list/post-list.astro'; import PostListTemplate from "../../page-components/post-list/post-list.astro";
import { getAllPostsForListView } from 'utils/api'; import { getAllPostsForListView } from "utils/api";
import {PostInfo} from 'types/PostInfo'; import { PostInfo } from "types/PostInfo";
import SEO from "components/seo/seo.astro"; import SEO from "components/seo/seo.astro";
import { Page } from 'astro'; import { Page } from "astro";
export async function getStaticPaths({ paginate }) { export async function getStaticPaths({ paginate }) {
const posts = await Astro.glob<PostInfo>('../../../content/blog/**/*.md') const posts = await Astro.glob<PostInfo>("../../../content/blog/**/*.md");
const postsToDisplay = getAllPostsForListView(posts, 'en'); const postsToDisplay = getAllPostsForListView(posts, "en");
return paginate(postsToDisplay, { pageSize: 8 }); return paginate(postsToDisplay, { pageSize: 8 });
} }
const { page } = Astro.props as {page: Page<PostInfo>}; const { page } = Astro.props as { page: Page<PostInfo> };
const pageIndex = page.currentPage; const pageIndex = page.currentPage;
const SEOTitle = `Post page ${pageIndex}`; const SEOTitle = `Post page ${pageIndex}`;
--- ---
<Document> <Document>
<SEO slot="head" title={SEOTitle} /> <SEO slot="head" title={SEOTitle} />
<PostListTemplate posts={page.data} page={page} rootURL={'/'}/> <PostListTemplate posts={page.data} page={page} rootURL={"/"} />
</Document> </Document>

View File

@@ -1,83 +1,84 @@
--- ---
import Document from '../../layouts/document.astro'; import Document from "../../layouts/document.astro";
import SEO from "components/seo/seo.astro"; import SEO from "components/seo/seo.astro";
import BlogPostLayout from 'components/blog-post-layout/blog-post-layout.astro'; import BlogPostLayout from "components/blog-post-layout/blog-post-layout.astro";
import PostTitleHeader from 'src/page-components/blog-post/post-title-header/post-title-header.astro'; import PostTitleHeader from "src/page-components/blog-post/post-title-header/post-title-header.astro";
import PostMetadata from 'src/page-components/blog-post/post-metadata/post-metadata.astro'; import PostMetadata from "src/page-components/blog-post/post-metadata/post-metadata.astro";
import TabsScript from 'src/page-components/blog-post/tabs-script/tabs-script.astro'; import TabsScript from "src/page-components/blog-post/tabs-script/tabs-script.astro";
import SuggestedArticles from 'src/page-components/blog-post/suggested-articles/suggested-articles.astro'; import SuggestedArticles from "src/page-components/blog-post/suggested-articles/suggested-articles.astro";
import TableOfContents from 'components/table-of-contents/table-of-contents.astro'; import TableOfContents from "components/table-of-contents/table-of-contents.astro";
import type { MarkdownInstance } from 'astro'; import type { MarkdownInstance } from "astro";
import {PostInfo} from 'types/PostInfo'; import { PostInfo } from "types/PostInfo";
import { Languages } from 'types/index'; import { Languages } from "types/index";
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await Astro.glob<PostInfo>('../../../content/blog/**/*.md') const posts = await Astro.glob<PostInfo>("../../../content/blog/**/*.md");
return posts.map(post => { return posts.map((post) => {
return { return {
params: { params: {
// TODO: Pass locale // TODO: Pass locale
postid: post.frontmatter.slug postid: post.frontmatter.slug,
}, },
props: { props: {
Content: post.Content, Content: post.Content,
post: post.frontmatter post: post.frontmatter,
} },
} };
}) });
} }
const { Content, post } = Astro.props as { const { Content, post } = Astro.props as {
post: PostInfo, post: PostInfo;
Content: MarkdownInstance<any>['Content'] Content: MarkdownInstance<any>["Content"];
} };
const translations = post?.translations || []; const translations = post?.translations || [];
const otherLangs = translations const otherLangs = translations
? (Object.keys(translations).filter( ? (Object.keys(translations).filter((t) => t !== post.locale) as Languages[])
(t) => t !== post.locale : [];
) as Languages[])
: [];
--- ---
<Document> <Document>
<TabsScript slot="head"/> <TabsScript slot="head" />
<!-- TODO: Add this --> <!-- TODO: Add this -->
<!-- pathName={router.asPath} --> <!-- pathName={router.asPath} -->
<SEO <SEO
slot="head" slot="head"
title={post.title} title={post.title}
description={post.description || post.excerpt} description={post.description || post.excerpt}
unicornsData={post.authors} unicornsData={post.authors}
publishedTime={post.published} publishedTime={post.published}
editedTime={post.edited} editedTime={post.edited}
keywords={post.tags} keywords={post.tags}
type="article" type="article"
canonical={post.originalLink} canonical={post.originalLink}
langData={{ langData={{
otherLangs, otherLangs,
currentLang: post.locale, currentLang: post.locale,
}} }}
shareImage={`/${post.slug}.twitter-preview.png`} shareImage={`/${post.slug}.twitter-preview.png`}
/> />
<article> <article>
<BlogPostLayout> <BlogPostLayout>
<div slot="left"> <div slot="left">
<TableOfContents headingsWithId={post.headingsWithId} /> <TableOfContents headingsWithId={post.headingsWithId} />
</div> </div>
<div slot="right"> <div slot="right">
<SuggestedArticles suggestedArticles={post.suggestedArticles} lang={"en"} /> <SuggestedArticles
</div> suggestedArticles={post.suggestedArticles}
<header role="banner" class="marginZeroAutoChild"> lang={"en"}
<PostTitleHeader post={post} /> />
<PostMetadata post={post} /> </div>
</header> <header role="banner" class="marginZeroAutoChild">
<main class="post-body" data-testid={"post-body-div"}> <PostTitleHeader post={post} />
<!-- {post.series ? ( <PostMetadata post={post} />
</header>
<main class="post-body" data-testid={"post-body-div"}>
<!-- {post.series ? (
<SeriesToC <SeriesToC
post={post} post={post}
postSeries={seriesPosts} postSeries={seriesPosts}
@@ -87,11 +88,11 @@ const otherLangs = translations
{post.translations && Object.keys(post.translations).length ? ( {post.translations && Object.keys(post.translations).length ? (
<TranslationsHeader post={post} /> <TranslationsHeader post={post} />
) : null} --> ) : null} -->
<Content/> <Content />
<!-- {post.series ? ( <!-- {post.series ? (
<SeriesNav post={post} postSeries={seriesPosts} /> <SeriesNav post={post} postSeries={seriesPosts} />
) : null} --> ) : null} -->
</main> </main>
</BlogPostLayout> </BlogPostLayout>
</article> </article>
</Document> </Document>

View File

@@ -1,45 +1,50 @@
--- ---
import Document from '../../../layouts/document.astro'; import Document from "../../../layouts/document.astro";
import SEO from "components/seo/seo.astro"; import SEO from "components/seo/seo.astro";
import UnicornsPage from '../../../page-components/unicorns/unicorn-page.astro'; import UnicornsPage from "../../../page-components/unicorns/unicorn-page.astro";
import { getAllPostsForUnicornListView } from 'utils/api'; import { getAllPostsForUnicornListView } from "utils/api";
import {PostInfo} from 'types/PostInfo'; import { PostInfo } from "types/PostInfo";
import { unicorns } from "utils/data"; import { unicorns } from "utils/data";
import { Page } from 'astro'; import { Page } from "astro";
export async function getStaticPaths() { export async function getStaticPaths() {
return unicorns.map(unicorn => ({ params: {unicornid: unicorn.id}})) return unicorns.map((unicorn) => ({ params: { unicornid: unicorn.id } }));
} }
const params = Astro.params as {unicornid: string}; const params = Astro.params as { unicornid: string };
const unicorn = unicorns.find(unicorn => unicorn.id === params.unicornid); const unicorn = unicorns.find((unicorn) => unicorn.id === params.unicornid);
const posts = await Astro.glob<PostInfo>('../../../../content/blog/**/*.md') const posts = await Astro.glob<PostInfo>("../../../../content/blog/**/*.md");
const enPosts = getAllPostsForUnicornListView(unicorn.id, posts, 'en'); const enPosts = getAllPostsForUnicornListView(unicorn.id, posts, "en");
const postsToDisplay = enPosts.slice(0, 8); const postsToDisplay = enPosts.slice(0, 8);
const page = { const page = {
total: enPosts.length, total: enPosts.length,
currentPage: 1, currentPage: 1,
size: postsToDisplay.length, size: postsToDisplay.length,
lastPage: Math.floor(enPosts.length / postsToDisplay.length), lastPage: Math.floor(enPosts.length / postsToDisplay.length),
url: { url: {
current: `/unicorns/${unicorn.id}`, current: `/unicorns/${unicorn.id}`,
next: `/unicorns/${unicorn.id}/page/2` next: `/unicorns/${unicorn.id}/page/2`,
} },
} as Page<PostInfo>; } as Page<PostInfo>;
const rootURL = `/unicorns/${unicorn.id}/`; const rootURL = `/unicorns/${unicorn.id}/`;
--- ---
<Document> <Document>
<SEO <SEO
slot="head" slot="head"
title={unicorn.name} title={unicorn.name}
description={unicorn.description} description={unicorn.description}
unicornsData={[unicorn]} unicornsData={[unicorn]}
type="profile" type="profile"
/> />
<UnicornsPage unicorn={unicorn} page={page} posts={postsToDisplay} rootURL={rootURL}/> <UnicornsPage
</Document> unicorn={unicorn}
page={page}
posts={postsToDisplay}
rootURL={rootURL}
/>
</Document>

View File

@@ -1,34 +1,48 @@
--- ---
import Document from '../../../../layouts/document.astro'; import Document from "../../../../layouts/document.astro";
import SEO from "components/seo/seo.astro"; import SEO from "components/seo/seo.astro";
import UnicornsPage from '../../../../page-components/unicorns/unicorn-page.astro'; import UnicornsPage from "../../../../page-components/unicorns/unicorn-page.astro";
import { getAllPostsForUnicornListView } from 'utils/api'; import { getAllPostsForUnicornListView } from "utils/api";
import {PostInfo} from 'types/PostInfo'; import { PostInfo } from "types/PostInfo";
import { unicorns } from "utils/data"; import { unicorns } from "utils/data";
import { Page } from 'astro'; import { Page } from "astro";
export async function getStaticPaths({ paginate }) { export async function getStaticPaths({ paginate }) {
const posts = await Astro.glob<PostInfo>('../../../../../content/blog/**/*.md') const posts = await Astro.glob<PostInfo>(
return unicorns.map(unicorn => { "../../../../../content/blog/**/*.md"
const postsToDisplay = getAllPostsForUnicornListView(unicorn.id, posts, 'en'); );
return paginate(postsToDisplay, { params: {unicornid: unicorn.id}, pageSize: 8 }); return unicorns.map((unicorn) => {
}) const postsToDisplay = getAllPostsForUnicornListView(
} unicorn.id,
posts,
const { page } = Astro.props as {page: Page<PostInfo>}; "en"
const params = Astro.params as {unicornid: string}; );
return paginate(postsToDisplay, {
const unicorn = unicorns.find(unicorn => unicorn.id === params.unicornid); params: { unicornid: unicorn.id },
const rootURL = `/unicorns/${unicorn.id}/`; pageSize: 8,
--- });
});
<Document> }
<SEO
slot="head" const { page } = Astro.props as { page: Page<PostInfo> };
title={unicorn.name} const params = Astro.params as { unicornid: string };
description={unicorn.description}
unicornsData={[unicorn]} const unicorn = unicorns.find((unicorn) => unicorn.id === params.unicornid);
type="profile" const rootURL = `/unicorns/${unicorn.id}/`;
/> ---
<UnicornsPage unicorn={unicorn} page={page} posts={page.data} rootURL={rootURL}/>
</Document> <Document>
<SEO
slot="head"
title={unicorn.name}
description={unicorn.description}
unicornsData={[unicorn]}
type="profile"
/>
<UnicornsPage
unicorn={unicorn}
page={page}
posts={page.data}
rootURL={rootURL}
/>
</Document>

View File

@@ -15,7 +15,7 @@ code .line::before {
margin-right: 1.5rem; margin-right: 1.5rem;
display: inline-block; display: inline-block;
text-align: right; text-align: right;
color: rgba(115,138,148,.4) color: rgba(115, 138, 148, 0.4);
} }
/* Start of Shiki Twoslash CSS: /* Start of Shiki Twoslash CSS:
@@ -77,7 +77,8 @@ pre.shiki:hover .dim {
pre.shiki div.dim { pre.shiki div.dim {
opacity: 0.5; opacity: 0.5;
} }
pre.shiki div.dim, pre.shiki div.highlight { pre.shiki div.dim,
pre.shiki div.highlight {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
@@ -90,7 +91,7 @@ pre.shiki div.line {
} }
/** Don't show the language identifiers */ /** Don't show the language identifiers */
pre.shiki .language-id{ pre.shiki .language-id {
display: none; display: none;
} }
@@ -163,7 +164,8 @@ pre code a {
} }
pre data-err { pre data-err {
/* Extracted from VS Code */ /* Extracted from VS Code */
background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c94824'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") repeat-x bottom left; background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c94824'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")
repeat-x bottom left;
padding-bottom: 3px; padding-bottom: 3px;
} }
pre .query { pre .query {
@@ -178,7 +180,8 @@ pre .query {
/* This sections keeps both of those two in in sync */ /* This sections keeps both of those two in in sync */
pre .error, pre .error-behind { pre .error,
pre .error-behind {
margin-left: -14px; margin-left: -14px;
margin-top: 8px; margin-top: 8px;
margin-bottom: 4px; margin-bottom: 4px;
@@ -346,7 +349,6 @@ pre .logger.log-log svg {
margin-right: 9px; margin-right: 9px;
} }
blockquote { blockquote {
position: relative; position: relative;
margin-left: 1em; margin-left: 1em;

View File

@@ -132,7 +132,6 @@ $robotoMono: "Roboto Mono", monospace;
} }
} }
%headline-uniwidth-2 { %headline-uniwidth-2 {
font-family: $asap; font-family: $asap;
font-weight: 400; font-weight: 400;

View File

@@ -1,59 +1,59 @@
.tabs { .tabs {
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
&__tab-list { &__tab-list {
padding: 0; padding: 0;
margin: 0 1rem -2px; margin: 0 1rem -2px;
} }
&__tab { &__tab {
display: inline-block; display: inline-block;
border: 1px solid transparent; border: 1px solid transparent;
border-bottom: none; border-bottom: none;
bottom: -2px; bottom: -2px;
position: relative; position: relative;
list-style: none; list-style: none;
padding: 6px 12px; padding: 6px 12px;
cursor: pointer; cursor: pointer;
background: var(--darkPrimary); background: var(--darkPrimary);
color: var(--backgroundColor); color: var(--backgroundColor);
border-radius: 0.5rem 0.5rem 0 0; border-radius: 0.5rem 0.5rem 0 0;
margin-right: 4px; margin-right: 4px;
&[aria-selected="true"] { &[aria-selected="true"] {
background: var(--backgroundColor); background: var(--backgroundColor);
border: 2px solid var(--darkPrimary); border: 2px solid var(--darkPrimary);
border-bottom: 0px; border-bottom: 0px;
margin-bottom: 2px; margin-bottom: 2px;
color: var(--darkPrimary); color: var(--darkPrimary);
} }
&[aria-disabled="true"] { &[aria-disabled="true"] {
color: GrayText; color: GrayText;
cursor: default; cursor: default;
} }
&:focus { &:focus {
box-shadow: 0 0 5px hsl(208, 99%, 50%); box-shadow: 0 0 5px hsl(208, 99%, 50%);
border-color: hsl(208, 99%, 50%); border-color: hsl(208, 99%, 50%);
outline: none; outline: none;
&:after { &:after {
content: ""; content: "";
position: absolute; position: absolute;
height: 5px; height: 5px;
left: -4px; left: -4px;
right: -4px; right: -4px;
bottom: -5px; bottom: -5px;
background: var(--backgroundColor); background: var(--backgroundColor);
} }
} }
} }
&__tab-panel { &__tab-panel {
border: 2px solid var(--darkPrimary); border: 2px solid var(--darkPrimary);
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 1.5rem; padding: 1.5rem;
} }
} }

View File

@@ -2,37 +2,37 @@ import { UnicornInfo } from "./UnicornInfo";
import { PostInfo } from "types/PostInfo"; import { PostInfo } from "types/PostInfo";
export interface CollectionInfo { export interface CollectionInfo {
slug: string; slug: string;
title: string; title: string;
authors: UnicornInfo[]; authors: UnicornInfo[];
description: string; description: string;
associatedSeries: string; associatedSeries: string;
published: string; published: string;
isbn?: string; isbn?: string;
type?: "book"; type?: "book";
coverImg: { coverImg: {
height: number; height: number;
width: number; width: number;
relativePath: string; relativePath: string;
}; };
socialImg?: string; socialImg?: string;
posts: Pick< posts: Pick<
PostInfo, PostInfo,
| "description" | "description"
| "excerpt" | "excerpt"
| "title" | "title"
| "order" | "order"
| "series" | "series"
| "slug" | "slug"
| "authors" | "authors"
| "content" | "content"
>[]; >[];
content: string; content: string;
buttons: Array<{ text: string; url: string }>; buttons: Array<{ text: string; url: string }>;
chapterList?: Array<{ chapterList?: Array<{
title: string; title: string;
description: string; description: string;
order: string; order: string;
}>; }>;
aboveFoldMarkdown?: string; aboveFoldMarkdown?: string;
} }

View File

@@ -1,8 +1,8 @@
export interface LicenseInfo { export interface LicenseInfo {
id: string; id: string;
licenceType: string; licenceType: string;
footerImg: string; footerImg: string;
explainLink: string; explainLink: string;
name: string; name: string;
displayName: string; displayName: string;
} }

View File

@@ -4,35 +4,35 @@ import { Languages } from "types/index";
import { MarkdownInstance } from "astro"; import { MarkdownInstance } from "astro";
export interface RawPostInfo { export interface RawPostInfo {
title: string; title: string;
published: string; published: string;
authors: string[]; authors: string[];
tags: string[]; tags: string[];
attached: string[]; attached: string[];
license: string; license: string;
description?: string; description?: string;
edited?: string; edited?: string;
series?: string; series?: string;
order?: number; order?: number;
originalLink?: string; originalLink?: string;
} }
export interface PostInfo extends RawPostInfo { export interface PostInfo extends RawPostInfo {
slug: string; slug: string;
locale: Languages; locale: Languages;
Content: MarkdownInstance<never>['Content']; Content: MarkdownInstance<never>["Content"];
authorsMeta: UnicornInfo[]; authorsMeta: UnicornInfo[];
licenseMeta: LicenseInfo; licenseMeta: LicenseInfo;
excerpt: string; excerpt: string;
wordCount: number; wordCount: number;
collectionSlug?: string | null; collectionSlug?: string | null;
translations: Partial<Record<Languages, string>>; translations: Partial<Record<Languages, string>>;
suggestedArticles: [PostInfo, PostInfo, PostInfo]; suggestedArticles: [PostInfo, PostInfo, PostInfo];
headingsWithId?: Array<{ headingsWithId?: Array<{
// Title value // Title value
value: string; value: string;
// ID // ID
slug: string; slug: string;
depth: number; depth: number;
}>; }>;
} }

View File

@@ -1,8 +1,8 @@
export interface PronounInfo { export interface PronounInfo {
id: string; id: string;
they: string; they: string;
them: string; them: string;
their: string; their: string;
theirs: string; theirs: string;
themselves: string; themselves: string;
} }

View File

@@ -1,25 +1,25 @@
export type RolesEnum = export type RolesEnum =
| { | {
id: "developer"; id: "developer";
prettyname: "Developer"; prettyname: "Developer";
} }
| { | {
id: "designer"; id: "designer";
prettyname: "Designer"; prettyname: "Designer";
} }
| { | {
id: "devops"; id: "devops";
prettyname: "Dev-ops"; prettyname: "Dev-ops";
} }
| { | {
id: "author"; id: "author";
prettyname: "Author"; prettyname: "Author";
} }
| { | {
id: "translator"; id: "translator";
prettyname: "Translator"; prettyname: "Translator";
} }
| { | {
id: "community"; id: "community";
prettyname: "Community Leader"; prettyname: "Community Leader";
}; };

View File

@@ -2,36 +2,36 @@ import { PronounInfo } from "./PronounInfo";
import { RolesEnum } from "./RolesInfo"; import { RolesEnum } from "./RolesInfo";
export interface RawUnicornInfo { export interface RawUnicornInfo {
id: string; id: string;
name: string; name: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
description: string; description: string;
socials: { socials: {
twitter?: string; twitter?: string;
github?: string; github?: string;
website?: string; website?: string;
linkedIn?: string; linkedIn?: string;
twitch?: string; twitch?: string;
dribbble?: string; dribbble?: string;
}; };
pronouns: string; pronouns: string;
profileImg: string; profileImg: string;
color: string; color: string;
roles: Array<RolesEnum['id']>; roles: Array<RolesEnum["id"]>;
} }
export interface UnicornInfo extends RawUnicornInfo { export interface UnicornInfo extends RawUnicornInfo {
rolesMeta: RolesEnum[]; rolesMeta: RolesEnum[];
pronounsMeta: PronounInfo; pronounsMeta: PronounInfo;
profileImgMeta: { profileImgMeta: {
// Relative to "public/unicorns" // Relative to "public/unicorns"
relativePath: string; relativePath: string;
// Relative to site root // Relative to site root
relativeServerPath: string; relativeServerPath: string;
// This is not stored, it's generated at build time // This is not stored, it's generated at build time
absoluteFSPath: string; absoluteFSPath: string;
height: number; height: number;
width: number; width: number;
}; };
} }

View File

@@ -1,6 +1,6 @@
declare module "gatsby-remark-embedder/dist/transformers/Twitch.js" { declare module "gatsby-remark-embedder/dist/transformers/Twitch.js" {
import { Transformer } from "@remark-embedder/core"; import { Transformer } from "@remark-embedder/core";
const transformer: Transformer<any>; const transformer: Transformer<any>;
export = transformer; export = transformer;
} }

View File

@@ -1,14 +1,14 @@
declare module "remark-behead" { declare module "remark-behead" {
import type { Node } from "unist"; import type { Node } from "unist";
import type { Root } from "mdast"; import type { Root } from "mdast";
import type { Plugin } from "unified"; import type { Plugin } from "unified";
export interface BeheadOptions { export interface BeheadOptions {
depth: number; depth: number;
after: number | string | Node; after: number | string | Node;
before: number | string | Node; before: number | string | Node;
between: [number | string | Node, number | string | Node]; between: [number | string | Node, number | string | Node];
} }
declare const plugin: Plugin<[BeheadOptions?] | void[], Root, string>; declare const plugin: Plugin<[BeheadOptions?] | void[], Root, string>;
export default plugin; export default plugin;
} }

View File

@@ -1,11 +1,11 @@
import lunr from "lunr"; import lunr from "lunr";
declare global { declare global {
interface Window { interface Window {
__LUNR__: { __LUNR__: {
index: lunr.Index; index: lunr.Index;
store: Record<string, any>; store: Record<string, any>;
__loaded: boolean | Promise<void>; __loaded: boolean | Promise<void>;
}; };
} }
} }

View File

@@ -2,49 +2,49 @@ import { PostInfo } from "types/PostInfo";
import { Languages } from "types/index"; import { Languages } from "types/index";
import { MarkdownInstance } from "astro"; import { MarkdownInstance } from "astro";
let allPostsCache = new WeakMap<object, MarkdownInstance<PostInfo>[]>(); const allPostsCache = new WeakMap<object, MarkdownInstance<PostInfo>[]>();
export function getAllPosts( export function getAllPosts(
posts: MarkdownInstance<PostInfo>[], posts: MarkdownInstance<PostInfo>[],
language: Languages, language: Languages,
cacheString: null | object = null cacheString: null | object = null
): MarkdownInstance<PostInfo>[] { ): MarkdownInstance<PostInfo>[] {
if (cacheString) { if (cacheString) {
const cacheData = allPostsCache.get(cacheString); const cacheData = allPostsCache.get(cacheString);
if (cacheData) return cacheData as any; if (cacheData) return cacheData as any;
} }
if (cacheString) allPostsCache.set(cacheString, posts); if (cacheString) allPostsCache.set(cacheString, posts);
return posts return posts.filter((post) => post.frontmatter.locale === language);
.filter(post => post.frontmatter.locale === language);
} }
const listViewCache = {}; const listViewCache = {};
export const getAllPostsForListView = ( export const getAllPostsForListView = (
posts: MarkdownInstance<PostInfo>[], posts: MarkdownInstance<PostInfo>[],
language: Languages, language: Languages
): PostInfo[] => { ): PostInfo[] => {
let allPosts = getAllPosts(posts, language, listViewCache); let allPosts = getAllPosts(posts, language, listViewCache);
// sort posts by date in descending order // sort posts by date in descending order
allPosts = allPosts.sort((post1, post2) => { allPosts = allPosts.sort((post1, post2) => {
const date1 = new Date(post1.frontmatter.published); const date1 = new Date(post1.frontmatter.published);
const date2 = new Date(post2.frontmatter.published); const date2 = new Date(post2.frontmatter.published);
return date1 > date2 ? -1 : 1; return date1 > date2 ? -1 : 1;
}); });
return allPosts.map(post => post.frontmatter).filter(post => post.locale === language); return allPosts
.map((post) => post.frontmatter)
.filter((post) => post.locale === language);
}; };
export const getAllPostsForUnicornListView = ( export const getAllPostsForUnicornListView = (
authorId: string, authorId: string,
posts: MarkdownInstance<PostInfo>[], posts: MarkdownInstance<PostInfo>[],
language: Languages, language: Languages
): PostInfo[] => { ): PostInfo[] => {
return getAllPostsForListView(posts, language) return getAllPostsForListView(posts, language).filter((post) =>
.filter(post => post.authorsMeta.find((postAuthor) => postAuthor.id === authorId)
post.authorsMeta.find(postAuthor => postAuthor.id === authorId) );
);
}; };

View File

@@ -11,63 +11,61 @@ export const siteDirectory = join(process.cwd(), "content/site");
export const sponsorsDirectory = join(process.cwd(), "public/sponsors"); export const sponsorsDirectory = join(process.cwd(), "public/sponsors");
const unicornsRaw: Array< const unicornsRaw: Array<
Omit<UnicornInfo, "roles" | "pronouns" | "profileImg"> & { Omit<UnicornInfo, "roles" | "pronouns" | "profileImg"> & {
roles: string[]; roles: string[];
pronouns: string; pronouns: string;
profileImg: string; profileImg: string;
} }
> = JSON.parse( > = JSON.parse(
fs.readFileSync(join(dataDirectory, "unicorns.json")).toString() fs.readFileSync(join(dataDirectory, "unicorns.json")).toString()
); );
const rolesRaw: RolesEnum[] = JSON.parse( const rolesRaw: RolesEnum[] = JSON.parse(
fs.readFileSync(join(dataDirectory, "roles.json")).toString() fs.readFileSync(join(dataDirectory, "roles.json")).toString()
); );
const pronounsRaw: PronounInfo[] = JSON.parse( const pronounsRaw: PronounInfo[] = JSON.parse(
fs.readFileSync(join(dataDirectory, "pronouns.json")).toString() fs.readFileSync(join(dataDirectory, "pronouns.json")).toString()
); );
const licensesRaw: LicenseInfo[] = JSON.parse( const licensesRaw: LicenseInfo[] = JSON.parse(
fs.readFileSync(join(dataDirectory, "licenses.json")).toString() fs.readFileSync(join(dataDirectory, "licenses.json")).toString()
); );
const fullUnicorns: UnicornInfo[] = unicornsRaw.map((unicorn) => { const fullUnicorns: UnicornInfo[] = unicornsRaw.map((unicorn) => {
const absoluteFSPath = join(dataDirectory, unicorn.profileImg); const absoluteFSPath = join(dataDirectory, unicorn.profileImg);
/** /**
* `getFullRelativePath` strips all prefixing `/`, so we must add one manually * `getFullRelativePath` strips all prefixing `/`, so we must add one manually
*/ */
const relativeServerPath = '/' + getFullRelativePath( const relativeServerPath =
"content/data/", "/" + getFullRelativePath("content/data/", unicorn.profileImg);
unicorn.profileImg const profileImgSize = getImageSize(unicorn.profileImg, dataDirectory);
);
const profileImgSize = getImageSize(unicorn.profileImg, dataDirectory);
// Mutation go BRR // Mutation go BRR
const newUnicorn: UnicornInfo = unicorn as never; const newUnicorn: UnicornInfo = unicorn as never;
newUnicorn.profileImgMeta = { newUnicorn.profileImgMeta = {
height: profileImgSize.height as number, height: profileImgSize.height as number,
width: profileImgSize.width as number, width: profileImgSize.width as number,
relativePath: unicorn.profileImg, relativePath: unicorn.profileImg,
relativeServerPath, relativeServerPath,
absoluteFSPath, absoluteFSPath,
}; };
newUnicorn.rolesMeta = unicorn.roles.map( newUnicorn.rolesMeta = unicorn.roles.map(
(role) => rolesRaw.find((rRole) => rRole.id === role)! (role) => rolesRaw.find((rRole) => rRole.id === role)!
); );
newUnicorn.pronounsMeta = pronounsRaw.find( newUnicorn.pronounsMeta = pronounsRaw.find(
(proWithNouns) => proWithNouns.id === unicorn.pronouns (proWithNouns) => proWithNouns.id === unicorn.pronouns
)!; )!;
return newUnicorn; return newUnicorn;
}); });
export { export {
fullUnicorns as unicorns, fullUnicorns as unicorns,
rolesRaw as roles, rolesRaw as roles,
pronounsRaw as pronouns, pronounsRaw as pronouns,
licensesRaw as licenses, licensesRaw as licenses,
}; };

View File

@@ -1,48 +1,48 @@
/** /**
* This should rare-to-never be used inside of an `.astro` file, instead, please use Astro.glob * This should rare-to-never be used inside of an `.astro` file, instead, please use Astro.glob
* *
* This file is really only useful when we need to get a list of all posts with metadata associated * This file is really only useful when we need to get a list of all posts with metadata associated
* when the Astro runtime isn't available, such as getting suggested articles and other instances. * when the Astro runtime isn't available, such as getting suggested articles and other instances.
*/ */
import { rehypeUnicornPopulatePost } from "./markdown/rehype-unicorn-populate-post"; import { rehypeUnicornPopulatePost } from "./markdown/rehype-unicorn-populate-post";
import { isNotJunk } from "junk"; import { isNotJunk } from "junk";
import { postsDirectory } from "./data"; import { postsDirectory } from "./data";
import { Languages, PostInfo } from "types/index"; import { Languages, PostInfo } from "types/index";
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
const getIndexPath = (lang: Languages) => { const getIndexPath = (lang: Languages) => {
const indexPath = lang !== "en" ? `index.${lang}.md` : `index.md`; const indexPath = lang !== "en" ? `index.${lang}.md` : `index.md`;
return indexPath; return indexPath;
}; };
export function getPostSlugs(lang: Languages) { export function getPostSlugs(lang: Languages) {
// Avoid errors trying to read from `.DS_Store` files // Avoid errors trying to read from `.DS_Store` files
return fs return fs
.readdirSync(postsDirectory) .readdirSync(postsDirectory)
.filter(isNotJunk) .filter(isNotJunk)
.filter((dir) => .filter((dir) =>
fs.existsSync(path.resolve(postsDirectory, dir, getIndexPath(lang))) fs.existsSync(path.resolve(postsDirectory, dir, getIndexPath(lang)))
); );
} }
export const getAllPosts = (lang: Languages): PostInfo[] => { export const getAllPosts = (lang: Languages): PostInfo[] => {
const slugs = getPostSlugs(lang); const slugs = getPostSlugs(lang);
return slugs.map(slug => { return slugs.map((slug) => {
const file = { const file = {
path: path.join(postsDirectory, slug, getIndexPath(lang)), path: path.join(postsDirectory, slug, getIndexPath(lang)),
data: { data: {
astro: { astro: {
frontmatter: {}, frontmatter: {},
}, },
}, },
}; };
(rehypeUnicornPopulatePost as any)()(undefined, file); (rehypeUnicornPopulatePost as any)()(undefined, file);
return { return {
...(file.data.astro.frontmatter as any || {}).frontmatterBackup, ...((file.data.astro.frontmatter as any) || {}).frontmatterBackup,
...file.data.astro.frontmatter ...file.data.astro.frontmatter,
}; };
}) });
} };

View File

@@ -4,14 +4,14 @@ import sizeOf from "image-size";
const absolutePathRegex = /^(?:[a-z]+:)?\/\//; const absolutePathRegex = /^(?:[a-z]+:)?\/\//;
export function getImageSize(src, dir) { export function getImageSize(src, dir) {
if (absolutePathRegex.exec(src)) { if (absolutePathRegex.exec(src)) {
return; return;
} }
// Treat `/` as a relative path, according to the server // Treat `/` as a relative path, according to the server
const shouldJoin = !path.isAbsolute(src) || src.startsWith("/"); const shouldJoin = !path.isAbsolute(src) || src.startsWith("/");
if (dir && shouldJoin) { if (dir && shouldJoin) {
src = path.join(dir, src); src = path.join(dir, src);
} }
return sizeOf(src); return sizeOf(src);
} }

View File

@@ -1,184 +1,181 @@
import { Languages, PostInfo } from "types/index"; import { Languages, PostInfo } from "types/index";
import { getAllPosts } from "./get-all-posts"; import { getAllPosts } from "./get-all-posts";
const postLangMap = new Map<string, ReturnType<typeof getAllPostsByLang>>(); const postLangMap = new Map<string, ReturnType<typeof getAllPostsByLang>>();
const getAllPostsByLang = ( const getAllPostsByLang = (
lang: Languages lang: Languages
): { suggestedPosts: PostInfo[]; dateSorted: PostInfo[] } => { ): { suggestedPosts: PostInfo[]; dateSorted: PostInfo[] } => {
if (postLangMap.has(lang)) return postLangMap.get(lang)!; if (postLangMap.has(lang)) return postLangMap.get(lang)!;
const suggestedPosts = getAllPosts(lang); const suggestedPosts = getAllPosts(lang);
// We must spread, since `sort` mutates the original array // We must spread, since `sort` mutates the original array
const dateSorted = [...suggestedPosts].sort((postA, postB) => { const dateSorted = [...suggestedPosts].sort((postA, postB) => {
return ( return (
// Newest first // Newest first
new Date(postB.published) < new Date(postA.published) ? -1 : 1 new Date(postB.published) < new Date(postA.published) ? -1 : 1
); );
}); });
postLangMap.set(lang, { suggestedPosts, dateSorted }); postLangMap.set(lang, { suggestedPosts, dateSorted });
return { suggestedPosts, dateSorted }; return { suggestedPosts, dateSorted };
}; };
export type OrderSuggestPosts = ReturnType< export type OrderSuggestPosts = ReturnType<
typeof getAllPostsByLang typeof getAllPostsByLang
>["suggestedPosts"]; >["suggestedPosts"];
/** /**
* Get 3 similar articles to suggest in sidebar. * Get 3 similar articles to suggest in sidebar.
* Base off of article series,and similar tags. * Base off of article series,and similar tags.
* If neither apply, simply grab latest articles * If neither apply, simply grab latest articles
* *
* However, they should take the precedence. If there are * However, they should take the precedence. If there are
* series articles, they should suggest higher than * series articles, they should suggest higher than
* matching tags * matching tags
* *
* We check exactly how similar tags are in general. For example, given one * We check exactly how similar tags are in general. For example, given one
* post with 4 tags that match, and another post with only 2, the one with * post with 4 tags that match, and another post with only 2, the one with
* 4 tags will show above the one with 2. * 4 tags will show above the one with 2.
* *
* For suggested articles, get the articles only within * For suggested articles, get the articles only within
* 1 series order of each other. * 1 series order of each other.
* *
* So, if we got "2", we could get: * So, if we got "2", we could get:
* 1, 3, 4 * 1, 3, 4
* *
* But not: * But not:
* 1, 3, 5 * 1, 3, 5
* *
* Or, alternatively, if we got "3", we could get: * Or, alternatively, if we got "3", we could get:
* 1, 2, 4 * 1, 2, 4
* *
* But not: * But not:
* 1, 4, 5 * 1, 4, 5
*/ */
const howManySimilarBetween = <T>(arr1: T[], arr2: T[]): number => { const howManySimilarBetween = <T>(arr1: T[], arr2: T[]): number => {
let match = 0; let match = 0;
for (let item of arr1) { for (const item of arr1) {
if (arr2.includes(item)) match++; if (arr2.includes(item)) match++;
} }
return match; return match;
}; };
const getOrderRange = (arr: OrderSuggestPosts) => { const getOrderRange = (arr: OrderSuggestPosts) => {
return arr.reduce<{ return arr.reduce<{
largest: null | OrderSuggestPosts[number]; largest: null | OrderSuggestPosts[number];
smallest: null | OrderSuggestPosts[number]; smallest: null | OrderSuggestPosts[number];
}>( }>(
(prev, curr) => { (prev, curr) => {
if (prev.smallest === null || prev.largest === null) { if (prev.smallest === null || prev.largest === null) {
return { return {
largest: curr, largest: curr,
smallest: curr, smallest: curr,
}; };
} }
if (curr.order! < prev.smallest.order!) { if (curr.order! < prev.smallest.order!) {
prev.smallest = curr; prev.smallest = curr;
} }
if (curr.order! > prev.largest.order!) { if (curr.order! > prev.largest.order!) {
prev.largest = curr; prev.largest = curr;
} }
return prev; return prev;
}, },
{ largest: null, smallest: null } { largest: null, smallest: null }
) as never as { ) as never as {
largest: OrderSuggestPosts[number]; largest: OrderSuggestPosts[number];
smallest: OrderSuggestPosts[number]; smallest: OrderSuggestPosts[number];
}; };
}; };
export const getSuggestedArticles = ( export const getSuggestedArticles = (postNode: PostInfo, lang: Languages) => {
postNode: PostInfo, const { suggestedPosts, dateSorted } = getAllPostsByLang(lang);
lang: Languages
) => { const extraSuggestedArticles: OrderSuggestPosts = [];
const { suggestedPosts, dateSorted } = getAllPostsByLang(lang); const suggestedArticles: OrderSuggestPosts = [];
const similarTags: Array<{
let extraSuggestedArticles: OrderSuggestPosts = []; post: OrderSuggestPosts[number];
let suggestedArticles: OrderSuggestPosts = []; howManyTagsSimilar: number;
let similarTags: Array<{ }> = [];
post: OrderSuggestPosts[number]; for (const post of suggestedPosts) {
howManyTagsSimilar: number; // Early "return" for value
}> = []; if (suggestedArticles.length >= 3) break;
for (let post of suggestedPosts) { // Don't return the same article
// Early "return" for value if (post.slug === postNode.slug) continue;
if (suggestedArticles.length >= 3) break;
// Don't return the same article if (!!post.series && post.series === postNode.series) {
if (post.slug === postNode.slug) continue; const { largest, smallest } =
getOrderRange([...suggestedArticles, postNode]) || {};
if (!!post.series && post.series === postNode.series) {
const { largest, smallest } = let newArticlePushed = false;
getOrderRange([...suggestedArticles, postNode]) || {}; if (
largest &&
let newArticlePushed = false; smallest &&
if ( (post.order === smallest.order! - 1 ||
largest && post.order === largest.order! + 1)
smallest && ) {
(post.order === smallest.order! - 1 || suggestedArticles.push(post);
post.order === largest.order! + 1) newArticlePushed = false;
) { }
suggestedArticles.push(post); /**
newArticlePushed = false; * Because we've just updated the `largest` and `smallest`, it's possible
} * there's another match in our list of suggested articles. Go check
/** *
* Because we've just updated the `largest` and `smallest`, it's possible * This may seem bad to do a while loop here, but I promise that we'll
* there's another match in our list of suggested articles. Go check * never have a series longer than even, like, 20 articles. This is a massive
* * improvement over looping through the entire list of articles.
* This may seem bad to do a while loop here, but I promise that we'll */
* never have a series longer than even, like, 20 articles. This is a massive while (newArticlePushed) {
* improvement over looping through the entire list of articles. if (suggestedArticles.length >= 3) break;
*/ if (extraSuggestedArticles.length === 0) break;
while (newArticlePushed) { const { largest, smallest } = getOrderRange(suggestedArticles) || {};
if (suggestedArticles.length >= 3) break; for (const suggestedPost of extraSuggestedArticles) {
if (extraSuggestedArticles.length === 0) break; if (
const { largest, smallest } = getOrderRange(suggestedArticles) || {}; suggestedPost.order === smallest.order! - 1 ||
for (let suggestedPost of extraSuggestedArticles) { suggestedPost.order === largest.order! + 1
if ( ) {
suggestedPost.order === smallest.order! - 1 || suggestedArticles.push(suggestedPost);
suggestedPost.order === largest.order! + 1 }
) { }
suggestedArticles.push(suggestedPost); }
} if (suggestedArticles.length >= 3) break;
} extraSuggestedArticles.push(post);
} }
if (suggestedArticles.length >= 3) break; const howManyTagsSimilar = howManySimilarBetween(
extraSuggestedArticles.push(post); post.tags,
} postNode.tags || []
const howManyTagsSimilar = howManySimilarBetween( );
post.tags, if (howManyTagsSimilar > 0) {
postNode.tags || [] similarTags.push({ post, howManyTagsSimilar });
); }
if (howManyTagsSimilar > 0) { }
similarTags.push({ post, howManyTagsSimilar });
} // Check to see if there are at least three suggested articles.
} // If not, fill it with another array of suggested articles.
const fillSuggestionArrayWith = (otherArr: OrderSuggestPosts) => {
// Check to see if there are at least three suggested articles. if (suggestedArticles.length < 3) {
// If not, fill it with another array of suggested articles. let sizeToPush = 3 - suggestedArticles.length;
const fillSuggestionArrayWith = (otherArr: OrderSuggestPosts) => { for (const item of otherArr) {
if (suggestedArticles.length < 3) { // Handle non-blog content, like about page
let sizeToPush = 3 - suggestedArticles.length; if (!item?.published) continue;
for (const item of otherArr) { // Don't suggest itself
// Handle non-blog content, like about page if (item.slug === postNode.slug) continue;
if (!item?.published) continue; // No duplicates, please!
// Don't suggest itself if (suggestedArticles.includes(item)) continue;
if (item.slug === postNode.slug) continue; suggestedArticles.push(item);
// No duplicates, please! sizeToPush--;
if (suggestedArticles.includes(item)) continue; if (sizeToPush <= 0) return;
suggestedArticles.push(item); }
sizeToPush--; }
if (sizeToPush <= 0) return; };
}
} const tagSimilaritySorted = similarTags
}; .sort((a, b) => b.howManyTagsSimilar - a.howManyTagsSimilar)
.map(({ post }) => post);
const tagSimilaritySorted = similarTags fillSuggestionArrayWith(tagSimilaritySorted);
.sort((a, b) => b.howManyTagsSimilar - a.howManyTagsSimilar)
.map(({ post }) => post); fillSuggestionArrayWith(dateSorted);
fillSuggestionArrayWith(tagSimilaritySorted);
return suggestedArticles;
fillSuggestionArrayWith(dateSorted); };
return suggestedArticles;
};

View File

@@ -1,77 +1,81 @@
import { Root } from "hast"; import { Root } from "hast";
import { Plugin } from "unified"; import { Plugin } from "unified";
import { visit } from "unist-util-visit"; import { visit } from "unist-util-visit";
import path from "path"; import path from "path";
/** /**
* They need to be the same `getImage` with the same `globalThis` instance, thanks to the "hack" workaround. * They need to be the same `getImage` with the same `globalThis` instance, thanks to the "hack" workaround.
*/ */
import { getImage } from "../../../node_modules/@astrojs/image"; import { getImage } from "../../../node_modules/@astrojs/image";
import sharp_service from "../../../node_modules/@astrojs/image/dist/loaders/sharp.js"; import sharp_service from "../../../node_modules/@astrojs/image/dist/loaders/sharp.js";
import {getImageSize} from "../get-image-size"; import { getImageSize } from "../get-image-size";
import {fileURLToPath} from "url"; import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
interface RehypeAstroImageProps { interface RehypeAstroImageProps {
maxHeight?: number; maxHeight?: number;
maxWidth?: number; maxWidth?: number;
} }
export const rehypeAstroImageMd: Plugin< export const rehypeAstroImageMd: Plugin<
[RehypeAstroImageProps | never], [RehypeAstroImageProps | never],
Root Root
> = ({ maxHeight, maxWidth }) => { > = ({ maxHeight, maxWidth }) => {
return async (tree, file) => { return async (tree, file) => {
// HACK: This is a hack that heavily relies on `getImage`'s internals :( // HACK: This is a hack that heavily relies on `getImage`'s internals :(
globalThis.astroImage = { globalThis.astroImage = {
loader: sharp_service ?? globalThis.astroImage?.loader, loader: sharp_service ?? globalThis.astroImage?.loader,
}; };
let imgNodes: any[] = []; const imgNodes: any[] = [];
visit(tree, (node: any) => { visit(tree, (node: any) => {
if (node.tagName === "img") { if (node.tagName === "img") {
imgNodes.push(node); imgNodes.push(node);
} }
}); });
await Promise.all( await Promise.all(
imgNodes.map(async (node) => { imgNodes.map(async (node) => {
const slug = path.dirname(file.path).split('/').at(-1); const slug = path.dirname(file.path).split("/").at(-1);
const filePathDir = path.resolve(__dirname, '../../../public/content/blog', slug) const filePathDir = path.resolve(
__dirname,
// TODO: How should remote images be handled? "../../../public/content/blog",
const dimensions = getImageSize(node.properties.src, filePathDir) || { slug
height: undefined, );
width: undefined,
}; // TODO: How should remote images be handled?
const dimensions = getImageSize(node.properties.src, filePathDir) || {
// TODO: Remote images? height: undefined,
if (!dimensions.height || !dimensions.width) return; width: undefined,
};
const imgRatioHeight = dimensions.height / dimensions.width;
const imgRatioWidth = dimensions.width / dimensions.height; // TODO: Remote images?
if (maxHeight && dimensions.height > maxHeight) { if (!dimensions.height || !dimensions.width) return;
dimensions.height = maxHeight;
dimensions.width = maxHeight * imgRatioWidth; const imgRatioHeight = dimensions.height / dimensions.width;
} const imgRatioWidth = dimensions.width / dimensions.height;
if (maxHeight && dimensions.height > maxHeight) {
if (maxWidth && dimensions.width > maxWidth) { dimensions.height = maxHeight;
dimensions.width = maxWidth; dimensions.width = maxHeight * imgRatioWidth;
dimensions.height = maxWidth * imgRatioHeight; }
}
if (maxWidth && dimensions.width > maxWidth) {
const imgProps = await getImage({ dimensions.width = maxWidth;
src: `/content/blog/${slug}/${node.properties.src}`, dimensions.height = maxWidth * imgRatioHeight;
height: dimensions.height, }
width: dimensions.width,
}); const imgProps = await getImage({
src: `/content/blog/${slug}/${node.properties.src}`,
node.properties.src = imgProps.src; height: dimensions.height,
}) width: dimensions.width,
); });
};
}; node.properties.src = imgProps.src;
})
);
};
};

View File

@@ -1,37 +1,37 @@
import { Root } from "hast"; import { Root } from "hast";
import { Plugin } from "unified"; import { Plugin } from "unified";
import {visit} from 'unist-util-visit' import { visit } from "unist-util-visit";
interface RehypeExcerptProps { interface RehypeExcerptProps {
maxLength: number; maxLength: number;
} }
export const rehypeExcerpt: Plugin< export const rehypeExcerpt: Plugin<[RehypeExcerptProps | never], Root> = ({
[RehypeExcerptProps | never], maxLength,
Root }) => {
> = ({maxLength}) => { return (tree, file) => {
return (tree, file) => { const getFileExcerpt = () =>
const getFileExcerpt = () => (file?.data?.astro as any)?.frontmatter?.excerpt as string; (file?.data?.astro as any)?.frontmatter?.excerpt as string;
const setFileExcerpt = (val) => { const setFileExcerpt = (val) => {
(file.data.astro as any).frontmatter.excerpt = val; (file.data.astro as any).frontmatter.excerpt = val;
} };
if (!getFileExcerpt()) { if (!getFileExcerpt()) {
setFileExcerpt(""); setFileExcerpt("");
} }
visit(tree, 'element', node => { visit(tree, "element", (node) => {
const fileExcerpt = getFileExcerpt(); const fileExcerpt = getFileExcerpt();
if (fileExcerpt.length >= maxLength) return; if (fileExcerpt.length >= maxLength) return;
// Don't get headers or anything other than text // Don't get headers or anything other than text
if (node.tagName === 'p') { if (node.tagName === "p") {
visit(node, 'text', textNode => { visit(node, "text", (textNode) => {
let newVal = fileExcerpt + textNode.value; let newVal = fileExcerpt + textNode.value;
if (newVal.length > maxLength) { if (newVal.length > maxLength) {
newVal = newVal.slice(0, maxLength - 3) + "..."; newVal = newVal.slice(0, maxLength - 3) + "...";
} }
setFileExcerpt(newVal); setFileExcerpt(newVal);
}) });
} }
}); });
}; };
}; };

View File

@@ -2,35 +2,35 @@ import { headingRank } from "hast-util-heading-rank";
import { hasProperty } from "hast-util-has-property"; import { hasProperty } from "hast-util-has-property";
import { toString } from "hast-util-to-string"; import { toString } from "hast-util-to-string";
import { Root, Parent } from "hast"; import { Root, Parent } from "hast";
import {visit} from "unist-util-visit"; import { visit } from "unist-util-visit";
/** /**
* Plugin to add `data-header-text`s to headings. * Plugin to add `data-header-text`s to headings.
*/ */
export const rehypeHeaderText = () => { export const rehypeHeaderText = () => {
return (tree: Root, file) => { return (tree: Root, file) => {
visit(tree, "element", (node: Parent["children"][number]) => { visit(tree, "element", (node: Parent["children"][number]) => {
if ( if (
headingRank(node) && headingRank(node) &&
"properties" in node && "properties" in node &&
node.properties && node.properties &&
!hasProperty(node, "data-header-text") !hasProperty(node, "data-header-text")
) { ) {
const headerText = toString(node); const headerText = toString(node);
node.properties["data-header-text"] = headerText; node.properties["data-header-text"] = headerText;
const headingWithID = { const headingWithID = {
value: headerText, value: headerText,
depth: headingRank(node)!, depth: headingRank(node)!,
slug: node.properties["id"] as string, slug: node.properties["id"] as string,
}; };
if (file.data.astro.frontmatter.headingsWithId) { if (file.data.astro.frontmatter.headingsWithId) {
file.data.astro.frontmatter.headingsWithId.push(headingWithID) file.data.astro.frontmatter.headingsWithId.push(headingWithID);
} else { } else {
file.data.astro.frontmatter.headingsWithId = [headingWithID]; file.data.astro.frontmatter.headingsWithId = [headingWithID];
} }
} }
}); });
}; };
}; };

View File

@@ -1,115 +1,116 @@
import { Root } from "hast"; import { Root } from "hast";
import { Plugin } from "unified"; import { Plugin } from "unified";
import { visit } from "unist-util-visit"; import { visit } from "unist-util-visit";
import { EMBED_SIZE } from "./constants"; import { EMBED_SIZE } from "./constants";
import { isRelativePath } from "../url-paths"; import { isRelativePath } from "../url-paths";
import { fromHtml } from "hast-util-from-html"; import { fromHtml } from "hast-util-from-html";
import path from "path"; import path from "path";
interface RehypeUnicornElementMapProps { interface RehypeUnicornElementMapProps {}
}
function escapeHTML(s) {
function escapeHTML(s) { if (!s) return s;
if (!s) return s; return s
return s .replace(/&/g, "&amp;")
.replace(/&/g, "&amp;") .replace(/"/g, "&quot;")
.replace(/"/g, "&quot;") .replace(/</g, "&lt;")
.replace(/</g, "&lt;") .replace(/>/g, "&gt;");
.replace(/>/g, "&gt;"); }
}
// TODO: Add switch/case and dedicated files ala "Components"
// TODO: Add switch/case and dedicated files ala "Components" export const rehypeUnicornElementMap: Plugin<
export const rehypeUnicornElementMap: Plugin< [RehypeUnicornElementMapProps | never],
[RehypeUnicornElementMapProps | never], Root
Root > = () => {
> = () => { return async (tree, file) => {
return async (tree, file) => { visit(tree, (node: any) => {
visit(tree, (node: any) => { if (node.tagName === "iframe") {
if (node.tagName === "iframe") { node.properties.width ??= EMBED_SIZE.w;
node.properties.width ??= EMBED_SIZE.w; node.properties.height ??= EMBED_SIZE.h;
node.properties.height ??= EMBED_SIZE.h; node.properties.loading ??= "lazy";
node.properties.loading ??= "lazy"; }
}
if (node.tagName === "video") {
if (node.tagName === "video") { node.properties.muted ??= true;
node.properties.muted ??= true; node.properties.autoPlay ??= true;
node.properties.autoPlay ??= true; node.properties.controls ??= true;
node.properties.controls ??= true; node.properties.loop ??= true;
node.properties.loop ??= true; node.properties.width ??= "100%";
node.properties.width ??= "100%"; node.properties.height ??= "auto";
node.properties.height ??= "auto"; }
}
if (node.tagName === "a") {
if (node.tagName === "a") { const href = node.properties.href;
const href = node.properties.href; const isInternalLink = isRelativePath(href || "");
const isInternalLink = isRelativePath(href || ""); if (!isInternalLink) {
if (!isInternalLink) { node.properties.target = "_blank";
node.properties.target = "_blank"; node.properties.rel = "nofollow noopener noreferrer";
node.properties.rel = "nofollow noopener noreferrer"; }
} }
}
if (node.tagName === "table" && !node.properties["has-changed"]) {
if (node.tagName === "table" && !node.properties["has-changed"]) { const children = [...node.children];
const children = [...node.children]; const properties = { ...node.properties, "has-changed": true };
const properties = { ...node.properties, "has-changed": true }; node.tagName = "div";
node.tagName = "div"; node.properties = {
node.properties = { class: "table-container",
class: "table-container", };
}; node.children = [
node.children = [ {
{ tagName: "table",
tagName: "table", type: "element",
type: "element", children,
children, properties,
properties, },
}, ];
]; }
}
if (
if ( node.tagName === "h1" ||
node.tagName === "h1" || node.tagName === "h2" ||
node.tagName === "h2" || node.tagName === "h3" ||
node.tagName === "h3" || node.tagName === "h4" ||
node.tagName === "h4" || node.tagName === "h5" ||
node.tagName === "h5" || node.tagName === "h6"
node.tagName === "h6" ) {
) { const id = node.properties.id;
const id = node.properties.id; const headerText = node.properties["data-header-text"];
const headerText = node.properties["data-header-text"]; node.properties.style =
node.properties.style = (node.properties.style || "") + "position: relative;";
(node.properties.style || "") + "position: relative;";
const headerLinkHTML = `
const headerLinkHTML = ` <a
<a href="#${id}"
href="#${id}" aria-label="Permalink for &quot;${escapeHTML(
aria-label="Permalink for &quot;${escapeHTML(headerText)}&quot;" headerText
class="anchor before" )}&quot;"
> class="anchor before"
<svg >
width="20" <svg
height="20" width="20"
viewBox="0 0 36 36" height="20"
fill="none" viewBox="0 0 36 36"
xmlns="http://www.w3.org/2000/svg" fill="none"
> xmlns="http://www.w3.org/2000/svg"
<path >
d="M8.10021 27.8995C6.14759 25.9469 6.14759 22.7811 8.10021 20.8284L12.6964 16.2322C14.4538 14.4749 17.303 14.4749 19.0604 16.2322L20.121 17.2929C20.7068 17.8787 21.6566 17.8787 22.2423 17.2929C22.8281 16.7071 22.8281 15.7574 22.2423 15.1716L21.1817 14.1109C18.2528 11.182 13.504 11.182 10.5751 14.1109L5.97889 18.7071C2.85469 21.8313 2.85469 26.8966 5.97889 30.0208C9.10308 33.145 14.1684 33.145 17.2926 30.0208L18.3533 28.9602C18.939 28.3744 18.939 27.4246 18.3533 26.8388C17.7675 26.2531 16.8177 26.2531 16.2319 26.8388L15.1713 27.8995C13.2187 29.8521 10.0528 29.8521 8.10021 27.8995Z" <path
fill="#153E67" d="M8.10021 27.8995C6.14759 25.9469 6.14759 22.7811 8.10021 20.8284L12.6964 16.2322C14.4538 14.4749 17.303 14.4749 19.0604 16.2322L20.121 17.2929C20.7068 17.8787 21.6566 17.8787 22.2423 17.2929C22.8281 16.7071 22.8281 15.7574 22.2423 15.1716L21.1817 14.1109C18.2528 11.182 13.504 11.182 10.5751 14.1109L5.97889 18.7071C2.85469 21.8313 2.85469 26.8966 5.97889 30.0208C9.10308 33.145 14.1684 33.145 17.2926 30.0208L18.3533 28.9602C18.939 28.3744 18.939 27.4246 18.3533 26.8388C17.7675 26.2531 16.8177 26.2531 16.2319 26.8388L15.1713 27.8995C13.2187 29.8521 10.0528 29.8521 8.10021 27.8995Z"
/> fill="#153E67"
<path />
d="M27.8992 8.10051C29.8518 10.0531 29.8518 13.219 27.8992 15.1716L23.303 19.7678C21.5456 21.5251 18.6964 21.5251 16.939 19.7678L15.8784 18.7071C15.2926 18.1213 14.3428 18.1213 13.7571 18.7071C13.1713 19.2929 13.1713 20.2426 13.7571 20.8284L14.8177 21.8891C17.7467 24.818 22.4954 24.818 25.4243 21.8891L30.0205 17.2929C33.1447 14.1687 33.1447 9.10339 30.0205 5.97919C26.8963 2.855 21.831 2.855 18.7068 5.97919L17.6461 7.03985C17.0604 7.62564 17.0604 8.57539 17.6461 9.16117C18.2319 9.74696 19.1817 9.74696 19.7675 9.16117L20.8281 8.10051C22.7808 6.14789 25.9466 6.14789 27.8992 8.10051Z" <path
fill="#153E67" d="M27.8992 8.10051C29.8518 10.0531 29.8518 13.219 27.8992 15.1716L23.303 19.7678C21.5456 21.5251 18.6964 21.5251 16.939 19.7678L15.8784 18.7071C15.2926 18.1213 14.3428 18.1213 13.7571 18.7071C13.1713 19.2929 13.1713 20.2426 13.7571 20.8284L14.8177 21.8891C17.7467 24.818 22.4954 24.818 25.4243 21.8891L30.0205 17.2929C33.1447 14.1687 33.1447 9.10339 30.0205 5.97919C26.8963 2.855 21.831 2.855 18.7068 5.97919L17.6461 7.03985C17.0604 7.62564 17.0604 8.57539 17.6461 9.16117C18.2319 9.74696 19.1817 9.74696 19.7675 9.16117L20.8281 8.10051C22.7808 6.14789 25.9466 6.14789 27.8992 8.10051Z"
/> fill="#153E67"
</svg> />
</a> </svg>
`; </a>
`;
const hastHeader = fromHtml(headerLinkHTML, { fragment: true });
node.children = [hastHeader, ...node.children]; const hastHeader = fromHtml(headerLinkHTML, { fragment: true });
} node.children = [hastHeader, ...node.children];
}); }
}; });
}; };
};

View File

@@ -1,25 +1,24 @@
import { Root } from "hast"; import { Root } from "hast";
import { Plugin } from "unified"; import { Plugin } from "unified";
import {getSuggestedArticles} from "../get-suggested-articles"; import { getSuggestedArticles } from "../get-suggested-articles";
interface RehypeUnicornGetSuggestedPostsProps { interface RehypeUnicornGetSuggestedPostsProps {}
}
export const rehypeUnicornGetSuggestedPosts: Plugin<
export const rehypeUnicornGetSuggestedPosts: Plugin< [RehypeUnicornGetSuggestedPostsProps | never],
[RehypeUnicornGetSuggestedPostsProps | never], Root
Root > = () => {
> = () => { return (_, file) => {
return (_, file) => { function setData(key: string, val: any) {
function setData(key: string, val: any) { (file.data.astro as any).frontmatter[key] = val;
(file.data.astro as any).frontmatter[key] = val; }
}
const post = {
const post = { ...(file.data.astro as any).frontmatter.frontmatterBackup,
...(file.data.astro as any).frontmatter.frontmatterBackup, ...(file.data.astro as any).frontmatter,
...(file.data.astro as any).frontmatter };
}
const suggestedArticles = getSuggestedArticles(post, "en");
const suggestedArticles = getSuggestedArticles(post, 'en'); setData("suggestedArticles", suggestedArticles);
setData("suggestedArticles", suggestedArticles); };
}; };
};

View File

@@ -1,81 +1,77 @@
import { Root } from "hast"; import { Root } from "hast";
import { Plugin } from "unified"; import { Plugin } from "unified";
import matter from "gray-matter"; import matter from "gray-matter";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import * as path from "path"; import * as path from "path";
import { licenses, unicorns } from "../data"; import { licenses, unicorns } from "../data";
interface RehypeUnicornPopulatePostProps { interface RehypeUnicornPopulatePostProps {}
}
export const rehypeUnicornPopulatePost: Plugin<
export const rehypeUnicornPopulatePost: Plugin< [RehypeUnicornPopulatePostProps | never],
[RehypeUnicornPopulatePostProps | never], Root
Root > = () => {
> = () => { return (_, file) => {
return (_, file) => { function setData(key: string, val: any) {
(file.data.astro as any).frontmatter[key] = val;
function setData(key: string, val: any) { }
(file.data.astro as any).frontmatter[key] = val;
} const fileContents = readFileSync(file.path, "utf8");
const { data: frontmatter } = matter(fileContents);
const fileContents = readFileSync(file.path, "utf8");
const { data: frontmatter } = matter(fileContents); const directorySplit = file.path.split(path.sep);
const directorySplit = file.path.split(path.sep); // This is the folder name, AKA how we generate the slug ID
const slug = directorySplit.at(-2);
// This is the folder name, AKA how we generate the slug ID
const slug = directorySplit.at(-2); // Calculate post locale
// index.md or index.es.md
// Calculate post locale const indexName = directorySplit.at(-1);
// index.md or index.es.md const indexSplit = indexName.split(".");
const indexName = directorySplit.at(-1); let locale = indexSplit.at(-2);
const indexSplit = indexName.split('.'); if (locale === "index") {
let locale = indexSplit.at(-2); locale = "en";
if (locale === 'index') { }
locale = 'en';
} // // TODO: Add translations
// if (fields.translations) {
// // TODO: Add translations // const langsToQuery: Languages[] = Object.keys(languages).filter(
// if (fields.translations) { // (l) => l !== lang
// const langsToQuery: Languages[] = Object.keys(languages).filter( // ) as never;
// (l) => l !== lang // pickedData.translations = langsToQuery
// ) as never; // .filter((lang) =>
// pickedData.translations = langsToQuery // fs.existsSync(resolve(dirname(fullPath), getIndexPath(lang)))
// .filter((lang) => // )
// fs.existsSync(resolve(dirname(fullPath), getIndexPath(lang))) // .reduce((prev, lang) => {
// ) // prev[lang] = languages[lang];
// .reduce((prev, lang) => { // return prev;
// prev[lang] = languages[lang]; // }, {} as Record<Languages, string>);
// return prev; // }
// }, {} as Record<Languages, string>);
// } // // TODO: Add collection slug
// if (fields.collectionSlug) {
// // TODO: Add collection slug // if (frontmatterData.series) {
// if (fields.collectionSlug) { // pickedData.collectionSlug = collectionsByName.find(
// if (frontmatterData.series) { // (collection) => collection.associatedSeries === frontmatterData.series
// pickedData.collectionSlug = collectionsByName.find( // )?.slug;
// (collection) => collection.associatedSeries === frontmatterData.series // }
// )?.slug; // if (!pickedData.collectionSlug) pickedData.collectionSlug = null;
// } // }
// if (!pickedData.collectionSlug) pickedData.collectionSlug = null;
// } const authorsMeta = (frontmatter.authors as string[]).map(
(author) => unicorns.find((unicorn) => unicorn.id === author)!
const authorsMeta = (frontmatter.authors as string[]).map( );
(author) => unicorns.find((unicorn) => unicorn.id === author)!
); let license;
if (frontmatter.license) {
let license; license = licenses.find((l) => l.id === frontmatter.license);
if (frontmatter.license) { }
license = licenses.find( if (!license) license = null;
(l) => l.id === frontmatter.license
); setData("slug", slug);
} setData("locale", locale);
if (!license) license = null; setData("authorsMeta", authorsMeta);
setData("license", license);
setData('slug', slug); setData("frontmatterBackup", frontmatter);
setData('locale', locale); };
setData('authorsMeta', authorsMeta); };
setData('license', license);
setData('frontmatterBackup', frontmatter);
};
};

View File

@@ -1,72 +1,76 @@
/** /**
* An ode to words * An ode to words
* *
* Oh words, what can be said of thee? * Oh words, what can be said of thee?
* *
* Not much me. * Not much me.
* *
* See, it's concieved that ye might have intreging definitions from one-to-another * See, it's concieved that ye might have intreging definitions from one-to-another
* *
* This is to say: "What is a word?" * This is to say: "What is a word?"
* *
* An existential question at best, a sisyphean effort at worst. * An existential question at best, a sisyphean effort at worst.
* *
* See, while `forms` and `angular` might be considered one word each: what of `@angular/forms`? Is that 2? * See, while `forms` and `angular` might be considered one word each: what of `@angular/forms`? Is that 2?
* *
* Or, what of `@someone mentioned Angular's forms`? Is that 4? * Or, what of `@someone mentioned Angular's forms`? Is that 4?
* *
* This is a long-winded way of saying "We know our word counter is inaccurate, but so is yours." * This is a long-winded way of saying "We know our word counter is inaccurate, but so is yours."
* *
* Please do let us know if you have strong thoughts/answers on the topic, * Please do let us know if you have strong thoughts/answers on the topic,
* we're happy to hear them. * we're happy to hear them.
*/ */
import { Root, Parent, Text } from "hast"; import { Root, Parent, Text } from "hast";
import { Node } from "unist"; import { Node } from "unist";
import { Plugin } from "unified"; import { Plugin } from "unified";
import { visit } from "unist-util-visit"; import { visit } from "unist-util-visit";
import { unified } from "unified"; import { unified } from "unified";
import english from "retext-english"; import english from "retext-english";
import rehypeRetext from 'rehype-retext'; import rehypeRetext from "rehype-retext";
import { validateConfig } from "astro/dist/types/core/config"; import { validateConfig } from "astro/dist/types/core/config";
interface RemarkCountProps {} interface RemarkCountProps {}
function count(counts: Record<string, number>) { function count(counts: Record<string, number>) {
return () => counter; return () => counter;
function counter(tree: Root) { function counter(tree: Root) {
visit(tree, visitor); visit(tree, visitor);
function visitor(node: Node) { function visitor(node: Node) {
if (node.type === 'SourceNode') { if (node.type === "SourceNode") {
const inlineCount = (node as never as {value: string}).value.split(/\b/g).length; const inlineCount = (node as never as { value: string }).value.split(
counts["InlineCodeWords"] = (counts["InlineCodeWords"] || 0) + inlineCount; /\b/g
} ).length;
counts[node.type] = (counts[node.type] || 0) + 1; counts["InlineCodeWords"] =
} (counts["InlineCodeWords"] || 0) + inlineCount;
} }
} counts[node.type] = (counts[node.type] || 0) + 1;
}
export const rehypeWordCount: Plugin<[RemarkCountProps | never], Root> = () => { }
return async (tree, file) => { }
const counts = {} as {
InlineCodeWords: number; export const rehypeWordCount: Plugin<[RemarkCountProps | never], Root> = () => {
RootNode: number; return async (tree, file) => {
ParagraphNode: number; const counts = {} as {
SentenceNode: number; InlineCodeWords: number;
WordNode: number; RootNode: number;
TextNode: number; ParagraphNode: number;
WhiteSpaceNode: number; SentenceNode: number;
PunctuationNode: number; WordNode: number;
SymbolNode: number; TextNode: number;
SourceNode: number; WhiteSpaceNode: number;
}; PunctuationNode: number;
SymbolNode: number;
await unified() SourceNode: number;
.use(rehypeRetext, unified().use(english).use(count(counts))) };
.run(tree);
await unified()
(file.data.astro as any).frontmatter.wordCount = (counts.InlineCodeWords || 0) + (counts.TextNode || 0); .use(rehypeRetext, unified().use(english).use(count(counts)))
}; .run(tree);
};
(file.data.astro as any).frontmatter.wordCount =
(counts.InlineCodeWords || 0) + (counts.TextNode || 0);
};
};

View File

@@ -1 +1 @@
export * from './tabs'; export * from "./tabs";

View File

@@ -5,29 +5,29 @@ import { Plugin } from "unified";
import { getHeaderNodeId, slugs } from "rehype-slug-custom-id"; import { getHeaderNodeId, slugs } from "rehype-slug-custom-id";
interface ElementNode extends Parent { interface ElementNode extends Parent {
tagName: string; tagName: string;
properties: any; properties: any;
} }
const isNodeHeading = (n: ElementNode) => const isNodeHeading = (n: ElementNode) =>
n.type === "element" && /h[1-6]/.exec(n.tagName); n.type === "element" && /h[1-6]/.exec(n.tagName);
const findLargestHeading = (nodes: ElementNode[]) => { const findLargestHeading = (nodes: ElementNode[]) => {
let largestSize = Infinity; let largestSize = Infinity;
for (let node of nodes) { for (const node of nodes) {
if (!isNodeHeading(node)) continue; if (!isNodeHeading(node)) continue;
const size = parseInt(node.tagName.substr(1), 10); const size = parseInt(node.tagName.substr(1), 10);
largestSize = Math.min(largestSize, size); largestSize = Math.min(largestSize, size);
} }
return largestSize; return largestSize;
}; };
const isNodeLargestHeading = (n: ElementNode, largestSize: number) => const isNodeLargestHeading = (n: ElementNode, largestSize: number) =>
isNodeHeading(n) && parseInt(n.tagName.substr(1), 10) === largestSize; isNodeHeading(n) && parseInt(n.tagName.substr(1), 10) === largestSize;
export interface RehypeTabsProps { export interface RehypeTabsProps {
injectSubheaderProps?: boolean; injectSubheaderProps?: boolean;
tabSlugifyProps?: Parameters<typeof getHeaderNodeId>[1]; tabSlugifyProps?: Parameters<typeof getHeaderNodeId>[1];
} }
/** /**
@@ -48,127 +48,127 @@ export interface RehypeTabsProps {
* @see https://github.com/reactjs/react-tabs * @see https://github.com/reactjs/react-tabs
*/ */
export const rehypeTabs: Plugin<[RehypeTabsProps | never], Root> = ({ export const rehypeTabs: Plugin<[RehypeTabsProps | never], Root> = ({
injectSubheaderProps = false, injectSubheaderProps = false,
tabSlugifyProps = {}, tabSlugifyProps = {},
}) => { }) => {
return (tree) => { return (tree) => {
const replaceTabNodes = (nodes: Node[]) => { const replaceTabNodes = (nodes: Node[]) => {
let sectionStarted = false; let sectionStarted = false;
const largestSize = findLargestHeading(nodes as ElementNode[]); const largestSize = findLargestHeading(nodes as ElementNode[]);
const tabsContainer = { const tabsContainer = {
type: "element", type: "element",
tagName: "div", tagName: "div",
properties: { properties: {
class: "tabs" class: "tabs",
}, },
children: [ children: [
{ {
type: "element", type: "element",
tagName: "ul", tagName: "ul",
properties: { properties: {
role: 'tablist', role: "tablist",
class: "tabs__tab-list" class: "tabs__tab-list",
}, },
children: [] as ElementNode[], children: [] as ElementNode[],
}, },
], ],
}; };
for (const localNode of nodes as ElementNode[]) { for (const localNode of nodes as ElementNode[]) {
if (!sectionStarted && !isNodeLargestHeading(localNode, largestSize)) { if (!sectionStarted && !isNodeLargestHeading(localNode, largestSize)) {
continue; continue;
} }
sectionStarted = true; sectionStarted = true;
if (isNodeLargestHeading(localNode, largestSize)) { if (isNodeLargestHeading(localNode, largestSize)) {
// Make sure that all tabs labeled "thing" aren't also labeled "thing2" // Make sure that all tabs labeled "thing" aren't also labeled "thing2"
slugs.reset(); slugs.reset();
const { id: headerSlug } = getHeaderNodeId( const { id: headerSlug } = getHeaderNodeId(
localNode, localNode,
tabSlugifyProps tabSlugifyProps
); );
// - 1 because the tabs are part of the header // - 1 because the tabs are part of the header
const idx = tabsContainer.children.length - 1; const idx = tabsContainer.children.length - 1;
const header = { const header = {
type: "element", type: "element",
tagName: "li", tagName: "li",
children: localNode.children, children: localNode.children,
properties: { properties: {
role: 'tab', role: "tab",
class: "tabs__tab", class: "tabs__tab",
"data-tabname": headerSlug, "data-tabname": headerSlug,
"aria-selected": idx === 0 ? "true" : 'false', "aria-selected": idx === 0 ? "true" : "false",
"aria-controls": `panel-${idx}`, "aria-controls": `panel-${idx}`,
"id": `tab-${idx}`, id: `tab-${idx}`,
tabIndex: idx === 0 ? "0" : "-1" tabIndex: idx === 0 ? "0" : "-1",
}, },
}; };
const contents = { const contents = {
type: "element", type: "element",
tagName: "div", tagName: "div",
children: [], children: [],
properties: { properties: {
id: `panel-${idx}`, id: `panel-${idx}`,
role: "tabpanel", role: "tabpanel",
class: "tabs__tab-panel", class: "tabs__tab-panel",
tabindex: 0, tabindex: 0,
"aria-labelledby": `tab-${idx}`, "aria-labelledby": `tab-${idx}`,
...(idx === 0 ? {} : {hidden: "true"}) ...(idx === 0 ? {} : { hidden: "true" }),
}, },
}; };
tabsContainer.children[0].children.push(header); tabsContainer.children[0].children.push(header);
tabsContainer.children.push(contents); tabsContainer.children.push(contents);
continue; continue;
} }
if (isNodeHeading(localNode) && injectSubheaderProps) { if (isNodeHeading(localNode) && injectSubheaderProps) {
// This is `tagName: tab` // This is `tagName: tab`
const lastTab = const lastTab =
tabsContainer.children[0].children[ tabsContainer.children[0].children[
tabsContainer.children[0].children.length - 1 tabsContainer.children[0].children.length - 1
]; ];
// Store the related tab ID in the attributes of the header // Store the related tab ID in the attributes of the header
localNode.properties["data-tabname"] = localNode.properties["data-tabname"] =
// Get the last tab's `data-tabname` property // Get the last tab's `data-tabname` property
lastTab.properties["data-tabname"]; lastTab.properties["data-tabname"];
// Add header ID to array // Add header ID to array
lastTab.properties["data-headers"] = JSON.stringify( lastTab.properties["data-headers"] = JSON.stringify(
JSON.parse(lastTab.properties["data-headers"] ?? "[]").concat( JSON.parse(lastTab.properties["data-headers"] ?? "[]").concat(
localNode.properties.id localNode.properties.id
) )
); );
} }
// Push into last `tab-panel` // Push into last `tab-panel`
tabsContainer.children[tabsContainer.children.length - 1].children.push( tabsContainer.children[tabsContainer.children.length - 1].children.push(
localNode localNode
); );
} }
return [tabsContainer]; return [tabsContainer];
}; };
replaceAllBetween( replaceAllBetween(
tree, tree,
{ type: "raw", value: "<!-- tabs:start -->" } as never, { type: "raw", value: "<!-- tabs:start -->" } as never,
{ type: "raw", value: "<!-- tabs:end -->" } as never, { type: "raw", value: "<!-- tabs:end -->" } as never,
replaceTabNodes replaceTabNodes
); );
replaceAllBetween( replaceAllBetween(
tree, tree,
{ type: "comment", value: " tabs:start " } as never, { type: "comment", value: " tabs:start " } as never,
{ type: "comment", value: " tabs:end " } as never, { type: "comment", value: " tabs:end " } as never,
replaceTabNodes replaceTabNodes
); );
return tree; return tree;
}; };
}; };

View File

@@ -8,13 +8,13 @@ import { languages } from "constants/index";
* code handles the parsing and converting of translation formats * code handles the parsing and converting of translation formats
*/ */
export function fileToOpenGraphConverter<T extends Languages>( export function fileToOpenGraphConverter<T extends Languages>(
lang: T lang: T
): T extends `${infer Lang}-${infer Region}` ): T extends `${infer Lang}-${infer Region}`
? `${Lang}_${Uppercase<Region>}` ? `${Lang}_${Uppercase<Region>}`
: T { : T {
const splitLang = lang.split("-"); const splitLang = lang.split("-");
if (splitLang.length === 1) return lang as never; if (splitLang.length === 1) return lang as never;
return `${splitLang[0]}_${splitLang[1].toUpperCase()}` as never; return `${splitLang[0]}_${splitLang[1].toUpperCase()}` as never;
} }
/** /**
@@ -30,12 +30,12 @@ export function fileToOpenGraphConverter<T extends Languages>(
* @example "es-es/posts/test" -> "posts/test" * @example "es-es/posts/test" -> "posts/test"
*/ */
export function removePrefixLanguageFromPath(path: string) { export function removePrefixLanguageFromPath(path: string) {
const langs = Object.keys(languages) as Languages[]; const langs = Object.keys(languages) as Languages[];
const matchedLang = langs.find( const matchedLang = langs.find(
(lang) => path.startsWith(lang) || path.startsWith("/" + lang) (lang) => path.startsWith(lang) || path.startsWith("/" + lang)
); );
if (!matchedLang) return path; if (!matchedLang) return path;
if (path.startsWith("/")) return "/" + path.slice(matchedLang.length + 2); if (path.startsWith("/")) return "/" + path.slice(matchedLang.length + 2);
// +1 since "es/path" needs to axe the trailing "/" // +1 since "es/path" needs to axe the trailing "/"
return path.slice(matchedLang.length + 1); return path.slice(matchedLang.length + 1);
} }

View File

@@ -9,33 +9,35 @@ import slash from "slash";
export const absolutePathRegex = /^(?:[a-z]+:)?\/\//; export const absolutePathRegex = /^(?:[a-z]+:)?\/\//;
export const isRelativePath = (str: string) => { export const isRelativePath = (str: string) => {
const isAbsolute = absolutePathRegex.exec(str); const isAbsolute = absolutePathRegex.exec(str);
if (isAbsolute) return false; if (isAbsolute) return false;
return true; return true;
}; };
var pathJoin = function(...pathArr){ const pathJoin = function (...pathArr) {
return pathArr.map(function(path){ return pathArr
if(path[0] === "/"){ .map(function (path) {
path = path.slice(1); if (path[0] === "/") {
} path = path.slice(1);
if (path.startsWith('./')) { }
path = path.slice(2); if (path.startsWith("./")) {
} path = path.slice(2);
if(path[path.length - 1] === "/"){ }
path = path.slice(0, path.length - 1); if (path[path.length - 1] === "/") {
} path = path.slice(0, path.length - 1);
return path; }
}).join("/"); return path;
} })
.join("/");
};
export const getFullRelativePath = (...paths: string[]) => { export const getFullRelativePath = (...paths: string[]) => {
return isRelativePath(paths[paths.length - 1]) return isRelativePath(paths[paths.length - 1])
? slash(pathJoin(...paths)) ? slash(pathJoin(...paths))
: paths[paths.length - 1]; : paths[paths.length - 1];
}; };
export const trimTrailingSlash = (path: string) => { export const trimTrailingSlash = (path: string) => {
if (path.endsWith("/")) return path.slice(0, path.length - 1); if (path.endsWith("/")) return path.slice(0, path.length - 1);
return path; return path;
}; };

View File

@@ -1,30 +1,30 @@
{ {
"compilerOptions": { "compilerOptions": {
// Enable top-level await, and other modern ESM features. // Enable top-level await, and other modern ESM features.
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
// Enable node-style module resolution, for things like npm package imports. // Enable node-style module resolution, for things like npm package imports.
"moduleResolution": "node", "moduleResolution": "node",
// Enable JSON imports. // Enable JSON imports.
"resolveJsonModule": true, "resolveJsonModule": true,
// Enable stricter transpilation for better output. // Enable stricter transpilation for better output.
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,
"jsx": "preserve", "jsx": "preserve",
"jsxImportSource": "preact", "jsxImportSource": "preact",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
// Add type definitions for our Vite runtime. // Add type definitions for our Vite runtime.
"types": ["@astrojs/image/client"], "types": ["@astrojs/image/client"],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"constants/*": ["./src/constants/*"], "constants/*": ["./src/constants/*"],
"uu-constants": ["./src/constants"], "uu-constants": ["./src/constants"],
"types/*": ["./src/types/*"], "types/*": ["./src/types/*"],
"uu-types": ["./src/types"], "uu-types": ["./src/types"],
"components/*": ["./src/components/*"], "components/*": ["./src/components/*"],
"utils/*": ["./src/utils/*"], "utils/*": ["./src/utils/*"],
"uu-utils": ["./src/utils"], "uu-utils": ["./src/utils"],
"assets/*": ["./src/assets/*"], "assets/*": ["./src/assets/*"]
} }
} }
} }

View File

@@ -1,20 +1,20 @@
{ {
"framework": "astro", "framework": "astro",
"redirects": [ "redirects": [
{ {
"source": "/authors/crutchcorn/", "source": "/authors/crutchcorn/",
"destination": "/unicorns/crutchcorn", "destination": "/unicorns/crutchcorn",
"permanent": true "permanent": true
}, },
{ {
"source": "/unicorns/", "source": "/unicorns/",
"destination": "/about", "destination": "/about",
"permanent": false "permanent": false
}, },
{ {
"source": "/posts/", "source": "/posts/",
"destination": "/", "destination": "/",
"permanent": true "permanent": true
} }
] ]
} }