first commit
This commit is contained in:
commit
ff7c974867
227 changed files with 12908 additions and 0 deletions
35
src/components/Accordion.astro
Normal file
35
src/components/Accordion.astro
Normal 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>
|
106
src/components/Article.astro
Normal file
106
src/components/Article.astro
Normal 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>
|
30
src/components/Education.astro
Normal file
30
src/components/Education.astro
Normal 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>
|
64
src/components/Footer.astro
Normal file
64
src/components/Footer.astro
Normal 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"
|
||||
>© {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>
|
17
src/components/FormattedDate.astro
Normal file
17
src/components/FormattedDate.astro
Normal 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
121
src/components/Head.astro
Normal 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>
|
19
src/components/Header.astro
Normal file
19
src/components/Header.astro
Normal 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
20
src/components/Link.astro
Normal 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>
|
5
src/components/Prose.astro
Normal file
5
src/components/Prose.astro
Normal 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>
|
49
src/components/Showcase.astro
Normal file
49
src/components/Showcase.astro
Normal 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} /> • Collection
|
||||
</span>
|
||||
) : (
|
||||
<FormattedDate date={collection.data.date} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</article>
|
23
src/components/ShowcasePage.astro
Normal file
23
src/components/ShowcasePage.astro
Normal 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>
|
27
src/components/Skills.astro
Normal file
27
src/components/Skills.astro
Normal 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>
|
5
src/components/SkinnyCenter.astro
Normal file
5
src/components/SkinnyCenter.astro
Normal 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
36
src/components/Work.astro
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue