mirror of
https://github.com/jaronpate/folio.git
synced 2026-10-19 01:07:39 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c4a878c12 | |||
| b095e0451d | |||
| a788c71edf |
@@ -1,20 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { contentDateAttr, formatContentDate } from '~/utils/dates';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
pages: Array<{
|
pages: Array<{
|
||||||
path: string;
|
path: string;
|
||||||
title: string;
|
title: string;
|
||||||
date: string | Date;
|
date: string;
|
||||||
}>;
|
}>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const formatDate = (dateString: string | Date) => {
|
|
||||||
return new Date(dateString).toLocaleDateString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
timeZone: 'UTC',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -27,8 +20,8 @@ const formatDate = (dateString: string | Date) => {
|
|||||||
>
|
>
|
||||||
<span class="listing__title">{{ page.title }}</span>
|
<span class="listing__title">{{ page.title }}</span>
|
||||||
<span class="listing__rule" aria-hidden="true"></span>
|
<span class="listing__rule" aria-hidden="true"></span>
|
||||||
<time :datetime="new Date(page.date).toISOString()">{{
|
<time :datetime="contentDateAttr(page.date)">{{
|
||||||
formatDate(page.date)
|
formatContentDate(page.date)
|
||||||
}}</time>
|
}}</time>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
isExternalHref,
|
isExternalHref,
|
||||||
projectEntryHref,
|
projectEntryHref,
|
||||||
projectOgImage,
|
projectOgImage,
|
||||||
|
sortProjects,
|
||||||
} from '~/utils/projects';
|
} from '~/utils/projects';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -30,7 +31,7 @@ const folderName = computed(
|
|||||||
const { data } = await useAsyncData(
|
const { data } = await useAsyncData(
|
||||||
() => `projects:${route.path}`,
|
() => `projects:${route.path}`,
|
||||||
async () => {
|
async () => {
|
||||||
const all = await queryCollection('projects').order('title', 'ASC').all();
|
const all = await queryCollection('projects').all();
|
||||||
const visible = all.filter((page) => import.meta.dev || !page.hidden);
|
const visible = all.filter((page) => import.meta.dev || !page.hidden);
|
||||||
const project = visible.find(
|
const project = visible.find(
|
||||||
(page) =>
|
(page) =>
|
||||||
@@ -44,7 +45,9 @@ const { data } = await useAsyncData(
|
|||||||
const prefix = contentPath.value.endsWith('/')
|
const prefix = contentPath.value.endsWith('/')
|
||||||
? contentPath.value
|
? contentPath.value
|
||||||
: `${contentPath.value}/`;
|
: `${contentPath.value}/`;
|
||||||
const pages = visible.filter((page) => page.path.startsWith(prefix));
|
const pages = sortProjects(
|
||||||
|
visible.filter((page) => page.path.startsWith(prefix)),
|
||||||
|
);
|
||||||
|
|
||||||
if (!pages.length) return null;
|
if (!pages.length) return null;
|
||||||
return { kind: 'folder' as const, project: null, pages };
|
return { kind: 'folder' as const, project: null, pages };
|
||||||
|
|||||||
@@ -6,11 +6,16 @@ import {
|
|||||||
personId,
|
personId,
|
||||||
websiteId,
|
websiteId,
|
||||||
} from '~/utils/site';
|
} from '~/utils/site';
|
||||||
import { isExternalHref, projectEntryHref } from '~/utils/projects';
|
import {
|
||||||
|
isExternalHref,
|
||||||
|
projectEntryHref,
|
||||||
|
sortProjects,
|
||||||
|
} from '~/utils/projects';
|
||||||
|
|
||||||
const { data: pages } = await useAsyncData('project-pages', async () => {
|
const { data: pages } = await useAsyncData('project-pages', async () => {
|
||||||
const all = await queryCollection('projects').order('title', 'ASC').all();
|
const all = await queryCollection('projects').all();
|
||||||
return import.meta.dev ? all : all.filter((page) => !page.hidden);
|
const visible = all.filter((page) => !page.hidden);
|
||||||
|
return sortProjects(visible);
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectsUrl = `${SITE_URL}/projects`;
|
const projectsUrl = `${SITE_URL}/projects`;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
} from '~/utils/fonts';
|
} from '~/utils/fonts';
|
||||||
import { BG_THEMES } from '~/utils/themes';
|
import { BG_THEMES } from '~/utils/themes';
|
||||||
import { SITE_URL } from '~/utils/site';
|
import { SITE_URL } from '~/utils/site';
|
||||||
|
import { sortProjects } from '~/utils/projects';
|
||||||
|
|
||||||
const TOKEN_NAMES = [
|
const TOKEN_NAMES = [
|
||||||
'--bg',
|
'--bg',
|
||||||
@@ -51,8 +52,8 @@ const { data: writingPages } = await useAsyncData(
|
|||||||
const { data: projectPages } = await useAsyncData(
|
const { data: projectPages } = await useAsyncData(
|
||||||
'styleguide-projects',
|
'styleguide-projects',
|
||||||
async () => {
|
async () => {
|
||||||
const all = await queryCollection('projects').order('title', 'ASC').all();
|
const all = await queryCollection('projects').all();
|
||||||
return all.filter((page) => !page.hidden).slice(0, 4);
|
return sortProjects(all.filter((page) => !page.hidden)).slice(0, 4);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { contentDateAttr, formatContentDate } from '~/utils/dates';
|
||||||
import {
|
import {
|
||||||
DEFAULT_IMAGE,
|
DEFAULT_IMAGE,
|
||||||
SITE_NAME,
|
SITE_NAME,
|
||||||
@@ -54,19 +55,11 @@ if (!data.value) {
|
|||||||
const page = computed(() => data.value?.article ?? null);
|
const page = computed(() => data.value?.article ?? null);
|
||||||
const isFolder = computed(() => data.value?.kind === 'folder');
|
const isFolder = computed(() => data.value?.kind === 'folder');
|
||||||
const articleTitle = page.value?.title ?? folderName.value;
|
const articleTitle = page.value?.title ?? folderName.value;
|
||||||
const formatDate = (date: Date | string) => {
|
|
||||||
return new Date(date).toLocaleDateString('en-US', {
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
timeZone: 'UTC',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const articleDescription = page.value
|
const articleDescription = page.value
|
||||||
? page.value.description
|
? page.value.description
|
||||||
: `Writing from ${folderName.value}`;
|
: `Writing from ${folderName.value}`;
|
||||||
const layoutDescription = page.value
|
const layoutDescription = page.value
|
||||||
? formatDate(page.value.date)
|
? formatContentDate(page.value.date, 'long')
|
||||||
: articleDescription;
|
: articleDescription;
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
@@ -83,10 +76,10 @@ useSeoMeta({
|
|||||||
twitterDescription: articleDescription,
|
twitterDescription: articleDescription,
|
||||||
twitterImage: DEFAULT_IMAGE,
|
twitterImage: DEFAULT_IMAGE,
|
||||||
articlePublishedTime: page.value
|
articlePublishedTime: page.value
|
||||||
? new Date(page.value.date).toISOString()
|
? contentDateAttr(page.value.date)
|
||||||
: undefined,
|
: undefined,
|
||||||
articleModifiedTime: page.value
|
articleModifiedTime: page.value
|
||||||
? new Date(page.value.date).toISOString()
|
? contentDateAttr(page.value.date)
|
||||||
: undefined,
|
: undefined,
|
||||||
articleAuthor: page.value ? [SITE_NAME] : undefined,
|
articleAuthor: page.value ? [SITE_NAME] : undefined,
|
||||||
});
|
});
|
||||||
@@ -131,12 +124,8 @@ useHead({
|
|||||||
url: articleUrl,
|
url: articleUrl,
|
||||||
headline: articleTitle,
|
headline: articleTitle,
|
||||||
description: articleDescription,
|
description: articleDescription,
|
||||||
datePublished: new Date(
|
datePublished: contentDateAttr(page.value.date),
|
||||||
page.value.date,
|
dateModified: contentDateAttr(page.value.date),
|
||||||
).toISOString(),
|
|
||||||
dateModified: new Date(
|
|
||||||
page.value.date,
|
|
||||||
).toISOString(),
|
|
||||||
image: {
|
image: {
|
||||||
'@type': 'ImageObject',
|
'@type': 'ImageObject',
|
||||||
'@id': `${articleUrl}#article-image`,
|
'@id': `${articleUrl}#article-image`,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
|
|
||||||
const { data: pages } = await useAsyncData('writing-pages', async () => {
|
const { data: pages } = await useAsyncData('writing-pages', async () => {
|
||||||
const all = await queryCollection('writing').order('date', 'DESC').all();
|
const all = await queryCollection('writing').order('date', 'DESC').all();
|
||||||
return import.meta.dev ? all : all.filter((page) => !page.hidden);
|
return all.filter((page) => !page.hidden);
|
||||||
});
|
});
|
||||||
|
|
||||||
const writingUrl = `${SITE_URL}/writing`;
|
const writingUrl = `${SITE_URL}/writing`;
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
const YMD = /^(\d{4})-(\d{2})-(\d{2})/;
|
||||||
|
const HAS_TZ = /(?:Z|[+-]\d{2}:?\d{2})$/i;
|
||||||
|
|
||||||
|
export function formatContentDate(value: string, month: 'short' | 'long' = 'short') {
|
||||||
|
const match = YMD.exec(value);
|
||||||
|
if (!match) return '';
|
||||||
|
return new Date(Date.UTC(+match[1], +match[2] - 1, +match[3], 12)).toLocaleDateString(
|
||||||
|
'en-US',
|
||||||
|
{ month, day: 'numeric', year: 'numeric', timeZone: 'UTC' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contentDateAttr(value: string) {
|
||||||
|
if (HAS_TZ.test(value)) return new Date(value).toISOString();
|
||||||
|
return YMD.exec(value)?.[0] ?? '';
|
||||||
|
}
|
||||||
@@ -5,6 +5,18 @@ export function projectEntryHref(page: {
|
|||||||
return page.href || `/projects${page.path}`;
|
return page.href || `/projects${page.path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sortProjects<T extends { title: string; date?: string | null }>(
|
||||||
|
pages: T[],
|
||||||
|
) {
|
||||||
|
return [...pages].sort((a, b) => {
|
||||||
|
if (!a.date && !b.date) return a.title.localeCompare(b.title);
|
||||||
|
if (!a.date) return 1;
|
||||||
|
if (!b.date) return -1;
|
||||||
|
if (a.date !== b.date) return b.date.localeCompare(a.date);
|
||||||
|
return a.title.localeCompare(b.title);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function isExternalHref(href: string) {
|
export function isExternalHref(href: string) {
|
||||||
return /^https?:\/\//i.test(href);
|
return /^https?:\/\//i.test(href);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -10,7 +10,7 @@ export default defineContentConfig({
|
|||||||
},
|
},
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
date: z.coerce.date(),
|
date: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
hidden: z.boolean().optional(),
|
hidden: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
@@ -26,6 +26,7 @@ export default defineContentConfig({
|
|||||||
description: z.string(),
|
description: z.string(),
|
||||||
href: z.string().optional(),
|
href: z.string().optional(),
|
||||||
hidden: z.boolean().optional(),
|
hidden: z.boolean().optional(),
|
||||||
|
date: z.string().optional(),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
title: Mystwright
|
title: Mystwright
|
||||||
description: An AI-driven mystery detective game. Generate, investigate, solve.
|
description: An AI-driven mystery detective game. Generate, investigate, solve.
|
||||||
href: https://mystwright.com/
|
href: https://mystwright.com/
|
||||||
|
hidden: true
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: Unicord 🦄
|
title: Unicord 🦄
|
||||||
description: A Discord bot SDK that works like a web server.
|
description: A Discord bot SDK that works like a web server.
|
||||||
# href: https://unicord.hat.fish
|
# href: https://unicord.hat.fish
|
||||||
|
date: 2025-01-12
|
||||||
---
|
---
|
||||||
|
|
||||||
I wanted writing a Discord bot to feel like writing a small web app. Register a handler, get a request, send a reply. *Bang!...bot done.* This became particularly interesting once bots could be served over HTTP.
|
I wanted writing a Discord bot to feel like writing a small web app. Register a handler, get a request, send a reply. *Bang!...bot done.* This became particularly interesting once bots could be served over HTTP.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: AI and the Barrier of Entry
|
title: AI and the Barrier of Entry
|
||||||
date: 2023-03-25T12:00:00.000Z
|
date: 2023-03-25T12:00:00.000Z
|
||||||
description: We are witnessing the next major wave of technological innovation. And it's never been easier to be a part of it.
|
description: We are witnessing the next major wave of technological innovation. And it's never been easier to be a part of it.
|
||||||
|
hidden: true
|
||||||
---
|
---
|
||||||
|
|
||||||
To be fair the term itself, **AI**, has had its meaning stretched pretty thin in the last few years. :margin-note[(Artificial Intelligence. The label has been doing too much work.)] It's been used to describe everything from a simple chatbot to a self driving car. That use is extremely broad. It doesn't really describe the space we think of as "AI".
|
To be fair the term itself, **AI**, has had its meaning stretched pretty thin in the last few years. :margin-note[(Artificial Intelligence. The label has been doing too much work.)] It's been used to describe everything from a simple chatbot to a self driving car. That use is extremely broad. It doesn't really describe the space we think of as "AI".
|
||||||
|
|||||||
Reference in New Issue
Block a user