initial commit
This commit is contained in:
commit
c70fc72952
143 changed files with 13594 additions and 0 deletions
6
.env
Normal file
6
.env
Normal file
|
@ -0,0 +1,6 @@
|
|||
ACCOUNT_ID_PLEX=
|
||||
SUPABASE_URL=
|
||||
SUPABASE_KEY=
|
||||
CF_ACCOUNT_ID=
|
||||
CF_ZONE_ID=
|
||||
RSS_TO_MASTODON_KV_NAMESPACE_ID=
|
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
# build output
|
||||
dist/
|
||||
|
||||
# workers
|
||||
wrangler.toml
|
||||
.wrangler
|
||||
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# environment variables
|
||||
.env.local
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# VS Code files
|
||||
.vscode/
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Cory Dransfeldt
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
9
README.md
Normal file
9
README.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# coryd.dev
|
||||
|
||||
Hi! I'm Cory. 👋🏻
|
||||
|
||||
This is the code for my personal website and portfolio. Built using [11ty](https://www.11ty.dev) and [other tools](https://coryd.dev/colophon).
|
||||
|
||||
[](https://follow.coryd.dev/@cory) [](https://www.buymeacoffee.com/cory) [](https://eleventy.dev)
|
||||
|
||||
[Music](https://coryd.dev/music) / [Watching](https://coryd.dev/watching) / [Books](https://coryd.dev/books) / [Now](https://coryd.dev/now)
|
67
_headers
Normal file
67
_headers
Normal file
|
@ -0,0 +1,67 @@
|
|||
/feeds/album-releases
|
||||
Content-Type: application/xml; charset=utf-8
|
||||
x-content-type-options: nosniff
|
||||
|
||||
/feeds/album-releases.json
|
||||
Content-Type: application/json
|
||||
|
||||
/feeds/all
|
||||
Content-Type: application/xml; charset=utf-8
|
||||
x-content-type-options: nosniff
|
||||
|
||||
/feeds/all.json
|
||||
Content-Type: application/json
|
||||
|
||||
/feeds/books
|
||||
Content-Type: application/xml; charset=utf-8
|
||||
x-content-type-options: nosniff
|
||||
|
||||
/feeds/books.json
|
||||
Content-Type: application/json
|
||||
|
||||
/feeds/links
|
||||
Content-Type: application/xml; charset=utf-8
|
||||
x-content-type-options: nosniff
|
||||
|
||||
/feeds/links.json
|
||||
Content-Type: application/json
|
||||
|
||||
/feeds/posts
|
||||
Content-Type: application/xml; charset=utf-8
|
||||
x-content-type-options: nosniff
|
||||
|
||||
/feeds/posts.json
|
||||
Content-Type: application/json
|
||||
|
||||
/feeds/movies
|
||||
Content-Type: application/xml; charset=utf-8
|
||||
x-content-type-options: nosniff
|
||||
|
||||
/feeds/movies.json
|
||||
Content-Type: application/json
|
||||
|
||||
/feeds/syndication
|
||||
Content-Type: application/xml; charset=utf-8
|
||||
x-content-type-options: nosniff
|
||||
|
||||
/api/search
|
||||
Content-Type: application/json
|
||||
|
||||
/.well-known/webfinger
|
||||
Content-Type: application/jrd+json; charset=utf-8
|
||||
|
||||
/blogroll.opml
|
||||
Content-Disposition: attachment; filename=cory-dransfeldt-blogroll.opml
|
||||
|
||||
/music/releases.ics
|
||||
Content-Type: text/calendar
|
||||
Cache-Control: public, max-age=0, must-revalidate
|
||||
Access-Control-Allow-Origin: *
|
||||
Content-Disposition: inline; filename="releases.ics"
|
||||
|
||||
/*
|
||||
Content-Security-Policy: upgrade-insecure-requests; block-all-mixed-content;
|
||||
X-Frame-Options: DENY
|
||||
X-XSS-Protection: 1; mode=block
|
||||
Referrer-Policy: strict-origin-when-cross-origin, no-referrer-when-downgrade
|
||||
Permissions-Policy: autoplay=(), camera=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=()
|
72
_redirects
Normal file
72
_redirects
Normal file
|
@ -0,0 +1,72 @@
|
|||
# 404s
|
||||
/now.html /now 301
|
||||
/contact.html /contact 301
|
||||
|
||||
# feeds
|
||||
/feed.xml /feeds/posts 301
|
||||
/follow.xml /feeds/all 301
|
||||
/feeds/posts.xml /feeds/posts 301
|
||||
/feeds/links.xml /feeds/links 301
|
||||
/feeds/books.xml /feeds/books 301
|
||||
/feeds/movies.xml /feeds/movies 301
|
||||
/feeds/all.xml /feeds/all 301
|
||||
/feeds/posts/ /feeds/posts 301
|
||||
/feeds/links/ /feeds/links 301
|
||||
/feeds/books/ /feeds/books 301
|
||||
/feeds/movies/ /feeds/movies 301
|
||||
/feeds/all/ /feeds/all 301
|
||||
|
||||
# general
|
||||
/articles/ /posts/ 301
|
||||
/tags /search 301
|
||||
|
||||
# blog posts
|
||||
/blog/fixing-safari-icloud-syncing /posts/2022/fixing-safari-icloud-syncing/ 301
|
||||
/blog/migrating-to-fastmail /posts/2022/migrating-to-fastmail/ 301
|
||||
/blog/client-side-webmentions-in-nextjs /posts/2023/client-side-webmentions-in-nextjs/ 301
|
||||
/blog/apple-centric-digital-privacy-tools /posts/2022/apple-centric-digital-privacy-tools/ 301
|
||||
/blog/automating-rss-syndication-with-nextjs-github /posts/2023/automating-rss-syndication-with-nextjs-github/ 301
|
||||
/blog/apple-music-a-tale-of-woe /posts/2021/apple-music-a-tale-of-woe/ 301
|
||||
/blog/adding-client-side-rendered-webmentions-to-my-blog /posts/2023/client-side-webmentions-in-nextjs/ 301
|
||||
/blog/automating-email-cleanup-in-gmail /posts/2022/automating-email-cleanup-in-gmail/ 301
|
||||
/blog/fastmail-handling-inbound-email-with-regex-filters-now-with-chatgpt /posts/2023/fastmail-handling-inbound-email-with-regex-filters-now-with-chatgpt/ 301
|
||||
/blog/simple-api-fetch-hooks-with-swr /posts/2022/simple-api-fetch-hooks-with-swr/ 301
|
||||
/2023/02/automatingandprobablyoverengineeringmy-nowpage /posts/2023/automating-and-overengineering-my-now-page/ 301
|
||||
/2023/01/workflows-handling-inbound-email-on-fastmail-with-regular-expressions /posts/2023/fastmail-handling-inbound-email-with-regex-filters-now-with-chatgpt/ 301
|
||||
/posts/2023/i-block-ads/null /posts/2023/i-block-ads/ 301
|
||||
/posts/2023/i-dont-want-streaming-music/null /posts/2023/i-dont-want-streaming-music/ 301
|
||||
/posts/2022/migrating-to-fastmail/null /posts/2022/migrating-to-fastmail/ 301
|
||||
/posts/2023/webmentions-in-eleventy/null /posts/2023/webmentions-in-eleventy/ 301
|
||||
/posts/2023/a-safari-specific-guide-to-making-the-modern-web-suck-less/null /posts/2023/a-safari-specific-guide-to-making-the-modern-web-suck-less/ 301
|
||||
/posts/2024/dont-be-afraid-to-admit-when-you-dont-know-something/ /posts/2024/dont-be-afraid-to-admin-when-you-dont-know-something/ 301
|
||||
/posts/2024/data-sharing-should-always-be-opt-in/ /posts/2024/access-to-data-isnt-a-grant-to-exploit-it/ 301
|
||||
/posts/2023/popular-posts-widget-using-eleventy-plausible/ /posts/2023/building-a-popular-posts-widget-in-eleventy-using-plausible-analytics/ 301
|
||||
/posts/2023/i-dont-want-streaming-music/ /posts/2023/i-dont-want-streaming-music-i-just-want-to-stream-my-music/ 301
|
||||
/posts/2021/apple-music-a-tale-of-woe/ /posts/2022/apple-music-a-tale-of-woe/ 301
|
||||
/posts/2024/weaving-music-in-and-out-of-my-personal-site/ /posts/2024/weaving-music-data-in-and-out-of-my-personal-website/ 301
|
||||
/posts/2023/fastmail-handling-inbound-email-with-regex-filters-now-with-chatgpt/ /posts/2023/workflows-handling-inbound-email-on-fastmail-with-regular-expressions-now-featuring-chatgpt/ 301
|
||||
/-want-anything-your-ai-generates/ /posts/2024/i-dont-want-anything-your-ai-generates/ 301
|
||||
/posts/2023/default-apps-2023/ /posts/2023/my-default-apps-2023-edition/ 301
|
||||
/posts/2024/dont-be-afraid-to-admin-when-you-dont-know-something/ /posts/2024/dont-be-afraid-to-admin-when-you-dont-know-something/ 301
|
||||
/posts/2023/displaying-listening-data-from-apple-music-using-musickit/ /posts/2023/displaying-listening-data-from-apple-music-using-musickitjs/ 301
|
||||
/posts/2024/2024-minimalism-as-self-preservation/ /posts/2024/minimalism-as-self-preservation/ 301
|
||||
/posts/2023/client-side-webmentions-in-nextjs/ /posts/2023/adding-client-side-webmentions-to-my-nextjs-blog/ 301
|
||||
/posts/2024/2024-adblocker-required/ /posts/2024/adblocker-required/ 301
|
||||
/posts/2023/scheduled-eleventy-builds-cron-github-actions/ /posts/2023/scheduled-eleventy-builds-on-vercel-with-cron-triggered-github-actions/ 301
|
||||
/posts/2024/against-the-commercial-web/ /posts/2024/against-the-commercial-internet/ 301
|
||||
/posts/2023/i-removed-tailwind-from-my-site/ /posts/2024/i-removed-tailwind-from-my-site/
|
||||
/posts/2024/handling-images-with-b2-netlify-image-cdn-hazel-mountain-duck/ /posts/2024/handling-images-with-b2-netlifys-image-cdn-hazel-and-mountain-duck/ 301
|
||||
/posts/2023/my-default-apps-2023-edition/ /uses 301
|
||||
/posts/2024/link-blogging-using-readwise/ /posts/2024/link-blogging-using-readwise-reader/ 301
|
||||
/2022/12/automating-email-cleanup-in-gmail /posts/2022/automating-email-cleanup-in-gmail/ 301
|
||||
/posts/2023/automate-syndicate-content-mastodon-eleventy/ /posts/2023/automate-and-syndicate-content-from-eleventy-to-mastodon/ 301
|
||||
/posts/2023/road-to-madness-apple-music-charts/ /posts/2023/road-to-madness-charting-apple-music-listening-data/ 301
|
||||
/posts/2023/semi-automated-hashtags-syndicated-posts/ /posts/2023/semi-automated-hashtags-for-syndicated-posts/ 301
|
||||
/posts/2023/automating-rss-syndication-with-nextjs-github/ /posts/2023/automating-rss-syndication-and-sharing-with-nextjs-and-github/ 301
|
||||
/posts/2023/locally-stored-music-and-storage-as-a-meaningful-constraint/ /posts/2023/doppler-locally-stored-music-and-storage-as-a-beneficial-constraint/ 301
|
||||
/blog/digital-privacy-tools /posts/2021/digital-privacy-tools/ 301
|
||||
/posts/2023/now-page-update-matter-favorites/ /posts/2023/now-page-update-favorite-articles-from-matter/ 301
|
||||
/posts/2023/now-playing-eleventy-netlify-edge-functions-emoji/ /posts/2023/displaying-now-playing-data-with-matching-emoji-using-netlify-edge-functions-and-eleventy/ 301
|
||||
/posts/2014/sublime-text-ctrl-tab-key-bindings/ /posts/2014/sublime-text-3-ctrl-tab-key-bindings/ 301
|
||||
/posts/2022/simple-api-fetch-hooks-with-swr/ /posts/2022/simple-data-fetching-with-custom-react-hooks-and-swr/ 301
|
||||
/posts/2023/drying-up-now-page-templates-eleventy/ /posts/2023/drying-up-now-page-templates-and-normalizing-data-in-eleventy/ 301
|
22
astro.config.mjs
Normal file
22
astro.config.mjs
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { defineConfig } from "astro/config";
|
||||
import cloudflare from "@astrojs/cloudflare";
|
||||
import react from "@astrojs/react";
|
||||
|
||||
export default defineConfig({
|
||||
output: "server",
|
||||
adapter: cloudflare(),
|
||||
integrations: [react()],
|
||||
vite: {
|
||||
resolve: {
|
||||
alias: {
|
||||
"@cdransf": "node_modules/@cdransf",
|
||||
"@components": "/src/components",
|
||||
"@data": "/src/utils/data",
|
||||
"@layouts": "/src/layouts",
|
||||
"@scripts": "/src/scripts",
|
||||
"@styles": "/src/styles",
|
||||
"@utils": "/src/utils",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
6984
package-lock.json
generated
Normal file
6984
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
28
package.json
Normal file
28
package.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "coryd.dev",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"build:worker": "node scripts/worker-build.mjs $WORKER_NAME",
|
||||
"deploy:worker": "wrangler deploy --env production --config workers/$npm_config_worker/wrangler.toml"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "^11.2.0",
|
||||
"@astrojs/react": "^3.6.2",
|
||||
"@tabler/icons-react": "^3.19.0",
|
||||
"astro": "^4.16.13",
|
||||
"luxon": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@supabase/supabase-js": "^2.45.4",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-anchor": "^9.2.0",
|
||||
"markdown-it-footnote": "^4.0.0",
|
||||
"markdown-it-prism": "^2.3.0"
|
||||
}
|
||||
}
|
BIN
public/fonts/ml.woff2
Normal file
BIN
public/fonts/ml.woff2
Normal file
Binary file not shown.
BIN
public/fonts/mlb.woff2
Normal file
BIN
public/fonts/mlb.woff2
Normal file
Binary file not shown.
BIN
public/fonts/mlbi.woff2
Normal file
BIN
public/fonts/mlbi.woff2
Normal file
Binary file not shown.
BIN
public/fonts/mli.woff2
Normal file
BIN
public/fonts/mli.woff2
Normal file
Binary file not shown.
288
public/scripts/index.js
Normal file
288
public/scripts/index.js
Normal file
|
@ -0,0 +1,288 @@
|
|||
window.addEventListener("load", () => {
|
||||
// menu keyboard controls
|
||||
(() => {
|
||||
const menuInput = document.getElementById("menu-toggle");
|
||||
const menuButtonContainer = document.querySelector(
|
||||
".menu-button-container"
|
||||
);
|
||||
const menuItems = document.querySelectorAll(".menu-primary li");
|
||||
|
||||
menuButtonContainer.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
menuInput.checked = !menuInput.checked;
|
||||
}
|
||||
});
|
||||
|
||||
menuItems.forEach((item) => {
|
||||
item.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
item.querySelector("a").click();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && menuInput.checked) menuInput.checked = false;
|
||||
});
|
||||
})();
|
||||
|
||||
// modal keyboard controls and scroll management
|
||||
(() => {
|
||||
const modalInputs = document.querySelectorAll(".modal-input");
|
||||
if (!modalInputs) return;
|
||||
|
||||
const toggleBodyScroll = (disableScroll) => {
|
||||
if (disableScroll) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
};
|
||||
|
||||
const checkModals = () => {
|
||||
let isAnyModalOpen = false;
|
||||
modalInputs.forEach((modalInput) => {
|
||||
if (modalInput.checked) isAnyModalOpen = true;
|
||||
});
|
||||
toggleBodyScroll(isAnyModalOpen);
|
||||
};
|
||||
|
||||
modalInputs.forEach((modalInput) => {
|
||||
modalInput.addEventListener("change", checkModals);
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
modalInputs.forEach((modalInput) => {
|
||||
if (modalInput.checked) modalInput.checked = false;
|
||||
});
|
||||
toggleBodyScroll(false);
|
||||
}
|
||||
});
|
||||
|
||||
checkModals();
|
||||
})();
|
||||
|
||||
// text toggle for media pages
|
||||
(() => {
|
||||
const button = document.querySelector("[data-toggle-button]");
|
||||
const content = document.querySelector("[data-toggle-content]");
|
||||
const text = document.querySelectorAll("[data-toggle-content] p");
|
||||
const minHeight = 500; // this needs to match the height set on [data-toggle-content].text-toggle-hidden in text-toggle.css
|
||||
const interiorHeight = Array.from(text).reduce(
|
||||
(acc, node) => acc + node.scrollHeight,
|
||||
0
|
||||
);
|
||||
|
||||
if (!button || !content || !text) return;
|
||||
|
||||
if (interiorHeight < minHeight) {
|
||||
content.classList.remove("text-toggle-hidden");
|
||||
button.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
button.addEventListener("click", () => {
|
||||
const isHidden = content.classList.toggle("text-toggle-hidden");
|
||||
button.textContent = isHidden ? "Show more" : "Show less";
|
||||
});
|
||||
})();
|
||||
|
||||
// search logic
|
||||
(() => {
|
||||
if (typeof MiniSearch === "undefined") return;
|
||||
|
||||
const miniSearch = new MiniSearch({
|
||||
fields: ["title", "description", "tags", "type"],
|
||||
idField: "id",
|
||||
storeFields: [
|
||||
"id",
|
||||
"title",
|
||||
"url",
|
||||
"description",
|
||||
"type",
|
||||
"tags",
|
||||
"total_plays",
|
||||
],
|
||||
searchOptions: {
|
||||
fields: ["title", "tags"],
|
||||
prefix: true,
|
||||
fuzzy: 0.1,
|
||||
boost: { title: 5, tags: 2, description: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
const $form = document.querySelector(".search__form");
|
||||
const $fallback = document.querySelector(".search__form--fallback");
|
||||
const $input = document.querySelector(".search__form--input");
|
||||
const $results = document.querySelector(".search__results");
|
||||
const $loadMoreButton = document.querySelector(".search__load-more");
|
||||
const $typeCheckboxes = document.querySelectorAll(
|
||||
'.search__form--type input[type="checkbox"]'
|
||||
);
|
||||
|
||||
$form.removeAttribute("action");
|
||||
$form.removeAttribute("method");
|
||||
if ($fallback) $fallback.remove();
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
let currentPage = 1;
|
||||
let currentResults = [];
|
||||
let total = 0;
|
||||
let debounceTimeout;
|
||||
|
||||
const parseMarkdown = (markdown) =>
|
||||
markdown
|
||||
? markdown
|
||||
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
||||
.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>')
|
||||
.replace(/\n/g, "<br>")
|
||||
.replace(/[#*_~`]/g, "")
|
||||
: "";
|
||||
|
||||
const truncateDescription = (markdown, maxLength = 150) => {
|
||||
const plainText =
|
||||
new DOMParser().parseFromString(parseMarkdown(markdown), "text/html")
|
||||
.body.textContent || "";
|
||||
return plainText.length > maxLength
|
||||
? `${plainText.substring(0, maxLength)}...`
|
||||
: plainText;
|
||||
};
|
||||
|
||||
const formatArtistTitle = (title, totalPlays) =>
|
||||
totalPlays > 0
|
||||
? `${title} <strong class="highlight-text">${totalPlays} plays</strong>`
|
||||
: title;
|
||||
|
||||
const renderSearchResults = (results) => {
|
||||
const resultHTML = results
|
||||
.map(
|
||||
({ title, url, description, type, total_plays }) => `
|
||||
<li class="search__results--result">
|
||||
<a href="${url}">
|
||||
<h3>${
|
||||
type === "artist" && total_plays
|
||||
? formatArtistTitle(title, total_plays)
|
||||
: title
|
||||
}</h3>
|
||||
</a>
|
||||
<p>${truncateDescription(description)}</p>
|
||||
</li>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
$results.innerHTML =
|
||||
resultHTML ||
|
||||
'<li class="search__results--no-results">No results found.</li>';
|
||||
$results.style.display = "block";
|
||||
};
|
||||
|
||||
const loadSearchIndex = async (query, types, page) => {
|
||||
try {
|
||||
const typeQuery = types.join(",");
|
||||
const response = await fetch(
|
||||
`https://coryd.dev/api/search?q=${query}&type=${typeQuery}&page=${page}&pageSize=${PAGE_SIZE}`
|
||||
);
|
||||
const { results, total: newTotal } = await response.json();
|
||||
total = newTotal;
|
||||
|
||||
const formattedResults = results.map((item) => ({
|
||||
...item,
|
||||
id: item.result_id,
|
||||
}));
|
||||
miniSearch.removeAll();
|
||||
miniSearch.addAll(formattedResults);
|
||||
return formattedResults;
|
||||
} catch (error) {
|
||||
console.error("Error fetching search data:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getSelectedTypes = () =>
|
||||
Array.from($typeCheckboxes)
|
||||
.filter((cb) => cb.checked)
|
||||
.map((cb) => cb.value);
|
||||
|
||||
const updateSearchResults = (results) => {
|
||||
if (currentPage === 1) {
|
||||
renderSearchResults(results);
|
||||
} else {
|
||||
appendSearchResults(results);
|
||||
}
|
||||
$loadMoreButton.style.display =
|
||||
currentPage * PAGE_SIZE < total ? "block" : "none";
|
||||
};
|
||||
|
||||
const appendSearchResults = (results) => {
|
||||
const newResultsHTML = results
|
||||
.map(
|
||||
({ title, url, description, type, total_plays }) => `
|
||||
<li class="search__results--result">
|
||||
<a href="${url}">
|
||||
<h3>${
|
||||
type === "artist" && total_plays
|
||||
? formatArtistTitle(title, total_plays)
|
||||
: title
|
||||
}</h3>
|
||||
</a>
|
||||
<p>${truncateDescription(description)}</p>
|
||||
</li>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
$results.insertAdjacentHTML("beforeend", newResultsHTML);
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
const query = $input.value.trim();
|
||||
if (!query) {
|
||||
renderSearchResults([]);
|
||||
$loadMoreButton.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await loadSearchIndex(query, getSelectedTypes(), 1);
|
||||
currentResults = results;
|
||||
currentPage = 1;
|
||||
updateSearchResults(results);
|
||||
};
|
||||
|
||||
$input.addEventListener("input", () => {
|
||||
clearTimeout(debounceTimeout);
|
||||
debounceTimeout = setTimeout(handleSearch, 300);
|
||||
});
|
||||
|
||||
$typeCheckboxes.forEach((cb) =>
|
||||
cb.addEventListener("change", handleSearch)
|
||||
);
|
||||
|
||||
$loadMoreButton.addEventListener("click", async () => {
|
||||
currentPage++;
|
||||
const nextResults = await loadSearchIndex(
|
||||
$input.value.trim(),
|
||||
getSelectedTypes(),
|
||||
currentPage
|
||||
);
|
||||
currentResults = [...currentResults, ...nextResults];
|
||||
updateSearchResults(nextResults);
|
||||
});
|
||||
})();
|
||||
|
||||
// pagination
|
||||
(() => {
|
||||
const dropdown = document.querySelector(".pagination select.client-side");
|
||||
|
||||
if (dropdown)
|
||||
dropdown.addEventListener("change", (event) => {
|
||||
const selectedOption = event.target.options[event.target.selectedIndex];
|
||||
const selectedHref = selectedOption.getAttribute("data-href");
|
||||
|
||||
if (selectedHref) window.location.href = selectedHref;
|
||||
});
|
||||
})();
|
||||
});
|
43
queries/functions/search.psql
Normal file
43
queries/functions/search.psql
Normal file
|
@ -0,0 +1,43 @@
|
|||
CREATE OR REPLACE FUNCTION public.search_optimized_index(search_query text, page_size integer, page_offset integer, types text[])
|
||||
RETURNS TABLE(
|
||||
result_id integer,
|
||||
url text,
|
||||
title text,
|
||||
description text,
|
||||
tags text,
|
||||
genre_name text,
|
||||
genre_url text,
|
||||
type text,
|
||||
total_plays text,
|
||||
rank real,
|
||||
total_count bigint
|
||||
)
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
s.id::integer AS result_id,
|
||||
s.url,
|
||||
s.title,
|
||||
s.description,
|
||||
array_to_string(s.tags, ', ') AS tags,
|
||||
s.genre_name,
|
||||
s.genre_url,
|
||||
s.type,
|
||||
s.total_plays,
|
||||
ts_rank_cd(to_tsvector('english', s.title || ' ' || s.description || array_to_string(s.tags, ' ')), plainto_tsquery('english', search_query)) AS rank,
|
||||
COUNT(*) OVER() AS total_count
|
||||
FROM
|
||||
optimized_search_index s
|
||||
WHERE(types IS NULL
|
||||
OR s.type = ANY(types))
|
||||
AND plainto_tsquery('english', search_query) @@ to_tsvector('english', s.title || ' ' || s.description || array_to_string(s.tags, ' '))
|
||||
ORDER BY
|
||||
s.type = 'post' DESC,
|
||||
s.content_date DESC NULLS LAST,
|
||||
rank DESC
|
||||
LIMIT page_size OFFSET page_offset;
|
||||
END;
|
||||
$$
|
||||
LANGUAGE plpgsql;
|
||||
|
26
queries/views/content/links.psql
Normal file
26
queries/views/content/links.psql
Normal file
|
@ -0,0 +1,26 @@
|
|||
CREATE OR REPLACE VIEW optimized_links AS
|
||||
SELECT
|
||||
l.id,
|
||||
l.title,
|
||||
l.date,
|
||||
l.description,
|
||||
l.link,
|
||||
a.mastodon,
|
||||
a.name,
|
||||
json_build_object('name', a.name, 'url', a.url, 'mastodon', a.mastodon) AS author,
|
||||
'link' AS type,
|
||||
(
|
||||
SELECT
|
||||
array_agg(t.name)
|
||||
FROM
|
||||
links_tags lt
|
||||
LEFT JOIN tags t ON lt.tags_id = t.id
|
||||
WHERE
|
||||
lt.links_id = l.id) AS tags,
|
||||
json_build_object('title', CONCAT(l.title, ' via ', a.name), 'url', l.link, 'description', l.description, 'date', l.date) AS feed
|
||||
FROM
|
||||
links l
|
||||
JOIN authors a ON l.author = a.id
|
||||
ORDER BY
|
||||
l.date DESC;
|
||||
|
126
queries/views/content/posts.psql
Normal file
126
queries/views/content/posts.psql
Normal file
|
@ -0,0 +1,126 @@
|
|||
CREATE OR REPLACE VIEW optimized_posts AS
|
||||
SELECT
|
||||
p.id,
|
||||
p.date,
|
||||
p.title,
|
||||
p.description,
|
||||
p.content,
|
||||
p.featured,
|
||||
p.slug AS url,
|
||||
p.mastodon_url,
|
||||
CASE WHEN df.filename_disk IS NOT NULL
|
||||
AND df.filename_disk != ''
|
||||
AND df.filename_disk != '/' THEN
|
||||
CONCAT('/', df.filename_disk)
|
||||
ELSE
|
||||
NULL
|
||||
END AS image,
|
||||
p.image_alt,
|
||||
CASE WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, p.date)) > 3 THEN
|
||||
TRUE
|
||||
ELSE
|
||||
FALSE
|
||||
END AS old_post,
|
||||
(
|
||||
SELECT
|
||||
json_agg(
|
||||
CASE WHEN pb.collection = 'youtube_player' THEN
|
||||
json_build_object('type', pb.collection, 'url', yp.url)
|
||||
WHEN pb.collection = 'github_banner' THEN
|
||||
json_build_object('type', pb.collection, 'url', gb.url)
|
||||
WHEN pb.collection = 'npm_banner' THEN
|
||||
json_build_object('type', pb.collection, 'url', nb.url, 'command', nb.command)
|
||||
WHEN pb.collection = 'rss_banner' THEN
|
||||
json_build_object('type', pb.collection, 'url', rb.url, 'text', rb.text)
|
||||
WHEN pb.collection = 'hero' THEN
|
||||
json_build_object('type', pb.collection, 'image', CONCAT('/', df_hero.filename_disk), 'alt_text', h.alt_text)
|
||||
WHEN pb.collection = 'markdown' THEN
|
||||
json_build_object('type', pb.collection, 'text', md.text)
|
||||
WHEN pb.collection = 'divider' THEN
|
||||
json_build_object('type', pb.collection, 'markup', d.markup)
|
||||
ELSE
|
||||
json_build_object('type', pb.collection)
|
||||
END)
|
||||
FROM
|
||||
posts_blocks pb
|
||||
LEFT JOIN youtube_player yp ON pb.collection = 'youtube_player'
|
||||
AND yp.id = pb.item::integer
|
||||
LEFT JOIN github_banner gb ON pb.collection = 'github_banner'
|
||||
AND gb.id = pb.item::integer
|
||||
LEFT JOIN npm_banner nb ON pb.collection = 'npm_banner'
|
||||
AND nb.id = pb.item::integer
|
||||
LEFT JOIN rss_banner rb ON pb.collection = 'rss_banner'
|
||||
AND rb.id = pb.item::integer
|
||||
LEFT JOIN hero h ON pb.collection = 'hero'
|
||||
AND h.id = pb.item::integer
|
||||
LEFT JOIN directus_files df_hero ON h.image = df_hero.id
|
||||
LEFT JOIN markdown md ON pb.collection = 'markdown'
|
||||
AND md.id = pb.item::integer
|
||||
LEFT JOIN divider d ON pb.collection = 'divider'
|
||||
AND d.id = pb.item::integer
|
||||
WHERE
|
||||
pb.posts_id = p.id) AS blocks,
|
||||
(
|
||||
SELECT
|
||||
array_agg(t.name)
|
||||
FROM
|
||||
posts_tags pt
|
||||
LEFT JOIN tags t ON pt.tags_id = t.id
|
||||
WHERE
|
||||
pt.posts_id = p.id) AS tags,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('name', g.name, 'url', g.slug))
|
||||
FROM
|
||||
posts_genres gp
|
||||
LEFT JOIN genres g ON gp.genres_id = g.id
|
||||
WHERE
|
||||
gp.posts_id = p.id) AS genres,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('name', a.name_string, 'url', a.slug, 'country', a.country, 'total_plays', a.total_plays))
|
||||
FROM
|
||||
posts_artists pa
|
||||
LEFT JOIN artists a ON pa.artists_id = a.id
|
||||
WHERE
|
||||
pa.posts_id = p.id) AS artists,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', b.title, 'author', b.author, 'url', b.slug)
|
||||
ORDER BY b.title)
|
||||
FROM
|
||||
posts_books pbk
|
||||
LEFT JOIN books b ON pbk.books_id = b.id
|
||||
WHERE
|
||||
pbk.posts_id = p.id) AS books,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', m.title, 'year', m.year, 'url', m.slug)
|
||||
ORDER BY m.year DESC)
|
||||
FROM
|
||||
posts_movies pm
|
||||
LEFT JOIN movies m ON pm.movies_id = m.id
|
||||
WHERE
|
||||
pm.posts_id = p.id) AS movies,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', s.title, 'year', s.year, 'url', s.slug))
|
||||
FROM
|
||||
posts_shows ps
|
||||
LEFT JOIN shows s ON ps.shows_id = s.id
|
||||
WHERE
|
||||
ps.posts_id = p.id) AS shows,
|
||||
json_build_object('title', p.title, 'url', CONCAT('https://coryd.dev', p.slug), 'description', p.content, 'date', p.date, 'image', CASE WHEN df.filename_disk IS NOT NULL
|
||||
AND df.filename_disk != ''
|
||||
AND df.filename_disk != '/' THEN
|
||||
CONCAT('/', df.filename_disk)
|
||||
ELSE
|
||||
NULL
|
||||
END) AS feed
|
||||
FROM
|
||||
posts p
|
||||
LEFT JOIN directus_files df ON p.image = df.id
|
||||
GROUP BY
|
||||
p.id,
|
||||
df.filename_disk;
|
||||
|
79
queries/views/feeds/all-content.psql
Normal file
79
queries/views/feeds/all-content.psql
Normal file
|
@ -0,0 +1,79 @@
|
|||
CREATE OR REPLACE VIEW optimized_all_activity AS
|
||||
WITH feed_data AS (
|
||||
SELECT
|
||||
p.date AS content_date,
|
||||
p.title,
|
||||
p.content AS description,
|
||||
CONCAT('https://coryd.dev', p.url) AS url,
|
||||
NULL AS image,
|
||||
NULL AS rating,
|
||||
p.tags,
|
||||
json_build_object('title', p.title, 'url', CONCAT('https://coryd.dev', p.url), 'description', p.content, 'date', p.date) AS feed
|
||||
FROM
|
||||
optimized_posts p
|
||||
UNION ALL
|
||||
SELECT
|
||||
l.date AS content_date,
|
||||
l.title,
|
||||
l.description,
|
||||
l.link AS url,
|
||||
NULL AS image,
|
||||
NULL AS rating,
|
||||
l.tags,
|
||||
json_build_object('title', CONCAT(l.title, ' via ', l.name), 'url', l.link, 'description', l.description, 'date', l.date) AS feed
|
||||
FROM
|
||||
optimized_links l
|
||||
UNION ALL
|
||||
SELECT
|
||||
b.date_finished AS content_date,
|
||||
b.title,
|
||||
b.description,
|
||||
CONCAT('https://coryd.dev', b.url) AS url,
|
||||
b.image,
|
||||
b.rating,
|
||||
b.tags,
|
||||
CASE WHEN LOWER(b.status) = 'finished' THEN
|
||||
json_build_object('title', b.title, 'url', CONCAT('https://coryd.dev', b.url), 'description', CASE WHEN b.review IS NOT NULL THEN
|
||||
b.review
|
||||
ELSE
|
||||
b.description
|
||||
END, 'image', b.image, 'rating', b.rating, 'date', b.date_finished)
|
||||
ELSE
|
||||
NULL
|
||||
END AS feed
|
||||
FROM
|
||||
optimized_books b
|
||||
UNION ALL
|
||||
SELECT
|
||||
m.last_watched AS content_date,
|
||||
m.title,
|
||||
m.description,
|
||||
CONCAT('https://coryd.dev', m.url) AS url,
|
||||
m.image,
|
||||
m.rating,
|
||||
m.tags,
|
||||
CASE WHEN m.last_watched IS NOT NULL THEN
|
||||
json_build_object('title', m.title, 'url', CONCAT('https://coryd.dev', m.url), 'description', CASE WHEN m.review IS NOT NULL THEN
|
||||
m.review
|
||||
ELSE
|
||||
m.description
|
||||
END, 'image', m.backdrop, 'rating', m.rating, 'date', m.last_watched)
|
||||
ELSE
|
||||
NULL
|
||||
END AS feed
|
||||
FROM
|
||||
optimized_movies m
|
||||
)
|
||||
SELECT
|
||||
json_agg(feed_data.* ORDER BY feed_data.content_date DESC) AS feed
|
||||
FROM (
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
feed_data
|
||||
WHERE
|
||||
feed IS NOT NULL
|
||||
ORDER BY
|
||||
content_date DESC
|
||||
LIMIT 20) AS feed_data;
|
||||
|
109
queries/views/feeds/search.psql
Normal file
109
queries/views/feeds/search.psql
Normal file
|
@ -0,0 +1,109 @@
|
|||
CREATE OR REPLACE VIEW optimized_search_index AS
|
||||
WITH search_data AS (
|
||||
SELECT
|
||||
'post' AS type,
|
||||
CONCAT('📝 ', p.title) AS title,
|
||||
CONCAT('https://coryd.dev', p.url) AS url,
|
||||
p.description AS description,
|
||||
p.tags,
|
||||
NULL AS genre_name,
|
||||
NULL AS genre_url,
|
||||
NULL::text AS total_plays,
|
||||
p.date AS content_date
|
||||
FROM
|
||||
optimized_posts p
|
||||
UNION ALL
|
||||
SELECT
|
||||
'link' AS type,
|
||||
CONCAT('🔗 ', l.title, ' via ', l.name) AS title,
|
||||
l.link AS url,
|
||||
l.description AS description,
|
||||
l.tags,
|
||||
NULL AS genre_name,
|
||||
NULL AS genre_url,
|
||||
NULL::text AS total_plays,
|
||||
l.date AS content_date
|
||||
FROM
|
||||
optimized_links l
|
||||
UNION ALL
|
||||
SELECT
|
||||
'book' AS type,
|
||||
CASE WHEN b.rating IS NOT NULL THEN
|
||||
CONCAT('📖 ', b.title, ' (', b.rating, ')')
|
||||
ELSE
|
||||
CONCAT('📖 ', b.title)
|
||||
END AS title,
|
||||
CONCAT('https://coryd.dev', b.url) AS url,
|
||||
b.description AS description,
|
||||
b.tags,
|
||||
NULL AS genre_name,
|
||||
NULL AS genre_url,
|
||||
NULL::text AS total_plays,
|
||||
b.date_finished AS content_date
|
||||
FROM
|
||||
optimized_books b
|
||||
WHERE
|
||||
LOWER(b.status) = 'finished'
|
||||
UNION ALL
|
||||
SELECT
|
||||
'artist' AS type,
|
||||
CONCAT(COALESCE(ar.emoji, ar.genre_emoji, '🎧'), ' ', ar.name) AS title,
|
||||
CONCAT('https://coryd.dev', ar.url) AS url,
|
||||
ar.description AS description,
|
||||
ARRAY[ar.genre_name] AS tags,
|
||||
ar.genre_name,
|
||||
CONCAT('https://coryd.dev', ar.genre_slug) AS genre_url,
|
||||
to_char(ar.total_plays::numeric, 'FM999,999,999,999') AS total_plays,
|
||||
NULL AS content_date
|
||||
FROM
|
||||
optimized_artists ar
|
||||
UNION ALL
|
||||
SELECT
|
||||
'genre' AS type,
|
||||
CONCAT(COALESCE(g.emoji, '🎵'), ' ', g.name) AS title,
|
||||
CONCAT('https://coryd.dev', g.url) AS url,
|
||||
g.description AS description,
|
||||
NULL AS tags,
|
||||
g.name AS genre_name,
|
||||
CONCAT('https://coryd.dev', g.url) AS genre_url,
|
||||
NULL::text AS total_plays,
|
||||
NULL AS content_date
|
||||
FROM
|
||||
optimized_genres g
|
||||
UNION ALL
|
||||
SELECT
|
||||
'show' AS type,
|
||||
CONCAT('📺 ', s.title, ' (', s.year, ')') AS title,
|
||||
CONCAT('https://coryd.dev', s.url) AS url,
|
||||
s.description AS description,
|
||||
s.tags,
|
||||
NULL AS genre_name,
|
||||
NULL AS genre_url,
|
||||
NULL::text AS total_plays,
|
||||
s.last_watched_at AS content_date
|
||||
FROM
|
||||
optimized_shows s
|
||||
WHERE
|
||||
s.last_watched_at IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT
|
||||
'movie' AS type,
|
||||
CONCAT('🎬 ', m.title, ' (', m.rating, ')') AS title,
|
||||
CONCAT('https://coryd.dev', m.url) AS url,
|
||||
m.description AS description,
|
||||
m.tags,
|
||||
NULL AS genre_name,
|
||||
NULL AS genre_url,
|
||||
NULL::text AS total_plays,
|
||||
m.last_watched AS content_date
|
||||
FROM
|
||||
optimized_movies m
|
||||
WHERE
|
||||
m.rating IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
ROW_NUMBER() OVER (ORDER BY url) AS id,
|
||||
*
|
||||
FROM
|
||||
search_data;
|
||||
|
65
queries/views/feeds/sitemap.psql
Normal file
65
queries/views/feeds/sitemap.psql
Normal file
|
@ -0,0 +1,65 @@
|
|||
CREATE OR REPLACE VIEW optimized_sitemap AS
|
||||
WITH sitemap_data AS (
|
||||
SELECT
|
||||
p.date AS content_date,
|
||||
p.title,
|
||||
CONCAT('https://coryd.dev', p.url) AS url,
|
||||
'monthly' AS changefreq,
|
||||
0.7 AS priority
|
||||
FROM
|
||||
optimized_posts p
|
||||
UNION ALL
|
||||
SELECT
|
||||
b.date_finished AS content_date,
|
||||
b.title,
|
||||
CONCAT('https://coryd.dev', b.url) AS url,
|
||||
'monthly' AS changefreq,
|
||||
0.5 AS priority
|
||||
FROM
|
||||
optimized_books b
|
||||
UNION ALL
|
||||
SELECT
|
||||
m.last_watched AS content_date,
|
||||
m.title,
|
||||
CONCAT('https://coryd.dev', m.url) AS url,
|
||||
'weekly' AS changefreq,
|
||||
0.6 AS priority
|
||||
FROM
|
||||
optimized_movies m
|
||||
UNION ALL
|
||||
SELECT
|
||||
NULL AS content_date,
|
||||
ar.name AS title,
|
||||
CONCAT('https://coryd.dev', ar.url) AS url,
|
||||
'monthly' AS changefreq,
|
||||
0.5 AS priority
|
||||
FROM
|
||||
optimized_artists ar
|
||||
UNION ALL
|
||||
SELECT
|
||||
NULL AS content_date,
|
||||
g.name AS title,
|
||||
CONCAT('https://coryd.dev', g.url) AS url,
|
||||
'yearly' AS changefreq,
|
||||
0.3 AS priority
|
||||
FROM
|
||||
optimized_genres g
|
||||
UNION ALL
|
||||
SELECT
|
||||
s.last_watched_at AS content_date,
|
||||
s.title,
|
||||
CONCAT('https://coryd.dev', s.url) AS url,
|
||||
'weekly' AS changefreq,
|
||||
0.8 AS priority
|
||||
FROM
|
||||
optimized_shows s
|
||||
)
|
||||
SELECT
|
||||
url,
|
||||
title,
|
||||
content_date AS lastmod,
|
||||
changefreq,
|
||||
priority
|
||||
FROM
|
||||
sitemap_data;
|
||||
|
86
queries/views/feeds/syndication.psql
Normal file
86
queries/views/feeds/syndication.psql
Normal file
|
@ -0,0 +1,86 @@
|
|||
CREATE OR REPLACE VIEW optimized_syndication AS
|
||||
WITH syndication_data AS (
|
||||
SELECT
|
||||
p.date AS content_date,
|
||||
p.title,
|
||||
p.description,
|
||||
CONCAT('https://coryd.dev', p.url) AS url,
|
||||
p.tags,
|
||||
json_build_object('title', CONCAT('📝 ', p.title, ' ',(
|
||||
SELECT
|
||||
array_to_string(array_agg('#' || initcap(replace(t.name, ' ', ''))), ' ')
|
||||
FROM unnest(p.tags) AS t(name))), 'description', p.description, 'url', CONCAT('https://coryd.dev', p.url), 'date', p.date) AS syndication
|
||||
FROM
|
||||
optimized_posts p
|
||||
UNION ALL
|
||||
SELECT
|
||||
l.date AS content_date,
|
||||
l.title,
|
||||
l.description,
|
||||
l.link AS url,
|
||||
l.tags,
|
||||
json_build_object('title', CONCAT('🔗 ', l.title, CASE WHEN l.mastodon IS NOT NULL THEN
|
||||
' via @' || split_part(l.mastodon, '@', 2) || '@' || split_part(split_part(l.mastodon, 'https://', 2), '/', 1)
|
||||
ELSE
|
||||
CONCAT(' via ', l.name)
|
||||
END, ' ',(
|
||||
SELECT
|
||||
array_to_string(array_agg('#' || initcap(replace(t.name, ' ', ''))), ' ')
|
||||
FROM unnest(l.tags) AS t(name))), 'description', l.description, 'url', l.link, 'date', l.date) AS syndication
|
||||
FROM
|
||||
optimized_links l
|
||||
UNION ALL
|
||||
SELECT
|
||||
b.date_finished AS content_date,
|
||||
b.title,
|
||||
b.description,
|
||||
CONCAT('https://coryd.dev', b.url) AS url,
|
||||
b.tags,
|
||||
CASE WHEN LOWER(b.status) = 'finished' THEN
|
||||
json_build_object('title', CONCAT('📖 ', b.title, CASE WHEN b.rating IS NOT NULL THEN
|
||||
' (' || b.rating || ')'
|
||||
ELSE
|
||||
''
|
||||
END, ' ',(
|
||||
SELECT
|
||||
array_to_string(array_agg('#' || initcap(replace(t.name, ' ', ''))), ' ')
|
||||
FROM unnest(b.tags) AS t(name))), 'description', b.description, 'url', CONCAT('https://coryd.dev', b.url), 'date', b.date_finished)
|
||||
ELSE
|
||||
NULL
|
||||
END AS syndication
|
||||
FROM
|
||||
optimized_books b
|
||||
UNION ALL
|
||||
SELECT
|
||||
m.last_watched AS content_date,
|
||||
m.title,
|
||||
m.description,
|
||||
CONCAT('https://coryd.dev', m.url) AS url,
|
||||
m.tags,
|
||||
CASE WHEN m.last_watched IS NOT NULL THEN
|
||||
json_build_object('title', CONCAT('🎥 ', m.title, CASE WHEN m.rating IS NOT NULL THEN
|
||||
' (' || m.rating || ')'
|
||||
ELSE
|
||||
''
|
||||
END, ' ',(
|
||||
SELECT
|
||||
array_to_string(array_agg('#' || initcap(replace(t.name, ' ', ''))), ' ')
|
||||
FROM unnest(m.tags) AS t(name))), 'description', m.description, 'url', CONCAT('https://coryd.dev', m.url), 'date', m.last_watched)
|
||||
ELSE
|
||||
NULL
|
||||
END AS syndication
|
||||
FROM
|
||||
optimized_movies m
|
||||
)
|
||||
SELECT
|
||||
json_agg(limited_data.*) AS syndication
|
||||
FROM (
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
syndication_data
|
||||
WHERE
|
||||
syndication IS NOT NULL
|
||||
ORDER BY
|
||||
content_date DESC
|
||||
LIMIT 20) AS limited_data;
|
22
queries/views/globals/index.psql
Normal file
22
queries/views/globals/index.psql
Normal file
|
@ -0,0 +1,22 @@
|
|||
CREATE OR REPLACE VIEW optimized_globals AS
|
||||
SELECT
|
||||
g.site_name,
|
||||
g.site_description,
|
||||
g.intro,
|
||||
g.author,
|
||||
g.email,
|
||||
g.mastodon,
|
||||
g.url,
|
||||
g.cdn_url,
|
||||
g.theme_color,
|
||||
g.site_type,
|
||||
g.locale,
|
||||
g.lang,
|
||||
g.webfinger_username,
|
||||
g.webfinger_hostname,
|
||||
CONCAT('/', df.filename_disk) AS avatar,
|
||||
CONCAT('/', df2.filename_disk) AS avatar_transparent
|
||||
FROM
|
||||
globals g
|
||||
LEFT JOIN directus_files df ON g.avatar = df.id
|
||||
LEFT JOIN directus_files df2 ON g.avatar_transparent = df2.id
|
14
queries/views/globals/nav.psql
Normal file
14
queries/views/globals/nav.psql
Normal file
|
@ -0,0 +1,14 @@
|
|||
CREATE OR REPLACE VIEW optimized_navigation AS
|
||||
SELECT
|
||||
n.id,
|
||||
n.menu_location,
|
||||
n.permalink,
|
||||
n.icon,
|
||||
n.title,
|
||||
n.sort,
|
||||
p.title AS page_title,
|
||||
p.permalink AS page_permalink
|
||||
FROM
|
||||
navigation n
|
||||
LEFT JOIN pages p ON n.pages = p.id;
|
||||
|
56
queries/views/globals/pages.psql
Normal file
56
queries/views/globals/pages.psql
Normal file
|
@ -0,0 +1,56 @@
|
|||
CREATE OR REPLACE VIEW optimized_pages AS
|
||||
SELECT
|
||||
p.id,
|
||||
p.title,
|
||||
p.permalink,
|
||||
p.description,
|
||||
CONCAT('/', df.filename_disk) AS open_graph_image,
|
||||
p.updated,
|
||||
(
|
||||
SELECT
|
||||
json_agg(
|
||||
CASE WHEN pb.collection = 'youtube_player' THEN
|
||||
json_build_object('type', pb.collection, 'url', yp.url)
|
||||
WHEN pb.collection = 'github_banner' THEN
|
||||
json_build_object('type', pb.collection, 'url', gb.url)
|
||||
WHEN pb.collection = 'npm_banner' THEN
|
||||
json_build_object('type', pb.collection, 'url', nb.url, 'command', nb.command)
|
||||
WHEN pb.collection = 'rss_banner' THEN
|
||||
json_build_object('type', pb.collection, 'url', rb.url, 'text', rb.text)
|
||||
WHEN pb.collection = 'hero' THEN
|
||||
json_build_object('type', pb.collection, 'image', CONCAT('/', df_hero.filename_disk), 'alt', h.alt_text)
|
||||
WHEN pb.collection = 'markdown' THEN
|
||||
json_build_object('type', pb.collection, 'text', md.text)
|
||||
WHEN pb.collection = 'divider' THEN
|
||||
json_build_object('type', pb.collection, 'markup', d.markup)
|
||||
WHEN pb.collection = 'addon_links' THEN
|
||||
json_build_object('type', pb.collection, 'addon_links', d.title)
|
||||
ELSE
|
||||
json_build_object('type', pb.collection)
|
||||
END ORDER BY pb.sort)
|
||||
FROM
|
||||
pages_blocks pb
|
||||
LEFT JOIN youtube_player yp ON pb.collection = 'youtube_player'
|
||||
AND yp.id = pb.item::integer
|
||||
LEFT JOIN github_banner gb ON pb.collection = 'github_banner'
|
||||
AND gb.id = pb.item::integer
|
||||
LEFT JOIN npm_banner nb ON pb.collection = 'npm_banner'
|
||||
AND nb.id = pb.item::integer
|
||||
LEFT JOIN rss_banner rb ON pb.collection = 'rss_banner'
|
||||
AND rb.id = pb.item::integer
|
||||
LEFT JOIN hero h ON pb.collection = 'hero'
|
||||
AND h.id = pb.item::integer
|
||||
LEFT JOIN directus_files df_hero ON h.image = df_hero.id
|
||||
LEFT JOIN markdown md ON pb.collection = 'markdown'
|
||||
AND md.id = pb.item::integer
|
||||
LEFT JOIN divider d ON pb.collection = 'divider'
|
||||
AND d.id = pb.item::integer
|
||||
WHERE
|
||||
pb.pages_id = p.id) AS blocks
|
||||
FROM
|
||||
pages p
|
||||
LEFT JOIN directus_files df ON p.open_graph_image = df.id
|
||||
GROUP BY
|
||||
p.id,
|
||||
df.filename_disk;
|
||||
|
95
queries/views/media/books.psql
Normal file
95
queries/views/media/books.psql
Normal file
|
@ -0,0 +1,95 @@
|
|||
CREATE OR REPLACE VIEW optimized_books AS
|
||||
SELECT
|
||||
b.date_finished,
|
||||
EXTRACT(YEAR FROM b.date_finished) AS year,
|
||||
b.author,
|
||||
b.description,
|
||||
b.title,
|
||||
b.progress,
|
||||
b.read_status AS status,
|
||||
b.star_rating AS rating,
|
||||
b.review,
|
||||
b.slug AS url,
|
||||
CONCAT('/', df.filename_disk) AS image,
|
||||
b.favorite,
|
||||
b.tattoo,
|
||||
(
|
||||
SELECT
|
||||
array_agg(t.name)
|
||||
FROM
|
||||
books_tags bt
|
||||
LEFT JOIN tags t ON bt.tags_id = t.id
|
||||
WHERE
|
||||
bt.books_id = b.id) AS tags,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('name', a.name_string, 'url', a.slug, 'country', a.country, 'total_plays', a.total_plays))
|
||||
FROM
|
||||
books_artists ba
|
||||
LEFT JOIN artists a ON ba.artists_id = a.id
|
||||
WHERE
|
||||
ba.books_id = b.id) AS artists,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', m.title, 'year', m.year, 'url', m.slug))
|
||||
FROM
|
||||
movies_books mb
|
||||
LEFT JOIN movies m ON mb.movies_id = m.id
|
||||
WHERE
|
||||
mb.books_id = b.id) AS movies,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('name', g.name, 'url', g.slug))
|
||||
FROM
|
||||
genres_books gb
|
||||
LEFT JOIN genres g ON gb.genres_id = g.id
|
||||
WHERE
|
||||
gb.books_id = b.id) AS genres,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', s.title, 'year', s.year, 'url', s.slug))
|
||||
FROM
|
||||
shows_books sb
|
||||
LEFT JOIN shows s ON sb.shows_id = s.id
|
||||
WHERE
|
||||
sb.books_id = b.id) AS shows,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', p.title, 'date', p.date, 'url', p.slug)
|
||||
ORDER BY p.date DESC)
|
||||
FROM
|
||||
posts_books pb
|
||||
LEFT JOIN posts p ON pb.posts_id = p.id
|
||||
WHERE
|
||||
pb.books_id = b.id) AS posts,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', rb.title, 'author', rb.author, 'url', rb.slug)
|
||||
ORDER BY rb.title)
|
||||
FROM
|
||||
related_books rbk
|
||||
LEFT JOIN books rb ON rbk.related_books_id = rb.id
|
||||
WHERE
|
||||
rbk.books_id = b.id) AS related_books,
|
||||
json_build_object('title', CONCAT(b.title, ' by ', b.author), 'image', CONCAT('/', df.filename_disk), 'url', b.slug, 'alt', CONCAT('Book cover from ', b.title, ' by ', b.author), 'subtext', CASE WHEN b.star_rating IS NOT NULL THEN
|
||||
b.star_rating
|
||||
ELSE
|
||||
NULL
|
||||
END) AS grid,
|
||||
CASE WHEN LOWER(b.read_status) = 'finished'
|
||||
AND b.star_rating IS NOT NULL THEN
|
||||
json_build_object('title', b.title, 'url', CONCAT('https://coryd.dev', b.slug), 'date', b.date_finished, 'description', CASE WHEN b.review IS NOT NULL THEN
|
||||
b.review
|
||||
ELSE
|
||||
b.description
|
||||
END, 'image', CONCAT('/', df.filename_disk), 'rating', b.star_rating)
|
||||
ELSE
|
||||
NULL
|
||||
END AS feed
|
||||
FROM
|
||||
books b
|
||||
LEFT JOIN directus_files df ON b.art = df.id
|
||||
GROUP BY
|
||||
b.id,
|
||||
df.filename_disk;
|
||||
|
106
queries/views/media/movies.psql
Normal file
106
queries/views/media/movies.psql
Normal file
|
@ -0,0 +1,106 @@
|
|||
CREATE OR REPLACE VIEW optimized_movies AS
|
||||
SELECT
|
||||
m.id,
|
||||
m.last_watched,
|
||||
m.title,
|
||||
m.year,
|
||||
m.collected,
|
||||
m.plays,
|
||||
m.favorite,
|
||||
m.tattoo,
|
||||
m.star_rating AS rating,
|
||||
m.description,
|
||||
m.review,
|
||||
m.slug AS url,
|
||||
CONCAT('/', df.filename_disk) AS image,
|
||||
CONCAT('/', df2.filename_disk) AS backdrop,
|
||||
json_build_object('title', m.title, 'url', m.slug, 'image', CONCAT('/', df.filename_disk), 'backdrop', CONCAT('/', df2.filename_disk), 'alt', CONCAT('Poster from ', m.title, ' (', m.year, ')'), 'subtext', CASE WHEN m.star_rating IS NOT NULL THEN
|
||||
CONCAT(m.star_rating, ' (', m.year, ')')
|
||||
ELSE
|
||||
CONCAT('(', m.year, ')')
|
||||
END) AS grid,
|
||||
(
|
||||
SELECT
|
||||
array_agg(t.name)
|
||||
FROM
|
||||
movies_tags mt
|
||||
LEFT JOIN tags t ON mt.tags_id = t.id
|
||||
WHERE
|
||||
mt.movies_id = m.id) AS tags,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('name', g.name, 'url', g.slug)
|
||||
ORDER BY g.name ASC)
|
||||
FROM
|
||||
genres_movies gm
|
||||
LEFT JOIN genres g ON gm.genres_id = g.id
|
||||
WHERE
|
||||
gm.movies_id = m.id) AS genres,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('name', a.name_string, 'url', a.slug, 'country', a.country, 'total_plays', a.total_plays)
|
||||
ORDER BY a.name_string ASC)
|
||||
FROM
|
||||
movies_artists ma
|
||||
LEFT JOIN artists a ON ma.artists_id = a.id
|
||||
WHERE
|
||||
ma.movies_id = m.id) AS artists,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', b.title, 'author', b.author, 'url', b.slug)
|
||||
ORDER BY b.title ASC)
|
||||
FROM
|
||||
movies_books mb
|
||||
LEFT JOIN books b ON mb.books_id = b.id
|
||||
WHERE
|
||||
mb.movies_id = m.id) AS books,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', s.title, 'year', s.year, 'url', s.slug)
|
||||
ORDER BY s.year DESC)
|
||||
FROM
|
||||
shows_movies sm
|
||||
LEFT JOIN shows s ON sm.shows_id = s.id
|
||||
WHERE
|
||||
sm.movies_id = m.id) AS shows,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', p.title, 'date', p.date, 'url', p.slug)
|
||||
ORDER BY p.date DESC)
|
||||
FROM
|
||||
posts_movies pm
|
||||
LEFT JOIN posts p ON pm.posts_id = p.id
|
||||
WHERE
|
||||
pm.movies_id = m.id) AS posts,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', rm.title, 'year', rm.year, 'url', rm.slug)
|
||||
ORDER BY rm.year DESC)
|
||||
FROM
|
||||
related_movies r
|
||||
LEFT JOIN movies rm ON r.related_movies_id = rm.id
|
||||
WHERE
|
||||
r.movies_id = m.id) AS related_movies,
|
||||
CASE WHEN m.star_rating IS NOT NULL
|
||||
AND m.last_watched IS NOT NULL THEN
|
||||
json_build_object('title', m.title, 'url', CONCAT('https://coryd.dev', m.slug), 'date', m.last_watched, 'description', CASE WHEN m.review IS NOT NULL THEN
|
||||
m.review
|
||||
ELSE
|
||||
m.description
|
||||
END, 'image', CONCAT('/', df2.filename_disk), 'rating', m.star_rating)
|
||||
ELSE
|
||||
NULL
|
||||
END AS feed
|
||||
FROM
|
||||
movies m
|
||||
LEFT JOIN directus_files df ON m.art = df.id
|
||||
LEFT JOIN directus_files df2 ON m.backdrop = df2.id
|
||||
GROUP BY
|
||||
m.id,
|
||||
df.filename_disk,
|
||||
df2.filename_disk
|
||||
ORDER BY
|
||||
m.last_watched DESC,
|
||||
m.favorite DESC,
|
||||
m.title ASC;
|
||||
|
17
queries/views/media/music/album-releases.psql
Normal file
17
queries/views/media/music/album-releases.psql
Normal file
|
@ -0,0 +1,17 @@
|
|||
CREATE OR REPLACE VIEW optimized_album_releases AS
|
||||
SELECT
|
||||
a.name AS title,
|
||||
a.release_date,
|
||||
a.release_link AS url,
|
||||
a.total_plays,
|
||||
CONCAT('/', df.filename_disk) AS image,
|
||||
json_build_object('name', ar.name_string, 'url', ar.slug, 'description', ar.description) AS artist,
|
||||
EXTRACT(EPOCH FROM a.release_date) AS release_timestamp,
|
||||
json_build_object('title', a.name, 'image', CONCAT('/', df.filename_disk), 'url', a.release_link, 'alt', CONCAT(a.name, ' by ', ar.name_string), 'subtext', CONCAT(ar.name_string, ' / ', TO_CHAR(a.release_date, 'Mon FMDD, YYYY'))) AS grid
|
||||
FROM
|
||||
albums a
|
||||
LEFT JOIN directus_files df ON a.art = df.id
|
||||
LEFT JOIN artists ar ON a.artist = ar.id
|
||||
WHERE
|
||||
a.release_date IS NOT NULL;
|
||||
|
94
queries/views/media/music/artists.psql
Normal file
94
queries/views/media/music/artists.psql
Normal file
|
@ -0,0 +1,94 @@
|
|||
CREATE OR REPLACE VIEW optimized_artists AS
|
||||
SELECT
|
||||
ar.name_string AS name,
|
||||
ar.slug AS url,
|
||||
ar.tentative,
|
||||
to_char(ar.total_plays, 'FM999,999,999,999') AS total_plays, -- Format total_plays with commas
|
||||
ar.country,
|
||||
ar.description,
|
||||
ar.favorite,
|
||||
g.name AS genre_name,
|
||||
g.slug AS genre_slug,
|
||||
g.emoji AS genre_emoji,
|
||||
json_build_object('name', g.name, 'url', g.slug, 'emoji', g.emoji) AS genre,
|
||||
ar.emoji,
|
||||
ar.tattoo,
|
||||
CONCAT('/', df.filename_disk) AS image,
|
||||
json_build_object('alt', CONCAT(to_char(ar.total_plays, 'FM999,999,999,999'), ' plays of ', ar.name_string), -- Format total_plays in alt text
|
||||
'subtext', CONCAT(to_char(ar.total_plays, 'FM999,999,999,999'), ' plays') -- Format total_plays in subtext
|
||||
) AS grid,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('name', a.name, 'release_year', a.release_year, 'total_plays', to_char(a.total_plays, 'FM999,999,999,999'), -- Format total_plays here as well
|
||||
'art', df_album.filename_disk)
|
||||
ORDER BY a.release_year)
|
||||
FROM
|
||||
albums a
|
||||
LEFT JOIN directus_files df_album ON a.art = df_album.id
|
||||
WHERE
|
||||
a.artist = ar.id) AS albums,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('id', c.id, 'date', c.date, 'venue_name', v.name, 'venue_name_short', trim(split_part(v.name, ',', 1)), 'venue_latitude', v.latitude, 'venue_longitude', v.longitude, 'notes', c.notes)
|
||||
ORDER BY c.date DESC)
|
||||
FROM
|
||||
concerts c
|
||||
LEFT JOIN venues v ON c.venue = v.id
|
||||
WHERE
|
||||
c.artist = ar.id) AS concerts,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', b.title, 'author', b.author, 'url', b.slug)
|
||||
ORDER BY b.date_finished DESC)
|
||||
FROM
|
||||
books_artists ba
|
||||
LEFT JOIN books b ON ba.books_id = b.id
|
||||
WHERE
|
||||
ba.artists_id = ar.id) AS books,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', m.title, 'year', m.year, 'url', m.slug)
|
||||
ORDER BY m.year DESC)
|
||||
FROM
|
||||
movies_artists ma
|
||||
LEFT JOIN movies m ON ma.movies_id = m.id
|
||||
WHERE
|
||||
ma.artists_id = ar.id) AS movies,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', s.title, 'year', s.year, 'url', s.slug)
|
||||
ORDER BY s.year DESC)
|
||||
FROM
|
||||
shows_artists sa
|
||||
LEFT JOIN shows s ON sa.shows_id = s.id
|
||||
WHERE
|
||||
sa.artists_id = ar.id) AS shows,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', p.title, 'date', p.date, 'url', p.slug)
|
||||
ORDER BY p.date DESC)
|
||||
FROM
|
||||
posts_artists pa
|
||||
LEFT JOIN posts p ON pa.posts_id = p.id
|
||||
WHERE
|
||||
pa.artists_id = ar.id) AS posts,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('name', related_ar.name_string, 'url', related_ar.slug, 'country', related_ar.country, 'total_plays', to_char(related_ar.total_plays, 'FM999,999,999,999'))
|
||||
ORDER BY related_ar.name_string)
|
||||
FROM
|
||||
related_artists ra
|
||||
LEFT JOIN artists related_ar ON ra.related_artists_id = related_ar.id
|
||||
WHERE
|
||||
ra.artists_id = ar.id) AS related_artists
|
||||
FROM
|
||||
artists ar
|
||||
LEFT JOIN directus_files df ON ar.art = df.id
|
||||
LEFT JOIN genres g ON ar.genres = g.id
|
||||
GROUP BY
|
||||
ar.id,
|
||||
df.filename_disk,
|
||||
g.name,
|
||||
g.slug,
|
||||
g.emoji;
|
||||
|
19
queries/views/media/music/concerts.psql
Normal file
19
queries/views/media/music/concerts.psql
Normal file
|
@ -0,0 +1,19 @@
|
|||
CREATE OR REPLACE VIEW optimized_concerts AS
|
||||
SELECT
|
||||
c.id,
|
||||
c.date,
|
||||
c.notes,
|
||||
CASE WHEN c.artist IS NOT NULL THEN
|
||||
json_build_object('name', a.name_string, 'url', a.slug)
|
||||
ELSE
|
||||
json_build_object('name', c.artist_name_string, 'url', NULL)
|
||||
END AS artist,
|
||||
json_build_object('name', v.name, 'name_short', trim(split_part(v.name, ',', 1)), 'latitude', v.latitude, 'longitude', v.longitude, 'notes', v.notes) AS venue,
|
||||
c.notes AS concert_notes
|
||||
FROM
|
||||
concerts c
|
||||
LEFT JOIN artists a ON c.artist = a.id
|
||||
LEFT JOIN venues v ON c.venue = v.id
|
||||
ORDER BY
|
||||
c.date DESC;
|
||||
|
49
queries/views/media/music/genres.psql
Normal file
49
queries/views/media/music/genres.psql
Normal file
|
@ -0,0 +1,49 @@
|
|||
CREATE OR REPLACE VIEW optimized_genres AS
|
||||
SELECT
|
||||
g.id,
|
||||
g.name,
|
||||
g.description,
|
||||
g.emoji,
|
||||
to_char(g.total_plays, 'FM999,999,999,999') AS total_plays,
|
||||
g.wiki_link,
|
||||
g.slug AS url,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('name', a.name_string, 'url', a.slug, 'image', CONCAT('/', df_artist.filename_disk), 'total_plays', to_char(a.total_plays, 'FM999,999,999,999'))
|
||||
ORDER BY a.total_plays DESC)
|
||||
FROM
|
||||
artists a
|
||||
LEFT JOIN directus_files df_artist ON a.art = df_artist.id
|
||||
WHERE
|
||||
a.genres = g.id) AS artists,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', b.title, 'author', b.author, 'url', b.slug))
|
||||
FROM
|
||||
books b
|
||||
JOIN genres_books gb ON gb.books_id = b.id
|
||||
WHERE
|
||||
gb.genres_id = g.id) AS books,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', m.title, 'year', m.year, 'url', m.slug)
|
||||
ORDER BY m.year DESC)
|
||||
FROM
|
||||
movies m
|
||||
JOIN genres_movies gm ON gm.movies_id = m.id
|
||||
WHERE
|
||||
gm.genres_id = g.id) AS movies,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', p.title, 'date', p.date, 'url', p.slug)
|
||||
ORDER BY p.date DESC)
|
||||
FROM
|
||||
posts_genres pg
|
||||
LEFT JOIN posts p ON pg.posts_id = p.id
|
||||
WHERE
|
||||
pg.genres_id = g.id) AS posts
|
||||
FROM
|
||||
genres g
|
||||
ORDER BY
|
||||
g.id ASC;
|
||||
|
28
queries/views/media/music/listens.psql
Normal file
28
queries/views/media/music/listens.psql
Normal file
|
@ -0,0 +1,28 @@
|
|||
CREATE OR REPLACE VIEW optimized_listens AS SELECT DISTINCT ON (l.id, l.listened_at, l.track_name, l.artist_name, l.album_name)
|
||||
l.id,
|
||||
l.listened_at,
|
||||
l.track_name,
|
||||
l.artist_name,
|
||||
l.album_name,
|
||||
l.album_key,
|
||||
CONCAT('/', df_art.filename_disk) AS artist_art,
|
||||
a.genres AS artist_genres,
|
||||
g.name AS genre_name,
|
||||
g.slug AS genre_url,
|
||||
a.country AS artist_country,
|
||||
a.slug AS artist_url,
|
||||
CONCAT('/', df_album.filename_disk) AS album_art
|
||||
FROM
|
||||
listens l
|
||||
LEFT JOIN artists a ON (l.artist_name = a.name_string)
|
||||
LEFT JOIN albums al ON (l.album_key = al.key)
|
||||
LEFT JOIN directus_files df_art ON (a.art = df_art.id)
|
||||
LEFT JOIN directus_files df_album ON (al.art = df_album.id)
|
||||
LEFT JOIN genres g ON (a.genres = g.id)
|
||||
ORDER BY
|
||||
l.id,
|
||||
l.listened_at,
|
||||
l.track_name,
|
||||
l.artist_name,
|
||||
l.album_name;
|
||||
|
20
queries/views/media/music/month/albums.psql
Normal file
20
queries/views/media/music/month/albums.psql
Normal file
|
@ -0,0 +1,20 @@
|
|||
CREATE OR REPLACE VIEW month_albums AS
|
||||
SELECT
|
||||
ol.album_name,
|
||||
ol.artist_name,
|
||||
COUNT(*) AS plays,
|
||||
ol.album_art,
|
||||
ol.artist_url,
|
||||
json_build_object('title', ol.album_name, 'image', ol.album_art, 'url', ol.artist_url, 'alt', CONCAT(ol.album_name, ' by ', ol.artist_name), 'subtext', ol.artist_name) AS grid
|
||||
FROM
|
||||
optimized_listens ol
|
||||
WHERE
|
||||
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '30 days'
|
||||
GROUP BY
|
||||
ol.album_name,
|
||||
ol.artist_name,
|
||||
ol.album_art,
|
||||
ol.artist_url
|
||||
ORDER BY
|
||||
plays DESC;
|
||||
|
19
queries/views/media/music/month/artists.psql
Normal file
19
queries/views/media/music/month/artists.psql
Normal file
|
@ -0,0 +1,19 @@
|
|||
CREATE OR REPLACE VIEW month_artists AS
|
||||
SELECT
|
||||
ol.artist_name,
|
||||
COUNT(*) AS plays,
|
||||
ol.artist_art,
|
||||
ol.artist_url,
|
||||
ARRAY_AGG(DISTINCT ol.genre_name) AS genres,
|
||||
json_build_object('title', ol.artist_name, 'image', ol.artist_art, 'url', ol.artist_url, 'alt', CONCAT(COUNT(*), ' plays of ', ol.artist_name), 'subtext', CONCAT(COUNT(*), ' plays')) AS grid
|
||||
FROM
|
||||
optimized_listens ol
|
||||
WHERE
|
||||
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '30 days'
|
||||
GROUP BY
|
||||
ol.artist_name,
|
||||
ol.artist_art,
|
||||
ol.artist_url
|
||||
ORDER BY
|
||||
plays DESC;
|
||||
|
16
queries/views/media/music/month/genres.psql
Normal file
16
queries/views/media/music/month/genres.psql
Normal file
|
@ -0,0 +1,16 @@
|
|||
CREATE OR REPLACE VIEW month_genres AS
|
||||
SELECT
|
||||
ol.genre_name,
|
||||
ol.genre_url,
|
||||
COUNT(*) AS plays,
|
||||
json_build_object('alt', ol.genre_name, 'subtext', CONCAT(COUNT(*), ' plays')) AS grid
|
||||
FROM
|
||||
optimized_listens ol
|
||||
WHERE
|
||||
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '30 days'
|
||||
GROUP BY
|
||||
ol.genre_name,
|
||||
ol.genre_url
|
||||
ORDER BY
|
||||
plays DESC;
|
||||
|
37
queries/views/media/music/month/tracks.psql
Normal file
37
queries/views/media/music/month/tracks.psql
Normal file
|
@ -0,0 +1,37 @@
|
|||
CREATE OR REPLACE VIEW month_tracks AS
|
||||
WITH track_stats AS (
|
||||
SELECT
|
||||
ol.track_name,
|
||||
ol.artist_name,
|
||||
ol.album_name,
|
||||
COUNT(*) AS plays,
|
||||
MAX(ol.listened_at) AS last_listened,
|
||||
ol.album_art,
|
||||
ol.artist_url,
|
||||
MAX(COUNT(*)) OVER () AS most_played
|
||||
FROM
|
||||
optimized_listens ol
|
||||
WHERE
|
||||
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '30 days'
|
||||
GROUP BY
|
||||
ol.track_name,
|
||||
ol.artist_name,
|
||||
ol.album_name,
|
||||
ol.album_art,
|
||||
ol.artist_url
|
||||
)
|
||||
SELECT
|
||||
track_name,
|
||||
artist_name,
|
||||
album_name,
|
||||
plays,
|
||||
last_listened,
|
||||
album_art,
|
||||
artist_url,
|
||||
json_build_object('title', track_name, 'artist', artist_name, 'url', artist_url, 'plays', plays, 'alt', CONCAT(track_name, ' by ', artist_name), 'subtext', CONCAT(album_name, ' (', plays, ' plays)'), 'percentage', ROUND((plays::decimal / most_played) * 100, 2)) AS chart
|
||||
FROM
|
||||
track_stats
|
||||
ORDER BY
|
||||
plays DESC,
|
||||
last_listened DESC;
|
||||
|
25
queries/views/media/music/now-playing.psql
Normal file
25
queries/views/media/music/now-playing.psql
Normal file
|
@ -0,0 +1,25 @@
|
|||
CREATE VIEW optimized_latest_listen AS
|
||||
WITH latest_listen AS (
|
||||
SELECT
|
||||
l.track_name,
|
||||
l.artist_name,
|
||||
a.emoji AS artist_emoji,
|
||||
g.emoji AS genre_emoji,
|
||||
a.slug AS url,
|
||||
ROW_NUMBER() OVER (ORDER BY l.listened_at DESC) AS row_num
|
||||
FROM
|
||||
listens l
|
||||
JOIN artists a ON l.artist_name = a.name_string
|
||||
LEFT JOIN genres g ON a.genres = g.id
|
||||
)
|
||||
SELECT
|
||||
track_name,
|
||||
artist_name,
|
||||
artist_emoji,
|
||||
genre_emoji,
|
||||
url
|
||||
FROM
|
||||
latest_listen
|
||||
WHERE
|
||||
row_num = 1;
|
||||
|
23
queries/views/media/music/recent-tracks.psql
Normal file
23
queries/views/media/music/recent-tracks.psql
Normal file
|
@ -0,0 +1,23 @@
|
|||
CREATE OR REPLACE VIEW recent_tracks AS
|
||||
SELECT
|
||||
ol.id,
|
||||
ol.listened_at,
|
||||
ol.track_name,
|
||||
ol.artist_name,
|
||||
ol.album_name,
|
||||
ol.album_key,
|
||||
ol.artist_art,
|
||||
ol.artist_genres,
|
||||
ol.genre_name,
|
||||
ol.artist_country,
|
||||
ol.album_art,
|
||||
ol.artist_url,
|
||||
ol.genre_url,
|
||||
json_build_object('title', ol.track_name, 'subtext', ol.artist_name, 'alt', CONCAT(ol.track_name, ' by ', ol.artist_name), 'url', ol.artist_url, 'image', ol.album_art, 'played_at', ol.listened_at) AS chart
|
||||
FROM
|
||||
optimized_listens ol
|
||||
WHERE
|
||||
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '7 days'
|
||||
ORDER BY
|
||||
TO_TIMESTAMP(ol.listened_at) DESC;
|
||||
|
20
queries/views/media/music/week/albums.psql
Normal file
20
queries/views/media/music/week/albums.psql
Normal file
|
@ -0,0 +1,20 @@
|
|||
CREATE OR REPLACE VIEW week_albums AS
|
||||
SELECT
|
||||
ol.album_name,
|
||||
ol.artist_name,
|
||||
COUNT(*) AS plays,
|
||||
ol.album_art,
|
||||
ol.artist_url,
|
||||
json_build_object('title', ol.album_name, 'image', ol.album_art, 'url', ol.artist_url, 'alt', CONCAT(ol.album_name, ' by ', ol.artist_name), 'subtext', ol.artist_name) AS grid
|
||||
FROM
|
||||
optimized_listens ol
|
||||
WHERE
|
||||
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '7 days'
|
||||
GROUP BY
|
||||
ol.album_name,
|
||||
ol.artist_name,
|
||||
ol.album_art,
|
||||
ol.artist_url
|
||||
ORDER BY
|
||||
plays DESC;
|
||||
|
19
queries/views/media/music/week/artists.psql
Normal file
19
queries/views/media/music/week/artists.psql
Normal file
|
@ -0,0 +1,19 @@
|
|||
CREATE OR REPLACE VIEW week_artists AS
|
||||
SELECT
|
||||
ol.artist_name,
|
||||
COUNT(*) AS plays,
|
||||
ol.artist_art,
|
||||
ol.artist_url,
|
||||
ARRAY_AGG(DISTINCT ol.genre_name) AS genres,
|
||||
json_build_object('title', ol.artist_name, 'image', ol.artist_art, 'url', ol.artist_url, 'alt', CONCAT(COUNT(*), ' plays of ', ol.artist_name), 'subtext', CONCAT(COUNT(*), ' plays')) AS grid
|
||||
FROM
|
||||
optimized_listens ol
|
||||
WHERE
|
||||
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '7 days'
|
||||
GROUP BY
|
||||
ol.artist_name,
|
||||
ol.artist_art,
|
||||
ol.artist_url
|
||||
ORDER BY
|
||||
plays DESC;
|
||||
|
16
queries/views/media/music/week/genres.psql
Normal file
16
queries/views/media/music/week/genres.psql
Normal file
|
@ -0,0 +1,16 @@
|
|||
CREATE OR REPLACE VIEW week_genres AS
|
||||
SELECT
|
||||
ol.genre_name,
|
||||
ol.genre_url,
|
||||
COUNT(*) AS plays,
|
||||
json_build_object('alt', ol.genre_name, 'subtext', CONCAT(COUNT(*), ' plays')) AS grid
|
||||
FROM
|
||||
optimized_listens ol
|
||||
WHERE
|
||||
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '7 days'
|
||||
GROUP BY
|
||||
ol.genre_name,
|
||||
ol.genre_url
|
||||
ORDER BY
|
||||
plays DESC;
|
||||
|
37
queries/views/media/music/week/tracks.psql
Normal file
37
queries/views/media/music/week/tracks.psql
Normal file
|
@ -0,0 +1,37 @@
|
|||
CREATE OR REPLACE VIEW week_tracks AS
|
||||
WITH track_stats AS (
|
||||
SELECT
|
||||
ol.track_name,
|
||||
ol.artist_name,
|
||||
ol.album_name,
|
||||
COUNT(*) AS plays,
|
||||
MAX(ol.listened_at) AS last_listened,
|
||||
ol.album_art,
|
||||
ol.artist_url,
|
||||
MAX(COUNT(*)) OVER () AS most_played
|
||||
FROM
|
||||
optimized_listens ol
|
||||
WHERE
|
||||
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '7 days'
|
||||
GROUP BY
|
||||
ol.track_name,
|
||||
ol.artist_name,
|
||||
ol.album_name,
|
||||
ol.album_art,
|
||||
ol.artist_url
|
||||
)
|
||||
SELECT
|
||||
track_name,
|
||||
artist_name,
|
||||
album_name,
|
||||
plays,
|
||||
last_listened,
|
||||
album_art,
|
||||
artist_url,
|
||||
json_build_object('title', track_name, 'artist', artist_name, 'url', artist_url, 'plays', plays, 'alt', CONCAT(track_name, ' by ', artist_name), 'subtext', CONCAT(album_name, ' (', plays, ' plays)'), 'percentage', ROUND((plays::decimal / most_played) * 100, 2)) AS chart
|
||||
FROM
|
||||
track_stats
|
||||
ORDER BY
|
||||
plays DESC,
|
||||
last_listened DESC;
|
||||
|
94
queries/views/media/shows.psql
Normal file
94
queries/views/media/shows.psql
Normal file
|
@ -0,0 +1,94 @@
|
|||
CREATE OR REPLACE VIEW optimized_shows AS
|
||||
SELECT
|
||||
s.id,
|
||||
s.title,
|
||||
s.year,
|
||||
s.collected,
|
||||
s.favorite,
|
||||
s.tattoo,
|
||||
s.description,
|
||||
s.review,
|
||||
s.slug AS url,
|
||||
CONCAT('/', df_art.filename_disk) AS image,
|
||||
CONCAT('/', df_backdrop.filename_disk) AS backdrop,
|
||||
json_build_object('title', s.title, 'image', CONCAT('/', df_art.filename_disk), 'backdrop', CONCAT('/', df_backdrop.filename_disk), 'url', s.slug, 'alt', CONCAT('Poster from ', s.title, ' (', s.year, ')'), 'subtext', COALESCE((
|
||||
SELECT
|
||||
CASE WHEN e1.last_watched_at >= NOW() - INTERVAL '90 days' THEN
|
||||
CONCAT('S', e1.season_number, 'E', e1.episode_number)
|
||||
ELSE
|
||||
CONCAT('(', s.year::text, ')')
|
||||
END FROM episodes e1
|
||||
WHERE
|
||||
e1.show = s.id ORDER BY e1.last_watched_at DESC, e1.season_number DESC, e1.episode_number DESC LIMIT 1), CONCAT('(', s.year::text, ')'))) AS grid,
|
||||
json_build_object('title', s.title, 'year', s.year, 'url', s.slug, 'image', CONCAT('/', df_art.filename_disk), 'backdrop', CONCAT('/', df_backdrop.filename_disk), 'formatted_episode', COALESCE((
|
||||
SELECT
|
||||
CONCAT('S', e2.season_number, 'E', e2.episode_number)
|
||||
FROM episodes e2
|
||||
WHERE
|
||||
e2.show = s.id ORDER BY e2.season_number DESC, e2.episode_number DESC LIMIT 1)), 'last_watched_at', MAX(e.last_watched_at)) AS episode,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', m.title, 'year', m.year, 'url', m.slug)
|
||||
ORDER BY m.year DESC)
|
||||
FROM
|
||||
shows_movies sm
|
||||
LEFT JOIN movies m ON sm.movies_id = m.id
|
||||
WHERE
|
||||
sm.shows_id = s.id) AS movies,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', b.title, 'author', b.author, 'url', b.slug)
|
||||
ORDER BY b.title)
|
||||
FROM
|
||||
shows_books sb
|
||||
LEFT JOIN books b ON sb.books_id = b.id
|
||||
WHERE
|
||||
sb.shows_id = s.id) AS books,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', p.title, 'date', p.date, 'url', p.slug)
|
||||
ORDER BY p.date DESC)
|
||||
FROM
|
||||
posts_shows ps
|
||||
LEFT JOIN posts p ON ps.posts_id = p.id
|
||||
WHERE
|
||||
ps.shows_id = s.id) AS posts,
|
||||
(
|
||||
SELECT
|
||||
array_agg(t.name)
|
||||
FROM
|
||||
shows_tags st
|
||||
LEFT JOIN tags t ON st.tags_id = t.id
|
||||
WHERE
|
||||
st.shows_id = s.id) AS tags,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('title', rs.title, 'year', rs.year, 'url', rs.slug)
|
||||
ORDER BY rs.year DESC)
|
||||
FROM
|
||||
related_shows sr
|
||||
LEFT JOIN shows rs ON sr.related_shows_id = rs.id
|
||||
WHERE
|
||||
sr.shows_id = s.id) AS related_shows,
|
||||
(
|
||||
SELECT
|
||||
json_agg(json_build_object('name', a.name_string, 'url', a.slug, 'country', a.country, 'total_plays', a.total_plays)
|
||||
ORDER BY a.name_string)
|
||||
FROM
|
||||
shows_artists sa
|
||||
LEFT JOIN artists a ON sa.artists_id = a.id
|
||||
WHERE
|
||||
sa.shows_id = s.id) AS artists,
|
||||
MAX(e.last_watched_at) AS last_watched_at
|
||||
FROM
|
||||
shows s
|
||||
LEFT JOIN episodes e ON s.id = e.show
|
||||
LEFT JOIN directus_files df_art ON s.art = df_art.id
|
||||
LEFT JOIN directus_files df_backdrop ON s.backdrop = df_backdrop.id
|
||||
GROUP BY
|
||||
s.id,
|
||||
df_art.filename_disk,
|
||||
df_backdrop.filename_disk
|
||||
ORDER BY
|
||||
MAX(e.last_watched_at) DESC;
|
||||
|
33
scripts/worker-build.mjs
Normal file
33
scripts/worker-build.mjs
Normal file
|
@ -0,0 +1,33 @@
|
|||
import fs from 'fs/promises'
|
||||
import dotenv from 'dotenv-flow'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const workerName = process.argv[2]
|
||||
|
||||
if (!workerName) {
|
||||
console.error('Please specify a worker name.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const templatePath = `workers/${workerName}/wrangler.template.toml`
|
||||
const outputPath = `workers/${workerName}/wrangler.toml`
|
||||
|
||||
async function generateToml() {
|
||||
try {
|
||||
const template = await fs.readFile(templatePath, 'utf8')
|
||||
const output = template
|
||||
.replace(/\${CF_ACCOUNT_ID}/g, process.env.CF_ACCOUNT_ID)
|
||||
.replace(/\${CF_ZONE_ID}/g, process.env.CF_ZONE_ID)
|
||||
.replace(/\${RSS_TO_MASTODON_KV_NAMESPACE_ID}/g, process.env.RSS_TO_MASTODON_KV_NAMESPACE_ID)
|
||||
|
||||
await fs.writeFile(outputPath, output)
|
||||
|
||||
console.log(`Generated wrangler.toml for ${workerName}`)
|
||||
} catch (error) {
|
||||
console.error('Error generating wrangler.toml:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
generateToml()
|
35
src/components/BlockRenderer.astro
Normal file
35
src/components/BlockRenderer.astro
Normal file
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
import AssociatedMedia from '@components/blocks//AssociatedMedia.astro';
|
||||
import GitHub from '@components/blocks/banners/GitHub.astro';
|
||||
import Hero from '@components/blocks//Hero.astro';
|
||||
import Modal from '@components/blocks//Modal.astro';
|
||||
import YouTubePlayer from '@components/blocks//YouTubePlayer.astro';
|
||||
import { md } from '@utils/helpers.js';
|
||||
|
||||
const { block } = Astro.props;
|
||||
const htmlContent = block.type === 'markdown' ? md(block.text) : '';
|
||||
---
|
||||
|
||||
{block.type === 'youtube_player' && (
|
||||
<YouTubePlayer url={block.url} />
|
||||
)}
|
||||
|
||||
{block.type === 'hero' && (
|
||||
<Hero image={block.image} alt={block.alt} />
|
||||
)}
|
||||
|
||||
{block.type === 'markdown' && (
|
||||
<div set:html={htmlContent}></div>
|
||||
)}
|
||||
|
||||
{block.type === 'modal' && (
|
||||
<Modal content={block.content} />
|
||||
)}
|
||||
|
||||
{block.type === 'associated_media' && (
|
||||
<AssociatedMedia media={block.media} />
|
||||
)}
|
||||
|
||||
{block.type === 'github_banner' && (
|
||||
<GitHub url={block.url} />
|
||||
)}
|
34
src/components/Footer.astro
Normal file
34
src/components/Footer.astro
Normal file
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
import NavLink from './nav/NavLink.astro';
|
||||
|
||||
const { nav, updated } = Astro.props;
|
||||
---
|
||||
|
||||
<footer style={updated ? undefined : 'margin-top: var(--spacing-3xl)'}>
|
||||
{updated && (
|
||||
<p class="updated">
|
||||
<em>This page was last updated on {new Date(updated).toLocaleDateString()}</em>
|
||||
</p>
|
||||
)}
|
||||
<nav aria-label="Social icons" class="social">
|
||||
{nav.footer_icons.map(link => (
|
||||
<NavLink
|
||||
url={link.permalink}
|
||||
title={link.title}
|
||||
icon={link.icon}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
<nav aria-label="Secondary site navigation" class="sub-pages">
|
||||
{nav.footer_text.map((link, index) => (
|
||||
<>
|
||||
<NavLink
|
||||
url={link.permalink}
|
||||
title={link.title}
|
||||
icon={link.icon}
|
||||
/>
|
||||
{index < nav.footer_text.length - 1 && <span>/</span>}
|
||||
</>
|
||||
))}
|
||||
</nav>
|
||||
</footer>
|
17
src/components/Header.astro
Normal file
17
src/components/Header.astro
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
import Menu from './nav/Menu.astro';
|
||||
|
||||
const { nav, siteName, url } = Astro.props;
|
||||
const isHomePage = url === '/';
|
||||
---
|
||||
|
||||
<section class="main-title">
|
||||
<h1>
|
||||
{isHomePage ? (
|
||||
siteName
|
||||
) : (
|
||||
<a href="/" tabindex="0">{siteName}</a>
|
||||
)}
|
||||
</h1>
|
||||
<Menu nav={nav} />
|
||||
</section>
|
43
src/components/IconMapper.astro
Normal file
43
src/components/IconMapper.astro
Normal file
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
import {
|
||||
IconArticle,
|
||||
IconHeadphones,
|
||||
IconDeviceTvOld,
|
||||
IconBooks,
|
||||
IconLink,
|
||||
IconInfoCircle,
|
||||
IconSearch,
|
||||
IconRss,
|
||||
IconBrandMastodon,
|
||||
IconMail,
|
||||
IconBrandGithub,
|
||||
IconBrandNpm,
|
||||
IconCoffee,
|
||||
IconDeviceWatch,
|
||||
IconHeartHandshake,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
const { icon } = Astro.props;
|
||||
|
||||
const iconComponents = {
|
||||
article: IconArticle,
|
||||
headphones: IconHeadphones,
|
||||
'device-tv-old': IconDeviceTvOld,
|
||||
books: IconBooks,
|
||||
link: IconLink,
|
||||
'info-circle': IconInfoCircle,
|
||||
search: IconSearch,
|
||||
rss: IconRss,
|
||||
'brand-mastodon': IconBrandMastodon,
|
||||
mail: IconMail,
|
||||
'brand-github': IconBrandGithub,
|
||||
'brand-npm': IconBrandNpm,
|
||||
coffee: IconCoffee,
|
||||
'device-watch': IconDeviceWatch,
|
||||
'heart-handshake': IconHeartHandshake,
|
||||
};
|
||||
|
||||
const SelectedIcon = iconComponents[icon?.toLowerCase()] || null;
|
||||
---
|
||||
|
||||
{SelectedIcon ? <SelectedIcon size={24} /> : null}
|
10
src/components/Intro.astro
Normal file
10
src/components/Intro.astro
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
import NowPlaying from "@components/blocks/NowPlaying.astro";
|
||||
|
||||
const { intro } = Astro.props;
|
||||
---
|
||||
|
||||
<article class="intro">
|
||||
<div set:html={intro} />
|
||||
<p class="music"><NowPlaying /></p>
|
||||
</article>
|
43
src/components/RecentActivity.astro
Normal file
43
src/components/RecentActivity.astro
Normal file
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
import { fetchMusicData } from '@utils/data/music.js';
|
||||
import { fetchShows } from '@utils/data/tv.js';
|
||||
import { fetchMovies } from '@utils/data/movies.js';
|
||||
import { fetchBooks } from '@utils/data/books.js';
|
||||
import { fetchLinks } from '@utils/data/links.js';
|
||||
import { IconActivity } from '@tabler/icons-react';
|
||||
|
||||
import Rss from '@components/blocks/banners/Rss.astro';
|
||||
|
||||
const music = await fetchMusicData();
|
||||
const tv = await fetchShows();
|
||||
const movies = await fetchMovies();
|
||||
const books = await fetchBooks();
|
||||
const links = await fetchLinks();
|
||||
|
||||
const track = music.week?.tracks[0];
|
||||
const show = tv.recentlyWatched[0];
|
||||
const movie = movies.recentlyWatched[0];
|
||||
const book = books.currentYear[0];
|
||||
const link = links[0];
|
||||
---
|
||||
|
||||
<article>
|
||||
<h2>
|
||||
<IconActivity size={24} />
|
||||
Recent activity
|
||||
</h2>
|
||||
<ul>
|
||||
<li><span class="music">Top track this week:</span> <a href={track.artist_url}>{track.track_name} by {track.artist_name}</a></li>
|
||||
<li><span class="tv">Last episode watched:</span> <strong class="highlight-text">{show.formatted_episode}</strong> of <a href={show.url}>{show.title}</a></li>
|
||||
<li><span class="movies">Last movie watched:</span> <a href={movie.url}>{movie.title}</a>{movie.rating ? ` (${movie.rating})` : ''}</li>
|
||||
<li><span class="books">Last book finished:</span> <a href={book.url}>{book.title}</a> by {book.author}{book.rating ? ` (${book.rating})` : ''}</li>
|
||||
<li>
|
||||
<span class="link">Last link shared:</span>
|
||||
<a href={link.link}>{link.title}</a>
|
||||
{link.author && (
|
||||
<span> via <a href={link.author.url}>{link.author.name}</a></span>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
<Rss url="/feeds" text="Subscribe to my movies, books, links or activity feed(s)" />
|
||||
</article>
|
33
src/components/RecentPosts.astro
Normal file
33
src/components/RecentPosts.astro
Normal file
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
import { IconClock, IconStar, IconArrowRight } from '@tabler/icons-react';
|
||||
import { fetchAllPosts } from '../utils/data/posts.js';
|
||||
import { md } from '@utils/helpers.js';
|
||||
|
||||
const posts = await fetchAllPosts();
|
||||
---
|
||||
|
||||
<h2>
|
||||
<IconClock size={24} />
|
||||
Recent posts
|
||||
</h2>
|
||||
{posts.slice(0, 5).map(post => (
|
||||
<article key={post.url}>
|
||||
<div class="post-meta">
|
||||
{post.featured && <IconStar size={16} />}
|
||||
<time datetime={post.date}>
|
||||
{new Date(post.date).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
<h3>
|
||||
<a href={post.url}>{post.title}</a>
|
||||
</h3>
|
||||
<p set:html={md(post.description)}></p>
|
||||
</article>
|
||||
))}
|
||||
<a class="icon-link" href="/posts">
|
||||
View all posts <IconArrowRight size={16} />
|
||||
</a>
|
110
src/components/blocks/AssociatedMedia.astro
Normal file
110
src/components/blocks/AssociatedMedia.astro
Normal file
|
@ -0,0 +1,110 @@
|
|||
---
|
||||
const {
|
||||
artists = [],
|
||||
books = [],
|
||||
genres = [],
|
||||
movies = [],
|
||||
posts = [],
|
||||
shows = [],
|
||||
} = Astro.props;
|
||||
|
||||
const media = [
|
||||
...(artists || []),
|
||||
...(books || []),
|
||||
...(genres || []),
|
||||
...(movies || []),
|
||||
...(posts || []),
|
||||
...(shows || []),
|
||||
];
|
||||
|
||||
if (media.length === 0) return null;
|
||||
|
||||
const sections = [
|
||||
{
|
||||
key: "artists",
|
||||
icon: "headphones",
|
||||
cssClass: "music",
|
||||
label: "Related artist(s)",
|
||||
items: artists || [],
|
||||
},
|
||||
{
|
||||
key: "books",
|
||||
icon: "books",
|
||||
cssClass: "books",
|
||||
label: "Related book(s)",
|
||||
items: books || [],
|
||||
},
|
||||
{
|
||||
key: "genres",
|
||||
icon: "headphones",
|
||||
cssClass: "music",
|
||||
label: "Related genre(s)",
|
||||
items: genres || [],
|
||||
},
|
||||
{
|
||||
key: "movies",
|
||||
icon: "movie",
|
||||
cssClass: "movies",
|
||||
label: "Related movie(s)",
|
||||
items: movies || [],
|
||||
},
|
||||
{
|
||||
key: "posts",
|
||||
icon: "article",
|
||||
cssClass: "article",
|
||||
label: "Related post(s)",
|
||||
items: posts || [],
|
||||
},
|
||||
{
|
||||
key: "shows",
|
||||
icon: "device-tv-old",
|
||||
cssClass: "tv",
|
||||
label: "Related show(s)",
|
||||
items: shows || [],
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<div class="associated-media">
|
||||
{
|
||||
sections.map(({ key, icon, cssClass, label, items }) => {
|
||||
if (!items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={key} class={cssClass}>
|
||||
<p>
|
||||
<IconMapper icon={icon} /> {label}
|
||||
</p>
|
||||
<ul>
|
||||
{items.map((item) => (
|
||||
<li>
|
||||
<a href={item.url}>{item.title || item.name}</a>
|
||||
{key === "artists" && item.total_plays > 0 && (
|
||||
<strong class="highlight-text">
|
||||
{item.total_plays}{" "}
|
||||
{item.total_plays === 1 ? "play" : "plays"}
|
||||
</strong>
|
||||
)}
|
||||
{key === "books" && <span>by {item.author}</span>}
|
||||
{(key === "movies" || key === "shows") && (
|
||||
<span>({item.year})</span>
|
||||
)}
|
||||
{key === "posts" && (
|
||||
<span>
|
||||
(
|
||||
{new Date(item.date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
)
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
7
src/components/blocks/Hero.astro
Normal file
7
src/components/blocks/Hero.astro
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
const { image, alt } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="hero">
|
||||
<img src={image} alt={alt} />
|
||||
</div>
|
7
src/components/blocks/MastodonPost.astro
Normal file
7
src/components/blocks/MastodonPost.astro
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
const { post } = Astro.props;
|
||||
---
|
||||
|
||||
<article class="mastodon-post">
|
||||
<p>{post.content}</p>
|
||||
</article>
|
8
src/components/blocks/Modal.astro
Normal file
8
src/components/blocks/Modal.astro
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="modal">
|
||||
<button class="close">Close</button>
|
||||
<div class="content">{content}</div>
|
||||
</div>
|
24
src/components/blocks/NowPlaying.astro
Normal file
24
src/components/blocks/NowPlaying.astro
Normal file
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
import { fetchNowPlaying } from '../../utils/data/nowPlaying.js';
|
||||
|
||||
const isProduction = import.meta.env.MODE === 'production';
|
||||
const nowPlayingData = await fetchNowPlaying();
|
||||
---
|
||||
|
||||
<span class="loading client-side" id="now-playing-content" set:html={nowPlayingData.content}></span>
|
||||
<noscript>
|
||||
<p>{nowPlayingData.content}</p>
|
||||
</noscript>
|
||||
|
||||
{isProduction && (<script type="module">
|
||||
async function updateNowPlaying() {
|
||||
const response = await fetch('/api/now-playing');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
document.getElementById('now-playing-content').innerHTML = data.content;
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(updateNowPlaying, 180000);
|
||||
updateNowPlaying();
|
||||
</script>)}
|
10
src/components/blocks/YouTubePlayer.astro
Normal file
10
src/components/blocks/YouTubePlayer.astro
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
const { url } = Astro.props;
|
||||
---
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src={url}
|
||||
allowfullscreen>
|
||||
</iframe>
|
13
src/components/blocks/banners/Coffee.astro
Normal file
13
src/components/blocks/banners/Coffee.astro
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
import { IconCoffee } from "@tabler/icons-react";
|
||||
---
|
||||
<div class="banner coffee">
|
||||
<p>
|
||||
<a
|
||||
class="coffee plausible-event-name=Buy+Me+a+Coffee+post+footer"
|
||||
href="https://buymeacoffee.com/cory"
|
||||
>
|
||||
<IconCoffee size={24} /> If you found this post helpful, you can buy me a coffee.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
8
src/components/blocks/banners/Error.astro
Normal file
8
src/components/blocks/banners/Error.astro
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
import { IconAlertCircle } from '@tabler/icons-react';
|
||||
|
||||
const { text } = Astro.props
|
||||
---
|
||||
<div class="banner error">
|
||||
<p><IconAlertCircle size={24} />{ text }</p>
|
||||
</div>
|
9
src/components/blocks/banners/GitHub.astro
Normal file
9
src/components/blocks/banners/GitHub.astro
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
import { IconBrandGithub } from '@tabler/icons-react';
|
||||
|
||||
const { url } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="banner github">
|
||||
<p><IconBrandGithub size={24} /> Take a look at <a href={url}>the GitHub repository for this project</a>. (Give it a star if you feel like it.)</p>
|
||||
</div>
|
12
src/components/blocks/banners/Npm.astro
Normal file
12
src/components/blocks/banners/Npm.astro
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
import { IconBrandNpm } from '@tabler/icons-react';
|
||||
|
||||
const { url, command } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="banner npm">
|
||||
<p>
|
||||
<IconBrandNpm size={24} />
|
||||
<a href={url}>You can take a look at this package on NPM</a> or install it by running <code>{command}</code>.
|
||||
</p>
|
||||
</div>
|
14
src/components/blocks/banners/OldPost.astro
Normal file
14
src/components/blocks/banners/OldPost.astro
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
import { IconClockX } from '@tabler/icons-react';
|
||||
|
||||
const { isOldPost } = Astro.props;
|
||||
---
|
||||
|
||||
{isOldPost && (
|
||||
<div class="banner old-post">
|
||||
<p>
|
||||
<IconClockX size={24} />
|
||||
This post is over 3 years old. I've probably changed my mind since it was written and it <em>could</em> be out of date.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
12
src/components/blocks/banners/Rss.astro
Normal file
12
src/components/blocks/banners/Rss.astro
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
import { IconRss } from '@tabler/icons-react';
|
||||
|
||||
const { url, text } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="banner rss">
|
||||
<p>
|
||||
<IconRss size={24} />
|
||||
<a href={url}>{text}</a>.
|
||||
</p>
|
||||
</div>
|
12
src/components/blocks/banners/Warning.astro
Normal file
12
src/components/blocks/banners/Warning.astro
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
|
||||
const { text } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="banner warning">
|
||||
<p>
|
||||
<IconAlertTriangle size={24} />
|
||||
{text}
|
||||
</p>
|
||||
</div>
|
30
src/components/nav/Menu.astro
Normal file
30
src/components/nav/Menu.astro
Normal file
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
import { IconMenu2, IconX } from '@tabler/icons-react';
|
||||
import NavLink from './NavLink.astro';
|
||||
|
||||
const { nav } = Astro.props;
|
||||
---
|
||||
|
||||
<menu>
|
||||
<input id="menu-toggle" type="checkbox" aria-hidden="true" />
|
||||
<label class="menu-button-container" for="menu-toggle" tabindex="0">
|
||||
<div class="menu-closed" aria-hidden="true">
|
||||
<IconMenu2 size={24} />
|
||||
</div>
|
||||
<div class="menu-open" aria-hidden="true">
|
||||
<IconX size={24} />
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<ul class="menu-primary" aria-label="Primary site navigation" id="primary-navigation">
|
||||
{nav.primary.map((link) => (
|
||||
<li>
|
||||
<NavLink
|
||||
url={link.permalink}
|
||||
title={link.title}
|
||||
icon={link.icon}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</menu>
|
24
src/components/nav/NavLink.astro
Normal file
24
src/components/nav/NavLink.astro
Normal file
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
import IconMapper from '@components/IconMapper.astro';
|
||||
|
||||
const { url, title, icon } = Astro.props;
|
||||
const isHttp = url.startsWith('http');
|
||||
const isActive = Astro.url.pathname === url;
|
||||
---
|
||||
|
||||
{isActive ? (
|
||||
<span class={`active icon ${icon?.toLowerCase()}`} aria-current="page">
|
||||
<IconMapper icon={icon} />
|
||||
<span>{title}</span>
|
||||
</span>
|
||||
) : (
|
||||
<a
|
||||
class={`icon ${icon}`}
|
||||
href={url}
|
||||
rel={isHttp ? 'me' : undefined}
|
||||
aria-label={title}
|
||||
>
|
||||
<IconMapper icon={icon} />
|
||||
<span>{title}</span>
|
||||
</a>
|
||||
)}
|
49
src/components/nav/Paginator.astro
Normal file
49
src/components/nav/Paginator.astro
Normal file
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react";
|
||||
|
||||
const { pagination } = Astro.props;
|
||||
const {
|
||||
currentPage,
|
||||
totalPages,
|
||||
hasPrevious,
|
||||
hasNext,
|
||||
previousPage,
|
||||
nextPage,
|
||||
pages,
|
||||
} = pagination;
|
||||
---
|
||||
|
||||
<nav aria-label="Pagination" class="pagination">
|
||||
<a
|
||||
href={hasPrevious ? previousPage : "#"}
|
||||
aria-label="Previous page"
|
||||
class={hasPrevious ? "" : "disabled"}
|
||||
>
|
||||
<IconArrowLeft size={24} />
|
||||
</a>
|
||||
|
||||
<select class="client-side" aria-label="Page selection">
|
||||
{pages.map((page, index) => (
|
||||
<option
|
||||
value={index}
|
||||
data-href={page.href}
|
||||
selected={page.number === currentPage}
|
||||
>
|
||||
{page.number} of {totalPages}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<noscript>
|
||||
<p>
|
||||
<span aria-current="page">{currentPage}</span> of {totalPages}
|
||||
</p>
|
||||
</noscript>
|
||||
|
||||
<a
|
||||
href={hasNext ? nextPage : "#"}
|
||||
aria-label="Next page"
|
||||
class={hasNext ? "" : "disabled"}
|
||||
>
|
||||
<IconArrowRight size={24} />
|
||||
</a>
|
||||
</nav>
|
1
src/env.d.ts
vendored
Normal file
1
src/env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference path="../.astro/types.d.ts" />
|
96
src/layouts/Layout.astro
Normal file
96
src/layouts/Layout.astro
Normal file
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
import "@styles/index.css";
|
||||
import Header from "../components/Header.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
import { fetchNavigation } from "../utils/data/nav.js";
|
||||
|
||||
const currentUrl = Astro.url.pathname;
|
||||
const nav = await fetchNavigation();
|
||||
const { globals, pageTitle, description, ogImage, fullUrl, schema } =
|
||||
Astro.props;
|
||||
const isProduction = import.meta.env.MODE === "production";
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<title>{globals.site_name}</title>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/ml.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/mlb.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content={description || globals?.site_description}
|
||||
/>
|
||||
<meta property="og:title" content={pageTitle || globals?.site_name} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={description || globals?.site_description}
|
||||
/>
|
||||
<meta property="og:type" content={schema || "website"} />
|
||||
<meta property="og:url" content={fullUrl} />
|
||||
<meta property="og:image" content={`${ogImage}?class=w800`} />
|
||||
<meta name="theme-color" content={globals?.theme_color} />
|
||||
<meta name="fediverse:creator" content={globals?.mastodon} />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href={`${globals?.cdn_url}${globals?.avatar_transparent}?class=w200`}
|
||||
/>
|
||||
<script defer src="/scripts/index.js"></script>
|
||||
{
|
||||
isProduction && (
|
||||
<>
|
||||
<script defer data-domain="coryd.dev" src="/js/script.js" />
|
||||
<script>
|
||||
window.plausible = window.plausible || function(){" "}
|
||||
{(window.plausible.q = window.plausible.q || []).push(arguments)}
|
||||
</script>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<noscript>
|
||||
<style>.client-side {display:none}</style>
|
||||
</noscript>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
(() => {
|
||||
const currentTheme = sessionStorage.getItem("theme");
|
||||
const metaColorScheme = document.querySelector(
|
||||
'meta[name="color-scheme"]'
|
||||
);
|
||||
const prefersDarkScheme = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
const themeToSet =
|
||||
currentTheme || (prefersDarkScheme ? "dark" : "light");
|
||||
if (!currentTheme) sessionStorage.setItem("theme", themeToSet);
|
||||
metaColorScheme?.setAttribute("content", themeToSet);
|
||||
})();
|
||||
</script>
|
||||
<div class="main-wrapper">
|
||||
<main>
|
||||
<Header nav={nav} siteName={globals?.site_name} url={currentUrl} />
|
||||
<div class="default-wrapper">
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
<Footer nav={nav} />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
33
src/pages/[permalink].astro
Normal file
33
src/pages/[permalink].astro
Normal file
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
import Layout from '@layouts/Layout.astro';
|
||||
import BlockRenderer from '@components/BlockRenderer.astro';
|
||||
import { fetchGlobals } from '@utils/data/globals.js';
|
||||
import { fetchPages } from '@utils/data/pages';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const pages = await fetchPages();
|
||||
return pages.map((page) => ({
|
||||
params: { permalink: page.permalink },
|
||||
props: { page },
|
||||
}));
|
||||
}
|
||||
|
||||
const globals = await fetchGlobals();
|
||||
const { page } = Astro.props;
|
||||
const currentUrl = Astro.url.pathname;
|
||||
---
|
||||
|
||||
<Layout
|
||||
globals={globals}
|
||||
pageTitle={page.title}
|
||||
description={page.description}
|
||||
ogImage={page.open_graph_image}
|
||||
updated={page.updated}
|
||||
currentUrl={currentUrl}
|
||||
>
|
||||
{page.blocks.map((block) => (
|
||||
<BlockRenderer block={block} />
|
||||
))}
|
||||
</Layout>
|
64
src/pages/feeds/syndication.xml.js
Normal file
64
src/pages/feeds/syndication.xml.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
import fetchSyndication from '../../utils/data/syndication.js';
|
||||
import { fetchGlobals } from '../../utils/data/globals.js';
|
||||
|
||||
export async function GET() {
|
||||
const globals = await fetchGlobals();
|
||||
const entries = await fetchSyndication();
|
||||
|
||||
if (!entries.length) return new Response('No feed entries found.', { status: 404 });
|
||||
|
||||
const title = globals.site_name || 'Syndicated content / Cory Dransfeldt';
|
||||
const permalink = '/feeds/syndication.xml';
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<atom:link href="${globals.url}${permalink}" rel="self" type="application/rss+xml" />
|
||||
<title><![CDATA[${title}]]></title>
|
||||
<description><![CDATA[${globals.site_description || ''}]]></description>
|
||||
<link>${globals.url}${permalink}</link>
|
||||
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
|
||||
<image>
|
||||
<title><![CDATA[${title}]]></title>
|
||||
<link>${globals.url}${permalink}</link>
|
||||
<url>${globals.cdn_url}${globals.avatar}?class=w200</url>
|
||||
<width>144</width>
|
||||
<height>144</height>
|
||||
</image>
|
||||
${entries
|
||||
.slice(0, 20)
|
||||
.map(
|
||||
(entry) => `
|
||||
<item>
|
||||
<title><![CDATA[${entry.syndication.title}]]></title>
|
||||
<link>${encodeAmp(entry.syndication.url)}</link>
|
||||
<pubDate>${new Date(entry.syndication.date).toUTCString()}</pubDate>
|
||||
<guid isPermaLink="false">${encodeAmp(entry.syndication.url)}</guid>
|
||||
<description><![CDATA[${escapeHTML(entry.syndication.description)}]]></description>
|
||||
</item>`
|
||||
)
|
||||
.join('')}
|
||||
</channel>
|
||||
</rss>`;
|
||||
|
||||
return new Response(xml, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/rss+xml',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function encodeAmp(url) {
|
||||
return url.replace(/&/g, '&');
|
||||
}
|
||||
|
||||
function escapeHTML(str) {
|
||||
if (!str) return '';
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
29
src/pages/index.astro
Normal file
29
src/pages/index.astro
Normal file
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Intro from '../components/Intro.astro';
|
||||
import { fetchGlobals } from '../utils/data/globals';
|
||||
import RecentActivity from '../components/RecentActivity.astro';
|
||||
import RecentPosts from '../components/RecentPosts.astro';
|
||||
|
||||
const globals = await fetchGlobals();
|
||||
const schema = 'blog';
|
||||
const pageTitle = globals.site_name;
|
||||
const description = 'This is a blog post description';
|
||||
const ogImage = globals.cdn_url + globals.avatar;
|
||||
const fullUrl = globals.url + '/blog/my-post';
|
||||
const themeColor = globals.theme_color;
|
||||
---
|
||||
<Layout
|
||||
globals={globals}
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
ogImage={ogImage}
|
||||
fullUrl={fullUrl}
|
||||
themeColor={themeColor}
|
||||
schema={schema}
|
||||
globals={globals}
|
||||
>
|
||||
<Intro intro={globals.intro} />
|
||||
<RecentActivity />
|
||||
<RecentPosts />
|
||||
</Layout>
|
59
src/pages/posts/[...page].astro
Normal file
59
src/pages/posts/[...page].astro
Normal file
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import { IconStar } from '@tabler/icons-react';
|
||||
import { fetchGlobals } from "@data/globals.js";
|
||||
import { fetchAllPosts } from "@data/posts.js";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Paginator from '@components/nav/Paginator.astro';
|
||||
import { md } from '@utils/helpers.js';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const globals = await fetchGlobals();
|
||||
const posts = await fetchAllPosts();
|
||||
const { page } = Astro.props;
|
||||
const currentUrl = Astro.url.pathname;
|
||||
|
||||
const currentPage = Astro.params.page ? parseInt(Astro.params.page, 10) : 1;
|
||||
const pageSize = 15;
|
||||
const totalPosts = posts.length;
|
||||
const totalPages = Math.ceil(totalPosts / pageSize);
|
||||
const start = (currentPage - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
const paginatedPosts = posts.slice(start, end);
|
||||
|
||||
const pagination = {
|
||||
currentPage,
|
||||
totalPages,
|
||||
hasPrevious: currentPage > 1,
|
||||
hasNext: currentPage < totalPages,
|
||||
previousPage: currentPage > 1 ? `/posts/${currentPage - 1}` : null,
|
||||
nextPage: currentPage < totalPages ? `/posts/${currentPage + 1}` : null,
|
||||
pages: Array.from({ length: totalPages }, (_, i) => ({
|
||||
number: i + 1,
|
||||
href: `/posts/${i + 1}`,
|
||||
})),
|
||||
};
|
||||
---
|
||||
|
||||
<Layout
|
||||
globals={globals}
|
||||
pageTitle="All posts"
|
||||
currentUrl={currentUrl}
|
||||
>
|
||||
{paginatedPosts.map((post) => (
|
||||
<article>
|
||||
<div class="post-meta">
|
||||
{post.featured && <IconStar size={16} />}
|
||||
<time datetime={post.date}>
|
||||
{DateTime.fromISO(post.date).toLocaleString(DateTime.DATE_FULL)}
|
||||
</time>
|
||||
</div>
|
||||
<h3>
|
||||
<a href={post.slug}>{post.title}</a>
|
||||
</h3>
|
||||
<p set:html={md(post.description)}></p>
|
||||
</article>
|
||||
))}
|
||||
|
||||
<Paginator pagination={pagination} appVersion="1.0.0" />
|
||||
</Layout>
|
102
src/pages/posts/[year]/[title].astro
Normal file
102
src/pages/posts/[year]/[title].astro
Normal file
|
@ -0,0 +1,102 @@
|
|||
---
|
||||
import { IconStar } from "@tabler/icons-react";
|
||||
import { fetchAllPosts } from "@data/posts.js";
|
||||
import { fetchGlobals } from "@data/globals.js";
|
||||
import { md } from '@utils/helpers.js';
|
||||
import OldPost from "@components/blocks/banners/OldPost.astro";
|
||||
import BlockRenderer from "@components/BlockRenderer.astro";
|
||||
import AssociatedMedia from "@components/blocks/AssociatedMedia.astro";
|
||||
import MastodonPost from "@components/blocks/MastodonPost.astro";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Coffee from "@components/blocks/banners/Coffee.astro";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await fetchAllPosts();
|
||||
|
||||
return posts.map((post) => {
|
||||
const match = post.url.match(/^\/posts\/(\d{4})\/(.+)$/);
|
||||
if (!match) throw new Error(`Invalid post URL: ${post.url}`);
|
||||
|
||||
const [, year, title] = match;
|
||||
|
||||
return {
|
||||
params: { year, title },
|
||||
props: { post },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { year, title } = Astro.params;
|
||||
const globals = await fetchGlobals();
|
||||
const currentUrl = Astro.url.pathname;
|
||||
const htmlContent = md(post.content);
|
||||
---
|
||||
|
||||
<Layout
|
||||
globals={globals}
|
||||
pageTitle={post.title}
|
||||
description={post.description}
|
||||
ogImage={post.open_graph_image}
|
||||
updated={post.updated}
|
||||
currentUrl={currentUrl}
|
||||
>
|
||||
<article class="standalone">
|
||||
<div class="post-meta">
|
||||
{post.featured && <IconStar size={16} />}
|
||||
<time datetime={post.date}>
|
||||
{
|
||||
new Date(post.date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
}
|
||||
</time>
|
||||
</div>
|
||||
<h3>{post.title}</h3>
|
||||
<div>
|
||||
{post.old_post && <OldPost />}
|
||||
{
|
||||
post.image && (
|
||||
<img
|
||||
srcset={`
|
||||
${globals.cdn_url}${post.image}?class=w200&type=webp 200w,
|
||||
${globals.cdn_url}${post.image}?class=w400&type=webp 400w,
|
||||
${globals.cdn_url}${post.image}?class=w800&type=webp 800w,
|
||||
${globals.cdn_url}${post.image}?class=w1600&type=webp 1600w
|
||||
`}
|
||||
sizes="(max-width: 450px) 200px,
|
||||
(max-width: 850px) 400px,
|
||||
(max-width: 1000px) 800px,
|
||||
1200px"
|
||||
src={`${globals.cdn_url}${post.image}?class=w200`}
|
||||
alt={post.image_alt?.replace(/['"]/g, "")}
|
||||
class="image-banner"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
width="200"
|
||||
height="auto"
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div set:html={htmlContent} />
|
||||
{
|
||||
post.blocks &&
|
||||
post.blocks.map((block) => <BlockRenderer block={block} />)
|
||||
}
|
||||
<!-- {post.mastodon_url && <MastodonPost url={post.mastodon_url} />} -->
|
||||
<AssociatedMedia
|
||||
artists={post.artists}
|
||||
books={post.books}
|
||||
genres={post.genres}
|
||||
movies={post.movies}
|
||||
posts={post.posts}
|
||||
shows={post.shows}
|
||||
/>
|
||||
<Coffee />
|
||||
</div>
|
||||
</article>
|
||||
</Layout>
|
26
src/pages/robots.txt.js
Normal file
26
src/pages/robots.txt.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { fetchAllRobots } from '../utils//data/robots.js';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const robots = await fetchAllRobots();
|
||||
|
||||
const sitemap = `Sitemap: https://coryd.dev/sitemap.xml\n\n`;
|
||||
const allowAll = `User-agent: *\nDisallow:\n\n`;
|
||||
const disallowedBots = robots
|
||||
.map((userAgent) => `User-agent: ${userAgent}`)
|
||||
.join('\n');
|
||||
const disallowAll = `\nDisallow: /`;
|
||||
|
||||
const robotsTxt = `${sitemap}${allowAll}${disallowedBots}${disallowAll}`;
|
||||
|
||||
return new Response(robotsTxt, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating robots.txt:', error);
|
||||
return new Response('Error generating robots.txt', { status: 500 });
|
||||
}
|
||||
}
|
31
src/styles/base/fonts.css
Normal file
31
src/styles/base/fonts.css
Normal file
|
@ -0,0 +1,31 @@
|
|||
@font-face {
|
||||
font-family: MonoLisa;
|
||||
src: url("/fonts/ml.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: MonoLisa;
|
||||
src: url("/fonts/mlb.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: MonoLisa;
|
||||
src: url("/fonts/mli.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: MonoLisa;
|
||||
src: url("/fonts/mlbi.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
487
src/styles/base/index.css
Normal file
487
src/styles/base/index.css
Normal file
|
@ -0,0 +1,487 @@
|
|||
html,
|
||||
body {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-color);
|
||||
background: var(--background-color);
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-color: var(--accent-color) var(--gray-light);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: var(--sizing-md);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--gray-light);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-color);
|
||||
border-radius: var(--border-radius-full);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-color-hover);
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: var(--color-lightest);
|
||||
background: var(--accent-color);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
}
|
||||
|
||||
.highlight-text {
|
||||
color: var(--text-color-inverted);
|
||||
background-color: var(--accent-color);
|
||||
padding: var(--spacing-xs);
|
||||
border-radius: var(--border-radius-slight);
|
||||
}
|
||||
|
||||
details > summary {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--gray-dark);
|
||||
padding-left: var(--spacing-lg);
|
||||
border-left: var(--sizing-xs) solid var(--gray-dark);
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
}
|
||||
|
||||
:is(h1, h2, h3) svg {
|
||||
stroke-width: var(--stroke-width-bold);
|
||||
}
|
||||
|
||||
strong,
|
||||
blockquote {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
em,
|
||||
blockquote {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: var(--sizing-svg-base);
|
||||
height: var(--sizing-svg-base);
|
||||
stroke-width: var(--stroke-width-default);
|
||||
}
|
||||
|
||||
/* images */
|
||||
img {
|
||||
border-radius: var(--border-radius-slight);
|
||||
|
||||
&.image-banner {
|
||||
border: var(--border-default);
|
||||
height: auto;
|
||||
width: 100%;
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
}
|
||||
}
|
||||
|
||||
/* lists */
|
||||
ul,
|
||||
ol {
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
padding-left: var(--spacing-base);
|
||||
|
||||
& li:not(:last-child) {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* brand + section colors */
|
||||
.article,
|
||||
.books,
|
||||
.brand-github,
|
||||
.brand-mastodon,
|
||||
.brand-npm,
|
||||
.coffee,
|
||||
.collected,
|
||||
.concerts,
|
||||
.country,
|
||||
.device-tv-old,
|
||||
.device-watch,
|
||||
.favorite,
|
||||
.headphones,
|
||||
.heart-handshake,
|
||||
.info-circle,
|
||||
.link,
|
||||
.mail,
|
||||
.mail-plus,
|
||||
.movies,
|
||||
.music,
|
||||
.rss,
|
||||
.search,
|
||||
.tattoo,
|
||||
.tv {
|
||||
&.article {
|
||||
--section-color: var(--article);
|
||||
}
|
||||
&.books {
|
||||
--section-color: var(--books);
|
||||
}
|
||||
&.brand-github {
|
||||
--section-color: var(--brand-github);
|
||||
}
|
||||
&.brand-mastodon {
|
||||
--section-color: var(--brand-mastodon);
|
||||
}
|
||||
&.brand-npm {
|
||||
--section-color: var(--brand-npm);
|
||||
}
|
||||
&.coffee {
|
||||
--section-color: var(--brand-buy-me-a-coffee);
|
||||
}
|
||||
&.collected {
|
||||
--section-color: var(--collected);
|
||||
}
|
||||
&.concerts {
|
||||
--section-color: var(--concerts);
|
||||
}
|
||||
&.country {
|
||||
--section-color: var(--country);
|
||||
}
|
||||
&.device-tv-old {
|
||||
--section-color: var(--tv);
|
||||
}
|
||||
&.device-watch {
|
||||
--section-color: var(--now);
|
||||
}
|
||||
&.favorite {
|
||||
--section-color: var(--favorite);
|
||||
}
|
||||
&.headphones {
|
||||
--section-color: var(--music);
|
||||
}
|
||||
&.heart-handshake {
|
||||
--section-color: var(--webrings);
|
||||
}
|
||||
&.info-circle {
|
||||
--section-color: var(--about);
|
||||
}
|
||||
&.link {
|
||||
--section-color: var(--link);
|
||||
}
|
||||
&.mail {
|
||||
--section-color: var(--brand-fastmail);
|
||||
}
|
||||
&.mail-plus {
|
||||
--section-color: var(--newsletter);
|
||||
}
|
||||
&.movies,
|
||||
&.tv {
|
||||
--section-color: var(--tv);
|
||||
}
|
||||
&.music {
|
||||
--section-color: var(--music);
|
||||
}
|
||||
&.rss {
|
||||
--section-color: var(--brand-rss);
|
||||
}
|
||||
&.search {
|
||||
--section-color: var(--search);
|
||||
}
|
||||
&.tattoo {
|
||||
--section-color: var(--tattoo);
|
||||
}
|
||||
|
||||
color: var(--section-color);
|
||||
|
||||
& svg {
|
||||
stroke: var(--section-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* links */
|
||||
a {
|
||||
color: var(--accent-color);
|
||||
|
||||
&.back-link {
|
||||
margin-bottom: var(--spacing-base);
|
||||
}
|
||||
|
||||
& > img {
|
||||
border: var(--border-default);
|
||||
}
|
||||
|
||||
& svg {
|
||||
stroke: var(--accent-color);
|
||||
}
|
||||
|
||||
&:is(:hover, :focus, :active) {
|
||||
color: var(--accent-color-hover);
|
||||
transition: color var(--transition-duration-default)
|
||||
var(--transition-ease-in-out);
|
||||
|
||||
& > img {
|
||||
border: var(--border-default-hover);
|
||||
transition: border var(--transition-duration-default)
|
||||
var(--transition-ease-in-out);
|
||||
}
|
||||
|
||||
& > svg {
|
||||
stroke: var(--accent-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:is(h1, h2, h3, a, p:not(.banner p), span, th, td, .post-meta):has(svg) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* headers */
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--line-height-md);
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-size-xl);
|
||||
|
||||
&.page-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
}
|
||||
|
||||
/* dividers */
|
||||
hr {
|
||||
color: var(--gray-light);
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
}
|
||||
|
||||
/* articles */
|
||||
article {
|
||||
margin-bottom: var(--spacing-base);
|
||||
|
||||
&:not([class], :last-of-type) {
|
||||
border-bottom: var(--border-gray);
|
||||
}
|
||||
|
||||
&.intro {
|
||||
border-bottom: var(--border-gray);
|
||||
|
||||
& p {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
& .post-meta {
|
||||
& svg {
|
||||
stroke: var(--gray-dark);
|
||||
width: var(--sizing-svg-sm);
|
||||
height: var(--sizing-svg-sm);
|
||||
}
|
||||
|
||||
& time {
|
||||
color: var(--gray-dark);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sup.footnote-ref {
|
||||
line-height: var(--line-height-xs);
|
||||
padding: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* tables */
|
||||
table {
|
||||
display: block;
|
||||
border: var(--border-gray);
|
||||
border-radius: var(--border-radius-slight);
|
||||
overflow-x: scroll;
|
||||
white-space: nowrap;
|
||||
caption-side: bottom;
|
||||
overscroll-behavior: none;
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
:is(th, td):not(:first-child, :last-child) {
|
||||
border-right: var(--border-gray);
|
||||
}
|
||||
|
||||
th,
|
||||
tr:not(:last-child) {
|
||||
border-bottom: var(--border-gray);
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: var(--spacing-sm);
|
||||
word-break: break-word;
|
||||
|
||||
&:first-child {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
max-width: 200px;
|
||||
border-inline-end: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-inline-end: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background: var(--gray-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: var(--font-weight-bold);
|
||||
background-color: var(--gray-lighter);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
min-width: calc(var(--spacing-3xl) * 2);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
&:first-child {
|
||||
background: var(--background-color);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
td:first-of-type,
|
||||
:where(thead, tfoot) th:nth-child(2) {
|
||||
border-inline-start: none;
|
||||
}
|
||||
|
||||
/* header */
|
||||
.main-title {
|
||||
width: 100%;
|
||||
padding-top: var(--spacing-3xl);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
& h1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* nav */
|
||||
.active,
|
||||
.active svg {
|
||||
cursor: not-allowed;
|
||||
color: var(--accent-color-active);
|
||||
stroke: var(--accent-color-active);
|
||||
}
|
||||
|
||||
/* layout */
|
||||
.default-wrapper {
|
||||
padding-top: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
main,
|
||||
footer {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
& .updated {
|
||||
text-align: center;
|
||||
font-size: var(--font-size-sm);
|
||||
margin: var(--spacing-3xl) 0 var(--spacing-lg);
|
||||
}
|
||||
|
||||
& nav {
|
||||
&.social,
|
||||
&.sub-pages {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.social {
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
width: 100%;
|
||||
|
||||
& .icon > span,
|
||||
& .active > span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .active {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&.sub-pages {
|
||||
font-size: var(--font-size-sm);
|
||||
padding-bottom: var(--spacing-3xl);
|
||||
gap: var(--sizing-sm);
|
||||
}
|
||||
}
|
||||
}
|
131
src/styles/base/reset.css
Normal file
131
src/styles/base/reset.css
Normal file
|
@ -0,0 +1,131 @@
|
|||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:where([hidden]:not([hidden='until-found'])) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:where(html) {
|
||||
font-size: 100%;
|
||||
-webkit-text-size-adjust: none;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-gutter: stable;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
:where(html:has(dialog:modal[open])) {
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:where(html:focus-within) {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
:where(body) {
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
:where(button) {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
:where(input, button, textarea, select) {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
:where(textarea) {
|
||||
resize: vertical;
|
||||
resize: block;
|
||||
}
|
||||
|
||||
:where(button, label, select, summary, [role='button'], [role='option']) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:where(:disabled) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:where(label:has(> input:disabled), label:has(+ input:disabled)) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:where(a) {
|
||||
color: inherit;
|
||||
text-underline-offset: var(--spacing-xs);
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: number;
|
||||
}
|
||||
|
||||
:where(ul, ol) {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
:where(img, svg, video, canvas, audio, iframe, embed, object) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:where(p, h1, h2, h3) {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
:where(hr) {
|
||||
border: none;
|
||||
border-block-start: 1px solid;
|
||||
border-block-start-color: currentColor;
|
||||
color: inherit;
|
||||
block-size: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
:where(dialog, [popover]) {
|
||||
border: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
inset: unset;
|
||||
max-width: unset;
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
:where(dialog:not([open], [popover]), [popover]:not(:popover-open)) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:where(:focus-visible) {
|
||||
outline: var(--border-default);
|
||||
outline-offset: 1px;
|
||||
border-radius: var(--border-radius-slight);
|
||||
box-shadow: 0 0 0 1px var(--accent-color);
|
||||
}
|
||||
|
||||
:where(:focus-visible, :target) {
|
||||
scroll-margin-block: 8vh;
|
||||
}
|
||||
|
||||
:where(.visually-hidden:not(:focus-within, :active)) {
|
||||
clip-path: inset(50%) !important;
|
||||
height: 1px !important;
|
||||
width: 1px !important;
|
||||
overflow: hidden !important;
|
||||
position: absolute !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
user-select: none !important;
|
||||
}
|
162
src/styles/base/vars.css
Normal file
162
src/styles/base/vars.css
Normal file
|
@ -0,0 +1,162 @@
|
|||
:root {
|
||||
/* colors */
|
||||
--blue-100: #a2c4ff;
|
||||
--blue-200: #6b9eff;
|
||||
--blue-300: #4a78ff;
|
||||
--blue-400: #3364ff;
|
||||
--blue-500: #2553e6;
|
||||
--blue-600: #1e42c7;
|
||||
|
||||
--gray-100: #f9fafb;
|
||||
--gray-200: #eceef1;
|
||||
--gray-300: #dfe3e8;
|
||||
--gray-400: #959eae;
|
||||
--gray-500: #7f899b;
|
||||
--gray-600: #626d7f;
|
||||
--gray-700: #545e71;
|
||||
--gray-800: #4a5365;
|
||||
--gray-900: #14161a;
|
||||
|
||||
--gray-lighter: light-dark(var(--gray-200), var(--gray-700));
|
||||
--gray-light: light-dark(var(--gray-300), var(--gray-600));
|
||||
--gray-medium: var(--gray-400);
|
||||
--gray-dark: light-dark(var(--gray-800), var(--gray-300));
|
||||
|
||||
/* base theme */
|
||||
--color-lightest: var(--gray-100);
|
||||
--color-darkest: var(--gray-900);
|
||||
--text-color: light-dark(var(--color-darkest), var(--color-lightest));
|
||||
--background-color: light-dark(var(--color-lightest), var(--color-darkest));
|
||||
--text-color-inverted: light-dark(
|
||||
var(--color-lightest),
|
||||
var(--color-darkest)
|
||||
);
|
||||
--background-color-inverted: light-dark(
|
||||
var(--color-darkest),
|
||||
var(--color-lightest)
|
||||
);
|
||||
--accent-color: light-dark(var(--blue-400), var(--blue-200));
|
||||
--accent-color-hover: light-dark(var(--blue-600), var(--blue-100));
|
||||
--accent-color-active: light-dark(var(--blue-600), var(--blue-100));
|
||||
|
||||
--brand-buy-me-a-coffee: light-dark(#0d0c22, #ffdd00);
|
||||
--brand-github: light-dark(#333, #f5f5f5);
|
||||
--brand-fastmail: light-dark(#0067b9, #ffc107);
|
||||
--brand-mastodon: light-dark(#563acc, #858afa);
|
||||
--brand-npm: #cb3837;
|
||||
--brand-rss: #f26522;
|
||||
|
||||
--article: light-dark(#007272, #00ffff);
|
||||
--about: light-dark(#e4513a, #ff967d);
|
||||
--books: light-dark(#1a7b1a, #6fff6f);
|
||||
--collected: light-dark(#9a501a, #ffae73);
|
||||
--concerts: light-dark(#cb426e, #ff82aa);
|
||||
--country: light-dark(#146a67, #80dcdc);
|
||||
--error: light-dark(#b81f1f, #ff8b8b);
|
||||
--favorite: light-dark(#b03c72, #ff9ccd);
|
||||
--link: light-dark(#7b5cba, #e2b8ff);
|
||||
--music: light-dark(#3d7099, #76b8cc);
|
||||
--newsletter: light-dark(#37b0b0, #91fffa);
|
||||
--now: light-dark(#cc1076, #ff82d5);
|
||||
--search: light-dark(#6b5e3a, #c0b594);
|
||||
--tattoo: light-dark(#951b1b, #ff7373);
|
||||
--tv: light-dark(#cc3600, #ff8f66);
|
||||
--warning: light-dark(#cc6f00, #ffbf66);
|
||||
--webrings: light-dark(#b054b0, #ffb3ff);
|
||||
|
||||
/* borders */
|
||||
--border-default: 1px solid var(--accent-color);
|
||||
--border-default-hover: 1px solid var(--accent-color-hover);
|
||||
--border-gray: 1px solid var(--gray-light);
|
||||
|
||||
/* fonts */
|
||||
--font-mono: MonoLisa, Menlo, Consolas, Monaco, Liberation Mono,
|
||||
Lucida Console, ui-monospace, monospace;
|
||||
|
||||
/* text */
|
||||
--font-size-xs: 0.7rem;
|
||||
--font-size-sm: 0.85rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.15rem;
|
||||
--font-size-xl: 1.3rem;
|
||||
--font-size-2xl: 1.45rem;
|
||||
--font-size-3xl: 1.6rem;
|
||||
|
||||
--font-weight-base: 400;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--line-height-sm: 1;
|
||||
--line-height-md: 1.5;
|
||||
--line-height-base: 2;
|
||||
|
||||
/* sizing */
|
||||
--sizing-xs: 0.25rem;
|
||||
--sizing-sm: 0.5rem;
|
||||
--sizing-md: 0.75rem;
|
||||
--sizing-lg: 1rem;
|
||||
--sizing-base: 1.5rem;
|
||||
--sizing-xl: 1.75rem;
|
||||
--sizing-2xl: 2rem;
|
||||
--sizing-3xl: 2.25rem;
|
||||
|
||||
--sizing-svg-sm: 18px;
|
||||
--sizing-svg-base: 24px;
|
||||
|
||||
/* spacing */
|
||||
--spacing-xs: var(--sizing-xs);
|
||||
--spacing-sm: var(--sizing-sm);
|
||||
--spacing-md: var(--sizing-md);
|
||||
--spacing-lg: var(--sizing-lg);
|
||||
--spacing-base: var(--sizing-base);
|
||||
--spacing-xl: var(--sizing-xl);
|
||||
--spacing-2xl: var(--sizing-2xl);
|
||||
--spacing-3xl: var(--sizing-3xl);
|
||||
|
||||
--margin-vertical-base-horizontal-zero: var(--spacing-base) 0;
|
||||
|
||||
/* radii */
|
||||
--border-radius-slight: var(--sizing-xs);
|
||||
--border-radius-full: 9999px;
|
||||
|
||||
/* aspect ratios */
|
||||
--aspect-ratio-square: 1/1;
|
||||
--aspect-ratio-vertical: 2/3;
|
||||
--aspect-ratio-banner: 3/2;
|
||||
|
||||
/* grid columns */
|
||||
--grid-columns-one: repeat(1, minmax(0, 1fr));
|
||||
--grid-columns-two: repeat(2, minmax(0, 1fr));
|
||||
--grid-columns-three: repeat(3, minmax(0, 1fr));
|
||||
--grid-columns-four: repeat(4, minmax(0, 1fr));
|
||||
--grid-columns-six: repeat(6, minmax(0, 1fr));
|
||||
|
||||
--grid-poster: var(--grid-columns-two);
|
||||
--grid-square: var(--grid-columns-two);
|
||||
--grid-vertical: var(--grid-columns-three);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
--grid-poster: var(--grid-columns-three);
|
||||
--grid-square: var(--grid-columns-four);
|
||||
--grid-vertical: var(--grid-columns-six);
|
||||
}
|
||||
|
||||
/* transitions */
|
||||
--transition-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-duration-default: 300ms;
|
||||
|
||||
/* svgs */
|
||||
--stroke-width-default: 1.4;
|
||||
--stroke-width-bold: 2;
|
||||
--inline-margin-bottom: -5px;
|
||||
|
||||
/* shadows */
|
||||
--box-shadow-media: inset 0 -85px 60px -40px var(--color-darkest);
|
||||
--box-shadow-text-toggle: inset 0 -120px 60px -60px var(--background-color);
|
||||
--text-shadow-default: rgba(0, 0, 0, 0.7) 0px 0px 10px;
|
||||
|
||||
/* modals */
|
||||
--modal-overlay-background: light-dark(#ffffffbf, #000000bf);
|
||||
|
||||
/* input accent color */
|
||||
accent-color: var(--accent-color);
|
||||
}
|
14
src/styles/components/addon-links.css
Normal file
14
src/styles/components/addon-links.css
Normal file
|
@ -0,0 +1,14 @@
|
|||
.addon-links {
|
||||
display: grid;
|
||||
gap: var(--sizing-base);
|
||||
grid-template-columns: var(--grid-columns-one);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
grid-template-columns: var(--grid-columns-two);
|
||||
}
|
||||
|
||||
& article {
|
||||
border-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
20
src/styles/components/badge-grid.css
Normal file
20
src/styles/components/badge-grid.css
Normal file
|
@ -0,0 +1,20 @@
|
|||
.badge-grid {
|
||||
display: grid;
|
||||
gap: var(--spacing-md);
|
||||
grid-template-columns: var(--grid-columns-three);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
grid-template-columns: var(--grid-columns-four);
|
||||
}
|
||||
|
||||
& > a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
& img {
|
||||
image-rendering: pixelated;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
63
src/styles/components/banners.css
Normal file
63
src/styles/components/banners.css
Normal file
|
@ -0,0 +1,63 @@
|
|||
.banner {
|
||||
padding: var(--spacing-md);
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
border: 1px solid;
|
||||
border-radius: var(--border-radius-slight);
|
||||
|
||||
& p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
|
||||
a {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
& svg {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
height: var(--sizing-lg);
|
||||
width: var(--sizing-lg);
|
||||
}
|
||||
}
|
||||
|
||||
&.coffee,
|
||||
&.error,
|
||||
&.github,
|
||||
&.npm,
|
||||
&.old-post,
|
||||
&.rss,
|
||||
&.warning {
|
||||
&.coffee {
|
||||
--banner-accent-color: var(--brand-buy-me-a-coffee);
|
||||
}
|
||||
&.error {
|
||||
--banner-accent-color: var(--error);
|
||||
}
|
||||
&.github {
|
||||
--banner-accent-color: var(--brand-github);
|
||||
}
|
||||
&.npm {
|
||||
--banner-accent-color: var(--brand-npm);
|
||||
}
|
||||
&.old-post {
|
||||
--banner-accent-color: var(--gray-dark);
|
||||
}
|
||||
&.rss {
|
||||
--banner-accent-color: var(--brand-rss);
|
||||
}
|
||||
&.warning {
|
||||
--banner-accent-color: var(--warning);
|
||||
}
|
||||
|
||||
border-color: var(--banner-accent-color);
|
||||
|
||||
& p a:is(:hover, :active, :focus) {
|
||||
color: var(--banner-accent-color);
|
||||
}
|
||||
|
||||
& svg {
|
||||
stroke: var(--banner-accent-color);
|
||||
}
|
||||
}
|
||||
}
|
28
src/styles/components/buttons.css
Normal file
28
src/styles/components/buttons.css
Normal file
|
@ -0,0 +1,28 @@
|
|||
@import url("./tab-buttons.css");
|
||||
@import url("./text-toggle.css");
|
||||
|
||||
button,
|
||||
.button {
|
||||
appearance: none;
|
||||
border: none;
|
||||
border: 2px solid var(--accent-color);
|
||||
border-radius: var(--border-radius-full);
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--line-height-base);
|
||||
white-space: nowrap;
|
||||
color: var(--text-color-inverted);
|
||||
background-color: var(--accent-color);
|
||||
transition: color var(--transition-duration-default)
|
||||
var(--transition-ease-in-out);
|
||||
|
||||
&:not(.active):is(:hover, :active, :focus, :focus-within) {
|
||||
background-color: var(--accent-color-hover);
|
||||
border: 2px solid var(--accent-color-hover);
|
||||
transition: background-color var(--transition-duration-default)
|
||||
var(--transition-ease-in-out),
|
||||
border var(--transition-duration-default) var(--transition-ease-in-out),
|
||||
color var(--transition-duration-default) var(--transition-ease-in-out);
|
||||
}
|
||||
}
|
65
src/styles/components/forms.css
Normal file
65
src/styles/components/forms.css
Normal file
|
@ -0,0 +1,65 @@
|
|||
::placeholder {
|
||||
color: var(--text-color);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
input:not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="checkbox"]),
|
||||
textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input:not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="checkbox"]),
|
||||
textarea,
|
||||
select {
|
||||
color: var(--text-color);
|
||||
border-radius: var(--border-radius-slight);
|
||||
background-color: var(--background-color);
|
||||
padding: var(--spacing-sm);
|
||||
border: var(--border-gray);
|
||||
}
|
||||
|
||||
form,
|
||||
input:not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="checkbox"]),
|
||||
textarea {
|
||||
margin-bottom: var(--spacing-base);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.search__form {
|
||||
margin-top: 0;
|
||||
|
||||
& .search__form--input::-webkit-search-cancel-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.search__form--type {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
margin-top: var(--sizing-md);
|
||||
border: none;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.search__results {
|
||||
margin: 0 0 var(--spacing-base);
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: none;
|
||||
|
||||
& li {
|
||||
margin: var(--spacing-sm) 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: var(--sizing-base);
|
||||
border-bottom: var(--border-gray);
|
||||
}
|
||||
}
|
||||
}
|
22
src/styles/components/mastodon-post.css
Normal file
22
src/styles/components/mastodon-post.css
Normal file
|
@ -0,0 +1,22 @@
|
|||
mastodon-post {
|
||||
width: 100%;
|
||||
|
||||
.mastodon-post-wrapper {
|
||||
& dl,
|
||||
& dt {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
& dl {
|
||||
align-items: center;
|
||||
|
||||
& dd {
|
||||
margin-left: var(--spacing-xs);
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: var(--spacing-lg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
67
src/styles/components/media-grid.css
Normal file
67
src/styles/components/media-grid.css
Normal file
|
@ -0,0 +1,67 @@
|
|||
.media-grid {
|
||||
display: grid;
|
||||
gap: var(--spacing-sm);
|
||||
|
||||
& ~ .pagination {
|
||||
margin-top: var(--spacing-base);
|
||||
}
|
||||
|
||||
&.poster {
|
||||
grid-template-columns: var(--grid-poster);
|
||||
|
||||
& a {
|
||||
aspect-ratio: var(--aspect-ratio-banner);
|
||||
}
|
||||
}
|
||||
|
||||
&.square {
|
||||
grid-template-columns: var(--grid-square);
|
||||
|
||||
& a {
|
||||
aspect-ratio: var(--aspect-ratio-square);
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
grid-template-columns: var(--grid-vertical);
|
||||
|
||||
& a {
|
||||
aspect-ratio: var(--aspect-ratio-vertical);
|
||||
}
|
||||
}
|
||||
|
||||
&:is(.poster, .square, .vertical) img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
& .item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
& .meta-text {
|
||||
color: var(--color-lightest);
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
padding: 0 var(--spacing-sm);
|
||||
bottom: var(--spacing-sm);
|
||||
|
||||
& .header,
|
||||
& .subheader {
|
||||
color: var(--color-lightest);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-md);
|
||||
text-shadow: var(--text-shadow-default);
|
||||
}
|
||||
|
||||
& .header {
|
||||
font-weight: var(--font-weight-bold);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 6;
|
||||
line-clamp: 6;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
136
src/styles/components/menu.css
Normal file
136
src/styles/components/menu.css
Normal file
|
@ -0,0 +1,136 @@
|
|||
menu {
|
||||
& .menu-primary {
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
top: calc(var(--spacing-3xl) * 1.75);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 3;
|
||||
|
||||
& > li {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: var(--spacing-sm) 0;
|
||||
width: 100%;
|
||||
background: var(--background-color);
|
||||
|
||||
& a,
|
||||
& .active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
& a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
& .active {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:is(.icon, .active) > svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:is(.icon, .active) > span {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#menu-toggle {
|
||||
display: none;
|
||||
|
||||
&:checked + .menu-button-container {
|
||||
& .menu-closed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .menu-open {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:checked) + .menu-button-container {
|
||||
& .menu-closed {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& .menu-open {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& ~ .menu-primary li {
|
||||
height: 0;
|
||||
padding: 0;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
&:checked ~ .menu-primary li {
|
||||
height: calc(var(--sizing-3xl) * 1.5);
|
||||
|
||||
@media (max-width: 767px) {
|
||||
border-bottom: var(--border-gray);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
@media (max-width: 767px) {
|
||||
border-top: var(--border-gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-button-container {
|
||||
display: unset;
|
||||
|
||||
& svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.menu-primary {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0;
|
||||
gap: var(--spacing-md);
|
||||
position: relative;
|
||||
top: unset;
|
||||
left: unset;
|
||||
width: auto;
|
||||
|
||||
& > li {
|
||||
background: none;
|
||||
|
||||
& a {
|
||||
width: var(--sizing-svg-base);
|
||||
height: var(--sizing-svg-base);
|
||||
}
|
||||
|
||||
:is(.icon, .active) > svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:is(.icon, .active) > span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#menu-toggle ~ .menu-primary li,
|
||||
#menu-toggle:checked ~ .menu-primary li {
|
||||
height: unset;
|
||||
}
|
||||
|
||||
.menu-button-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
63
src/styles/components/modal.css
Normal file
63
src/styles/components/modal.css
Normal file
|
@ -0,0 +1,63 @@
|
|||
.modal-wrapper,
|
||||
.modal-body {
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.modal-wrapper {
|
||||
background: var(--modal-overlay-background);
|
||||
z-index: 3;
|
||||
|
||||
.modal-body {
|
||||
background: var(--background-color);
|
||||
padding: var(--spacing-lg) var(--spacing-base);
|
||||
overflow-y: auto;
|
||||
border-radius: var(--border-radius-slight);
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
max-width: 75%;
|
||||
max-height: 75%;
|
||||
inset: 12.5%;
|
||||
border: var(--border-gray);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-input {
|
||||
display: none;
|
||||
|
||||
&:checked ~ .modal-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:not(:checked) ~ .modal-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-toggle,
|
||||
.modal-close {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
|
||||
svg {
|
||||
stroke: var(--accent-color);
|
||||
|
||||
&:is(:hover, :focus, :active) {
|
||||
stroke: var(--accent-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
103
src/styles/components/music-chart.css
Normal file
103
src/styles/components/music-chart.css
Normal file
|
@ -0,0 +1,103 @@
|
|||
.music-chart {
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
|
||||
& ol {
|
||||
padding-left: 0;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
list-style-position: outside;
|
||||
}
|
||||
}
|
||||
|
||||
& .item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
& .progress-bar-wrapper {
|
||||
max-width: 40%;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
|
||||
& img {
|
||||
width: calc(var(--sizing-3xl) * 1.5);
|
||||
height: calc(var(--sizing-3xl) * 1.5);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
width: calc(var(--sizing-3xl) * 2);
|
||||
height: calc(var(--sizing-3xl) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
& .info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: calc(75% - var(--sizing-lg));
|
||||
}
|
||||
}
|
||||
|
||||
& .meta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
width: calc(75% - var(--sizing-lg));
|
||||
}
|
||||
}
|
||||
|
||||
& .meta-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
gap: var(--spacing-xs);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: 85%;
|
||||
}
|
||||
}
|
||||
|
||||
& .title {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
& .title,
|
||||
& .subtext,
|
||||
& time {
|
||||
line-height: var(--line-height-md);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
& .subtext,
|
||||
& time {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
& time {
|
||||
margin-top: var(--spacing-sm);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
src/styles/components/paginator.css
Normal file
25
src/styles/components/paginator.css
Normal file
|
@ -0,0 +1,25 @@
|
|||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-base);
|
||||
|
||||
& button {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& a {
|
||||
display: flex;
|
||||
|
||||
&.disabled svg[data-tablericon-name^="arrow-"] {
|
||||
cursor: not-allowed;
|
||||
stroke: var(--gray-medium);
|
||||
stroke-width: var(--stroke-width-default);
|
||||
}
|
||||
}
|
||||
|
||||
& p {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
13
src/styles/components/progress-bar.css
Normal file
13
src/styles/components/progress-bar.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
.progress-bar-wrapper {
|
||||
display: flex;
|
||||
height: var(--sizing-lg);
|
||||
width: 100%;
|
||||
background-color: var(--gray-light);
|
||||
border-radius: var(--border-radius-full);
|
||||
overflow: hidden;
|
||||
|
||||
& .progress-bar {
|
||||
background-color: var(--accent-color);
|
||||
border-radius: var(--border-radius-full);
|
||||
}
|
||||
}
|
38
src/styles/components/tab-buttons.css
Normal file
38
src/styles/components/tab-buttons.css
Normal file
|
@ -0,0 +1,38 @@
|
|||
#tracks-recent,
|
||||
#tracks-chart,
|
||||
.tracks-recent,
|
||||
.tracks-chart {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#tracks-recent:checked ~ .tracks-recent,
|
||||
#tracks-chart:checked ~ .tracks-chart {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input[id="tracks-recent"] ~ .tracks-recent,
|
||||
input[id="tracks-chart"] ~ .tracks-chart {
|
||||
margin-top: var(--spacing-base);
|
||||
}
|
||||
|
||||
[for="tracks-recent"] {
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
#tracks-recent:checked ~ [for="tracks-recent"],
|
||||
#tracks-chart:checked ~ [for="tracks-chart"] {
|
||||
cursor: not-allowed;
|
||||
border-color: var(--accent-color);
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
|
||||
#tracks-recent:not(:checked) ~ [for="tracks-recent"],
|
||||
#tracks-chart:not(:checked) ~ [for="tracks-chart"] {
|
||||
color: var(--accent-color);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#tracks-recent:not(:checked) ~ [for="tracks-recent"]:is(:hover, :active),
|
||||
#tracks-chart:not(:checked) ~ [for="tracks-chart"]:is(:hover, :active) {
|
||||
color: var(--accent-color-hover);
|
||||
}
|
23
src/styles/components/text-toggle.css
Normal file
23
src/styles/components/text-toggle.css
Normal file
|
@ -0,0 +1,23 @@
|
|||
[data-toggle-content] {
|
||||
&.text-toggle-hidden {
|
||||
position: relative;
|
||||
height: 500px;
|
||||
overflow: hidden;
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
|
||||
& p:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
content: "";
|
||||
box-shadow: var(--box-shadow-text-toggle);
|
||||
width: 100%;
|
||||
height: 20%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
15
src/styles/components/youtube-player.css
Normal file
15
src/styles/components/youtube-player.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
youtube-video {
|
||||
aspect-ratio: 16/9;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border: var(--border-default);
|
||||
border-radius: var(--border-radius-slight);
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
|
||||
&:hover {
|
||||
border: var(--border-default-hover);
|
||||
transition: border var(--transition-duration-default)
|
||||
var(--transition-ease-in-out);
|
||||
}
|
||||
}
|
81
src/styles/feed.liquid
Normal file
81
src/styles/feed.liquid
Normal file
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
permalink: /assets/styles/feed.xsl
|
||||
---
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes" />
|
||||
<xsl:template match="/">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||
<head>
|
||||
<title>
|
||||
<xsl:value-of select="/rss/channel/title" /> / {{ globals.site_name }}
|
||||
</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="{{ globals.theme_color }}" />
|
||||
<meta name="fediverse:creator" content="{{ globals.mastodon }}" />
|
||||
<meta name="generator" content="Eleventy" />
|
||||
<meta name="robots" content="noai, noimageai" />
|
||||
<link rel="preload" href="/assets/fonts/ml.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/assets/fonts/mlb.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="/assets/styles/index.css?v={% appVersion %}" type="text/css" />
|
||||
</head>
|
||||
<body class="feed">
|
||||
<div class="main-wrapper">
|
||||
<main>
|
||||
{% render "header.liquid", globals:globals, page:page, nav:nav %}
|
||||
<div class="default-wrapper">
|
||||
<h2 class="page-title">
|
||||
<xsl:value-of select="/rss/channel/title" />
|
||||
</h2>
|
||||
<article class="intro">
|
||||
<p>
|
||||
<xsl:value-of select="/rss/channel/description" />
|
||||
</p>
|
||||
<p>
|
||||
<strong class="highlight-text">Subscribe by adding the URL below to your feed reader
|
||||
of choice.</strong>
|
||||
</p>
|
||||
<p>
|
||||
<pre class="small">
|
||||
<code><xsl:value-of select="rss/channel/atom:link/@href"/></code>
|
||||
</pre>
|
||||
</p>
|
||||
<p>
|
||||
<a href="/feeds">View more of the feeds from my site.</a>
|
||||
</p>
|
||||
</article>
|
||||
<section>
|
||||
<xsl:for-each select="/rss/channel/item">
|
||||
<article>
|
||||
<time>Published: <xsl:value-of select="pubDate" /></time>
|
||||
<h3>
|
||||
<a>
|
||||
<xsl:attribute name="href">
|
||||
<xsl:value-of select="link" />
|
||||
</xsl:attribute>
|
||||
<xsl:value-of select="title" />
|
||||
</a>
|
||||
</h3>
|
||||
<xsl:value-of select="description" disable-output-escaping="yes" />
|
||||
<xsl:if test="enclosure">
|
||||
<img class="image-banner" src="{enclosure/@url}" alt="{title}" />
|
||||
</xsl:if>
|
||||
</article>
|
||||
</xsl:for-each>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
{% render "footer.liquid",
|
||||
page:page,
|
||||
nav:nav,
|
||||
updated:updated,
|
||||
pageUpdated:page.updated
|
||||
%}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
39
src/styles/index.css
Normal file
39
src/styles/index.css
Normal file
|
@ -0,0 +1,39 @@
|
|||
@layer reset, defaults, base, page, components, plugins;
|
||||
|
||||
/* style resets */
|
||||
@import url("./base/reset.css") layer(reset);
|
||||
|
||||
/* core defaults */
|
||||
@import url("./base/fonts.css") layer(defaults);
|
||||
@import url("./base/vars.css") layer(defaults);
|
||||
|
||||
/* base styles */
|
||||
@import url("./base/index.css") layer(base);
|
||||
|
||||
/* plugins */
|
||||
@import url("./plugins/prism.css") layer(plugins);
|
||||
|
||||
/* page styles */
|
||||
@import url("./pages/about.css") layer(page);
|
||||
@import url("./pages/books.css") layer(page);
|
||||
@import url("./pages/contact.css") layer(page);
|
||||
@import url("./pages/links.css") layer(page);
|
||||
@import url("./pages/media.css") layer(page);
|
||||
@import url("./pages/music.css") layer(page);
|
||||
@import url("./pages/watching.css") layer(page);
|
||||
@import url("./pages/webrings.css") layer(page);
|
||||
|
||||
/* component styles */
|
||||
@import url("./components/addon-links.css") layer(components);
|
||||
@import url("./components/badge-grid.css") layer(components);
|
||||
@import url("./components/banners.css") layer(components);
|
||||
@import url("./components/buttons.css") layer(components);
|
||||
@import url("./components/forms.css") layer(components);
|
||||
@import url("./components/mastodon-post.css") layer(components);
|
||||
@import url("./components/media-grid.css") layer(components);
|
||||
@import url("./components/menu.css") layer(components);
|
||||
@import url("./components/modal.css") layer(components);
|
||||
@import url("./components/music-chart.css") layer(components);
|
||||
@import url("./components/paginator.css") layer(components);
|
||||
@import url("./components/progress-bar.css") layer(components);
|
||||
@import url("./components/youtube-player.css") layer(components);
|
24
src/styles/pages/about.css
Normal file
24
src/styles/pages/about.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
:root {
|
||||
--avatar-size: 16rem;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
--avatar-size: 24rem;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
& img {
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
}
|
||||
|
||||
.about-title {
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
text-align: center;
|
||||
}
|
87
src/styles/pages/books.css
Normal file
87
src/styles/pages/books.css
Normal file
|
@ -0,0 +1,87 @@
|
|||
:is(.book-entry, .book-focus) img {
|
||||
height: auto;
|
||||
aspect-ratio: var(--aspect-ratio-vertical);
|
||||
}
|
||||
|
||||
.book-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
|
||||
&:not(:last-of-type) {
|
||||
padding-bottom: var(--spacing-base);
|
||||
border-bottom: var(--border-gray);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-base);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
& img {
|
||||
max-width: calc(var(--sizing-3xl) * 4);
|
||||
}
|
||||
|
||||
& .media-meta {
|
||||
margin-top: var(--sizing-base);
|
||||
align-items: center;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
margin-top: 0;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
& .description p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
& .progress-bar-wrapper {
|
||||
max-width: 75%;
|
||||
margin-bottom: 0;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
margin-top: 0;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.book-focus {
|
||||
& .book-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-base);
|
||||
margin-bottom: var(--spacing-base);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
& img {
|
||||
border: var(--border-default);
|
||||
}
|
||||
|
||||
& .media-meta {
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
width: auto;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
& .progress-bar-wrapper {
|
||||
max-width: 50%;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
src/styles/pages/contact.css
Normal file
36
src/styles/pages/contact.css
Normal file
|
@ -0,0 +1,36 @@
|
|||
.contact-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: var(--grid-columns-one);
|
||||
gap: var(--spacing-base);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
grid-template-columns: var(--grid-columns-two);
|
||||
}
|
||||
|
||||
& .hp,
|
||||
& label > span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& textarea {
|
||||
height: calc(var(--sizing-3xl) * 5);
|
||||
}
|
||||
|
||||
& .column.description {
|
||||
& p:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contact-success-wrapper {
|
||||
text-align: center;
|
||||
|
||||
& h2 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
15
src/styles/pages/links.css
Normal file
15
src/styles/pages/links.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
.link-grid {
|
||||
display: grid;
|
||||
gap: var(--spacing-sm);
|
||||
grid-template-columns: var(--grid-columns-one);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
grid-template-columns: var(--grid-columns-two);
|
||||
}
|
||||
|
||||
& .link-box {
|
||||
border: var(--border-gray);
|
||||
border-radius: var(--border-radius-slight);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
}
|
||||
}
|
42
src/styles/pages/media.css
Normal file
42
src/styles/pages/media.css
Normal file
|
@ -0,0 +1,42 @@
|
|||
.media-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
|
||||
& .title {
|
||||
font-size: var(--font-size-xl);
|
||||
line-height: var(--line-height-md);
|
||||
}
|
||||
|
||||
& .sub-meta {
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
& svg {
|
||||
width: var(--sizing-svg-sm);
|
||||
height: var(--sizing-svg-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a:is(:hover, :active, :focus) .media-overlay::after {
|
||||
border: var(--border-default-hover);
|
||||
transition: border-color var(--transition-duration-default)
|
||||
var(--transition-ease-in-out);
|
||||
}
|
||||
|
||||
.media-overlay::after {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: var(--border-default);
|
||||
box-shadow: var(--box-shadow-media);
|
||||
border-radius: var(--border-radius-slight);
|
||||
}
|
||||
|
||||
.associated-media {
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue