first commit

This commit is contained in:
Troy 2024-12-23 21:18:55 +00:00
commit ff7c974867
Signed by: troy
GPG key ID: DFC06C02ED3B4711
227 changed files with 12908 additions and 0 deletions

View file

@ -0,0 +1,35 @@
---
import { Icon } from "astro-icon/components";
type Props = {
institution: String;
qualification: String;
grades: Array<String>;
isOpen?: boolean;
};
const { institution, qualification, grades, isOpen = false } = Astro.props;
---
<div class="grid">
<details open={isOpen === true ? "open" : null} class="group">
<summary
class="flex cursor-pointer items-center justify-between py-3 font-bold"
>
<p class="m-0">
{institution}
</p>
<span class="transition group-open:rotate-180">
<Icon name="mdi:chevron-down" class="h-6 w-auto text-tertiary" />
</span>
</summary>
<div class="p-4 text-sm text-neutral-600 dark:text-neutral-400">
<p class="my-0">
{qualification}
</p>
<ul>
{grades.map((grade) => <li>{grade}</li>)}
</ul>
</div>
</details>
</div>

View file

@ -0,0 +1,106 @@
---
import Layout from "@layouts/Layout.astro";
import Prose from "@components/Prose.astro";
import FormattedDate from "@components/FormattedDate.astro";
import { readingTime } from "@lib/utils";
import { Icon } from "astro-icon/components";
const { article, isPost = false } = Astro.props;
const { Content } = await article.render();
let datesMatch = false;
if (article.data.date.getTime() == article.data.updated?.getTime()) {
datesMatch = true;
}
const listFormatter = new Intl.ListFormat("en-GB", {
style: "long",
type: "conjunction",
});
---
<Layout
title={article.data.title}
description={article.data.description}
image={article.data.image.url.src}
date={article.data.date}
updated={article.data.updated}
tags={article.data.tags}
>
<div class="mx-auto mb-16 max-w-prose">
<h1
class="animate-reveal break-words text-start text-4xl font-medium opacity-0"
>
{article.data.title}
</h1>
<div
class="flex animate-reveal flex-col items-start opacity-0 [animation-delay:0.3s]"
>
<div
class="mt-4 flex flex-col items-start gap-2 text-lg text-tertiary md:flex-row"
>
<div class="flex items-center gap-2">
<Icon name="mdi:calendar" />
{
datesMatch ? (
<p title="Date">
<FormattedDate date={article.data.date} />
</p>
) : (
<>
<p title="Date">
<FormattedDate date={article.data.date} />
</p>
<Icon name="mdi:trending-up" />
<p title="Updated">
<FormattedDate date={article.data.updated} />
</p>
</>
)
}
</div>
{
isPost ? (
<div class="flex items-center gap-2">
<Icon name="mdi:timer" />
<p title="Word count">{readingTime(article.body)}</p>
</div>
) : null
}
</div>
{
article.data.extraAuthors ? (
<div class="mt-2 flex items-center gap-2 text-tertiary">
<p>
In collaboration with{" "}
{listFormatter.format(article.data.extraAuthors)}
</p>
</div>
) : null
}
<ul class="mt-4 flex flex-wrap gap-1">
{
article.data.categories.map((category: string) => (
<li class="rounded border border-accent bg-accent/50 px-1 py-0.5 text-sm capitalize text-primary invert">
{category}
</li>
))
}
{
article.data.tags.map((tag: string) => (
<li class="rounded border border-accent bg-accent/50 px-1 py-0.5 text-sm capitalize text-secondary">
{tag}
</li>
))
}
</ul>
</div>
</div>
<div
class="mx-auto max-w-prose animate-reveal opacity-0 [animation-delay:0.6s]"
>
<Prose>
<Content />
</Prose>
</div>
</Layout>

View file

@ -0,0 +1,30 @@
---
import { getCollection } from "astro:content";
import { dateRange } from "@lib/utils";
import Accordion from "@components/Accordion.astro";
const collection = (await getCollection("education")).sort(
(a, b) =>
new Date(b.data.dateStart).valueOf() - new Date(a.data.dateStart).valueOf(),
);
const education = await Promise.all(
collection.map(async (item) => {
const { Content } = await item.render();
return { ...item, Content };
}),
);
---
<div>
{
education.map((entry) => (
<Accordion
institution={`${entry.data.institution} (${dateRange(entry.data.dateStart, entry.data.dateEnd)})`}
qualification={entry.data.qualification}
grades={entry.data.grades}
isOpen={entry.data.isOpen}
/>
))
}
</div>

View file

@ -0,0 +1,64 @@
---
import { SITE } from "@consts";
import Link from "@components/Link.astro";
import { Icon } from "astro-icon/components";
---
<footer class="mt-auto">
<div class="mx-auto w-full max-w-screen-lg p-4 py-6 lg:py-8">
<div class="md:flex md:justify-between">
<div class="mb-6 text-secondary md:mb-0">
<a class="inline-flex items-center" href="#top" title="Back to top">
<Icon name="icon" title={SITE.TITLE} class="h-8 w-auto ease-in-out" />
<div
class="ml-2 hidden flex-none text-sm font-bold capitalize md:visible lg:block"
>
Troy Lusty
</div>
</a>
</div>
<div class="text-left sm:gap-6 md:text-right">
<div>
<h2 class="mb-6 text-sm font-semibold uppercase text-secondary">
Sections
</h2>
<ul class="font-medium text-tertiary">
{
SITE.NAVLINKS.map((i) => (
<li class="mb-4 last:mb-0">
<a
data-navlink
href={i.href}
class="capitalize hover:text-secondary"
>
{i.name}
</a>
</li>
))
}
</ul>
</div>
</div>
</div>
<div class="mt-12 sm:flex sm:items-center sm:justify-between lg:mt-16">
<span class="text-sm text-tertiary sm:text-center"
>&copy; {new Date().getFullYear()}
<a href="/" class="hover:text-secondary">{SITE.TITLE}</a>. All Rights
Reserved.
</span>
<div class="mt-4 flex gap-5 sm:mt-0 sm:justify-center">
{
SITE.LINKS.map((i) => (
<Link href={i.href}>
<Icon
name={i.icon}
title={i.name}
class="h-5 w-5 text-tertiary hover:text-secondary"
/>
</Link>
))
}
</div>
</div>
</div>
</footer>

View file

@ -0,0 +1,17 @@
---
interface Props {
date?: Date;
}
const { date = new Date() } = Astro.props;
---
<time datetime={date.toISOString()}>
{
date.toLocaleDateString("en-GB", {
day: "2-digit",
month: "short",
year: "numeric",
})
}
</time>

121
src/components/Head.astro Normal file
View file

@ -0,0 +1,121 @@
---
import { SITE } from "@consts";
import gradient from "../../public/assets/gradient.avif";
interface Props {
title: string;
description: string;
image?: string;
date?: Date;
updated?: Date;
tags?: Array<string>;
}
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image = gradient.src, date, updated } = Astro.props;
let { tags } = Astro.props;
if (typeof tags !== "undefined") {
tags = SITE.KEYWORDS.concat(tags);
}
import inter from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url";
import redhatmono from "@fontsource-variable/red-hat-mono/files/red-hat-mono-latin-wght-normal.woff2?url";
---
<head>
<!-- Global Metadata -->
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta content="True" name="HandheldFriendly" />
<meta content="en-gb" name="lang" />
<!-- Favicon -->
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/favicon.ico" sizes="32x32" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<!-- Generator -->
<meta name="generator" content={Astro.generator} />
<!-- Author -->
<meta content={SITE.AUTHOR} name="author" />
<!-- Sitemap -->
<link rel="sitemap" href="/sitemap-index.xml" />
<!-- RSS -->
<link
rel="alternate"
type="application/rss+xml"
title={SITE.TITLE}
href="/rss.xml"
}
/>
<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<!-- Keywords -->
<meta
content={tags ? tags?.toString() : SITE.KEYWORDS.toString()}
name="keywords"
/>
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, Astro.url)} />
<meta property="og:site_name" content={SITE.TITLE} />
{
date ? (
<meta property="article:published_time" content={date.toISOString()} />
) : null
}
{
updated ? (
<meta property="article:modified_time" content={updated.toISOString()} />
) : null
}
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image, Astro.url)} />
<!-- View Transitions -->
<style>
@view-transition {
navigation: auto;
}
</style>
<!-- Disable Dark Reader Statically -->
<meta name="darkreader-lock" />
<!-- Font Preload -->
<link
rel="preload"
as="font"
type="font/woff2"
href={inter}
crossorigin="anonymous"
/>
<link
rel="preload"
as="font"
type="font/woff2"
href={redhatmono}
crossorigin="anonymous"
/>
</head>

View file

@ -0,0 +1,19 @@
---
import { SITE } from "@consts";
import { Icon } from "astro-icon/components";
---
<header id="header" class="mx-auto w-full max-w-screen-lg p-4">
<div
class="flex h-12 items-center justify-between leading-[0px] text-secondary"
>
<a class="inline-flex items-center" href="/" title={SITE.TITLE}>
<Icon name="icon" title={SITE.TITLE} class="h-8 w-auto ease-in-out" />
<div
class="ml-2 hidden flex-none text-sm font-bold capitalize md:visible lg:block"
>
Troy Lusty
</div>
</a>
</div>
</header>

20
src/components/Link.astro Normal file
View file

@ -0,0 +1,20 @@
---
import type { HTMLAttributes } from "astro/types";
interface Props extends HTMLAttributes<"a"> {
href: string;
external?: boolean;
class?: string;
}
const { href, external = true, ...rest } = Astro.props;
---
<a
href={href}
rel={external ? "noopener nofollow noreferrer" : ""}
target={external ? "_blank" : "_self"}
{...rest}
>
<slot />
</a>

View file

@ -0,0 +1,5 @@
<div
class="prose max-w-full prose-headings:text-secondary prose-h1:text-xl prose-h1:font-bold prose-p:max-w-full prose-p:text-pretty prose-p:break-words prose-p:text-lg prose-p:text-tertiary prose-a:text-secondary prose-a:underline prose-a:decoration-tertiary/30 prose-a:decoration-wavy prose-blockquote:border-secondary prose-strong:text-secondary prose-code:whitespace-pre-wrap prose-code:font-semibold prose-code:text-tertiary prose-code:before:content-none prose-code:after:content-none prose-pre:w-fit prose-pre:max-w-full prose-pre:border prose-pre:border-accent prose-pre:bg-accent/50 prose-pre:text-tertiary prose-li:text-tertiary prose-li:marker:text-secondary prose-img:max-h-[90vh] prose-img:w-auto prose-img:max-w-full prose-img:rounded prose-video:max-h-[95vh] prose-video:w-auto prose-video:max-w-full prose-video:rounded"
>
<slot />
</div>

View file

@ -0,0 +1,49 @@
---
import { Image } from "astro:assets";
import FormattedDate from "@components/FormattedDate.astro";
type Props = {
collection: any;
};
const { collection } = Astro.props;
---
<article
class="group relative isolate mx-auto flex w-full flex-col justify-end overflow-hidden rounded-lg px-8 pb-8 pt-40"
>
<Image
src={collection.data.image.url}
alt={collection.data.image.alt}
title={collection.data.title}
loading="eager"
class="absolute inset-0 h-full w-full object-cover duration-300 ease-in-out group-hover:scale-105"
fit="cover"
/>
<div
class="absolute inset-0 bg-gradient-to-t from-black via-transparent to-transparent"
>
</div>
<a
class="absolute inset-0 z-20"
href={`/${collection.collection}/${collection.slug}`}
aria-label={collection.data.title}></a>
<h3
class="z-10 mt-3 w-fit text-xl font-medium text-primary dark:text-secondary"
>
{collection.data.title}
</h3>
<div
class="z-10 w-fit gap-y-1 overflow-hidden text-sm leading-6 text-tertiary"
>
{
collection.data.collection ? (
<span>
<FormattedDate date={collection.data.date} /> &bull; Collection
</span>
) : (
<FormattedDate date={collection.data.date} />
)
}
</div>
</article>

View file

@ -0,0 +1,23 @@
---
import Layout from "@layouts/Layout.astro";
import { SITE } from "@consts";
import Showcase from "@components/Showcase.astro";
interface Props {
content: any;
CONSTS: any;
}
const { content, CONSTS } = Astro.props;
---
<Layout title={SITE.TITLE} description={CONSTS.DESCRIPTION}>
<h1 class="animate-reveal break-words text-4xl font-medium opacity-0">
{CONSTS.TITLE}
</h1>
<div
class="mt-16 grid animate-reveal grid-cols-1 gap-6 opacity-0 [animation-delay:0.4s] md:grid-cols-3 md:[&>*:nth-child(4n+2)]:col-span-2 md:[&>*:nth-child(4n+3)]:col-span-2 md:[&>*:only-child]:col-span-3"
>
{content.map((article: any) => <Showcase collection={article} />)}
</div>
</Layout>

View file

@ -0,0 +1,27 @@
---
import { getCollection } from "astro:content";
import { Icon } from "astro-icon/components";
const collection = await getCollection("skills");
const skills = await Promise.all(
collection.map(async (item) => {
const { Content } = await item.render();
return { ...item, Content };
}),
);
---
<ul class="flex max-w-full list-none flex-wrap gap-4 px-0">
{
skills.map((entry) => (
<li>
<Icon
name={entry.data.icon}
title={entry.data.title}
class="h-12 w-auto text-secondary"
/>
</li>
))
}
</ul>

View file

@ -0,0 +1,5 @@
<div
class="mx-auto mb-8 mt-2 max-w-full p-4 pb-16 md:mb-32 md:mt-16 md:max-w-screen-lg md:p-5"
>
<slot />
</div>

36
src/components/Work.astro Normal file
View file

@ -0,0 +1,36 @@
---
import { getCollection } from "astro:content";
import { dateRange } from "@lib/utils";
const collection = (await getCollection("work")).sort(
(a, b) =>
new Date(b.data.dateStart).valueOf() - new Date(a.data.dateStart).valueOf(),
);
const work = await Promise.all(
collection.map(async (item) => {
const { Content } = await item.render();
return { ...item, Content };
}),
);
---
<ul class="list-none pl-0">
{
work.map((entry) => (
<li class="pl-0">
<h3>
<span>{entry.data.company}</span>
<span>({dateRange(entry.data.dateStart, entry.data.dateEnd)})</span>
</h3>
<p>{entry.data.role}</p>
<article>
<entry.Content />
</article>
{entry.data.article ? (
<a href={entry.data.article}>See related project</a>
) : null}
</li>
))
}
</ul>