Update repository information
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
182
.gitignore
vendored
Normal file
@ -0,0 +1,182 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Working files for my website
|
||||
|
||||
__Source/
|
||||
|
||||
# Eleventy site output folder
|
||||
|
||||
_site/
|
||||
|
||||
# Logs
|
||||
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Caches
|
||||
|
||||
.cache
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
22
README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Leilukin's Hub
|
||||
|
||||
Source code of my hobby website, [Leilukin's Hub](https://leilukin.com/). Built with [Eleventy](https://www.11ty.dev/) and hosted on [Hostinger](https://www.hostinger.my/).
|
||||
|
||||
More technical information can be found on my site's [colophon page](https://leilukin.com/colophon).
|
||||
|
||||
## Run Locally
|
||||
Recommended JavaScript runtime: [Bun](https://bun.sh).
|
||||
|
||||
1. Clone this repository locally
|
||||
```
|
||||
git clone https://git.32bit.cafe/Leilukin/leilukin-site.git
|
||||
```
|
||||
1. Install dependencies
|
||||
```
|
||||
bun install
|
||||
```
|
||||
1. Run the project
|
||||
```
|
||||
bun run start
|
||||
```
|
||||
1. Open http://localhost:8080/ in your browser
|
50
eleventy.config.js
Normal file
@ -0,0 +1,50 @@
|
||||
// Plugins
|
||||
import { InputPathToUrlTransformPlugin } from "@11ty/eleventy";
|
||||
import pluginRss from "@11ty/eleventy-plugin-rss";
|
||||
import pluginEleventyNavigation from "@11ty/eleventy-navigation";
|
||||
import pluginSyntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
|
||||
import pluginEmbedEverything from "eleventy-plugin-embed-everything";
|
||||
import pluginWordcount from "eleventy-plugin-wordcount-extended";
|
||||
import pluginTOC from "@uncenter/eleventy-plugin-toc";
|
||||
|
||||
// Custom Configurations
|
||||
import markdownItConfig from "./src/_config/markdown-it.js";
|
||||
import filesConfig from "./src/_config/files.js";
|
||||
import collectionsConfig from "./src/_config/collections.js";
|
||||
import filtersConfig from "./src/_config/filters.js";
|
||||
import shortCodesConfig from "./src/_config/shortcodes.js";
|
||||
|
||||
export default function(eleventyConfig) {
|
||||
// Plugins
|
||||
eleventyConfig.addPlugin(InputPathToUrlTransformPlugin);
|
||||
eleventyConfig.addPlugin(pluginRss);
|
||||
eleventyConfig.addPlugin(pluginEleventyNavigation);
|
||||
eleventyConfig.addPlugin(pluginSyntaxHighlight, { preAttributes: { tabindex: 0 } });
|
||||
eleventyConfig.addPlugin(pluginEmbedEverything, { add: ['soundcloud'] });
|
||||
eleventyConfig.addPlugin(pluginWordcount);
|
||||
eleventyConfig.addPlugin(pluginTOC, {
|
||||
tags: ['h2', 'h3', 'h4', 'h5', 'h6'],
|
||||
wrapper: function (toc) {
|
||||
return `<nav class="toc" aria-labelledby="toc-heading">${toc}</nav>`;
|
||||
},
|
||||
});
|
||||
|
||||
// Custom Configurations
|
||||
eleventyConfig.addPlugin(markdownItConfig);
|
||||
eleventyConfig.addPlugin(filesConfig);
|
||||
eleventyConfig.addPlugin(collectionsConfig);
|
||||
eleventyConfig.addPlugin(filtersConfig);
|
||||
eleventyConfig.addPlugin(shortCodesConfig);
|
||||
|
||||
// Eleventy bundle plugin
|
||||
eleventyConfig.addBundle("css");
|
||||
eleventyConfig.addBundle("js", { toFileDirectory: "assets/js" });
|
||||
|
||||
return {
|
||||
markdownTemplateEngine: "njk",
|
||||
htmlTemplateEngine: "njk",
|
||||
dir: {
|
||||
input: "src"
|
||||
}
|
||||
};
|
||||
};
|
36
package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "leilukin-site",
|
||||
"description": "Leilukin's personal and hobby website.",
|
||||
"author": "Leilukin",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.32bit.cafe/Leilukin/leilukin-site"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "bunx @11ty/eleventy --serve --quiet",
|
||||
"build": "bunx @11ty/eleventy"
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@11ty/eleventy": "^3.0.0",
|
||||
"@11ty/eleventy-fetch": "^5.0.2",
|
||||
"@11ty/eleventy-navigation": "^1.0.1",
|
||||
"@11ty/eleventy-plugin-rss": "^2.0.3",
|
||||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0",
|
||||
"@uncenter/eleventy-plugin-toc": "^1.0.3",
|
||||
"@zachleat/details-utils": "^2.0.2",
|
||||
"eleventy-plugin-embed-everything": "^1.20.0",
|
||||
"eleventy-plugin-wordcount-extended": "^0.2.1",
|
||||
"install": "^0.13.0",
|
||||
"markdown-it-anchor": "^9.2.0",
|
||||
"markdown-it-attribution": "^0.1.4",
|
||||
"markdown-it-attrs": "^4.3.1",
|
||||
"markdown-it-bracketed-spans": "^1.0.1",
|
||||
"markdown-it-deflist": "^3.0.0",
|
||||
"markdown-it-footnote": "^4.0.0",
|
||||
"slugify": "^1.6.6"
|
||||
}
|
||||
}
|
22
src/.htaccess.njk
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
permalink: "{{ page.filePathStem }}"
|
||||
eleventyExcludeFromCollections: true
|
||||
eleventyAllowMissingExtension: true
|
||||
---
|
||||
ErrorDocument 404 /404.html
|
||||
|
||||
<FilesMatch "\.(ico|svg|avif|webp|jpg|jpeg|png|gif|woff2)$">
|
||||
Header set Cache-Control "max-age=31536000, public"
|
||||
</FilesMatch>
|
||||
<FilesMatch "\.(css|js)$">
|
||||
Header set Cache-Control "no-cache, public"
|
||||
</FilesMatch>
|
||||
|
||||
RewriteEngine on
|
||||
|
||||
# Redirect obfuscated email links to mailto link
|
||||
RewriteRule ^.*{{ sitemeta.siteAuthor.emailDecoyUrl }}.*$ "mailto:{{ sitemeta.siteAuthor.email }}" [R=301,L]
|
||||
|
||||
# Block bad bots
|
||||
RewriteCond %{HTTP_USER_AGENT} ({{ robots.htaccess }}) [NC]
|
||||
RewriteRule .* https://nocommercialuse.org/ [L]
|
4
src/404.njk
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
layout: misc/404
|
||||
permalink: "{{ page.filePathStem }}.html"
|
||||
---
|
139
src/_bundle/css/comments.css
Normal file
@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Author: Vera Konigin
|
||||
* Site: https://groundedwren.neocities.org
|
||||
* Contact: vera@groundedwren.com
|
||||
*
|
||||
* File Description: Styles for the guestbook control
|
||||
* CSS variables come from https://groundedwren.neocities.org/styles/gwBoilerPlatePersonalization.css
|
||||
*/
|
||||
:root { --icon-color: var(--clr-body-txt); }
|
||||
|
||||
.comment-form {
|
||||
display: grid;
|
||||
border: 0.1rem solid var(--clr-body-txt);
|
||||
padding: 1em;
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
.comment-form,
|
||||
.input-horizontal-wrapper { gap: 1em; }
|
||||
|
||||
.comment-form-title {
|
||||
text-align: center;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.comment-form input,
|
||||
.comment-form textarea {
|
||||
color: var(--clr-body-txt);
|
||||
background-color: var(--clr-quote-bg);
|
||||
}
|
||||
|
||||
.input-horizontal-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%/1, max(16rem, 100%/3)), 1fr));
|
||||
}
|
||||
|
||||
.input-vertical,
|
||||
.comment-box-container {
|
||||
display: grid;
|
||||
gap: 0.3em;
|
||||
}
|
||||
|
||||
.input-vertical input:focus,
|
||||
.comment-box-container textarea:focus { outline: 0.15em solid var(--clr-link); }
|
||||
|
||||
.comment-box-container textarea { resize: vertical; }
|
||||
|
||||
.inline-banner {
|
||||
padding: 0.5em;
|
||||
word-break: break-word;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5em;
|
||||
background-color: var(--clr-quote-bg);
|
||||
}
|
||||
|
||||
.inline-banner.warning { background-color: #9e0f00; }
|
||||
|
||||
.inline-banner.warning a { color: unset; }
|
||||
|
||||
.form-footer,
|
||||
.comment-footer {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.form-footer { justify-content: flex-end; }
|
||||
|
||||
.form-footer input,
|
||||
.comment-footer button,
|
||||
.show-comment {
|
||||
border: none;
|
||||
background: var(--clr-link-btn-bg);
|
||||
color: var(--clr-link-btn-txt);
|
||||
padding: 0.3em 0.6em;
|
||||
border-radius: 0.2em;
|
||||
}
|
||||
|
||||
.comment-footer button,
|
||||
.show-comment { font-size: initial; }
|
||||
|
||||
gw-comment-card:not(.collapsed) .show-comment { display: none; }
|
||||
gw-comment-card.collapsed .comment-article > *:not(.comment-header) { display: none !important; }
|
||||
gw-comment-card.collapsed .show-comment { display: block; }
|
||||
gw-comment-card.collapsed .comment-header-right time { display: none; }
|
||||
|
||||
.comments-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
max-width: 100%;
|
||||
gap: 1.5em;
|
||||
}
|
||||
|
||||
.comment-article {
|
||||
padding: 0.3em 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.comment-article blockquote {
|
||||
max-width: unset !important;
|
||||
overflow-wrap: break-word;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.comment-article > button { max-width: fit-content; }
|
||||
|
||||
.comment-header {
|
||||
display: grid;
|
||||
grid-template-columns: 0fr auto 1fr;
|
||||
gap: 0.4em;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.comment-article .comment-article {
|
||||
margin-left: 2em;
|
||||
margin-top: 0.5em;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.comment-id, .comment-header-right > time {
|
||||
font-size: 0.85em;
|
||||
font-style: italic;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.comment-header-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.commenter-name {
|
||||
font-size: 1.1em;
|
||||
font-weight: 700;
|
||||
overflow-wrap: break-word;
|
||||
}
|
528
src/_bundle/js/comments.js
Normal file
@ -0,0 +1,528 @@
|
||||
/**
|
||||
* @file Comments control
|
||||
* @author Vera Konigin vera@groundedwren.com
|
||||
* https://groundedwren.neocities.org
|
||||
*/
|
||||
|
||||
window.GW = window.GW || {};
|
||||
(function Controls(ns) {
|
||||
ns.CommentForm = class CommentForm extends HTMLElement {
|
||||
//#region staticProperties
|
||||
static instanceCount = 0;
|
||||
static instanceMap = {};
|
||||
//#endregion
|
||||
|
||||
//#region instance properties
|
||||
instanceId;
|
||||
isInitialized;
|
||||
titleText;
|
||||
discordURL;
|
||||
encodedPath;
|
||||
fallbackEmail;
|
||||
|
||||
//#region element properties
|
||||
formEl;
|
||||
titleEl;
|
||||
bannerEl;
|
||||
|
||||
dispNameInpt;
|
||||
emailInpt;
|
||||
websiteInpt;
|
||||
respToInpt;
|
||||
commentInpt;
|
||||
|
||||
resetBtn;
|
||||
submitBtn;
|
||||
//#endregion
|
||||
//#endregion
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.instanceId = CommentForm.instanceCount++;
|
||||
CommentForm.instanceMap[this.instanceId] = this;
|
||||
}
|
||||
|
||||
get idKey() {
|
||||
return `gw-comment-form-${this.instanceId}`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this.isInitialized) { return; }
|
||||
|
||||
this.titleText = this.getAttribute("titleText") || "Add a Comment";
|
||||
this.discordURL = this.getAttribute("discordURL");
|
||||
this.encodedPath = this.getAttribute("encodedPath");
|
||||
this.fallbackEmail = this.getAttribute("fallbackEmail");
|
||||
|
||||
this.renderContent();
|
||||
this.registerHandlers();
|
||||
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
//Markup
|
||||
this.innerHTML = `
|
||||
<form id="${this.idKey}-form"
|
||||
aria-labelledby="${this.idKey}-title"
|
||||
aria-describedby="${this.idKey}-banner"
|
||||
class="comment-form"
|
||||
autocomplete="off"
|
||||
>
|
||||
<h2 id="${this.idKey}-title" class="comment-form-title">${this.titleText}</h2>
|
||||
<div class="input-horizontal-wrapper">
|
||||
<div class="input-vertical">
|
||||
<label for="${this.idKey}-dispName">
|
||||
Display name<span aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input id="${this.idKey}-dispName" type="text" maxlength="1000" required="true">
|
||||
</div>
|
||||
<div class="input-vertical">
|
||||
<label for="${this.idKey}-email">Email</label>
|
||||
<input id="${this.idKey}-email" type="email">
|
||||
</div>
|
||||
<div class="input-vertical">
|
||||
<label for="${this.idKey}-website">Website</label>
|
||||
<input id="${this.idKey}-website" type="text" maxlength="1000">
|
||||
</div>
|
||||
<div class="input-vertical">
|
||||
<label for="${this.idKey}-respTo">Response to</label>
|
||||
<input id="${this.idKey}-respTo" type="number">
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-box-container">
|
||||
<div class="input-vertical">
|
||||
<label for="${this.idKey}-comment">
|
||||
Comment<span aria-hidden="true">*</span>
|
||||
</label>
|
||||
<textarea id="${this.idKey}-comment"
|
||||
minlength="1"
|
||||
maxlength="1000"
|
||||
required="true"
|
||||
rows="5"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div id="${this.idKey}-banner" class="inline-banner" aria-live="polite">
|
||||
<gw-icon iconKey="circle-info" title="info"></gw-icon>
|
||||
<span>Comments are manually approved</span>
|
||||
</div>
|
||||
<div class="form-footer">
|
||||
<input id="${this.idKey}-reset" type="reset" value="Reset">
|
||||
<input id="${this.idKey}-submit" type="submit" value="Submit">
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
//element properties
|
||||
this.formEl = document.getElementById(`${this.idKey}-form`);
|
||||
this.titleEl = document.getElementById(`${this.idKey}-title`);
|
||||
this.bannerEl = document.getElementById(`${this.idKey}-banner`);
|
||||
|
||||
this.dispNameInpt = document.getElementById(`${this.idKey}-dispName`);
|
||||
this.emailInpt = document.getElementById(`${this.idKey}-email`);
|
||||
this.websiteInpt = document.getElementById(`${this.idKey}-website`);
|
||||
this.respToInpt = document.getElementById(`${this.idKey}-respTo`);
|
||||
this.commentInpt = document.getElementById(`${this.idKey}-comment`);
|
||||
|
||||
this.resetBtn = document.getElementById(`${this.idKey}-reset`);
|
||||
this.submitBtn = document.getElementById(`${this.idKey}-submit`);
|
||||
|
||||
//default values
|
||||
this.dispNameInpt.value = localStorage.getItem("comment-name") || "";
|
||||
this.emailInpt.value = localStorage.getItem("comment-email") || "";
|
||||
this.websiteInpt.value = localStorage.getItem("comment-website") || "";
|
||||
}
|
||||
|
||||
//#region Handlers
|
||||
registerHandlers() {
|
||||
this.formEl.onsubmit = this.onSubmit;
|
||||
}
|
||||
|
||||
onSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const contentObj = {
|
||||
name: this.dispNameInpt.value,
|
||||
email: this.emailInpt.value,
|
||||
website: this.websiteInpt.value,
|
||||
responseTo: this.respToInpt.value,
|
||||
comment: (
|
||||
this.commentInpt.value || ""
|
||||
).replaceAll("\n", "<br>").replaceAll("(", "\\("),
|
||||
timestamp: new Date().toUTCString(),
|
||||
};
|
||||
const contentAry = [];
|
||||
for (let contentKey in contentObj) {
|
||||
contentAry.push(`${contentKey}=${contentObj[contentKey]}`);
|
||||
}
|
||||
|
||||
const request = new XMLHttpRequest();
|
||||
request.open(
|
||||
"POST",
|
||||
this.discordURL || atob("aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3Mv" + this.encodedPath),
|
||||
true
|
||||
);
|
||||
request.setRequestHeader("Content-Type", "application/json");
|
||||
|
||||
request.onreadystatechange = () => {
|
||||
if (request.readyState !== XMLHttpRequest.DONE) { return; }
|
||||
if (Math.floor(request.status / 100) !== 2) {
|
||||
console.log(request.responseText);
|
||||
this.bannerEl.classList.add("warning");
|
||||
this.bannerEl.innerHTML =
|
||||
`
|
||||
<gw-icon iconKey="triangle-exclamation" title="warning"></gw-icon>
|
||||
<span>
|
||||
That didn't work.
|
||||
${this.fallbackEmail
|
||||
? `<a class="full" href="mailto:${this.fallbackEmail}?subject=Comment on ${document.title}&body=${contentAry.join("; ")}">Click here to send as an email instead</a>.`
|
||||
: ""
|
||||
}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
else {
|
||||
alert("Your comment has been submitted!");
|
||||
}
|
||||
};
|
||||
|
||||
request.send(JSON.stringify({
|
||||
embeds: [{
|
||||
fields: Object.keys(contentObj).map(key => { return { name: key, value: contentObj[key] }})
|
||||
}]
|
||||
}));
|
||||
|
||||
localStorage.setItem("comment-name", contentObj.name);
|
||||
localStorage.setItem("comment-email", contentObj.email);
|
||||
localStorage.setItem("comment-website", contentObj.website);
|
||||
|
||||
this.formEl.reset();
|
||||
this.dispNameInpt.value = contentObj.name;
|
||||
this.emailInpt.value = contentObj.email;
|
||||
this.websiteInpt.value = contentObj.website;
|
||||
};
|
||||
//#endregion
|
||||
};
|
||||
customElements.define("gw-comment-form", ns.CommentForm);
|
||||
|
||||
ns.CommentList = class CommentList extends HTMLElement {
|
||||
//#region staticProperties
|
||||
static instanceCount = 0;
|
||||
static instanceMap = {};
|
||||
static Data = [];
|
||||
//#endregion
|
||||
|
||||
//#region instance properties
|
||||
instanceId;
|
||||
isInitialized;
|
||||
gSpreadsheetId;
|
||||
gSheetId;
|
||||
isNewestFirst;
|
||||
gwCommentFormId;
|
||||
|
||||
//#region element properties
|
||||
//#endregion
|
||||
//#endregion
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.instanceId = CommentList.instanceCount++;
|
||||
CommentList.instanceMap[this.instanceId] = this;
|
||||
CommentList.Data[this.instanceId] = {};
|
||||
}
|
||||
|
||||
get idKey() {
|
||||
return `gw-comment-list-${this.instanceId}`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this.isInitialized) { return; }
|
||||
|
||||
this.gSpreadsheetId = this.getAttribute("gSpreadsheetId");
|
||||
this.gSheetId = this.getAttribute("gSheetId");
|
||||
this.isNewestFirst = this.getAttribute("isNewestFirst");
|
||||
this.gwCommentFormId = this.getAttribute("gwCommentFormId");
|
||||
|
||||
this.loadAndRender();
|
||||
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
async loadAndRender() {
|
||||
this.innerHTML = `
|
||||
<div class="inline-banner">
|
||||
<gw-icon iconkey="circle-info" title="info"></gw-icon>
|
||||
<span>Comments loading....</span>
|
||||
</div>
|
||||
`
|
||||
|
||||
const sheetReader = new GW.Gizmos.GoogleSheetsReader(this.gSpreadsheetId, this.gSheetId);
|
||||
await sheetReader.loadData();
|
||||
this.innerHTML = "";
|
||||
|
||||
const allComments = sheetReader.rowData;
|
||||
if (this.isNewestFirst) {
|
||||
allComments.reverse();
|
||||
}
|
||||
|
||||
this.renderContent();
|
||||
this.registerHandlers();
|
||||
|
||||
const allCommentsIndex = {};
|
||||
const topLevelCommentIdxs = [];
|
||||
const childCommentIdxs = [];
|
||||
for (let i = 0; i < allComments.length; i++) {
|
||||
const comment = allComments[i];
|
||||
allCommentsIndex[comment.ID] = i;
|
||||
if (!comment.ResponseTo) {
|
||||
topLevelCommentIdxs.push(i);
|
||||
}
|
||||
else {
|
||||
childCommentIdxs.push(i);
|
||||
}
|
||||
}
|
||||
childCommentIdxs.forEach(childIdx => {
|
||||
const replyId = allComments[childIdx].ResponseTo;
|
||||
const respondeeComment = allComments[allCommentsIndex[replyId]];
|
||||
|
||||
respondeeComment.ChildIdxs = respondeeComment.ChildIdxs || [];
|
||||
respondeeComment.ChildIdxs.push(childIdx);
|
||||
});
|
||||
|
||||
let commentsToBuild = [];
|
||||
topLevelCommentIdxs.forEach(
|
||||
topCommentIdx => commentsToBuild.push({
|
||||
parent: this.containerEl,
|
||||
comment: allComments[topCommentIdx]
|
||||
})
|
||||
);
|
||||
|
||||
while (commentsToBuild.length > 0) {
|
||||
let { parent, comment } = commentsToBuild.shift();
|
||||
if (!comment.Timestamp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CommentList.Data[this.instanceId][comment.ID] = comment;
|
||||
|
||||
parent.insertAdjacentHTML("beforeend", `
|
||||
<gw-comment-card id="${this.idKey}-cmt-${comment.ID}"
|
||||
listInstance=${this.instanceId}
|
||||
commentId=${comment.ID}
|
||||
gwCommentFormId=${this.gwCommentFormId || ""}
|
||||
></gw-comment-card>
|
||||
`);
|
||||
|
||||
const commentEl = document.getElementById(`${this.idKey}-cmt-${comment.ID}`);
|
||||
(comment.ChildIdxs || []).forEach(
|
||||
childIdx => commentsToBuild.push({
|
||||
parent: commentEl.articleEl,
|
||||
comment: allComments[childIdx]
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
//Markup
|
||||
this.innerHTML = `
|
||||
<div id="${this.idKey}-container" class="comments-container"">
|
||||
</div>
|
||||
`;
|
||||
|
||||
//element properties
|
||||
this.containerEl = document.getElementById(`${this.idKey}-container`);
|
||||
}
|
||||
|
||||
//#region Handlers
|
||||
registerHandlers() {
|
||||
}
|
||||
//#endregion
|
||||
};
|
||||
customElements.define("gw-comment-list", ns.CommentList);
|
||||
|
||||
ns.CommentCard = class CommentCard extends HTMLElement {
|
||||
//#region staticProperties
|
||||
static instanceCount = 0;
|
||||
static instanceMap = {};
|
||||
//#endregion
|
||||
|
||||
//#region instance properties
|
||||
instanceId;
|
||||
isInitialized;
|
||||
|
||||
commentId;
|
||||
gwCommentFormId;
|
||||
|
||||
replyToId;
|
||||
numChildren;
|
||||
commenterName;
|
||||
datetime;
|
||||
websiteURL;
|
||||
commentText;
|
||||
|
||||
//#region element properties
|
||||
articleEl;
|
||||
replyBtn;
|
||||
//#endregion
|
||||
//#endregion
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.instanceId = CommentCard.instanceCount++;
|
||||
CommentCard.instanceMap[this.instanceId] = this;
|
||||
}
|
||||
|
||||
get idKey() {
|
||||
return `gw-comment-card-${this.instanceId}`;
|
||||
}
|
||||
|
||||
//#region HTMLElement implementation
|
||||
connectedCallback() {
|
||||
if (this.isInitialized) { return; }
|
||||
|
||||
this.commentId = this.getAttribute("commentId");
|
||||
this.gwCommentFormId = this.getAttribute("gwCommentFormId");
|
||||
|
||||
const commentData = ns.CommentList.Data[this.getAttribute("listInstance")][this.commentId];
|
||||
|
||||
this.replyToId = commentData.ResponseTo;
|
||||
this.numChildren = (commentData.ChildIdxs || []).length;
|
||||
this.commenterName = commentData["Display Name"];
|
||||
this.datetime = commentData.Timestamp;
|
||||
this.websiteURL = commentData.Website;
|
||||
this.commentText = this.parseCommentText(commentData.Comment);
|
||||
|
||||
this.renderContent();
|
||||
this.registerHandlers();
|
||||
|
||||
this.isInitialized = true;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
renderContent() {
|
||||
let headerText = this.replyToId
|
||||
? `Comment #${this.commentId} replying to #${this.replyToId}`
|
||||
: `Top level comment #${this.commentId}`;
|
||||
headerText += ` with ${this.numChildren} direct ${this.numChildren == 1 ? "reply" : "replies"}`;
|
||||
|
||||
const displayTimestamp = this.datetime.toLocaleString(
|
||||
undefined,
|
||||
{ dateStyle: "short", timeStyle: "short" }
|
||||
);
|
||||
|
||||
const commenterNameEl = this.websiteURL
|
||||
? `<a href="${this.websiteURL}" target="_blank" class="commenter-name">${this.commenterName}</a>`
|
||||
: `<span class="commenter-name">${this.commenterName}</span>`;
|
||||
|
||||
//Markup
|
||||
this.innerHTML = `
|
||||
<article id="${this.idKey}-article"
|
||||
aria-labelledby="${this.idKey}-header"
|
||||
class="comment-article"
|
||||
>
|
||||
<div id="${this.idKey}-header" class="comment-header">
|
||||
<div class="comment-id" role="img" aria-label="${headerText}">
|
||||
<span aria-hidden="true" class="comment-id">#${this.commentId}</span>
|
||||
</div>
|
||||
${commenterNameEl}
|
||||
<div class="comment-header-right">
|
||||
<time id="${this.idKey}-timestamp"
|
||||
datetime="${this.datetime.toISOString()}"
|
||||
tabindex="-1"
|
||||
>${displayTimestamp}</time>
|
||||
<button id="${this.idKey}-show" class="show-comment">Show #${this.commentId}</button>
|
||||
</div>
|
||||
</div>
|
||||
<blockquote>${this.commentText}</blockquote>
|
||||
<div class="comment-footer">
|
||||
<button id="${this.idKey}-reply">Reply to #${this.commentId}</button>
|
||||
<button id="${this.idKey}-hide">Hide #${this.commentId}</button>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
|
||||
//element properties
|
||||
this.articleEl = document.getElementById(`${this.idKey}-article`);
|
||||
this.timestamp = document.getElementById(`${this.idKey}-timestamp`);
|
||||
this.replyBtn = document.getElementById(`${this.idKey}-reply`);
|
||||
this.hideBtn = document.getElementById(`${this.idKey}-hide`);
|
||||
this.showBtn = document.getElementById(`${this.idKey}-show`);
|
||||
}
|
||||
|
||||
//#region Handlers
|
||||
registerHandlers() {
|
||||
this.replyBtn.onclick = this.onReply;
|
||||
this.hideBtn.onclick = this.onHide;
|
||||
this.showBtn.onclick = this.onShow;
|
||||
}
|
||||
|
||||
onReply = () => {
|
||||
const gwCommentForm = document.getElementById(this.gwCommentFormId);
|
||||
const respToInpt = gwCommentForm.respToInpt;
|
||||
if (!respToInpt) {
|
||||
alert("Comment form not found");
|
||||
return;
|
||||
}
|
||||
|
||||
respToInpt.value = this.commentId;
|
||||
respToInpt.focus();
|
||||
};
|
||||
|
||||
onHide = () => {
|
||||
this.classList.add("collapsed");
|
||||
this.showBtn.focus();
|
||||
};
|
||||
|
||||
onShow = () => {
|
||||
this.classList.remove("collapsed");
|
||||
this.timestamp.focus();
|
||||
};
|
||||
//#endregion
|
||||
|
||||
parseCommentText(commentString) {
|
||||
let commentText = "";
|
||||
let linkObj = {};
|
||||
|
||||
for(let i = 0; i < commentString.length; i++){
|
||||
let char = commentString.charAt(i);
|
||||
switch (char) {
|
||||
case '[':
|
||||
linkObj = {tStart: i};
|
||||
break;
|
||||
case ']':
|
||||
if(linkObj.tStart !== undefined && linkObj.tStart !== i-1) {
|
||||
linkObj.tEnd = i;
|
||||
}
|
||||
else { linkObj = {}; }
|
||||
break;
|
||||
case '(':
|
||||
if(linkObj.tEnd !== undefined && linkObj.tEnd === i-1) {
|
||||
linkObj.lStart = i;
|
||||
}
|
||||
else { linkObj = {}; }
|
||||
break;
|
||||
case ')':
|
||||
if(linkObj.lStart !== undefined && linkObj.lStart !== i-1) {
|
||||
linkObj.lEnd = i;
|
||||
}
|
||||
else { linkObj = {}; }
|
||||
break;
|
||||
}
|
||||
if(linkObj.lEnd !== undefined) {
|
||||
const linkText = commentString.substring(linkObj.tStart + 1, linkObj.tEnd);
|
||||
const linkURL = commentString.substring(linkObj.lStart + 1, linkObj.lEnd);
|
||||
commentText = commentText.substring(0, commentText.length - (i - linkObj.tStart));
|
||||
commentText += `<a href="${linkURL}" target="_blank">${linkText}</a>`;
|
||||
linkObj = {};
|
||||
}
|
||||
else {
|
||||
commentText += char;
|
||||
}
|
||||
}
|
||||
return commentText;
|
||||
}
|
||||
};
|
||||
customElements.define("gw-comment-card", ns.CommentCard);
|
||||
}) (window.GW.Controls = window.GW.Controls || {});
|
179
src/_bundle/js/googleSheetsReaderGizmo.js
Normal file
@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Author: Vera Konigin
|
||||
* Site: https://groundedwren.neocities.org
|
||||
* Contact: vera@groundedwren.com
|
||||
*
|
||||
* File Description: Gizmo for reading from Google Sheets.
|
||||
* Neocities editor users will see a lot of linter errors in this file, but none of them are real errors. The linter just doesn't understand some modern JS.
|
||||
*/
|
||||
|
||||
/**
|
||||
* By default, any JavaScript code written is defined in the global namespace, which means it's accessible directly under the "window" element.
|
||||
* If you have a lot of scripts, this can lead to clutter and naming collisions (what if two different scripts use a variable called "i"? They can inadvertently mess each other up).
|
||||
* To get around this, we define the registerNamespace function in the global namespace, which just confines all the code in the function passed to it to a property under window.
|
||||
* That property is represented as the "path" parameter. It is passed to the function for ease of access.
|
||||
*/
|
||||
function registerNamespace(path, nsFunc)
|
||||
{
|
||||
var ancestors = path.split(".");
|
||||
|
||||
var ns = window;
|
||||
for(var i = 0; i < ancestors.length; i++)
|
||||
{
|
||||
ns[ancestors[i]] = ns[ancestors[i]] || {};
|
||||
ns = ns[ancestors[i]];
|
||||
}
|
||||
nsFunc(ns);
|
||||
}
|
||||
|
||||
registerNamespace("GW.Gizmos", function(ns) {
|
||||
/**
|
||||
* A class to read from one page in a google sheet document
|
||||
*/
|
||||
ns.GoogleSheetsReader = class GoogleSheetsReader
|
||||
{
|
||||
//setResponse is intended for React - it's how google responds to our GET. Fortunately valid JSON is inside this call, so we can just parse it out.
|
||||
static #RESPONSE_PREFIX = "setResponse(";
|
||||
static #RESPONSE_SUFFIX = ");";
|
||||
|
||||
//Here we can define any custom types based on column label. "Timestamp" is intended for ISO 8601 format date/time strings.
|
||||
static #CUSTOM_LABEL_TYPES = {
|
||||
"Timestamp": "timestamp"
|
||||
};
|
||||
|
||||
spreadsheetId; //The ID of the spreadsheet. This is the part just after /d/ in the docs.google.com URL
|
||||
sheetName; //The name of the particular sheet we're after
|
||||
|
||||
#sheetURL; //Composed request URL
|
||||
|
||||
loadPromise = null; //A promise created when loading begins and which resolves when data has finished loading.
|
||||
tableJSON = null; //Raw JS Object version of the returned JSON.
|
||||
rowData = null; //Parsed row data
|
||||
colData = null; //A shortcut to the the column data in tableJSON
|
||||
colIndex = null; //An index from column label to its metadata, plus array position
|
||||
|
||||
/**
|
||||
* Constructs a GoogleSheetsReader object
|
||||
* spreadsheetId is the part of the docs.google.com URL just after /d/
|
||||
* sheetName is the name of the particular page
|
||||
*/
|
||||
constructor(spreadsheetId, sheetName)
|
||||
{
|
||||
this.spreadsheetId = spreadsheetId;
|
||||
this.sheetName = sheetName;
|
||||
this.#sheetURL = `https://docs.google.com/spreadsheets/d/${spreadsheetId}/gviz/tq?sheet=${sheetName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and parses sheet data via HTTP GET
|
||||
* Returns null on success, and an error string on failure.
|
||||
*/
|
||||
async loadData()
|
||||
{
|
||||
this.loadPromise = this.#loadData();
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
async #loadData()
|
||||
{
|
||||
this.tableJSON = null;
|
||||
this.rowData = null;
|
||||
this.colData = null;
|
||||
this.colIndex = null;
|
||||
|
||||
const response = await fetch(this.#sheetURL);
|
||||
if (response.ok)
|
||||
{
|
||||
return response.text().then((unparsedData) =>
|
||||
{
|
||||
//This is parsing out the valid JSON from the React method they gave us
|
||||
const targetData = unparsedData.split(
|
||||
GoogleSheetsReader.#RESPONSE_PREFIX
|
||||
)[1].split(
|
||||
GoogleSheetsReader.#RESPONSE_SUFFIX
|
||||
)[0];
|
||||
|
||||
this.tableJSON = GoogleSheetsReader.#applyCustomLabelTypes(JSON.parse(targetData).table);
|
||||
this.rowData = GoogleSheetsReader.#parseAllRows(this.tableJSON);
|
||||
this.colIndex = GoogleSheetsReader.#indexColumns(this.tableJSON);
|
||||
this.colData = this.tableJSON.cols;
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
return response.statusText || response.status;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides any google-returned column data types with custom ones based on label
|
||||
*/
|
||||
static #applyCustomLabelTypes(tableJSON)
|
||||
{
|
||||
tableJSON.cols.forEach(col => {
|
||||
if(this.#CUSTOM_LABEL_TYPES[col.label])
|
||||
{
|
||||
col.type = this.#CUSTOM_LABEL_TYPES[col.label]
|
||||
};
|
||||
});
|
||||
return tableJSON;
|
||||
}
|
||||
|
||||
static #parseAllRows(tableJSON)
|
||||
{
|
||||
const rowDataArray = [];
|
||||
for(let i = 0; i < tableJSON.rows.length; i++)
|
||||
{
|
||||
rowDataArray.push(this.#parseRow(i, tableJSON));
|
||||
}
|
||||
return rowDataArray;
|
||||
}
|
||||
|
||||
static #parseRow(rowIdx, tableJSON)
|
||||
{
|
||||
const rowData = {};
|
||||
const cells = tableJSON.rows[rowIdx].c;
|
||||
|
||||
for(let i = 0; i < cells.length; i++)
|
||||
{
|
||||
rowData[tableJSON.cols[i].label] = this.#parseCellType(cells[i], tableJSON.cols[i].type);
|
||||
}
|
||||
return rowData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a cell based on its type. Further custom types will need their own parsing added here.
|
||||
*/
|
||||
static #parseCellType(cellData, cellType)
|
||||
{
|
||||
switch (cellType)
|
||||
{
|
||||
case "string":
|
||||
return cellData ? cellData.v : cellData;
|
||||
case "number":
|
||||
return cellData ? cellData.v : cellData;
|
||||
case "datetime":
|
||||
case "date":
|
||||
return (cellData && cellData.v) ? eval("new " + cellData.v) : null;
|
||||
case "timestamp":
|
||||
const cellTimestamp = new Date(cellData ? cellData.v : "");
|
||||
return isNaN(cellTimestamp) ? null : cellTimestamp;
|
||||
default:
|
||||
return cellData;
|
||||
}
|
||||
}
|
||||
|
||||
static #indexColumns(tableJSON)
|
||||
{
|
||||
const colIndex = {};
|
||||
for(let i = 0; i < tableJSON.cols.length; i++)
|
||||
{
|
||||
const colData = tableJSON.cols[i];
|
||||
colIndex[colData.label] = {...colData, index: i};
|
||||
}
|
||||
return colIndex;
|
||||
}
|
||||
};
|
||||
});
|
453
src/_bundle/js/svgIconControl.js
Normal file
@ -0,0 +1,453 @@
|
||||
/**
|
||||
* Author: Vera Konigin
|
||||
* Site: https://groundedwren.neocities.org
|
||||
* Contact: vera@groundedwren.com
|
||||
*
|
||||
* File Description: A web component to site SVG icons easily
|
||||
*/
|
||||
|
||||
/**
|
||||
* By default, any JavaScript code written is defined in the global namespace, which means it's accessible directly under the "window" element.
|
||||
* If you have a lot of scripts, this can lead to clutter and naming collisions (what if two different scripts use a variable called "i"? They can inadvertently mess each other up).
|
||||
* To get around this, we define the registerNamespace function in the global namespace, which just confines all the code in the function passed to it to a property under window.
|
||||
* That property is represented as the "path" parameter. It is passed to the function for ease of access.
|
||||
*/
|
||||
function registerNamespace(path, nsFunc)
|
||||
{
|
||||
var ancestors = path.split(".");
|
||||
|
||||
var ns = window;
|
||||
for(var i = 0; i < ancestors.length; i++)
|
||||
{
|
||||
ns[ancestors[i]] = ns[ancestors[i]] || {};
|
||||
ns = ns[ancestors[i]];
|
||||
}
|
||||
nsFunc(ns);
|
||||
}
|
||||
|
||||
registerNamespace("GW.Controls.SVGLib", function(ns) {
|
||||
const XML_NAMESPACE = "http://www.w3.org/2000/svg";
|
||||
|
||||
/**
|
||||
* https://www.w3.org/TR/SVG/eltindex.html
|
||||
*/
|
||||
ns.ElementTypes = {
|
||||
svg: "svg", //viewBox, preserveAspectRatio, zoomAndPan, transform - https://www.w3.org/TR/SVG/struct.html#SVGElement
|
||||
circle: "circle", //cx, cy, r - https://www.w3.org/TR/SVG/shapes.html#CircleElement
|
||||
linearGradient: "linearGradient", //x1, y1, x2, y2, gradientUnits, gradientTransform, spreadMethod, href - https://www.w3.org/TR/SVG/pservers.html#LinearGradientElement
|
||||
stop: "stop", //offset, stop-color https://www.w3.org/TR/SVG/pservers.html#StopElement
|
||||
defs: "defs", //https://www.w3.org/TR/SVG/struct.html#DefsElement
|
||||
rect: "rect", //x, y, width, height, rx, ry - https://www.w3.org/TR/SVG/shapes.html#RectElement
|
||||
text: "text", //x, y, dominant-baseline, text-anchor, fill - https://www.w3.org/TR/SVG/text.html#TextElement
|
||||
a: "a", //href, target - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/a
|
||||
path: "path", //d, pathLength - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path
|
||||
title: "title", // - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title
|
||||
desc: "desc", // - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/desc
|
||||
foreignObject: "foreignObject", // - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut to create an SVG element and append it to a parent
|
||||
* @param parent the parent element
|
||||
* ... see ns.createElement
|
||||
*/
|
||||
function createChildElement(parent, elementType, attributes, innerHTML)
|
||||
{
|
||||
var childEl = createElement(elementType, attributes, innerHTML);
|
||||
parent.appendChild(childEl);
|
||||
return childEl;
|
||||
}
|
||||
ns.createChildElement = createChildElement;
|
||||
|
||||
/**
|
||||
* Creates an SVG element to spec
|
||||
* @param elementType type of SVG element (see ns.ElementTypes)
|
||||
* @param attributes object of attributes and values
|
||||
* @param innerHTML inner HTML
|
||||
*/
|
||||
function createElement(elementType, attributes, innerHTML)
|
||||
{
|
||||
var el = document.createElementNS(XML_NAMESPACE, elementType);
|
||||
Object.keys(attributes || {}).forEach((attr) => el.setAttributeNS(null, attr, attributes[attr]));
|
||||
el.innerHTML = innerHTML || null;
|
||||
return el;
|
||||
}
|
||||
ns.createElement = createElement;
|
||||
|
||||
//#region Icons
|
||||
const ICON_CITATION = "<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->";
|
||||
const ICON_CLASS = "icon";
|
||||
|
||||
/**
|
||||
* Icon SVG path definitions
|
||||
*/
|
||||
ns.Icons = {
|
||||
"circle-info": {
|
||||
title: "info",
|
||||
viewBox: "0 0 512 512",
|
||||
d: "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
|
||||
},
|
||||
"play": {
|
||||
title: "play",
|
||||
viewBox: "0 0 384 512",
|
||||
d: "M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"
|
||||
},
|
||||
"backward-step": {
|
||||
title: "backward step",
|
||||
viewBox: "0 0 320 512",
|
||||
d: "M267.5 440.6c9.5 7.9 22.8 9.7 34.1 4.4s18.4-16.6 18.4-29V96c0-12.4-7.2-23.7-18.4-29s-24.5-3.6-34.1 4.4l-192 160L64 241V96c0-17.7-14.3-32-32-32S0 78.3 0 96V416c0 17.7 14.3 32 32 32s32-14.3 32-32V271l11.5 9.6 192 160z"
|
||||
},
|
||||
"forward-step": {
|
||||
title: "forward step",
|
||||
viewBox: "0 0 320 512",
|
||||
d: "M52.5 440.6c-9.5 7.9-22.8 9.7-34.1 4.4S0 428.4 0 416V96C0 83.6 7.2 72.3 18.4 67s24.5-3.6 34.1 4.4l192 160L256 241V96c0-17.7 14.3-32 32-32s32 14.3 32 32V416c0 17.7-14.3 32-32 32s-32-14.3-32-32V271l-11.5 9.6-192 160z"
|
||||
},
|
||||
"pause": {
|
||||
title: "pause",
|
||||
viewBox: "0 0 320 512",
|
||||
d: "M48 64C21.5 64 0 85.5 0 112V400c0 26.5 21.5 48 48 48H80c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H48zm192 0c-26.5 0-48 21.5-48 48V400c0 26.5 21.5 48 48 48h32c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H240z"
|
||||
},
|
||||
"triangle-exclamation": {
|
||||
title: "warning",
|
||||
viewBox: "0 0 512 512",
|
||||
d: "M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
||||
},
|
||||
"laptop-code": {
|
||||
title: "coding",
|
||||
viewBox: "0 0 640 512",
|
||||
d: "M64 96c0-35.3 28.7-64 64-64H512c35.3 0 64 28.7 64 64V352H512V96H128V352H64V96zM0 403.2C0 392.6 8.6 384 19.2 384H620.8c10.6 0 19.2 8.6 19.2 19.2c0 42.4-34.4 76.8-76.8 76.8H76.8C34.4 480 0 445.6 0 403.2zM281 209l-31 31 31 31c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-48-48c-9.4-9.4-9.4-24.6 0-33.9l48-48c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9zM393 175l48 48c9.4 9.4 9.4 24.6 0 33.9l-48 48c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l31-31-31-31c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0z"
|
||||
},
|
||||
"guitar": {
|
||||
title: "guitar",
|
||||
viewBox: "0 0 512 512",
|
||||
d: "M465 7c-9.4-9.4-24.6-9.4-33.9 0L383 55c-2.4 2.4-4.3 5.3-5.5 8.5l-15.4 41-77.5 77.6c-45.1-29.4-99.3-30.2-131 1.6c-11 11-18 24.6-21.4 39.6c-3.7 16.6-19.1 30.7-36.1 31.6c-25.6 1.3-49.3 10.7-67.3 28.6C-16 328.4-7.6 409.4 47.5 464.5s136.1 63.5 180.9 18.7c17.9-17.9 27.4-41.7 28.6-67.3c.9-17 15-32.3 31.6-36.1c15-3.4 28.6-10.5 39.6-21.4c31.8-31.8 31-85.9 1.6-131l77.6-77.6 41-15.4c3.2-1.2 6.1-3.1 8.5-5.5l48-48c9.4-9.4 9.4-24.6 0-33.9L465 7zM208 256a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"
|
||||
},
|
||||
"pen-fancy": {
|
||||
title: "pen",
|
||||
viewBox: "0 0 512 512",
|
||||
d: "M373.5 27.1C388.5 9.9 410.2 0 433 0c43.6 0 79 35.4 79 79c0 22.8-9.9 44.6-27.1 59.6L277.7 319l-10.3-10.3-64-64L193 234.3 373.5 27.1zM170.3 256.9l10.4 10.4 64 64 10.4 10.4-19.2 83.4c-3.9 17.1-16.9 30.7-33.8 35.4L24.4 510.3l95.4-95.4c2.6 .7 5.4 1.1 8.3 1.1c17.7 0 32-14.3 32-32s-14.3-32-32-32s-32 14.3-32 32c0 2.9 .4 5.6 1.1 8.3L1.7 487.6 51.5 310c4.7-16.9 18.3-29.9 35.4-33.8l83.4-19.2z"
|
||||
},
|
||||
"glasses": {
|
||||
title: "glasses",
|
||||
viewBox: "0 0 576 512",
|
||||
d: "M118.6 80c-11.5 0-21.4 7.9-24 19.1L57 260.3c20.5-6.2 48.3-12.3 78.7-12.3c32.3 0 61.8 6.9 82.8 13.5c10.6 3.3 19.3 6.7 25.4 9.2c3.1 1.3 5.5 2.4 7.3 3.2c.9 .4 1.6 .7 2.1 1l.6 .3 .2 .1 .1 0 0 0 0 0s0 0-6.3 12.7h0l6.3-12.7c5.8 2.9 10.4 7.3 13.5 12.7h40.6c3.1-5.3 7.7-9.8 13.5-12.7l6.3 12.7h0c-6.3-12.7-6.3-12.7-6.3-12.7l0 0 0 0 .1 0 .2-.1 .6-.3c.5-.2 1.2-.6 2.1-1c1.8-.8 4.2-1.9 7.3-3.2c6.1-2.6 14.8-5.9 25.4-9.2c21-6.6 50.4-13.5 82.8-13.5c30.4 0 58.2 6.1 78.7 12.3L481.4 99.1c-2.6-11.2-12.6-19.1-24-19.1c-3.1 0-6.2 .6-9.2 1.8L416.9 94.3c-12.3 4.9-26.3-1.1-31.2-13.4s1.1-26.3 13.4-31.2l31.3-12.5c8.6-3.4 17.7-5.2 27-5.2c33.8 0 63.1 23.3 70.8 56.2l43.9 188c1.7 7.3 2.9 14.7 3.5 22.1c.3 1.9 .5 3.8 .5 5.7v6.7V352v16c0 61.9-50.1 112-112 112H419.7c-59.4 0-108.5-46.4-111.8-105.8L306.6 352H269.4l-1.2 22.2C264.9 433.6 215.8 480 156.3 480H112C50.1 480 0 429.9 0 368V352 310.7 304c0-1.9 .2-3.8 .5-5.7c.6-7.4 1.8-14.8 3.5-22.1l43.9-188C55.5 55.3 84.8 32 118.6 32c9.2 0 18.4 1.8 27 5.2l31.3 12.5c12.3 4.9 18.3 18.9 13.4 31.2s-18.9 18.3-31.2 13.4L127.8 81.8c-2.9-1.2-6-1.8-9.2-1.8zM64 325.4V368c0 26.5 21.5 48 48 48h44.3c25.5 0 46.5-19.9 47.9-45.3l2.5-45.6c-2.3-.8-4.9-1.7-7.5-2.5c-17.2-5.4-39.9-10.5-63.6-10.5c-23.7 0-46.2 5.1-63.2 10.5c-3.1 1-5.9 1.9-8.5 2.9zM512 368V325.4c-2.6-.9-5.5-1.9-8.5-2.9c-17-5.4-39.5-10.5-63.2-10.5c-23.7 0-46.4 5.1-63.6 10.5c-2.7 .8-5.2 1.7-7.5 2.5l2.5 45.6c1.4 25.4 22.5 45.3 47.9 45.3H464c26.5 0 48-21.5 48-48z"
|
||||
},
|
||||
"clock": {
|
||||
title: "glasses",
|
||||
viewBox: "0 0 512 512",
|
||||
d: "M256 0a256 256 0 1 1 0 512A256 256 0 1 1 256 0zM232 120V256c0 8 4 15.5 10.7 20l96 64c11 7.4 25.9 4.4 33.3-6.7s4.4-25.9-6.7-33.3L280 243.2V120c0-13.3-10.7-24-24-24s-24 10.7-24 24z"
|
||||
},
|
||||
"envelope": {
|
||||
title: "envelope",
|
||||
viewBox: "0 0 512 512",
|
||||
d: "M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"
|
||||
},
|
||||
"discord": {
|
||||
title: "discord",
|
||||
viewBox: "0 0 640 512",
|
||||
d: "M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"
|
||||
},
|
||||
"plus": {
|
||||
title: "plus",
|
||||
viewBox: "0 0 448 512",
|
||||
d: "M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32V224H48c-17.7 0-32 14.3-32 32s14.3 32 32 32H192V432c0 17.7 14.3 32 32 32s32-14.3 32-32V288H400c17.7 0 32-14.3 32-32s-14.3-32-32-32H256V80z"
|
||||
},
|
||||
"circle-check": {
|
||||
title: "done",
|
||||
viewBox: "0 0 512 512",
|
||||
d: "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"
|
||||
},
|
||||
"delete-left": {
|
||||
title: "delete",
|
||||
viewBox: "0 0 576 512",
|
||||
d: "M576 128c0-35.3-28.7-64-64-64H205.3c-17 0-33.3 6.7-45.3 18.7L9.4 233.4c-6 6-9.4 14.1-9.4 22.6s3.4 16.6 9.4 22.6L160 429.3c12 12 28.3 18.7 45.3 18.7H512c35.3 0 64-28.7 64-64V128zM271 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z"
|
||||
},
|
||||
"link": {
|
||||
title: "link",
|
||||
viewBox: "0 0 640 512",
|
||||
d: "M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"
|
||||
},
|
||||
"xmark": {
|
||||
title: "close",
|
||||
viewBox: "0 0 384 512",
|
||||
d: "M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"
|
||||
},
|
||||
"trash": {
|
||||
title: "trash",
|
||||
viewBox: "0 0 448 512",
|
||||
d: "M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"
|
||||
},
|
||||
"thumbtack": {
|
||||
title: "trash",
|
||||
viewBox: "0 0 384 512",
|
||||
d: "M32 32C32 14.3 46.3 0 64 0H320c17.7 0 32 14.3 32 32s-14.3 32-32 32H290.5l11.4 148.2c36.7 19.9 65.7 53.2 79.5 94.7l1 3c3.3 9.8 1.6 20.5-4.4 28.8s-15.7 13.3-26 13.3H32c-10.3 0-19.9-4.9-26-13.3s-7.7-19.1-4.4-28.8l1-3c13.8-41.5 42.8-74.8 79.5-94.7L93.5 64H64C46.3 64 32 49.7 32 32zM160 384h64v96c0 17.7-14.3 32-32 32s-32-14.3-32-32V384z"
|
||||
},
|
||||
"pen-to-square": {
|
||||
title: "edit",
|
||||
viewBox: "0 0 512 512",
|
||||
d: "M471.6 21.7c-21.9-21.9-57.3-21.9-79.2 0L362.3 51.7l97.9 97.9 30.1-30.1c21.9-21.9 21.9-57.3 0-79.2L471.6 21.7zm-299.2 220c-6.1 6.1-10.8 13.6-13.5 21.9l-29.6 88.8c-2.9 8.6-.6 18.1 5.8 24.6s15.9 8.7 24.6 5.8l88.8-29.6c8.2-2.7 15.7-7.4 21.9-13.5L437.7 172.3 339.7 74.3 172.4 241.7zM96 64C43 64 0 107 0 160V416c0 53 43 96 96 96H352c53 0 96-43 96-96V320c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H96z"
|
||||
},
|
||||
"book": {
|
||||
title: "book",
|
||||
viewBox: "0 0 448 512",
|
||||
d: "M96 0C43 0 0 43 0 96V416c0 53 43 96 96 96H384h32c17.7 0 32-14.3 32-32s-14.3-32-32-32V384c17.7 0 32-14.3 32-32V32c0-17.7-14.3-32-32-32H384 96zm0 384H352v64H96c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16zm16 48H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16s7.2-16 16-16z"
|
||||
},
|
||||
"gamepad": {
|
||||
title: "game",
|
||||
viewBox: "0 0 640 512",
|
||||
d: "M192 64C86 64 0 150 0 256S86 448 192 448H448c106 0 192-86 192-192s-86-192-192-192H192zM496 168a40 40 0 1 1 0 80 40 40 0 1 1 0-80zM392 304a40 40 0 1 1 80 0 40 40 0 1 1 -80 0zM168 200c0-13.3 10.7-24 24-24s24 10.7 24 24v32h32c13.3 0 24 10.7 24 24s-10.7 24-24 24H216v32c0 13.3-10.7 24-24 24s-24-10.7-24-24V280H136c-13.3 0-24-10.7-24-24s10.7-24 24-24h32V200z"
|
||||
},
|
||||
"hammer": {
|
||||
title: "game",
|
||||
viewBox: "0 0 576 512",
|
||||
d: "M413.5 237.5c-28.2 4.8-58.2-3.6-80-25.4l-38.1-38.1C280.4 159 272 138.8 272 117.6V105.5L192.3 62c-5.3-2.9-8.6-8.6-8.3-14.7s3.9-11.5 9.5-14l47.2-21C259.1 4.2 279 0 299.2 0h18.1c36.7 0 72 14 98.7 39.1l44.6 42c24.2 22.8 33.2 55.7 26.6 86L503 183l8-8c9.4-9.4 24.6-9.4 33.9 0l24 24c9.4 9.4 9.4 24.6 0 33.9l-88 88c-9.4 9.4-24.6 9.4-33.9 0l-24-24c-9.4-9.4-9.4-24.6 0-33.9l8-8-17.5-17.5zM27.4 377.1L260.9 182.6c3.5 4.9 7.5 9.6 11.8 14l38.1 38.1c6 6 12.4 11.2 19.2 15.7L134.9 484.6c-14.5 17.4-36 27.4-58.6 27.4C34.1 512 0 477.8 0 435.7c0-22.6 10.1-44.1 27.4-58.6z"
|
||||
},
|
||||
"location-dot": {
|
||||
title: "location",
|
||||
viewBox: "0 0 384 512",
|
||||
d: "M215.7 499.2C267 435 384 279.4 384 192C384 86 298 0 192 0S0 86 0 192c0 87.4 117 243 168.3 307.2c12.3 15.3 35.1 15.3 47.4 0zM192 128a64 64 0 1 1 0 128 64 64 0 1 1 0-128z"
|
||||
},
|
||||
"box-open": {
|
||||
title: "box",
|
||||
viewBox: "0 0 640 512",
|
||||
d: "M58.9 42.1c3-6.1 9.6-9.6 16.3-8.7L320 64 564.8 33.4c6.7-.8 13.3 2.7 16.3 8.7l41.7 83.4c9 17.9-.6 39.6-19.8 45.1L439.6 217.3c-13.9 4-28.8-1.9-36.2-14.3L320 64 236.6 203c-7.4 12.4-22.3 18.3-36.2 14.3L37.1 170.6c-19.3-5.5-28.8-27.2-19.8-45.1L58.9 42.1zM321.1 128l54.9 91.4c14.9 24.8 44.6 36.6 72.5 28.6L576 211.6v167c0 22-15 41.2-36.4 46.6l-204.1 51c-10.2 2.6-20.9 2.6-31 0l-204.1-51C79 419.7 64 400.5 64 378.5v-167L191.6 248c27.8 8 57.6-3.8 72.5-28.6L318.9 128h2.2z"
|
||||
},
|
||||
"calendar": {
|
||||
title: "box",
|
||||
viewBox: "0 0 448 512",
|
||||
d: "M128 0c17.7 0 32 14.3 32 32V64H288V32c0-17.7 14.3-32 32-32s32 14.3 32 32V64h48c26.5 0 48 21.5 48 48v48H0V112C0 85.5 21.5 64 48 64H96V32c0-17.7 14.3-32 32-32zM0 192H448V464c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V192zm64 80v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H80c-8.8 0-16 7.2-16 16zm128 0v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H208c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H336zM64 400v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H80c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H208zm112 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H336c-8.8 0-16 7.2-16 16z"
|
||||
},
|
||||
"people-group": {
|
||||
title: "people",
|
||||
viewBox: "0 0 640 512",
|
||||
d: "M72 88a56 56 0 1 1 112 0A56 56 0 1 1 72 88zM64 245.7C54 256.9 48 271.8 48 288s6 31.1 16 42.3V245.7zm144.4-49.3C178.7 222.7 160 261.2 160 304c0 34.3 12 65.8 32 90.5V416c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V389.2C26.2 371.2 0 332.7 0 288c0-61.9 50.1-112 112-112h32c24 0 46.2 7.5 64.4 20.3zM448 416V394.5c20-24.7 32-56.2 32-90.5c0-42.8-18.7-81.3-48.4-107.7C449.8 183.5 472 176 496 176h32c61.9 0 112 50.1 112 112c0 44.7-26.2 83.2-64 101.2V416c0 17.7-14.3 32-32 32H480c-17.7 0-32-14.3-32-32zm8-328a56 56 0 1 1 112 0A56 56 0 1 1 456 88zM576 245.7v84.7c10-11.3 16-26.1 16-42.3s-6-31.1-16-42.3zM320 32a64 64 0 1 1 0 128 64 64 0 1 1 0-128zM240 304c0 16.2 6 31 16 42.3V261.7c-10 11.3-16 26.1-16 42.3zm144-42.3v84.7c10-11.3 16-26.1 16-42.3s-6-31.1-16-42.3zM448 304c0 44.7-26.2 83.2-64 101.2V448c0 17.7-14.3 32-32 32H288c-17.7 0-32-14.3-32-32V405.2c-37.8-18-64-56.5-64-101.2c0-61.9 50.1-112 112-112h32c61.9 0 112 50.1 112 112z"
|
||||
},
|
||||
"comments": {
|
||||
title: "comments",
|
||||
viewBox: "0 0 640 512",
|
||||
d: "M208 352c114.9 0 208-78.8 208-176S322.9 0 208 0S0 78.8 0 176c0 38.6 14.7 74.3 39.6 103.4c-3.5 9.4-8.7 17.7-14.2 24.7c-4.8 6.2-9.7 11-13.3 14.3c-1.8 1.6-3.3 2.9-4.3 3.7c-.5 .4-.9 .7-1.1 .8l-.2 .2 0 0 0 0C1 327.2-1.4 334.4 .8 340.9S9.1 352 16 352c21.8 0 43.8-5.6 62.1-12.5c9.2-3.5 17.8-7.4 25.3-11.4C134.1 343.3 169.8 352 208 352zM448 176c0 112.3-99.1 196.9-216.5 207C255.8 457.4 336.4 512 432 512c38.2 0 73.9-8.7 104.7-23.9c7.5 4 16 7.9 25.2 11.4c18.3 6.9 40.3 12.5 62.1 12.5c6.9 0 13.1-4.5 15.2-11.1c2.1-6.6-.2-13.8-5.8-17.9l0 0 0 0-.2-.2c-.2-.2-.6-.4-1.1-.8c-1-.8-2.5-2-4.3-3.7c-3.6-3.3-8.5-8.1-13.3-14.3c-5.5-7-10.7-15.4-14.2-24.7c24.9-29 39.6-64.7 39.6-103.4c0-92.8-84.9-168.9-192.6-175.5c.4 5.1 .6 10.3 .6 15.5z"
|
||||
},
|
||||
"clipboard-check": {
|
||||
title: "clipboard",
|
||||
viewBox: "0 0 384 512",
|
||||
d: "M192 0c-41.8 0-77.4 26.7-90.5 64H64C28.7 64 0 92.7 0 128V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64H282.5C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM305 273L177 401c-9.4 9.4-24.6 9.4-33.9 0L79 337c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L271 239c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"
|
||||
},
|
||||
"user": {
|
||||
title: "clipboard",
|
||||
viewBox: "0 0 448 512",
|
||||
d: "M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"
|
||||
},
|
||||
"door-open": {
|
||||
title: "open door",
|
||||
viewBox: "0 0 576 512",
|
||||
d: "M320 32c0-9.9-4.5-19.2-12.3-25.2S289.8-1.4 280.2 1l-179.9 45C79 51.3 64 70.5 64 92.5V448H32c-17.7 0-32 14.3-32 32s14.3 32 32 32H96 288h32V480 32zM256 256c0 17.7-10.7 32-24 32s-24-14.3-24-32s10.7-32 24-32s24 14.3 24 32zm96-128h96V480c0 17.7 14.3 32 32 32h64c17.7 0 32-14.3 32-32s-14.3-32-32-32H512V128c0-35.3-28.7-64-64-64H352v64z"
|
||||
},
|
||||
"scroll": {
|
||||
title: "scroll",
|
||||
viewBox: "0 0 576 512",
|
||||
d: "M0 80v48c0 17.7 14.3 32 32 32H48 96V80c0-26.5-21.5-48-48-48S0 53.5 0 80zM112 32c10 13.4 16 30 16 48V384c0 35.3 28.7 64 64 64s64-28.7 64-64v-5.3c0-32.4 26.3-58.7 58.7-58.7H480V128c0-53-43-96-96-96H112zM464 480c61.9 0 112-50.1 112-112c0-8.8-7.2-16-16-16H314.7c-14.7 0-26.7 11.9-26.7 26.7V384c0 53-43 96-96 96H368h96z"
|
||||
},
|
||||
"star": {
|
||||
title: "star",
|
||||
viewBox: "0 0 576 512",
|
||||
d: "M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"
|
||||
},
|
||||
"boxes-stacked": {
|
||||
title: "stacked boxes",
|
||||
viewBox: "0 0 576 512",
|
||||
d: "M248 0H208c-26.5 0-48 21.5-48 48V160c0 35.3 28.7 64 64 64H352c35.3 0 64-28.7 64-64V48c0-26.5-21.5-48-48-48H328V80c0 8.8-7.2 16-16 16H264c-8.8 0-16-7.2-16-16V0zM64 256c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H224c35.3 0 64-28.7 64-64V320c0-35.3-28.7-64-64-64H184v80c0 8.8-7.2 16-16 16H120c-8.8 0-16-7.2-16-16V256H64zM352 512H512c35.3 0 64-28.7 64-64V320c0-35.3-28.7-64-64-64H472v80c0 8.8-7.2 16-16 16H408c-8.8 0-16-7.2-16-16V256H352c-15 0-28.8 5.1-39.7 13.8c4.9 10.4 7.7 22 7.7 34.2V464c0 12.2-2.8 23.8-7.7 34.2C323.2 506.9 337 512 352 512z"
|
||||
},
|
||||
"person-running": {
|
||||
title: "person running",
|
||||
viewBox: "0 0 448 512",
|
||||
d: "M320 48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM125.7 175.5c9.9-9.9 23.4-15.5 37.5-15.5c1.9 0 3.8 .1 5.6 .3L137.6 254c-9.3 28 1.7 58.8 26.8 74.5l86.2 53.9-25.4 88.8c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l28.7-100.4c5.9-20.6-2.6-42.6-20.7-53.9L238 299l30.9-82.4 5.1 12.3C289 264.7 323.9 288 362.7 288H384c17.7 0 32-14.3 32-32s-14.3-32-32-32H362.7c-12.9 0-24.6-7.8-29.5-19.7l-6.3-15c-14.6-35.1-44.1-61.9-80.5-73.1l-48.7-15c-11.1-3.4-22.7-5.2-34.4-5.2c-31 0-60.8 12.3-82.7 34.3L57.4 153.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l23.1-23.1zM91.2 352H32c-17.7 0-32 14.3-32 32s14.3 32 32 32h69.6c19 0 36.2-11.2 43.9-28.5L157 361.6l-9.5-6c-17.5-10.9-30.5-26.8-37.9-44.9L91.2 352z"
|
||||
},
|
||||
"handshake": {
|
||||
title: "handshake",
|
||||
viewBox: "0 0 640 512",
|
||||
d: "M323.4 85.2l-96.8 78.4c-16.1 13-19.2 36.4-7 53.1c12.9 17.8 38 21.3 55.3 7.8l99.3-77.2c7-5.4 17-4.2 22.5 2.8s4.2 17-2.8 22.5l-20.9 16.2L550.2 352H592c26.5 0 48-21.5 48-48V176c0-26.5-21.5-48-48-48H516h-4-.7l-3.9-2.5L434.8 79c-15.3-9.8-33.2-15-51.4-15c-21.8 0-43 7.5-60 21.2zm22.8 124.4l-51.7 40.2C263 274.4 217.3 268 193.7 235.6c-22.2-30.5-16.6-73.1 12.7-96.8l83.2-67.3c-11.6-4.9-24.1-7.4-36.8-7.4C234 64 215.7 69.6 200 80l-72 48H48c-26.5 0-48 21.5-48 48V304c0 26.5 21.5 48 48 48H156.2l91.4 83.4c19.6 17.9 49.9 16.5 67.8-3.1c5.5-6.1 9.2-13.2 11.1-20.6l17 15.6c19.5 17.9 49.9 16.6 67.8-2.9c4.5-4.9 7.8-10.6 9.9-16.5c19.4 13 45.8 10.3 62.1-7.5c17.9-19.5 16.6-49.9-2.9-67.8l-134.2-123z"
|
||||
},
|
||||
"reply": {
|
||||
title: "reply",
|
||||
viewBox: "0 0 512 512",
|
||||
d: "M205 34.8c11.5 5.1 19 16.6 19 29.2v64H336c97.2 0 176 78.8 176 176c0 113.3-81.5 163.9-100.2 174.1c-2.5 1.4-5.3 1.9-8.1 1.9c-10.9 0-19.7-8.9-19.7-19.7c0-7.5 4.3-14.4 9.8-19.5c9.4-8.8 22.2-26.4 22.2-56.7c0-53-43-96-96-96H224v64c0 12.6-7.4 24.1-19 29.2s-25 3-34.4-5.4l-160-144C3.9 225.7 0 217.1 0 208s3.9-17.7 10.6-23.8l160-144c9.4-8.5 22.9-10.6 34.4-5.4z"
|
||||
},
|
||||
"d20": {
|
||||
title: "d20",
|
||||
viewBox: "0 0 512 512",
|
||||
d: "M48.7 125.8l53.2 31.9c7.8 4.7 17.8 2 22.2-5.9L201.6 12.1c3-5.4-.9-12.1-7.1-12.1c-1.6 0-3.2 .5-4.6 1.4L47.9 98.8c-9.6 6.6-9.2 20.9 .8 26.9zM16 171.7V295.3c0 8 10.4 11 14.7 4.4l60-92c5-7.6 2.6-17.8-5.2-22.5L40.2 158C29.6 151.6 16 159.3 16 171.7zM310.4 12.1l77.6 139.6c4.4 7.9 14.5 10.6 22.2 5.9l53.2-31.9c10-6 10.4-20.3 .8-26.9L322.1 1.4c-1.4-.9-3-1.4-4.6-1.4c-6.2 0-10.1 6.7-7.1 12.1zM496 171.7c0-12.4-13.6-20.1-24.2-13.7l-45.3 27.2c-7.8 4.7-10.1 14.9-5.2 22.5l60 92c4.3 6.7 14.7 3.6 14.7-4.4V171.7zm-49.3 246L286.1 436.6c-8.1 .9-14.1 7.8-14.1 15.9v52.8c0 3.7 3 6.8 6.8 6.8c.8 0 1.6-.1 2.4-.4l172.7-64c6.1-2.2 10.1-8 10.1-14.5c0-9.3-8.1-16.5-17.3-15.4zM233.2 512c3.7 0 6.8-3 6.8-6.8V452.6c0-8.1-6.1-14.9-14.1-15.9l-160.6-19c-9.2-1.1-17.3 6.1-17.3 15.4c0 6.5 4 12.3 10.1 14.5l172.7 64c.8 .3 1.6 .4 2.4 .4zM41.7 382.9l170.9 20.2c7.8 .9 13.4-7.5 9.5-14.3l-85.7-150c-5.9-10.4-20.7-10.8-27.3-.8L30.2 358.2c-6.5 9.9-.3 23.3 11.5 24.7zm439.6-24.8L402.9 238.1c-6.5-10-21.4-9.6-27.3 .8L290.2 388.5c-3.9 6.8 1.6 15.2 9.5 14.3l170.1-20c11.8-1.4 18-14.7 11.5-24.6zm-216.9 11l78.4-137.2c6.1-10.7-1.6-23.9-13.9-23.9H183.1c-12.3 0-20 13.3-13.9 23.9l78.4 137.2c3.7 6.4 13 6.4 16.7 0zM174.4 176H337.6c12.2 0 19.9-13.1 14-23.8l-80-144c-2.8-5.1-8.2-8.2-14-8.2h-3.2c-5.8 0-11.2 3.2-14 8.2l-80 144c-5.9 10.7 1.8 23.8 14 23.8z"
|
||||
},
|
||||
"screwdriver-wrench": {
|
||||
title: "settings",
|
||||
viewBox: "0 0 512 512",
|
||||
d: "M78.6 5C69.1-2.4 55.6-1.5 47 7L7 47c-8.5 8.5-9.4 22-2.1 31.6l80 104c4.5 5.9 11.6 9.4 19 9.4h54.1l109 109c-14.7 29-10 65.4 14.3 89.6l112 112c12.5 12.5 32.8 12.5 45.3 0l64-64c12.5-12.5 12.5-32.8 0-45.3l-112-112c-24.2-24.2-60.6-29-89.6-14.3l-109-109V104c0-7.5-3.5-14.5-9.4-19L78.6 5zM19.9 396.1C7.2 408.8 0 426.1 0 444.1C0 481.6 30.4 512 67.9 512c18 0 35.3-7.2 48-19.9L233.7 374.3c-7.8-20.9-9-43.6-3.6-65.1l-61.7-61.7L19.9 396.1zM512 144c0-10.5-1.1-20.7-3.2-30.5c-2.4-11.2-16.1-14.1-24.2-6l-63.9 63.9c-3 3-7.1 4.7-11.3 4.7H352c-8.8 0-16-7.2-16-16V102.6c0-4.2 1.7-8.3 4.7-11.3l63.9-63.9c8.1-8.1 5.2-21.8-6-24.2C388.7 1.1 378.5 0 368 0C288.5 0 224 64.5 224 144l0 .8 85.3 85.3c36-9.1 75.8 .5 104 28.7L429 274.5c49-23 83-72.8 83-130.5zM56 432a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z"
|
||||
},
|
||||
"dumbbell": {
|
||||
title: "dumbbell",
|
||||
viewBox: "0 0 640 512",
|
||||
d: "M96 64c0-17.7 14.3-32 32-32h32c17.7 0 32 14.3 32 32V224v64V448c0 17.7-14.3 32-32 32H128c-17.7 0-32-14.3-32-32V384H64c-17.7 0-32-14.3-32-32V288c-17.7 0-32-14.3-32-32s14.3-32 32-32V160c0-17.7 14.3-32 32-32H96V64zm448 0v64h32c17.7 0 32 14.3 32 32v64c17.7 0 32 14.3 32 32s-14.3 32-32 32v64c0 17.7-14.3 32-32 32H544v64c0 17.7-14.3 32-32 32H480c-17.7 0-32-14.3-32-32V288 224 64c0-17.7 14.3-32 32-32h32c17.7 0 32 14.3 32 32zM416 224v64H224V224H416z"
|
||||
},
|
||||
"chevron-down": {
|
||||
title: "chevron down",
|
||||
viewBox: "0 0 512 512",
|
||||
d: "M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"
|
||||
},
|
||||
"chevron-up": {
|
||||
title: "chevron up",
|
||||
viewBox: "0 0 512 512",
|
||||
d: "M233.4 105.4c12.5-12.5 32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L256 173.3 86.6 342.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l192-192z"
|
||||
},
|
||||
"chevron-left": {
|
||||
title: "chevron left",
|
||||
viewBox: "0 0 320 512",
|
||||
d: "M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"
|
||||
},
|
||||
"chevron-right": {
|
||||
title: "chevron right",
|
||||
viewBox: "0 0 320 512",
|
||||
d: "M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"
|
||||
},
|
||||
};
|
||||
|
||||
ns.createIcon = function createIcon(iconDef, title, desc, classes, style)
|
||||
{
|
||||
var iconEl = createElement(
|
||||
ns.ElementTypes.svg,
|
||||
{
|
||||
"viewBox": iconDef.viewBox,
|
||||
"class": `${ICON_CLASS} ${classes}`,
|
||||
"role": "img",
|
||||
"style": style || ""
|
||||
},
|
||||
ICON_CITATION
|
||||
);
|
||||
|
||||
iconEl.insertAdjacentHTML("afterbegin",
|
||||
`<style>
|
||||
.icon {
|
||||
fill: black; /*Default color*/
|
||||
fill: var(--icon-color);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
</style>`
|
||||
);
|
||||
|
||||
createChildElement(
|
||||
iconEl,
|
||||
ns.ElementTypes.title,
|
||||
{},
|
||||
title || iconDef.title
|
||||
);
|
||||
if (desc)
|
||||
{
|
||||
createChildElement(
|
||||
iconEl,
|
||||
ns.ElementTypes.desc,
|
||||
{},
|
||||
desc
|
||||
);
|
||||
}
|
||||
|
||||
createChildElement(
|
||||
iconEl,
|
||||
ns.ElementTypes.path,
|
||||
{
|
||||
"d": iconDef.d
|
||||
}
|
||||
);
|
||||
|
||||
return iconEl;
|
||||
};
|
||||
//#endregion
|
||||
});
|
||||
|
||||
registerNamespace("GW.Controls", function(ns) {
|
||||
ns.IconEl = class IconEl extends HTMLElement
|
||||
{
|
||||
//#region staticProperties
|
||||
static observedAttributes = [];
|
||||
static instanceCount = 0;
|
||||
static instanceMap = {};
|
||||
//#endregion
|
||||
|
||||
//#region instance properties
|
||||
instanceId;
|
||||
iconObj;
|
||||
titleText;
|
||||
isInitialized;
|
||||
|
||||
//#region element properties
|
||||
//#endregion
|
||||
//#endregion
|
||||
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
this.instanceId = IconEl.instanceCount++;
|
||||
IconEl.instanceMap[this.instanceId] = this;
|
||||
}
|
||||
|
||||
get idKey()
|
||||
{
|
||||
return `gw-icon-${this.instanceId}`;
|
||||
}
|
||||
|
||||
//#region HTMLElement implementation
|
||||
connectedCallback()
|
||||
{
|
||||
if (this.isInitialized) { return; }
|
||||
|
||||
this.iconObj = ns.SVGLib.Icons[this.getAttribute("iconKey")];
|
||||
|
||||
this.titleText = this.getAttribute("title");
|
||||
if (this.hasAttribute("titleId"))
|
||||
{
|
||||
const titleEl = document.getElementById(this.getAttribute("titleId"));
|
||||
this.titleText = titleEl.innerText;
|
||||
titleEl.remove();
|
||||
}
|
||||
|
||||
this.iconDescription = this.getAttribute("description");
|
||||
this.iconClasses = this.getAttribute("iconClasses");
|
||||
this.iconStyle = this.getAttribute("iconStyle");
|
||||
|
||||
this.renderContent();
|
||||
this.isInitialized = true;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
renderContent()
|
||||
{
|
||||
if (!this.iconObj)
|
||||
{
|
||||
debugger;
|
||||
return;
|
||||
}
|
||||
|
||||
//Markup
|
||||
this.insertAdjacentHTML("beforebegin",
|
||||
`<style>
|
||||
gw-icon {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>`
|
||||
);
|
||||
this.appendChild(
|
||||
ns.SVGLib.createIcon(
|
||||
this.iconObj,
|
||||
this.titleText,
|
||||
this.iconDescription,
|
||||
this.iconClasses,
|
||||
this.iconStyle,
|
||||
)
|
||||
);
|
||||
|
||||
//element properties
|
||||
}
|
||||
};
|
||||
customElements.define("gw-icon", ns.IconEl);
|
||||
});
|
12
src/_config/collections.js
Normal file
@ -0,0 +1,12 @@
|
||||
export default function(eleventyConfig) {
|
||||
// Add content categories to a collection
|
||||
eleventyConfig.addCollection("categories", function(collectionApi) {
|
||||
let categories = new Set();
|
||||
let contents = collectionApi.getFilteredByTag('contents');
|
||||
contents.forEach(p => {
|
||||
let cats = p.data.categories;
|
||||
cats.forEach(c => categories.add(c));
|
||||
});
|
||||
return Array.from(categories).sort();
|
||||
});
|
||||
}
|
11
src/_config/files.js
Normal file
@ -0,0 +1,11 @@
|
||||
// Passthrough File Copy
|
||||
export default function(eleventyConfig) {
|
||||
eleventyConfig.addPassthroughCopy("./src/assets/");
|
||||
eleventyConfig.addWatchTarget("./src/assets/");
|
||||
eleventyConfig.addWatchTarget("./src/_bundle/");
|
||||
eleventyConfig.addPassthroughCopy({
|
||||
"./src/assets/favicon/favicon.ico": "/favicon.ico",
|
||||
"./src/assets/favicon/apple-touch-icon.png": "/apple-touch-icon.png",
|
||||
"node_modules/@zachleat/details-utils/details-utils.js": "assets/js/details-utils.js",
|
||||
});
|
||||
}
|
32
src/_config/filters.js
Normal file
@ -0,0 +1,32 @@
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
export default function(eleventyConfig) {
|
||||
// Filter: Filter contents by category
|
||||
eleventyConfig.addFilter("filterByCategory", function(contents, cat) {
|
||||
cat = cat.toLowerCase();
|
||||
let result = contents.filter(item => {
|
||||
let cats = item.data.categories.map(c => c.toLowerCase());
|
||||
return cats.includes(cat);
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
// Filter: Format dates
|
||||
eleventyConfig.addFilter("formatDate", (date) => {
|
||||
const dateFormat = "d LLLL yyyy";
|
||||
if (typeof date === "object") {
|
||||
return DateTime.fromJSDate(date).toFormat(dateFormat);
|
||||
}
|
||||
return DateTime.fromISO(date, { setZone: true }).toFormat(dateFormat);
|
||||
});
|
||||
|
||||
// Filter: Limit number of items displayed
|
||||
eleventyConfig.addFilter("itemLimit", function(array, maximum) {
|
||||
return array.slice(0, maximum);
|
||||
});
|
||||
|
||||
// Filter: Thousands separator
|
||||
eleventyConfig.addFilter("thousands", function(num) {
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
});
|
||||
}
|
106
src/_config/markdown-it.js
Normal file
@ -0,0 +1,106 @@
|
||||
// markdown-it plugins
|
||||
import markdownIt from "markdown-it";
|
||||
import markdownItAnchor from "markdown-it-anchor";
|
||||
import markdownItAttribution from "markdown-it-attribution";
|
||||
import markdownItAttrs from "markdown-it-attrs";
|
||||
import markdownItBracketedSpans from 'markdown-it-bracketed-spans';
|
||||
import markdownItDefList from "markdown-it-deflist";
|
||||
import markdownItFootnote from "markdown-it-footnote";
|
||||
|
||||
// Configure slug filter
|
||||
import slugify from "slugify";
|
||||
|
||||
// Enable exporting markdown-it library to another module
|
||||
export let markdownLibrary;
|
||||
|
||||
export default function(eleventyConfig) {
|
||||
// Configure markdown-it-anchor plugins
|
||||
eleventyConfig.setLibrary('md', markdownIt().use(markdownItAnchor))
|
||||
const linkAfterHeader = markdownItAnchor.permalink.linkAfterHeader({
|
||||
class: "heading-anchor",
|
||||
symbol: "<span hidden>#</span>",
|
||||
style: "aria-labelledby",
|
||||
});
|
||||
const markdownItAnchorOptions = {
|
||||
level: [2, 3, 4, 5],
|
||||
slugify: (str) =>
|
||||
slugify(str, {
|
||||
lower: true,
|
||||
strict: true,
|
||||
remove: /["]/g,
|
||||
}),
|
||||
tabIndex: false,
|
||||
permalink(slug, opts, state, idx) {
|
||||
state.tokens.splice(idx, 0,
|
||||
Object.assign(new state.Token("div_open", "div", 1), {
|
||||
// Add class "header-wrapper [h1 or h2 or h3]"
|
||||
attrs: [["class", `heading-wrapper ${state.tokens[idx].tag}`]],
|
||||
block: true,
|
||||
})
|
||||
);
|
||||
|
||||
state.tokens.splice(idx + 4, 0,
|
||||
Object.assign(new state.Token("div_close", "div", -1), {
|
||||
block: true,
|
||||
})
|
||||
);
|
||||
|
||||
linkAfterHeader(slug, opts, state, idx + 1);
|
||||
},
|
||||
};
|
||||
|
||||
/* Markdown Overrides */
|
||||
markdownLibrary = markdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
})
|
||||
.use(markdownItAnchor, markdownItAnchorOptions)
|
||||
.use(markdownItAttribution)
|
||||
.use(markdownItAttrs)
|
||||
.use(markdownItBracketedSpans)
|
||||
.use(markdownItDefList)
|
||||
.use(markdownItFootnote);
|
||||
|
||||
// Configure linkify
|
||||
markdownLibrary.linkify.set({ fuzzyLink: false });
|
||||
|
||||
// Configure markdown-it-footnote
|
||||
markdownLibrary.renderer.rules.footnote_block_open = () => (
|
||||
'<hr class="footnotes-sep">\n' +
|
||||
'<section class="footnotes" aria-labelledby="footnotes">\n' +
|
||||
`<div class="heading-wrapper h2">
|
||||
<h2 id="footnotes" class="footnotes__title">Footnotes</h2>
|
||||
<a class="heading-anchor" href="#footnotes" aria-labelledby="footnotes"><span hidden="">#</span></a>
|
||||
</div>\n` +
|
||||
'<ol class="footnotes-list">\n'
|
||||
);
|
||||
|
||||
markdownLibrary.renderer.rules.footnote_anchor = (tokens, idx, options, env, slf) => {
|
||||
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
|
||||
|
||||
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
|
||||
|
||||
/* ↩ with escape code to prevent display as Apple Emoji on iOS */
|
||||
return `
|
||||
<p class="footnote-item__back">
|
||||
<a href="#fnref${id}" class="footnote-backref">
|
||||
<span aria-hidden="true">\u21a9\uFE0E</span>
|
||||
Back to reference ${id}
|
||||
</a>
|
||||
</p>
|
||||
`;
|
||||
};
|
||||
|
||||
const renderRules = {
|
||||
footnote_caption: ['[', '[Note ']
|
||||
};
|
||||
Object.keys(renderRules).map(rule => {
|
||||
let defaultRender = markdownLibrary.renderer.rules[rule];
|
||||
markdownLibrary.renderer.rules[rule] = (tokens, idx, options, env, self) => {
|
||||
return defaultRender(tokens, idx, options, env, self).replace(...renderRules[rule]);
|
||||
}
|
||||
});
|
||||
|
||||
/* This is the part that tells 11ty to swap to our custom config */
|
||||
eleventyConfig.setLibrary("md", markdownLibrary);
|
||||
}
|
54
src/_config/shortcodes.js
Normal file
@ -0,0 +1,54 @@
|
||||
import slugify from "slugify";
|
||||
import { markdownLibrary } from "./markdown-it.js";
|
||||
|
||||
export default function(eleventyConfig) {
|
||||
// Shortcode: <cite> tag
|
||||
eleventyConfig.addShortcode('cite', (str) => `<cite>${str}</cite>`);
|
||||
|
||||
// Shortcode: Manual heading anchor
|
||||
eleventyConfig.addPairedShortcode('headingAnchor', (title, hLevel, id=slugify(title)) => {
|
||||
return `<div class="heading-wrapper h${hLevel}">
|
||||
<h${hLevel} id="${id}">${title}</h${hLevel}>
|
||||
<a class="heading-anchor" href="#${id}" aria-labelledby="${id}"><span hidden="">#</span></a>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
// Shortcode: Custom container
|
||||
eleventyConfig.addPairedShortcode('container', (children, el, className) => {
|
||||
const classMarkup = className ? ` class="${className}"` : "";
|
||||
const content = markdownLibrary.render(children);
|
||||
return `<${el}${classMarkup}>${content}</${el}>`;
|
||||
});
|
||||
|
||||
// Shortcode: Image figure and figcaption
|
||||
eleventyConfig.addPairedShortcode('imgFigure', (
|
||||
caption, img, alt=caption, className, enableLazyLoading=true
|
||||
) => {
|
||||
const classMarkup = className ? ` class="${className}"` : "";
|
||||
const figcaption = markdownLibrary.renderInline(caption);
|
||||
return `<figure${classMarkup}>
|
||||
<img src="${img}" alt="${alt}"${enableLazyLoading ? ' loading="lazy"' : ''}>
|
||||
<figcaption>${figcaption}</figcaption>
|
||||
</figure>`;
|
||||
});
|
||||
|
||||
// Shortcode: Content disclosure
|
||||
eleventyConfig.addPairedShortcode('disclosure', (content, summary) => {
|
||||
const summaryMarkup = markdownLibrary.renderInline(summary);
|
||||
const contentMarkup = markdownLibrary.render(content);
|
||||
return `<details class="content-disclosure">
|
||||
<summary class="content-disclosure__summary">${summaryMarkup}</summary>
|
||||
<div class="content-disclosure__content">${contentMarkup}</div>
|
||||
</details>`;
|
||||
});
|
||||
|
||||
// Shortcode: Content warning disclosure
|
||||
eleventyConfig.addPairedShortcode('contentWarning', (content, warning) => {
|
||||
const warningMarkup = markdownLibrary.renderInline(warning);
|
||||
const contentMarkup = markdownLibrary.render(content);
|
||||
return `<details class="contnet-warning">
|
||||
<summary class="contnet-warning__warning"><strong><span aria-hidden="true">⚠️</span> Content Warning:</strong> ${warningMarkup}</summary>
|
||||
<div class="contnet-warning__content">${contentMarkup}</div>
|
||||
</details>`;
|
||||
});
|
||||
}
|
32
src/_data/robots.js
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
Modified from Robb Knight's script:
|
||||
https://rknight.me/blog/blocking-bots-with-nginx/
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
|
||||
export default async function () {
|
||||
const url = "https://raw.githubusercontent.com/ai-robots-txt/ai.robots.txt/main/robots.txt";
|
||||
let txt = await EleventyFetch(url, {
|
||||
duration: "1w",
|
||||
type: "text",
|
||||
});
|
||||
|
||||
const botExceptions = ["Applebot", "CCBot"];
|
||||
const botExceptionsFullStr = botExceptions.map(bot => "User-agent: " + bot)
|
||||
|
||||
txt = txt
|
||||
.split("\n")
|
||||
.filter((line) => !botExceptionsFullStr.includes(line))
|
||||
.join("\n");
|
||||
|
||||
const bots = txt
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("User-agent:"))
|
||||
.map((line) => line.split(":")[1].trim().replace(/\s/gi, ".*"));
|
||||
|
||||
return {
|
||||
txt: txt,
|
||||
htaccess: bots.join('|'),
|
||||
};
|
||||
}
|
28
src/_data/sitemeta.js
Normal file
@ -0,0 +1,28 @@
|
||||
export const siteName = "Leilukin's Hub";
|
||||
export const siteDomain = "leilukin.com";
|
||||
export const siteUrl = "https://" + siteDomain || "http://localhost:8080";
|
||||
export const siteAuthor = {
|
||||
name: "Leilukin",
|
||||
email: "contact@leilukin.com",
|
||||
emailEncoded: '<span class="email-encoded">contact@leilukin<b>.mail</b>.com</span>',
|
||||
emailDecoyUrl: "emailme/",
|
||||
url: siteUrl + "/about"
|
||||
};
|
||||
export const siteLanguage = "en";
|
||||
export const siteLocale = "en_MY";
|
||||
export const siteDescription = siteAuthor.name + "'s personal website.";
|
||||
export const fediverseHandle = "@Leilukin@dragonscave.space";
|
||||
export const feedPath = "/feed.xml";
|
||||
export const feedUrl = siteUrl + feedPath;
|
||||
export const subsites = [
|
||||
{
|
||||
siteName: "Tumbleblog",
|
||||
subDomain: `tumbleblog.${siteDomain}`,
|
||||
siteUrl() { return "https://" + this.subDomain; }
|
||||
},
|
||||
{
|
||||
siteName: `Beehive - ${siteAuthor.name}'s Fanlisitng Collective`,
|
||||
subDomain: `fan.${siteDomain}`,
|
||||
siteUrl() { return "https://" + this.subDomain; }
|
||||
},
|
||||
];
|
19
src/_includes/asummersend/base.njk
Normal file
@ -0,0 +1,19 @@
|
||||
{%- css %}{% include "src/assets/css/asummersend.css" %}{%- endcss %}
|
||||
|
||||
{% extends "global/baselayout.njk" %}
|
||||
|
||||
{% block metaTitle %}
|
||||
<meta property="og:title" content="{{ title + ' | ' if title }}A Summer’s End — Hong Kong 1986 Shrine">
|
||||
{% endblock %}
|
||||
|
||||
{% block pageTitle %}
|
||||
{{ title + " | " if title }}A Summer’s End — Hong Kong 1986 Shrine | {{ sitemeta.siteName | safe }}
|
||||
{% endblock %}
|
||||
|
||||
{% block favicon %}
|
||||
<link rel="icon" type="image/x-icon" href="/assets/shrines/asummersend/images/favicon.ico">
|
||||
{% endblock %}
|
||||
|
||||
{% block hero %}{% include "asummersend/hero.njk" %}{% endblock %}
|
||||
{% block navbar %}{% include "asummersend/navbar.njk" %}{% endblock %}
|
||||
{% block footer %}{% include "asummersend/footer.njk" %}{% endblock %}
|
7
src/_includes/asummersend/content.njk
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
layout: asummersend/base
|
||||
---
|
||||
|
||||
{% extends "global/content.njk" %}
|
||||
|
||||
{% block shrineInfo %}{% include "asummersend/shrineinfo.njk" %}{% endblock %}
|
6
src/_includes/asummersend/footer.njk
Normal file
@ -0,0 +1,6 @@
|
||||
{% extends "global/footer.njk" %}
|
||||
|
||||
{% block footerContent %}
|
||||
<p>Made with ♥ by {{ sitemeta.siteAuthor.name }} • Shrine Launched: 16 February 2023</p>
|
||||
{% endblock %}
|
||||
|
46
src/_includes/asummersend/hero.njk
Normal file
@ -0,0 +1,46 @@
|
||||
{% extends "global/hero.njk" %}
|
||||
|
||||
{% block heroImg %}
|
||||
<picture>
|
||||
<source srcset="/assets/shrines/asummersend/images/asummersend-header-320.avif" media="(orientation: landscape)" />
|
||||
<img src="/assets/shrines/asummersend/images/asummersend-header.avif" alt="Banner of A Summer’s End — Hong Kong 1986 Shrine" />
|
||||
</picture>
|
||||
{% endblock %}
|
||||
|
||||
{% block eventScript %}
|
||||
const todayEvent = getTodayEvent();
|
||||
|
||||
if (todayEvent) {
|
||||
heroTopBarEl.classList.remove('hidden');
|
||||
heroTopBarEl.innerHTML = todayEvent;
|
||||
}
|
||||
|
||||
function getTodayEvent() {
|
||||
const date = new Date();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const year = date.getFullYear();
|
||||
|
||||
const aseReleaseDate = new Date("2020-04-23").getFullYear();
|
||||
const aseAnniversary = year - aseReleaseDate;
|
||||
|
||||
if (month === 2 && day === 16)
|
||||
return `
|
||||
Today is Michelle Cheung's birthday. Happy Birthday Michelle!
|
||||
`;
|
||||
else if (month === 4 && day === 23)
|
||||
return `
|
||||
Today is the ${aseAnniversary}-year anniversary of the release of {% cite "A Summer’s End — Hong Kong 1986" %}!
|
||||
`;
|
||||
else if (month === 8 && day === 9)
|
||||
return `
|
||||
Today is Cecelia Cortes' birthday. Happy Birthday Cecelia!
|
||||
`;
|
||||
else if (month === 12 && day === 28)
|
||||
return `
|
||||
Today is Sam Wong's birthday. Happy Birthday Sam!
|
||||
`;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
{% endblock %}
|
31
src/_includes/asummersend/myplaylist.njk
Normal file
@ -0,0 +1,31 @@
|
||||
{% macro myASEPlaylist(intro) %}
|
||||
{% imgFigure "/assets/projects/playlists/My-Dear-Summer-Lover-cover.avif", "Cover image of A Summer’s End fanmix 'My Dear Summer Lover: A Sam x Michelle Fanmix'" %}
|
||||
[Image description: Sam and Michelle from A Summer’s End almost kisses, with a play symbol and text on the top left corner, and the text "My Dear Summer Lover: A Sam x Michelle Fanmix" in a digital style font in front.]
|
||||
{% endimgFigure %}
|
||||
|
||||
{{ intro }} [{% cite "A Summer’s End — Hong Kong 1986" %}](https://www.asummersend.com/home), dedicated to Sam and Michelle’s love story.
|
||||
|
||||
This mix contains mostly Cantonese and English songs, with one Mandarin song. Majority of these songs were performed by Hong Kong artists and released in the 80s, because A Summer’s End took place in Hong Kong and in the 80s.
|
||||
|
||||
I have carefully selected and arranged these tracks, so the lyrics would reflect Sam and Michelle’s relationship development throughout {% cite "A Summer’s End" %}. Therefore, the content of this mix could be considered spoilers if you have not played the visual novel.
|
||||
|
||||
Since Oracle and Bone has cited Anita Mui as a major inspiration for them and this visual novel, I have also included multiple songs performed by Anita Mui in this mix. Because A Summer’s End is a lesbian story, when I was choosing which songs to include in this mix, I also prioritize songs that were performed by women artists.
|
||||
|
||||
Tracklist:
|
||||
1. [輕輕嘆]{lang="zh"} (Sigh Softly) — Deanie Ip
|
||||
2. [愛我便說愛我吧]{lang="zh"} (Just Say You Love Me) — Anita Mui
|
||||
3. [夏日戀人]{lang="zh"} (Summer Lover) — Anita Mui
|
||||
4. [星空下的戀人]{lang="zh"} (Lovers Under the Stars) — Shirley Kwan
|
||||
5. I Want Your Love — CHIC
|
||||
6. [激情]{lang="zh"} (Passion) — Sandy Lam
|
||||
7. When Will I See You Again — Teresa Carpio
|
||||
8. [愛你、想你]{lang="zh"} (Love You, Miss You) — Anita Mui
|
||||
9. Within You’ll Remain — Julia Hsu
|
||||
10. I Want to Know What Love Is — Tina Arena
|
||||
11. Touch — Anita Mui
|
||||
12. [親密愛人]{lang="zh"} (Intimate Lover) — Anita Mui
|
||||
|
||||
[Listen on Spotify](https://open.spotify.com/playlist/3SIV7VjSKhspYwugVIQjug){.link-btn}
|
||||
|
||||
<iframe style="border-radius:12px" src="https://open.spotify.com/embed/playlist/3SIV7VjSKhspYwugVIQjug?utm_source=generator" width="100%" height="380" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>
|
||||
{% endmacro %}
|
11
src/_includes/asummersend/navbar.njk
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "global/navbar.njk" %}
|
||||
{% set shrineHomeUrl = "/shrines/asummersend/" %}
|
||||
|
||||
{% block navbarLinks %}
|
||||
{% set navPages = collections.all | eleventyNavigation("A Summer’s End Shrine") %}
|
||||
{%- for entry in navPages %}
|
||||
<li>
|
||||
<a {% if entry.url == page.url %}aria-current="page"{% endif %} href="{{ entry.url }}">{{ entry.title }}</a>
|
||||
</li>
|
||||
{%- endfor %}
|
||||
{% endblock %}
|
12
src/_includes/asummersend/shrineinfo.njk
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends "global/shrineinfo.njk" %}
|
||||
|
||||
{% block shrineAbout %}
|
||||
<p>Welcome to {{ sitemeta.siteAuthor.name }}'s shrine for {% cite "A Summer’s End — Hong Kong 1986" %}, an indie visual novel developed by Oracle and Bone. Set in 1980s Hong Kong, it tells of a love story between two women.</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block shrineLinks %}
|
||||
<li><a href="https://www.asummersend.com/">Official Website</a>
|
||||
</li>
|
||||
<li><a href="https://store.steampowered.com/app/1111370/A_Summers_End__Hong_Kong_1986/">Steam</a></li>
|
||||
<li><a href="https://oracleandbone.itch.io/a-summers-end">itch.io</a></li>
|
||||
{% endblock %}
|
19
src/_includes/cassettebeasts/base.njk
Normal file
@ -0,0 +1,19 @@
|
||||
{%- css %}{% include "src/assets/css/cassettebeasts.css" %}{%- endcss %}
|
||||
|
||||
{% extends "global/baselayout.njk" %}
|
||||
|
||||
{% block metaTitle %}
|
||||
<meta property="og:title" content="{{ title + ' | ' if title }}Cassette Beasts Shrine">
|
||||
{% endblock %}
|
||||
|
||||
{% block pageTitle %}
|
||||
{{ title + " | " if title }}Cassette Beasts Shrine | {{ sitemeta.siteName | safe }}
|
||||
{% endblock %}
|
||||
|
||||
{% block favicon %}
|
||||
<link rel="icon" type="image/x-icon" href="/assets/shrines/cassettebeasts/images/favicon.ico">
|
||||
{% endblock %}
|
||||
|
||||
{% block hero %}{% include "cassettebeasts/hero.njk" %}{% endblock %}
|
||||
{% block navbar %}{% include "cassettebeasts/navbar.njk" %}{% endblock %}
|
||||
{% block footer %}{% include "cassettebeasts/footer.njk" %}{% endblock %}
|
7
src/_includes/cassettebeasts/content.njk
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
layout: cassettebeasts/base
|
||||
---
|
||||
|
||||
{% extends "global/content.njk" %}
|
||||
|
||||
{% block shrineInfo %}{% include "cassettebeasts/shrineinfo.njk" %}{% endblock %}
|
6
src/_includes/cassettebeasts/footer.njk
Normal file
@ -0,0 +1,6 @@
|
||||
{% extends "global/footer.njk" %}
|
||||
|
||||
{% block footerContent %}
|
||||
<p>Made with ♥ by {{ sitemeta.siteAuthor.name }} • Shrine Launched: 3 August 2023</p>
|
||||
{% endblock %}
|
||||
|
40
src/_includes/cassettebeasts/hero.njk
Normal file
@ -0,0 +1,40 @@
|
||||
{% extends "global/hero.njk" %}
|
||||
|
||||
{% block heroImg %}
|
||||
<picture>
|
||||
<source srcset="/assets/shrines/cassettebeasts/images/cassettebeasts-header-320.avif" media="(orientation: landscape)" />
|
||||
<img src="/assets/shrines/cassettebeasts/images/cassettebeasts-header-640.avif" alt="Banner of Cassette Beasts Shrine" />
|
||||
</picture>
|
||||
{% endblock %}
|
||||
|
||||
{% block eventScript %}
|
||||
const todayEvent = getTodayEvent();
|
||||
|
||||
if (todayEvent) {
|
||||
heroTopBarEl.classList.remove('hidden');
|
||||
heroTopBarEl.innerHTML = todayEvent;
|
||||
}
|
||||
|
||||
function getTodayEvent() {
|
||||
const date = new Date();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const year = date.getFullYear();
|
||||
|
||||
const cbReleaseDate = new Date("2023-04-26").getFullYear();
|
||||
const pierReleaseDate = new Date("2023-10-04").getFullYear();
|
||||
const cbAnniversary = year - cbReleaseDate;
|
||||
const pierAnniversary = year - pierReleaseDate;
|
||||
|
||||
if (month === 4 && day === 26)
|
||||
return `
|
||||
Today is the ${cbAnniversary}-year anniversary of the release of {% cite "Cassette Beasts" %}!
|
||||
`;
|
||||
else if (month === 10 && day === 4)
|
||||
return `
|
||||
Today is the ${pierAnniversary}-year anniversary of the release of <cite>Pier of the Unknown</cite>, the first DLC expansion of {% cite "Cassette Beasts" %}!
|
||||
`;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
{% endblock %}
|
11
src/_includes/cassettebeasts/navbar.njk
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "global/navbar.njk" %}
|
||||
{% set shrineHomeUrl = "/shrines/cassettebeasts/" %}
|
||||
|
||||
{% block navbarLinks %}
|
||||
{% set navPages = collections.all | eleventyNavigation("Cassette Beasts Shrine") %}
|
||||
{%- for entry in navPages %}
|
||||
<li>
|
||||
<a {% if entry.url == page.url %}aria-current="page"{% endif %} href="{{ entry.url }}">{{ entry.title }}</a>
|
||||
</li>
|
||||
{%- endfor %}
|
||||
{% endblock %}
|
14
src/_includes/cassettebeasts/shrineinfo.njk
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends "global/shrineinfo.njk" %}
|
||||
|
||||
{% block shrineAbout %}
|
||||
<p>Welcome to {{ sitemeta.siteAuthor.name }}'s shrine for {% cite "Cassette Beasts" %}, an indie monster collecting turn-based open-world role-playing video game developed by Bytten Studio and published by Raw Fury.</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block shrineLinks %}
|
||||
<li><a href="https://www.cassettebeasts.com/">Official Website</a>
|
||||
</li>
|
||||
<li><a href="https://wiki.cassettebeasts.com/">Official Wiki</a></li>
|
||||
<li><a href="https://store.steampowered.com/app/1321440/Cassette_Beasts/">Steam</a></li>
|
||||
<li><a href="https://bytten-studio.com/">Bytten Studio</a></li>
|
||||
<li><a href="https://fan.leilukin.com/cassettebeasts/">Cassette Beasts fanlisting</a></li>
|
||||
{% endblock %}
|
53
src/_includes/global/baselayout.njk
Normal file
@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ sitemeta.siteLanguage }}" dir="ltr" id="top">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
{# Open Graph Meta #}
|
||||
{% block metaTitle %}
|
||||
<meta property="og:title" content="{{ title if title else sitemeta.siteName | safe }}">
|
||||
{% endblock %}
|
||||
{% include "global/meta.njk" %}
|
||||
{# Feeds #}
|
||||
{% include "global/feeds.njk" %}
|
||||
{# Fonts #}
|
||||
{% include "global/fonts.njk" %}
|
||||
{# CSS #}
|
||||
{% include "global/css-bundle.njk" %}
|
||||
{% if hasTooltips %}
|
||||
<link rel="stylesheet" href="/assets/css/tooltips.css">
|
||||
{% endif %}
|
||||
<style>{% getBundle "css" %}</style>
|
||||
{# Favicon #}
|
||||
{% block favicon %}{% endblock %}
|
||||
{# Verifications #}
|
||||
<link rel="me" href="https://dragonscave.space/@Leilukin">
|
||||
<link rel="me" href="https://github.com/Leilukin">
|
||||
<link rel="authorization_endpoint" href="https://indieauth.com/auth">
|
||||
{# JavaScript #}
|
||||
<script src="{% getBundleFileUrl 'js' %}" defer></script>
|
||||
{% if toc %}
|
||||
<script src="{{'/assets/js/details-utils.js'}}" defer></script>
|
||||
{% endif %}
|
||||
{% if hasCodeBlock %}
|
||||
<script src="/assets/js/copycode.js" defer></script>
|
||||
{% endif %}
|
||||
{% if hasTooltips %}
|
||||
<script src="/assets/js/tooltips.js" defer></script>
|
||||
{% endif %}
|
||||
|
||||
<title>
|
||||
{% block pageTitle %}{{ title + " | " if title }}{{ sitemeta.siteName | safe }}{% endblock %}
|
||||
</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="skip-btn"><a href="#content">Skip to content</a></div>
|
||||
{% block hero %}{% include "global/hero.njk" %}{% endblock %}
|
||||
{% block navbar %}{% include "global/navbar.njk" %}{% endblock %}
|
||||
<main id="content">
|
||||
{{ content | safe }}
|
||||
</main>
|
||||
{% block footer %}{% include "global/footer.njk" %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
115
src/_includes/global/content.njk
Normal file
@ -0,0 +1,115 @@
|
||||
<header class="main__header">
|
||||
{% if isArticle or hasBreadcrumbs %}
|
||||
<nav class="breadcrumbs" aria-labelledby="breadcrumbs-title">
|
||||
<h2 class="visually-hidden" id="breadcrumbs-title">Breadcrumbs</h2>
|
||||
{% set breadcrumbNavPages = collections.all | eleventyNavigationBreadcrumb(eleventyNavigation.key or articleTitle or pageTitle or title) %}
|
||||
{%- for entry in breadcrumbNavPages %}
|
||||
<a href="{{ entry.url }}">{{ entry.title }}</a>
|
||||
<span aria-hidden="true">➔</span>
|
||||
{%- endfor %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<h1>{{ articleTitle or pageTitle or title }}</h1>
|
||||
|
||||
{% if isArticle %}
|
||||
<div class="article__info">
|
||||
<p>
|
||||
{{ content | wordcount | thousands }} words.
|
||||
Posted on <time datetime="{{ date }}">{{ date | formatDate }}</time> by {{ sitemeta.siteAuthor.name }}
|
||||
</p>
|
||||
{% if updated %}
|
||||
<p>Last updated on <time datetime="{{ updated }}">{{ updated | formatDate }}</time></p>
|
||||
{% endif %}
|
||||
{% if categories %}
|
||||
<p>Categories:
|
||||
{% for cat in categories %}
|
||||
<a href="/categories/{{ cat | slugify }}">{{ cat }}</a>{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if desc %}
|
||||
<p>{{ desc }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
<content-wrapper>
|
||||
{% if toc %}
|
||||
{% include "global/toc.njk" %}
|
||||
{% endif %}
|
||||
|
||||
{% set contentEl = "article" if isArticle or articleElement else "div" %}
|
||||
|
||||
<{{contentEl}} class="content{{' content--divided' if isContentDivided }}">
|
||||
{{ content | safe }}
|
||||
|
||||
{% if tags and tags.includes("posts") %}
|
||||
{%- if collections.posts.length > 1 %}
|
||||
<nav class="blog__post--pagination" aria-labelledby="nextprev-title">
|
||||
<h2 class="visually-hidden" id="nextprev-title">Next and Previous Blog Posts</h2>
|
||||
{%- set previousPost = collections.posts | getPreviousCollectionItem %}
|
||||
{%- set nextPost = collections.posts | getNextCollectionItem %}
|
||||
{%- if nextPost or previousPost %}
|
||||
<ul class="blog__post--nextprev">
|
||||
{%- if previousPost %}
|
||||
<li class="blog__post--prev">
|
||||
<p>Previous Post:</p>
|
||||
<a href="{{ previousPost.url }}">{{ previousPost.data.articleTitle }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{%- if nextPost %}
|
||||
<li class="blog__post--next">
|
||||
<p>Next Post:</p>
|
||||
<a href="{{ nextPost.url }}">{{ nextPost.data.articleTitle }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{%- endif %}
|
||||
</nav>
|
||||
{%- endif %}
|
||||
{% endif %}
|
||||
</{{contentEl}}>
|
||||
|
||||
{% if
|
||||
tags and tags.includes("articles")
|
||||
or tags and tags.includes("posts")
|
||||
or page.url === "/articles/"
|
||||
or tags and tags.includes("blog pages")
|
||||
%}
|
||||
{% include "main/content-nav.njk" %}
|
||||
{% endif %}
|
||||
|
||||
{% block shrineInfo %}{% endblock %}
|
||||
</content-wrapper>
|
||||
|
||||
{%- css %}
|
||||
.breadcrumbs {
|
||||
margin-bottom: 0.7em;
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.blog__post--pagination {
|
||||
padding-top: 1em;
|
||||
margin-block-start: 2.5em;
|
||||
border-top: 0.1em solid var(--clr-title-border);
|
||||
}
|
||||
|
||||
.blog__post--nextprev {
|
||||
list-style-type: "";
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
gap: 0.7em;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-areas: 'prev next';
|
||||
}
|
||||
|
||||
.blog__post--prev { grid-area: prev; }
|
||||
.blog__post--next { grid-area: next; }
|
||||
{% endcss %}
|
14
src/_includes/global/css-bundle.njk
Normal file
@ -0,0 +1,14 @@
|
||||
{% set cssFiles = [
|
||||
"global",
|
||||
"a11y-syntax-highlighting-dark",
|
||||
"general",
|
||||
"content",
|
||||
"plugins",
|
||||
"components",
|
||||
"pridesymbols",
|
||||
"utility"
|
||||
] %}
|
||||
|
||||
{%- for file in cssFiles -%}
|
||||
<link rel="stylesheet" href="{{'/assets/css/' + file + '.css'}}">
|
||||
{%- endfor -%}
|
1
src/_includes/global/feeds.njk
Normal file
@ -0,0 +1 @@
|
||||
<link rel="alternate" type="application/rss+xml" title="{{ sitemeta.siteName }}" href="{{ sitemeta.feedPath }}">
|
19
src/_includes/global/fonts.njk
Normal file
@ -0,0 +1,19 @@
|
||||
<link rel="preload" href="/assets/fonts/readex-pro/readex-pro-v22-latin-regular.woff2" as="font" type="font/woff2" crossorigin="anonymous">
|
||||
<link rel="preload" href="/assets/fonts/readex-pro/readex-pro-v22-latin-700.woff2" as="font" type="font/woff2" crossorigin="anonymous">
|
||||
<style>
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Readex Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('/assets/fonts/readex-pro/readex-pro-v22-latin-regular.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Readex Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('/assets/fonts/readex-pro/readex-pro-v22-latin-700.woff2') format('woff2');
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/assets/fonts/fonts.css">
|
53
src/_includes/global/footer.njk
Normal file
@ -0,0 +1,53 @@
|
||||
<footer class="footer">
|
||||
<p>
|
||||
<svg class="inline-icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"/></svg>
|
||||
<a rel="nofollow, noindex" href="{{ sitemeta.siteAuthor.emailDecoyUrl }}">Contact me by email</a> (<a href="https://useplaintext.email/">plain text email</a> encouraged)
|
||||
</p>
|
||||
|
||||
{%- block footerContent %}
|
||||
{{ footerContent }}
|
||||
{% endblock -%}
|
||||
|
||||
{%- if tags and tags.includes("shrine pages") %}
|
||||
<nav class="footer__shrines" aria-labelledby="backto-title">
|
||||
<p id="backto-title">Back to:</p>
|
||||
<ul class="inline-nav footer__links">
|
||||
<li><a href="/shrines/">Shrine Directory</a></li>
|
||||
<li><a href="/">{{ sitemeta.siteName }}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
{%- endif -%}
|
||||
|
||||
<p>
|
||||
<svg class="inline-icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M96 64c0-17.7 14.3-32 32-32l320 0 64 0c70.7 0 128 57.3 128 128s-57.3 128-128 128l-32 0c0 53-43 96-96 96l-192 0c-53 0-96-43-96-96L96 64zM480 224l32 0c35.3 0 64-28.7 64-64s-28.7-64-64-64l-32 0 0 128zM32 416l512 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 480c-17.7 0-32-14.3-32-32s14.3-32 32-32z"/></svg>
|
||||
<a href="https://ko-fi.com/leilukin">Support me on Ko-Fi</a>
|
||||
</p>
|
||||
|
||||
{%- include "global/h-card.njk" -%}
|
||||
{%- include "global/top-btn.njk" %}
|
||||
</footer>
|
||||
|
||||
{%- css %}
|
||||
:root { --footer-gap: 0.5em; }
|
||||
|
||||
.footer {
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
background: var(--clr-main-footer-bg);
|
||||
padding: 1.5rem 1rem clamp(1.5rem, calc(100% - 1.5rem), 3.5rem);
|
||||
text-align: center;
|
||||
display: grid;
|
||||
gap: var(--footer-gap);
|
||||
}
|
||||
|
||||
.footer__links,
|
||||
.footer__shrines {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
column-gap: var(--footer-gap);
|
||||
}
|
||||
|
||||
.footer__links { justify-self: center; }
|
||||
.footer__shrines { align-self: center; }
|
||||
{% endcss %}
|
7
src/_includes/global/h-card.njk
Normal file
@ -0,0 +1,7 @@
|
||||
<div class="h-card hidden">
|
||||
<span class="p-name p-nickname">Leilukin</span>
|
||||
<a href="/" class="u-url">Home page</a>
|
||||
<img src="https://i.postimg.cc/RZJgS6tY/leilukin-bee.avif" alt="Leilukin" class="u-photo" loading="lazy">
|
||||
<span class="p-country-name">Malaysia</span>
|
||||
<span class="p-note">They/she. A proudly queer and autistic <span class="p-gender-identity">non-binary</span> lesbian from Malaysia.</span>
|
||||
</div>
|
46
src/_includes/global/hero.njk
Normal file
@ -0,0 +1,46 @@
|
||||
<header class="hero">
|
||||
<div class="hero__top-bar hidden"></div>
|
||||
<div class="hero__img">
|
||||
{% block heroImg %}
|
||||
{{ heroImg }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{%- css %}
|
||||
.hero {
|
||||
width: 100%;
|
||||
background-color: var(--clr-hero-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero__top-bar {
|
||||
background: var(--clr-navbar-bg);
|
||||
width: 100%;
|
||||
padding: 0.5em 0.7em;
|
||||
}
|
||||
|
||||
.hero__img {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.hero img {
|
||||
object-fit: contain;
|
||||
overflow: hidden;
|
||||
max-height: 16rem;
|
||||
}
|
||||
{% endcss %}
|
||||
|
||||
{%- js %}
|
||||
const hero = document.querySelector(".hero");
|
||||
const heroTopBarEl = document.querySelector(".hero__top-bar");
|
||||
const headerImgEl = document.querySelector(".hero__img");
|
||||
|
||||
{% block eventScript %}
|
||||
{{ eventScript }}
|
||||
{% endblock %}
|
||||
{% endjs %}
|
26
src/_includes/global/macros.njk
Normal file
@ -0,0 +1,26 @@
|
||||
{%- macro articleList(tag) -%}
|
||||
<ul>
|
||||
{%- for article in collections[tag] -%}
|
||||
<li>
|
||||
<p class="item-list__title">
|
||||
<a href="{{ article.url }}">{{ article.data.articleTitle }}</a>
|
||||
</p>
|
||||
<time datetime="{{ article.date }}">{{ article.date | formatDate }}</time>
|
||||
</li>
|
||||
{%- endfor -%}
|
||||
</ul>
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro modEntry(params) -%}
|
||||
{% headingAnchor 3 %}{{ params.title }}{% endheadingAnchor %}
|
||||
<div class="mod-entry">
|
||||
<img src="/assets/projects/{{ params.bannerFile }}" alt="Banner of {{ params.title }} mod" loading="lazy">
|
||||
<p>{{ params.desc | safe }}</p>
|
||||
<h4>Download the mod:</h4>
|
||||
<div class="mod-entry__downloads">
|
||||
{%- for download in params.downloads -%}
|
||||
<a class="link-btn" href="{{ download.url }}">{{ download.site }}</a>
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro -%}
|
12
src/_includes/global/meta.njk
Normal file
@ -0,0 +1,12 @@
|
||||
<link rel="canonical" href="{{ sitemeta.siteUrl }}{{ page.url }}">
|
||||
<meta name="generator" content="{{ eleventy.generator }}">
|
||||
<meta name="author" content="{{ sitemeta.siteAuthor.name }}">
|
||||
<meta name="description" content="{{ desc or metadata.desc or sitemeta.siteDescription }}">
|
||||
<meta name="fediverse:creator" content="{{ sitemeta.fediverseHandle }}">
|
||||
<meta property="og:site_name" content="{{ sitemeta.siteName | safe }}" />
|
||||
<meta property="og:type" content="{{ "article" if isArticle or articleElement else "website" }}">
|
||||
<meta property="og:description" content="{{ desc or metadata.desc or sitemeta.siteDescription }}">
|
||||
<meta property="og:url" content="{{ sitemeta.siteUrl }}{{ page.url }}">
|
||||
<meta property="og:locale" content="{{ sitemeta.siteLocale }}">
|
||||
<meta property="og:image" content="{{ sitemeta.siteUrl }}/assets/leilukin/leilukins-hub-meta-img.png">
|
||||
<meta name="theme-color" content="#3d2163">
|
105
src/_includes/global/navbar.njk
Normal file
@ -0,0 +1,105 @@
|
||||
{% set navLinksEl %}
|
||||
{%- if tags and tags.includes("shrine pages") %}
|
||||
<li>
|
||||
<a {% if currentUrl === shrineHomeUrl %}aria-current="page"{% endif %} href="{{ shrineHomeUrl }}">Shrine Home</a>
|
||||
</li>
|
||||
{%- endif -%}
|
||||
{% block navbarLinks %}{% endblock %}
|
||||
{%- if tags and tags.includes("shrine pages") %}
|
||||
<li><a href="/shrines/">Shrine Directory</a></li>
|
||||
<li><a href="/">Main Site</a></li>
|
||||
{%- endif -%}
|
||||
{% endset %}
|
||||
|
||||
<nav class="navbar" aria-labelledby="top-level-nav-title">
|
||||
<h2 class="visually-hidden" id="top-level-nav-title">Top Level</h2>
|
||||
<ul class="navbar__menu navbar__links">{{ navLinksEl | safe }}</ul>
|
||||
<button class="navbar__toggle" popovertarget="nav-menu" aria-label="Toggle navigation menu">
|
||||
<svg aria-hidden="true" focusable="false" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M0 96C0 78.3 14.3 64 32 64l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 128C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 288c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32L32 448c-17.7 0-32-14.3-32-32s14.3-32 32-32l384 0c17.7 0 32 14.3 32 32z"/></svg>
|
||||
Navigation
|
||||
</button>
|
||||
</nav>
|
||||
<div popover id="nav-menu" class="navbar__popover">
|
||||
<ul class="navbar__menu">{{ navLinksEl | safe }}</ul>
|
||||
</div>
|
||||
|
||||
{%- css %}
|
||||
.navbar {
|
||||
background: var(--clr-navbar-bg);
|
||||
width: 100%;
|
||||
z-index: 998;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 0.6em;
|
||||
}
|
||||
|
||||
.navbar__menu {
|
||||
list-style-type: "";
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
flex-wrap: wrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.navbar__menu a {
|
||||
color: var(--clr-navbar-link);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.navbar__menu a:hover { color: var(--clr-link-hover); }
|
||||
.navbar__menu a:focus { outline-offset: 0.2em; }
|
||||
|
||||
.navbar__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.navbar__toggle {
|
||||
background-color: inherit;
|
||||
color: var(--clr-navbar-link);
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 0.3em;
|
||||
}
|
||||
|
||||
.navbar__toggle svg { fill: currentColor; }
|
||||
|
||||
.navbar__toggle:focus,
|
||||
.navbar__menu a:focus { outline-offset: 0.1em; }
|
||||
.navbar__toggle:focus,
|
||||
.navbar__menu a:focus { outline: 0.15em solid var(--clr-navbar-link); }
|
||||
|
||||
.navbar__popover {
|
||||
background: var(--clr-navbar-bg);
|
||||
border: 0.15em solid var(--clr-navbar-link);
|
||||
padding: 1.5em;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.navbar__popover::backdrop {
|
||||
background-color: black;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@supports selector([popover]) {
|
||||
.navbar__toggle { display: flex; }
|
||||
.navbar__links { display: none; }
|
||||
}
|
||||
|
||||
/* Tablet screen size */
|
||||
@media only screen and (min-width: 43.75rem) {
|
||||
.navbar { padding: 1em 0.6em; }
|
||||
.navbar__toggle, .navbar__popover { display: none; }
|
||||
.navbar__links { display: flex; }
|
||||
}
|
||||
{% endcss %}
|
98
src/_includes/global/pagination-oldnew.njk
Normal file
@ -0,0 +1,98 @@
|
||||
{% set firstLabel %}
|
||||
<svg class="inline-icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160zm352-160l-160 160c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L301.3 256 438.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0z"></svg>
|
||||
Newest{% endset %}
|
||||
|
||||
{% set prevLabel %}
|
||||
<svg class="inline-icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"/></svg>
|
||||
Newer{% endset %}
|
||||
|
||||
{% set nextLabel %}Older
|
||||
<svg class="inline-icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M278.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-160 160c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L210.7 256 73.4 118.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l160 160z"/></svg>
|
||||
{% endset %}
|
||||
|
||||
{% set lastLabel %}Oldest
|
||||
<svg class="inline-icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M470.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L402.7 256 265.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160zm-352 160l160-160c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L210.7 256 73.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0z"/></svg>
|
||||
{% endset %}
|
||||
|
||||
<nav aria-labelledby="pagination-title" class="pagination__wrapper">
|
||||
<h2 class="visually-hidden" id="pagination-title">Pagination</h2>
|
||||
<ul class="pagination">
|
||||
<li>
|
||||
{% if pagination.href.first %}
|
||||
<a href="{{ pagination.href.first }}">
|
||||
{{ firstLabel | safe }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ firstLabel | safe }}
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
{% if pagination.href.previous %}
|
||||
<a href=" {{ pagination.href.previous }}">
|
||||
<i class="fa-solid fa-angle-left"></i>
|
||||
{{ prevLabel | safe }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ prevLabel | safe }}
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
{% if pagination.href.next %}
|
||||
<a href=" {{ pagination.href.next }}">
|
||||
{{ nextLabel | safe }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ nextLabel | safe }}
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
{% if pagination.href.last %}
|
||||
<a href=" {{ pagination.href.last }}">
|
||||
{{ lastLabel | safe }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ lastLabel | safe }}
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{%- css %}
|
||||
.pagination__wrapper {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
margin-top: 3em;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
list-style-type: "";
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pagination li {
|
||||
text-align: center;
|
||||
padding: 0.3em 0.7em;
|
||||
color: var(--clr-title-border);
|
||||
background-color: var(--clr-code-bg);
|
||||
}
|
||||
|
||||
.pagination li:has(a) { background-color: var(--clr-title-border); }
|
||||
.pagination li:has(a):hover { background-color: var(--clr-link-hover); }
|
||||
|
||||
.pagination li:has(a):focus-within {
|
||||
outline: 0.2em solid var(--clr-title-border);
|
||||
outline-offset: 0.15em;
|
||||
}
|
||||
|
||||
.pagination li a {
|
||||
color: var(--clr-link-btn-hover);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pagination li a:focus { outline: none; }
|
||||
{% endcss %}
|
39
src/_includes/global/shrineinfo.njk
Normal file
@ -0,0 +1,39 @@
|
||||
<aside class="right-sidebar" aria-label="Right sidebar">
|
||||
<div class="shrine__info sidebar--sticky">
|
||||
<h2>About</h2>
|
||||
{% block shrineAbout %}
|
||||
{{ shrineAbout }}
|
||||
{% endblock %}
|
||||
<h3>Links</h3>
|
||||
<ul>
|
||||
{% block shrineLinks %}
|
||||
{{ shrineLinks }}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{%- css %}
|
||||
.right-sidebar {
|
||||
background-color: var(--clr-content-bg);
|
||||
font-size: clamp(0.9rem, 0.9rem + 3vw, 1rem);
|
||||
}
|
||||
|
||||
.shrine__info {
|
||||
padding: 1rem;
|
||||
max-height: var(--ht-sticky-sidebar);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.shrine__info h2 {
|
||||
font-size: clamp(1.5rem, 1rem + 3vw, 1.7rem);
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
|
||||
.shrine__info h3 {
|
||||
font-size: clamp(1.3rem, 1rem + 3vw, 1.5rem);
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.shrine__info ul { margin-top: 0.5em; }
|
||||
{% endcss %}
|
57
src/_includes/global/toc.njk
Normal file
@ -0,0 +1,57 @@
|
||||
<aside class="left-sidebar" aria-label="Left sidebar">
|
||||
<details-utils force-open="(min-width: 60rem)" force-restore>
|
||||
<details class="toc__wrapper sidebar--sticky">
|
||||
<summary class="toc__heading" id="toc-heading">
|
||||
Table of Contents
|
||||
</summary>
|
||||
{{ content | toc | safe }}
|
||||
</details>
|
||||
</details-utils>
|
||||
</aside>
|
||||
|
||||
{%- css %}
|
||||
.toc__wrapper {
|
||||
max-height: var(--ht-sticky-sidebar);
|
||||
overflow-x: auto;
|
||||
background-color: var(--clr-content-bg);
|
||||
padding: 1rem 1.3rem;
|
||||
}
|
||||
|
||||
.toc__heading {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--clr-sub-heading);
|
||||
}
|
||||
|
||||
.toc__wrapper[open] > .toc__heading {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.toc ol, .toc ol ol {
|
||||
display: grid;
|
||||
gap: 0.3em;
|
||||
}
|
||||
|
||||
.toc ol {
|
||||
border-top: 0.1em solid var(--clr-title-border);
|
||||
padding-left: 1.3em;
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.toc ol ol {
|
||||
border-top: none;
|
||||
list-style-type: disc;
|
||||
padding-left: 1em;
|
||||
padding-top: 0.3em;
|
||||
}
|
||||
|
||||
.toc ol a {
|
||||
font-size: 1.1rem;
|
||||
padding-left: 0.3em;
|
||||
}
|
||||
|
||||
.toc ol ol a {
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
{% endcss %}
|
43
src/_includes/global/top-btn.njk
Normal file
@ -0,0 +1,43 @@
|
||||
<a href="#top" class="top-btn">
|
||||
<svg class="top-btn__arrow" focusable="false" aria-hidden="true" viewBox="0 0 26 28">
|
||||
<path d="M25.172 15.172c0 0.531-0.219 1.031-0.578 1.406l-1.172 1.172c-0.375 0.375-0.891 0.594-1.422 0.594s-1.047-0.219-1.406-0.594l-4.594-4.578v11c0 1.125-0.938 1.828-2 1.828h-2c-1.062 0-2-0.703-2-1.828v-11l-4.594 4.578c-0.359 0.375-0.875 0.594-1.406 0.594s-1.047-0.219-1.406-0.594l-1.172-1.172c-0.375-0.375-0.594-0.875-0.594-1.406s0.219-1.047 0.594-1.422l10.172-10.172c0.359-0.375 0.875-0.578 1.406-0.578s1.047 0.203 1.422 0.578l10.172 10.172c0.359 0.375 0.578 0.891 0.578 1.422z"></path>
|
||||
</svg>
|
||||
Back to Top
|
||||
</a>
|
||||
|
||||
{%- css %}
|
||||
.top-btn,
|
||||
.top-btn:hover {
|
||||
color: var(--clr-top-btn-txt);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.top-btn {
|
||||
position: fixed;
|
||||
bottom: 0.5rem;
|
||||
right: 0.5rem;
|
||||
z-index: 999;
|
||||
|
||||
background-color: var(--clr-top-btn-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 50em;
|
||||
padding: 0.3em 0.5em;
|
||||
gap: 0.2em;
|
||||
}
|
||||
|
||||
.top-btn:focus {
|
||||
outline: 0.25em solid var(--clr-top-btn-bg);
|
||||
outline-offset: 0.15em;
|
||||
}
|
||||
|
||||
.top-btn__arrow {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
aspect-ratio: 1 / 1;
|
||||
stroke-width: 0;
|
||||
stroke: currentColor;
|
||||
fill: currentColor;
|
||||
}
|
||||
{% endcss %}
|
16
src/_includes/main/archive.njk
Normal file
@ -0,0 +1,16 @@
|
||||
<ul>
|
||||
{% for content in contentList %}
|
||||
<li>
|
||||
<p class="item-list__title">
|
||||
<a href="{{ content.url }}">
|
||||
{% if content.data.pageTitle %}{{ content.data.pageTitle | safe }}
|
||||
{% elif content.data.title %}{{ content.data.title | safe }}
|
||||
{% else %}
|
||||
<code>{{ content.url }}</code>
|
||||
{% endif %}
|
||||
</a>
|
||||
</p>
|
||||
<time datetime="{{ content.date }}">{{ content.date | formatDate }}</time>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
5
src/_includes/main/base.njk
Normal file
@ -0,0 +1,5 @@
|
||||
{% extends "global/baselayout.njk" %}
|
||||
|
||||
{% block hero %}{% include "main/hero.njk" %}{% endblock %}
|
||||
{% block navbar %}{% include "main/navbar.njk" %}{% endblock %}
|
||||
{% block footer %}{% include "main/footer.njk" %}{% endblock %}
|
7
src/_includes/main/changelogs-list.njk
Normal file
@ -0,0 +1,7 @@
|
||||
{% for log in changelogList | reverse %}
|
||||
{% headingAnchor 2, log.fileSlug %}
|
||||
{{ log.date | formatDate }}
|
||||
{% endheadingAnchor %}
|
||||
|
||||
{{ log.content | safe }}
|
||||
{%- endfor %}
|
62
src/_includes/main/changelogs.njk
Normal file
@ -0,0 +1,62 @@
|
||||
---
|
||||
layout: main/content
|
||||
articleElement: true
|
||||
---
|
||||
|
||||
{% set currentUrl %}{{ page.url }}{% endset %}
|
||||
|
||||
<nav class="changelog__nav" aria-labelledby="changelog-nav-title">
|
||||
<p class="changelog__nav--title" id="changelog-nav-title">Changelog Archive:</p>
|
||||
<ul class="inline-nav changelog__nav--links">
|
||||
<li><a
|
||||
{% if currentUrl === "/changelogs/" %}aria-current="page"{% endif %}
|
||||
href="/changelogs"
|
||||
>Latest</a></li>
|
||||
{% set navPages = collections.all | eleventyNavigation("Changelogs") | reverse %}
|
||||
{%- for entry in navPages %}
|
||||
<li><a
|
||||
{% if entry.url == page.url %}aria-current="page"{% endif %}
|
||||
href="{{ entry.url }}"
|
||||
>{{ entry.title }}</a></li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<p>To get notified of the updates on this website, you can subscribe to its <a href="{{ sitemeta.feedPath }}"><svg class="inline-icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M0 64C0 46.3 14.3 32 32 32c229.8 0 416 186.2 416 416c0 17.7-14.3 32-32 32s-32-14.3-32-32C384 253.6 226.4 96 32 96C14.3 96 0 81.7 0 64zM0 416a64 64 0 1 1 128 0A64 64 0 1 1 0 416zM32 160c159.1 0 288 128.9 288 288c0 17.7-14.3 32-32 32s-32-14.3-32-32c0-123.7-100.3-224-224-224c-17.7 0-32-14.3-32-32s14.3-32 32-32z"/></svg>RSS feed</a>, which contains new articles, blog posts and website changelogs.</p>
|
||||
|
||||
{{ content | safe }}
|
||||
|
||||
{%- css %}
|
||||
.changelog__nav,
|
||||
.changelog__nav--links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.changelog__nav {
|
||||
flex-direction: column;
|
||||
gap: 0.2em;
|
||||
align-self: center;
|
||||
font-weight: 700;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.changelog__nav--links li {
|
||||
text-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.changelog__nav--links [aria-current="page"] {
|
||||
text-decoration: none;
|
||||
color: var(--clr-bold-txt);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.changelog__nav {
|
||||
flex-direction: row;
|
||||
gap: 1em;
|
||||
}
|
||||
}
|
||||
{% endcss %}
|
23
src/_includes/main/content-nav.njk
Normal file
@ -0,0 +1,23 @@
|
||||
<aside class="right-sidebar" aria-label="Right sidebar">
|
||||
<nav class="content__nav sidebar--sticky" aria-labelledby="my-contents-title">
|
||||
<h2 class="content__nav--title" id="my-contents-title">My Contents</h2>
|
||||
<ul class="content__nav--links">
|
||||
{% for archive in collections.archive %}
|
||||
<li><a href="{{ archive.url }}">{{ archive.data.title }}</a></li>
|
||||
{% endfor %}
|
||||
<li><a href="/articles/">Articles</a></li>
|
||||
{% for page in collections["blog pages"] %}
|
||||
<li><a href="{{ page.url }}">{{ page.data.navTitle or page.data.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{%- css %}
|
||||
.content__nav {
|
||||
padding: 1.2em clamp(1em, 5%, 1.5em);
|
||||
background-color: var(--clr-content-bg);
|
||||
}
|
||||
|
||||
.content__nav--title { font-size: 1.7rem; }
|
||||
{% endcss %}
|
5
src/_includes/main/content.njk
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: main/base
|
||||
---
|
||||
|
||||
{% extends "global/content.njk" %}
|
29
src/_includes/main/footer.njk
Normal file
@ -0,0 +1,29 @@
|
||||
{% extends "global/footer.njk" %}
|
||||
|
||||
{% block footerContent %}
|
||||
<h2 class="visually-hidden">Footer Navigation:</h2>
|
||||
{% set currentUrl %}{{ page.url }}{% endset %}
|
||||
<ul class="inline-nav footer__links">
|
||||
<li>
|
||||
<a href="{{ sitemeta.feedPath }}"><svg class="inline-icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M0 64C0 46.3 14.3 32 32 32c229.8 0 416 186.2 416 416c0 17.7-14.3 32-32 32s-32-14.3-32-32C384 253.6 226.4 96 32 96C14.3 96 0 81.7 0 64zM0 416a64 64 0 1 1 128 0A64 64 0 1 1 0 416zM32 160c159.1 0 288 128.9 288 288c0 17.7-14.3 32-32 32s-32-14.3-32-32c0-123.7-100.3-224-224-224c-17.7 0-32-14.3-32-32s14.3-32 32-32z"/></svg>RSS Feed</a>
|
||||
</li>
|
||||
<li><a
|
||||
{% if currentUrl === "/sitemap/" %}aria-current="page"{% endif %}
|
||||
href="/sitemap/
|
||||
">Site Map</a></li>
|
||||
{%- for link in collections["footer links"] -%}
|
||||
<li><a
|
||||
{% if link.url == page.url %}aria-current="page"{% endif %}
|
||||
href="{{ link.url }}"
|
||||
>{{ link.data.title }}</a></li>
|
||||
{%- endfor -%}
|
||||
{%- for statement in collections.statements -%}
|
||||
<li><a
|
||||
{% if statement.url == page.url %}aria-current="page"{% endif %}
|
||||
href="{{ statement.url }}"
|
||||
>{{ statement.data.title }}</a></li>
|
||||
{%- endfor -%}
|
||||
</ul>
|
||||
<p>Made with ♥ by {{ sitemeta.siteAuthor.name }} since 11 September 2022</p>
|
||||
{% endblock %}
|
||||
|
141
src/_includes/main/hero.njk
Normal file
@ -0,0 +1,141 @@
|
||||
{% extends "global/hero.njk" %}
|
||||
|
||||
{% block heroImg %}
|
||||
<img fetchpriority="high" src="/assets/leilukin/Leilukins-Hub-website-banner.avif" alt="Banner of Leilukin's Hub" width="900" height="300">
|
||||
{% endblock %}
|
||||
|
||||
{% set heroDropShadow %}
|
||||
drop-shadow(0.1rem 0.1rem 0.2rem black)
|
||||
drop-shadow(0.1rem 0.1rem 0.2rem rgba(30, 30, 30, 0.8))
|
||||
{% endset%}
|
||||
{%- css %}.hero img { filter: {{ heroDropShadow }}; }{% endcss %}
|
||||
|
||||
{% block eventScript %}
|
||||
const todayEvent = getTodayEvent();
|
||||
|
||||
if (todayEvent) {
|
||||
heroTopBarEl.classList.remove('hidden');
|
||||
heroTopBarEl.innerHTML = todayEvent.blurb;
|
||||
|
||||
if (todayEvent.class) {
|
||||
headerImgEl.classList.add(todayEvent.class);
|
||||
}
|
||||
}
|
||||
|
||||
function getTodayEvent() {
|
||||
const date = new Date();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const year = date.getFullYear();
|
||||
const weekOfMonth = Math.ceil(day / 7);
|
||||
|
||||
const leilukinsHubLaunchDate = new Date("2022-09-11").getFullYear();
|
||||
const siteAnniversary = year - leilukinsHubLaunchDate;
|
||||
|
||||
if (month === 3 && day === 1)
|
||||
return {
|
||||
blurb: `Today is <a href="https://www.unaids.org/en/zero-discrimination-day">Zero Discrimination Day</a>`,
|
||||
class: "flag-progress-intersex",
|
||||
};
|
||||
else if (month === 3 && day === 8)
|
||||
return {
|
||||
blurb: `Today is <a href="https://www.internationalwomensday.com/">International Women's Day</a>`,
|
||||
class: "symbol-venus",
|
||||
};
|
||||
else if (month === 3 && day === 31)
|
||||
return {
|
||||
blurb: `Today is <a href="https://www.manygendersonevoice.org/tdov.html">Trans Day of Visibility</a>`,
|
||||
class: "flag-trans",
|
||||
};
|
||||
else if (month === 4 && day === 6)
|
||||
return {
|
||||
blurb: `Today is <a href="https://internationalasexualityday.org/en">International Asexuality Day</a>`,
|
||||
class: "flag-ace",
|
||||
};
|
||||
else if (month === 4 && day === 26)
|
||||
return {
|
||||
blurb: `Today is <a href="https://www.lesbianvisibilityweek.com">Lesbian Visibility Day</a>`,
|
||||
class: "flag-lesbian",
|
||||
};
|
||||
else if (month === 4 && weekOfMonth === 4)
|
||||
return {
|
||||
blurb: `This week is <a href="https://www.lesbianvisibilityweek.com">Lesbian Visibility Week</a>`,
|
||||
class: "flag-lesbian",
|
||||
};
|
||||
else if (month === 5 && day === 17)
|
||||
return {
|
||||
blurb: `Today is <a href="https://may17.org">International Day Against Homophobia, Biphobia and Transphobia</a>`,
|
||||
class: "flag-progress",
|
||||
};
|
||||
else if (month === 5 && day === 19)
|
||||
return {
|
||||
blurb: `Today is <a href="https://www.believeoutloud.com/voices/article/agender-pride-day/">Agender Pride Day</a>`,
|
||||
class: "flag-agender",
|
||||
};
|
||||
else if (month === 5 && day === 25)
|
||||
return {
|
||||
blurb: `Today is <a href="https://www.papyrus-uk.org/pan-visibility-day/">Pansexual and Panromantic Awareness and Visibility Day</a>`,
|
||||
class: "flag-pan",
|
||||
};
|
||||
else if (month === 6)
|
||||
return {
|
||||
blurb: `Happy <a href="https://www.loc.gov/lgbt-pride-month/about/">Pride Month</a>!`,
|
||||
class: "flag-progress-intersex",
|
||||
};
|
||||
else if (month === 7 && day === 14)
|
||||
return {
|
||||
blurb: `Today is <a href="https://www.manygendersonevoice.org/non-binary-peoples-day.html">Non-Binary People's Day</a>`,
|
||||
class: "flag-non-binary",
|
||||
};
|
||||
else if (month === 7 && day === 28)
|
||||
return {
|
||||
blurb: `Today is Leilukin's Birthday`
|
||||
};
|
||||
else if (month === 7)
|
||||
return{
|
||||
blurb: `Happy <a href="https://diversity.ldeo.columbia.edu/heritage-months/disability-pride">Disability Pride Month</a>!`,
|
||||
class: "flag-disability"
|
||||
}
|
||||
else if (month === 8 && day === 25)
|
||||
return {
|
||||
blurb: `Today is <a href="https://aromanticspectrumday.net/">Aromantic Spectrum Visibility Day</a>`,
|
||||
class: "flag-aro",
|
||||
};
|
||||
else if (month === 9 && day === 11)
|
||||
return {
|
||||
blurb: `Today is the ${siteAnniversary}-year anniversary of the launch of {{ sitemeta.siteName }}`
|
||||
};
|
||||
else if (month === 9 && day === 23)
|
||||
return {
|
||||
blurb: `Today is <a href="https://bivisibilityday.com/about">Bi Visibility Day</a>`,
|
||||
class: "flag-bi",
|
||||
};
|
||||
else if (month === 10 && day === 8)
|
||||
return {
|
||||
blurb: `Today is <a href="https://www.lgbtiqhealth.org.au/international_lesbian_day2">International Lesbian Day</a>`,
|
||||
class: "flag-lesbian",
|
||||
};
|
||||
else if (month === 10 && day === 11)
|
||||
return {
|
||||
blurb: `Today is <a href="https://www.hrc.org/resources/national-coming-out-day">National Coming Out Day</a>`,
|
||||
class: "flag-rainbow",
|
||||
};
|
||||
else if (month === 10 && day === 17)
|
||||
return {
|
||||
blurb: `Today is the start of <a href="https://www.grlgbtqhealthcareconsortium.org/significantdates/genderfluid-visibility-week">Genderfluid Visibility Week</a>`,
|
||||
class: "flag-genderfluid",
|
||||
};
|
||||
else if (month === 10 && day === 26)
|
||||
return {
|
||||
blurb: `Today is <a href="https://interactadvocates.org/intersex-awareness-day">Intersex Awareness Day</a>`,
|
||||
class: "flag-intersex",
|
||||
};
|
||||
else if (month === 11 && day === 13)
|
||||
return {
|
||||
blurb: `Today is the start of <a href="https://glaad.org/transweek/">Transgender Awareness Week</a>`,
|
||||
class: "flag-trans",
|
||||
};
|
||||
else
|
||||
return null;
|
||||
}
|
||||
{% endblock %}
|
48
src/_includes/main/links.njk
Normal file
@ -0,0 +1,48 @@
|
||||
---
|
||||
layout: main/content.njk
|
||||
articleElement: true
|
||||
isContentDivided: true
|
||||
---
|
||||
{% set linkGroups = collections["link groups"] %}
|
||||
|
||||
<section class="content__section">
|
||||
<h2>Link to My Website</h2>
|
||||
{% include "main/my-button.njk" %}
|
||||
</section>
|
||||
|
||||
<section class="content__section">
|
||||
<h2>Links by Category</h2>
|
||||
|
||||
<p>Use the following tabs to view my link collection by category.</p>
|
||||
<nav aria-labelledby="tabs-title">
|
||||
<h3 class="visually-hidden" id="tabs-title">Tabs Group</h3>
|
||||
<ul class="tabs">
|
||||
{% for group in linkGroups %}
|
||||
<li><a class="tab__btn" href="#{{ group.data.title | slugify }}">{{ group.data.title }}</a></li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
<article class="tabwrap">
|
||||
{% for group in linkGroups %}
|
||||
<section id="{{ group.data.title | slugify }}" class="content__section">
|
||||
{% headingAnchor 2 %}{{ group.data.title }}{% endheadingAnchor %}
|
||||
|
||||
{% if group.data.toc %}
|
||||
<div class="tabwrap__toc">
|
||||
<h3>Table of Contents</h3>
|
||||
{{ group.content | toc | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ group.content | safe }}
|
||||
|
||||
{% if group.data.updated %}
|
||||
<p class="update-info">(This {{ group.data.title }} list was last updated on <time datetime="{{ group.data.updated }}">{{ group.data.updated | formatDate }}</time>)</p>
|
||||
{% endif %}
|
||||
|
||||
<a href="#linktabs" class="tab__btn tab__back">Back to tabs</a>
|
||||
</section>
|
||||
{%- endfor %}
|
||||
</article>
|
13
src/_includes/main/my-button.njk
Normal file
@ -0,0 +1,13 @@
|
||||
<p>You can link to my website using my site button. I strongly recommend you to upload my site button to your own website instead of hotlinking the button.</p>
|
||||
|
||||
<div class="web-graphics my-btn">
|
||||
<img src="/assets/leilukin/Leilukins-Hub-button-88x31.png" alt="Leilukin's Hub website button with 88×31 pixel size" width="88" height="31" loading="lazy">
|
||||
<img src="/assets/leilukin/Leilukins-Hub-button-200x40.png" alt="Leilukin's Hub website button with 200×40 pixel size" width="200" height="40" loading="lazy">
|
||||
</div>
|
||||
|
||||
{%- css %}
|
||||
.my-btn {
|
||||
align-items: center;
|
||||
margin-top: 1em;
|
||||
}
|
||||
{% endcss %}
|
11
src/_includes/main/navbar.njk
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "global/navbar.njk" %}
|
||||
|
||||
{% block navbarLinks %}
|
||||
{% set currentUrl %}{{ page.url }}{% endset %}
|
||||
{% set navPages = collections["navbar links"] | eleventyNavigation %}
|
||||
{%- for entry in navPages %}
|
||||
<li>
|
||||
<a {% if entry.url == page.url %}aria-current="page"{% endif %} href="{{ entry.url }}">{{ entry.title }}</a>
|
||||
</li>
|
||||
{%- endfor %}
|
||||
{% endblock %}
|
9
src/_includes/main/slashpage.njk
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
layout: main/content
|
||||
---
|
||||
|
||||
{{ content | safe }}
|
||||
|
||||
{% if updated %}
|
||||
<p class="update-info">(This {{ keyword if keyword else "page" }} was last updated on <time datetime="{{ updated }}">{{ updated | formatDate }}</time>)</p>
|
||||
{% endif %}
|
39
src/_includes/main/statuscafe.njk
Normal file
@ -0,0 +1,39 @@
|
||||
<div id="statuscafe" class="text-box">
|
||||
<h3>My Lastest Status Update</h3>
|
||||
<div id="statuscafe-userinfo"></div>
|
||||
<div id="statuscafe-content">
|
||||
<noscript>
|
||||
<p>Looks like you have JavaScript disabled. JavaScript is required to fetch my latest status from status.cafe and display it here.</p>
|
||||
</noscript>
|
||||
</div>
|
||||
|
||||
<p><a href="https://status.cafe/users/leilukin">View my statuses on status.cafe</a></p>
|
||||
</div>
|
||||
|
||||
{%- css %}#statuscafe-content { margin-bottom: 0.5em; }{% endcss %}
|
||||
|
||||
{%- js %}
|
||||
const statusCafeContent = document.getElementById("statuscafe-content");
|
||||
statusCafeContent.innerHTML = `
|
||||
<p>Fetching data from status.cafe...</p>
|
||||
`;
|
||||
|
||||
const fetchStatusCafe = async () => {
|
||||
try {
|
||||
const res = await fetch('https://status.cafe/users/leilukin/status.json');
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.content.length) {
|
||||
document.getElementById("statuscafe-content").innerHTML = "No status yet."
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("statuscafe-userinfo").innerHTML = data.face + ' ' + data.timeAgo;
|
||||
statusCafeContent.innerHTML = data.content;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
document.getElementById("statuscafe-content").textContent = `[ERROR] ${error}`;
|
||||
}
|
||||
};
|
||||
fetchStatusCafe();
|
||||
{% endjs %}
|
15
src/_includes/main/support-me.njk
Normal file
@ -0,0 +1,15 @@
|
||||
<p>If you enjoy my work, you can support me on <a href="https://ko-fi.com/leilukin">Ko-Fi</a>.</p>
|
||||
<div class="support-me">
|
||||
<!-- Ko-Fi button -->
|
||||
<a href="https://ko-fi.com/A1042UH4"><img src="/assets/buttons/misc/ko-fi.webp" alt="Buy Me a Coffee at ko-fi.com" width="141" height="36" loading="lazy"></a>
|
||||
</div>
|
||||
|
||||
{%- css %}
|
||||
.support-me {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
margin-top: 0.7em;
|
||||
}
|
||||
{% endcss %}
|
56
src/_includes/main/webcliques.njk
Normal file
@ -0,0 +1,56 @@
|
||||
{%- css %}.color-bg { padding: 0.125em 0.3em; }{% endcss %}
|
||||
|
||||
{%- macro webCliqueImg(file, width, height, alt="") -%}
|
||||
<img src="/assets/cliques/{{ file }}" alt="{{ alt }}" class="inline-img" width="{{ width }}" height="{{ height }}" loading="lazy">
|
||||
{%- endmacro -%}
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
{{ webCliqueImg("30plusclub.gif", 88, 31) }}
|
||||
<a href="https://moonpr1sm.com/random/30plusclub">30+ Club</a>
|
||||
</li>
|
||||
<li><a href="http://cliqued.wings.nu/">rainbow</a> 🌈 <span class="color-bg" style="background-color: black; color: white;">black</span> and <span class="color-bg" style="background-color: purple; color: white;">purple</span></li>
|
||||
<li><a href="http://aromatic.wings.nu/">aromatic</a> // lavender</li>
|
||||
<li>
|
||||
<a href="https://dogspit.nekoweb.org/dogperson.html">dogperson</a> -
|
||||
{{ webCliqueImg("dogperson.png", 14, 14) }} Pomeranians and Schipperkes
|
||||
</li>
|
||||
<li>My favourite hot bevarage is <a href="https://cliques.moudoku.com/hot">Coffee</a></li>
|
||||
<li>
|
||||
{{ webCliqueImg("caffeineNATION.png", 80, 15) }}
|
||||
<a href="https://caffeinated.wings.nu/">caffeineNATION</a> ☕︎ mocha latte
|
||||
</li>
|
||||
<li>My favourite food is: <a href="https://frankie.fanacular.net/food">Mushrooms</a></li>
|
||||
<li>
|
||||
<a href="http://cliqued.wings.nu/">OTF</a>
|
||||
{{ webCliqueImg("OTF.png", 15, 16) }}
|
||||
<a href="/shrines/cassettebeasts"><cite>Cassette Beasts</cite></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://ballonlea.net/trickyfox/">LINKED !</a>
|
||||
{{ webCliqueImg("linked.png", 20, 20) }}
|
||||
<a href="https://wiki.cassettebeasts.com/wiki/Meredith">Meredith Chen</a>
|
||||
</li>
|
||||
<li>
|
||||
My <a href="https://frankie.fanacular.net/otp">One True Pairing</a> is: Sam Wong and Michelle Cheung (<a href="/shrines/asummersend"><cite>A Summer’s End — Hong Kong 1986</cite></a>)
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://starwars.fandom.com/wiki/Juhani">Juhani</a> is the <a href="https://cliques.moudoku.com/guardian">guardian</a> of this website
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://cliqued.wings.nu/">unexpected song</a>
|
||||
{{ webCliqueImg("unexpectedsong.png", 16, 16) }}
|
||||
<a href="https://bulbapedia.bulbagarden.net/wiki/Steven_Stone">Steven Stone</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://pkmn.caelestis.nu" rel="noopener noreferrer">I Choose You!</a> :: Breloom
|
||||
{{ webCliqueImg("breloom.png", 23, 25) }}
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://cliqued.wings.nu/">my jam!!</a>
|
||||
{{ webCliqueImg("myjam.png", 16, 16) }}
|
||||
<a href="https://www.youtube.com/watch?v=lP2-68Y0onM">"Same Old Story"</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Pixel cliques I joined can be found on my <a href="/adoptables">Adoptables page</a>.</p>
|
12
src/_includes/misc/404.njk
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
layout: misc/base
|
||||
title: "404: Page Not Found"
|
||||
h1: "404: Page Not Found"
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
|
||||
<p>Oops! Either the page you are looking for does not exist, or it has been moved to a diffrent part of this site.</p>
|
||||
|
||||
<section class="index__btn-wrapper">
|
||||
<a href="/" class="index__link">🏠 Back to Home</a>
|
||||
</section>
|
29
src/_includes/misc/base.njk
Normal file
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
{# Open Graph meta #}
|
||||
<meta property="og:title" content="{% if title %}{{ title }}{% else %}{{ sitemeta.siteName }}{% endif %}">
|
||||
{% include "global/meta.njk" %}
|
||||
|
||||
{# Feeds #}
|
||||
{% include "global/feeds.njk" %}
|
||||
|
||||
{# Style #}
|
||||
{% include "global/fonts.njk" %}
|
||||
<link rel="stylesheet" href="/assets/css/misc.css">
|
||||
|
||||
<title>{% if title %} {{ title }} | {% endif %} {{ sitemeta.siteName }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<img src="/assets/leilukin/Leilukins-Hub-website-banner.avif" alt="{{ sitemeta.siteName }} website banner">
|
||||
<h1>{{ h1 }}</h1>
|
||||
|
||||
{{ content | safe }}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
19
src/_includes/pokemonoras/base.njk
Normal file
@ -0,0 +1,19 @@
|
||||
{%- css %}{% include "src/assets/css/pokemonoras.css" %}{%- endcss %}
|
||||
|
||||
{% extends "global/baselayout.njk" %}
|
||||
|
||||
{% block metaTitle %}
|
||||
<meta property="og:title" content="{{ title + ' | ' if title }}Pokémon Omega Ruby and Alpha Sapphire Shrine">
|
||||
{% endblock %}
|
||||
|
||||
{% block pageTitle %}
|
||||
{{ title + " | " if title }}Pokémon Omega Ruby and Alpha Sapphire Shrine | {{ sitemeta.siteName | safe }}
|
||||
{% endblock %}
|
||||
|
||||
{% block favicon %}
|
||||
<link rel="icon" type="image/x-icon" href="/assets/shrines/pokemonoras/images/favicon.ico">
|
||||
{% endblock %}
|
||||
|
||||
{% block hero %}{% include "pokemonoras/hero.njk" %}{% endblock %}
|
||||
{% block navbar %}{% include "pokemonoras/navbar.njk" %}{% endblock %}
|
||||
{% block footer %}{% include "pokemonoras/footer.njk" %}{% endblock %}
|
7
src/_includes/pokemonoras/content.njk
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
layout: pokemonoras/base
|
||||
---
|
||||
|
||||
{% extends "global/content.njk" %}
|
||||
|
||||
{% block shrineInfo %}{% include "pokemonoras/shrineinfo.njk" %}{% endblock %}
|
6
src/_includes/pokemonoras/footer.njk
Normal file
@ -0,0 +1,6 @@
|
||||
{% extends "global/footer.njk" %}
|
||||
|
||||
{% block footerContent %}
|
||||
<p>Made with ♥ by {{ sitemeta.siteAuthor.name }} • Shrine Launched: 21 November 2024</p>
|
||||
{% endblock %}
|
||||
|
34
src/_includes/pokemonoras/hero.njk
Normal file
@ -0,0 +1,34 @@
|
||||
{% extends "global/hero.njk" %}
|
||||
|
||||
{% block heroImg %}
|
||||
<picture>
|
||||
<source srcset="/assets/shrines/pokemonoras/images/pokemonoras-header-320.avif" media="(orientation: landscape)" />
|
||||
<img src="/assets/shrines/pokemonoras/images/pokemonoras-header-640.avif" alt="Banner of Pokémon Omega Ruby and Alpha Sapphire Shrine" />
|
||||
</picture>
|
||||
{% endblock %}
|
||||
|
||||
{% block eventScript %}
|
||||
const todayEvent = getTodayEvent();
|
||||
|
||||
if (todayEvent) {
|
||||
heroTopBarEl.classList.remove('hidden');
|
||||
heroTopBarEl.innerHTML = todayEvent;
|
||||
}
|
||||
|
||||
function getTodayEvent() {
|
||||
const date = new Date();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const year = date.getFullYear();
|
||||
|
||||
const orasReleaseDate = new Date("2014-11-21").getFullYear();
|
||||
const orasAnniversary = year - orasReleaseDate;
|
||||
|
||||
if (month === 11 && day === 21)
|
||||
return `
|
||||
Today is the ${orasAnniversary}-year anniversary of the release of <cite>Pokémon Omega Ruby"</cite> and <cite>Alpha Sapphire</cite>!
|
||||
`;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
{% endblock %}
|
11
src/_includes/pokemonoras/navbar.njk
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "global/navbar.njk" %}
|
||||
{% set shrineHomeUrl = "/shrines/pokemonoras/" %}
|
||||
|
||||
{% block navbarLinks %}
|
||||
{% set navPages = collections.all | eleventyNavigation("ORAS Shrine") %}
|
||||
{%- for entry in navPages %}
|
||||
<li>
|
||||
<a {% if entry.url == page.url %}aria-current="page"{% endif %} href="{{ entry.url }}">{{ entry.title }}</a>
|
||||
</li>
|
||||
{%- endfor %}
|
||||
{% endblock %}
|
10
src/_includes/pokemonoras/shrineinfo.njk
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "global/shrineinfo.njk" %}
|
||||
|
||||
{% block shrineAbout %}
|
||||
<p>Welcome to {{ sitemeta.siteAuthor.name }}'s shrine for {% cite "Pokémon Omega Ruby and Alpha Sapphire" %}, remakes of the 2002 Game Boy Advance role-playing video games {% cite "Pokémon Ruby" %} and {% cite "Pokémon Sapphire" %}. The games are part of the sixth generation of the {% cite "Pokémon" %} main series of video games, developed by Game Freak and published by The Pokémon Company and Nintendo for the Nintendo 3DS.</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block shrineLinks %}
|
||||
<li><a href="https://bulbapedia.bulbagarden.net/wiki/Pok%C3%A9mon_Omega_Ruby_and_Alpha_Sapphire">Bulbapedia</a></li>
|
||||
<li><a href="https://pkmn.redcrown.net/hoenn/"><cite>Pokémon Ruby</cite>, <cite>Sapphire</cite>, <cite>Omega Ruby</cite> and <cite>Alpha Sapphire</cite> fanlisting</a></li>
|
||||
{% endblock %}
|
19
src/_includes/starwarskotor/base.njk
Normal file
@ -0,0 +1,19 @@
|
||||
{%- css %}{% include "src/assets/css/starwarskotor.css" %}{%- endcss %}
|
||||
|
||||
{% extends "global/baselayout.njk" %}
|
||||
|
||||
{% block metaTitle %}
|
||||
<meta property="og:title" content="{{ title + ' | ' if title }}Star Wars: Knights of the Old Republic Shrine">
|
||||
{% endblock %}
|
||||
|
||||
{% block pageTitle %}
|
||||
{{ title + " | " if title }}Star Wars: Knights of the Old Republic Shrine | {{ sitemeta.siteName | safe }}
|
||||
{% endblock %}
|
||||
|
||||
{% block favicon %}
|
||||
<link rel="icon" type="image/x-icon" href="/assets/shrines/starwarskotor/images/favicon.ico">
|
||||
{% endblock %}
|
||||
|
||||
{% block hero %}{% include "starwarskotor/hero.njk" %}{% endblock %}
|
||||
{% block navbar %}{% include "starwarskotor/navbar.njk" %}{% endblock %}
|
||||
{% block footer %}{% include "starwarskotor/footer.njk" %}{% endblock %}
|
7
src/_includes/starwarskotor/content.njk
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
layout: starwarskotor/base
|
||||
---
|
||||
|
||||
{% extends "global/content.njk" %}
|
||||
|
||||
{% block shrineInfo %}{% include "starwarskotor/shrineinfo.njk" %}{% endblock %}
|
6
src/_includes/starwarskotor/footer.njk
Normal file
@ -0,0 +1,6 @@
|
||||
{% extends "global/footer.njk" %}
|
||||
|
||||
{% block footerContent %}
|
||||
<p>Made with ♥ and the Force by {{ sitemeta.siteAuthor.name }} • Shrine Launched: 17 February 2023</p>
|
||||
{% endblock %}
|
||||
|
40
src/_includes/starwarskotor/hero.njk
Normal file
@ -0,0 +1,40 @@
|
||||
{% extends "global/hero.njk" %}
|
||||
|
||||
{% block heroImg %}
|
||||
<picture>
|
||||
<source srcset="/assets/shrines/starwarskotor/images/swkotor-header-320.avif" media="(orientation: landscape)" />
|
||||
<img src="/assets/shrines/starwarskotor/images/swkotor-header.avif" alt="anner of Star Wars: Knights of the Old Republic Shrine" />
|
||||
</picture>
|
||||
{% endblock %}
|
||||
|
||||
{% block eventScript %}
|
||||
const todayEvent = getTodayEvent();
|
||||
|
||||
if (todayEvent) {
|
||||
heroTopBarEl.classList.remove('hidden');
|
||||
heroTopBarEl.innerHTML = todayEvent;
|
||||
}
|
||||
|
||||
function getTodayEvent() {
|
||||
const date = new Date();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const year = date.getFullYear();
|
||||
|
||||
const kotor1ReleaseDate = new Date("2003-07-15").getFullYear();
|
||||
const kotor2ReleaseDate = new Date("2004-12-06").getFullYear();
|
||||
const kotor1Anniversary = year - kotor1ReleaseDate;
|
||||
const kotor2Anniversary = year - kotor2ReleaseDate;
|
||||
|
||||
if (month === 7 && day === 15)
|
||||
return `
|
||||
Today is the ${kotor1Anniversary}-year anniversary of the release of <cite>Star Wars: Knihgts of the Old Republic</cite>
|
||||
`;
|
||||
else if (month === 12 && day === 6)
|
||||
return `
|
||||
Today is the ${kotor2Anniversary}-year anniversary of the release of <cite>Star Wars: Knights of the Old Republic II — The Sith Lords</cite>
|
||||
`;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
{% endblock %}
|
67
src/_includes/starwarskotor/modlist-info.md
Normal file
@ -0,0 +1,67 @@
|
||||
{% set kotor2FullTitle %}{{ " II: The Sith Lords" if page.url.includes("kotor2") else ""}}{% endset %}
|
||||
{% set kotor2ShortTitle %}{{ " 2" if page.url.includes("kotor2") else ""}}{% endset %}
|
||||
|
||||
{% container "article", "content__section" %}
|
||||
## Spoiler Warning
|
||||
|
||||
WARNING: The full list of my mod build contains spoilers for <cite>Star Wars: Knights of the Old Republic{{ kotor2FullTitle }}</cite>!
|
||||
|
||||
My mod build is not designed for new <cite>KotOR{{ kotor2ShortTitle }}</cite> players, so I will not censor any information that contains spoilers for the game. In general, I do not recommend using mods in your very first playthrough of any video game, so you can judge and choose which mods you want to use after finishing the game for the first time. The only exceptions are the [KotOR 1 Community Patch](https://deadlystream.com/files/file/1258-kotor-1-community-patch/) for {% cite "KotOR 1" %} and [The Sith Lords Restored Content Mod](https://deadlystream.com/files/file/578-tsl-restored-content-mod/) for <cite>KotOR{{ kotor2ShortTitle }}</cite>, which are absolutely essential mods that even if you want to use them in your first playthrough, you definitely have my support.
|
||||
|
||||
If you are going to play <cite>KotOR{{ kotor2ShortTitle }}</cite> for the first time, and you do not mind using mods, I would suggest checking out the Spoiler-Free version of [KotOR Community Portal’s KotOR mod builds](https://kotor.neocities.org/modding/) instead.
|
||||
{% endcontainer %}
|
||||
|
||||
{% container "article", "content__section" %}
|
||||
## Mod Categories
|
||||
|
||||
The mods I included in my mod build are divided into the following categories, which clarifies the types of changes the mod makes:
|
||||
|
||||
Added Content
|
||||
: Added content is an entirely fan-made new content that is not intended in the vanilla game.
|
||||
|
||||
Appearance Change
|
||||
: Appearance change is not inherently graphics improvement, but rather a modification to the default appearance of a character, item, location, interface, etc.
|
||||
|
||||
Bugfix
|
||||
: As the name implies, this type of mod fixes bugs with the vanilla game.
|
||||
|
||||
Graphics Improvement
|
||||
: This type of mod improves the graphics in the vanilla game in some way, by retexturing and/or remodeling a character, item, location, etc. Usually these mods come with high-resolution textures.
|
||||
|
||||
Immersion
|
||||
: These mods are not inherently bugfixes, but changes done to improve the internal consistency of the game’s content.
|
||||
|
||||
Mechanics Change
|
||||
: This type of mod makes changes to the game’s system which directly impacts the way you play the game, varying from changing the camera angle of a location to altering the core stats of a class.
|
||||
|
||||
Modified Content
|
||||
: Modified content is not inherently new content, but rather alterations to the vanilla game’s content because the vanilla content has some shortcomings. The changes made by this kind of mod were never intended to be in the vanilla game.
|
||||
|
||||
Patch
|
||||
: A patch is a simple mod that changes the function of another mod, either by fixing bugs, resolving mod conflicts or altering content made by another mod.
|
||||
|
||||
Restored Content
|
||||
: This type of mod restores content that has been cut from the vanilla game.
|
||||
|
||||
Sound Change
|
||||
: As the name implies, these mods make changes to the sound in the vanilla game. The sound could be music or ambient audios.
|
||||
{% endcontainer %}
|
||||
|
||||
{% container "article", "content__section" %}
|
||||
## Mod Tiers
|
||||
|
||||
The tiers for each mod included in my mod build are not necessarily a judgement on the quality of the mods themselves. In my mod builds, mod tiers are ranked on a scale of 1-4, based on how important the individual mods are for my experience with the {% cite "KotOR" %} games.
|
||||
|
||||
Tier 1 - Essential
|
||||
: This tier indicates mods that I cannot play the {% cite "KotOR" %} games without. Usually this is because I find those mods make very crucial changes to the game, typically by fixing things that bother me the most in the vanilla game. Similarly, mods of the Patch category that belong to this tier make critical changes to other mods. I consider excluding Tier 1 mods will negatively affect my experience with the {% cite "KotOR" %} games.
|
||||
|
||||
Tier 2 - Very Important
|
||||
: This tier indicates mods that vastly improve my experience with the game. Mods belong to this tier are the bread and butter of my mod build. That said, unlike Tier 1 mods, excluding Tier 2 mods from my playthrough does not actively make my {% cite "KotOR" %} experience worse either.
|
||||
|
||||
Tier 3 - Somewhat Important
|
||||
: This tier indicates mods that make changes that are in smaller scope or more subjective compared to Tier 1 and Tier 2 mods. Usually this is because the changes made by Tier 3 mods are less noticeable or more of a matter of personal taste.
|
||||
|
||||
Tier 4 - Optional
|
||||
: This tier indicates mods that make even smaller or more subjective changes than Tier 3 mods, to the point that using these mods is optional. I include Tier 4 mods in my build for the purpose of maximizing my immersion, or because I use these mods for customization purposes for my characters.
|
||||
{.deflist-1col}
|
||||
{% endcontainer %}
|
11
src/_includes/starwarskotor/navbar.njk
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "global/navbar.njk" %}
|
||||
{% set shrineHomeUrl = "/shrines/starwarskotor/" %}
|
||||
|
||||
{% block navbarLinks %}
|
||||
{% set navPages = collections.all | eleventyNavigation("KotOR Shrine") %}
|
||||
{%- for entry in navPages %}
|
||||
<li>
|
||||
<a {% if entry.url == page.url %}aria-current="page"{% endif %} href="{{ entry.url }}">{{ entry.title }}</a>
|
||||
</li>
|
||||
{%- endfor %}
|
||||
{% endblock %}
|
11
src/_includes/starwarskotor/shrineinfo.njk
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "global/shrineinfo.njk" %}
|
||||
|
||||
{% block shrineAbout %}
|
||||
<p>Welcome to {{ sitemeta.siteAuthor.name }}'s shrine for {% cite "Star Wars: Knights of the Old Republic" %}, a series of role-playing video games set in the {% cite "Star Wars" %} universe, taking place almost 4,000 years before the events of the Skywalker film saga.</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block shrineLinks %}
|
||||
<li><a href="https://kotor.neocities.org/">KOTOR Community Portal</a></li>
|
||||
<li><a href="https://glitterskies.org/kotor/"><cite>Star Wars: KotOR</cite> fanlisting</a></li>
|
||||
<li><a href="https://glitterskies.org/kotor2/"><cite>Star Wars: KotOR II</cite> fanlisting</a></li>
|
||||
{% endblock %}
|
13
src/articles/articles.11tydata.js
Normal file
@ -0,0 +1,13 @@
|
||||
export default {
|
||||
tags: ["articles", "contents", "feed items"],
|
||||
layout: "main/content",
|
||||
permalink: "/articles/{{ page.fileSlug }}/",
|
||||
isArticle: true,
|
||||
eleventyComputed: {
|
||||
title: (data) => `${data.articleTitle} | Articles`,
|
||||
eleventyNavigation: {
|
||||
key: (data) => data.articleTitle,
|
||||
parent: "Articles"
|
||||
}
|
||||
}
|
||||
}
|
3
src/articles/featured/featured.11tydata.js
Normal file
@ -0,0 +1,3 @@
|
||||
export default {
|
||||
tags: "featured articles"
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
---
|
||||
articleTitle: The modders who spent 15 years fixing Knights of the Old Republic 2 @ Ars Technica
|
||||
date: 2022-09-11
|
||||
desc: Ars Technica's article on the KotOR 2's modding community to celebrate the game's 15th anniversary. I was interviewed for my same-gender romance mods for KotOR 2.
|
||||
categories: ["my interviews", "star wars kotor 2", "video game mods"]
|
||||
---
|
||||
|
||||
On 6 December 2019, to celebrate the 15th anniversary of {% cite "Star Wars: Knights of the Old Republic 2" %}, Ars Technica has published [an article about the modding community of KotOR 2](https://arstechnica.com/gaming/2019/12/the-modders-that-spent-15-years-fixing-knights-of-the-old-republic-2/). I was interviewed by the author of this article, Austin Taylor, for my same-gender romance mods for {% cite "KotOR 2" %}.
|
||||
|
||||
> ### Romance? Yes, Please
|
||||
>
|
||||
> [Unlike the first KOTOR](https://starwars.fandom.com/wiki/Juhani), there are no same-gender romance options in Obsidian’s sequel. Leilukin, a modder on the Nexus Forums, has focused much of her modding work since 2016 on [fixing that oversight.](https://www.nexusmods.com/kotor2/videogamemods/927)
|
||||
>
|
||||
> “As much as I love KOTOR2, I have always been frustrated by the fact that the romance content with the companions and certain NPCs… are all heterosexual,” she said.
|
||||
>
|
||||
> Some of these fixes are relatively simple. In vanilla KOTOR2, for instance, only male characters can recruit The Handmaiden upon leaving Telos, while female characters are the only ones who can recruit The Disciple during the main quest on Dantooine. By default, Leiluken’s mod simply flips that gender check, so only male characters can recruit the Disciple and only females can recruit The Handmaiden.
|
||||
>
|
||||
> But Leiluken’s mod goes further than that. The most challenging part, she said, was finding every reference to the Exile’s gender throughout the game spoken by gender-specific party members, then “add\[ing\] extra dialogue that replaces the pronouns and splic\[ing\] voiceover files myself.”
|
||||
>
|
||||
> To recruit both The Handmaiden and The Disciple at the same time, players can install the [PartySwap mod by DarthTyren](https://deadlystream.com/files/file/544-partyswap/). That mod was initially lacking the same-gender romance dialogue from Leiluken’s mod. But Leiluken has since added a compatibility installation option, letting the two mods now work together and fully opening up the same-gender romance options of your dreams.
|
||||
|
||||
Read the full article on Ars Technica here:
|
||||
[The modders who spent 15 years fixing Knights of the Old Republic 2](https://arstechnica.com/gaming/2019/12/the-modders-that-spent-15-years-fixing-knights-of-the-old-republic-2/)
|
40
src/articles/featured/lgbtq-booklet-interview-misi-bawang.md
Normal file
@ -0,0 +1,40 @@
|
||||
---
|
||||
articleTitle: "Floating in a Sky Full of Pride: LGBTQ+ Booklet Artist Interview from MISI:Bawang"
|
||||
date: 2022-09-11
|
||||
desc: My submission for MISI:Bawang's booklet about LGBTQ+ Malaysians was accepted and published. Here is MISI:Bawang's interview with me regarding my work and my queer identity.
|
||||
categories: ["my interviews", "lgbtq+", "malaysia", "a summer's end"]
|
||||
---
|
||||
|
||||
<p class="center-text"><i>(This article is also available on <a href="https://leilukinart.tumblr.com/post/676165415142031360/misibawang-a-malaysian-digital-booklet-project">my art blog on Tumblr</a>)</i></p>
|
||||
|
||||
[Floating in a Sky Full of Pride](https://bawangqueerbooklet.msolidariti.org/), the LGBTQ+ booklet of [MISI:Bawang](https://misibawang.msolidariti.org/), a Malaysian digital booklet project, has been released on Valentine’s Day 2022. Since my submitted art piece, Life of a Bee with Pride, has been selected to be included in the booklet, I had received the honour of being interviewed by MISI:Bawang to share my personal experience with my LGBTQ+ identity, favourite queer art/media, and a ✨ special message to the Malaysian LGBTQ+ community. 🏳🌈
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
MISI:Bawang has shared their interview with me and other Malaysian LGBTQ+ artists and authors who submitted their work to the booklet on Twitter and Instagram. Here are the [Twitter thread](https://twitter.com/misi_bawang/status/1470247663471763464) and [Instagram post](https://www.instagram.com/p/CXSlIbsJXsM/) that include my interview.
|
||||
|
||||
Text version of my interview below:
|
||||
|
||||
## **“What was your experience discovering your LGBTQ+ identity?”**
|
||||
|
||||
> The first clear signs of my attraction to women I was aware of started from my secondary school days. However, due to lack of knowledge and awareness of same-gender attraction and identities, I brushed it off as a teenage phase.
|
||||
>
|
||||
> However, that changed as I started becoming active on Tumblr since I was 19. At first, I joined Tumblr to connect with people who share my interest in Star Wars and BioWare games. This led me to get to know LGBTQ+ from different parts of the world, which is especially helped by the fact that BioWare games are known for LGBTQ+ representation, therefore LGBTQ+ people are visible in BioWare player communities. This started my journey of questioning my sexuality and gender.
|
||||
>
|
||||
> As I continue to stay on Tumblr, my engagement with LGBTQ+ people on Tumblr had gone beyond shared interest in entertainment media. Through Tumblr, I discovered many LGBTQ+ resources that helped me figure out my identity. As a result, at the age of 22, I eventually realised that I am a non-binary lesbian.
|
||||
|
||||
## **“Are there any LGBTQ+ related art (books, films, etc) that gives you comfort, inspiration, and strength? If so, tell us about them!”**
|
||||
|
||||
> A Summer’s End - Hong Kong 1986 is the LGBTQ+ art that gives me comfort, inspiration and strength the most. It is an indie visual novel developed by the Asian Canadian studio Oracle and Bone. The story is about a lesbian romance that takes place in Hong Kong in the year of 1986.
|
||||
>
|
||||
> A Summer’s End is the LGBTQ+ art I relate to most because it strongly resonates with my identity as a Cantonese-speaking Chinese lesbian who has grown up with Hong Kong media. I relate to both the protagonists, Michelle and Sam, as their characters represent different parts of my life as a Chinese lesbian. I also relate to the story’s theme of hoping for the future of our homeland despite the political uncertainties.
|
||||
|
||||
## **“Do you have a message to LGBTQ+ community living in Malaysia?”**
|
||||
|
||||
> First and foremost, you are not alone. LGBTQ+ Malaysians have a community that continues to survive despite the hardship we have to face due to society’s bigotry and misunderstanding of LGBTQ+ people and identities.
|
||||
>
|
||||
> I hope the LGBTQ+ community in Malaysia will continue to grow and thrive, and more LGBTQ+ Malaysians who are still questioning and feeling lonely could be reached out. Let us work towards a future where we could live freely.
|
335
src/articles/myarticles/accessible-footnotes.md
Normal file
@ -0,0 +1,335 @@
|
||||
---
|
||||
articleTitle: How I (Tried to) Implement Accessible Footnotes
|
||||
date: 2024-08-06T00:04:00+0800
|
||||
updated: 2024-12-03T23:51:27+0800
|
||||
desc: "How I implement accessible footnotes, at least to the best of my ability. Written for 32-Bit Cafe's Community Code Jam #5."
|
||||
categories: ["32-bit cafe", "accessibility", "html", "css", "eleventy", "markdown-it"]
|
||||
toc: true
|
||||
hasCodeBlock: true
|
||||
---
|
||||
|
||||
[](https://32bit.cafe/~xandra/events/codejam5/){.inline-img}
|
||||
{.center-text}
|
||||
|
||||
(32-Bit Cafe "Back to School" button made by [Loren](https://ribo.zone/)){.center-text}
|
||||
|
||||
[32-Bit Cafe](https://32bit.cafe/) is holding its fifth community code jam, titled ["Back to School"](https://32bit.cafe/~xandra/events/codejam5/), from 4 to 17 August 2024. I have been looking forward to participating in 32-Bit Cafe's community code jam for the first time, so I am excited. This motivates me to finally write a how-to article I have been meaning to do for a while: how to implement accessible footnotes on Leilukin's Hub, or at least, I tried to do so to the best of my abilities.
|
||||
|
||||
On [32-Bit Cafe's Discourse forum](https://discourse.32bit.cafe/), I made a [post on 28 June 2024](https://discourse.32bit.cafe/t/handling-citations-and-or-footnotes/1061/2?u=leilukin) in response to [solaria](https://solaria.neocities.org/)'s thread ["Handling Citations and/or Footnotes"](https://discourse.32bit.cafe/t/handling-citations-and-or-footnotes/1061) to share my methods of adding footnotes on my website. Now, I am writing an extended version of that post of mine in the form of this article, so I could share what I learned about web page footnotes on my website as well.
|
||||
|
||||
(Note: This article assumes a foundational familiarity with HTML and CSS)
|
||||
|
||||
## About Footnotes
|
||||
|
||||
Plagiarism.org [defines footnotes](https://www.plagiarism.org/article/what-are-footnotes) as notes placed at the bottom of a page, and what footnotes do is to cite references or comment on a designated part of the text above it.
|
||||
|
||||
My use case of footnotes is citing sources of information, particularly citing the same source multiple times on the same page when information from the same source is spread across my page. As of this writing, my website pages that use footnotes include the [trivia page of my {% cite "A Summer’s End — Hong Kong 1986" %} shrine 1](/shrines/asummersend/trivia/) and the [facts page of my {% cite "Cassette Beasts" %} shrine](/shrines/cassettebeasts/facts/). You are free to look at the HTML and CSS for reference.
|
||||
|
||||
Footnotes are used both on print and on the web. However, maintaining footnotes on the web can be tedious, especially if you want to update a web page to add or remove them, since you will need to change the number references of existing footnotes.
|
||||
|
||||
## Attempted to Use CSS Counters
|
||||
|
||||
When I was searching for how to implement accessible footnotes, I discovered [Kitty Giraudel](https://kittygiraudel.com/)'s article, ["Accessible Footnotes with CSS"](https://www.sitepoint.com/accessible-footnotes-css/) which teaches the method of using the combination of HTML [`aria-describedby`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby) attribute and [CSS counters](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters) to add numbered references, to reduce the hassle of manually re-numbering all existing footnotes in case you want to update or reorder the references.
|
||||
|
||||
Her article also teaches adding a highlight background colour when heading to a footnote from a reference, as well as providing back links to head back to a reference from a footnote, by using the combination of the backlink Unicode icon (↩) and the HTML `aria-label` attribute with a value of “Back to content”. The uses of ARIA labels make footnotes more screen reader-friendly.
|
||||
|
||||
Furthermore, Kitty created the [eleventy-plugin-footnotes plugin](https://github.com/KittyGiraudel/eleventy-plugin-footnotes) for the static site generator [Eleventy](https://www.11ty.dev/), and wrote a blog post about it, ["Footnotes in 11ty"](https://kittygiraudel.com/2020/12/02/footnotes-in-11ty/). Since I already had begun to [use Eleventy to build my website](/blog/posts/2024-04-21-april-2024-leilukins-hub-overhaul-with-eleventy), I tried Kitty's plugin.
|
||||
|
||||
Unfortunately, a known limitation of using CSS counter method to generate footnotes is [not being able to reference the same footnote multiple times](https://github.com/KittyGiraudel/eleventy-plugin-footnotes/issues/83). As I want to be able to reference the same footnote more than once when I am citing sources on my shrine pages, I had to give up the plugin and look for an alternative.
|
||||
|
||||
## Starting with markdown-it-footnote Plugin's Default Footnote Markup
|
||||
|
||||
Kitty Giraudel's blog post about creating footnotes in Eleventy mentioned [markdown-it-footnote](https://github.com/markdown-it/markdown-it-footnote), the footnotes plugin for [markdown-it](https://github.com/markdown-it/markdown-it) Markdown parser, which is integrated in Eleventy by default to convert Markdown to HTML. Kitty commented on the accessibility shortcomings of markdown-it-footnote in the blog post:
|
||||
|
||||
> [...]it’s not super accessible (let alone by default), even considering all the customisation options. That’s because the footnote references end up being numbers (e.g. [1]) which are meaningless when listed or tabbed through because devoid of their surrounding context.
|
||||
|
||||
That said, since the plugin is customisable, I still wanted to give it a shot to see if I could make configurations to improve its accessibility, so I installed markdown-it-footnote and looked into the plugin's default HTML markup output.
|
||||
|
||||
Here is a sample of what the HTML markup output of markdown-it-footnote looks like:
|
||||
|
||||
```html
|
||||
<p>This is a paragraph with the first footnote reference. <sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup></p>
|
||||
|
||||
<p>Here is the second paragraph with the second footnote reference. <sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup></p>
|
||||
|
||||
<p>This the third paragraph, but with a foootnote reference that points to the first footnote. <sup class="footnote-ref"><a href="#fn1" id="fnref1:1">[1:1]</a></sup></p>
|
||||
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<ol class="footnotes-list">
|
||||
<li id="fn1" class="footnote-item">First footnote <a href="#fnref1" class="footnote-backref">↩︎</a> <a href="#fnref1:1" class="footnote-backref">↩︎</a></li>
|
||||
<li id="fn2" class="footnote-item">Second footnote <a href="#fnref2" class="footnote-backref">↩︎</a></li>
|
||||
</ol>
|
||||
</section>
|
||||
```
|
||||
|
||||
This is what this HTML markup will look like on a live web page:
|
||||
|
||||

|
||||
|
||||
What this HTML markup does:
|
||||
- Adding a footnote reference as a superscript by using the [`<sup>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sup) tag with numbers as the content.
|
||||
- Using a `<hr>` horizontal line element to separate the main body of the page and the footnote section;
|
||||
- Using the `<ol>` tag to render the footnotes as a numbered list;
|
||||
- Each footnote list item has an `id` attribute
|
||||
- If a footnote is referenced more than once, the additional footnote references are labelled with colons.
|
||||
|
||||
(Note: the class names were added by markdown-it-footnote, but if you do not use it, you can rename the class name to whatever you want, and even if you use markdown-it-footnote, you can still change the class names if you want to go deep into it)
|
||||
|
||||
Next step is trying to improve this HTML markup by making it accessible.
|
||||
|
||||
## Make markdown-it-footnote's Markup More Accessible
|
||||
|
||||
### Clarify Footnote Reference Labels
|
||||
|
||||
As pointed out by Kitty Giraudel, labelling the footnote reference as mere numbers like [1] is an accessibility shortcoming, because these reference links become meaningless when being focused on by tabbing through the links, as these reference links would be devoid of context.
|
||||
|
||||
What I did to tackle this was changing the reference labels by adding a word "Footnote" to clarify that these links are for footnotes. For example, [1] becomes [Footnote #1], so when screen readers focus on a footnote reference link, it will be read out as "Footnote number one link".
|
||||
```html
|
||||
<sup class="footnote-ref">
|
||||
<a href="#fn1" id="fnref1">[1]</a>
|
||||
</sup>
|
||||
```
|
||||
|
||||
Optionally, if you want your footnote labels to still display only numbers on screen, you can use a CSS class to [visually hide](https://www.a11yproject.com/posts/how-to-hide-content/) the extra characters in the footnote label. This `.visually-hidden` utility class is a useful tool in your arsenal for accessible web design, as it is often used to hide a content that you do not mean to show on screen, but you still want it to be read by assistive technology.
|
||||
|
||||
```css
|
||||
.visually-hidden {
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
}
|
||||
```
|
||||
|
||||
To make my footnote reference labels display only a number on screen, I use a `span` tag with `visually-hidden` as the value of the `class` attribute to wrap around "Footnote #", so on screen, [Footnote #1] is changed to [1].
|
||||
|
||||
```html
|
||||
<sup class="footnote-ref">
|
||||
<a href="#fn1" id="fnref1">
|
||||
[<span class="visually-hidden">Footnote #</span>1]
|
||||
</a>
|
||||
</sup>
|
||||
```
|
||||
|
||||
### Add ARIA Label to Footnote Back Links
|
||||
|
||||
Next step is adding an ARIA label to the footnote backlinks, by adding an `aria-label` attribute with "Back to reference #[Insert reference number]" as its value. For example:
|
||||
|
||||
```html
|
||||
<a aria-label="Back to reference #1" href="#fnref1" class="footnote-backref">↩︎</a>
|
||||
```
|
||||
|
||||
When a footnote back link is focused on via tabbing, screen readers will read it out as "Back to reference number one link".
|
||||
|
||||
### Add Heading to Footnote Section
|
||||
|
||||
To make it clear that the section is for footnotes, I add a HTML heading before the list of footnotes, such as:
|
||||
```html
|
||||
<h2>Footnotes</h2>
|
||||
```
|
||||
|
||||
## Final HTML Markup for Accessible Footnotes
|
||||
|
||||
Here is my final HTML markup sample to create accessible footnotes:
|
||||
|
||||
```html
|
||||
<p>This is a paragraph with the first footnote reference. <sup class="footnote-ref"><a href="#fn1" id="fnref1">[<span class="visually-hidden">Footnote #</span>1]</a></sup></p>
|
||||
|
||||
<p>Here is the second paragraph with the second footnote reference. <sup class="footnote-ref"><a href="#fn2" id="fnref2">[<span class="visually-hidden">Footnote #</span>2]</a></sup></p>
|
||||
|
||||
<p>This the third paragraph, but with a foootnote reference that points to the first footnote. <sup class="footnote-ref"><a href="#fn1" id="fnref1:1">[<span class="visually-hidden">Footnote #</span>1:1]</a></sup></p>
|
||||
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<h2>Footnotes</h2>
|
||||
<ol class="footnotes-list">
|
||||
<li id="fn1" class="footnote-item">
|
||||
First footnote
|
||||
<a aria-label="Back to reference #1" href="#fnref1" class="footnote-backref">↩︎</a>
|
||||
<a aria-label="Back to reference #1:1" href="#fnref1:1" class="footnote-backref">↩︎</a>
|
||||
</li>
|
||||
<li id="fn2" class="footnote-item">
|
||||
Second footnote
|
||||
<a aria-label="Back to reference #2" href="#fnref2" class="footnote-backref">↩︎</a>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
```
|
||||
|
||||
As for the CSS, in addition to using the `.visually-hidden` utility class, I refer to Kitty Giraudel's ["Accessible Footnotes with CSS"](https://www.sitepoint.com/accessible-footnotes-css/) article to style the highlight background color when heading to a footnote from a reference.
|
||||
|
||||
## Configure markdown-it-footnote in Eleventy's Configuration File
|
||||
|
||||
(If you do not use Eleventy, you may skip this section)
|
||||
|
||||
My final step was to configure the markdown-it-footnote plugin, by editing Eleventy's configuration file (I named mine `eleventy.config.js`), so the plugin would render my accessible footnote HTML markup.
|
||||
|
||||
```js
|
||||
// markdown-it plugins
|
||||
const markdownIt = require("markdown-it");
|
||||
const markdownItFootnote = require("markdown-it-footnote");
|
||||
let markdownLibrary;
|
||||
|
||||
module.exports = function (eleventyConfig) {
|
||||
/* Markdown Overrides */
|
||||
markdownLibrary = markdownIt({
|
||||
html: true,
|
||||
}).use(markdownItFootnote);
|
||||
|
||||
// Configure markdown-it-footnote
|
||||
markdownLibrary.renderer.rules.footnote_block_open = () => (
|
||||
'<hr class="footnotes-sep">\n' +
|
||||
'<section class="footnotes">\n' +
|
||||
`<h2>Footnotes</h2>\n`
|
||||
);
|
||||
|
||||
markdownLibrary.renderer.rules.footnote_anchor = (tokens, idx, options, env, slf) => {
|
||||
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
|
||||
|
||||
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
|
||||
|
||||
/* ↩ with escape code to prevent display as Apple Emoji on iOS */
|
||||
return ` <a aria-label="Back to reference #${id}" href="#fnref${id}" class="footnote-backref">\u21a9\uFE0E</a>`;
|
||||
};
|
||||
|
||||
const renderRules = {
|
||||
footnote_caption: ['[', '[<span class="visually-hidden">Footnote #</span>'],
|
||||
};
|
||||
Object.keys(renderRules).map(rule => {
|
||||
let defaultRender = markdownLibrary.renderer.rules[rule];
|
||||
markdownLibrary.renderer.rules[rule] = (tokens, idx, options, env, self) => {
|
||||
return defaultRender(tokens, idx, options, env, self).replace(...renderRules[rule]);
|
||||
}
|
||||
});
|
||||
|
||||
/* This is the part that tells 11ty to swap to our custom config */
|
||||
eleventyConfig.setLibrary("md", markdownLibrary);
|
||||
}
|
||||
```
|
||||
|
||||
If you are a fellow Eleventy user, feel free to borrow my markdown-it-footnote configurations in your own. Make sure you install markdown-it-footnote first by keying in the installation command in the terminal:
|
||||
|
||||
```powershell
|
||||
npm install markdown-it-footnote --save
|
||||
```
|
||||
|
||||
## Bonus: Alternatives to Footnotes
|
||||
|
||||
As you likely have realised, footnotes are really tricky to implement on web pages, so you may be wondering: is there any alternative to footnotes?
|
||||
|
||||
For extra comments and tangents, by favourite approach is using HTML's details disclosure element with the `<details>` tag, with a [`<summary>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary) element and a sibling element wrapping the content displayed when the disclosure is expanded.
|
||||
|
||||
I have been using the disclosure element for additional comments or information on this website, like in my blog post, ["My Cassette Beasts Fanlisting Application has been Approved"](/blog/posts/2024-06-24-cassette-beasts-fanlisting-approved/). As for the styling, initially I indented the content inside the disclosure to differentiate the disclosure content from the rest of the content of the page, but recently I switched to adding borders to the content after being inspired by [Starbreaker](https://starbreaker.org/)'s website, who also has a post about footnotes, ["Footnotes: No Fun to Create, Either"](https://starbreaker.org/blog/tech/footnotes-no-fun-to-create-either/index.html).
|
||||
|
||||
{% disclosure "More ways to use `<details>` and `<summary>` elements" %}
|
||||
`<details>` and `<summary>` have become among my favourite HTML elements due to how useful they are. In addition to extra comments and info, Leilukin's Hub has also used the elements for the following purposes:
|
||||
- Table of contents, like the one this very article has
|
||||
- Hiding spoilers, like in my {% cite "Cassette Beasts" %} shrine article, ["How Cassette Beasts is Much More than a Pokémon Clone"](/shrines/cassettebeasts/articles/cassette-beasts-more-than-a-pokemon-clone/)
|
||||
- Content warnings, like in my [{% cite "A Summer’s End — Hong Kong 1986" %} shrine's gallery page](/shrines/asummersend/gallery/), with its styling being inspired by another Kitty Giraudel's blog post, ["A content warning component"](https://kittygiraudel.com/2022/09/04/a-content-warning-component/)
|
||||
{% enddisclosure %}
|
||||
|
||||
As for citing sources, the simplest way would be naming and linking to the source in the main text of your page, though it does mean you may end up repeating the title of the source if you want to attribute to it more than once on your page. This was once the approach of my shrine pages, though I found the pages looked rather cluttered, so I eventually chose to use markdown-it-footnote alongside Eleventy.
|
||||
|
||||
## Wrapping Up
|
||||
|
||||
Creating and maintaining footnotes on web pages is tricky, so I hope my article about accessible footnotes is helpful if you want to create them.
|
||||
|
||||
I am still not completely certain if my method is the best, although I tried to the best of my abilities, so I am interested in hearing feedback for my way of implementing accessible footnotes.
|
||||
|
||||
## Update 3 December 2024: Enlarge Link Target Area
|
||||
|
||||
On 3 December 2024, [~hedy](https://home.hedy.dev/) emailed me talking about this article, offering additional tips for improving footnotes. She also recommended me to check out [Seirdy](https://seirdy.one/)'s blog as a reference for formatting footnotes.
|
||||
|
||||
I found ~hedy's suggestions good, so I made further improvements to my footnotes, by enlarging the target area of links. To achieve this, I changed the reference labels and footnote backlinks by using visible longer link text instead of ARIA labels.
|
||||
|
||||
Specifically, for reference labels, I now use "[Note 1]" instead of just a number like "[1]", while for footnote backlinks, I use "↩︎ Back to reference 1" instead of just a ↩︎ symbol.
|
||||
|
||||
In addition, I wrap each footnote backlink in a paragraph element (`<p>`), to place each backlink per line to improve footnotes' readability.
|
||||
|
||||
Larger link target area is even better because mobile device users would find it easier to tap on the reference links and the footnote backlinks, and sighted visitors on desktop are also benefited because they can see the purpose of these links on plain slight.
|
||||
|
||||
### Updated HTML Markup
|
||||
|
||||
Here is an example of how the final HTML markup would look like with larger link target area:
|
||||
|
||||
```html
|
||||
<p>This is a paragraph with the first footnote reference. <sup class="footnote-ref"><a href="#fn1" id="fnref1">[Note 1]</a></sup></p>
|
||||
|
||||
<p>Here is the second paragraph with the second footnote reference. <sup class="footnote-ref"><a href="#fn2" id="fnref2">[Note 2]</a></sup></p>
|
||||
|
||||
<p>This the third paragraph, but with a foootnote reference that points to the first footnote. <sup class="footnote-ref"><a href="#fn1" id="fnref1:1">[Note 1:1]</a></sup></p>
|
||||
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<h2>Footnotes</h2>
|
||||
<ol class="footnotes-list">
|
||||
<li id="fn1" class="footnote-item">
|
||||
First footnote
|
||||
<p><a href="#fnref1" class="footnote-backref">↩︎ Back to reference 1</a></p>
|
||||
<p><a href="#fnref1:1" class="footnote-backref">↩︎ Back to reference 1:1</a></p>
|
||||
</li>
|
||||
<li id="fn2" class="footnote-item">
|
||||
Second footnote
|
||||
<p><a href="#fnref2" class="footnote-backref">↩︎ Back to reference 2</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
```
|
||||
|
||||
### Updated markdown-it-footnotes Configuration
|
||||
|
||||
Here is the code of the configuration for the markdown-it-footnotes plugin:
|
||||
|
||||
```js
|
||||
// markdown-it plugins
|
||||
const markdownIt = require("markdown-it");
|
||||
const markdownItFootnote = require("markdown-it-footnote");
|
||||
let markdownLibrary;
|
||||
|
||||
module.exports = function (eleventyConfig) {
|
||||
/* Markdown Overrides */
|
||||
markdownLibrary = markdownIt({
|
||||
html: true,
|
||||
}).use(markdownItFootnote);
|
||||
|
||||
// Configure markdown-it-footnote
|
||||
markdownLibrary.renderer.rules.footnote_block_open = () => (
|
||||
'<hr class="footnotes-sep">\n' +
|
||||
'<section class="footnotes">\n' +
|
||||
`<h2>Footnotes</h2>\n`
|
||||
);
|
||||
|
||||
markdownLibrary.renderer.rules.footnote_anchor = (tokens, idx, options, env, slf) => {
|
||||
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
|
||||
|
||||
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
|
||||
|
||||
/* ↩ with escape code to prevent display as Apple Emoji on iOS */
|
||||
return `
|
||||
<p class="footnote-item__back">
|
||||
<a href="#fnref${id}" class="footnote-backref">
|
||||
<span aria-hidden="true">\u21a9\uFE0E</span>
|
||||
Back to reference ${id}
|
||||
</a>
|
||||
</p>
|
||||
`;
|
||||
};
|
||||
|
||||
const renderRules = {
|
||||
footnote_caption: ['[', '[Note '],
|
||||
};
|
||||
Object.keys(renderRules).map(rule => {
|
||||
let defaultRender = markdownLibrary.renderer.rules[rule];
|
||||
markdownLibrary.renderer.rules[rule] = (tokens, idx, options, env, self) => {
|
||||
return defaultRender(tokens, idx, options, env, self).replace(...renderRules[rule]);
|
||||
}
|
||||
});
|
||||
|
||||
/* This is the part that tells 11ty to swap to our custom config */
|
||||
eleventyConfig.setLibrary("md", markdownLibrary);
|
||||
}
|
||||
```
|
@ -0,0 +1,58 @@
|
||||
---
|
||||
articleTitle: Anti-Harry Potter and Anti-J. K. Rowling Masterlist
|
||||
date: 2023-03-19
|
||||
updated: 2025-03-05T07:43:53+0800
|
||||
desc: My masterlist of criticisms of the Harry Potter series and J. K. Rowling.
|
||||
categories: ["harry potter", "jk rowling"]
|
||||
toc: true
|
||||
---
|
||||
|
||||
{% imgFigure "/assets/banners/harry-potter-free-site.png", "An anime girl crossing arms with a sidelong look and a Harry Potter-free message" %}
|
||||
This site is Harry Potter free. Lookin [<i>sic</i>] for it? Leave.
|
||||
{% endimgFigure %}
|
||||
|
||||
(Credit to Tumblr user [comradesaucegay](https://comradesaucegay.tumblr.com/post/621403039459426304) ([archived](https://web.archive.org/web/20241122135107/https://comradesaucegay.tumblr.com/post/621403039459426304/grumpsaesthetics-you-know-those-old-hentai-free)) for this Harry Potter-free website banner){.center-text}
|
||||
|
||||
Like many Millennials, I grew up with the {% cite "Harry Potter" %} series and was a major fan of the series. I had read the original 7 books and watched their film adaptations. The series was a passion of mine during my early- to mid-teen years. While the {% cite "Harry Potter" %} novels were far from the first books I read, they were the first fantasy novels I read, and the series’ concept of a magical world set in a contemporary setting fascinated teenage me.
|
||||
|
||||
However, even during my {% cite "Harry Potter" %} fixation years, there were things from the series that bothered me, from the idea of house elves being a slave race that naturally love being slaves, to Snape’s unconvincing “redemption” arc. As I got older and became more aware of social issues, I started to notice more problems with the series. In addition, reading more books has also made me realise that even on a technical writing level, the {% cite "Harry Potter" %} series was mediocre at best. As a result, I had stopped becoming a {% cite "Harry Potter" %} fan even before J. K. Rowling’s anti-trans sentiments got mainstream attention.
|
||||
|
||||
Unfortunately, many adults with nostalgia goggles still refuse to let go of {% cite "Harry Potter" %}, and they believe {% cite "Harry Potter" %} can be separated or “reclaimed” by J. K. Rowling, despite how much Rowling’s worldview and prejudice are inextricably linked to her writing.
|
||||
|
||||
Therefore, I am compiling this masterlist by curating various materials that are critical of the {% cite "Harry Potter" %} series and J. K. Rowling. Items are ordered in chronological order. This is far from a comprehensive list of all the pieces that criticise {% cite "Harry Potter" %} and Rowling; this list is my curated list of ones that I have read, watched or listened to, and I personally endorse.
|
||||
|
||||
## Articles
|
||||
|
||||
* [Harry Potter and the Universal Declaration of Human Rights](https://alinautrata.medium.com/all-the-things-that-are-fucked-up-about-harry-potter-58267e1bf3ee) by Alina Utrata (28 October 2016)
|
||||
* [Addressing The Claims In JK Rowling’s Justification For Transphobia](https://katymontgomerie.medium.com/addressing-the-claims-in-jk-rowlings-justification-for-transphobia-7b6f761e8f8f) by Katy Montgomerie (16 June 2020)
|
||||
* [The Antisemitism of ‘Harry Potter’ Returns in ‘Hogwarts Legacy’](https://www.themarysue.com/is-hogwarts-legacy-anti-semitic-hogwarts-legacy-anti-semitic-allegations-explained/) by Jack Doyle (8 February 2023)
|
||||
* [Hogwarts Legacy wants to make everyone happy, but Harry Potter gets in the way](https://www.polygon.com/reviews/23603142/hogwarts-legacy-review-harry-potter-jk-rowling-transphobic-ps5-pc-xbox) by by Gita Jackson (17 February 2023)
|
||||
* [Is J.K. Rowling transphobic? Let’s let her speak for herself.](https://www.vox.com/culture/23622610/jk-rowling-transphobic-statements-timeline-history-controversy) by Aja Romano (3 March 2023)
|
||||
* [Talking Shit About Harry Potter](https://starbreaker.org/blog/entertainment/talking-shit-about-harry-potter/index.html) by Matthew Graybosch (19 June 2024)
|
||||
|
||||
This post was in fact prompted by this very masterlist and our email exchange about the topic.{.item-list__indent}
|
||||
|
||||
## Videos
|
||||
|
||||
* [Harry Potter is GARBAGE, and Here's Why](https://www.youtube.com/watch?v=wPwWb9z3XSY) by satenmadpun (25 August 2020)
|
||||
* [We need to talk about Dudley Dursley- Harry Potter and Fatphobia](https://www.youtube.com/watch?v=4AziZgoi3q0) by Ok2BeFat (30 May 2021)
|
||||
* [Harry Potter](https://www.youtube.com/watch?v=-1iaJWSwUZs) by Shaun (4 March 2022)
|
||||
* [JK Rowling's New Friends](https://www.youtube.com/watch?v=Ou_xvXJJk7k) by Shaun (14 October 2022)
|
||||
* [Is Harry Potter Bad?](https://www.youtube.com/watch?v=U3dE0sYZqvI) by hoots (30 December 2022)
|
||||
* [The Consumerist Dystopia of Harry Potter](https://www.youtube.com/watch?v=UBftW7FzOVI) by verilybitchie (21 January 2023)
|
||||
* [Explaining JK Rowling’s Transphobia](https://www.youtube.com/watch?v=_GBUArD51KY) by Jessie Gender (27 January 2023)
|
||||
* [Hogwarts Legacy and the Politics of Potter](https://www.youtube.com/watch?v=K1mHwn6eUUk) by Wisecrack (11 February 2023)
|
||||
* [Hogwarts: A Legacy Of Hate (The Jimquisition)](https://www.youtube.com/watch?v=uNKyQVsgKLg) by James Stephanie Sterling (13 February 2023)
|
||||
* [The ALLEGED Witch Trials of J.K. Rowling (What The Megan Phelps Podcast Won't Tell You)](https://www.youtube.com/watch?v=9ncYTEY7aVk) by Caelan Conrad (18 March 2023)
|
||||
* [The Harry Potter Villains Were Trans?](https://www.youtube.com/watch?v=7xJF4XYarzI) by Brigitte Empire (3 April 2023)
|
||||
* [Maybe The Wizard Game Just Wasn't Very Good (The Jimquisition)](https://www.youtube.com/watch?v=2pggtSI_95A) by James Stephanie Sterling (20 November 2023)
|
||||
* [J.K. Rowling & Holocaust Denial](https://www.youtube.com/watch?v=whJJGqVtkEk) by Caelan Conrad (16 March 2024)
|
||||
* [Did J.K. Rowling STEAL Harry Potter (Rip-off? Plagiarism?!)](https://www.youtube.com/watch?v=Cmx_YSPcujE) by Caelan Conrad (3 June 2024)
|
||||
* [Harry Potter is Also Ableist](https://www.youtube.com/watch?v=oYgFHBXyVE4) by Ember Green (9 December 2024)
|
||||
* [J. K. Rowling: The Real Story](https://www.youtube.com/watch?v=NeFUqCrmPC0) by Caelan Conrad (4 March 2025)
|
||||
|
||||
## Podcasts
|
||||
|
||||
* [The Shrieking Shack](https://soundcloud.com/shriekingshack), hosted by Xeecee and Liz
|
||||
|
||||
A book reading podcast that originally started out as a {% cite "Harry Potter" %} reread podcast made by and for lapsed fans that goes through every chapter of the {% cite "Harry Potter" %} books and their film adaptations and analyses the writing and politics of the series and J. K. Rowling.{.item-list__indent}
|
@ -0,0 +1,26 @@
|
||||
---
|
||||
articleTitle: Designated Lesbian Syndrome in Fandom
|
||||
date: 2023-08-18
|
||||
desc: On fandom's tokenisation of lesbians, and how I hate it as a lesbian myself.
|
||||
categories: ["fandom culture"]
|
||||
---
|
||||
|
||||
Over the years of interacting with and observing fandom, I have come to [dislike fandom and shipping culture](../my-dislike-of-shipping-culture) for many reasons. One major reason I want to talk about here is the "Designated Lesbian Syndrome" phenomenon.
|
||||
|
||||
I first encountered this term from [Tumblr user desolationlesbian's post](https://desolationlesbian.tumblr.com/post/662432767484870656/designated-lesbian-syndrome-n-when-fans-of-a):
|
||||
|
||||
> Designated Lesbian Syndrome (n.): When fans of a piece of media pick two female characters and pair them together as the token F/F ship and put them in everything, but do not bother to engage with their dynamic in any way more substantive than having them hold hands in the background. Everyone agrees it’s a good ship and it has nothing but their full support, yet this never materializes into real effort or content creation.
|
||||
>
|
||||
> Most common with works that have little-to-no narratively important women, but in rare cases can even happen to major and complex characters who are then reduced to token background lesbians as everyone hones in on the boys.
|
||||
|
||||
The example I am most familiar with is the fandom of Grand Admiral Thrawn and the Chiss from {% cite "Star Wars" %} — specifically, some shippers of Thrawn/Eli Vanto, also known as Thranto, pair Ar'alani and Karyn Faro as their token "pair-the-spares" femslash ship. However, while Thranto shippers have create fan content about the Ar'alani/Karyn Faro ship, they do not do it with even a fraction of the interest and enthusiasm as they do Thranto. This is evident in the difference between the amount of fan works these two pairings have received on Archive On Our Own (AO3): as of this writing, the [Thrawn | Mitth'raw'nuruodo/Eli Vanto](https://archiveofourown.org/tags/Thrawn%20%7C%20Mitth'raw'nuruodo*s*Eli%20Vanto/works) tag contains 1,358 works, while the [Ar'alani/Karyn Faro](https://archiveofourown.org/tags/Ar'alani*s*Karyn%20Faro/works) tag contains 34 works only. It is also worth noting that if you use the tag page's filter function, you will notice that out of these 34 Ar'alani/Faro works, 23 of them are also tagged with Thrawn/Eli Vanto, proving the significant overlap between Thranto and Ar'alani/Faro shippers.
|
||||
|
||||
Part of the Designated Lesbian Syndrome also involves shippers headcanonning the female characters who could be considered "threats" to their favourite ship that involved male characters as lesbians. It does not matter if the female characters are actually romantically involved with the male characters' in the source material or not. Thrawn never has a canon love interest in both Legends and the Disney continuities, but the fact that Ar'alani and Faro are close to Thrawn as his long-time friend and protégée respectively is enough to make them the go-to candidates for lesbian headcanons from shippers who ship Thrawn with anyone else. Lesbian Ar'alani and Faro headcanons are most common among Thrawn/Eli shippers, but I had seen at least one Thrawn/Governor Pryce (a pairing that I cannot stand at all) shipper literally headcanons Ar'alani as a lesbian so Pryce does not need to worry about Ar'alani fighting with Pryce over Thrawn.
|
||||
|
||||
Ar'alani and Karyn Faro are unfortunately perfect examples of the above post by desolationlesbian of major female characters who are reduced to token background lesbians by shippers. As a huge fan of both Ar'alani and Faro as characters, I find it immensely frustrating, to the extent that the Thrawn shipping fandom has utterly turned me off on both Thranto and Ar'alani/Faro ships.
|
||||
|
||||
As a lesbian myself, I loathe the Designated Lesbian Syndrome with a passion, because this phenomenon is literally tokenism, specifically tokenisation of lesbians. As a consequence, I have reached the point where when it comes to fandoms with big Male/Male (M/M) ships that are not canon, I only trust lesbian headcanons from fellow lesbians. Yes, this means I do not trust lesbian Ar'alani and Karyn Faro headcanons from people who are not lesbians.
|
||||
|
||||
I really hate that it comes to this because as a lesbian, in theory I am in favour of lesbian headcanons for female characters who are cishet-coded or who do not have a canon sexuality. Unfortunately, lesbian headcanons have been used as a progressive way to get female characters out of the way of Male/Male ships (and occasionally Male/Female ships as well, though this phenomenon is more common in fandoms of M/M ships), not because these shippers actually care about lesbians and the female characters.
|
||||
|
||||
These shippers rarely put as much energy in talking about or creating fan content for the female characters they headcanon as lesbians as they do the male characters from their ships. These shippers can pretend they care about lesbians all they want, but my lesbian ass can see through their performative bullshit, and recognise their lesbian headcanons are nothing more than tokenism.
|
26
src/articles/myarticles/living-with-retinitis-pigmentosa.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
articleTitle: Living with Retinitis Pigmentosa
|
||||
date: 2024-12-15T13:14:07+0800
|
||||
desc: I talk about being visually impaired, particularly with a genetic eye disease that causes gradual vision loss and currently has no cure.
|
||||
categories: ["personal life", "disability", "retinitis pigmentosa"]
|
||||
---
|
||||
|
||||
On 30 November 2024, I was diagnosed with retinitis pigmentosa (RP) after seeing an eye doctor.
|
||||
|
||||
My feelings after learning that I have this genetic eye disease that causes gradual vision loss and currently has no cure was more relieved than upset, because I was relieved that to have finally understood the reason behind a lot of the difficulties in my daily life as I grew older, in addition to my autism.
|
||||
|
||||
Furthermore, my parents and I already suspected that I have inherited an eye condition that causes visual impairment after learning that some of my relatives on my father's side are starting to have eyesight problems. Therefore, I have accepted the possibility that I might have some incurable eye condition even before my official RP diagnosis. I had relied on glasses throughout most of my life due to having myopia and astigmatism, so I was already not new to vision conditions.
|
||||
|
||||
In retrospect, my RP symptoms started to manifest to the point of impacting my daily life in my early 20s. I started to bump into things more often when I walk. My eyes got more sensitive to bright light, including sunlight, to the extent that I found it difficult to see things under sunlight and thus needing sunglasses. My subconscious began to get afraid of falling, causing me to get much more cautious when I walk, especially when walking on steps and crossing over drains, especially in places I was not familiar with. I also had a hard time seeing things not directly in front of me without turning my head.
|
||||
|
||||
For a while, my parents and I thought this was because of my autism, as it makes me find it difficult to multitask, including paying attention to all my surroundings at once outside, and me spending too much time sitting in front of a computer, especially since my jobs have all involved a large amount of computer usage, and thus getting less physical exercise. However, now that I was diagnosed with RP, things finally clicked into place: turns out, my peripheral vision had started to get weaker, and my eyes became more sensitive to bright light, including sunlight. In addition, my eye doctor discovered that I am having a mild cataract as a complication of RP.
|
||||
|
||||
Since there is currently no cure for RP, the best I can do is to take care of my remaining vision as much as I can, mainly by continuing to wear sunglasses every time I go out in daytime to protect my eyes from sunlight. I am also adjusting my habits of using digital devices, by making the font sizes of my devices larger to make things more comfortable for me to read and trying to take regular breaks from digital screens.
|
||||
|
||||
Being diagnosed with a genetic eye disease that can cause vision loss also made me glad that I have started to learn to use screen readers. Even before my diagnosis, I have been using screen readers such as NVDA on Windows and TalkBack on Android to test website accessibility, including my own websites. It is a good idea to prepare myself for the possibility of losing enough vision to the degree of needing low vision aid and assistive technologies. In fact, since I was diagnosed with RP through a dilated eye exam, I was unable to see things clearly for about 12 hours after receiving the eye drop that dilated my eyes, so for the immediate couple of hours after seeing my eye doctor, I needed to activate TalkBack to be able to read things on my Android phone. Furthermore, I am also planning on learning braille.
|
||||
|
||||
I do not write this article to ask for pity. I have accepted that I am visually impaired and autistic, and my disabilities are a huge part of who I am, as much as being a non-binary lesbian on the asexual and aromantic spectrum. Instead, I am sharing my story to let other people who have similar conditions that they are not alone, and we are worthy.
|
||||
|
||||
I coded a [responsive Disability Pride flag in CSS](/projects/snippets/disability-pride-flag-background) during the Disability Pride Month in July (which is also my birth month) 2024, so it would be fitting to conclude this article with this Pride flag I coded.
|
||||
|
||||
<div class="flag-el flag-disability" role="img" aria-label="Disability Pride flag coded in CSS" style="margin-top: 1.7em;"></div>
|
40
src/articles/myarticles/love-letter-to-myself.md
Normal file
@ -0,0 +1,40 @@
|
||||
---
|
||||
articleTitle: A Love Letter to Myself
|
||||
date: 2025-02-10T20:31:26+0800
|
||||
desc: I decided to take the opportunity of 32-Bit Cafe's 2025 Valentine's Day code jam to write a love letter to myself.
|
||||
categories: ["personal life"]
|
||||
---
|
||||
|
||||
[](https://32bit.cafe/vday25/){.inline-img}
|
||||
{.center-text}
|
||||
|
||||
[32-Bit Cafe](https://32bit.cafe/) is hosting a code jam for Valentine's Day 2025, titled ["Party for One"](https://32bit.cafe/vday25/), as the theme is making a digital Valentine's Day gift dedicated to yourself. After much consideration, I decided to take this opportunity to write a love letter to myself, as a Valentine's Day gift to myself and a reminder of my self-worth.
|
||||
|
||||
---
|
||||
|
||||
Dear Leilukin,
|
||||
|
||||
You and I go way back to the beginning of your life, and we have stuck with each other through thick and thin. There is so much I want to tell you, and I hope by writing this letter, you will appreciate how much I cherish, admire and respect you.
|
||||
|
||||
You have come a long way since your childhood and adolescence. You have grown from an insecure child with low self-esteem after enduring years of bullying at school, desperate for external validation and other people's approval, to an adult who takes pride in who they are and does not care about what others think of them. Therefore, it is unsurprising that you said that while adulthood certainly has its challenges, you do not miss being a child or teenager, and you actually enjoy being an adult.
|
||||
|
||||
I know very well that your self-confidence did not come easy, and it did not happen overnight either. After being traumatised by years-long abuse from a secondary school bully and later a personal betrayal from someone who sided with said bully, whom you thought was your friend, you went on to struggled with trust issues and lack of faith in yourself throughout your early adulthood, even though you never met those people who hurt you the most again, until you attended therapy when you were approaching your late 20s. With your therapist's help, you learned that life is like a train, with the people you have met in your life as your passengers, and after the people who hurt you and vandalised your train left, you can choose to redecorate your train and welcome new people in your life who would treat you better. Learning this metaphor was what finally made you learn that it is not worth letting the people who hurt you to continue to define your life.
|
||||
|
||||
I am proud of you for figuring out you are a queer, autistic, non-binary lesbian on the asexual and aromantic spectrum as an adult, despite the lack of queer and autistic resources and media representation when you grew up. It is fortunate that you encountered online queer and autistic communities and resources that helped you figure yourself out, but at the end of the day, it was you who decided to take active steps in understanding yourself better, so you know how to live your life better, and to not be afraid to be different. When you were a kid, fellow students in school had bullied you for being a socially awkward and friendless "weird" kid, but normality is a social construct, so your worth as a person is not determined by how "normal" you are perceived by others. You are unique, and your uniqueness is worth celebrating. You do not need to rely on someone else for your happiness, either.
|
||||
|
||||
The self-worth you spent years in building up has also given you the strength to accept your [retinitis pigmentosa diagnosis](living-with-retinitis-pigmentosa.md), as you understand that the possibility of losing sight is not the end of the world. Queer and disability pride in the face of a cisheternormative and ableist society is an act of rebellion. Being a non-binary lesbian who refuses to conform to traditional gender roles is also a defiance to a patriarchy.
|
||||
|
||||
Learning to look after your well-being and to set boundaries are crucial for a happy and fulfilling life, so I applaud you for making decisions out of consideration for your well-being, including:
|
||||
|
||||
- Going to therapy to improve your mental health;
|
||||
- Leaving an openly anti-gay church to join a queer-affirming one;
|
||||
- Switching your career path to web development, a field you actually feel confident and interested in doing, after being burned out from graphic design as a profession;
|
||||
- Seeing an eye doctor to get diagnosed with your eye problems;
|
||||
- [Quitting your first web developer job](/blog/posts/2024-12-16-leaving-my-first-developer-job) when it is clear that your web developer skills were not valued by your employers enough, and the job will likely make you more miserable in the long run if you stayed.
|
||||
|
||||
Furthermore, I admire your spirit of treating learning as a lifelong process, and understanding the importance of thinking for yourself, by keeping yourself curious and not being afraid of learning new things, while remaining true to yourself, by standing firm with your convictions to be kind and striving to be authentic.
|
||||
|
||||
Together we will live the best life we can possibly have. No matter what happens, I will always love you.
|
||||
|
||||
Love,<br>
|
||||
Me
|
19
src/articles/myarticles/my-dislike-of-shipping-culture.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
articleTitle: My Disinterest in Shipping and Dislike of Shipping Culture
|
||||
date: 2023-08-14
|
||||
desc: About my lack of interest in shipping and dislike of shipping culture in fandom, and why.
|
||||
categories: ["fandom culture"]
|
||||
---
|
||||
|
||||
*(Note: The original version of this writing was posted on my Tumblr blog on 3 May 2021)*
|
||||
{.center-text}
|
||||
|
||||
I feel that the older I get, the less interested I am in shipping in general, and the less I can understand grown adults in late 20s or older who build their personality around shipping and cannot engage with any fiction without shipping something (before I remade my Tumblr account in 2015, I used to follow a 30-something-year-old fandom blogger who was literally like this).
|
||||
|
||||
I have also come to actually dislike shipping culture, especially the mindset of engaging with media primarily through a shipping lens at the expense of the characterisation, non-romantic relationships, themes, and general content of a piece of media. Throughout my years of experience with various fandoms, I have witnessed so many incredible characters and non-romantic relationships get ignored or underappreciated by the fandom because they’re not part of a popular ship. On the other hand, there are also many amazing characters get mischaracterised to make a fanon ship work, or get reduced to a handful of tropes as shippers Flanderise them in their shipping fan works.
|
||||
|
||||
Do not even get me started on the shippers who will complain about not being able to enjoy a piece of media just because “there’s no couple to ship”. For example, I’ve literally seen people on Tumblr make such complaints about the film {% cite "Parasite" %}, as if a profound piece of art that brutally critiques the class system does not matter if it does not provide any shipping material. It’s honestly very infuriating to see shippers like these are so willing to ignore art with meaningful story, characters and themes just because it lacks shipping fodder for them to project fanon onto.
|
||||
|
||||
Speaking of fanon, I really hate it when shippers, particularly shippers of popular non-canon pairings, expect anyone who is a fan of the same piece of media or any of the characters involved in the pairing to care about ships and treat the popular fanon ships as canon. These shippers also tend to act like their fanon and headcanons are universal, using languages like “the fandom has decided–” and “everyone has agreed that–”. And these shippers wonder why others who do not engage with that piece of media for shipping get sick and tired of shipping taking over discussions about said piece of media.
|
||||
|
||||
If you are so invested in shipping to the point that you are willing to disregard a piece of media just because you cannot find anything to ship, or you treat it as a personal attack when people talk about being sick and tired of shipping culture as well as how shipping dominates discussions about a piece of media, maybe you need to consider taking a step back from shipping and learning to take your shipping goggles off when you interact with a piece of media.
|
3
src/articles/myarticles/myarticles.11tydata.js
Normal file
@ -0,0 +1,3 @@
|
||||
export default {
|
||||
tags: "my articles"
|
||||
}
|
185
src/articles/myarticles/palestine-masterlist.md
Normal file
@ -0,0 +1,185 @@
|
||||
---
|
||||
articleTitle: Palestine Masterlist
|
||||
date: 2024-06-19T22:19:00+0800
|
||||
updated: 2024-12-11T23:46:44+0800
|
||||
desc: My masterlist of resources related to Palestine.
|
||||
categories: ["palestine"]
|
||||
toc: true
|
||||
---
|
||||
|
||||

|
||||
|
||||
(Credit to [rosemary](https://hillhouse.neocities.org/journal/notes/palestine) for the Stand with Palestine button)
|
||||
|
||||
From the river to the sea, Palestine will be free.
|
||||
|
||||
Here, I am compiling a list of resources related to Palestine. This list is not comprehensive by any means, but I still want to do my part in using my platform to support an important cause.
|
||||
|
||||
## Educate Yourself
|
||||
|
||||
* [Decolonize Palestine](https://decolonizepalestine.com/)
|
||||
* [The Palestine Academy](https://www.thepalestineacademy.com/)
|
||||
* Haymarket's [Free Ebooks for a Free Palestine](https://www.haymarketbooks.org/blogs/495-free-ebooks-for-a-free-palestine)
|
||||
* [Palestine, Today: Explore how Palestine has been transformed since the Nakba](https://today.visualizingpalestine.org/)
|
||||
* [Palestine Open Maps](https://palopenmaps.org/)
|
||||
* [Palestine Remembered](https://www.palestineremembered.com/)
|
||||
|
||||
## Articles
|
||||
|
||||
* [What is the Ethical Way to Climb Out of Hell?](https://medium.com/@thegriefwitch/what-is-the-ethical-way-to-climb-out-of-hell-8ed51bdc327b) by The Grief Witch (17 October 2023)
|
||||
* [What Does It Mean to Be Palestinian Now?](https://www.thenation.com/article/world/what-does-it-mean-to-be-palestinian-now/) by The Nation (25 January 2024)
|
||||
|
||||
## Petitions
|
||||
|
||||
* [An Open Letter from Palestinian Christians to Western Church Leaders and Theologians](https://www.change.org/p/an-open-letter-from-palestinian-christians-to-western-church-leaders-and-theologians)
|
||||
|
||||
## Donations and Charities
|
||||
|
||||
* [Daily click to help Palestine on arab.org](https://arab.org/click-to-help/palestine/)
|
||||
* [Palestine Children's Relief Fund](https://www.pcrf.net/)
|
||||
* [eSims for Gaza](https://gazaesims.com/)
|
||||
* [Crips for eSims for Gaza](https://chuffed.org/project/crips-for-esims-for-gaza)
|
||||
* [Gaza Soup Kitchen](https://gazasoupkitchen.org/)
|
||||
* [Gaza Funds](https://gazafunds.com)
|
||||
* [Operation Olive Branch](https://docs.google.com/spreadsheets/d/1vtMLLOzuc6GpkFySyVtKQOY2j-Vvg0UsChMCFst_WLA/htmlview)
|
||||
* [Operation Poppy Flower](https://www.operationpoppyflower.com/)
|
||||
* [Strawberry Seed Collective](https://linktr.ee/strawberryseedcollective)
|
||||
* [Project Watermelon](https://linktr.ee/projectwatermelon)
|
||||
* [Bees And Watermelons](https://linktr.ee/beesandwatermelons)
|
||||
* [The ButterflyEffect Project](https://docs.google.com/spreadsheets/u/0/d/e/2PACX-1vTKQYInYewFiGUX4afdHK-rANJDT4dgOC4IV6elKYNvYI2HvOTf_6IsTqt5m2KXcr_pGxcqR8AvsAJi/pubhtml)
|
||||
* [el-shab-hussein and nabulsi's Vetted Gaza Evacuation Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0)
|
||||
* [GazaVetters](https://www.tumblr.com/gazavetters) ([Gaza Vetters spreadsheet](https://docs.google.com/spreadsheets/d/1YGgkXoyam7tnbXb-vqWsHFs3Puyf_xYeXY2dPrZQY1M/edit?gid=0#gid=0))
|
||||
* [Ottawa4Palestine](https://linktr.ee/ottawa4palestine)'s [Gaza Evacuation Relief Fund](https://docs.google.com/spreadsheets/d/1FC1fK-3r-w3wPi2AdYho5_0j148QC6wkFIMdJA5kX4c/edit?gid=0#gid=0)
|
||||
* [GoFund(water)Me(lons)](https://docs.google.com/spreadsheets/d/1o_kH9RnGmKOXmEcMUBTWcMhgdTKIequ__xiPym7RGtE/edit?gid=0#gid=0)
|
||||
* [Helping the People of Gaza - Mutual Aid Fund](https://www.gofundme.com/f/helping-the-people-of-gaza-mutual-aid-fund)
|
||||
* [Care for Gaza](https://x.com/careforgaza)
|
||||
* [Direct Aid for Gaza](https://x.com/GazaDirectAid)
|
||||
* [Life For Gaza](https://gaza-city.ensany.com/campaign/6737?lang=en)
|
||||
* [Taawon](https://www.taawon.org/en/content/gaza-needs-us-now)
|
||||
* [World Food Programme (WFP)](https://www.wfp.org/emergencies/palestine-emergency)
|
||||
* [Medical Aid for Palestinians](https://www.map.org.uk/)
|
||||
* [Help Dr. Ibrahem to treat the pateints homeless people](https://www.gofundme.com/f/urgent-help-dribrahem-to-treat-the-pateints-homeless-people)
|
||||
* [Support for Tents in Gaza](https://www.gofundme.com/f/support-for-marginalized-refugees-in-jordan)
|
||||
* [Gaza Municipality's water project](https://gaza-city.ensany.com/campaign/6737?lang=EN)
|
||||
|
||||
## Boycotts
|
||||
|
||||
* [BDS Movement](https://bdsmovement.net/)
|
||||
* [No Tech For Apartheid](https://www.notechforapartheid.com/)
|
||||
* Attention to website owners: [Boycott Wix](https://boycottwix.org/) — The BDS movement has [listed Wix as an organic boycott target](https://bdsmovement.net/Act-Now-Against-These-Companies-Profiting-From-Genocide), meaning the BDS movement did not initiate this grassroots boycott campaign, but supports it due to the boycotted brand's complicity in Israel's genocide and apartheid against Palestinians.
|
||||
|
||||
## Spotlight Crowdfunding Campaigns
|
||||
|
||||
There are Palestinians who have reached out to me on Tumblr to ask for help with sharing their fundraising campaigns, so I am using my platform to share them. These campaigns have been verified by other Palestinians and trusted sources (including ones who are on Tumblr), so I encourage you to help to donate and share at the best of your abilities.
|
||||
|
||||
* [Help...Save a Pregnant Mother's Life and Support Her Family](https://www.gofundme.com/f/ne9gzx-help-them-to-survive) (verified by [nabulsi](https://www.tumblr.com/nabulsi/751944563232587777) and shared by [90-ghost](https://www.tumblr.com/90-ghost/752304563682476032))
|
||||
* [Help Sahar and Her Family to Evacuate Gaza](https://www.gofundme.com/f/help-sahar-and-her-family-to-evacuate-gaza) (verified by [el-shab-hussein](https://www.tumblr.com/el-shab-hussein/749777880017502208/another-fundraiser-i-trust))
|
||||
* [Help Us Escape Gaza: A Mother's Plea for Safety](https://www.gofundme.com/f/help-us-escape-gaza-a-mothers-plea-for-safety) (#182 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A186))
|
||||
* [From War to Education: Abdelrahman Resilient Journey](https://www.gofundme.com/f/from-war-to-education-abdelrahmans-resilient-journey) (verified by [nabulsi](https://www.tumblr.com/nabulsi/753544070688915456) and shared by [90-ghost](https://www.tumblr.com/90-ghost/753258782216404992))
|
||||
* [Please Help Tahani save her children and husband](https://www.gofundme.com/f/i-have-nothing-left-my-home-and-workplace-have-be) (verified by [el-shab-hussein](https://www.tumblr.com/el-shab-hussein/748756401076207616/list-of-fundraisers-for-direct-contacts-from))
|
||||
* [Please help Shamaly family SURVIVE Gaza & start a new life!](https://www.gofundme.com/f/please-help-shamaly-family-start-a-new-life) (verified by [nabulsi](https://www.tumblr.com/nabulsi/753715707609530368))
|
||||
* [Help us reunite with our kids who are stuck in Gaza](https://www.gofundme.com/f/reunite-us-with-our-beloved-4-kids-stuck-in-gaza) (shared by [90-ghost](https://www.tumblr.com/90-ghost/754096091530723328/donate-to-help-us-reunite-with-our-kids-who-are))
|
||||
* [Urgent ! Save kids life .](https://www.gofundme.com/f/help-alaa-family-to-survive-the-war-in-gaza) (shared by [90-ghost](https://www.tumblr.com/90-ghost/751443466082484224))
|
||||
* [Trapped Family in Gaza Appeals for Help to Survive](https://www.gofundme.com/f/wyuehr-trapped-family-in-gaza-appeals-for-help-to-survive) (#174 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A178); also shared by [fairuzfan](https://www.tumblr.com/fairuzfan/752928036559749120) and [90-ghost](https://www.tumblr.com/90-ghost/752026987709825024))
|
||||
* [Help Ahmed family to travel to a save place](https://www.gofundme.com/f/help-ahmed-family-to-travel) (veridied by [nabulsi](https://www.tumblr.com/nabulsi/752576616277868544/hello-how-are-you-my-name-is-ahmed-im-from))
|
||||
* [Help my family escape Gaza](https://www.gofundme.com/f/byt93-help-my-family-escape-gaza) (#231 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A235))
|
||||
* [Hope for Gaza: Support Ashraf's Family Rebuild Their Lives](https://www.gofundme.com/f/hope-for-gaza-support-ashrafs-family-rebuild-their-lives) (verified by [90-ghost](https://www.tumblr.com/90-ghost/754535164856664064/legit-fundraiser))
|
||||
* [A Father's Plea: Help Save My Family](https://www.gofundme.com/f/btuqqt-save-my-familys-life) (verified by [el-shab-hussein](https://www.tumblr.com/el-shab-hussein/754087744556449792/vetted-family-fundraiser-masterpost-3))
|
||||
* [Help Moamen Majed rebuild his life after the war](https://www.gofundme.com/f/help-moamen-majed-rebuild-his-life-after-the-war) (verified by [90-ghost](https://www.tumblr.com/90-ghost/755355718664830976/moamenmajed-gaza-are-they-a-vetted-fundraiser))
|
||||
* [Save Gaza: A Brighter Future for Aya and Her Family](https://www.gofundme.com/f/save-gaza-a-brighter-future-for-aya-and-her-family) (shared by [90-ghost](https://www.tumblr.com/90-ghost/754992024939347968))
|
||||
* [EMERGENCY: help my relatives evacuate to safety](https://www.gofundme.com/f/emergency-help-my-relatives-evacuate-to-safety) (#137 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A141); also shared by [90-ghost](https://www.tumblr.com/90-ghost/756556584649424896))
|
||||
* [Help Palestine and Family Survive the Gaza Crisis](https://www.gofundme.com/f/help-palestine-and-family-survive-the-gaza-crisis) (shared by [90-ghost](https://www.tumblr.com/90-ghost/756556732829974528))
|
||||
* [Support Safaa's Quest To Get Her Family To Safety](https://www.gofundme.com/f/support-safaas-quest-to-get-her-family-to-safety) (shared by [90-ghost](https://www.tumblr.com/90-ghost/756556669674274816))
|
||||
* [Help Doaa Rebuild Her Sewing Business](https://www.gofundme.com/f/help-doaa-rebuild-her-sewing-business) (shared by [90-ghost](https://www.tumblr.com/90-ghost/756556522290626560))
|
||||
* [Help the Munna Tashmali Family Rebuild Their Home and Future](https://www.gofundme.com/f/help-the-munna-tashmali-family-rebuild-their-home-and-future) (verified by [nabulsi](https://www.tumblr.com/nabulsi/754393532315353089/donate-to-help-the-munna-tashmali-family-rebuild))
|
||||
* [Help Gaza Family Evacuate to Safety](https://www.gofundme.com/f/helping-gaza-family-to-get-out) (shared by [90-ghost](https://www.tumblr.com/90-ghost/756899215473606656))
|
||||
* [Help me save my children from death in the Gaza war](https://www.gofundme.com/f/help-me-save-my-children-from-death-in-the-gaza-war) ([verified by association](https://www.tumblr.com/ana-bananya/759290042834386944/dina-is-mahmoud-1995s-sister-confirmed-here); this fundraising campaign belongs to the sister of Mahmoud Alkhaldi, whose campaign is linked just above)
|
||||
* [Help us for the sake of God](https://www.gofundme.com/f/help-us-for-the-sake-of-god) (verified by [90-ghost](https://www.tumblr.com/90-ghost/755446251161632768/faites-un-don-%C3%A0-help-us-for-the-sake-of-god))
|
||||
* [we need help](https://www.gofundme.com/f/bfth82-we-need-help) (shared by [90-ghost](https://www.tumblr.com/90-ghost/756901702912868352))
|
||||
* [Help little children of Gaza stay safe and alive!](https://www.gofundme.com/f/help-our-families-evacuate-gaza-before-too-late) (verified by [nabulsi](https://www.tumblr.com/nabulsi/751214090403430400/donate-to-help-little-children-stay-safe-and))
|
||||
* [Support Fatima's Family in Gaza After Heartbreaking Tragedy](https://www.gofundme.com/f/support-fatimas-family-in-gaza-after-heartbreaking-tragedy) (shared by [90-ghost](https://www.tumblr.com/90-ghost/756556061504307200))
|
||||
* [Get my relatives out of Gaza](https://www.gofundme.com/f/3b6yr-get-my-relatives-out-of-gaza) (verified by [90-ghost](https://www.tumblr.com/90-ghost/757079385198215168/legit-fundraiser))
|
||||
* [Help Shaima's family reunite in Egypt](https://www.gofundme.com/f/help-shymaas-family-reunite-in-egypt) (#141 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A145))
|
||||
* [Save the life of an innocent child](https://www.gofundme.com/f/save-the-life-of-an-innocent-child) (shared by [90-ghost](https://www.tumblr.com/90-ghost/755122452856700928) and listed as #406 in [The ButterflyEffect Project's Verified Campaigns](https://docs.google.com/spreadsheets/d/e/2PACX-1vTKQYInYewFiGUX4afdHK-rANJDT4dgOC4IV6elKYNvYI2HvOTf_6IsTqt5m2KXcr_pGxcqR8AvsAJi/pubhtml?urp=gmail_link&gxid=-8203366#))
|
||||
* [Save Ibrahim and his children](https://www.gofundme.com/f/save-ibrahim-and-his-children) (verified by [90-ghost](https://www.tumblr.com/90-ghost/757288769795850240/legit-fundraiser))
|
||||
* [Help Shahd in Gaza!](https://www.gofundme.com/f/help-shahd-in-gaza) (#224 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A228))
|
||||
* [Save Dr. Farhat's family from genocide in Gaza](https://www.gofundme.com/f/saving-dr-farhats-family-towards-hope) (#248 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A252))
|
||||
* [Assist in evacuating my family from the war in Gaza.](https://www.gofundme.com/f/Help-Mohammed-alhabil-Family) (#166 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A170))
|
||||
* [Family relief and survival from massacres](https://www.gofundme.com/f/family-relief-and-survival-from-massacres) ([verified by association](https://www.tumblr.com/a-shade-of-blue/761542408474345472); this fundraiser belongs to a niece of Muhammad Al-Habil, whose fundraiser is linked just above)
|
||||
* [Help me and my family escape to safety](https://www.gofundme.com/f/help-me-and-my-family-escape-to-safety) ([verified by association](https://www.tumblr.com/a-shade-of-blue/762595796863647744/only-330-raised-of-50000-goal-last-donation); this fundraiser belongs to a nephew of Muhammad Al-Habil, whose fundraiser is linked just above)
|
||||
* [Emergency: Help Evacuate My Family From GAZA WAR](https://www.gofundme.com/f/help-save-the-lives-of-my-family-in-gaza) (#26 in [Operation Olive Branch spreadsheet's masterlist tab](https://docs.google.com/spreadsheets/d/1vtMLLOzuc6GpkFySyVtKQOY2j-Vvg0UsChMCFst_WLA/edit?gid=1061843896#gid=1061843896&fvid=647479800); also listed in [Project Watermelon Donation Links spreadsheet's Donate to Families in Gaza tab](https://docs.google.com/spreadsheets/d/16XhzsCbsRV-cMAzRA8gTNxaYt_FAbf6nq3ZNGP4Q9U8/edit?gid=1452518893#gid=1452518893), under the family name Haya Nahed Alshawish)
|
||||
* [Urgent Help Needed to Evacuate My Family from Gaza](https://www.gofundme.com/f/e6vc7g-urgent-help-needed-to-evacuate-my-family-from-gaza) (shared by [90-ghost](https://www.tumblr.com/90-ghost/756556255005491200))
|
||||
* [Help Nour and his family escape from the war in Gaza](https://www.gofundme.com/f/donate-to-help-nour-and-his-family-escape-the-war-in-gaza) (verified by [roadimusprime](https://www.tumblr.com/roadimusprime/757885015014227968/and-at-the-time-that-i-am-writing-this-our) who helped setting up this fundraiser)
|
||||
* [Help Musab and his family,my pets survive this war in Gaza.](https://www.gofundme.com/f/help-musab-and-his-family-survive-this-war-in-gaza-they-nee) (Musab's story was shared by [90-ghost](https://www.tumblr.com/90-ghost/755436620006686720), but this new fundraiser [needed to be created for Musab due to a scam](https://www.tumblr.com/musababed/757808283298004993))
|
||||
* [Hope and Safety for Amal and Her Children in Gaza](https://www.gofundme.com/f/hope-and-safety-for-amal-and-her-children-in-gaza) (shared by [90-ghost](https://www.tumblr.com/90-ghost/755297146774978560))
|
||||
* [Please Save What's Left of My Family](https://www.gofundme.com/f/donate-to-restore-the-lives-of-the-balousha-family) (verified by [90-ghost](https://www.tumblr.com/90-ghost/757288369570152448/legit-fundraiser))
|
||||
* [Save Mohamed and his elderly parents from genocide](https://www.gofundme.com/f/save-mohamed-and-his-elderly-parents-from-genocide?attribution_id=sl:7b8ea3b1-7192-4e6d-ac64-285c74c9c372) (verified by [90-ghost](https://www.tumblr.com/90-ghost/755378263140024320/legit-fundraiser))
|
||||
* [Support Gaza Family's Journey to Recovery](https://www.gofundme.com/f/fzgga8-support-gaza-familys-journey-to-recovery) (shared by [90-ghost](https://www.tumblr.com/90-ghost/757170837598126080))
|
||||
* [Help us build new hope for me and my family](https://www.gofundme.com/f/help-us-build-new-hope-for-me-and-my-family) (shared by [90-ghost](https://www.tumblr.com/90-ghost/754544734691786752) and [northgazaupdates](https://www.tumblr.com/northgazaupdates/754969078935044096))
|
||||
* [Helping the Helles family achieve a better future](https://www.gofundme.com/f/helping-the-helles-family-achieve-a-better-future) (verified by [90-ghost](https://www.tumblr.com/90-ghost/757170722766454784/legit-fundraiser))
|
||||
* [Help save my family to survive the genocide in Gaza](https://www.gofundme.com/f/Stand-With-AlBalawi-Family) (verified by [90-ghost](https://www.tumblr.com/90-ghost/754486025365209088/legit-fundraiser))
|
||||
* [Helping my family rebuild our home](https://www.gofundme.com/f/dr66mz-helping-my-family-rebuild-our-home) (#198 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A202))
|
||||
* [Help Mohamed's children escape from the Gaza war to Safety](https://www.gofundme.com/f/please-help-this-family-get-out-of-the-gaza-war) (shared by [90-ghost](https://www.tumblr.com/90-ghost/755749578786127872))
|
||||
* [Death chases my family in Gaza; help me save them](https://www.gofundme.com/f/death-chases-my-family-in-gaza-help-me-save-them) (#151 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A155))
|
||||
* [Help My Family Evacuate to Safety](https://www.gofundme.com/f/gtuw5-help-my-family-evacuate-to-safety) (verified by association; this fundraiser was [shared by their friend Mohi](https://www.tumblr.com/dlxxv-vetted-donations/758442260915421184), whose [fundraiser](https://www.gofundme.com/f/support-mohis-fight-for-survival-in-gaza) was shared by [90-ghost](https://www.tumblr.com/90-ghost/757063664601858048))
|
||||
* [Help Youssef Provide Medicine and Shelter](https://www.gofundme.com/f/help-youssef-provide-medicine-and-shelter) (#255 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A259); row 132 in [Bees and Watermelons](https://linktr.ee/beesandwatermelons)' [priority campaigns spreadsheet](https://docs.google.com/spreadsheets/d/1J-1KNHXmvY098fuCDWiOIm1fcphAgkXA8nnWqOSXouw/edit?gid=283092143#gid=283092143&range=B132))
|
||||
* [Save Malak's Dreams and Family from Gaza](https://www.gofundme.com/f/save-malaks-dreams-and-family-from-gaza) (verified by [90-ghost](https://www.tumblr.com/90-ghost/756023439469477889/legit-fundraiser))
|
||||
* [Help Alaa family from this hard war](https://www.gofundme.com/f/sp46z-help-my-family-to-travel-to-a-safe-place) (#99 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?pli=1&gid=0#gid=0&range=A103))
|
||||
* [Help Ahmed Jehad and his newborn survive!](https://www.gofundme.com/f/ahmed-jehad-and-his-newborn-need-to-survive) (Row 171 in [Bees and Watermelons](https://linktr.ee/beesandwatermelons)' [priority campaigns spreadsheet](https://docs.google.com/spreadsheets/d/1J-1KNHXmvY098fuCDWiOIm1fcphAgkXA8nnWqOSXouw/edit?gid=283092143#gid=283092143&range=B171))
|
||||
* [Help Evacuate My Family from Gaza to Safety](https://www.gofundme.com/f/reconstruction-of-a-destroyed-house) (#125 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A129))
|
||||
* [Help my family survive and start a new life](https://www.gofundme.com/f/help-baby-omar-and-his-family-survive) (shared by [90-ghost](https://www.tumblr.com/90-ghost/758166499964878848))
|
||||
* [Evacuate Yousef and his family from Gaza](https://www.gofundme.com/f/evacuate-yousef-and-his-family-from-gaza) (#246 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A250))
|
||||
* [Help Nour and her Family Achieve a Better Future](https://www.gofundme.com/f/save-nour-and-her-family-from-war-help-them-escape) (shared by [90-ghost](https://www.tumblr.com/90-ghost/756899183536078848))
|
||||
* [Support Aseeel Fight for Life and Family in Gaza](https://www.gofundme.com/f/support-aseels-fight-for-life-and-family-in-gaza) (verified by [90-ghost](https://www.tumblr.com/90-ghost/757064652185337856/legit))
|
||||
* [Support & Help My Family From Gaza Strip](https://www.gofundme.com/f/help-the-family-of-misk-sohaib-get-out-to-safety) (shared by [90-ghost](https://www.tumblr.com/90-ghost/757065363567640576))
|
||||
* [Help Oday And Family Evacuate Gaza , Rebuild Lives](https://www.gofundme.com/f/help-oday-and-family-evacuate-gaza-rebuild-lives) (#261 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A265))
|
||||
* [Help heba and his family to survive their life](https://www.gofundme.com/f/help-heba-and-his-family-to-survive-their-life) (shared by [90-ghost](https://www.tumblr.com/90-ghost/757185259555127296))
|
||||
* [Defend my brother's family from genocide in gaza](https://www.gofundme.com/f/help-poor-injyred-in-gaza) (shared by [90-ghost](https://www.tumblr.com/90-ghost/758172410029719552))
|
||||
* [Support Asil Fight for Life and Family in Gaza](https://www.gofundme.com/f/support-aseels-fight-for-life-and-family-in-gaza) (verified by [90-ghost](https://www.tumblr.com/90-ghost/757743956823048192/dont-forget-to-help-asel))
|
||||
* [Support Family escape from rafah to save place](https://www.gofundme.com/f/help-ayahs-family-escape-gaza-crisis) (verified by association; this fundraiser was [shared by their friend Mohi](https://www.tumblr.com/ana-bananya/758258451537395712/this-campaign-belongs-to-a-friend-of-mohiy-gaza), whose [fundraiser](https://www.gofundme.com/f/support-mohis-fight-for-survival-in-gaza) was shared by [90-ghost](https://www.tumblr.com/90-ghost/757063664601858048))
|
||||
* [Bara El Shaer Family Survival Fund](https://www.gofundme.com/f/el-shaer-family-survival-fund) (Baraa has an [Instagram account](https://www.instagram.com/bara_belal038/) that has been used for years, and this fundraiser has also been shared by another Instagram user, [river2cgza](https://www.instagram.com/river2cgza/), along with other verified fundraisers)
|
||||
* [Help Safaa secure her children‘s lives after war](https://www.gofundme.com/f/help-safaa-secure-her-familys-life-after-war) (#135 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A139))
|
||||
* [Desperate plea: Help my family evacuate Gaza war!](https://www.gofundme.com/f/karamsaid) (#109 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A113))
|
||||
* [Help us build new hope for me and my family](https://www.gofundme.com/f/a6mv2z-help-us-build-new-hope-for-me-and-my-family) ([verified by association](https://www.tumblr.com/a-shade-of-blue/759269512056029184/donate-to-help-us-build-new-hope-for-me-and-my); Hassan is a nephew of Mohammed Alhabeel, whose fundraiser is lited as #166 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A170). Note that this is not the same fundraiser as the above that shared the same title)
|
||||
* [Help Dr. Shahad and Farah complete their education](https://www.gofundme.com/f/help-dr-shahad-and-farah) (shared by [90-ghost](https://www.tumblr.com/90-ghost/756898822254395392) and vetted by association; Farah is [Nesma's sister](https://www.tumblr.com/nesmamomen/755734008155996160/only-5-seconds-of-your-time), and Nesma's fundraiser has been vetted by [el-shab-hussein](https://www.tumblr.com/el-shab-hussein/749848198102827008/hello-good-sir-ive-been-supporting-a-family-but) and shared by [nabulsi](https://www.tumblr.com/nabulsi/752822250810605568))
|
||||
* [Amira's Story: Between Hope and Resilience - A Call for Soli](https://www.gofundme.com/f/amiras-story-between-hope-and-resilience-a-call-for-soli) (verified by [nabulsi](https://www.tumblr.com/nabulsi/752577548412731392/donate-to-amiras-story-between-hope-and))
|
||||
* [Help Save Ahmed Family From Gaza](https://www.gofundme.com/f/help-save-ahmed-family-from-gaza) ([verified by association](https://www.tumblr.com/a-shade-of-blue/759268417249361920); Ahmed is the brother of Mohammed Alhabeel, whose fundraiser is lited as #166 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A170).)
|
||||
* [From Rubble to Success: Helping Osama Rebuild His Dream](https://www.gofundme.com/f/Helping-Osama-Rebuild-His-Dream) (shared by [90-ghost](https://www.tumblr.com/90-ghost/760346545235329024))
|
||||
* [Support a Family's Journey to Safety and Peace](https://www.gofundme.com/f/support-a-family-journey-to-safety-and-peace) ([verified by association](https://www.tumblr.com/c-u-c-koo-4-40k/763044851084296192/everyone-this-is-a-friend-of-osama-basil-sh); this fundraiser belongs to a friend of Osama, whose fundraiser is linked just above. Also shared by [90-ghost](https://www.tumblr.com/90-ghost/763057383436107776))
|
||||
* [Please help my family in Gaza](https://www.gofundme.com/f/q37qd5-please-help-my-family-in-gaza) (shared by [90-ghost](https://www.tumblr.com/90-ghost/760328893428367360))
|
||||
* [Help Eslam Save Her Family](https://www.gofundme.com/f/help-eslam-save-her-family) (row 175 in [Bees and Watermelons](https://linktr.ee/beesandwatermelons)' [priority campaigns spreadsheet](https://docs.google.com/spreadsheets/d/1J-1KNHXmvY098fuCDWiOIm1fcphAgkXA8nnWqOSXouw/edit?gid=283092143#gid=283092143&range=D175))
|
||||
* [Helping my family to evacuate from Gaza](https://www.gofundme.com/f/kr85g-safe-my-family) (verified by [gaza-evacuation-funds](https://www.tumblr.com/gaza-evacuation-funds/760224788809695232/todays-vetted-fundraisers); also [verified by association](https://www.tumblr.com/dlxxv-vetted-donations/759148525328220160/this-campaign-is-vetted-by-association-through); Ayaa Mahmoud, the person this fundraiser belongs to, is the sister-in-law of Eslam, another Palestinian whose fundraiser is linked just above)
|
||||
* [Helping Lama & Mohammed to evacuate from Gaza](https://www.gofundme.com/f/helping-lama-mohammed-to-evacuate-from-gaza) ([verified by association](https://www.tumblr.com/dlxxv-vetted-donations/759976289261305856/lama-is-indeed-vetted-by-association-through); Lama's husband is an acquaintance of Ayaa Mahmoud, whose fundraiser is linked just above)
|
||||
* [Help Abood From the War](https://www.gofundme.com/f/help-abboud-from-the-war) ([verified by association](https://www.tumblr.com/dlxxv-vetted-donations/758342969132728320); this fundraiser belongs to the brother of Mohi, whose [fundraiser](https://www.gofundme.com/f/support-mohis-fight-for-survival-in-gaza) was shared by [90-ghost](https://www.tumblr.com/90-ghost/757063664601858048))
|
||||
* [Save My Family from the War Nightmare in Gaza](https://www.gofundme.com/f/abgwv-help-us) (shared by [northgazaupdates](https://www.tumblr.com/northgazaupdates/756089494338420736))
|
||||
* [Help Salem Family From Gaza](https://www.gofundme.com/f/help-salem-family-from-gaza) (verified by [90-ghost](https://www.tumblr.com/90-ghost/757065439413174272/legit))
|
||||
* [Please save me, my family and my future from the brutal war](https://www.gofundme.com/f/help-me-to-complete-my-education-in-gaza) (verified by [90-ghost](https://www.tumblr.com/90-ghost/755122761409642496/legit-fundraiser))
|
||||
* [Help Nour and his family escape from the war in Gaza](https://www.gofundme.com/f/donate-to-help-nour-and-his-family-escape-the-war-in-gaza) (shared by [90-ghost](https://www.tumblr.com/90-ghost/760377411969875968))
|
||||
* [Help Muhammad and his family live a better life](https://www.gofundme.com/f/znu6m-help-muhammad-and-his-family-live-a-better-life) (verified by [90-ghost](https://www.tumblr.com/90-ghost/760285761067253760/legit-fundraiser))
|
||||
* [Donate to Ahmed's Journey to Safety and Education](https://www.gofundme.com/f/donate-to-ahmeds-journey-to-safety-and-education) (shared by [90-ghost](https://www.tumblr.com/90-ghost/758711734896918528))
|
||||
* [Support Amal's Family in War-Torn Gaza Escape to Egypt](https://www.gofundme.com/f/support-amals-family-in-wartorn-gaza-escape-to-egypt) ([verified by association](https://www.tumblr.com/acepumpkinpatrick/761630225423646720); this fundraiser belongs to the brother of [Asmaa](https://www.tumblr.com/asmaamajed2/761630455342776320), whose [fundraiser](https://www.gofundme.com/f/8wewmz-help-asmaa-to-continue-school-outside-of-gaza) has been shared by [90-ghost](https://www.tumblr.com/90-ghost/758170896860151808))
|
||||
* [Help Us Escape the Ravages of War: Emergency Evacuation Fund](https://www.gofundme.com/f/help-us-escape-the-ravages-of-war-emergency-evacuation-fund) (#212 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A216))
|
||||
* [Help Nevin alserr family escape the war](https://www.gofundme.com/f/support-niveen-familys-journey-to-safety) (#314 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A318))
|
||||
* [Help Displaced Family in Gaza](https://www.gofundme.com/f/gga9a-help-displaced-families-in-gaza) (shared by [Salaah Bilaal](https://www.tumblr.com/bilal-salah0/759280967486767104/please-share-the-post), whose fundraiser is #132 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A136))
|
||||
* [Your help is the only hope to save Abdallah & his family from genocide](https://gogetfunding.com/your-help-is-the-only-hope-to-save-abdallah-his-family-from-genocide/) (#315 in [el-shab-hussein and nabulsi’s Vetted Gaza Fundraiser List](https://docs.google.com/spreadsheets/d/1yYkNp5U3ANwILl2MknJi9G7ArY4uVTEEQ1CVfzR8Ioo/edit?gid=0#gid=0&range=A319))
|
||||
* [Rising from the Ashes: Ghada’s Journey of Hope and Resilien](https://www.gofundme.com/f/rising-from-the-ashes-ghadas-journey-of-hope-and-resilien) (#6 in [GazaVetters](https://www.tumblr.com/gazavetters)' [spreadsheet](https://docs.google.com/spreadsheets/d/1YGgkXoyam7tnbXb-vqWsHFs3Puyf_xYeXY2dPrZQY1M/edit?gid=0#gid=0&range=A11))
|
||||
* [Urgent,Help us Recover our Home, Education,Work and FAMILY](https://www.gofundme.com/f/urgent-help-us-recover-our-home-education-and-work) (shared by [gaza-evacuation-funds](https://www.tumblr.com/gaza-evacuation-funds/762875245776502784/donate-to-urgenthelp-us-recover-our-home))
|
||||
* [Ahmed's Autism Journey: a Path of Hope and Healing](https://www.paypal.com/donate?campaign_id=EJ892Z3JYTPYS) ([vetted by association](https://www.tumblr.com/a-shade-of-blue/762812657802838016/vetted-by-association-as-you-can-see-above-this); this campaign was [promoted by Nesma Ahmed](https://www.tumblr.com/nesmamomen/762057439135842304), whose [fundraiser](https://www.gofundme.com/f/help-nesmas-and-her-family-evacuate) had been vetted by [el-shab-hussein](https://www.tumblr.com/el-shab-hussein/749848198102827008/hello-good-sir-ive-been-supporting-a-family-but))
|
||||
* [Help my family survive and start a new life](https://www.gofundme.com/f/urgent-help-needed-a-journey-from-gaza-to-safety) (#75 in [GazaVetters](https://www.tumblr.com/gazavetters)' [spreadsheet](https://docs.google.com/spreadsheets/d/1YGgkXoyam7tnbXb-vqWsHFs3Puyf_xYeXY2dPrZQY1M/edit?gid=0#gid=0&range=A80). Note that this is not the same fundraiser as the above that shared the same title)
|
||||
* [Help Anas & Ahmed rebuild their future in Gaza](https://www.gofundme.com/f/Help-Anas-and-Ahmed-in-Gaza) (verified by [90-ghost](https://www.tumblr.com/90-ghost/762445389624492032/legit) and listed as #83 in [GazaVetters](https://www.tumblr.com/gazavetters)' [spreadsheet](https://docs.google.com/spreadsheets/d/1YGgkXoyam7tnbXb-vqWsHFs3Puyf_xYeXY2dPrZQY1M/edit?gid=0#gid=0&range=A88))
|
||||
* [Help Mahmoud's Family Overcome War Tragedy](https://www.gofundme.com/f/help-mahmouds-family-overcome-war-tragedy) (verified by [90-ghost](https://www.tumblr.com/90-ghost/755121680324657152/legit-fundraiser))
|
||||
* [Help me get food water and medicine for my little kids !!](https://www.gofundme.com/f/r8kj4-help-me-get-food-water-and-medicine-for-my-little-kids) ([verified by Salaah Bilaal](https://www.tumblr.com/mohammedayyads-blog/760010459463254016/donate-to-help-me-get-food-water-and-medicine-for))
|
||||
* [Emergency Appeal: Help Save My Children's](https://www.gofundme.com/f/emergency-appeal-help-save-my-childrens) (#187 in [GazaVetters](https://www.tumblr.com/gazavetters)' [spreadsheet](https://docs.google.com/spreadsheets/d/1YGgkXoyam7tnbXb-vqWsHFs3Puyf_xYeXY2dPrZQY1M/edit?gid=0#gid=0&range=A192))
|
||||
* [Help me get my family out of Gaza. Their lives have become v](https://www.gofundme.com/f/help-me-get-my-family-out-of-gaza-their-lives-have-become-v) (#192 in [GazaVetters](https://www.tumblr.com/gazavetters)' [spreadsheet](https://docs.google.com/spreadsheets/d/1YGgkXoyam7tnbXb-vqWsHFs3Puyf_xYeXY2dPrZQY1M/edit?gid=0#gid=0&range=A197))
|
||||
* [Save the family of Salman Hellis from the Gaza Strip](https://www.gofundme.com/f/save-the-family-of-salman-hellis-from-the-gaza-strip) (verified by [90-ghost](https://www.tumblr.com/90-ghost/761647879548993536))
|
||||
* [Support Khawla's Family in Gaza Crisis](https://www.gofundme.com/f/support-khawlas-family-in-gaza-crisis) ([verified by association](https://www.tumblr.com/ana-bananya/761532739985047552/donate-to-support-khawlas-family-in-gaza-crisis); this fundraiser belongs to the sister of Mohi, whose [fundraiser](https://www.gofundme.com/f/support-mohis-fight-for-survival-in-gaza) was shared by [90-ghost](https://www.tumblr.com/90-ghost/757063664601858048))
|
||||
* [Help Ahlam Rebuild Her Life and Dreams](https://www.gofundme.com/f/help-ahlam-rebuild-her-life-and-dreams) (#73 in [GazaVetters](https://www.tumblr.com/gazavetters)' [spreadsheet](https://docs.google.com/spreadsheets/d/1YGgkXoyam7tnbXb-vqWsHFs3Puyf_xYeXY2dPrZQY1M/edit?gid=0#gid=0&range=A78))
|
||||
* [Help Zaen and Yehya to get out of Gaza](https://www.gofundme.com/f/help-zaen-and-yehya-to-get-out-of-gaza) (#127 in [GazaVetters](https://www.tumblr.com/gazavetters)' [spreadsheet](https://docs.google.com/spreadsheets/d/1YGgkXoyam7tnbXb-vqWsHFs3Puyf_xYeXY2dPrZQY1M/edit?gid=0#gid=0&range=A132))
|
||||
* [Help this family to get out of Gaza](https://www.gofundme.com/f/help-this-family-to-get-out-of-gaza) (#111 in [GazaVetters](https://www.tumblr.com/gazavetters)' [spreadsheet](https://docs.google.com/spreadsheets/d/1YGgkXoyam7tnbXb-vqWsHFs3Puyf_xYeXY2dPrZQY1M/edit?gid=0#gid=0&range=A116))
|
||||
* [Help me to evacuate from this crazy war in Gaza](https://www.gofundme.com/f/m6hdrt-support-ahmad-to-complete-his-education) (#119 in [GazaVetters](https://www.tumblr.com/gazavetters)' [spreadsheet](https://docs.google.com/spreadsheets/d/1YGgkXoyam7tnbXb-vqWsHFs3Puyf_xYeXY2dPrZQY1M/edit?gid=0#gid=0&range=A124))
|
||||
* [Help Ahmed Continue His Education and Support His Family](https://www.gofundme.com/f/help-ahmed-continue-his-education-and-support-his-family) (#303 in [GazaVetters](https://www.tumblr.com/gazavetters)' [spreadsheet](https://docs.google.com/spreadsheets/d/1YGgkXoyam7tnbXb-vqWsHFs3Puyf_xYeXY2dPrZQY1M/edit?gid=0#gid=0&range=A308))
|
||||
* [Support Ibrahim’s Fight For Survival](https://www.gofundme.com/f/support-ibrahims-fight-for-survival) (#220 in [GazaVetters](https://www.tumblr.com/gazavetters)' [spreadsheet](https://docs.google.com/spreadsheets/d/1YGgkXoyam7tnbXb-vqWsHFs3Puyf_xYeXY2dPrZQY1M/edit?gid=0#gid=0&range=A225))
|
||||
* [Please help us with travel and treatment for urgent need](https://www.gofundme.com/f/please-help-us-with-travel-and-treatment-for-urgent-need) (#99 in [GazaVetters](https://www.tumblr.com/gazavetters)' [spreadsheet](https://docs.google.com/spreadsheets/d/1YGgkXoyam7tnbXb-vqWsHFs3Puyf_xYeXY2dPrZQY1M/edit?gid=0#gid=0&range=A104))
|
||||
* [Donate to help Mona's famil to evacuate from Gaza](https://www.gofundme.com/f/donate-to-help-monas-famil-to-evacuate-from-gaza) (#253 in [GazaVetters](https://www.tumblr.com/gazavetters)' [spreadsheet](https://docs.google.com/spreadsheets/d/1YGgkXoyam7tnbXb-vqWsHFs3Puyf_xYeXY2dPrZQY1M/edit?gid=0#gid=0&range=A258))
|
||||
* [Help me evacuate my family to safety and peace.](https://www.gofundme.com/f/help-me-evacuate-my-family-to-safety-and-peace) (#284 in [GazaVetters](https://www.tumblr.com/gazavetters)' [spreadsheet](https://docs.google.com/spreadsheets/d/1YGgkXoyam7tnbXb-vqWsHFs3Puyf_xYeXY2dPrZQY1M/edit?gid=0#gid=0&range=A289))
|
||||
* [Help my family](https://www.gofundme.com/f/3td2x-help-my-family) (shared by [90-ghost](https://www.tumblr.com/90-ghost/768134181180129280))
|
BIN
src/assets/adoptables/32bitcafe-vacation/alexandra-stamp.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
src/assets/adoptables/32bitcafe-vacation/passport.png
Normal file
After Width: | Height: | Size: 188 KiB |
BIN
src/assets/adoptables/32bitcafe-vacation/ribose-riverfish.png
Normal file
After Width: | Height: | Size: 741 B |
BIN
src/assets/adoptables/alexandra-diddles.png
Normal file
After Width: | Height: | Size: 956 B |
BIN
src/assets/adoptables/kittyfriends/artwork-bat.gif
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
src/assets/adoptables/kittyfriends/artwork-pumpkin.gif
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/adoptables/kittyfriends/artwork-vampire.gif
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
src/assets/adoptables/kittyfriends/artwork-witch.gif
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
src/assets/adoptables/kittyfriends/krish.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/adoptables/kittyfriends/leilukin-blackkitty.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/assets/adoptables/kittyfriends/leilukin-purplekitty.png
Normal file
After Width: | Height: | Size: 14 KiB |