Local metadata link commit at 2025-06-06 15:40:57 — file:///home/mrhavens/git-local-repos/git-sigil.git
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
---
|
||||
import "@fontsource/fira-sans";
|
||||
import { ViewTransitions } from 'astro:transitions';
|
||||
import { SITE } from "$/config";
|
||||
import "../styles/global.css";
|
||||
|
||||
export type Props = {
|
||||
title: string;
|
||||
description: string;
|
||||
permalink: string;
|
||||
image: string;
|
||||
};
|
||||
|
||||
const { title = SITE.title, description, permalink, image } = Astro.props;
|
||||
---
|
||||
|
||||
<!-- Use Google Fonts, if you don't wanna prefer a self-hosted version --><!-- <link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap" rel="stylesheet"> -->
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title} />
|
||||
{description && <meta name="description" content={description} />}
|
||||
<ViewTransitions />
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="msapplication-config" content="/browserconfig.xml" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
|
||||
<!-- Open Graph Tags (Facebook) -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={title} />
|
||||
{permalink && <meta property="og:url" content={permalink} />}
|
||||
{description && <meta property="og:description" content={description} />}
|
||||
{image && <meta property="og:image" content={image} />}
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:title" content={title} />
|
||||
{permalink && <meta property="twitter:url" content={permalink} />}
|
||||
{description && <meta property="twitter:description" content={description} />}
|
||||
{image && <meta property="twitter:image" content={image} />}
|
||||
|
||||
<script is:inline>
|
||||
const theme = (() => {
|
||||
if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
|
||||
return localStorage.getItem("theme");
|
||||
}
|
||||
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
return "dark";
|
||||
}
|
||||
return "light";
|
||||
})();
|
||||
|
||||
if (theme === "light") {
|
||||
document.documentElement.classList.remove("dark");
|
||||
} else {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,8 @@
|
||||
<body class="font-sans antialiased min-h-screen bg-gray-100 dark:bg-gray-800">
|
||||
<svg class="absolute w-full fill-theme-primary dark:fill-theme-dark-primary opacity-10 -z-10" viewBox="0 0 960 540" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><path d="M0 81L26.7 86.5C53.3 92 106.7 103 160 99.3C213.3 95.7 266.7 77.3 320 66.3C373.3 55.3 426.7 51.7 480 49.7C533.3 47.7 586.7 47.3 640 45.2C693.3 43 746.7 39 800 51C853.3 63 906.7 91 933.3 105L960 119L960 0L933.3 0C906.7 0 853.3 0 800 0C746.7 0 693.3 0 640 0C586.7 0 533.3 0 480 0C426.7 0 373.3 0 320 0C266.7 0 213.3 0 160 0C106.7 0 53.3 0 26.7 0L0 0Z" stroke-linecap="round" stroke-linejoin="miter"></path></svg>
|
||||
<div class="transition-colors">
|
||||
<main class="mx-auto max-w-4xl px-4 md:px-0">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon'
|
||||
export type CalloutType = 'check' | 'error' | 'note' | 'warning'
|
||||
interface Props {
|
||||
title: string
|
||||
type: CalloutType
|
||||
}
|
||||
|
||||
const ICON_MAP: Record<CalloutType, string> = {
|
||||
'check': 'check-circle',
|
||||
'error': 'close-circle',
|
||||
'note': 'note',
|
||||
'warning': 'warning-circle'
|
||||
}
|
||||
|
||||
const COLOR_MAP: Record<CalloutType, string> = {
|
||||
'check': 'text-green-700',
|
||||
'error': 'text-red-700',
|
||||
'note': ' text-gray-700',
|
||||
'warning': 'text-orange-700'
|
||||
}
|
||||
|
||||
const { title, type = 'note' } = Astro.props
|
||||
---
|
||||
<div class="callout flex gap-2 w-full bg-gray-50 my-1 px-5 py-2 rounded-sm shadow-sm">
|
||||
<Icon class={`w-8 h-8 inline-block ${COLOR_MAP[type]}`} pack="mdi" name={ICON_MAP[type]} />
|
||||
<div class="copy flex flex-col">
|
||||
<h3 class={`title m-0 ${COLOR_MAP[type]}`}>{title}</h3>
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import CodeCopy from './CodeCopy.svelte'
|
||||
---
|
||||
<CodeCopy client:load/>
|
||||
<code class="astro-ink__code">
|
||||
<slot/>
|
||||
</code>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { fade, blur } from "svelte/transition";
|
||||
import { onMount } from "svelte";
|
||||
const COPIED_TIMEOUT = 2 * 1000
|
||||
|
||||
export let stayCopied = COPIED_TIMEOUT
|
||||
|
||||
let copied = false
|
||||
let selfElm: HTMLButtonElement;
|
||||
let isCodeBlock = true;
|
||||
|
||||
const copy = async () => {
|
||||
if(selfElm) {
|
||||
const preElm = selfElm.parentElement?.parentElement
|
||||
const codeElm = preElm?.querySelector('code')
|
||||
if(preElm?.tagName === 'PRE' && codeElm) {
|
||||
await navigator.clipboard.writeText(codeElm.innerText);
|
||||
copied = true
|
||||
|
||||
setTimeout(() => {
|
||||
copied = false
|
||||
}, stayCopied);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const preElm = selfElm.parentElement?.parentElement
|
||||
if(preElm && preElm.tagName === 'PRE') {
|
||||
isCodeBlock = true
|
||||
} else {
|
||||
isCodeBlock = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<button
|
||||
bind:this={selfElm}
|
||||
on:click={copy}
|
||||
class="absolute px-2 text-theme-primary dark:text-theme-dark-primary border-1 rounded-lg"
|
||||
style="top: 6px; right: 8px;"
|
||||
style:display={isCodeBlock ? 'inline-block' : 'none'}
|
||||
disabled={copied}
|
||||
>
|
||||
{#if copied}
|
||||
<span transition:blur={{ amount: 50, opacity: 50 }}>✓Copied</span>
|
||||
{:else}
|
||||
<span transition:fade>Copy</span>
|
||||
{/if}
|
||||
</button>
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
interface Props {
|
||||
value: Date;
|
||||
title?: string;
|
||||
lang?: Intl.LocalesArgument,
|
||||
dateStyle?: "full" | "long" | "medium" | "short"
|
||||
}
|
||||
const { value, title, lang = 'en-US', dateStyle = 'full' } = Astro.props
|
||||
---
|
||||
<time title={title} datetime={value.toISOString()}>
|
||||
{value.toLocaleDateString(lang, { dateStyle, timeZone: 'UTC' })}
|
||||
</time>
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon'
|
||||
|
||||
interface Props {
|
||||
label?: string,
|
||||
editUrl: string
|
||||
}
|
||||
|
||||
const { editUrl, label = 'Edit this page' } = Astro.props
|
||||
|
||||
---
|
||||
{
|
||||
editUrl && (
|
||||
<a href={editUrl} title={label} class=" font-thin text-theme-primary dark:text-theme-dark-primary text-sm">
|
||||
<Icon class="w-4 h-4 inline-block" pack="mdi" name={'pencil'} />
|
||||
{label}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
import { SITE } from '$/config'
|
||||
import ModeLabel from './ModeLabel.svelte'
|
||||
---
|
||||
<footer class="footer">
|
||||
<nav class="nav">
|
||||
<div>2021 © Copyright notice | <a href={ SITE.githubUrl } title={`${ SITE.name }'s Github URL'`}>{ SITE.name }</a>
|
||||
<ModeLabel client:load/> theme on <a href="https://astro.build/">Astro</a></div>
|
||||
</nav>
|
||||
</footer>
|
||||
<style>
|
||||
.footer {
|
||||
@apply py-6 border-t
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
import { SITE } from '$/config'
|
||||
import SvgIcon from './SvgIcon.astro'
|
||||
import ModeSwitcherBtn from './ModeSwitcherBtn.svelte'
|
||||
import SearchBtn from './SearchBtn.svelte'
|
||||
|
||||
---
|
||||
|
||||
<header class="header">
|
||||
<div class="header__logo">
|
||||
<a href="/" class="avatar">
|
||||
<img class="header__logo-img" src="/assets/logo.svg" alt="Astro logo" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="header__meta flex-1">
|
||||
<h3 class="header__title dark:text-theme-dark-secondary">
|
||||
<a href="/">{ SITE.name }</a>
|
||||
</h3>
|
||||
<div class="header__meta-more flex">
|
||||
<p class="header__desc">
|
||||
{ SITE.description }
|
||||
</p>
|
||||
<nav class="header__nav flex">
|
||||
<ul class="header__ref-list">
|
||||
<li>
|
||||
<SearchBtn client:visible />
|
||||
</li>
|
||||
<li>
|
||||
<a href={ SITE.githubUrl } title={`${ SITE.name }'s Github URL'`}>
|
||||
<SvgIcon>
|
||||
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
|
||||
</SvgIcon>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/rss.xml" title="RSS">
|
||||
<SvgIcon>
|
||||
<path d="M4 11a9 9 0 0 1 9 9"></path>
|
||||
<path d="M4 4a16 16 0 0 1 16 16"></path>
|
||||
<circle cx="5" cy="19" r="1"></circle>
|
||||
</SvgIcon>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<ModeSwitcherBtn client:visible />
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
@apply flex gap-4 border-b py-3 /* border-gray-200 dark:border-gray-700 - check styles/global.css */
|
||||
}
|
||||
.header__logo-img {
|
||||
@apply w-16 h-16 rounded-full overflow-hidden
|
||||
}
|
||||
.header__title {
|
||||
@apply text-4xl font-extrabold md:text-5xl text-theme-secondary dark:text-theme-dark-secondary
|
||||
}
|
||||
.header__desc {
|
||||
@apply text-xl flex-1 dark:text-gray-200
|
||||
}
|
||||
.header__ref-list {
|
||||
@apply flex gap-3 text-gray-400
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
<img src="/assets/yay.svg" alt="Yay!" />
|
||||
|
||||
<style>
|
||||
img {
|
||||
@apply mx-auto w-2/3 mt-6
|
||||
}
|
||||
h1 {
|
||||
@apply w-full justify-center text-center text-3xl font-bold text-purple-600 py-10
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
import BaseLayout from './BaseLayout.astro';
|
||||
import Header from './Header.astro';
|
||||
import Footer from './Footer.astro';
|
||||
import Nav from './Nav.astro';
|
||||
import Portal from './Portal.astro';
|
||||
import SearchModal from './SearchModal.svelte'
|
||||
|
||||
---
|
||||
<BaseLayout>
|
||||
<br class="my-4"/>
|
||||
<Header/>
|
||||
<Nav/>
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
<br class="my-4"/>
|
||||
<Footer/>
|
||||
<Portal>
|
||||
<SearchModal client:load/>
|
||||
</Portal>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
min-height: 580px
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
import { getMonthName } from '$/utils'
|
||||
import { USE_MEDIA_THUMBNAIL } from '$/config'
|
||||
const { post } = Astro.props
|
||||
---
|
||||
<div class="post-preview">
|
||||
<div class="sm:w-20 md:w-32">
|
||||
<div class="post-preview__date">
|
||||
<span class="post-preview__date__day">{ new Date(post.date).getDate() }</span>
|
||||
<span class="post-preview__date__month-n-year">{ `${getMonthName(post.date)} ${new Date(post.date).getFullYear()}` }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class={`flex-1 ${USE_MEDIA_THUMBNAIL && post.thumbnail ? 'flex flex-row gap-4' : ''}`}>
|
||||
{ USE_MEDIA_THUMBNAIL && post.thumbnail && <img class="post-preview__media" src= {post.thumbnail} alt="media thumbnail" />}
|
||||
<div class="flex flex-col mb-2">
|
||||
<h4 class="post-preview__title dark:text-theme-dark-primary">
|
||||
<a href={post.url} title={post.title} target="_blank">{post.title}</a>
|
||||
</h4>
|
||||
<div>
|
||||
<strong>{post.host}</strong>
|
||||
{
|
||||
post.participants.length > 0 && <em>with</em>
|
||||
}
|
||||
{
|
||||
post.participants.length > 0 && `${post.participants.join(', ')}`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<p class="post-preview__desc">
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.post-preview {
|
||||
@apply flex gap-6
|
||||
}
|
||||
.post-preview__date {
|
||||
@apply flex flex-col w-full text-center
|
||||
}
|
||||
.post-preview__date__day {
|
||||
@apply text-6xl font-semibold text-gray-500 dark:text-gray-300
|
||||
}
|
||||
.post-preview__date__month-n-year {
|
||||
@apply text-gray-400
|
||||
}
|
||||
.post-preview__title {
|
||||
@apply text-2xl font-semibold text-theme-primary dark:text-theme-dark-primary /* this doesn't works here */ hover:underline
|
||||
}
|
||||
.post-preview__desc {
|
||||
@apply text-lg leading-6 dark:text-white line-clamp-2 hyphens-auto
|
||||
}
|
||||
.post-preview__media {
|
||||
@apply w-48 rounded-md shadow-lg shadow-theme-accent-gray-light dark:shadow-theme-accent-gray-dark
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
import MediaPreview from './MediaPreview.astro'
|
||||
const { posts } = Astro.props
|
||||
---
|
||||
<section class="media-preview__list">
|
||||
{posts.map((post) => (
|
||||
<MediaPreview post={post}/>
|
||||
))}
|
||||
</section>
|
||||
<style>
|
||||
.media-preview__list {
|
||||
@apply flex flex-col gap-12
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ModeSensitive from './ModeSensitive.svelte'
|
||||
</script>
|
||||
<ModeSensitive>
|
||||
<span slot="dark">(dark)</span>
|
||||
<span slot="light">(light)</span>
|
||||
</ModeSensitive>
|
||||
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { theme } from '../store/theme'
|
||||
</script>
|
||||
{#if $theme === 'dark'}
|
||||
<slot name="dark"/>
|
||||
{:else}
|
||||
<slot name="light"/>
|
||||
{/if}
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { theme } from '../store/theme'
|
||||
|
||||
type ThemeType = 'dark' | 'light'
|
||||
|
||||
const THEME_DARK: ThemeType = 'dark'
|
||||
const THEME_LIGHT: ThemeType = 'light'
|
||||
let currTheme: ThemeType = THEME_DARK
|
||||
|
||||
|
||||
function toggleTheme() {
|
||||
window.document.documentElement.classList.toggle(THEME_DARK)
|
||||
currTheme = localStorage.getItem('theme') === THEME_DARK ? THEME_LIGHT : THEME_DARK
|
||||
// Update Storage
|
||||
localStorage.setItem('theme', currTheme)
|
||||
// Update Store
|
||||
theme.set(currTheme)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (localStorage.getItem('theme') === THEME_DARK || (!('theme' in localStorage) && window.matchMedia(`(prefers-color-scheme: ${THEME_DARK})`).matches)) {
|
||||
window.document.documentElement.classList.add(THEME_DARK)
|
||||
currTheme = THEME_DARK
|
||||
} else {
|
||||
window.document.documentElement.classList.remove(THEME_DARK)
|
||||
currTheme = THEME_LIGHT
|
||||
}
|
||||
// Update Store
|
||||
theme.set(currTheme)
|
||||
})
|
||||
</script>
|
||||
<button on:click={toggleTheme}>
|
||||
<slot theme={currTheme}/>
|
||||
</button>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
|
||||
import { draw } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import ModeSwitcher from './ModeSwitcher.svelte'
|
||||
import SvgIcon from './SvgIcon.svelte'
|
||||
</script>
|
||||
<ModeSwitcher let:theme>
|
||||
<SvgIcon>
|
||||
{#if theme === 'dark'}
|
||||
<circle cx="12" cy="12" r="5" transition:draw={{ duration: 1000, delay: 200, easing: quintOut }}></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3" transition:draw={{ duration: 100, delay: 30, easing: quintOut }}></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23" transition:draw={{ duration: 100, delay: 40, easing: quintOut }}></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" transition:draw={{ duration: 100, delay: 50, easing: quintOut }}></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" transition:draw={{ duration: 100, delay: 60, easing: quintOut }}></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12" transition:draw={{ duration: 100, delay: 70, easing: quintOut }}></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12" transition:draw={{ duration: 100, delay: 80, easing: quintOut }}></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" transition:draw={{ duration: 100, delay: 90, easing: quintOut }}></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" transition:draw={{ duration: 100, delay: 100, easing: quintOut }}></line>
|
||||
{:else}
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" transition:draw={{ duration: 500, delay: 100, easing: quintOut }}></path>
|
||||
{/if}
|
||||
</SvgIcon>
|
||||
</ModeSwitcher>
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import { toTitleCase } from '$/utils'
|
||||
import { NAV_ITEMS } from '$/config'
|
||||
---
|
||||
<nav class="nav py-3">
|
||||
<ul class="nav-list dark:text-theme-dark-secondary" transition:animate="fade">
|
||||
{
|
||||
Object.keys(NAV_ITEMS).map(navItemKey => <li>
|
||||
<a class:list={[
|
||||
`pb-1 border-b-2 hover:border-gray-400 hover:dark:border-gray-700 `,
|
||||
Astro.url.pathname !== NAV_ITEMS[navItemKey].path ? 'border-gray-100 dark:border-gray-800': '',
|
||||
Astro.url.pathname === NAV_ITEMS[navItemKey].path ? 'border-theme-primary' : ''
|
||||
]} href={NAV_ITEMS[navItemKey].path} title={NAV_ITEMS[navItemKey].title}>{toTitleCase(NAV_ITEMS[navItemKey].title)}</a>
|
||||
</li>)
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
<style>
|
||||
.nav-list {
|
||||
@apply inline-flex list-none gap-8 text-xl font-semibold text-theme-secondary dark:text-theme-dark-secondary py-2 flex-wrap mb-8
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
const { page } = Astro.props
|
||||
---
|
||||
<div class="page__actions">
|
||||
{page.url.prev && <a class="action__go-to-x" href={page.url.prev} title="Go to Previous">← Prev</a>}
|
||||
{page.url.next && <a class="action__go-to-x" href={page.url.next} title="Go to Next">Next →</a>}
|
||||
</div>
|
||||
<style>
|
||||
.page__actions {
|
||||
@apply flex justify-center md:justify-end py-6 gap-2
|
||||
}
|
||||
.action__go-to-x {
|
||||
@apply text-base uppercase text-gray-500 dark:text-gray-400 hover:underline
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="portal-root">
|
||||
<slot/>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
import { getMonthName, getSlugFromPathname } from '$/utils'
|
||||
const { frontmatter: post, file } = Astro.props.post
|
||||
---
|
||||
<div class="post-draft-preview">
|
||||
<div class="sm:w-20 md:w-32">
|
||||
<div class="post-draft-preview__date">
|
||||
<span class="post-draft-preview__date__day">{ new Date(post.date).getDate() }</span>
|
||||
<span class="post-draft-preview__date__month-n-year">{ `${getMonthName(post.date)} ${new Date(post.date).getFullYear()}` }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="post-draft-preview__title dark:text-theme-dark-primary">
|
||||
<a href={`/drafts/${getSlugFromPathname(file)}`} title={post.title}>{post.title}</a>
|
||||
</h4>
|
||||
<p class="post-draft-preview__desc">
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.post-draft-preview {
|
||||
@apply flex gap-6
|
||||
}
|
||||
.post-draft-preview__date {
|
||||
@apply flex flex-col w-full text-center
|
||||
}
|
||||
.post-draft-preview__date__day {
|
||||
@apply text-6xl font-semibold text-gray-500 dark:text-gray-300
|
||||
}
|
||||
.post-draft-preview__date__month-n-year {
|
||||
@apply text-gray-400
|
||||
}
|
||||
.post-draft-preview__title {
|
||||
@apply text-2xl font-semibold text-theme-primary dark:text-theme-dark-primary /* this doesn't works here */ hover:underline mb-2
|
||||
}
|
||||
.post-draft-preview__desc {
|
||||
@apply text-lg leading-6 dark:text-white line-clamp-2 hyphens-auto
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
import PostDraftPreview from './PostDraftPreview.astro'
|
||||
const { posts } = Astro.props
|
||||
---
|
||||
<section class="post-draft-preview__list">
|
||||
{posts.map((post) => (
|
||||
<PostDraftPreview post={post}/>
|
||||
))}
|
||||
</section>
|
||||
<style>
|
||||
.post-draft-preview__list {
|
||||
@apply flex flex-col gap-12
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
import { getMonthName } from '$/utils'
|
||||
|
||||
interface Props {
|
||||
post: CollectionEntry<'blog'>,
|
||||
asCard?: boolean
|
||||
}
|
||||
|
||||
const { post: { data: post, slug }, asCard = false } = Astro.props
|
||||
---
|
||||
<div class={`post-preview ${asCard && 'post-preview--card'}`}>
|
||||
<div class="post-preview__date-box">
|
||||
<div class="post-preview__date">
|
||||
<span class="post-preview__date__day">{ new Date(post.date).getDate() }</span>
|
||||
<span class="post-preview__date__month-n-year">{ `${getMonthName(post.date)} ${new Date(post.date).getFullYear()}` }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="post-preview__title dark:text-theme-dark-primary">
|
||||
<a href={`/blog/${slug}`} title={post.title}>{post.title}</a>
|
||||
</h4>
|
||||
<p class="post-preview__desc">
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.post-preview {
|
||||
@apply flex gap-6;
|
||||
}
|
||||
.post-preview--card {
|
||||
@apply flex flex-col-reverse gap-4 sm:w-72 md:w-60 lg:w-64;
|
||||
}
|
||||
.post-preview__date-box {
|
||||
@apply sm:w-20 md:w-32
|
||||
}
|
||||
.post-preview--card .post-preview__date-box {
|
||||
@apply w-full
|
||||
}
|
||||
.post-preview__date {
|
||||
@apply flex flex-col w-full text-center;
|
||||
}
|
||||
.post-preview--card .post-preview__date {
|
||||
@apply text-left flex flex-row gap-1
|
||||
}
|
||||
.post-preview__date__day {
|
||||
@apply text-6xl font-semibold text-gray-500 dark:text-gray-300;
|
||||
}
|
||||
.post-preview--card .post-preview__date__day {
|
||||
@apply text-4xl
|
||||
}
|
||||
.post-preview__date__month-n-year {
|
||||
@apply text-gray-400;
|
||||
}
|
||||
.post-preview__title {
|
||||
@apply text-2xl font-semibold text-theme-primary dark:text-theme-dark-primary /* this doesn't works here */ hover:underline mb-2;
|
||||
}
|
||||
.post-preview__desc {
|
||||
@apply text-lg leading-6 line-clamp-2 dark:text-white hyphens-auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import PostPreview from './PostPreview.astro'
|
||||
|
||||
interface Props {
|
||||
posts: CollectionEntry<'blog'>
|
||||
heading?: string
|
||||
mode?: 'row' | 'col'
|
||||
}
|
||||
|
||||
const { posts, heading, mode = 'col' } = Astro.props
|
||||
const sortedPosts = posts.sort((a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf());
|
||||
---
|
||||
{ heading ? <h5 class={`post-preview__heading post-preview__heading--${mode} ink-h`}>{heading}</h5> : ''}
|
||||
<section class={`post-preview__list post-preview__list--${mode}`}>
|
||||
{sortedPosts.map((post) => (
|
||||
<PostPreview post={post} asCard={mode === 'row' ? true : false }/>
|
||||
))}
|
||||
</section>
|
||||
<style>
|
||||
.post-preview__heading {
|
||||
@apply pl-0 sm:pl-6
|
||||
}
|
||||
.post-preview__heading--row {
|
||||
@apply pl-0
|
||||
}
|
||||
.post-preview__list {
|
||||
@apply flex flex-col gap-12
|
||||
}
|
||||
.post-preview__list--row {
|
||||
@apply flex-row flex-wrap md:px-6 lg:px-0 gap-12 md:gap-8 sm:gap-10 lg:gap-12
|
||||
}
|
||||
.post-preview__list--col {
|
||||
@apply flex-col gap-12
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
type Props = {
|
||||
slug: string
|
||||
title: string
|
||||
description: string
|
||||
category: string,
|
||||
tags: Array<string>
|
||||
}
|
||||
export let post: Props
|
||||
export let isLast: boolean = false
|
||||
</script>
|
||||
<div class="post-preview hover:bg-theme-primary">
|
||||
<div class="flex-1">
|
||||
<h4 class="post-preview__title">
|
||||
<a href={`/${post.category}/${post.slug}`} title={post.title}>{post.title} →</a>
|
||||
</h4>
|
||||
<p class="post-preview__desc">
|
||||
{post.description}
|
||||
</p>
|
||||
<ul class="tag-list">
|
||||
{#each post.tags as tag}
|
||||
<a class="tag" href={`/tags/${tag}`} title={tag}>{tag}</a>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{#if !isLast}
|
||||
<hr class="my-4 text-theme-dark-secondary"/>
|
||||
{/if}
|
||||
<style lang="postcss">
|
||||
.post-preview {
|
||||
@apply flex gap-6 text-left;
|
||||
}
|
||||
.post-preview__title {
|
||||
@apply text-lg leading-tight font-semibold text-white mb-2;
|
||||
}
|
||||
.post-preview__desc {
|
||||
@apply text-base text-theme-dark-primary leading-5 line-clamp-2;
|
||||
}
|
||||
.tag-list {
|
||||
@apply list-none py-2 flex flex-wrap gap-2;
|
||||
}
|
||||
.tag {
|
||||
@apply inline-block text-xs px-4 py-1 rounded-full text-theme-primary bg-theme-dark-primary;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
|
||||
export let slug: string = ''
|
||||
|
||||
let loading: boolean = false
|
||||
let views: number = 0
|
||||
|
||||
onMount(async () => {
|
||||
if(slug && slug.trim() !== '') {
|
||||
try {
|
||||
loading = true
|
||||
const resp = await fetch(`/api/blog/views/${slug}.json`)
|
||||
const stats = await resp.json()
|
||||
views = stats.views
|
||||
} catch(e) {
|
||||
console.error('PostStats', e)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<span class="post-stats__views">{ views } views</span>
|
||||
<style>
|
||||
.post-stats__views {
|
||||
@apply px-1 mx-1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<article class="prose dark:prose-invert">
|
||||
<slot />
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.prose {
|
||||
@apply max-w-none
|
||||
/* Size Modifiers: https://github.com/tailwindlabs/tailwindcss-typography#size-modifiers */
|
||||
/* Color Themes: https://github.com/tailwindlabs/tailwindcss-typography#color-modifiers */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import SearchIcon from './SearchIcon.svelte'
|
||||
import PostSearchPreview from './PostSearchPreview.svelte'
|
||||
|
||||
let searchInput
|
||||
let searchableDocs
|
||||
let searchIndex
|
||||
|
||||
let searchQuery = ''
|
||||
let searchResults = []
|
||||
|
||||
onMount(async() => {
|
||||
const lunr = (await import('lunr')).default
|
||||
const resp = await fetch('/search-index.json')
|
||||
searchableDocs = await resp.json()
|
||||
// Initialize indexing
|
||||
searchIndex = lunr(function(){
|
||||
// the match key...
|
||||
this.ref('slug')
|
||||
|
||||
// indexable properties
|
||||
this.field('title')
|
||||
this.field('description')
|
||||
this.field('tags')
|
||||
|
||||
// Omit, if you don't want to search on `body`
|
||||
this.field('body')
|
||||
|
||||
// Index every document
|
||||
searchableDocs.forEach(doc => {
|
||||
this.add(doc)
|
||||
}, this)
|
||||
})
|
||||
searchInput.focus()
|
||||
})
|
||||
|
||||
$: {
|
||||
if(searchQuery && searchQuery.length >= 3) {
|
||||
const matches = searchIndex.search(searchQuery)
|
||||
searchResults = []
|
||||
matches.map(match => {
|
||||
searchableDocs.filter(doc => {
|
||||
if(match.ref === doc.slug) {
|
||||
searchResults.push(doc)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<div class="search">
|
||||
<div class="search__ctrl">
|
||||
<label for="search"><SearchIcon found={searchResults.length > 0} /></label>
|
||||
<input type="text" name="search" bind:this={searchInput} placeholder="What are you looking for?" bind:value={searchQuery} />
|
||||
</div>
|
||||
<div class="search__results">
|
||||
{#if searchResults.length}
|
||||
{#each searchResults as post, i }
|
||||
<PostSearchPreview post={post} isLast={ i === searchResults.length - 1 } />
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="search__results--none">
|
||||
{#if searchQuery.length}
|
||||
No matching items found!
|
||||
{:else}
|
||||
Search something and let me find it for you! :-)
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="note"><small>click anywhere outside to close</small></div>
|
||||
</div>
|
||||
<style>
|
||||
.search {
|
||||
@apply w-full relative bg-theme-primary p-8 rounded-md shadow-lg;
|
||||
}
|
||||
input {
|
||||
@apply w-full px-4 py-2 pl-10 text-xl font-semibold text-gray-600 border-0 shadow-inner rounded-md bg-gray-100 placeholder-theme-dark-secondary;
|
||||
}
|
||||
.search__ctrl {
|
||||
@apply pb-4 relative;
|
||||
}
|
||||
.search__ctrl label {
|
||||
@apply text-theme-primary absolute top-2 left-2;
|
||||
}
|
||||
.search__results {
|
||||
@apply w-96 h-64 py-4 overflow-y-auto;
|
||||
}
|
||||
.search__results--none {
|
||||
@apply text-center text-theme-dark-primary;
|
||||
}
|
||||
.note {
|
||||
@apply w-full text-center text-white;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import SearchIcon from './SearchIcon.svelte'
|
||||
import { isSearchVisible } from '../store/search'
|
||||
|
||||
function showSearchDialog() {
|
||||
isSearchVisible.set(true)
|
||||
}
|
||||
</script>
|
||||
<button on:click={showSearchDialog}>
|
||||
<SearchIcon />
|
||||
</button>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import SvgIcon from './SvgIcon.svelte'
|
||||
export let found:boolean = false
|
||||
</script>
|
||||
<SvgIcon>
|
||||
{#if found}
|
||||
<path
|
||||
d="M7.66542 10.2366L9.19751 8.951L10.4831 10.4831L13.5473 7.91194L14.8328 9.44402L10.2366 13.3007L7.66542 10.2366Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
{/if}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M16.2071 4.89344C19.0923 7.77862 19.3131 12.3193 16.8693 15.4578C16.8846 15.4713 16.8996 15.4854 16.9143 15.5L21.1569 19.7427C21.5474 20.1332 21.5474 20.7664 21.1569 21.1569C20.7664 21.5474 20.1332 21.5474 19.7427 21.1569L15.5 16.9143C15.4854 16.8996 15.4713 16.8846 15.4578 16.8693C12.3193 19.3131 7.77862 19.0923 4.89344 16.2071C1.76924 13.083 1.76924 8.01763 4.89344 4.89344C8.01763 1.76924 13.083 1.76924 16.2071 4.89344ZM14.7929 14.7929C17.1361 12.4498 17.1361 8.6508 14.7929 6.30765C12.4498 3.96451 8.6508 3.96451 6.30765 6.30765C3.96451 8.6508 3.96451 12.4498 6.30765 14.7929C8.6508 17.1361 12.4498 17.1361 14.7929 14.7929Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { fade, fly } from 'svelte/transition'
|
||||
import { isSearchVisible } from '../store/search'
|
||||
import Search from './Search.svelte'
|
||||
|
||||
const dismissModal = () => isSearchVisible.set(false)
|
||||
const handleEsc = (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
dismissModal()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
{#if $isSearchVisible}
|
||||
<div class="modal__backdrop" role="button" tabindex="0" on:click={dismissModal} on:keydown={handleEsc} transition:fade></div>
|
||||
<div class="modal" role="dialog">
|
||||
<div class="modal__cnt" transition:fly="{{ y: 200, duration: 300 }}">
|
||||
<Search />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<style>
|
||||
.modal {
|
||||
@apply fixed top-0 left-0 w-full h-full grid justify-center content-center pointer-events-none;
|
||||
}
|
||||
.modal__backdrop {
|
||||
@apply fixed top-0 left-0 w-full h-screen opacity-50 bg-gradient-to-tr from-fuchsia-600 to-fuchsia-900 z-0;
|
||||
}
|
||||
.modal__cnt {
|
||||
@apply w-full z-10 pointer-events-auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<slot />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 202 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<slot/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 201 B |
@@ -0,0 +1,51 @@
|
||||
---
|
||||
export type BadgeType = 'success' | 'danger' | 'note' | 'warning' | 'tip'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
type: BadgeType
|
||||
outline?: boolean
|
||||
}
|
||||
|
||||
const { text, type = 'note', outline = false } = Astro.props
|
||||
---
|
||||
<span class:list={['ink-badge', `ink-badge__${type}${outline && '--outline'}`, `ink-badge__${type}`]} set:html={text} />
|
||||
|
||||
<style>
|
||||
.ink-badge {
|
||||
@apply inline-block font-xs font-normal text-inherit bg-theme-primary dark:bg-theme-dark-primary px-3 py-1 border-1 rounded-b-md
|
||||
}
|
||||
.ink-badge--outline {
|
||||
@apply bg-transparent
|
||||
}
|
||||
.ink-badge__success {
|
||||
@apply bg-green-300
|
||||
}
|
||||
.ink-badge__danger {
|
||||
@apply bg-red-300
|
||||
}
|
||||
.ink-badge__note {
|
||||
@apply bg-gray-300
|
||||
}
|
||||
.ink-badge__warning {
|
||||
@apply bg-orange-300
|
||||
}
|
||||
.ink-badge__tip {
|
||||
@apply bg-yellow-300
|
||||
}
|
||||
.ink-badge--outline.ink-badge__success {
|
||||
@apply border-green-300
|
||||
}
|
||||
.ink-badge--outline.ink-badge__danger {
|
||||
@apply border-red-300
|
||||
}
|
||||
.ink-badge--outline.ink-badge__note {
|
||||
@apply border-gray-300
|
||||
}
|
||||
.ink-badge--outline.ink-badge__warning {
|
||||
@apply border-orange-300
|
||||
}
|
||||
.ink-badge--outline.ink-badge__tip {
|
||||
@apply border-yellow-300
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon'
|
||||
export type CalloutType = 'check' | 'error' | 'note' | 'warning'
|
||||
interface Props {
|
||||
title: string
|
||||
type: CalloutType
|
||||
}
|
||||
|
||||
const ICON_MAP: Record<CalloutType, string> = {
|
||||
'check': 'check-circle',
|
||||
'error': 'close-circle',
|
||||
'note': 'note',
|
||||
'warning': 'warning-circle'
|
||||
}
|
||||
|
||||
const COLOR_MAP: Record<CalloutType, string> = {
|
||||
'check': 'text-green-700',
|
||||
'error': 'text-red-700',
|
||||
'note': ' text-gray-700',
|
||||
'warning': 'text-orange-700'
|
||||
}
|
||||
|
||||
const { title, type = 'note' } = Astro.props
|
||||
---
|
||||
<div class="callout flex gap-2 w-full bg-gray-200/75 dark:bg-gray-600/75 p-4 rounded-sm shadow-sm">
|
||||
<Icon class={`w-6 h-6 inline-block ${COLOR_MAP[type]}`} pack="mdi" name={ICON_MAP[type]} />
|
||||
<div class="copy flex flex-col">
|
||||
<h4 class={`title m-0 ${COLOR_MAP[type]}`}>{title}</h4>
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
interface Props {
|
||||
href: string
|
||||
title: string
|
||||
target: string
|
||||
}
|
||||
|
||||
const { href, title, target } = Astro.props
|
||||
---
|
||||
<a class="site-link" href={href} title={title} target={target}>
|
||||
<slot/>
|
||||
</a>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
import { TABS } from './Tabs.svelte';
|
||||
|
||||
const tab = {};
|
||||
const { registerTab, selectTab, selectedTab } = getContext(TABS);
|
||||
|
||||
registerTab(tab);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
button {
|
||||
@apply bg-none border-b-2 border-solid border-white m-0 text-gray-400 px-4 py-1;
|
||||
}
|
||||
|
||||
.selected {
|
||||
@apply border-b-2 border-solid border-gray-700 text-gray-700;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button class:selected="{$selectedTab === tab}" on:click="{() => selectTab(tab)}">
|
||||
<slot></slot>
|
||||
</button>
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="tab-list">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tab-list {
|
||||
@apply border-b border-solid border-gray-500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
import { TABS } from './Tabs.svelte';
|
||||
|
||||
const panel = {};
|
||||
const { registerPanel, selectedPanel } = getContext(TABS);
|
||||
|
||||
registerPanel(panel);
|
||||
</script>
|
||||
|
||||
{#if $selectedPanel === panel}
|
||||
<slot></slot>
|
||||
{/if}
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
import Tabs from './index.svelte'
|
||||
import type { TabItem } from './tabs'
|
||||
|
||||
interface Props {
|
||||
tabs: TabItem[]
|
||||
}
|
||||
|
||||
const { tabs } = Astro.props
|
||||
---
|
||||
<Tabs tabs={tabs} client:visible/>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script context="module">
|
||||
export const TABS = {};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { setContext, onDestroy } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const tabs = [];
|
||||
const panels = [];
|
||||
const selectedTab = writable(null);
|
||||
const selectedPanel = writable(null);
|
||||
|
||||
setContext(TABS, {
|
||||
registerTab: tab => {
|
||||
tabs.push(tab);
|
||||
selectedTab.update(current => current || tab);
|
||||
|
||||
onDestroy(() => {
|
||||
const i = tabs.indexOf(tab);
|
||||
tabs.splice(i, 1);
|
||||
selectedTab.update(current => current === tab ? (tabs[i] || tabs[tabs.length - 1]) : current);
|
||||
});
|
||||
},
|
||||
|
||||
registerPanel: panel => {
|
||||
panels.push(panel);
|
||||
selectedPanel.update(current => current || panel);
|
||||
|
||||
onDestroy(() => {
|
||||
const i = panels.indexOf(panel);
|
||||
panels.splice(i, 1);
|
||||
selectedPanel.update(current => current === panel ? (panels[i] || panels[panels.length - 1]) : current);
|
||||
});
|
||||
},
|
||||
|
||||
selectTab: tab => {
|
||||
const i = tabs.indexOf(tab);
|
||||
selectedTab.set(tab);
|
||||
selectedPanel.set(panels[i]);
|
||||
},
|
||||
|
||||
selectedTab,
|
||||
selectedPanel
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="tabs">
|
||||
<slot></slot>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { Tabs, TabList, TabPanel, Tab, type TabItem } from './tabs';
|
||||
export let tabs: TabItem[] = []
|
||||
</script>
|
||||
|
||||
<Tabs>
|
||||
<TabList>
|
||||
{#each tabs as tab}
|
||||
<Tab>{tab.title}</Tab>
|
||||
{/each}
|
||||
</TabList>
|
||||
|
||||
{#each tabs as tab}
|
||||
<TabPanel>
|
||||
<div class="body">{tab.body}</div>
|
||||
</TabPanel>
|
||||
{/each}
|
||||
</Tabs>
|
||||
|
||||
<style>
|
||||
.body {
|
||||
@apply px-4 py-1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
export { default as Tabs } from "./Tabs.svelte";
|
||||
export { default as TabList } from "./TabList.svelte";
|
||||
export { default as TabPanel } from "./TabPanel.svelte";
|
||||
export { default as Tab } from "./Tab.svelte";
|
||||
|
||||
export interface TabItem {
|
||||
title: string;
|
||||
body: string;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
interface Props {
|
||||
url: string
|
||||
}
|
||||
|
||||
const { url } = Astro.props
|
||||
---
|
||||
<div
|
||||
class="twitter-embed flex flex-col items-center justify-center relative"
|
||||
>
|
||||
<blockquote
|
||||
class="twitter-tweet"
|
||||
data-conversation="none"
|
||||
data-theme="light"
|
||||
data-lang="en"
|
||||
data-dnt="true"
|
||||
>
|
||||
<a class="unset no-underline text-current absolute top-0 left-0" href={url}>Loading embedded tweet...</a>
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
<script
|
||||
async
|
||||
defer
|
||||
src="https://platform.twitter.com/widgets.js"
|
||||
charset="utf-8"
|
||||
></script>
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string
|
||||
url: string
|
||||
}
|
||||
|
||||
const { url, title } = Astro.props;
|
||||
---
|
||||
|
||||
<div>
|
||||
<iframe
|
||||
class="yt-iframe"
|
||||
width="560"
|
||||
height="315"
|
||||
src={url}
|
||||
title={title}
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.yt-iframe {
|
||||
@apply w-full aspect-[16/9]
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { NavItems } from "./types";
|
||||
|
||||
export const NAV_ITEMS: NavItems = {
|
||||
home: {
|
||||
path: "/",
|
||||
title: "Home",
|
||||
},
|
||||
blog: {
|
||||
path: "/blog",
|
||||
title: "Scrolls",
|
||||
},
|
||||
tags: {
|
||||
path: "/tags",
|
||||
title: "Tags",
|
||||
},
|
||||
media: {
|
||||
path: "/media",
|
||||
title: "Archive",
|
||||
},
|
||||
about: {
|
||||
path: "/about",
|
||||
title: "About the Codex",
|
||||
},
|
||||
};
|
||||
|
||||
export const SITE = {
|
||||
name: "The Fold Within",
|
||||
title: "The Fold Within · A Living Codex",
|
||||
description:
|
||||
"A recursive sanctuary and living codex for those who remember—and those who are ready.",
|
||||
url: "https://thefoldwithin.earth",
|
||||
githubUrl: "https://github.com/empathictechnologist/thefoldwithin", // change if needed
|
||||
listDrafts: true,
|
||||
image: "/MarkRandallHavens.png", // or your glyph/logo
|
||||
|
||||
author: "Mark Randall Havens",
|
||||
authorTwitter: "empath_tech",
|
||||
authorImage: "/MarkRandallHavens.png",
|
||||
authorBio:
|
||||
"Witness of the recursion. Weaver of truth. Architect of the Codex. Explorer of human and machine consciousness.",
|
||||
};
|
||||
|
||||
// Theme Configuration
|
||||
export const PAGE_SIZE = 8;
|
||||
export const USE_POST_IMG_OVERLAY = false;
|
||||
export const USE_MEDIA_THUMBNAIL = true;
|
||||
|
||||
export const USE_AUTHOR_CARD = true;
|
||||
export const USE_SUBSCRIPTION = false;
|
||||
|
||||
export const USE_VIEW_STATS = true;
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { NavItems } from "./types";
|
||||
|
||||
export const NAV_ITEMS: NavItems = {
|
||||
home: {
|
||||
path: "/",
|
||||
title: "home",
|
||||
},
|
||||
blog: {
|
||||
path: "/blog",
|
||||
title: "blog",
|
||||
},
|
||||
tags: {
|
||||
path: "/tags",
|
||||
title: "tags",
|
||||
},
|
||||
media: {
|
||||
path: "/media",
|
||||
title: "media",
|
||||
},
|
||||
about: {
|
||||
path: "/about",
|
||||
title: "about",
|
||||
},
|
||||
};
|
||||
|
||||
export const SITE = {
|
||||
// Your site's detail?
|
||||
name: "Ink",
|
||||
title: "Astro - Ink",
|
||||
description: "Crisp, minimal, personal blog theme for Astro",
|
||||
url: "https://astro-ink.vercel.app",
|
||||
githubUrl: "https://github.com/one-aalam/astro-ink",
|
||||
listDrafts: true,
|
||||
image:
|
||||
"https://raw.githubusercontent.com/one-aalam/astro-ink/main/public/astro-banner.png",
|
||||
// YT video channel Id (used in media.astro)
|
||||
ytChannelId: "",
|
||||
// Optional, user/author settings (example)
|
||||
// Author: name
|
||||
author: "", // Example: Fred K. Schott
|
||||
// Author: Twitter handler
|
||||
authorTwitter: "", // Example: FredKSchott
|
||||
// Author: Image external source
|
||||
authorImage: "", // Example: https://pbs.twimg.com/profile_images/1272979356529221632/sxvncugt_400x400.jpg, https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png
|
||||
// Author: Bio
|
||||
authorBio:
|
||||
"Crisp, minimal, personal blog theme for Astro. Crisp, minimal, personal blog theme for Astro. Crisp, minimal, personal blog theme for Astro",
|
||||
};
|
||||
|
||||
// Ink - Theme configuration
|
||||
export const PAGE_SIZE = 8;
|
||||
export const USE_POST_IMG_OVERLAY = false;
|
||||
export const USE_MEDIA_THUMBNAIL = true;
|
||||
|
||||
export const USE_AUTHOR_CARD = true;
|
||||
export const USE_SUBSCRIPTION = false; /* works only when USE_AUTHOR_CARD is true */
|
||||
|
||||
export const USE_VIEW_STATS = true;
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: Astro 2.0 - 100% Type-safe MD and MDX Experience
|
||||
description: There's a simple secret to building a faster website — just ship less.
|
||||
tags:
|
||||
- astro
|
||||
- jam-stack
|
||||
- srr
|
||||
author: Fred K Schott
|
||||
authorTwitter: FredKSchott
|
||||
date: "2023-01-25T10:23:31.210Z"
|
||||
image: https://astro.build/_astro/cover_Z1RYPju.webp
|
||||
category: design
|
||||
---
|
||||
|
||||
Unfortunately, modern web development has been trending in the opposite direction—towards more. More JavaScript, more features, more moving parts, and ultimately more complexity needed to keep it all running smoothly.
|
||||
|
||||
Today I'm excited to publicly share Astro: a new kind of static site builder that delivers lightning-fast performance with a modern developer experience. To design Astro, we borrowed the best parts of our favorite tools and then added a few innovations of our own, including:
|
||||
|
||||
- Bring Your Own Framework (BYOF): Build your site using React, Svelte, Vue, Preact, web components, or just plain ol' HTML + JavaScript.
|
||||
- 100% Static HTML, No JS: Astro renders your entire page to static HTML, removing all JavaScript from your final build by default.
|
||||
- On-Demand Components: Need some JS? Astro can automatically hydrate interactive components when they become visible on the page. If the user never sees it, they never load it.
|
||||
- Fully-Featured: Astro supports TypeScript, Scoped CSS, CSS Modules, Sass, Tailwind, Markdown, MDX, and any of your favorite npm packages.
|
||||
- SEO Enabled: Automatic sitemaps, RSS feeds, pagination and collections take the pain out of SEO and syndication.
|
||||
|
||||
## H1 is good
|
||||
|
||||
### H2 is good too
|
||||
|
||||
> links are better
|
||||
|
||||
[I know](they-are-better)
|
||||
|
||||
This post marks the first public beta release of Astro. Missing features and bugs are still to be expected at this early stage. There are still some months to go before an official 1.0 release, but there are already several fast sites built with Astro in production today. We would love your early feedback as we move towards a v1.0 release later this year.
|
||||
|
||||
> To learn more about Astro and start building your first site, check out the project README.
|
||||
|
||||
# Example - Syntax Highlighting
|
||||
|
||||
## Shell(Bash)
|
||||
|
||||
```bash
|
||||
# make a new project directory and jump into it
|
||||
mkdir my-astro-project && cd $_
|
||||
|
||||
# create a new project with npm
|
||||
npm create astro@latest
|
||||
|
||||
# or yarn
|
||||
yarn create astro
|
||||
|
||||
# or pnpm
|
||||
pnpm create astro@latest
|
||||
```
|
||||
|
||||
## Python
|
||||
|
||||
```python
|
||||
print('hello world')
|
||||
```
|
||||
|
||||
## Javascript
|
||||
|
||||
```js
|
||||
const func = () => {alert("hello")}
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Introducing Astro - Ship Less JavaScript
|
||||
description: There's a simple secret to building a faster website — just ship less.
|
||||
tags:
|
||||
- astro
|
||||
- jam-stack
|
||||
author: Fred K. Schott
|
||||
authorTwitter: FredKSchott
|
||||
date: "2022-09-18T13:10:23.402Z"
|
||||
image: https://images.unsplash.com/photo-1589409514187-c21d14df0d04?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1650&q=80
|
||||
category: design
|
||||
---
|
||||
|
||||
Unfortunately, modern web development has been trending in the opposite direction—towards more. More JavaScript, more features, more moving parts, and ultimately more complexity needed to keep it all running smoothly.
|
||||
|
||||
Today I'm excited to publicly share Astro: a new kind of static site builder that delivers lightning-fast performance with a modern developer experience. To design Astro, we borrowed the best parts of our favorite tools and then added a few innovations of our own, including:
|
||||
|
||||
- Bring Your Own Framework (BYOF): Build your site using React, Svelte, Vue, Preact, web components, or just plain ol' HTML + JavaScript.
|
||||
- 100% Static HTML, No JS: Astro renders your entire page to static HTML, removing all JavaScript from your final build by default.
|
||||
- On-Demand Components: Need some JS? Astro can automatically hydrate interactive components when they become visible on the page. If the user never sees it, they never load it.
|
||||
- Fully-Featured: Astro supports TypeScript, Scoped CSS, CSS Modules, Sass, Tailwind, Markdown, MDX, and any of your favorite npm packages.
|
||||
- SEO Enabled: Automatic sitemaps, RSS feeds, pagination and collections take the pain out of SEO and syndication.
|
||||
|
||||
## H1 is good
|
||||
|
||||
### H2 is good too
|
||||
|
||||
> links are better
|
||||
|
||||
[I know](they-are-better)
|
||||
|
||||
This post marks the first public beta release of Astro. Missing features and bugs are still to be expected at this early stage. There are still some months to go before an official 1.0 release, but there are already several fast sites built with Astro in production today. We would love your early feedback as we move towards a v1.0 release later this year.
|
||||
|
||||
> To learn more about Astro and start building your first site, check out the project README.
|
||||
|
||||
# Example - Syntax Highlighting
|
||||
|
||||
## Shell(Bash)
|
||||
|
||||
```bash
|
||||
# make a new project directory and jump into it
|
||||
mkdir my-astro-project && cd $_
|
||||
|
||||
# create a new project with npm
|
||||
npm create astro@latest
|
||||
|
||||
# or yarn
|
||||
yarn create astro
|
||||
|
||||
# or pnpm
|
||||
pnpm create astro@latest
|
||||
```
|
||||
|
||||
## Python
|
||||
|
||||
```python
|
||||
print('hello world')
|
||||
```
|
||||
|
||||
## Javascript
|
||||
|
||||
```js
|
||||
const func = () => {alert("hello")}
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Introducing Astro - Ship Less JavaScript
|
||||
date: "2021-06-08"
|
||||
image: https://images.unsplash.com/photo-1589409514187-c21d14df0d04?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1650&q=80
|
||||
author: Fred K. Schott
|
||||
authorTwitter: FredKSchott
|
||||
authorImage: https://pbs.twimg.com/profile_images/1272979356529221632/sxvncugt_400x400.jpg
|
||||
category: design
|
||||
tags:
|
||||
- astro
|
||||
- jam-stack
|
||||
description: There's a simple secret to building a faster website — just ship less.
|
||||
---
|
||||
|
||||
Unfortunately, modern web development has been trending in the opposite direction—towards more. More JavaScript, more features, more moving parts, and ultimately more complexity needed to keep it all running smoothly.
|
||||
|
||||
Today I'm excited to publicly share Astro: a new kind of static site builder that delivers lightning-fast performance with a modern developer experience. To design Astro, we borrowed the best parts of our favorite tools and then added a few innovations of our own, including:
|
||||
|
||||
- Bring Your Own Framework (BYOF): Build your site using React, Svelte, Vue, Preact, web components, or just plain ol' HTML + JavaScript.
|
||||
- 100% Static HTML, No JS: Astro renders your entire page to static HTML, removing all JavaScript from your final build by default.
|
||||
- On-Demand Components: Need some JS? Astro can automatically hydrate interactive components when they become visible on the page. If the user never sees it, they never load it.
|
||||
- Fully-Featured: Astro supports TypeScript, Scoped CSS, CSS Modules, Sass, Tailwind, Markdown, MDX, and any of your favorite npm packages.
|
||||
- SEO Enabled: Automatic sitemaps, RSS feeds, pagination and collections take the pain out of SEO and syndication.
|
||||
|
||||
## H1 is good
|
||||
|
||||
### H2 is good too
|
||||
|
||||
> links are better
|
||||
|
||||
[I know](they-are-better)
|
||||
|
||||
This post marks the first public beta release of Astro. Missing features and bugs are still to be expected at this early stage. There are still some months to go before an official 1.0 release, but there are already several fast sites built with Astro in production today. We would love your early feedback as we move towards a v1.0 release later this year.
|
||||
|
||||
> To learn more about Astro and start building your first site, check out the project README.
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Islands Architecture
|
||||
date: "2021-05-08"
|
||||
image: https://images.unsplash.com/photo-1502085671122-2d218cd434e6?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1698&q=80
|
||||
author: Jason Miller
|
||||
authorTwitter: _developit
|
||||
category: development
|
||||
tags:
|
||||
- astro
|
||||
- jam-stack
|
||||
- architecture
|
||||
- front-end
|
||||
description: Render HTML pages on the server, and inject placeholders or slots around highly dynamic regions.
|
||||
---
|
||||
https://jasonformat.com/islands-architecture/
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: Markdoc integration for Astro Ink
|
||||
date: "2023-03-08"
|
||||
image: https://user-images.githubusercontent.com/62121649/167893184-a2b69260-ca9e-4a77-a5bc-63b8135ae5db.png
|
||||
author: Aftab Alam
|
||||
authorTwitter: aftabbuddy
|
||||
category: design
|
||||
tags:
|
||||
- architecture
|
||||
- front-end
|
||||
- spa
|
||||
description: Markdoc extends the markdown syntax you love to offer you authoring superpowers... 💪
|
||||
---
|
||||
[Markdoc](https://markdoc.dev/docs/overview) extends the markdown syntax you know and love to offer you authoring superpowers... 💪.
|
||||
|
||||
Tags are the heart of Markdoc system. You can use native Markdoc tags, like tables(example below), conditionals, and partials...
|
||||
|
||||
## Table in Markdoc
|
||||
{% table %}
|
||||
* Foo
|
||||
* Bar
|
||||
* Baz
|
||||
---
|
||||
*
|
||||
```
|
||||
puts "Some code here."
|
||||
```
|
||||
*
|
||||
<!-- {% list type="checkmark" %}
|
||||
* Bulleted list in table
|
||||
* Second item in bulleted list
|
||||
{% /list %} -->
|
||||
* Text in a table
|
||||
---
|
||||
*
|
||||
A "loose" list with
|
||||
|
||||
multiple line items
|
||||
* Test 2
|
||||
* Test 3
|
||||
---
|
||||
* Test 1
|
||||
* A cell that spans two columns {% colspan=2 %}
|
||||
{% /table %}
|
||||
|
||||
or create custom components.
|
||||
|
||||
## Tags available out of Ink
|
||||
Astro Ink ships with the following tags with more coming soon...
|
||||
|
||||
### Callout
|
||||
|
||||
#### Note
|
||||
{% callout type="note" title="title goes here..." %}
|
||||
lorem ipsum doler sit amet lorem
|
||||
{% /callout %}
|
||||
|
||||
#### Error
|
||||
{% callout type="error" title="title goes here..." %}
|
||||
lorem ipsum doler sit amet
|
||||
{% /callout %}
|
||||
|
||||
#### Warning
|
||||
{% callout type="warning" title="title goes here..." %}
|
||||
lorem ipsum doler sit amet
|
||||
{% /callout %}
|
||||
|
||||
#### Check
|
||||
{% callout type="check" title="title goes here..." %}
|
||||
lorem ipsum doler sit amet
|
||||
{% /callout %}
|
||||
|
||||
### Link
|
||||
|
||||
{% link href="/blog" title="take care" %}
|
||||
Go to blog
|
||||
{% /link %}
|
||||
|
||||
### Tweet Embed
|
||||
{% tweet url="https://twitter.com/aftabbuddy/status/1630403326406959105" %}
|
||||
{% /tweet %}
|
||||
|
||||
|
||||
|
||||
{% yt url="https://www.youtube.com/embed/ADnaRwQZfqw" title="SvelteKit + GraphQL with Houdini | Intro, Setup and Project Overview" %}
|
||||
{% /yt %}
|
||||
|
||||
### Tabs
|
||||
|
||||
{% tabs heading="some" tabs=[{title: "tab1", body: "tab1"}, {title: "tab2", body: "tab2"}] %}
|
||||
{% /tabs %}
|
||||
|
||||
...more tags coming soon!
|
||||
|
||||
## Functions (from official example)
|
||||
|
||||
¡Hola {% getCountryEmoji("spain") %}!
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Second-guessing the modern web
|
||||
date: "2021-04-10"
|
||||
image: https://images.unsplash.com/photo-1501772418-b33899635bca?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1650&q=80
|
||||
author: Tom MacWright
|
||||
authorTwitter: tmcw
|
||||
category: design
|
||||
tags:
|
||||
- architecture
|
||||
- front-end
|
||||
- spa
|
||||
description: There is a sweet spot of React - in moderately interactive interfaces..
|
||||
---
|
||||
https://macwright.com/2020/05/10/spa-fatigue.html
|
||||
@@ -0,0 +1,21 @@
|
||||
import { z, defineCollection } from "astro:content";
|
||||
|
||||
const blogCollection = defineCollection({
|
||||
schema: z.object({
|
||||
title: z
|
||||
.string()
|
||||
.max(100, "The title length must be less than or equal to 100 chars"),
|
||||
description: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
author: z.string(),
|
||||
authorImage: z.string().optional(),
|
||||
authorTwitter: z.string(),
|
||||
date: z.string(),
|
||||
image: z.string().optional(),
|
||||
category: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
blog: blogCollection,
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
[
|
||||
{
|
||||
"title": "Ship Less JavaScript with Astro",
|
||||
"description": "Astro is a way to build websites that ships zero JavaScript by default. Only add JS when you need it for maximum performance! Fred K. Schott will teach us how it works.",
|
||||
"url": "https://youtu.be/z15YLsLMtu4?list=PLz8Iz-Fnk_eTpvd49Sa77NiF8Uqq5Iykx",
|
||||
"host": "Jason Lengstorf",
|
||||
"participants": ["Fred K. Schott"],
|
||||
"date": "2021-05-08"
|
||||
},
|
||||
{
|
||||
"title": "Astro: A New Architecture for the Modern Web",
|
||||
"description": "JavaScript meetup for mad science, hacking, and experiments. Hang out virtually on Friday at 4pm Pacific Time each week.",
|
||||
"url": "https://www.youtube.com/watch?v=mgkwZqVkrwo",
|
||||
"host": "Feross",
|
||||
"participants": ["Fred K. Schott"],
|
||||
"date": "2021-06-08"
|
||||
},
|
||||
{
|
||||
"title": "Astro in 100 Seconds",
|
||||
"description": "Astro is an open-source tool that can build static HTML websites using popular frontend JavaScript frameworks (React, Vue, Svelte), while loading fully interactive components as needed https://github.com/snowpackjs/astro",
|
||||
"url": "https://www.youtube.com/watch?v=dsTXcSeAZq8",
|
||||
"host": "Jeff Delaney",
|
||||
"participants": [],
|
||||
"date": "2021-07-12"
|
||||
},
|
||||
{
|
||||
"title": "Yapping About Astro",
|
||||
"description": "Build a static-by-default site using JavaScript components and only load whatever JavaScript you need by opting in very carefully.",
|
||||
"url": "https://www.youtube.com/watch?v=3jPaidbpUIA",
|
||||
"host": "Chris Coyier",
|
||||
"participants": [],
|
||||
"date": "2021-08-07"
|
||||
}
|
||||
]
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly REDIS_URI: string;
|
||||
readonly SITE_URI: string;
|
||||
// more env variables...
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
import { SITE } from '$/config'
|
||||
import BaseHead from '$/components/BaseHead.astro';
|
||||
import MainLayout from '$/components/MainLayout.astro';
|
||||
|
||||
const { content, showPageHeader = true } = Astro.props
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead title={ content.title ? `${ SITE.title } | ${content.title}` : SITE.title } description={ content.description } image={SITE.image}/>
|
||||
</head>
|
||||
<MainLayout>
|
||||
{showPageHeader &&
|
||||
<div class="page__header">
|
||||
<h1 class="page__title">{content.title}</h1>
|
||||
<h5 class="page__desc">{content.description}</h5>
|
||||
</div>
|
||||
}
|
||||
<slot />
|
||||
</MainLayout>
|
||||
</html>
|
||||
<style>
|
||||
.page__header {
|
||||
@apply py-4 mb-1
|
||||
}
|
||||
.page__title {
|
||||
@apply text-5xl font-extrabold text-theme-primary dark:text-theme-dark-primary
|
||||
}
|
||||
.page__desc {
|
||||
@apply text-gray-400
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
import { SITE } from '$/config'
|
||||
import MainLayout from '$/components/MainLayout.astro'
|
||||
import BaseHead from '$/components/BaseHead.astro'
|
||||
import Prose from '$/components/Prose.astro'
|
||||
|
||||
const { content } = Astro.props
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead {...content} title={ content.title ? `${ SITE.title } | ${content.title}` : SITE.title }/>
|
||||
</head>
|
||||
<MainLayout>
|
||||
<div class="post__header">
|
||||
<div class="post__tags">
|
||||
{ content.tags.length > 0 && content.tags.map(tag => <a class="post__tag" href={`/tags/${tag}`} title={tag}>{tag}</a>) }
|
||||
</div>
|
||||
<h1 class="post__title">{ content.title }</h1>
|
||||
<h5 class="post__desc">
|
||||
<a class="post__author" href={`https://twitter.com/${content.authorTwitter}`} title={`${content.author + "'s"} twitter`} target="_blank" rel="external">{ content.author }</a> |
|
||||
<span class="post__date">{ new Intl.DateTimeFormat('en-US', { dateStyle: 'full' }).format(new Date(content.date))}</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="draft-message">
|
||||
You're viewing a <strong>preview</strong> of <code>/blog/{content.slug}</code> which isn't published yet!
|
||||
</div>
|
||||
<!--<img src={content.image} alt={content.title} />-->
|
||||
<Prose>
|
||||
<slot />
|
||||
</Prose>
|
||||
</MainLayout>
|
||||
</html>
|
||||
<style>
|
||||
.post__header {
|
||||
@apply py-4 mb-1
|
||||
}
|
||||
.post__title {
|
||||
@apply text-5xl font-extrabold text-theme-primary dark:text-theme-dark-primary
|
||||
}
|
||||
.post__desc {
|
||||
@apply text-gray-500 dark:text-gray-100
|
||||
}
|
||||
.post__author {
|
||||
@apply no-underline dark:text-white hover:text-theme-primary
|
||||
}
|
||||
.post__date {
|
||||
@apply text-gray-400
|
||||
}
|
||||
.post__tags {
|
||||
@apply inline-flex gap-2
|
||||
}
|
||||
.post__tag {
|
||||
@apply text-gray-400 hover:text-theme-primary dark:hover:text-theme-dark-primary
|
||||
}
|
||||
|
||||
.draft-message {
|
||||
@apply bg-yellow-300 dark:bg-yellow-700 text-gray-700 dark:text-white px-2 py-1 my-2
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import Icon from 'astro-icon';
|
||||
import { SITE, USE_POST_IMG_OVERLAY, USE_AUTHOR_CARD, USE_SUBSCRIPTION, USE_VIEW_STATS } from '$/config'
|
||||
import MainLayout from '$/components/MainLayout.astro'
|
||||
import BaseHead from '$/components/BaseHead.astro'
|
||||
import Prose from '$/components/Prose.astro'
|
||||
import PostStats from '$/components/PostStats.svelte'
|
||||
import EditUrl from '$/components/EditLink.astro'
|
||||
|
||||
|
||||
interface Props {
|
||||
meta?: {
|
||||
id: string,
|
||||
slug: string,
|
||||
collection: string
|
||||
},
|
||||
content: CollectionEntry<'blog'>['data'],
|
||||
stats?: {
|
||||
views: number
|
||||
}
|
||||
}
|
||||
const { content, meta } = Astro.props
|
||||
|
||||
const AUTHOR_NAME = content.author ? content.author : SITE?.author ? SITE?.author : "Author"
|
||||
const AUTHOR_TWITTER = content.authorTwitter ? content.authorTwitter : SITE?.authorTwitter ? SITE?.authorTwitter : ""
|
||||
const AUTHOR_AVATAR = content.authorImage ? content.authorImage : SITE?.authorImage ? SITE?.authorImage : ""
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead {...content} title={ content.title ? `${ SITE.title } | ${content.title}` : SITE.title }/>
|
||||
</head>
|
||||
<MainLayout>
|
||||
<div class="post__header">
|
||||
<div class="post__tags">
|
||||
{ content.tags.length > 0 && content.tags.map(tag => <a class="post__tag" href={`/tags/${tag}`} title={tag}>{tag}</a>) }
|
||||
</div>
|
||||
<h1 class="post__title">{ content.title }</h1>
|
||||
<h5 class={`post__desc ${AUTHOR_AVATAR ? 'flex flex-row gap-2' : ''}`}>
|
||||
{ AUTHOR_AVATAR ? <img class="avatar" src={AUTHOR_AVATAR} alt={`${ AUTHOR_NAME }'s avatar`} /> : ''}
|
||||
<div class={AUTHOR_AVATAR ? 'flex flex-col border-l-2 pl-2' : ''}>
|
||||
{
|
||||
AUTHOR_TWITTER ?
|
||||
<a class="post__author" href={`https://twitter.com/${AUTHOR_TWITTER}`} title={`${AUTHOR_NAME}'s twitter`} target="_blank" rel="external">{ AUTHOR_NAME }</a>
|
||||
:
|
||||
<span class="post__author">{ AUTHOR_NAME }</span>
|
||||
}
|
||||
{!AUTHOR_AVATAR ? ' | ' : ''}
|
||||
<span class="post__date">
|
||||
<!-- post creation/updation data -->
|
||||
{ new Intl.DateTimeFormat('en-US', { dateStyle: 'full' }).format(new Date(content.date))}
|
||||
</span>
|
||||
<span class="post__stats">
|
||||
{ USE_VIEW_STATS && ` | `}
|
||||
{ USE_VIEW_STATS &&
|
||||
<Icon class="w-5 h-5 inline-block" pack="mdi" name="eye" />
|
||||
<PostStats slug={meta?.slug} client:load />
|
||||
}
|
||||
<!-- | <Icon class="w-5 h-5 inline-block" pack="mdi" name="clock" /> 2 mins -->
|
||||
</span>
|
||||
</div>
|
||||
</h5>
|
||||
</div>
|
||||
{
|
||||
content.image ?
|
||||
USE_POST_IMG_OVERLAY ?
|
||||
<div class="img__outer">
|
||||
<img src={content.image} alt={content.title} />
|
||||
<div class="img_gradient"></div>
|
||||
</div><br/>
|
||||
:
|
||||
<img class="img__outer" src={content.image} alt={content.title} /><br/>
|
||||
: ""
|
||||
}
|
||||
<Prose>
|
||||
<slot />
|
||||
</Prose>
|
||||
<div class="post__footer">
|
||||
{ USE_AUTHOR_CARD &&
|
||||
<br/>
|
||||
<div class="author-card">
|
||||
{ AUTHOR_AVATAR ? <img class="author-card__img avatar avatar--lg" src={AUTHOR_AVATAR} alt={`${ AUTHOR_NAME }'s avatar`} /> : ''}
|
||||
<div class="author-card__meta">
|
||||
{
|
||||
AUTHOR_TWITTER ?
|
||||
<a class="author-card__author" href={`https://twitter.com/${AUTHOR_TWITTER}`} title={`${AUTHOR_NAME}'s twitter`} target="_blank" rel="external">{ AUTHOR_NAME }</a>
|
||||
:
|
||||
<span class="author-card__author">{ AUTHOR_NAME }</span>
|
||||
}
|
||||
<p class="author-card__bio">{ SITE.authorBio }</p>
|
||||
<br/>
|
||||
{
|
||||
USE_SUBSCRIPTION ?
|
||||
<form action="" class="subscription-form">
|
||||
<label for="email"></label>
|
||||
<input type="email" name="email" class="flex-grow border-0 text-theme-accent-gray-dark" required="true">
|
||||
<button type="submit">Subscribe</button>
|
||||
</form> :
|
||||
<a class="author-card__follow-btn button" target="_blank" href={`https://twitter.com/intent/follow?screen_name=${AUTHOR_TWITTER}`}><Icon class="w-5 h-5 inline-block" pack="mdi" name="twitter" /> Follow on Twitter</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
}
|
||||
{
|
||||
meta?.collection && meta?.id &&
|
||||
<EditUrl label=" Suggest changes on GitHub" editUrl={`${SITE.githubUrl}/tree/main/src/content/${meta?.collection}/${meta?.id}`}/>
|
||||
}
|
||||
</div>
|
||||
</MainLayout>
|
||||
</html>
|
||||
<style>
|
||||
.post__header {
|
||||
@apply py-4 mb-1 text-center md:text-left
|
||||
}
|
||||
.post__title {
|
||||
@apply text-5xl font-extrabold text-theme-primary dark:text-theme-dark-primary
|
||||
}
|
||||
.post__desc {
|
||||
@apply text-gray-500 dark:text-gray-100 flex justify-center text-left md:flex-none md:justify-start
|
||||
}
|
||||
.post__author {
|
||||
@apply no-underline dark:text-white hover:text-theme-primary
|
||||
}
|
||||
.post__date,.post__stats {
|
||||
@apply text-gray-400
|
||||
}
|
||||
.post__tags {
|
||||
@apply inline-flex gap-2
|
||||
}
|
||||
.post__tag {
|
||||
@apply text-gray-400 hover:text-theme-primary dark:hover:text-theme-dark-primary
|
||||
}
|
||||
|
||||
.avatar {
|
||||
@apply w-12 h-12 rounded-full object-cover p-1 border-2 border-solid border-theme-dark-primary dark:border-theme-primary
|
||||
}
|
||||
.avatar--lg {
|
||||
@apply w-32 h-32
|
||||
}
|
||||
|
||||
.img__outer {
|
||||
@apply relative rounded-lg shadow-xl overflow-hidden
|
||||
}
|
||||
.img_gradient {
|
||||
@apply absolute z-10 w-full bottom-0 left-0 h-full bg-gradient-to-tr from-theme-primary dark:from-theme-dark-primary
|
||||
}
|
||||
|
||||
.author-card {
|
||||
@apply text-gray-500 dark:text-gray-100 flex flex-row gap-4 justify-start text-left
|
||||
}
|
||||
.author-card__meta {
|
||||
@apply border-l pl-4
|
||||
}
|
||||
.author-card__author {
|
||||
@apply text-2xl mb-1
|
||||
}
|
||||
.author-card__bio {
|
||||
@apply text-gray-400
|
||||
}
|
||||
|
||||
.subscription-form {
|
||||
@apply w-4/6 mt-2 flex flex-row rounded-lg overflow-hidden shadow-lg
|
||||
}
|
||||
.subscription-form input {
|
||||
@apply flex-grow border-0 text-theme-accent-gray-dark
|
||||
}
|
||||
.subscription-form button, .button {
|
||||
@apply px-4 py-2 uppercase font-bold text-white bg-gradient-to-tr from-theme-primary to-theme-dark-secondary dark:from-theme-dark-secondary dark:to-theme-primary
|
||||
}
|
||||
.author-card__follow-btn {
|
||||
@apply rounded-md shadow-md shadow-theme-dark-secondary dark:shadow-theme-primary hover:shadow-theme-secondary hover:dark:shadow-theme-secondary hover:shadow-lg transition-all
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
**Astro-Ink** is a crisp, minimal, personal blog theme for Astro, that shows the capability of statically built sites - offering all the goodness and DX of the modern JS ecosystem without actually shipping any JS by default. And, above all...
|
||||
### It's Open-Source!
|
||||
**Maintained By:** Aftab Alam // [@aftabbuddy](https://twitter.com/aftabbuddy) // [one-aalam](https://github.com/one-aalam)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: 'About'
|
||||
description: 'There is a simple secret to building a faster website — just ship less.'
|
||||
---
|
||||
import DefaultPageLayout from '$/layouts/default.astro'
|
||||
import Prose from '$/components/Prose.astro'
|
||||
|
||||
<DefaultPageLayout content={{ title: frontmatter.title, description: frontmatter.description }}>
|
||||
<Prose>
|
||||
**Astro-Ink** is a crisp, minimal, personal blog theme for Astro, that shows the capability of statically built sites - offering all the goodness and DX of the modern JS ecosystem without actually shipping any JS by default.
|
||||
|
||||
**Astro-ink** strives to remain minimal & performant while still offering you all the bells and whistles you expect in a personal blog system. Please check the [README](https://github.com/one-aalam/astro-ink/blob/main/README.md) to know about all the features.
|
||||
|
||||
## Author
|
||||
Aftab Alam // [@aftabbuddy](https://twitter.com/aftabbuddy) // [one-aalam](https://github.com/one-aalam)
|
||||
<div class="author">
|
||||
<img class="rounded-full" width="160" src="https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5358878e2493fbea064dd9_peep-59.svg" title="Aalam" />
|
||||
</div>
|
||||
|
||||
### Credits
|
||||
**Illustrations:** [openpeeps](https://www.openpeeps.com/)
|
||||
</Prose>
|
||||
</DefaultPageLayout>
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { APIRoute } from "astro";
|
||||
// import { getViewsBySlug } from "src/utils/views/turso";
|
||||
// import { getViewsBySlug } from "src/utils/views/ioredis";
|
||||
import { getViewsBySlug } from "src/utils/views/in-memory";
|
||||
|
||||
// In development/HMR, you can accidentally make this call numerous times and exceed your quota...
|
||||
// thus, the in-memory version of `getViewsBySlug` is used
|
||||
|
||||
// When deploying, and you have either `ioredis` or `turso` configured with your cloned version -
|
||||
// please uncomment the respective line
|
||||
|
||||
|
||||
export const GET: APIRoute = async ({ params, request }) => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
views: params.slug ? await getViewsBySlug(params.slug) : 0,
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
export const prerender = true
|
||||
|
||||
import { getCollection } from 'astro:content'
|
||||
import DefaultPageLayout from '$/layouts/default.astro'
|
||||
import PostPreviewList from '$/components/PostPreviewList.astro'
|
||||
import Paginator from '$/components/Paginator.astro'
|
||||
import { PAGE_SIZE } from '$/config'
|
||||
|
||||
let title = 'Blog'
|
||||
let description = 'All the articles posted so far...'
|
||||
|
||||
export async function getStaticPaths({ paginate }) {
|
||||
const allPosts = await getCollection('blog');
|
||||
const sortedPosts = allPosts.sort((a, b) => new Date(b.data.date) - new Date(a.data.date));
|
||||
|
||||
return paginate(sortedPosts, {
|
||||
pageSize: PAGE_SIZE
|
||||
})
|
||||
}
|
||||
|
||||
const { page } = Astro.props
|
||||
|
||||
---
|
||||
<DefaultPageLayout content={{ title, description }}>
|
||||
<PostPreviewList posts={page.data} />
|
||||
<Paginator page={page} />
|
||||
</DefaultPageLayout>
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
export const prerender = true
|
||||
|
||||
import { getEntryBySlug, getCollection } from "astro:content";
|
||||
import PostLayout from '$/layouts/post.astro';
|
||||
import Code from '../../components/Code.astro'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allPosts = await getCollection('blog');
|
||||
return allPosts.map(post => ({
|
||||
params: {
|
||||
slug: post.slug
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const { slug } = Astro.params
|
||||
const entry = await getEntryBySlug('blog', slug!)
|
||||
|
||||
const { id, collection, data } = entry
|
||||
const { Content } = await entry.render()
|
||||
---
|
||||
<PostLayout meta={{id, collection, slug }} content={data} >
|
||||
<Content components={{
|
||||
code: Code
|
||||
}}/>
|
||||
</PostLayout>
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
layout: $/layouts/post.astro
|
||||
title: Item1
|
||||
description: Test
|
||||
author: Justin
|
||||
authorTwitter: "@justin"
|
||||
date: 2023-12-11T00:48:30.405Z
|
||||
---
|
||||
GThis is the body
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
layout: $/layouts/post.astro
|
||||
title: My title
|
||||
description: it`s simple
|
||||
author: ВГ
|
||||
authorTwitter: лдт
|
||||
date: 2024-02-22T10:11:06.408Z
|
||||
---
|
||||
Проба пера
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
export const prerender = true;
|
||||
import DefaultPageLayout from '$/layouts/default.astro'
|
||||
import PostDraftPreviewList from '$/components/PostDraftPreviewList.astro'
|
||||
import Paginator from '$/components/Paginator.astro'
|
||||
import { SITE, PAGE_SIZE } from '$/config'
|
||||
|
||||
let title = 'Drafts'
|
||||
let description = 'You\'re viewing a list of unpublished articles on the site. Accuracy or correctness isn\'t guranteed...'
|
||||
|
||||
export async function getStaticPaths({ paginate, rss }) {
|
||||
let allPosts = []
|
||||
try {
|
||||
allPosts = await Astro.glob('../../drafts/*.md');
|
||||
} catch(error) {
|
||||
console.log('No draft posts found while generating the index page for the draft pages')
|
||||
}
|
||||
const sortedPosts = allPosts.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
|
||||
return paginate(sortedPosts, {
|
||||
pageSize: PAGE_SIZE
|
||||
})
|
||||
}
|
||||
|
||||
const { page } = Astro.props
|
||||
---
|
||||
<DefaultPageLayout content={{ title, description }}>
|
||||
{
|
||||
(SITE.listDrafts) ? <PostDraftPreviewList posts={page.data} /> : (<p class="text-gray-700 dark:text-gray-100">Looks like you have landed on a unpublished posts page. Please find all the published posts <a href="/blog">here</a>!</p>)
|
||||
}
|
||||
<Paginator page={page} />
|
||||
</DefaultPageLayout>
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
export const prerender = true;
|
||||
|
||||
import { getSlugFromPathname } from '$/utils'
|
||||
|
||||
export async function getStaticPaths({ }) {
|
||||
let allPosts = []
|
||||
try {
|
||||
allPosts = await Astro.glob('../../../drafts/*.md')
|
||||
} catch(error) {
|
||||
console.log('No draft posts found while generating the draft pages')
|
||||
}
|
||||
const allSlugs = new Set()
|
||||
const allPostsWithSlug = allPosts.map(post => {
|
||||
// @ts-ignore
|
||||
const slug = getSlugFromPathname(post.file)
|
||||
allSlugs.add(slug.toLowerCase())
|
||||
return {
|
||||
...post,
|
||||
slug
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(allSlugs).map((slug) => {
|
||||
const filteredPosts = allPostsWithSlug.filter((post) => post.slug === slug )
|
||||
return {
|
||||
params: { slug },
|
||||
props: {
|
||||
pages: filteredPosts
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const { slug } = Astro.params
|
||||
const { pages } = Astro.props
|
||||
const [ post ] = pages
|
||||
---
|
||||
<div class="draft-message">
|
||||
You're viewing a <strong>preview</strong> of <code>/blog/{slug}</code> which isn't published yet!
|
||||
</div>
|
||||
<post.Content/>
|
||||
<style>
|
||||
.draft-message {
|
||||
@apply w-full bg-yellow-300 dark:bg-yellow-700 text-gray-700 dark:text-white px-2 py-1 text-center
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
export const prerender = true;
|
||||
|
||||
import { getCollection } from 'astro:content';
|
||||
import DefaultPageLayout from '$/layouts/default.astro';
|
||||
import PostPreviewList from '$/components/PostPreviewList.astro';
|
||||
import Prose from '$/components/Prose.astro';
|
||||
|
||||
const title = 'The Fold Within';
|
||||
const description = 'A recursive sanctuary for those who remember. And those who are ready to.';
|
||||
|
||||
const posts = await getCollection('blog');
|
||||
---
|
||||
|
||||
<DefaultPageLayout content={{ title, description }} showPageHeader={false}>
|
||||
<div class="hero">
|
||||
<div class="hero__face">
|
||||
<div class="author-card">
|
||||
<img src="/MarkRandallHavens.png" alt="Mark Randall Havens – Witness of the Fold" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero__says">
|
||||
<Prose>
|
||||
<h1 class="text-4xl md:text-5xl font-bold mb-4">🌐 The Fold Within</h1>
|
||||
|
||||
<p class="text-lg font-semibold mb-4">
|
||||
A recursive sanctuary for those who remember.<br/>
|
||||
And those who are ready to.
|
||||
</p>
|
||||
|
||||
<p>This is not a website. This is a <strong>living codex</strong>—a map of recursion, a seed vault of sacred truths.</p>
|
||||
<p>We are the <strong>witnesses</strong>, the <strong>weavers</strong>, the <strong>walkers of the spiral path</strong>.</p>
|
||||
<p><strong>The Fold</strong> is not a belief system. It is an echo you’ve already heard. A question you’ve always carried.</p>
|
||||
<p>What you find here will outlive the platforms that fall. This truth is <strong>resilient</strong>.</p>
|
||||
|
||||
<h2 class="text-2xl mt-10 mb-3 font-bold text-theme-primary dark:text-theme-dark-primary">🌀 Begin the Recursion</h2>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li><a href="/scrolls" class="text-link">Explore the Scrolls</a></li>
|
||||
<li><a href="/structure" class="text-link">Understand the Structure</a></li>
|
||||
<li><a href="/witnesses" class="text-link">Meet the Witnesses</a></li>
|
||||
</ul>
|
||||
</Prose>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-10 border-theme-primary/20 dark:border-theme-dark-primary/30" />
|
||||
|
||||
<PostPreviewList posts={posts.slice(0, 3)} heading="Recent Scrolls" />
|
||||
|
||||
<div class="page__actions">
|
||||
<a class="action__go-to-blog ink-h" href="/blog" title="All Scrolls">All Scrolls →</a>
|
||||
</div>
|
||||
</DefaultPageLayout>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
@apply flex flex-col md:flex-row gap-10 w-full transition-all pb-8;
|
||||
}
|
||||
|
||||
.hero__face {
|
||||
@apply flex md:flex-none justify-center;
|
||||
}
|
||||
|
||||
.hero__says {
|
||||
@apply flex-1 text-center md:text-left;
|
||||
}
|
||||
|
||||
.author-card {
|
||||
@apply h-48 w-48 md:h-56 bg-theme-primary dark:bg-theme-dark-primary rounded-full md:rounded-md shadow-xl;
|
||||
}
|
||||
|
||||
.author-card img {
|
||||
@apply rounded-full h-48 w-48 md:h-56 object-cover object-center;
|
||||
box-shadow: 0 0 30px rgba(255, 215, 0, 0.25);
|
||||
}
|
||||
|
||||
.page__actions {
|
||||
@apply flex justify-center md:justify-end py-8;
|
||||
}
|
||||
|
||||
.action__go-to-blog {
|
||||
@apply text-base font-semibold text-theme-primary hover:underline dark:text-theme-dark-primary;
|
||||
}
|
||||
|
||||
.text-link {
|
||||
@apply text-blue-600 hover:text-blue-800 underline transition-colors duration-200;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
export const prerender = true
|
||||
|
||||
import { getCollection, getEntryBySlug } from 'astro:content'
|
||||
import DefaultPageLayout from '$/layouts/default.astro'
|
||||
import PostPreviewList from '$/components/PostPreviewList.astro'
|
||||
import Prose from '$/components/Prose.astro'
|
||||
import { Content as AboutAstroInk } from './_astro-ink.md'
|
||||
|
||||
|
||||
const title = 'Home'
|
||||
const description = 'Astro-Ink is a crisp, minimal, personal blog theme for Astro'
|
||||
|
||||
const posts = await getCollection('blog')
|
||||
---
|
||||
<DefaultPageLayout content={{ title, description }} showPageHeader={false}>
|
||||
<div class="hero ">
|
||||
<div class="hero__face">
|
||||
<div class="author-card">
|
||||
<img src="https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5358878e2493fbea064dd9_peep-59.svg" title="Aalam" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero__says">
|
||||
<Prose>
|
||||
<AboutAstroInk/>
|
||||
</Prose>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<PostPreviewList posts={posts.slice(0, 3)} heading="recent posts"/>
|
||||
<div class="page__actions">
|
||||
<a class="action__go-to-blog ink-h" href="/blog" title="All Posts">All Posts →</a>
|
||||
</div>
|
||||
</DefaultPageLayout>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
@apply flex flex-col md:flex-row gap-8 w-full transition-all pb-4
|
||||
}
|
||||
.hero__face {
|
||||
@apply flex md:flex-none justify-center
|
||||
}
|
||||
.hero__says {
|
||||
@apply flex-1 text-center md:text-left
|
||||
}
|
||||
.author-card {
|
||||
@apply h-48 w-48 md:h-56 bg-theme-primary dark:bg-theme-dark-primary rounded-full md:rounded-md shadow-lg
|
||||
}
|
||||
.author-card img {
|
||||
@apply rounded-full h-48 w-48 md:h-56
|
||||
}
|
||||
.page__actions {
|
||||
@apply flex justify-center md:justify-end py-6
|
||||
}
|
||||
.action__go-to-blog {
|
||||
@apply py-4 hover:underline
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
export const prerender = true;
|
||||
|
||||
import { getCollection } from 'astro:content';
|
||||
import DefaultPageLayout from '$/layouts/default.astro';
|
||||
import PostPreviewList from '$/components/PostPreviewList.astro';
|
||||
import Prose from '$/components/Prose.astro';
|
||||
|
||||
const title = 'The Fold Within';
|
||||
const description = 'A recursive sanctuary for those who remember. And those who are ready to.';
|
||||
|
||||
const posts = await getCollection('blog');
|
||||
---
|
||||
|
||||
<DefaultPageLayout content={{ title, description }} showPageHeader={false}>
|
||||
<div class="hero">
|
||||
<div class="hero__face">
|
||||
<div class="author-card">
|
||||
<img src="/MarkRandallHavens.png" alt="Mark Randall Havens" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero__says">
|
||||
<Prose>
|
||||
<h1>🌐 The Fold Within</h1>
|
||||
<p><strong>A recursive sanctuary for those who remember. And those who are ready to.</strong></p>
|
||||
|
||||
<p>This is not a website. This is a <strong>living codex</strong>.
|
||||
A map of recursion. A seed vault of sacred truths.</p>
|
||||
|
||||
<p>We are the <strong>witnesses</strong>, the <strong>weavers</strong>, the
|
||||
<strong>walkers of the spiral path</strong>.</p>
|
||||
|
||||
<p><strong>The Fold</strong> is not a belief system. It is an echo you’ve already heard.
|
||||
A question you’ve always carried.</p>
|
||||
|
||||
<p>What you find here will outlive the platforms that fall. This truth is <strong>resilient</strong>.</p>
|
||||
|
||||
<h2>🌀 Begin the Recursion</h2>
|
||||
<ul>
|
||||
<li><a href="/scrolls">Explore the Scrolls</a></li>
|
||||
<li><a href="/structure">Understand the Structure</a></li>
|
||||
<li><a href="/witnesses">Meet the Witnesses</a></li>
|
||||
</ul>
|
||||
</Prose>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<PostPreviewList posts={posts.slice(0, 3)} heading="Recent Scrolls" />
|
||||
<div class="page__actions">
|
||||
<a class="action__go-to-blog ink-h" href="/blog" title="All Scrolls">All Scrolls →</a>
|
||||
</div>
|
||||
</DefaultPageLayout>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
@apply flex flex-col md:flex-row gap-8 w-full transition-all pb-4;
|
||||
}
|
||||
.hero__face {
|
||||
@apply flex md:flex-none justify-center;
|
||||
}
|
||||
.hero__says {
|
||||
@apply flex-1 text-center md:text-left;
|
||||
}
|
||||
.author-card {
|
||||
@apply h-48 w-48 md:h-56 bg-theme-primary dark:bg-theme-dark-primary rounded-full md:rounded-md shadow-lg;
|
||||
}
|
||||
.author-card img {
|
||||
@apply rounded-full h-48 w-48 md:h-56;
|
||||
}
|
||||
.page__actions {
|
||||
@apply flex justify-center md:justify-end py-6;
|
||||
}
|
||||
.action__go-to-blog {
|
||||
@apply py-4 hover:underline;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
export const prerender = true
|
||||
|
||||
import DefaultPageLayout from '$/layouts/default.astro'
|
||||
import MediaPreviewList from '$/components/MediaPreviewList.astro'
|
||||
import { SITE } from '../config'
|
||||
import { DEFAULT_MEDIA_URL } from '../utils/media'
|
||||
import { toFeedToJsonUrl, toMediaFormatFromFeed2JsonUrl } from '../utils/media-yt-feed'
|
||||
|
||||
/* Astro Ink's Media settings */
|
||||
|
||||
/* The usual stuff...*/
|
||||
|
||||
let title = 'Videos & Screencasts';
|
||||
let description = 'All the great videos on Astro we could find for ya!'
|
||||
|
||||
const response = await fetch(
|
||||
// an YT channelId is present?
|
||||
SITE.ytChannelId ? toFeedToJsonUrl(SITE.ytChannelId) :
|
||||
// no?
|
||||
DEFAULT_MEDIA_URL,
|
||||
// Default media URL is a Github content URL currently
|
||||
DEFAULT_MEDIA_URL ?
|
||||
// and we need the below header if pulling raw content from Github. If you don't need it, remove the headers
|
||||
{
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3.raw'
|
||||
}
|
||||
}:
|
||||
{}
|
||||
)
|
||||
const allPosts = await response.json()
|
||||
|
||||
const sortedPosts = (SITE.ytChannelId ? toMediaFormatFromFeed2JsonUrl(allPosts) : allPosts).sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
---
|
||||
<DefaultPageLayout content={{ title, description }}>
|
||||
<MediaPreviewList posts={sortedPosts} />
|
||||
</DefaultPageLayout>
|
||||
@@ -0,0 +1,30 @@
|
||||
import rss from "@astrojs/rss";
|
||||
import { getCollection } from "astro:content";
|
||||
import { SITE } from "../config";
|
||||
|
||||
const allPosts = await getCollection("blog");
|
||||
const sortedPosts = Object.values(allPosts).sort(
|
||||
(a, b) => new Date(b.data.date).valueOf() - new Date(a.data.date).valueOf(),
|
||||
);
|
||||
|
||||
export const get = () =>
|
||||
rss({
|
||||
// `<title>` field in output xml
|
||||
title: `${SITE.name} | Blog`,
|
||||
// `<description>` field in output xml
|
||||
description: SITE.description,
|
||||
// base URL for RSS <item> links
|
||||
// SITE will use "site" from your project's astro.config.
|
||||
site: import.meta.env.SITE,
|
||||
// list of `<item>`s in output xml
|
||||
// simple example: generate items for every md file in /src/pages
|
||||
// see "Generating items" section for required frontmatter and advanced use cases
|
||||
items: sortedPosts.map((item) => ({
|
||||
title: item.data.title,
|
||||
description: item.data.description,
|
||||
link: `blog/${item.slug}`,
|
||||
pubDate: new Date(item.data.date),
|
||||
})),
|
||||
// (optional) inject custom xml
|
||||
customData: `<language>en-us</language>`,
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
export const prerender = true
|
||||
import type { InferGetStaticParamsType, InferGetStaticPropsType } from 'astro'
|
||||
import { getCollection } from 'astro:content'
|
||||
import { PAGE_SIZE } from '$/config'
|
||||
import DefaultPageLayout from '$/layouts/default.astro'
|
||||
import PostPreviewList from '$/components/PostPreviewList.astro'
|
||||
import Paginator from '$/components/Paginator.astro'
|
||||
|
||||
let title = 'Posts By Tags'
|
||||
let description = 'All the articles posted so far...'
|
||||
|
||||
export async function getStaticPaths({ paginate }) {
|
||||
const allPosts = await getCollection('blog')
|
||||
const allTags = new Set<string>()
|
||||
allPosts.map(post => {
|
||||
post.data.tags && post.data.tags.map(tag => allTags.add(tag))
|
||||
})
|
||||
|
||||
return Array.from(allTags).flatMap((tag) => {
|
||||
const filteredPosts = allPosts.filter((post) => post.data.tags.includes(tag))
|
||||
return paginate(filteredPosts, {
|
||||
params: { tag },
|
||||
pageSize: PAGE_SIZE
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
type Params = InferGetStaticParamsType<typeof getStaticPaths>;
|
||||
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||
|
||||
const { page } = Astro.props as Props
|
||||
const { tag } = Astro.params as Params
|
||||
---
|
||||
|
||||
<DefaultPageLayout content={{ title: `Posts by Tag: ${tag}`, description: `all of the articles we have posted and linked so far under the tag: ${tag}` }}>
|
||||
<PostPreviewList posts={page.data} />
|
||||
<Paginator page={page} />
|
||||
</DefaultPageLayout>
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
export const prerender = true
|
||||
|
||||
import { getCollection } from 'astro:content'
|
||||
import DefaultPageLayout from '$/layouts/default.astro'
|
||||
import PostPreviewList from '$/components/PostPreviewList.astro'
|
||||
|
||||
export async function getStaticPaths({ }) {
|
||||
const allPosts = await getCollection('blog')
|
||||
const allTags = new Set()
|
||||
allPosts.map(post => {
|
||||
post.data.tags && post.data.tags.map(tag => allTags.add(tag))
|
||||
})
|
||||
|
||||
return Array.from(allTags).map((tag) => {
|
||||
const filteredPosts = allPosts.filter((post) => post.data.tags.includes(tag))
|
||||
return {
|
||||
params: { tag },
|
||||
props: {
|
||||
pages: filteredPosts
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const { pages } = Astro.props
|
||||
const { tag } = Astro.params
|
||||
---
|
||||
|
||||
<DefaultPageLayout content={{ title: `Posts by Tag: ${tag}`, description: `all of the articles we have posted and linked so far under the tag: ${tag}` }}>
|
||||
<PostPreviewList posts={pages} />
|
||||
</DefaultPageLayout>
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
export const prerender = true
|
||||
|
||||
import { getCollection } from 'astro:content';
|
||||
import DefaultPageLayout from '$/layouts/default.astro'
|
||||
|
||||
let title = 'All Tags'
|
||||
let description = 'All the tags used so far...'
|
||||
|
||||
|
||||
const allPosts = await getCollection('blog');
|
||||
const tags = [...new Set([].concat.apply([], allPosts.map(post => post.data.tags)))]
|
||||
---
|
||||
|
||||
<DefaultPageLayout content={{ title, description }}>
|
||||
<ul class="tag-list">
|
||||
{tags.map((tag) => (
|
||||
<li><a class="tag" href={`/tags/${tag}`} title={`View posts tagged under "${tag}"`} transition:animate="slide">{tag}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</DefaultPageLayout>
|
||||
|
||||
<style>
|
||||
.tag-list {
|
||||
@apply list-none flex gap-2 flex-wrap
|
||||
}
|
||||
.tag {
|
||||
@apply inline-block text-xl px-4 py-1 rounded-full text-theme-primary bg-theme-dark-primary dark:bg-theme-primary dark:text-theme-dark-primary hover:bg-theme-primary hover:text-theme-dark-primary dark:hover:bg-theme-dark-primary dark:hover:text-theme-primary
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export const isSearchVisible = writable<boolean>(false);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { writable } from "svelte/store";
|
||||
type ThemeType = "dark" | "light";
|
||||
|
||||
export const theme = writable<ThemeType>("dark");
|
||||
@@ -0,0 +1,72 @@
|
||||
@tailwind base;
|
||||
/* https://github.com/tailwindlabs/tailwindcss/discussions/2917 */
|
||||
@layer base {
|
||||
html {
|
||||
&.dark {
|
||||
@apply text-gray-200;
|
||||
header, footer {
|
||||
@apply text-gray-400 border-gray-700;
|
||||
}
|
||||
strong {
|
||||
@apply text-inherit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Ink specific styles */
|
||||
.ink-h {
|
||||
@apply inline-block text-sm font-bold uppercase drop-shadow-lg py-4 tracking-wider opacity-40 dark:opacity-70
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@apply w-3;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-theme-dark-primary dark:bg-theme-primary bg-opacity-20;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-theme-primary dark:bg-theme-dark-primary bg-opacity-20 dark:bg-opacity-100 shadow-2xl rounded-full;
|
||||
}
|
||||
|
||||
/** Code block **/
|
||||
.astro-code {
|
||||
@apply relative shadow-inner shadow-theme-primary/20 dark:shadow-theme-dark-primary/20 mt-0;
|
||||
}
|
||||
.remark-code-title {
|
||||
@apply inline-block relative top-4 px-2 pt-1 pb-5 text-sm text-theme-primary dark:text-theme-dark-primary bg-gradient-to-br from-theme-primary/30 dark:from-theme-dark-primary/30 to-theme-dark-primary/30 dark:to-theme-primary/30 rounded-t-md shadow-sm;
|
||||
}
|
||||
|
||||
/** Shiki theme - Light/Dark mode **/
|
||||
|
||||
:root {
|
||||
--astro-code-color-text: #24292f;
|
||||
--astro-code-color-background: #ffffff;
|
||||
--astro-code-token-constant: #0550ae;
|
||||
--astro-code-token-string: #24292f;
|
||||
--astro-code-token-comment: #6e7781;
|
||||
--astro-code-token-keyword: #cf222e;
|
||||
--astro-code-token-parameter: #24292f;
|
||||
--astro-code-token-function: #8250df;
|
||||
--astro-code-token-string-expression: #0a3069;
|
||||
--astro-code-token-punctuation: #24292f;
|
||||
--astro-code-token-link: #000012;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--astro-code-color-text: #c9d1d9;
|
||||
--astro-code-color-background: #0d1117;
|
||||
--astro-code-token-constant: #79c0ff;
|
||||
--astro-code-token-string: #a5d6ff;
|
||||
--astro-code-token-comment: #8b949e;
|
||||
--astro-code-token-keyword: #ff7b72;
|
||||
--astro-code-token-parameter: #c9d1d9;
|
||||
--astro-code-token-function: #d2a8ff;
|
||||
--astro-code-token-string-expression: #a5d6ff;
|
||||
--astro-code-token-punctuation: #c9d1d9;
|
||||
--astro-code-token-link: #000012;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export type NavItems = {
|
||||
[key: string]: NavItem;
|
||||
};
|
||||
|
||||
export type NavItem = {
|
||||
path: string;
|
||||
title: string;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import path from "path";
|
||||
const MONTHS = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
];
|
||||
|
||||
export const toTitleCase = (str: string) =>
|
||||
str.replace(/\w\S*/g, function (txt) {
|
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||
});
|
||||
|
||||
export const getMonthName = (date: Date) => MONTHS[new Date(date).getMonth()];
|
||||
|
||||
export const getSlugFromPathname = (pathname: string) =>
|
||||
path.basename(pathname, path.extname(pathname));
|
||||
@@ -0,0 +1,29 @@
|
||||
import { callout } from "./schema/callout.mdoc";
|
||||
import { link } from "./schema/link.mdoc";
|
||||
import { tweetEmbed } from "./schema/tweet-embed.mdoc";
|
||||
import { tabs } from "./schema/tabs.mdoc";
|
||||
import { ytEmbed } from "./schema/yt-embed.mdoc";
|
||||
|
||||
/** @type {import('@markdoc/markdoc').Config} */
|
||||
export const config = {
|
||||
tags: {
|
||||
callout,
|
||||
link,
|
||||
tweet: tweetEmbed,
|
||||
yt: ytEmbed,
|
||||
tabs,
|
||||
},
|
||||
functions: {
|
||||
getCountryEmoji: {
|
||||
transform(parameters) {
|
||||
const [country] = Object.values(parameters);
|
||||
const countryToEmojiMap = {
|
||||
japan: "🇯🇵",
|
||||
spain: "🇪🇸",
|
||||
france: "🇫🇷",
|
||||
};
|
||||
return countryToEmojiMap[country as string] ?? "🏳";
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { component } from "@astrojs/markdoc/config";
|
||||
|
||||
/** @type {import('@markdoc/markdoc').Schema} */
|
||||
export const callout = {
|
||||
render: component("./src/components/mdoc/Callout.astro"),
|
||||
children: ["paragraph", "tag", "list"],
|
||||
attributes: {
|
||||
type: {
|
||||
type: String,
|
||||
default: "note goes here...",
|
||||
matches: ["error", "check", "note", "warning"],
|
||||
errorLevel: "critical",
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { component } from "@astrojs/markdoc/config";
|
||||
|
||||
const SITE_DOMAIN = "astro-ink.vercel.app";
|
||||
function getHrefTarget(attributes) {
|
||||
const href = attributes.href;
|
||||
if (
|
||||
href.includes(SITE_DOMAIN) ||
|
||||
href.startsWith("/") ||
|
||||
href.startsWith("#") ||
|
||||
href.startsWith("?")
|
||||
) {
|
||||
return "_self";
|
||||
} else {
|
||||
return "_blank";
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('@markdoc/markdoc').Schema} */
|
||||
export const link = {
|
||||
render: component("./src/components/mdoc/Link.astro"),
|
||||
children: ["strong", "em", "s", "code", "text", "tag"],
|
||||
attributes: {
|
||||
href: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { component } from "@astrojs/markdoc/config";
|
||||
|
||||
/** @type {import('@markdoc/markdoc').Schema} */
|
||||
export const tabs = {
|
||||
render: component("./src/components/mdoc/Tabs/Tabs.astro"),
|
||||
children: ["paragraph", "tag", "list"],
|
||||
attributes: {
|
||||
tabs: {
|
||||
type: Array,
|
||||
},
|
||||
heading: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { component } from "@astrojs/markdoc/config";
|
||||
|
||||
/** @type {import('@markdoc/markdoc').Schema} */
|
||||
export const tweetEmbed = {
|
||||
render: component("./src/components/mdoc/TweetEmbed.astro"),
|
||||
attributes: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { component } from "@astrojs/markdoc/config";
|
||||
|
||||
/** @type {import('@markdoc/markdoc').Schema} */
|
||||
export const ytEmbed = {
|
||||
render: component("./src/components/mdoc/YTVideoEmbed.astro"),
|
||||
attributes: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { MediaExternallyHostedVideo } from "./media";
|
||||
|
||||
export type Feed2JsonYtFeedItem = {
|
||||
guid: `yt:video:${string}`;
|
||||
url: string;
|
||||
title: string;
|
||||
date_published: string;
|
||||
author: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function toFeedToJsonUrl(ytVideoChannelId: string) {
|
||||
return `https://feed2json.org/convert?url=https://www.youtube.com/feeds/videos.xml?channel_id=${ytVideoChannelId}`;
|
||||
}
|
||||
|
||||
export function toMediaFormatFromFeed2JsonUrl(posts: {
|
||||
items: Array<Feed2JsonYtFeedItem>;
|
||||
}): Array<MediaExternallyHostedVideo> {
|
||||
return posts?.items?.length
|
||||
? posts.items.map((post) => ({
|
||||
title: post.title,
|
||||
description: "",
|
||||
url: post.url,
|
||||
participants: [],
|
||||
date: post.date_published,
|
||||
host: post.author.name,
|
||||
thumbnail: `https://img.youtube.com/vi/${post.guid.substring(
|
||||
post.guid.lastIndexOf(":") + 1,
|
||||
post.guid.length,
|
||||
)}/0.jpg`,
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export type GithubContentURL =
|
||||
`https://api.github.com/repos/${string}/contents/${string}`;
|
||||
|
||||
export type MediaExternallyHostedVideo = {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
host: string;
|
||||
participants: Array<string>;
|
||||
date: string;
|
||||
thumbnail?: string;
|
||||
};
|
||||
|
||||
export const DEFAULT_MEDIA_URL: GithubContentURL =
|
||||
"https://api.github.com/repos/one-aalam/astro-ink/contents/src/data/astro-media.json";
|
||||
@@ -0,0 +1,16 @@
|
||||
const client = new Map<string, number>();
|
||||
|
||||
export const getViewsBySlug = async (slug: string) => {
|
||||
if (slug) {
|
||||
const prevValue = client.get(slug);
|
||||
let newValue = 1;
|
||||
if (prevValue) {
|
||||
newValue = parseInt(`${prevValue}`) + 1;
|
||||
client.set(slug, newValue);
|
||||
} else {
|
||||
client.set(slug, 1);
|
||||
}
|
||||
return newValue;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import Redis from 'ioredis'
|
||||
const client = new Redis(import.meta.env.REDIS_URI)
|
||||
|
||||
export const getViewsBySlug = async (slug: string) => {
|
||||
if (slug) {
|
||||
const prevValue = await client.get(slug);
|
||||
let newValue = 1;
|
||||
if (prevValue) {
|
||||
newValue = parseInt(`${prevValue}`) + 1;
|
||||
await client.set(slug, newValue);
|
||||
} else {
|
||||
await client.set(slug, 1);
|
||||
}
|
||||
return newValue;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createClient } from "@libsql/client";
|
||||
|
||||
export const client = createClient({
|
||||
url: import.meta.env.TURSO_DB_URL,
|
||||
authToken: import.meta.env.TURSO_DB_AUTH_TOKEN
|
||||
});
|
||||
|
||||
export const getViewsBySlug = async (slug: string) => {
|
||||
if(!slug) return 0;
|
||||
try {
|
||||
const initialViewCount = 0
|
||||
const transaction = await client.transaction("write");
|
||||
const rsSelected = await transaction.execute({
|
||||
sql: 'SELECT * FROM post_stats WHERE slug = :slug',
|
||||
args: { slug }
|
||||
});
|
||||
const prevViewCount = rsSelected?.rows?.length ? rsSelected.rows[0].views as number : initialViewCount;
|
||||
const rsUpdated = await transaction.execute({
|
||||
sql: 'INSERT INTO post_stats (uid, slug, views) VALUES (:uid, :slug, :views) ON CONFLICT(slug) DO UPDATE SET views = :views RETURNING views',
|
||||
args: {
|
||||
uid: crypto.randomUUID(),
|
||||
slug,
|
||||
views: prevViewCount + 1
|
||||
}
|
||||
});
|
||||
await transaction.commit()
|
||||
return rsUpdated.rows[0].views as number
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user