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

View File

@@ -1,11 +1,11 @@
{
"useTabs": true,
"overrides": [
{
"files": ["**/*.astro"],
"options": {
"parser": "astro"
}
}
]
}
{
"useTabs": true,
"overrides": [
{
"files": ["**/*.astro"],
"options": {
"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 { rehypeUnicornPopulatePost } from "./src/utils/markdown/rehype-unicorn-populate-post";
import { rehypeWordCount } from "./src/utils/markdown/rehype-word-count";
import {rehypeUnicornGetSuggestedPosts} from "./src/utils/markdown/rehype-unicorn-get-suggested-posts";
import copy from 'rollup-plugin-copy'
import { rehypeUnicornGetSuggestedPosts } from "./src/utils/markdown/rehype-unicorn-get-suggested-posts";
import copy from "rollup-plugin-copy";
// TODO: Create types
import behead from "remark-behead";
@@ -26,90 +26,93 @@ import rehypeRaw from "rehype-raw";
import image from "@astrojs/image";
export default defineConfig({
integrations: [image()],
vite: {
ssr: {
external: ["svgo"],
},
plugins: [
{
...copy({
hook: 'options',
flatten: false,
targets: [
{
src: 'content/**/*',
dest: 'public/content'
}
]
}),
enforce: 'pre'
}
]
},
markdown: {
mode: "md",
syntaxHighlight: false,
extendDefaultPlugins: false,
remarkPlugins: [
remarkGfm,
// Remove complaining about "div cannot be in p element"
remarkUnwrapImages,
/* start remark plugins here */
[behead, { depth: 1 }],
[
remarkEmbedder as any,
{
transformers: [oembedTransformer, [TwitchTransformer, { parent }]],
} as RemarkEmbedderOptions,
],
[
remarkTwoslash,
{
themes: ["css-variables"],
} as UserConfigSettings,
],
],
rehypePlugins: [
rehypeUnicornPopulatePost,
rehypeUnicornGetSuggestedPosts,
// This is required to handle unsafe HTML embedded into Markdown
rehypeRaw,
// 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
[
rehypeSlug,
{
maintainCase: true,
removeAccents: true,
enableCustomId: true,
},
],
[
rehypeTabs,
{
injectSubheaderProps: true,
tabSlugifyProps: {
enableCustomId: true,
},
},
],
rehypeHeaderText,
/**
* Insert custom HTML generation code here
*/
[
rehypeAstroImageMd,
{
maxHeight: 768,
maxWidth: 768,
}
],
rehypeUnicornElementMap,
[rehypeExcerpt, {
maxLength: 150
}],
rehypeWordCount
],
} as AstroUserConfig["markdown"] as never,
integrations: [image()],
vite: {
ssr: {
external: ["svgo"],
},
plugins: [
{
...copy({
hook: "options",
flatten: false,
targets: [
{
src: "content/**/*",
dest: "public/content",
},
],
}),
enforce: "pre",
},
],
},
markdown: {
mode: "md",
syntaxHighlight: false,
extendDefaultPlugins: false,
remarkPlugins: [
remarkGfm,
// Remove complaining about "div cannot be in p element"
remarkUnwrapImages,
/* start remark plugins here */
[behead, { depth: 1 }],
[
remarkEmbedder as any,
{
transformers: [oembedTransformer, [TwitchTransformer, { parent }]],
} as RemarkEmbedderOptions,
],
[
remarkTwoslash,
{
themes: ["css-variables"],
} as UserConfigSettings,
],
],
rehypePlugins: [
rehypeUnicornPopulatePost,
rehypeUnicornGetSuggestedPosts,
// This is required to handle unsafe HTML embedded into Markdown
rehypeRaw,
// 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
[
rehypeSlug,
{
maintainCase: true,
removeAccents: true,
enableCustomId: true,
},
],
[
rehypeTabs,
{
injectSubheaderProps: true,
tabSlugifyProps: {
enableCustomId: true,
},
},
],
rehypeHeaderText,
/**
* Insert custom HTML generation code here
*/
[
rehypeAstroImageMd,
{
maxHeight: 768,
maxWidth: 768,
},
],
rehypeUnicornElementMap,
[
rehypeExcerpt,
{
maxLength: 150,
},
],
rehypeWordCount,
],
} as AstroUserConfig["markdown"] as never,
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,499 +1,427 @@
[
{
"id": "crutchcorn",
"name": "Corbin Crutchley",
"firstName": "Corbin",
"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! 💅",
"socials": {
"twitter": "crutchcorn",
"github": "crutchcorn",
"twitch": "crutchcorn"
},
"pronouns": "they/themselves",
"profileImg": "./crutchcorn.png",
"color": "#ba68c8",
"roles": [
"devops",
"developer",
"author",
"community"
]
},
{
"id": "fennifith",
"name": "James Fenn",
"firstName": "James",
"lastName": "Fenn",
"description": "Enjoys writing software on loud keyboards. Starts too many projects. Consumes food.",
"socials": {
"twitter": "fennifith",
"github": "fennifith"
},
"pronouns": "he",
"profileImg": "./fennifith.jpg",
"color": "#0091EA",
"roles": [
"developer",
"author",
"community"
]
},
{
"id": "evelynhathaway",
"name": "Evelyn Hathaway",
"firstName": "Evelyn",
"lastName": "Hathaway",
"description": "👩‍💻🌈 I'm a student and software developer with a strong passion for frontend and backend JavaScript and web accessibility.",
"socials": {
"twitter": "eeveedev",
"github": "evelynhathaway"
},
"pronouns": "she",
"profileImg": "proud.png",
"color": "#ef5f17",
"roles": [
"developer",
"devops",
"community"
]
},
{
"id": "adueppen",
"name": "Alex Dueppen",
"firstName": "Alex",
"lastName": "Dueppen",
"description": "I do stuff sometimes.",
"socials": {
"twitter": "AlexDueppen",
"github": "adueppen",
"website": "https://ajd.sh/"
},
"pronouns": "he",
"profileImg": "./adueppen.png",
"color": "#69ffff",
"roles": [
"developer",
"community"
]
},
{
"id": "zavukodlak",
"name": "Vukašin Anđelković",
"firstName": "Vukašin",
"lastName": "Anđelković",
"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!",
"socials": {
"twitter": "vukkashin",
"website": "https://vukash.in/",
"dribbble": "vukashin"
},
"pronouns": "he",
"profileImg": "./vukashin.png",
"color": "#3485FF",
"roles": [
"designer"
]
},
{
"id": "tommyemo",
"name": "Tom Wellington",
"firstName": "Tom",
"lastName": "Wellington",
"description": "I design icons and user interfaces, among other things. he/him ✌️",
"socials": {
"twitter": "tommy_emo_",
"website": "https://www.tommyemo.net/"
},
"pronouns": "he",
"profileImg": "./tommyemo.jpg",
"color": "#8539EB",
"roles": [
"designer"
]
},
{
"id": "edpratti",
"name": "Eduardo Pratti",
"firstName": "Eduardo",
"lastName": "Pratti",
"description": "UI designer and developer wannabe. Cares about negative space, layout grids and Bloodborne challenge runs.",
"socials": {
"twitter": "edpratti",
"website": "http://pratti.design"
},
"pronouns": "he",
"profileImg": "./edpratti.jpg",
"color": "#FF3300",
"roles": [
"designer",
"author"
]
},
{
"id": "sarsamurmu",
"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.",
"socials": {
"twitter": "sarsamurmu",
"github": "sarsamurmu",
"website": "https://sarsamurmu.github.io"
},
"pronouns": "he",
"profileImg": "./sarsamurmu.png",
"color": "#7C4DFF",
"roles": [
"developer"
]
},
{
"id": "MDutro",
"name": "Micah Dutro",
"firstName": "Micah",
"lastName": "Dutro",
"description": "A non-profit lawyer turned budding web developer.",
"socials": {
"github": "MDutro"
},
"pronouns": "he",
"profileImg": "./mdutro.jpg",
"color": "#7C4DFF",
"roles": [
"developer",
"author",
"community"
]
},
{
"id": "reikaze",
"name": "Kevin Mai",
"firstName": "Kevin",
"lastName": "Mai",
"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.",
"socials": {
"twitter": "Reikaze0",
"github": "Reikaze"
},
"pronouns": "he",
"profileImg": "./reikaze.jpg",
"color": "#ba68c8",
"roles": [
"author"
]
},
{
"id": "thodges314",
"name": "Thomas Hodges",
"firstName": "Thomas",
"lastName": "Hodges",
"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": {
"github": "thodges314",
"linkedIn": "thomas-hodges"
},
"pronouns": "he",
"profileImg": "./thodges.png",
"color": "#ba68c8",
"roles": [
"author"
]
},
{
"id": "skatcat31",
"name": "Robert Mennell",
"firstName": "Robert",
"lastName": "Mennell",
"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": {
"github": "skatcat31",
"linkedIn": "rnmennell"
},
"color": "#ba68c8",
"profileImg": "./hello.png",
"pronouns": "he",
"roles": [
"author",
"community"
]
},
{
"id": "seanmiller",
"name": "Sean Miller",
"firstName": "Sean",
"lastName": "Miller",
"description": "Howdy! Computer Science major at Texas A&M University, with a minor in cybersecurity. Super passionate about all things software!",
"socials": {
"twitter": "beastosean",
"github": "tamuseanmiller",
"website": "https://sean.millerfamily.tech",
"linkedIn": "tamuseanmiller"
},
"pronouns": "he",
"profileImg": "./seanmiller.jpg",
"color": "#551a8b",
"roles": [
"author"
]
},
{
"id": "pierremtb",
"name": "Pierre Jacquier",
"firstName": "Pierre",
"lastName": "Jacquier",
"description": "Junior Hardware Engineer at Algolux. Computationally curious.",
"socials": {
"twitter": "PierreJacquier",
"github": "pierremtb",
"website": "https://pierrejacquier.com",
"linkedIn": "pierrejacquier"
},
"pronouns": "he",
"profileImg": "./pierremtb.jpg",
"color": "#FFEB3B",
"roles": [
"author"
]
},
{
"id": "maisydino",
"name": "Maisy Dinosaur",
"firstName": "Maisy",
"lastName": "Dinosaur",
"description": "I do a lot of stuff sometimes. Part-time fullstack, full time dog petter.",
"socials": {
"twitter": "rodentman87",
"github": "rodentman87",
"website": "https://likesdinosaurs.com"
},
"pronouns": "she",
"profileImg": "./maisydino.jpg",
"color": "#FDF6E3",
"roles": [
"author",
"community"
]
},
{
"id": "bobrossrtx",
"name": "Bobrossrtx",
"firstName": "Owen",
"lastName": "Boreham",
"description": "I have over 1000 years of software development experience, do not underestimate me!",
"socials": {
"twitter": "bobrossrtx",
"github": "bobrossrtx",
"website": "https://www.owenboreham.tech"
},
"pronouns": "he",
"profileImg": "./bobrossrtx.jpg",
"color": "#b7e11e",
"roles": [
"developer",
"author"
]
},
{
"id": "ljtech",
"name": "Landon Johnson",
"firstName": "Landon",
"lastName": "Johnson",
"description": "Hello there, my name is Lj. I am a full stack developer.",
"socials": {
"twitter": "ljtechdotca",
"github": "ljtechdotca",
"twitch": "ljtechdotca",
"website": "https://ljtech.ca"
},
"pronouns": "he",
"profileImg": "./ljtechdotca.png",
"color": "#7b61ff",
"roles": [
"author"
]
},
{
"id": "SkyHawk_0",
"name": "Joshua Hawkins",
"firstName": "Joshua",
"lastName": "Hawkins",
"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.",
"socials": {},
"pronouns": "he",
"profileImg": "./goofy.png",
"color": "#18BBC9",
"roles": [
"author"
]
},
{
"id": "splatkillwill",
"name": "William (Will) Lohan",
"firstName": "William",
"lastName": "Lohan",
"description": "",
"socials": {
"github": "william-lohan",
"twitch": "splat_killwill",
"website": "https://gatimus.com/",
"linkedIn": "william-lohan-b202637a"
},
"pronouns": "they/themselves",
"profileImg": "./splatkillwill.jpg",
"color": "#BF00FF",
"roles": [
"author"
]
},
{
"id": "fmothe",
"name": "Federico Mothe",
"firstName": "Federico",
"lastName": "Mothe",
"description": "Software Dev from Argentina learning the dark magic of frontend development and typescript.",
"socials": {
"twitter": "FedericoMothe",
"github": "fmothe",
"twitch": "mothevv"
},
"pronouns": "he",
"profileImg": "./fmothe.jpg",
"color": "#18BBC9",
"roles": [
"translator"
]
},
{
"id": "jahirfiquitiva",
"name": "Jahir Fiquitiva",
"firstName": "Jahir",
"lastName": "Fiquitiva",
"description": "Passionate and creative full-stack software engineer based in Colombia 🇨🇴.",
"socials": {
"twitter": "jahirfiquitiva",
"github": "jahirfiquitiva",
"website": "https://jahir.dev",
"linkedIn": "jahirfiquitiva"
},
"pronouns": "he",
"profileImg": "./jahirfiquitiva.jpg",
"color": "#3867d6",
"roles": [
"translator"
]
},
{
"id": "kaleem",
"name": "Kaleem",
"firstName": "Kaleem",
"lastName": "",
"description": "Software Engineer, Simplifying programming, writing about learnings and lessons learned.",
"socials": {
"twitter": "kaleemniz",
"github": "kaleem68",
"linkedIn": "nixamani5"
},
"pronouns": "he",
"profileImg": "./kaleem.jpeg",
"color": "#a8b3ba",
"roles": [
"author"
]
},
{
"id": "qarnax",
"name": "Qarnax",
"firstName": "",
"lastName": "",
"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 😊",
"socials": {
"twitch": "qarnax_",
"twitter": "qarnax",
"github": "qarnax801"
},
"profileImg": "./qarnax.jpg",
"color": "",
"roles": [
"developer",
"author",
"community",
"translator"
]
},
{
"id": "alexchadwick",
"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"
]
}
{
"id": "crutchcorn",
"name": "Corbin Crutchley",
"firstName": "Corbin",
"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! 💅",
"socials": {
"twitter": "crutchcorn",
"github": "crutchcorn",
"twitch": "crutchcorn"
},
"pronouns": "they/themselves",
"profileImg": "./crutchcorn.png",
"color": "#ba68c8",
"roles": ["devops", "developer", "author", "community"]
},
{
"id": "fennifith",
"name": "James Fenn",
"firstName": "James",
"lastName": "Fenn",
"description": "Enjoys writing software on loud keyboards. Starts too many projects. Consumes food.",
"socials": {
"twitter": "fennifith",
"github": "fennifith"
},
"pronouns": "he",
"profileImg": "./fennifith.jpg",
"color": "#0091EA",
"roles": ["developer", "author", "community"]
},
{
"id": "evelynhathaway",
"name": "Evelyn Hathaway",
"firstName": "Evelyn",
"lastName": "Hathaway",
"description": "👩‍💻🌈 I'm a student and software developer with a strong passion for frontend and backend JavaScript and web accessibility.",
"socials": {
"twitter": "eeveedev",
"github": "evelynhathaway"
},
"pronouns": "she",
"profileImg": "proud.png",
"color": "#ef5f17",
"roles": ["developer", "devops", "community"]
},
{
"id": "adueppen",
"name": "Alex Dueppen",
"firstName": "Alex",
"lastName": "Dueppen",
"description": "I do stuff sometimes.",
"socials": {
"twitter": "AlexDueppen",
"github": "adueppen",
"website": "https://ajd.sh/"
},
"pronouns": "he",
"profileImg": "./adueppen.png",
"color": "#69ffff",
"roles": ["developer", "community"]
},
{
"id": "zavukodlak",
"name": "Vukašin Anđelković",
"firstName": "Vukašin",
"lastName": "Anđelković",
"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!",
"socials": {
"twitter": "vukkashin",
"website": "https://vukash.in/",
"dribbble": "vukashin"
},
"pronouns": "he",
"profileImg": "./vukashin.png",
"color": "#3485FF",
"roles": ["designer"]
},
{
"id": "tommyemo",
"name": "Tom Wellington",
"firstName": "Tom",
"lastName": "Wellington",
"description": "I design icons and user interfaces, among other things. he/him ✌️",
"socials": {
"twitter": "tommy_emo_",
"website": "https://www.tommyemo.net/"
},
"pronouns": "he",
"profileImg": "./tommyemo.jpg",
"color": "#8539EB",
"roles": ["designer"]
},
{
"id": "edpratti",
"name": "Eduardo Pratti",
"firstName": "Eduardo",
"lastName": "Pratti",
"description": "UI designer and developer wannabe. Cares about negative space, layout grids and Bloodborne challenge runs.",
"socials": {
"twitter": "edpratti",
"website": "http://pratti.design"
},
"pronouns": "he",
"profileImg": "./edpratti.jpg",
"color": "#FF3300",
"roles": ["designer", "author"]
},
{
"id": "sarsamurmu",
"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.",
"socials": {
"twitter": "sarsamurmu",
"github": "sarsamurmu",
"website": "https://sarsamurmu.github.io"
},
"pronouns": "he",
"profileImg": "./sarsamurmu.png",
"color": "#7C4DFF",
"roles": ["developer"]
},
{
"id": "MDutro",
"name": "Micah Dutro",
"firstName": "Micah",
"lastName": "Dutro",
"description": "A non-profit lawyer turned budding web developer.",
"socials": {
"github": "MDutro"
},
"pronouns": "he",
"profileImg": "./mdutro.jpg",
"color": "#7C4DFF",
"roles": ["developer", "author", "community"]
},
{
"id": "reikaze",
"name": "Kevin Mai",
"firstName": "Kevin",
"lastName": "Mai",
"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.",
"socials": {
"twitter": "Reikaze0",
"github": "Reikaze"
},
"pronouns": "he",
"profileImg": "./reikaze.jpg",
"color": "#ba68c8",
"roles": ["author"]
},
{
"id": "thodges314",
"name": "Thomas Hodges",
"firstName": "Thomas",
"lastName": "Hodges",
"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": {
"github": "thodges314",
"linkedIn": "thomas-hodges"
},
"pronouns": "he",
"profileImg": "./thodges.png",
"color": "#ba68c8",
"roles": ["author"]
},
{
"id": "skatcat31",
"name": "Robert Mennell",
"firstName": "Robert",
"lastName": "Mennell",
"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": {
"github": "skatcat31",
"linkedIn": "rnmennell"
},
"color": "#ba68c8",
"profileImg": "./hello.png",
"pronouns": "he",
"roles": ["author", "community"]
},
{
"id": "seanmiller",
"name": "Sean Miller",
"firstName": "Sean",
"lastName": "Miller",
"description": "Howdy! Computer Science major at Texas A&M University, with a minor in cybersecurity. Super passionate about all things software!",
"socials": {
"twitter": "beastosean",
"github": "tamuseanmiller",
"website": "https://sean.millerfamily.tech",
"linkedIn": "tamuseanmiller"
},
"pronouns": "he",
"profileImg": "./seanmiller.jpg",
"color": "#551a8b",
"roles": ["author"]
},
{
"id": "pierremtb",
"name": "Pierre Jacquier",
"firstName": "Pierre",
"lastName": "Jacquier",
"description": "Junior Hardware Engineer at Algolux. Computationally curious.",
"socials": {
"twitter": "PierreJacquier",
"github": "pierremtb",
"website": "https://pierrejacquier.com",
"linkedIn": "pierrejacquier"
},
"pronouns": "he",
"profileImg": "./pierremtb.jpg",
"color": "#FFEB3B",
"roles": ["author"]
},
{
"id": "maisydino",
"name": "Maisy Dinosaur",
"firstName": "Maisy",
"lastName": "Dinosaur",
"description": "I do a lot of stuff sometimes. Part-time fullstack, full time dog petter.",
"socials": {
"twitter": "rodentman87",
"github": "rodentman87",
"website": "https://likesdinosaurs.com"
},
"pronouns": "she",
"profileImg": "./maisydino.jpg",
"color": "#FDF6E3",
"roles": ["author", "community"]
},
{
"id": "bobrossrtx",
"name": "Bobrossrtx",
"firstName": "Owen",
"lastName": "Boreham",
"description": "I have over 1000 years of software development experience, do not underestimate me!",
"socials": {
"twitter": "bobrossrtx",
"github": "bobrossrtx",
"website": "https://www.owenboreham.tech"
},
"pronouns": "he",
"profileImg": "./bobrossrtx.jpg",
"color": "#b7e11e",
"roles": ["developer", "author"]
},
{
"id": "ljtech",
"name": "Landon Johnson",
"firstName": "Landon",
"lastName": "Johnson",
"description": "Hello there, my name is Lj. I am a full stack developer.",
"socials": {
"twitter": "ljtechdotca",
"github": "ljtechdotca",
"twitch": "ljtechdotca",
"website": "https://ljtech.ca"
},
"pronouns": "he",
"profileImg": "./ljtechdotca.png",
"color": "#7b61ff",
"roles": ["author"]
},
{
"id": "SkyHawk_0",
"name": "Joshua Hawkins",
"firstName": "Joshua",
"lastName": "Hawkins",
"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.",
"socials": {},
"pronouns": "he",
"profileImg": "./goofy.png",
"color": "#18BBC9",
"roles": ["author"]
},
{
"id": "splatkillwill",
"name": "William (Will) Lohan",
"firstName": "William",
"lastName": "Lohan",
"description": "",
"socials": {
"github": "william-lohan",
"twitch": "splat_killwill",
"website": "https://gatimus.com/",
"linkedIn": "william-lohan-b202637a"
},
"pronouns": "they/themselves",
"profileImg": "./splatkillwill.jpg",
"color": "#BF00FF",
"roles": ["author"]
},
{
"id": "fmothe",
"name": "Federico Mothe",
"firstName": "Federico",
"lastName": "Mothe",
"description": "Software Dev from Argentina learning the dark magic of frontend development and typescript.",
"socials": {
"twitter": "FedericoMothe",
"github": "fmothe",
"twitch": "mothevv"
},
"pronouns": "he",
"profileImg": "./fmothe.jpg",
"color": "#18BBC9",
"roles": ["translator"]
},
{
"id": "jahirfiquitiva",
"name": "Jahir Fiquitiva",
"firstName": "Jahir",
"lastName": "Fiquitiva",
"description": "Passionate and creative full-stack software engineer based in Colombia 🇨🇴.",
"socials": {
"twitter": "jahirfiquitiva",
"github": "jahirfiquitiva",
"website": "https://jahir.dev",
"linkedIn": "jahirfiquitiva"
},
"pronouns": "he",
"profileImg": "./jahirfiquitiva.jpg",
"color": "#3867d6",
"roles": ["translator"]
},
{
"id": "kaleem",
"name": "Kaleem",
"firstName": "Kaleem",
"lastName": "",
"description": "Software Engineer, Simplifying programming, writing about learnings and lessons learned.",
"socials": {
"twitter": "kaleemniz",
"github": "kaleem68",
"linkedIn": "nixamani5"
},
"pronouns": "he",
"profileImg": "./kaleem.jpeg",
"color": "#a8b3ba",
"roles": ["author"]
},
{
"id": "qarnax",
"name": "Qarnax",
"firstName": "",
"lastName": "",
"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 😊",
"socials": {
"twitch": "qarnax_",
"twitter": "qarnax",
"github": "qarnax801"
},
"profileImg": "./qarnax.jpg",
"color": "",
"roles": ["developer", "author", "community", "translator"]
},
{
"id": "alexchadwick",
"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",
"private": true,
"description": "Learning programming from magically majestic words",
"version": "0.3.0-alpha.1",
"bugs": {
"url": "https://github.com/unicorn-utterances/unicorn-utterances/issues"
},
"homepage": "https://unicorn-utterances.com",
"keywords": [
"blog",
"education",
"programming"
],
"license": "MPL-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/unicorn-utterances/unicorn-utterances.git"
},
"scripts": {
"debug": "node --inspect-brk ./node_modules/astro/astro.js dev --experimental-integrations",
"dev": "astro dev --experimental-integrations",
"start": "astro dev --experimental-integrations",
"build": "astro build --experimental-integrations",
"preview": "astro preview --experimental-integrations",
"host:local": "cd dist && ws --http2 --compress",
"format": "prettier -w . --cache --plugin-search-dir=.",
"lint": "eslint . --ext .js,.ts,.astro"
},
"devDependencies": {
"@astrojs/image": "^0.7.1",
"@remark-embedder/core": "^3.0.1",
"@remark-embedder/transformer-oembed": "^3.0.0",
"@types/classnames": "^2.3.1",
"@types/node": "^18.7.18",
"@typescript-eslint/parser": "^4.33.0",
"astro": "^1.2.8",
"astro-icon": "^0.7.3",
"classnames": "^2.3.2",
"dayjs": "^1.11.5",
"eslint": "^7.32.0",
"eslint-plugin-astro": "^0.19.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"gatsby-remark-embedder": "^6.0.1",
"gray-matter": "^4.0.3",
"hast": "^1.0.0",
"hast-util-from-html": "^1.0.0",
"hast-util-has-property": "^2.0.0",
"hast-util-heading-rank": "^2.1.0",
"hast-util-to-string": "^2.0.0",
"image-size": "^1.0.2",
"junk": "^4.0.0",
"prettier": "^2.7.1",
"prettier-plugin-astro": "^0.5.4",
"rehype-raw": "^6.1.1",
"rehype-retext": "^3.0.2",
"rehype-slug-custom-id": "^1.1.0",
"remark-behead": "^3.1.0",
"remark-gfm": "^3.0.1",
"remark-shiki-twoslash": "^3.1.0",
"remark-unwrap-images": "^3.0.1",
"retext-english": "^4.1.0",
"rollup-plugin-copy": "^3.4.0",
"sass": "^1.54.9",
"slash": "^4.0.0",
"terser": "^5.15.0",
"unified": "^10.1.2",
"unist-util-replace-all-between": "^0.1.1",
"unist-util-visit": "^4.1.1"
}
"name": "unicorn-utterances-site",
"private": true,
"description": "Learning programming from magically majestic words",
"version": "0.3.0-alpha.1",
"bugs": {
"url": "https://github.com/unicorn-utterances/unicorn-utterances/issues"
},
"homepage": "https://unicorn-utterances.com",
"keywords": [
"blog",
"education",
"programming"
],
"license": "MPL-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/unicorn-utterances/unicorn-utterances.git"
},
"scripts": {
"debug": "node --inspect-brk ./node_modules/astro/astro.js dev --experimental-integrations",
"dev": "astro dev --experimental-integrations",
"start": "astro dev --experimental-integrations",
"build": "astro build --experimental-integrations",
"preview": "astro preview --experimental-integrations",
"host:local": "cd dist && ws --http2 --compress",
"format": "prettier -w . --cache --plugin-search-dir=.",
"lint": "eslint . --ext .js,.ts,.astro"
},
"devDependencies": {
"@astrojs/image": "^0.7.1",
"@remark-embedder/core": "^3.0.1",
"@remark-embedder/transformer-oembed": "^3.0.0",
"@types/classnames": "^2.3.1",
"@types/node": "^18.7.18",
"@typescript-eslint/parser": "^4.33.0",
"astro": "^1.2.8",
"astro-icon": "^0.7.3",
"classnames": "^2.3.2",
"dayjs": "^1.11.5",
"eslint": "^7.32.0",
"eslint-plugin-astro": "^0.19.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"gatsby-remark-embedder": "^6.0.1",
"gray-matter": "^4.0.3",
"hast": "^1.0.0",
"hast-util-from-html": "^1.0.0",
"hast-util-has-property": "^2.0.0",
"hast-util-heading-rank": "^2.1.0",
"hast-util-to-string": "^2.0.0",
"image-size": "^1.0.2",
"junk": "^4.0.0",
"prettier": "^2.7.1",
"prettier-plugin-astro": "^0.5.4",
"rehype-raw": "^6.1.1",
"rehype-retext": "^3.0.2",
"rehype-slug-custom-id": "^1.1.0",
"remark-behead": "^3.1.0",
"remark-gfm": "^3.0.1",
"remark-shiki-twoslash": "^3.1.0",
"remark-unwrap-images": "^3.0.1",
"retext-english": "^4.1.0",
"rollup-plugin-copy": "^3.4.0",
"sass": "^1.54.9",
"slash": "^4.0.0",
"terser": "^5.15.0",
"unified": "^10.1.2",
"unist-util-replace-all-between": "^0.1.1",
"unist-util-visit": "^4.1.1"
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/sw.js")
.then(serviceWorker => {
console.log("Service Worker registered: ", serviceWorker);
})
.catch(error => {
console.error("Error registering the Service Worker: ", error);
});
}
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/sw.js")
.then((serviceWorker) => {
console.log("Service Worker registered: ", serviceWorker);
})
.catch((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.leftContainer}>
<slot name="left"/>
</div>
<div class={layoutStyles.centerContainer}>
<slot/>
</div>
<div class={layoutStyles.rightContainer}>
<slot name="right"/>
</div>
<div class={layoutStyles.leftContainer}>
<slot name="left" />
</div>
<div class={layoutStyles.centerContainer}>
<slot />
</div>
<div class={layoutStyles.rightContainer}>
<slot name="right" />
</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";
---
<button
class={`${btnStyles.darkLightBtn} baseBtn`}
id="theme-toggle-button"
aria-pressed="false"
aria-label={"Is dark mode enabled?"}
class={`${btnStyles.darkLightBtn} baseBtn`}
id="theme-toggle-button"
aria-pressed="false"
aria-label={"Is dark mode enabled?"}
>
<Icon 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;"/>
<Icon
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>
<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 DarkLightButton from "components/dark-light-button/dark-light-button.astro";
import { Icon } from 'astro-icon';
// const { back } = useHistory();
const rootPath = `/`;
const isBase = Astro.url.pathname === rootPath;
const isBlogPost = Astro.url.pathname.startsWith(`${rootPath}posts`);
const isCollection = Astro.url.pathname.startsWith(`${rootPath}collections`);
---
<div class={layoutStyles.horizCenter}>
<header class={layoutStyles.header} aria-label={"Toolbar for primary action buttons"}>
<div class={layoutStyles.headerInsideContainer}>
{!isBase ? (<>
<button id="backbtn" class={`${layoutStyles.backBtn} baseBtn`} aria-label="Go back">
<Icon height="36" width="36" name="back" />
</button>
<script is:inline defer src="/scripts/backbtn.min.js"/>
</>
) : (
<div />
)}
<div class={layoutStyles.iconList}>
<!-- <AnalyticsLink category={"outbound"} href="https://discord.gg/FMcvc6T" className={"baseBtn"}
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>
---
import layoutStyles from "./layout.module.scss";
import DarkLightButton from "components/dark-light-button/dark-light-button.astro";
import { Icon } from "astro-icon";
// const { back } = useHistory();
const rootPath = `/`;
const isBase = Astro.url.pathname === rootPath;
const isBlogPost = Astro.url.pathname.startsWith(`${rootPath}posts`);
const isCollection = Astro.url.pathname.startsWith(`${rootPath}collections`);
---
<div class={layoutStyles.horizCenter}>
<header
class={layoutStyles.header}
aria-label={"Toolbar for primary action buttons"}
>
<div class={layoutStyles.headerInsideContainer}>
{
!isBase ? (
<>
<button
id="backbtn"
class={`${layoutStyles.backBtn} baseBtn`}
aria-label="Go back"
>
<Icon height="36" width="36" name="back" />
</button>
<script is:inline defer src="/scripts/backbtn.min.js" />
</>
) : (
<div />
)
}
<div class={layoutStyles.iconList}>
<!-- <AnalyticsLink category={"outbound"} href="https://discord.gg/FMcvc6T" className={"baseBtn"}
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 {
display: string;
pageNumber: number;
ariaLabel?: string;
display: string;
pageNumber: number;
ariaLabel?: string;
}
export const DR = {
ariaLabel: "Go to the next set of pages",
display: '...'
ariaLabel: "Go to the next set of pages",
display: "...",
};
export const DL = {
ariaLabel: "Go to previous set of pages",
display: '...'
ariaLabel: "Go to previous set of pages",
display: "...",
};
const range = (start: number, end: number): Page[] => {
let length = end - start + 1;
return Array.from({ length }, (_, idx) => {
const page = idx + start;
return {
display: String(page),
pageNumber: page,
ariaLabel: `Goto page ${page}`
}
});
const length = end - start + 1;
return Array.from({ length }, (_, idx) => {
const page = idx + start;
return {
display: String(page),
pageNumber: page,
ariaLabel: `Goto page ${page}`,
};
});
};
interface GetPaginationRangeProps {
totalCount: number;
pageSize: number;
siblingCount?: number;
currentPage: number;
totalCount: number;
pageSize: number;
siblingCount?: number;
currentPage: number;
}
export const getPaginationRange = ({
totalCount,
pageSize,
siblingCount = 1,
currentPage
totalCount,
pageSize,
siblingCount = 1,
currentPage,
}: GetPaginationRangeProps): Page[] => {
const totalPageCount = Math.ceil(totalCount / pageSize);
const totalPageCount = Math.ceil(totalCount / pageSize);
const totalPageCountPage: Page = {
display: `${totalPageCount}`,
pageNumber: totalPageCount
}
const totalPageCountPage: Page = {
display: `${totalPageCount}`,
pageNumber: totalPageCount,
};
const totalPageNumbers = siblingCount + 5;
const totalPageNumbers = siblingCount + 5;
if (totalPageNumbers >= totalPageCount) {
return range(1, totalPageCount);
}
if (totalPageNumbers >= totalPageCount) {
return range(1, totalPageCount);
}
const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
const rightSiblingIndex = Math.min(
currentPage + siblingCount,
totalPageCount
);
const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
const rightSiblingIndex = Math.min(
currentPage + siblingCount,
totalPageCount
);
const shouldShowLeftDots = leftSiblingIndex > 2;
const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2;
const shouldShowLeftDots = leftSiblingIndex > 2;
const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2;
const firstPageIndex: Page = {
display: "1",
pageNumber: 1
};
const lastPageIndex: Page = {
display: `${totalPageCount}`,
pageNumber: totalPageCount
};
const firstPageIndex: Page = {
display: "1",
pageNumber: 1,
};
const lastPageIndex: Page = {
display: `${totalPageCount}`,
pageNumber: totalPageCount,
};
if (!shouldShowLeftDots && shouldShowRightDots) {
let leftItemCount = 3 + 2 * siblingCount;
let leftRange = range(1, leftItemCount);
const lastPage = leftRange[leftRange.length - 1]
const DR_Page: Page = {
...DR,
pageNumber: currentPage + 2
}
if (!shouldShowLeftDots && shouldShowRightDots) {
const leftItemCount = 3 + 2 * siblingCount;
const leftRange = range(1, leftItemCount);
const lastPage = leftRange[leftRange.length - 1];
const DR_Page: Page = {
...DR,
pageNumber: currentPage + 2,
};
return [...leftRange, DR_Page, totalPageCountPage];
}
return [...leftRange, DR_Page, totalPageCountPage];
}
if (shouldShowLeftDots && !shouldShowRightDots) {
let rightItemCount = 3 + 2 * siblingCount;
let rightRange = range(
totalPageCount - rightItemCount + 1,
totalPageCount
);
const DL_Page: Page = {
...DL,
pageNumber: currentPage - 2
}
return [firstPageIndex, DL_Page, ...rightRange];
}
if (shouldShowLeftDots && !shouldShowRightDots) {
const rightItemCount = 3 + 2 * siblingCount;
const rightRange = range(
totalPageCount - rightItemCount + 1,
totalPageCount
);
const DL_Page: Page = {
...DL,
pageNumber: currentPage - 2,
};
return [firstPageIndex, DL_Page, ...rightRange];
}
if (shouldShowLeftDots && shouldShowRightDots) {
let middleRange = range(leftSiblingIndex, rightSiblingIndex);
const DL_Page: Page = {
...DL,
pageNumber: currentPage - 2
}
const DR_Page: Page = {
...DR,
pageNumber: currentPage + 2
}
return [firstPageIndex, DL_Page, ...middleRange, DR_Page, lastPageIndex];
}
if (shouldShowLeftDots && shouldShowRightDots) {
const middleRange = range(leftSiblingIndex, rightSiblingIndex);
const DL_Page: Page = {
...DL,
pageNumber: currentPage - 2,
};
const DR_Page: Page = {
...DR,
pageNumber: currentPage + 2,
};
return [firstPageIndex, DL_Page, ...middleRange, DR_Page, lastPageIndex];
}
return [];
};
return [];
};

View File

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

View File

@@ -5,12 +5,12 @@ import { PostInfo } from "types/PostInfo";
// import { PostListContext } from "constants/post-list-context";
export interface PostListProps {
showWordCount?: boolean;
numberOfArticles?: number;
wordCount?: number;
unicornData?: PostInfo['authorsMeta'];
listAriaLabel: string;
postsToDisplay: PostInfo[]
showWordCount?: boolean;
numberOfArticles?: number;
wordCount?: number;
unicornData?: PostInfo["authorsMeta"];
listAriaLabel: string;
postsToDisplay: PostInfo[];
}
/**
* 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;
---
<ul
class={listStyle.postsListContainer}
aria-label={listAriaLabel}
role="list"
>
{postsToDisplay.map((post) => (
<PostCard post={post}/>
))}
</ul>
<ul class={listStyle.postsListContainer} aria-label={listAriaLabel} role="list">
{postsToDisplay.map((post) => <PostCard post={post} />)}
</ul>

View File

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

View File

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

View File

@@ -1,19 +1,33 @@
---
import {SEOProps} from './shared';
type Props = Pick<SEOProps, 'editedTime' | 'publishedTime' | 'keywords' | 'unicornsData'>;
const { keywords, editedTime, publishedTime, unicornsData } = Astro.props as Props;
const author = unicornsData.map(uni => uni.name).join(",");
---
<>{keywords?.length ? keywords.map(keyword =>
<meta property="article:tag" content={keyword} />) : 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} />}
</>
---
import { SEOProps } from "./shared";
type Props = Pick<
SEOProps,
"editedTime" | "publishedTime" | "keywords" | "unicornsData"
>;
const { keywords, editedTime, publishedTime, unicornsData } =
Astro.props as Props;
const author = unicornsData.map((uni) => uni.name).join(",");
---
<>
{
keywords?.length
? keywords.map((keyword) => (
<meta property="article:tag" content={keyword} />
))
: 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';
type Props = Pick<SEOProps, 'publishedTime' | 'isbn' | 'unicornsData'>;
const {
publishedTime,
unicornsData,
isbn
} = Astro.props as Props;
const author = unicornsData!.map((uni)=> uni.name).join(",");
---
<meta property="book:release_date" content={publishedTime} />
<meta property="book:author" content={author} />
<>{isbn && <meta property="book:isbn" content={isbn} />}</>
---
import { SEOProps } from "./shared";
type Props = Pick<SEOProps, "publishedTime" | "isbn" | "unicornsData">;
const { publishedTime, unicornsData, isbn } = Astro.props as Props;
const author = unicornsData!.map((uni) => uni.name).join(",");
---
<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 {SEOProps} from './shared';
type Props = Pick<SEOProps, 'langData'> & {
pathName: string;
siteMetadata: {siteUrl: string}
};
const {langData, siteMetadata, pathName} = Astro.props as Props;
---
{langData?.currentLang && (
<link
rel="alternate"
href={
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}` +
removePrefixLanguageFromPath(pathName || "")
}
href-lang={lang}
/>
)) : null}
---
import { removePrefixLanguageFromPath } from "utils/translations";
import { SEOProps } from "./shared";
type Props = Pick<SEOProps, "langData"> & {
pathName: string;
siteMetadata: { siteUrl: string };
};
const { langData, siteMetadata, pathName } = Astro.props as Props;
---
<>
{
langData?.currentLang && (
<link
rel="alternate"
href={
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}` +
removePrefixLanguageFromPath(pathName || "")
}
href-lang={lang}
/>
))
: null
}
</>

View File

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

View File

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

View File

@@ -1,29 +1,34 @@
---
import {SEOProps} from './shared';
type Props = Pick<SEOProps, 'title' | 'unicornsData' | 'type'> & {
metaDescription: string;
metaImage: string;
siteMetadata: {twitterHandle: string}
uniTwitter?: string;
};
const {
title,
metaDescription,
siteMetadata,
metaImage,
unicornsData,
uniTwitter,
type
} = Astro.props as Props;
---
{/* Twitter SEO */}
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={metaDescription} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content={siteMetadata.twitterHandle} />
<meta name="twitter:image" content={metaImage} />
{type === "article" && unicornsData?.length === 1 && uniTwitter ? (
<meta property="twitter:creator" content={`@${uniTwitter}`} />
) : null}
---
import { SEOProps } from "./shared";
type Props = Pick<SEOProps, "title" | "unicornsData" | "type"> & {
metaDescription: string;
metaImage: string;
siteMetadata: { twitterHandle: string };
uniTwitter?: string;
};
const {
title,
metaDescription,
siteMetadata,
metaImage,
unicornsData,
uniTwitter,
type,
} = Astro.props as Props;
---
{/* Twitter SEO */}
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={metaDescription} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content={siteMetadata.twitterHandle} />
<meta name="twitter:image" content={metaImage} />
<>
{
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}}>
window.onload = () => {
const tocListRef = document.querySelector('#tocList');
<script define:vars={{ headingsToDisplaySlugs }}>
window.onload = () => {
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 highlightFirstActive = () => {
if (!tocListRef) return;
let firstVisibleLink =
tocListRef.querySelector(".toc-is-visible");
const handleObserver = (entries) => {
const highlightFirstActive = () => {
if (!tocListRef) return;
let firstVisibleLink = tocListRef.querySelector(".toc-is-visible");
linkRefs.forEach((linkRef) => {
linkRef.classList.remove("toc-is-active");
});
linkRefs.forEach((linkRef) => {
linkRef.classList.remove("toc-is-active");
});
if (firstVisibleLink) {
firstVisibleLink.classList.add("toc-is-active");
}
if (firstVisibleLink) {
firstVisibleLink.classList.add("toc-is-active");
}
if (!firstVisibleLink && previousSection.current) {
tocListRef
.querySelector(`a[href="#${previousSection.current}"]`)
.parentElement.classList.add("toc-is-active");
}
};
if (!firstVisibleLink && previousSection.current) {
tocListRef
.querySelector(`a[href="#${previousSection.current}"]`)
.parentElement.classList.add("toc-is-active");
}
};
entries.forEach((entry) => {
let href = `#${entry.target.getAttribute("id")}`,
link = linkRefs.find(
(l) => l.firstElementChild.getAttribute("href") === href
);
entries.forEach((entry) => {
let href = `#${entry.target.getAttribute("id")}`,
link = linkRefs.find(
(l) => l.firstElementChild.getAttribute("href") === href
);
if (!link) return;
if (entry.isIntersecting && entry.intersectionRatio >= 1) {
link.classList.add("toc-is-visible");
const newPreviousSection = entry.target.getAttribute("id");
previousSection.current = newPreviousSection;
} else {
link.classList.remove("toc-is-visible");
}
if (!link) return;
if (entry.isIntersecting && entry.intersectionRatio >= 1) {
link.classList.add("toc-is-visible");
const newPreviousSection = entry.target.getAttribute("id");
previousSection.current = newPreviousSection;
} else {
link.classList.remove("toc-is-visible");
}
highlightFirstActive();
});
};
highlightFirstActive();
});
};
const observer = new IntersectionObserver(handleObserver, {
rootMargin: "0px",
threshold: 1,
});
const observer = new IntersectionObserver(handleObserver, {
rootMargin: "0px",
threshold: 1,
});
const headingsEls = headingsToDisplaySlugs.map((headingToDisplay) => {
return document.getElementById(headingToDisplay);
});
const headingsEls = headingsToDisplaySlugs.map((headingToDisplay) => {
return document.getElementById(headingToDisplay);
});
headingsEls
.filter((a) => a)
.forEach((heading) => {
observer.observe(heading);
});
}
</script>
headingsEls
.filter((a) => a)
.forEach((heading) => {
observer.observe(heading);
});
};
</script>

View File

@@ -6,7 +6,7 @@ import { PostInfo } from "types/PostInfo";
import HeadingIntersectionObserverScript from "./heading-intersection-observer-script.astro";
interface TableOfContentsProps {
headingsWithId: PostInfo["headingsWithId"];
headingsWithId: PostInfo["headingsWithId"];
}
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
const headingsToDisplay = headings
.map((h) => Object.assign({}, h, { depth: h.depth - minDepth + 1 }))
.filter((headingInfo) => headingInfo.depth <= 3);
.map((h) => Object.assign({}, h, { depth: h.depth - minDepth + 1 }))
.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"}>
<ol
class={tableOfContentsStyle.tableList}
role="list"
id="tocList"
>
{headingsToDisplay.map((headingInfo, i) => {
const liClassNames = classnames(tableOfContentsStyle.tocLi, {
[tableOfContentsStyle.tocH1]: headingInfo.depth === 1,
[tableOfContentsStyle.tocH2]: headingInfo.depth === 2,
[tableOfContentsStyle.tocH3]: headingInfo.depth === 3,
});
return (
<li
class={liClassNames}
data-headingitem="true"
>
<a href={`#${headingInfo.slug}`}>{headingInfo.value}</a>
</li>
);
})}
</ol>
<ol class={tableOfContentsStyle.tableList} role="list" id="tocList">
{
headingsToDisplay.map((headingInfo, i) => {
const liClassNames = classnames(tableOfContentsStyle.tocLi, {
[tableOfContentsStyle.tocH1]: headingInfo.depth === 1,
[tableOfContentsStyle.tocH2]: headingInfo.depth === 2,
[tableOfContentsStyle.tocH3]: headingInfo.depth === 3,
});
return (
<li class={liClassNames} data-headingitem="true">
<a href={`#${headingInfo.slug}`}>{headingInfo.value}</a>
</li>
);
})
}
</ol>
</aside>
<HeadingIntersectionObserverScript headingsToDisplaySlugs={headingsToDisplaySlugs} />
<HeadingIntersectionObserverScript
headingsToDisplaySlugs={headingsToDisplaySlugs}
/>

View File

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

View File

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

View File

@@ -6,32 +6,32 @@ let parent: string;
// Try & Catch to allow for hosts themselves to be passed
// `new URL('domain.com')` will fail/throw, but is a valid host
try {
const url = new URL(siteUrl);
// URLs like 'localhost:3000' might not give host.
// Throw in order to catch in wrapper handler
if (!url.host) throw new Error();
parent = url.host;
const url = new URL(siteUrl);
// URLs like 'localhost:3000' might not give host.
// Throw in order to catch in wrapper handler
if (!url.host) throw new Error();
parent = url.host;
} catch (_) {
const url = new URL("https://" + siteUrl);
parent = url.host;
const url = new URL("https://" + siteUrl);
parent = url.host;
}
// Twitch embed throws error with strings like 'localhost:3000', but
// those persist with `new URL().host`
if (parent.startsWith("localhost")) {
parent = "localhost";
parent = "localhost";
}
const siteMetadata = {
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`,
siteUrl,
disqusShortname: "unicorn-utterances",
repoPath: "unicorn-utterances/unicorn-utterances",
relativeToPosts: "/content/blog",
keywords:
"programming,development,mobile,web,game,utterances,software engineering,javascript,angular,react,computer science",
twitterHandle: "@unicornuttrncs",
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`,
siteUrl,
disqusShortname: "unicorn-utterances",
repoPath: "unicorn-utterances/unicorn-utterances",
relativeToPosts: "/content/blog",
keywords:
"programming,development,mobile,web,game,utterances,software engineering,javascript,angular,react,computer science",
twitterHandle: "@unicornuttrncs",
};
export { parent, siteUrl, buildMode, siteMetadata };

View File

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

View File

@@ -1,66 +1,66 @@
export const COLORS = {
//main styles
darkPrimary: { light: "#153E67", dark: "#E4F4FF" },
primary: { light: "#127DB3", dark: "#127DB3" },
lightPrimary: { light: "#315e81", dark: "#bdd9e9" },
black: { light: "black", dark: "white" },
white: { light: "white", dark: "black" },
darkGrey: { light: "rgba(0, 0, 0, 0.64)", dark: "rgba(255, 255, 255, .64)" },
highImpactBlack: {
light: "rgba(0, 0, 0, 0.87)",
dark: "rgba(255, 255, 255, .87)",
},
midImpactBlack: {
light: "rgba(0, 0, 0, 0.64)",
dark: "rgba(255, 255, 255, .64)",
},
lowImpactBlack: {
light: "rgba(0, 0, 0, 0.58)",
dark: "rgba(255, 255, 255, .58)",
},
minImpactBlack: {
light: "rgba(0, 0, 0, 0.2)",
dark: "rgba(255, 255, 255, .2)",
},
backgroundColor: { light: "#E4F4FF", dark: "#072a41" },
cardActiveBackground: { light: "#EBF6FC", dark: "#163954" },
cardActiveBoxShadow: {
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",
},
codeBlockBackground: { light: "white", dark: "#202746" },
codeInlineBackground: { light: "#cbe8fb", dark: "#1d495e" },
//code styles
codeBackgroundColor: { light: "#fff", dark: "#161b1d" },
textColor: { light: "#5e6687", dark: "#7ea2b4" },
stringColor: { light: "#007396", dark: "#7ee2c4" },
keywordColor: { light: "#846c00", dark: "#b5ea94" },
operatorColor: { light: "#b74c00", dark: "#935c25" },
punctuationColor: { light: "#006fce", dark: "#7ea2b4" },
constantColor: { light: "#aa05d4", dark: "#cf8ae1" },
functionColor: { light: "#5357d2", dark: "#c1c3ff" },
selectionColor: { light: "#dfe2f1", dark: "#cfe0ec" },
commentColor: { light: "#898ea4", dark: "#c7d5d7" },
propColor: { light: "#c08b30", dark: "#8a8a0f" },
varColor: { light: "#3d8fd1", dark: "#88c1e2" },
selectorColor: { light: "#6679cc", dark: "#bdbdff" },
urlColor: { light: "#22a9c9", dark: "#9affde" },
insertedUnderlineColor: { light: "#202746", dark: "#ebf8ff" },
highlightColor: { light: "#c94922", dark: "#ff92c0" },
lineNumbersColor: { light: "#979db4", dark: "#abe1fa" },
lineHighlightColor: {
light: "rgba(107, 115, 148, 0.2)",
dark: "rgba(235, 248, 255, 0.2)",
},
lineHighlightFadeColor: {
light: "rgba(107, 115, 148, 0)",
dark: "rgba(235, 248, 255, 0)",
},
scrollBarBG: {
light: "rgba(18, 125, 179, 0.3)",
dark: "rgba(228, 244, 255, 0.3)",
},
scrollBarThumb: { light: "var(--primary)", dark: "var(--darkPrimary)" },
//main styles
darkPrimary: { light: "#153E67", dark: "#E4F4FF" },
primary: { light: "#127DB3", dark: "#127DB3" },
lightPrimary: { light: "#315e81", dark: "#bdd9e9" },
black: { light: "black", dark: "white" },
white: { light: "white", dark: "black" },
darkGrey: { light: "rgba(0, 0, 0, 0.64)", dark: "rgba(255, 255, 255, .64)" },
highImpactBlack: {
light: "rgba(0, 0, 0, 0.87)",
dark: "rgba(255, 255, 255, .87)",
},
midImpactBlack: {
light: "rgba(0, 0, 0, 0.64)",
dark: "rgba(255, 255, 255, .64)",
},
lowImpactBlack: {
light: "rgba(0, 0, 0, 0.58)",
dark: "rgba(255, 255, 255, .58)",
},
minImpactBlack: {
light: "rgba(0, 0, 0, 0.2)",
dark: "rgba(255, 255, 255, .2)",
},
backgroundColor: { light: "#E4F4FF", dark: "#072a41" },
cardActiveBackground: { light: "#EBF6FC", dark: "#163954" },
cardActiveBoxShadow: {
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",
},
codeBlockBackground: { light: "white", dark: "#202746" },
codeInlineBackground: { light: "#cbe8fb", dark: "#1d495e" },
//code styles
codeBackgroundColor: { light: "#fff", dark: "#161b1d" },
textColor: { light: "#5e6687", dark: "#7ea2b4" },
stringColor: { light: "#007396", dark: "#7ee2c4" },
keywordColor: { light: "#846c00", dark: "#b5ea94" },
operatorColor: { light: "#b74c00", dark: "#935c25" },
punctuationColor: { light: "#006fce", dark: "#7ea2b4" },
constantColor: { light: "#aa05d4", dark: "#cf8ae1" },
functionColor: { light: "#5357d2", dark: "#c1c3ff" },
selectionColor: { light: "#dfe2f1", dark: "#cfe0ec" },
commentColor: { light: "#898ea4", dark: "#c7d5d7" },
propColor: { light: "#c08b30", dark: "#8a8a0f" },
varColor: { light: "#3d8fd1", dark: "#88c1e2" },
selectorColor: { light: "#6679cc", dark: "#bdbdff" },
urlColor: { light: "#22a9c9", dark: "#9affde" },
insertedUnderlineColor: { light: "#202746", dark: "#ebf8ff" },
highlightColor: { light: "#c94922", dark: "#ff92c0" },
lineNumbersColor: { light: "#979db4", dark: "#abe1fa" },
lineHighlightColor: {
light: "rgba(107, 115, 148, 0.2)",
dark: "rgba(235, 248, 255, 0.2)",
},
lineHighlightFadeColor: {
light: "rgba(107, 115, 148, 0)",
dark: "rgba(235, 248, 255, 0)",
},
scrollBarBG: {
light: "rgba(18, 125, 179, 0.3)",
dark: "rgba(228, 244, 255, 0.3)",
},
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";
:root {
--animSpeed: 200ms;
--animStyle: ease-in-out;
--cardOutlineStyle: 1px solid var(--primary);
--cardRadius: #{$baseUnit}px;
--filterBarIconSize: #{3 * $baseUnit}px;
--listViewPadding: #{1.5 * $baseUnit}px;
font-size: $rootFontSize;
line-height: 1.2;
scrollbar-color: var(--darkPrimary) var(--backgroundColor);
transition: scrollbar-color var(--animStyle) var(--animSpeed);
--animSpeed: 200ms;
--animStyle: ease-in-out;
--cardOutlineStyle: 1px solid var(--primary);
--cardRadius: #{$baseUnit}px;
--filterBarIconSize: #{3 * $baseUnit}px;
--listViewPadding: #{1.5 * $baseUnit}px;
font-size: $rootFontSize;
line-height: 1.2;
scrollbar-color: var(--darkPrimary) var(--backgroundColor);
transition: scrollbar-color var(--animStyle) var(--animSpeed);
}
@include from($startMediumScreenSize) {
:root {
--listViewPadding: #{$baseUnit * 2.5}px;
}
:root {
--listViewPadding: #{$baseUnit * 2.5}px;
}
}
:focus {
outline-color: var(--darkPrimary);
outline-color: var(--darkPrimary);
}
//without this all <button>s have bad outline colors in firefox
:focus::-moz-focus-inner {
padding: 0; //prevent weirdness just in case
border-color: var(--darkPrimary);
padding: 0; //prevent weirdness just in case
border-color: var(--darkPrimary);
}
*::-webkit-scrollbar {
width: 12px;
width: 12px;
}
*::-webkit-scrollbar-track {
background: var(--scrollBarBG);
border-radius: 10px;
background: var(--scrollBarBG);
border-radius: 10px;
}
*::-webkit-scrollbar-thumb {
border-radius: 10px;
background: var(--scrollBarThumb);
border-radius: 10px;
background: var(--scrollBarThumb);
}
.listViewContent {
margin: 0 auto;
max-width: #{160 * $baseUnit}px; // 1280px
padding: 0 var(--listViewPadding);
margin: 0 auto;
max-width: #{160 * $baseUnit}px; // 1280px
padding: 0 var(--listViewPadding);
}
.postViewContent {
padding: #{$baseUnit * 2.5}px;
overflow-wrap: break-word;
padding: #{$baseUnit * 2.5}px;
overflow-wrap: break-word;
}
.postViewContent > * {
margin: 0 auto;
margin: 0 auto;
}
body {
background-color: var(--backgroundColor);
margin: 0;
padding: 0;
@extend %body-1;
color: var(--black);
transition: color var(--animStyle) var(--animSpeed),
background-color var(--animStyle) var(--animSpeed);
background-color: var(--backgroundColor);
margin: 0;
padding: 0;
@extend %body-1;
color: var(--black);
transition: color var(--animStyle) var(--animSpeed),
background-color var(--animStyle) var(--animSpeed);
}
.medium-zoom-overlay {
background: var(--backgroundColor) !important;
background: var(--backgroundColor) !important;
}
/* https://snook.ca/archives/html_and_css/hiding-content-for-accessibility */
.visually-hidden {
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
clip: rect(1px, 1px, 1px, 1px);
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
clip: rect(1px, 1px, 1px, 1px);
}
.visually-hidden a:focus,
.visually-hidden input:focus,
.visually-hidden button:focus {
position: static;
width: auto;
height: auto;
position: static;
width: auto;
height: auto;
}
.filterDropdown {
// This maps to the dropdown button. If the asset is changes, thus this must as well
transform-origin: 26px 26px;
transition: transform 300ms var(--animStyle);
// This maps to the dropdown button. If the asset is changes, thus this must as well
transform-origin: 26px 26px;
transition: transform 300ms var(--animStyle);
}
.expandedIcon .filterDropdown {
transform: rotate(180deg);
transform: rotate(180deg);
}
.baseBtn {
cursor: pointer;
cursor: pointer;
}
.baseBtn,
.btnLike {
appearance: none;
text-decoration: none;
background: none;
border: none;
transition: background var(--animSpeed) var(--animStyle),
box-shadow var(--animSpeed) var(--animStyle),
border-color var(--animSpeed) var(--animStyle),
color var(--animStyle) var(--animSpeed);
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
border-radius: var(--cardRadius);
font-size: 1rem;
padding: #{math.div($baseUnit, 2)}px #{$baseUnit}px;
color: var(--darkPrimary);
appearance: none;
text-decoration: none;
background: none;
border: none;
transition: background var(--animSpeed) var(--animStyle),
box-shadow var(--animSpeed) var(--animStyle),
border-color var(--animSpeed) var(--animStyle),
color var(--animStyle) var(--animSpeed);
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
border-radius: var(--cardRadius);
font-size: 1rem;
padding: #{math.div($baseUnit, 2)}px #{$baseUnit}px;
color: var(--darkPrimary);
}
.baseBtn:hover {
background: var(--cardActiveBackground);
border-color: transparent;
box-shadow: var(--cardActiveBoxShadow);
background: var(--cardActiveBackground);
border-color: transparent;
box-shadow: var(--cardActiveBoxShadow);
}
.baseBtn svg,
.btnLike svg {
$size: #{$baseUnit * 4}px;
height: $size;
width: $size;
flex-shrink: 0;
$size: #{$baseUnit * 4}px;
height: $size;
width: $size;
flex-shrink: 0;
}
$pendIconMarg: #{$baseUnit}px;
.baseBtn.prependIcon svg,
.btnLike.prependIcon svg {
margin-right: $pendIconMarg;
margin-right: $pendIconMarg;
}
.baseBtn.appendIcon svg,
.btnLike.appendIcon svg {
margin-left: $pendIconMarg;
margin-left: $pendIconMarg;
}
.post-body {
margin: 0 auto #{$baseUnit * 4}px;
max-width: 768px;
line-height: 1.7;
margin: 0 auto #{$baseUnit * 4}px;
max-width: 768px;
line-height: 1.7;
// Fix autolink-headings anchors positioning
.anchor {
line-height: 1;
padding-right: 24px;
// Fix autolink-headings anchors positioning
.anchor {
line-height: 1;
padding-right: 24px;
svg {
vertical-align: middle;
}
}
svg {
vertical-align: middle;
}
}
.anchor.before {
position: absolute;
top: 0;
left: 0;
transform: translateX(-100%);
padding-right: 24px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.anchor.before {
position: absolute;
top: 0;
left: 0;
transform: translateX(-100%);
padding-right: 24px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
h1 .anchor svg,
h2 .anchor svg,
h3 .anchor svg,
h4 .anchor svg,
h5 .anchor svg,
h6 .anchor svg {
visibility: hidden;
}
h1 .anchor svg,
h2 .anchor svg,
h3 .anchor svg,
h4 .anchor svg,
h5 .anchor svg,
h6 .anchor svg {
visibility: hidden;
}
h1:hover .anchor svg,
h2:hover .anchor svg,
h3:hover .anchor svg,
h4:hover .anchor svg,
h5:hover .anchor svg,
h6:hover .anchor svg,
h1 .anchor:focus svg,
h2 .anchor:focus svg,
h3 .anchor:focus svg,
h4 .anchor:focus svg,
h5 .anchor:focus svg,
h6 .anchor:focus svg {
visibility: visible;
}
h1:hover .anchor svg,
h2:hover .anchor svg,
h3:hover .anchor svg,
h4:hover .anchor svg,
h5:hover .anchor svg,
h6:hover .anchor svg,
h1 .anchor:focus svg,
h2 .anchor:focus svg,
h3 .anchor:focus svg,
h4 .anchor:focus svg,
h5 .anchor:focus svg,
h6 .anchor:focus svg {
visibility: visible;
}
img {
margin: 0 auto;
display: block;
max-width: 100%;
img {
margin: 0 auto;
display: block;
max-width: 100%;
&[src$=".svg"] {
width: 100%;
max-height: 50vh;
}
}
&[src$=".svg"] {
width: 100%;
max-height: 50vh;
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 1.25em;
line-height: 1.5;
margin-bottom: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 1.25em;
line-height: 1.5;
margin-bottom: 0;
}
h1 + h2,
h2 + h3,
h3 + h4,
h4 + h5,
h5 + h6 {
margin-top: 0.75em;
}
h1 + h2,
h2 + h3,
h3 + h4,
h4 + h5,
h5 + h6 {
margin-top: 0.75em;
}
table tr:last-child th:first-child {
border-bottom-left-radius: 10px;
}
table tr:last-child th:first-child {
border-bottom-left-radius: 10px;
}
table tr:last-child td:first-child {
border-bottom-left-radius: 10px;
}
table tr:last-child td:first-child {
border-bottom-left-radius: 10px;
}
table tr:last-child td:last-child {
border-bottom-right-radius: 10px;
}
table tr:last-child td:last-child {
border-bottom-right-radius: 10px;
}
.table-container {
max-width: 100%;
overflow: auto;
}
.table-container {
max-width: 100%;
overflow: auto;
}
table {
border: var(--cardOutlineStyle);
border-radius: var(--cardRadius);
border-collapse: collapse;
// Border-collapse and border-radius don't mix. This is a workaround for that issue
box-shadow: 0 0 0 1px var(--primary);
overflow: hidden;
table {
border: var(--cardOutlineStyle);
border-radius: var(--cardRadius);
border-collapse: collapse;
// Border-collapse and border-radius don't mix. This is a workaround for that issue
box-shadow: 0 0 0 1px var(--primary);
overflow: hidden;
@include until($endSmallScreenSize) {
ul {
padding: 0;
list-style: none;
}
}
}
@include until($endSmallScreenSize) {
ul {
padding: 0;
list-style: none;
}
}
}
tr,
td,
th {
border: var(--cardOutlineStyle);
}
tr,
td,
th {
border: var(--cardOutlineStyle);
}
td,
th {
padding-left: 1rem;
padding-right: 1rem;
td,
th {
padding-left: 1rem;
padding-right: 1rem;
@include until($endSmallScreenSize) {
padding-left: 5px;
padding-right: 5px;
}
}
@include until($endSmallScreenSize) {
padding-left: 5px;
padding-right: 5px;
}
}
iframe {
width: 100%;
min-height: 500px;
border: var(--cardOutlineStyle);
border-radius: 8px;
overflow: hidden;
}
iframe {
width: 100%;
min-height: 500px;
border: var(--cardOutlineStyle);
border-radius: 8px;
overflow: hidden;
}
p > code {
display: inline;
padding: 0 0.4em;
font-size: 85%;
color: var(--black);
background-color: var(--codeInlineBackground);
border-radius: 4px;
}
p > code {
display: inline;
padding: 0 0.4em;
font-size: 85%;
color: var(--black);
background-color: var(--codeInlineBackground);
border-radius: 4px;
}
details {
border: 2px solid var(--darkPrimary);
border-radius: 0.5rem;
margin-bottom: 1em;
details {
border: 2px solid var(--darkPrimary);
border-radius: 0.5rem;
margin-bottom: 1em;
summary {
padding: .5rem 1.5rem;
background: var(--darkPrimary);
color: var(--backgroundColor);
cursor: pointer;
}
summary {
padding: 0.5rem 1.5rem;
background: var(--darkPrimary);
color: var(--backgroundColor);
cursor: pointer;
}
summary ~ * {
margin-left: 1.5rem;
margin-right: 1.5rem;
}
}
summary ~ * {
margin-left: 1.5rem;
margin-right: 1.5rem;
}
}
}
.post-lower-area {
margin: 0 auto;
max-width: #{$baseUnit * 115}px;
margin: 0 auto;
max-width: #{$baseUnit * 115}px;
.postBottom {
display: flex;
justify-content: space-between;
.postBottom {
display: flex;
justify-content: space-between;
@include until($startMediumScreenSize) {
flex-direction: column;
@include until($startMediumScreenSize) {
flex-direction: column;
.btnLike {
order: 2;
}
.btnLike {
order: 2;
}
.baseBtn {
margin-top: 20px;
margin-bottom: 5px;
}
}
}
.baseBtn {
margin-top: 20px;
margin-bottom: 5px;
}
}
}
}
// Please use this sparingly. There's massive A11y concerns
.unlink {
text-decoration: none;
color: inherit;
text-decoration: none;
color: inherit;
}
pre {
overflow: auto;
border: var(--cardOutlineStyle);
border-radius: 8px;
background: white;
overflow: auto;
border: var(--cardOutlineStyle);
border-radius: 8px;
background: white;
}
.lowercase {
text-transform: lowercase;
text-transform: lowercase;
}
.pointer {
cursor: pointer;
cursor: pointer;
}
.circleImg {
flex-shrink: 0;
flex-grow: 0;
border-radius: 50%;
flex-shrink: 0;
flex-grow: 0;
border-radius: 50%;
}
a {
color: var(--darkPrimary);
color: var(--darkPrimary);
}
svg.strokeicon {
&,
* {
transition: stroke var(--animStyle) var(--animSpeed);
stroke: var(--darkPrimary);
}
&,
* {
transition: stroke var(--animStyle) var(--animSpeed);
stroke: var(--darkPrimary);
}
}
svg:not(.strokeicon) {
&,
* {
transition: fill var(--animStyle) var(--animSpeed);
fill: var(--darkPrimary);
}
&,
* {
transition: fill var(--animStyle) var(--animSpeed);
fill: var(--darkPrimary);
}
}
.marginZeroAutoChild {
& > * {
margin: 0 auto;
}
& > * {
margin: 0 auto;
}
}
li > ul > li {
margin: 1rem 0;
margin: 1rem 0;
}
.toc-is-active {
font-weight: bold;
font-weight: bold;
a {
color: var(--darkPrimary) !important;
}
a {
color: var(--darkPrimary) !important;
}
}
kbd {
background-color: #eee;
border-radius: 3px;
border: 1px solid #b4b4b4;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 2px 0 0 rgba(255, 255, 255, 0.7) inset;
color: #333;
display: inline-block;
font-size: 0.85em;
font-weight: 700;
line-height: 1;
padding: 2px 4px;
white-space: nowrap;
background-color: #eee;
border-radius: 3px;
border: 1px solid #b4b4b4;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 2px 0 0 rgba(255, 255, 255, 0.7) inset;
color: #333;
display: inline-block;
font-size: 0.85em;
font-weight: 700;
line-height: 1;
padding: 2px 4px;
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 clipId = `path-1-inside-1${id}`;
const props = Astro.props;
---
<svg
width="36"
height="36"
viewBox="0 0 36 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clip-path={`url(#${clipId})`}>
<path
fill-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"
fill="#153E67"
/>
</g>
<defs>
<clipPath id={clipId}>
<rect width="36" height="36" fill="white" />
</clipPath>
</defs>
</svg>
<svg
width="36"
height="36"
viewBox="0 0 36 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clip-path={`url(#${clipId})`}>
<path
fill-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"
fill="#153E67"></path>
</g>
<defs>
<clip-path id={clipId}>
<rect width="36" height="36" fill="white"></rect>
</clip-path>
</defs>
</svg>

View File

@@ -1,27 +1,26 @@
---
import {v4 as uuidV4} from 'uuid';
import { v4 as uuidV4 } from "uuid";
const id = uuidV4();
const clipId = `path-1-inside-1${id}`;
const props = Astro.props;
---
<svg
width="36"
height="36"
viewBox="0 0 36 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
width="36"
height="36"
viewBox="0 0 36 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath={`url(#${clipId.current})`}>
<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"
fill="#153E67"
/>
</g>
<defs>
<clipPath id={clipId.current}>
<rect width="36" height="36" fill="white" />
</clipPath>
</defs>
</svg>
<g clip-path={`url(#${clipId})`}>
<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"
fill="#153E67"></path>
</g>
<defs>
<clip-path id={clipId}>
<rect width="36" height="36" fill="white"></rect>
</clip-path>
</defs>
</svg>

View File

@@ -1,70 +1,70 @@
---
import ThemeStyle from "../page-components/layouts/theme-style.astro";
import BlockingThemeChangerScript from "../page-components/layouts/blocking-theme-changer-script.astro";
import Layout from "../components/layout/layout.astro";
import '../global.scss';
---
<html class="light">
<head>
<meta name="viewport" content="width=device-width">
<meta charset="utf-8"/>
<link
rel="preload"
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"
/>
<link rel="icon" href="/favicon.ico" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#127db3" />
<link
rel="apple-touch-icon"
sizes="48x48"
href="/icons/icon-48x48.png?v=2a"
/>
<link
rel="apple-touch-icon"
sizes="72x72"
href="/icons/icon-72x72.png?v=2a"
/>
<link
rel="apple-touch-icon"
sizes="96x96"
href="/icons/icon-96x96.png?v=2a"
/>
<link
rel="apple-touch-icon"
sizes="144x144"
href="/icons/icon-144x144.png?v=2a"
/>
<link
rel="apple-touch-icon"
sizes="192x192"
href="/icons/icon-192x192.png?v=2a"
/>
<link
rel="apple-touch-icon"
sizes="256x256"
href="/icons/icon-256x256.png?v=2a"
/>
<link
rel="apple-touch-icon"
sizes="384x384"
href="/icons/icon-384x384.png?v=2a"
/>
<link
rel="apple-touch-icon"
sizes="512x512"
href="/icons/icon-512x512.png?v=2a"
/>
<script defer src="/uninstall-sw.js" is:inline/>
<ThemeStyle/>
<slot name="head"/>
</head>
<body>
<BlockingThemeChangerScript/>
<Layout>
<slot />
</Layout>
</body>
</html>
---
import ThemeStyle from "../page-components/layouts/theme-style.astro";
import BlockingThemeChangerScript from "../page-components/layouts/blocking-theme-changer-script.astro";
import Layout from "../components/layout/layout.astro";
import "../global.scss";
---
<html class="light">
<head>
<meta name="viewport" content="width=device-width" />
<meta charset="utf-8" />
<link
rel="preload"
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"
/>
<link rel="icon" href="/favicon.ico" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#127db3" />
<link
rel="apple-touch-icon"
sizes="48x48"
href="/icons/icon-48x48.png?v=2a"
/>
<link
rel="apple-touch-icon"
sizes="72x72"
href="/icons/icon-72x72.png?v=2a"
/>
<link
rel="apple-touch-icon"
sizes="96x96"
href="/icons/icon-96x96.png?v=2a"
/>
<link
rel="apple-touch-icon"
sizes="144x144"
href="/icons/icon-144x144.png?v=2a"
/>
<link
rel="apple-touch-icon"
sizes="192x192"
href="/icons/icon-192x192.png?v=2a"
/>
<link
rel="apple-touch-icon"
sizes="256x256"
href="/icons/icon-256x256.png?v=2a"
/>
<link
rel="apple-touch-icon"
sizes="384x384"
href="/icons/icon-384x384.png?v=2a"
/>
<link
rel="apple-touch-icon"
sizes="512x512"
href="/icons/icon-512x512.png?v=2a"
/>
<script defer src="/uninstall-sw.js" is:inline></script>
<ThemeStyle />
<slot name="head" />
</head>
<body>
<BlockingThemeChangerScript />
<Layout>
<slot />
</Layout>
</body>
</html>

View File

@@ -5,7 +5,7 @@ import dayjs from "dayjs";
import { PostInfo } from "types/PostInfo";
interface PostMetadataProps {
post: PostInfo;
post: PostInfo;
}
const { post } = Astro.props as PostMetadataProps;
@@ -14,8 +14,8 @@ const { authorsMeta } = post;
let originalHost;
if (post.originalLink) {
const url = new URL(post.originalLink);
originalHost = url.host;
const url = new URL(post.originalLink);
originalHost = url.host;
}
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}>
<UserProfilePic authors={authorsMeta} class={styles.postMetadataAuthorImagesContainer} />
<div class={styles.textDiv}>
<h2 class={styles.authorName} data-testid="post-meta-author-name">
<span>by </span>
{authorsMeta.map((author, i) => {
return (
<>
{i !== 0 && <span>{", "}</span>}
<a href={`/unicorns/${author.id}`} class={styles.authorLink}>
{author.name}
</a>
</>
);
})}
</h2>
<div class={styles.belowName}>
<p>{publishedStr}</p>
<p>{post.wordCount} words</p>
</div>
</div>
{!!post.originalLink && (
<p class={styles.originalLink}>
Originally posted at&nbsp;
<a href={post.originalLink} target="_blank" rel="nofollow noopener noreferrer">
{originalHost}
</a>
</p>
)}
</div>
<UserProfilePic
authors={authorsMeta}
class={styles.postMetadataAuthorImagesContainer}
/>
<div class={styles.textDiv}>
<h2 class={styles.authorName} data-testid="post-meta-author-name">
<span>by</span>
{
authorsMeta.map((author, i) => {
return (
<>
{i !== 0 && <span>{", "}</span>}
<a href={`/unicorns/${author.id}`} class={styles.authorLink}>
{author.name}
</a>
</>
);
})
}
</h2>
<div class={styles.belowName}>
<p>{publishedStr}</p>
<p>{post.wordCount} words</p>
</div>
</div>
{
!!post.originalLink && (
<p class={styles.originalLink}>
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";
interface PostTitleHeaderProps {
post: PostInfo;
post: PostInfo;
}
const {post} = Astro.props as PostTitleHeaderProps;
const { post } = Astro.props as PostTitleHeaderProps;
const { title, tags } = post;
---
<div class={styles.container}>
<h1 class={styles.title}>{title}</h1>
<ul aria-label="Post tags" role="list" class={styles.tags}>
{tags.map((tag) => (
<li role="listitem">
{tag}
</li>
))}
</ul>
</div>
<h1 class={styles.title}>{title}</h1>
<ul aria-label="Post tags" role="list" class={styles.tags}>
{tags.map((tag) => <li role="listitem">{tag}</li>)}
</ul>
</div>

View File

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

View File

@@ -1,50 +1,50 @@
@import "../../../styles/vars";
@import "../../../styles/text_styles";
@import "../../../components/post-card/post-card.module";
.list {
list-style: none;
margin: 0;
padding: 0;
}
.localCard {
padding: 1rem !important;
margin-bottom: 1rem;
text-decoration: none;
}
.localCard a {
text-decoration: none;
}
.aTag {
display: block;
}
.localCard:hover .titleTag {
text-decoration: underline;
transition: text-decoration var(--animSpeed);
}
.header {
@extend %headline-4;
margin-bottom: 1rem;
}
.author {
color: var(--midImpactBlack) !important;
text-underline-style: none;
}
.srOnly {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
@import "../../../styles/vars";
@import "../../../styles/text_styles";
@import "../../../components/post-card/post-card.module";
.list {
list-style: none;
margin: 0;
padding: 0;
}
.localCard {
padding: 1rem !important;
margin-bottom: 1rem;
text-decoration: none;
}
.localCard a {
text-decoration: none;
}
.aTag {
display: block;
}
.localCard:hover .titleTag {
text-decoration: underline;
transition: text-decoration var(--animSpeed);
}
.header {
@extend %headline-4;
margin-bottom: 1rem;
}
.author {
color: var(--midImpactBlack) !important;
text-underline-style: none;
}
.srOnly {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
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 {
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/
*
* Huge thanks to Josh for outlining how to do this
*/
/**
* DARK MODE CODE
*
* 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
* 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";
const mql = window.matchMedia("(prefers-color-scheme: dark)");
const prefersDarkFromMQ = mql.matches;
const prefersDarkFromLocalStorage = localStorage.getItem(STORAGE_KEY);
let colorMode = "light";
const hasUsedToggle = typeof prefersDarkFromLocalStorage === "string";
if (hasUsedToggle) {
colorMode = prefersDarkFromLocalStorage;
} else {
colorMode = prefersDarkFromMQ ? "dark" : "light";
}
let root = document.documentElement;
// TODO: migrate to `classList`
root.className = colorMode;
}
const boundFn = String(setColorsByTheme)
.replace("COLOR_MODE_STORAGE_KEY", COLOR_MODE_STORAGE_KEY);
let calledFunction = `(${boundFn})()`;
calledFunction = (await Terser.minify(calledFunction)).code!;
---
<script set:html={calledFunction} />
---
import * as Terser from "terser";
import { 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/
*
* Huge thanks to Josh for outlining how to do this
*/
/**
* DARK MODE CODE
*
* 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
* 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";
const mql = window.matchMedia("(prefers-color-scheme: dark)");
const prefersDarkFromMQ = mql.matches;
const prefersDarkFromLocalStorage = localStorage.getItem(STORAGE_KEY);
let colorMode = "light";
const hasUsedToggle = typeof prefersDarkFromLocalStorage === "string";
if (hasUsedToggle) {
colorMode = prefersDarkFromLocalStorage;
} else {
colorMode = prefersDarkFromMQ ? "dark" : "light";
}
let root = document.documentElement;
// TODO: migrate to `classList`
root.className = colorMode;
}
const boundFn = String(setColorsByTheme).replace(
"COLOR_MODE_STORAGE_KEY",
COLOR_MODE_STORAGE_KEY
);
let calledFunction = `(${boundFn})()`;
calledFunction = (await Terser.minify(calledFunction)).code!;
---
<script set:html={calledFunction}></script>

View File

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

View File

@@ -1,26 +1,38 @@
---
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";
interface PostListHeaderProps {
siteDescription: string;
siteDescription: string;
}
const { siteDescription } = Astro.props as PostListHeaderProps;
---
<div class={styles.container} role="banner" aria-label={`Banner for Unicorn Utterances`}>
<div class={styles.headerPic}>
<Image format={'png'} loading={"eager"} sizes={"300px"} alt={`Smiling cartoon unicorn with a
bowtie`} src={unicornLogo} 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>
<div
class={styles.container}
role="banner"
aria-label={`Banner for Unicorn Utterances`}
>
<div class={styles.headerPic}>
<Image
format={"png"}
loading={"eager"}
sizes={"300px"}
alt={`Smiling cartoon unicorn with a
bowtie`}
src={unicornLogo}
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";
export interface PostListTemplateProps {
posts: PostInfo[];
rootURL: string;
page: Pick<Page<PostInfo>, 'total' | 'currentPage' | 'size' | 'lastPage' | 'url'>
posts: PostInfo[];
rootURL: string;
page: Pick<
Page<PostInfo>,
"total" | "currentPage" | "size" | "lastPage" | "url"
>;
}
const {
posts,
page,
rootURL
} = Astro.props as PostListTemplateProps;
const { posts, page, rootURL } = Astro.props as PostListTemplateProps;
---
<div>
<!-- <PostListProvider
<!-- <PostListProvider
posts={posts}
numberOfPages={numberOfPages}
limitNumber={limitNumber}
pageIndex={pageIndex}
> -->
<PostListHeader siteDescription={siteMetadata.description} />
<main>
<!-- <FilterSearchBar /> -->
<PostList listAriaLabel="List of posts" postsToDisplay={posts} />
</main>
<Pagination page={page} rootURL={rootURL} />
<!-- <Pagination absolutePath={router.basePath} /> -->
<!-- </PostListProvider> -->
<PostListHeader siteDescription={siteMetadata.description} />
<main>
<!-- <FilterSearchBar /> -->
<PostList listAriaLabel="List of posts" postsToDisplay={posts} />
</main>
<Pagination page={page} rootURL={rootURL} />
<!-- <Pagination absolutePath={router.basePath} /> -->
<!-- </PostListProvider> -->
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,15 @@
---
import Document from '../layouts/document.astro';
import PostListTemplate, { PostListTemplateProps } from '../page-components/post-list/post-list.astro';
import { getAllPostsForListView } from 'utils/api';
import {PostInfo} from 'types/PostInfo';
import Document from "../layouts/document.astro";
import PostListTemplate, {
PostListTemplateProps,
} 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";
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 page = {
@@ -17,12 +19,12 @@ const page = {
lastPage: Math.floor(enPosts.length / postsToDisplay.length),
url: {
current: Astro.url.href,
next: '/page/2'
}
} as PostListTemplateProps['page'];
next: "/page/2",
},
} as PostListTemplateProps["page"];
---
<Document>
<SEO slot="head" title="Homepage" />
<PostListTemplate posts={postsToDisplay} page={page} rootURL={'/'}/>
</Document>
<PostListTemplate posts={postsToDisplay} page={page} rootURL={"/"} />
</Document>

View File

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

View File

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

View File

@@ -1,34 +1,48 @@
---
import Document from '../../../../layouts/document.astro';
import SEO from "components/seo/seo.astro";
import UnicornsPage from '../../../../page-components/unicorns/unicorn-page.astro';
import { getAllPostsForUnicornListView } from 'utils/api';
import {PostInfo} from 'types/PostInfo';
import { unicorns } from "utils/data";
import { Page } from 'astro';
export async function getStaticPaths({ paginate }) {
const posts = await Astro.glob<PostInfo>('../../../../../content/blog/**/*.md')
return unicorns.map(unicorn => {
const postsToDisplay = getAllPostsForUnicornListView(unicorn.id, posts, 'en');
return paginate(postsToDisplay, { params: {unicornid: unicorn.id}, pageSize: 8 });
})
}
const { page } = Astro.props as {page: Page<PostInfo>};
const params = Astro.params as {unicornid: string};
const unicorn = unicorns.find(unicorn => unicorn.id === params.unicornid);
const rootURL = `/unicorns/${unicorn.id}/`;
---
<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>
---
import Document from "../../../../layouts/document.astro";
import SEO from "components/seo/seo.astro";
import UnicornsPage from "../../../../page-components/unicorns/unicorn-page.astro";
import { getAllPostsForUnicornListView } from "utils/api";
import { PostInfo } from "types/PostInfo";
import { unicorns } from "utils/data";
import { Page } from "astro";
export async function getStaticPaths({ paginate }) {
const posts = await Astro.glob<PostInfo>(
"../../../../../content/blog/**/*.md"
);
return unicorns.map((unicorn) => {
const postsToDisplay = getAllPostsForUnicornListView(
unicorn.id,
posts,
"en"
);
return paginate(postsToDisplay, {
params: { unicornid: unicorn.id },
pageSize: 8,
});
});
}
const { page } = Astro.props as { page: Page<PostInfo> };
const params = Astro.params as { unicornid: string };
const unicorn = unicorns.find((unicorn) => unicorn.id === params.unicornid);
const rootURL = `/unicorns/${unicorn.id}/`;
---
<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;
display: inline-block;
text-align: right;
color: rgba(115,138,148,.4)
color: rgba(115, 138, 148, 0.4);
}
/* Start of Shiki Twoslash CSS:
@@ -77,7 +77,8 @@ pre.shiki:hover .dim {
pre.shiki div.dim {
opacity: 0.5;
}
pre.shiki div.dim, pre.shiki div.highlight {
pre.shiki div.dim,
pre.shiki div.highlight {
margin: 0;
padding: 0;
}
@@ -90,7 +91,7 @@ pre.shiki div.line {
}
/** Don't show the language identifiers */
pre.shiki .language-id{
pre.shiki .language-id {
display: none;
}
@@ -163,7 +164,8 @@ pre code a {
}
pre data-err {
/* 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;
}
pre .query {
@@ -178,7 +180,8 @@ pre .query {
/* This sections keeps both of those two in in sync */
pre .error, pre .error-behind {
pre .error,
pre .error-behind {
margin-left: -14px;
margin-top: 8px;
margin-bottom: 4px;
@@ -346,7 +349,6 @@ pre .logger.log-log svg {
margin-right: 9px;
}
blockquote {
position: relative;
margin-left: 1em;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
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>;
export = transformer;
const transformer: Transformer<any>;
export = transformer;
}

View File

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

View File

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

View File

@@ -2,49 +2,49 @@ import { PostInfo } from "types/PostInfo";
import { Languages } from "types/index";
import { MarkdownInstance } from "astro";
let allPostsCache = new WeakMap<object, MarkdownInstance<PostInfo>[]>();
const allPostsCache = new WeakMap<object, MarkdownInstance<PostInfo>[]>();
export function getAllPosts(
posts: MarkdownInstance<PostInfo>[],
language: Languages,
cacheString: null | object = null
posts: MarkdownInstance<PostInfo>[],
language: Languages,
cacheString: null | object = null
): MarkdownInstance<PostInfo>[] {
if (cacheString) {
const cacheData = allPostsCache.get(cacheString);
if (cacheData) return cacheData as any;
}
if (cacheString) {
const cacheData = allPostsCache.get(cacheString);
if (cacheData) return cacheData as any;
}
if (cacheString) allPostsCache.set(cacheString, posts);
if (cacheString) allPostsCache.set(cacheString, posts);
return posts
.filter(post => post.frontmatter.locale === language);
return posts.filter((post) => post.frontmatter.locale === language);
}
const listViewCache = {};
export const getAllPostsForListView = (
posts: MarkdownInstance<PostInfo>[],
language: Languages,
posts: MarkdownInstance<PostInfo>[],
language: Languages
): PostInfo[] => {
let allPosts = getAllPosts(posts, language, listViewCache);
let allPosts = getAllPosts(posts, language, listViewCache);
// sort posts by date in descending order
allPosts = allPosts.sort((post1, post2) => {
const date1 = new Date(post1.frontmatter.published);
const date2 = new Date(post2.frontmatter.published);
return date1 > date2 ? -1 : 1;
});
// sort posts by date in descending order
allPosts = allPosts.sort((post1, post2) => {
const date1 = new Date(post1.frontmatter.published);
const date2 = new Date(post2.frontmatter.published);
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 = (
authorId: string,
posts: MarkdownInstance<PostInfo>[],
language: Languages,
authorId: string,
posts: MarkdownInstance<PostInfo>[],
language: Languages
): PostInfo[] => {
return getAllPostsForListView(posts, language)
.filter(post =>
post.authorsMeta.find(postAuthor => postAuthor.id === authorId)
);
return getAllPostsForListView(posts, language).filter((post) =>
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");
const unicornsRaw: Array<
Omit<UnicornInfo, "roles" | "pronouns" | "profileImg"> & {
roles: string[];
pronouns: string;
profileImg: string;
}
Omit<UnicornInfo, "roles" | "pronouns" | "profileImg"> & {
roles: string[];
pronouns: string;
profileImg: string;
}
> = JSON.parse(
fs.readFileSync(join(dataDirectory, "unicorns.json")).toString()
fs.readFileSync(join(dataDirectory, "unicorns.json")).toString()
);
const rolesRaw: RolesEnum[] = JSON.parse(
fs.readFileSync(join(dataDirectory, "roles.json")).toString()
fs.readFileSync(join(dataDirectory, "roles.json")).toString()
);
const pronounsRaw: PronounInfo[] = JSON.parse(
fs.readFileSync(join(dataDirectory, "pronouns.json")).toString()
fs.readFileSync(join(dataDirectory, "pronouns.json")).toString()
);
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 absoluteFSPath = join(dataDirectory, unicorn.profileImg);
/**
* `getFullRelativePath` strips all prefixing `/`, so we must add one manually
*/
const relativeServerPath = '/' + getFullRelativePath(
"content/data/",
unicorn.profileImg
);
const profileImgSize = getImageSize(unicorn.profileImg, dataDirectory);
const absoluteFSPath = join(dataDirectory, unicorn.profileImg);
/**
* `getFullRelativePath` strips all prefixing `/`, so we must add one manually
*/
const relativeServerPath =
"/" + getFullRelativePath("content/data/", unicorn.profileImg);
const profileImgSize = getImageSize(unicorn.profileImg, dataDirectory);
// Mutation go BRR
const newUnicorn: UnicornInfo = unicorn as never;
// Mutation go BRR
const newUnicorn: UnicornInfo = unicorn as never;
newUnicorn.profileImgMeta = {
height: profileImgSize.height as number,
width: profileImgSize.width as number,
relativePath: unicorn.profileImg,
relativeServerPath,
absoluteFSPath,
};
newUnicorn.profileImgMeta = {
height: profileImgSize.height as number,
width: profileImgSize.width as number,
relativePath: unicorn.profileImg,
relativeServerPath,
absoluteFSPath,
};
newUnicorn.rolesMeta = unicorn.roles.map(
(role) => rolesRaw.find((rRole) => rRole.id === role)!
);
newUnicorn.rolesMeta = unicorn.roles.map(
(role) => rolesRaw.find((rRole) => rRole.id === role)!
);
newUnicorn.pronounsMeta = pronounsRaw.find(
(proWithNouns) => proWithNouns.id === unicorn.pronouns
)!;
newUnicorn.pronounsMeta = pronounsRaw.find(
(proWithNouns) => proWithNouns.id === unicorn.pronouns
)!;
return newUnicorn;
return newUnicorn;
});
export {
fullUnicorns as unicorns,
rolesRaw as roles,
pronounsRaw as pronouns,
licensesRaw as licenses,
fullUnicorns as unicorns,
rolesRaw as roles,
pronounsRaw as pronouns,
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 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.
*/
import { rehypeUnicornPopulatePost } from "./markdown/rehype-unicorn-populate-post";
import { isNotJunk } from "junk";
import { postsDirectory } from "./data";
import { Languages, PostInfo } from "types/index";
import * as fs from "fs";
import * as path from "path";
const getIndexPath = (lang: Languages) => {
const indexPath = lang !== "en" ? `index.${lang}.md` : `index.md`;
return indexPath;
};
export function getPostSlugs(lang: Languages) {
// Avoid errors trying to read from `.DS_Store` files
return fs
.readdirSync(postsDirectory)
.filter(isNotJunk)
.filter((dir) =>
fs.existsSync(path.resolve(postsDirectory, dir, getIndexPath(lang)))
);
}
export const getAllPosts = (lang: Languages): PostInfo[] => {
const slugs = getPostSlugs(lang);
return slugs.map(slug => {
const file = {
path: path.join(postsDirectory, slug, getIndexPath(lang)),
data: {
astro: {
frontmatter: {},
},
},
};
(rehypeUnicornPopulatePost as any)()(undefined, file);
return {
...(file.data.astro.frontmatter as any || {}).frontmatterBackup,
...file.data.astro.frontmatter
};
})
}
/**
* 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
* when the Astro runtime isn't available, such as getting suggested articles and other instances.
*/
import { rehypeUnicornPopulatePost } from "./markdown/rehype-unicorn-populate-post";
import { isNotJunk } from "junk";
import { postsDirectory } from "./data";
import { Languages, PostInfo } from "types/index";
import * as fs from "fs";
import * as path from "path";
const getIndexPath = (lang: Languages) => {
const indexPath = lang !== "en" ? `index.${lang}.md` : `index.md`;
return indexPath;
};
export function getPostSlugs(lang: Languages) {
// Avoid errors trying to read from `.DS_Store` files
return fs
.readdirSync(postsDirectory)
.filter(isNotJunk)
.filter((dir) =>
fs.existsSync(path.resolve(postsDirectory, dir, getIndexPath(lang)))
);
}
export const getAllPosts = (lang: Languages): PostInfo[] => {
const slugs = getPostSlugs(lang);
return slugs.map((slug) => {
const file = {
path: path.join(postsDirectory, slug, getIndexPath(lang)),
data: {
astro: {
frontmatter: {},
},
},
};
(rehypeUnicornPopulatePost as any)()(undefined, file);
return {
...((file.data.astro.frontmatter as any) || {}).frontmatterBackup,
...file.data.astro.frontmatter,
};
});
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,115 +1,116 @@
import { Root } from "hast";
import { Plugin } from "unified";
import { visit } from "unist-util-visit";
import { EMBED_SIZE } from "./constants";
import { isRelativePath } from "../url-paths";
import { fromHtml } from "hast-util-from-html";
import path from "path";
interface RehypeUnicornElementMapProps {
}
function escapeHTML(s) {
if (!s) return s;
return s
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
// TODO: Add switch/case and dedicated files ala "Components"
export const rehypeUnicornElementMap: Plugin<
[RehypeUnicornElementMapProps | never],
Root
> = () => {
return async (tree, file) => {
visit(tree, (node: any) => {
if (node.tagName === "iframe") {
node.properties.width ??= EMBED_SIZE.w;
node.properties.height ??= EMBED_SIZE.h;
node.properties.loading ??= "lazy";
}
if (node.tagName === "video") {
node.properties.muted ??= true;
node.properties.autoPlay ??= true;
node.properties.controls ??= true;
node.properties.loop ??= true;
node.properties.width ??= "100%";
node.properties.height ??= "auto";
}
if (node.tagName === "a") {
const href = node.properties.href;
const isInternalLink = isRelativePath(href || "");
if (!isInternalLink) {
node.properties.target = "_blank";
node.properties.rel = "nofollow noopener noreferrer";
}
}
if (node.tagName === "table" && !node.properties["has-changed"]) {
const children = [...node.children];
const properties = { ...node.properties, "has-changed": true };
node.tagName = "div";
node.properties = {
class: "table-container",
};
node.children = [
{
tagName: "table",
type: "element",
children,
properties,
},
];
}
if (
node.tagName === "h1" ||
node.tagName === "h2" ||
node.tagName === "h3" ||
node.tagName === "h4" ||
node.tagName === "h5" ||
node.tagName === "h6"
) {
const id = node.properties.id;
const headerText = node.properties["data-header-text"];
node.properties.style =
(node.properties.style || "") + "position: relative;";
const headerLinkHTML = `
<a
href="#${id}"
aria-label="Permalink for &quot;${escapeHTML(headerText)}&quot;"
class="anchor before"
>
<svg
width="20"
height="20"
viewBox="0 0 36 36"
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"
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"
fill="#153E67"
/>
</svg>
</a>
`;
const hastHeader = fromHtml(headerLinkHTML, { fragment: true });
node.children = [hastHeader, ...node.children];
}
});
};
};
import { Root } from "hast";
import { Plugin } from "unified";
import { visit } from "unist-util-visit";
import { EMBED_SIZE } from "./constants";
import { isRelativePath } from "../url-paths";
import { fromHtml } from "hast-util-from-html";
import path from "path";
interface RehypeUnicornElementMapProps {}
function escapeHTML(s) {
if (!s) return s;
return s
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
// TODO: Add switch/case and dedicated files ala "Components"
export const rehypeUnicornElementMap: Plugin<
[RehypeUnicornElementMapProps | never],
Root
> = () => {
return async (tree, file) => {
visit(tree, (node: any) => {
if (node.tagName === "iframe") {
node.properties.width ??= EMBED_SIZE.w;
node.properties.height ??= EMBED_SIZE.h;
node.properties.loading ??= "lazy";
}
if (node.tagName === "video") {
node.properties.muted ??= true;
node.properties.autoPlay ??= true;
node.properties.controls ??= true;
node.properties.loop ??= true;
node.properties.width ??= "100%";
node.properties.height ??= "auto";
}
if (node.tagName === "a") {
const href = node.properties.href;
const isInternalLink = isRelativePath(href || "");
if (!isInternalLink) {
node.properties.target = "_blank";
node.properties.rel = "nofollow noopener noreferrer";
}
}
if (node.tagName === "table" && !node.properties["has-changed"]) {
const children = [...node.children];
const properties = { ...node.properties, "has-changed": true };
node.tagName = "div";
node.properties = {
class: "table-container",
};
node.children = [
{
tagName: "table",
type: "element",
children,
properties,
},
];
}
if (
node.tagName === "h1" ||
node.tagName === "h2" ||
node.tagName === "h3" ||
node.tagName === "h4" ||
node.tagName === "h5" ||
node.tagName === "h6"
) {
const id = node.properties.id;
const headerText = node.properties["data-header-text"];
node.properties.style =
(node.properties.style || "") + "position: relative;";
const headerLinkHTML = `
<a
href="#${id}"
aria-label="Permalink for &quot;${escapeHTML(
headerText
)}&quot;"
class="anchor before"
>
<svg
width="20"
height="20"
viewBox="0 0 36 36"
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"
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"
fill="#153E67"
/>
</svg>
</a>
`;
const hastHeader = fromHtml(headerLinkHTML, { fragment: true });
node.children = [hastHeader, ...node.children];
}
});
};
};

View File

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

View File

@@ -1,81 +1,77 @@
import { Root } from "hast";
import { Plugin } from "unified";
import matter from "gray-matter";
import { readFileSync } from "fs";
import * as path from "path";
import { licenses, unicorns } from "../data";
interface RehypeUnicornPopulatePostProps {
}
export const rehypeUnicornPopulatePost: Plugin<
[RehypeUnicornPopulatePostProps | never],
Root
> = () => {
return (_, file) => {
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 directorySplit = file.path.split(path.sep);
// 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
const indexName = directorySplit.at(-1);
const indexSplit = indexName.split('.');
let locale = indexSplit.at(-2);
if (locale === 'index') {
locale = 'en';
}
// // TODO: Add translations
// if (fields.translations) {
// const langsToQuery: Languages[] = Object.keys(languages).filter(
// (l) => l !== lang
// ) as never;
// pickedData.translations = langsToQuery
// .filter((lang) =>
// fs.existsSync(resolve(dirname(fullPath), getIndexPath(lang)))
// )
// .reduce((prev, lang) => {
// prev[lang] = languages[lang];
// return prev;
// }, {} as Record<Languages, string>);
// }
// // TODO: Add collection slug
// if (fields.collectionSlug) {
// if (frontmatterData.series) {
// pickedData.collectionSlug = collectionsByName.find(
// (collection) => collection.associatedSeries === frontmatterData.series
// )?.slug;
// }
// if (!pickedData.collectionSlug) pickedData.collectionSlug = null;
// }
const authorsMeta = (frontmatter.authors as string[]).map(
(author) => unicorns.find((unicorn) => unicorn.id === author)!
);
let license;
if (frontmatter.license) {
license = licenses.find(
(l) => l.id === frontmatter.license
);
}
if (!license) license = null;
setData('slug', slug);
setData('locale', locale);
setData('authorsMeta', authorsMeta);
setData('license', license);
setData('frontmatterBackup', frontmatter);
};
};
import { Root } from "hast";
import { Plugin } from "unified";
import matter from "gray-matter";
import { readFileSync } from "fs";
import * as path from "path";
import { licenses, unicorns } from "../data";
interface RehypeUnicornPopulatePostProps {}
export const rehypeUnicornPopulatePost: Plugin<
[RehypeUnicornPopulatePostProps | never],
Root
> = () => {
return (_, file) => {
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 directorySplit = file.path.split(path.sep);
// 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
const indexName = directorySplit.at(-1);
const indexSplit = indexName.split(".");
let locale = indexSplit.at(-2);
if (locale === "index") {
locale = "en";
}
// // TODO: Add translations
// if (fields.translations) {
// const langsToQuery: Languages[] = Object.keys(languages).filter(
// (l) => l !== lang
// ) as never;
// pickedData.translations = langsToQuery
// .filter((lang) =>
// fs.existsSync(resolve(dirname(fullPath), getIndexPath(lang)))
// )
// .reduce((prev, lang) => {
// prev[lang] = languages[lang];
// return prev;
// }, {} as Record<Languages, string>);
// }
// // TODO: Add collection slug
// if (fields.collectionSlug) {
// if (frontmatterData.series) {
// pickedData.collectionSlug = collectionsByName.find(
// (collection) => collection.associatedSeries === frontmatterData.series
// )?.slug;
// }
// if (!pickedData.collectionSlug) pickedData.collectionSlug = null;
// }
const authorsMeta = (frontmatter.authors as string[]).map(
(author) => unicorns.find((unicorn) => unicorn.id === author)!
);
let license;
if (frontmatter.license) {
license = licenses.find((l) => l.id === frontmatter.license);
}
if (!license) license = null;
setData("slug", slug);
setData("locale", locale);
setData("authorsMeta", authorsMeta);
setData("license", license);
setData("frontmatterBackup", frontmatter);
};
};

View File

@@ -1,72 +1,76 @@
/**
* An ode to words
*
* Oh words, what can be said of thee?
*
* Not much me.
*
* See, it's concieved that ye might have intreging definitions from one-to-another
*
* This is to say: "What is a word?"
*
* 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?
*
* 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."
*
* Please do let us know if you have strong thoughts/answers on the topic,
* we're happy to hear them.
*/
import { Root, Parent, Text } from "hast";
import { Node } from "unist";
import { Plugin } from "unified";
import { visit } from "unist-util-visit";
import { unified } from "unified";
import english from "retext-english";
import rehypeRetext from 'rehype-retext';
import { validateConfig } from "astro/dist/types/core/config";
interface RemarkCountProps {}
function count(counts: Record<string, number>) {
return () => counter;
function counter(tree: Root) {
visit(tree, visitor);
function visitor(node: Node) {
if (node.type === 'SourceNode') {
const inlineCount = (node as never as {value: string}).value.split(/\b/g).length;
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;
RootNode: number;
ParagraphNode: number;
SentenceNode: number;
WordNode: number;
TextNode: number;
WhiteSpaceNode: number;
PunctuationNode: number;
SymbolNode: number;
SourceNode: number;
};
await unified()
.use(rehypeRetext, unified().use(english).use(count(counts)))
.run(tree);
(file.data.astro as any).frontmatter.wordCount = (counts.InlineCodeWords || 0) + (counts.TextNode || 0);
};
};
/**
* An ode to words
*
* Oh words, what can be said of thee?
*
* Not much me.
*
* See, it's concieved that ye might have intreging definitions from one-to-another
*
* This is to say: "What is a word?"
*
* 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?
*
* 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."
*
* Please do let us know if you have strong thoughts/answers on the topic,
* we're happy to hear them.
*/
import { Root, Parent, Text } from "hast";
import { Node } from "unist";
import { Plugin } from "unified";
import { visit } from "unist-util-visit";
import { unified } from "unified";
import english from "retext-english";
import rehypeRetext from "rehype-retext";
import { validateConfig } from "astro/dist/types/core/config";
interface RemarkCountProps {}
function count(counts: Record<string, number>) {
return () => counter;
function counter(tree: Root) {
visit(tree, visitor);
function visitor(node: Node) {
if (node.type === "SourceNode") {
const inlineCount = (node as never as { value: string }).value.split(
/\b/g
).length;
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;
RootNode: number;
ParagraphNode: number;
SentenceNode: number;
WordNode: number;
TextNode: number;
WhiteSpaceNode: number;
PunctuationNode: number;
SymbolNode: number;
SourceNode: number;
};
await unified()
.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";
interface ElementNode extends Parent {
tagName: string;
properties: any;
tagName: string;
properties: any;
}
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[]) => {
let largestSize = Infinity;
for (let node of nodes) {
if (!isNodeHeading(node)) continue;
const size = parseInt(node.tagName.substr(1), 10);
largestSize = Math.min(largestSize, size);
}
return largestSize;
let largestSize = Infinity;
for (const node of nodes) {
if (!isNodeHeading(node)) continue;
const size = parseInt(node.tagName.substr(1), 10);
largestSize = Math.min(largestSize, size);
}
return largestSize;
};
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 {
injectSubheaderProps?: boolean;
tabSlugifyProps?: Parameters<typeof getHeaderNodeId>[1];
injectSubheaderProps?: boolean;
tabSlugifyProps?: Parameters<typeof getHeaderNodeId>[1];
}
/**
@@ -48,127 +48,127 @@ export interface RehypeTabsProps {
* @see https://github.com/reactjs/react-tabs
*/
export const rehypeTabs: Plugin<[RehypeTabsProps | never], Root> = ({
injectSubheaderProps = false,
tabSlugifyProps = {},
injectSubheaderProps = false,
tabSlugifyProps = {},
}) => {
return (tree) => {
const replaceTabNodes = (nodes: Node[]) => {
let sectionStarted = false;
return (tree) => {
const replaceTabNodes = (nodes: Node[]) => {
let sectionStarted = false;
const largestSize = findLargestHeading(nodes as ElementNode[]);
const largestSize = findLargestHeading(nodes as ElementNode[]);
const tabsContainer = {
type: "element",
tagName: "div",
properties: {
class: "tabs"
},
children: [
{
type: "element",
tagName: "ul",
properties: {
role: 'tablist',
class: "tabs__tab-list"
},
children: [] as ElementNode[],
},
],
};
const tabsContainer = {
type: "element",
tagName: "div",
properties: {
class: "tabs",
},
children: [
{
type: "element",
tagName: "ul",
properties: {
role: "tablist",
class: "tabs__tab-list",
},
children: [] as ElementNode[],
},
],
};
for (const localNode of nodes as ElementNode[]) {
if (!sectionStarted && !isNodeLargestHeading(localNode, largestSize)) {
continue;
}
sectionStarted = true;
for (const localNode of nodes as ElementNode[]) {
if (!sectionStarted && !isNodeLargestHeading(localNode, largestSize)) {
continue;
}
sectionStarted = true;
if (isNodeLargestHeading(localNode, largestSize)) {
// Make sure that all tabs labeled "thing" aren't also labeled "thing2"
slugs.reset();
const { id: headerSlug } = getHeaderNodeId(
localNode,
tabSlugifyProps
);
if (isNodeLargestHeading(localNode, largestSize)) {
// Make sure that all tabs labeled "thing" aren't also labeled "thing2"
slugs.reset();
const { id: headerSlug } = getHeaderNodeId(
localNode,
tabSlugifyProps
);
// - 1 because the tabs are part of the header
const idx = tabsContainer.children.length - 1;
// - 1 because the tabs are part of the header
const idx = tabsContainer.children.length - 1;
const header = {
type: "element",
tagName: "li",
children: localNode.children,
properties: {
role: 'tab',
class: "tabs__tab",
"data-tabname": headerSlug,
"aria-selected": idx === 0 ? "true" : 'false',
"aria-controls": `panel-${idx}`,
"id": `tab-${idx}`,
tabIndex: idx === 0 ? "0" : "-1"
},
};
const header = {
type: "element",
tagName: "li",
children: localNode.children,
properties: {
role: "tab",
class: "tabs__tab",
"data-tabname": headerSlug,
"aria-selected": idx === 0 ? "true" : "false",
"aria-controls": `panel-${idx}`,
id: `tab-${idx}`,
tabIndex: idx === 0 ? "0" : "-1",
},
};
const contents = {
type: "element",
tagName: "div",
children: [],
properties: {
id: `panel-${idx}`,
role: "tabpanel",
class: "tabs__tab-panel",
tabindex: 0,
"aria-labelledby": `tab-${idx}`,
...(idx === 0 ? {} : {hidden: "true"})
},
};
const contents = {
type: "element",
tagName: "div",
children: [],
properties: {
id: `panel-${idx}`,
role: "tabpanel",
class: "tabs__tab-panel",
tabindex: 0,
"aria-labelledby": `tab-${idx}`,
...(idx === 0 ? {} : { hidden: "true" }),
},
};
tabsContainer.children[0].children.push(header);
tabsContainer.children[0].children.push(header);
tabsContainer.children.push(contents);
continue;
}
tabsContainer.children.push(contents);
continue;
}
if (isNodeHeading(localNode) && injectSubheaderProps) {
// This is `tagName: tab`
const lastTab =
tabsContainer.children[0].children[
tabsContainer.children[0].children.length - 1
];
if (isNodeHeading(localNode) && injectSubheaderProps) {
// This is `tagName: tab`
const lastTab =
tabsContainer.children[0].children[
tabsContainer.children[0].children.length - 1
];
// Store the related tab ID in the attributes of the header
localNode.properties["data-tabname"] =
// Get the last tab's `data-tabname` property
lastTab.properties["data-tabname"];
// Store the related tab ID in the attributes of the header
localNode.properties["data-tabname"] =
// Get the last tab's `data-tabname` property
lastTab.properties["data-tabname"];
// Add header ID to array
lastTab.properties["data-headers"] = JSON.stringify(
JSON.parse(lastTab.properties["data-headers"] ?? "[]").concat(
localNode.properties.id
)
);
}
// Add header ID to array
lastTab.properties["data-headers"] = JSON.stringify(
JSON.parse(lastTab.properties["data-headers"] ?? "[]").concat(
localNode.properties.id
)
);
}
// Push into last `tab-panel`
tabsContainer.children[tabsContainer.children.length - 1].children.push(
localNode
);
}
// Push into last `tab-panel`
tabsContainer.children[tabsContainer.children.length - 1].children.push(
localNode
);
}
return [tabsContainer];
};
return [tabsContainer];
};
replaceAllBetween(
tree,
{ type: "raw", value: "<!-- tabs:start -->" } as never,
{ type: "raw", value: "<!-- tabs:end -->" } as never,
replaceTabNodes
);
replaceAllBetween(
tree,
{ type: "comment", value: " tabs:start " } as never,
{ type: "comment", value: " tabs:end " } as never,
replaceTabNodes
);
return tree;
};
replaceAllBetween(
tree,
{ type: "raw", value: "<!-- tabs:start -->" } as never,
{ type: "raw", value: "<!-- tabs:end -->" } as never,
replaceTabNodes
);
replaceAllBetween(
tree,
{ type: "comment", value: " tabs:start " } as never,
{ type: "comment", value: " tabs:end " } as never,
replaceTabNodes
);
return tree;
};
};

View File

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

View File

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

View File

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

View File

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