apply formatting rules

This commit is contained in:
2025-11-05 09:45:08 -05:00
parent a1ea385448
commit 6b86b0adb2
24 changed files with 679 additions and 5080 deletions
+31 -27
View File
@@ -1,28 +1,32 @@
{ {
"$ref": "#/definitions/blog", "$ref": "#/definitions/blog",
"definitions": { "definitions": {
"blog": { "blog": {
"type": "object", "type": "object",
"properties": { "properties": {
"title": { "title": {
"type": "string" "type": "string"
}, },
"pubDate": { "pubDate": {
"type": "string" "type": "string"
}, },
"tags": { "tags": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
} }
}, },
"$schema": { "$schema": {
"type": "string" "type": "string"
} }
}, },
"required": ["title", "pubDate", "tags"], "required": [
"additionalProperties": false "title",
} "pubDate",
}, "tags"
"$schema": "http://json-schema.org/draft-07/schema#" ],
} "additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
+8 -35
View File
@@ -1,37 +1,10 @@
import __ASTRO_IMAGE_IMPORT_1OkzEl from "src/assets/blog/gunpla/all-the-parts.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md";
import __ASTRO_IMAGE_IMPORT_Zi2DqH from "src/assets/blog/gunpla/box.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md";
import __ASTRO_IMAGE_IMPORT_FYQiW from "src/assets/blog/gunpla/final.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md";
import __ASTRO_IMAGE_IMPORT_rrnp from "src/assets/blog/ileopard/ileopard-2-0-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md";
import __ASTRO_IMAGE_IMPORT_Z1ESWoO from "src/assets/blog/ileopard/itunes-7.gif?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md";
import __ASTRO_IMAGE_IMPORT_1G57ng from "src/assets/blog/ileopard/mac-os-10-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md"; import __ASTRO_IMAGE_IMPORT_1G57ng from "src/assets/blog/ileopard/mac-os-10-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md";
import __ASTRO_IMAGE_IMPORT_Z1ESWoO from "src/assets/blog/ileopard/itunes-7.gif?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md";
import __ASTRO_IMAGE_IMPORT_rrnp from "src/assets/blog/ileopard/ileopard-2-0-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md";
import __ASTRO_IMAGE_IMPORT_3KcDr from "src/assets/blog/my-ai-portrait.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fcreating-a-dating-profile-with-ai.md"; import __ASTRO_IMAGE_IMPORT_3KcDr from "src/assets/blog/my-ai-portrait.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fcreating-a-dating-profile-with-ai.md";
export default new Map([ import __ASTRO_IMAGE_IMPORT_Zi2DqH from "src/assets/blog/gunpla/box.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md";
[ import __ASTRO_IMAGE_IMPORT_1OkzEl from "src/assets/blog/gunpla/all-the-parts.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md";
"src/assets/blog/my-ai-portrait.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fcreating-a-dating-profile-with-ai.md", import __ASTRO_IMAGE_IMPORT_FYQiW from "src/assets/blog/gunpla/final.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md";
__ASTRO_IMAGE_IMPORT_3KcDr, export default new Map([["src/assets/blog/ileopard/mac-os-10-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md", __ASTRO_IMAGE_IMPORT_1G57ng], ["src/assets/blog/ileopard/itunes-7.gif?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md", __ASTRO_IMAGE_IMPORT_Z1ESWoO], ["src/assets/blog/ileopard/ileopard-2-0-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md", __ASTRO_IMAGE_IMPORT_rrnp], ["src/assets/blog/my-ai-portrait.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fcreating-a-dating-profile-with-ai.md", __ASTRO_IMAGE_IMPORT_3KcDr], ["src/assets/blog/gunpla/box.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md", __ASTRO_IMAGE_IMPORT_Zi2DqH], ["src/assets/blog/gunpla/all-the-parts.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md", __ASTRO_IMAGE_IMPORT_1OkzEl], ["src/assets/blog/gunpla/final.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md", __ASTRO_IMAGE_IMPORT_FYQiW]]);
],
[
"src/assets/blog/ileopard/mac-os-10-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md",
__ASTRO_IMAGE_IMPORT_1G57ng,
],
[
"src/assets/blog/ileopard/itunes-7.gif?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md",
__ASTRO_IMAGE_IMPORT_Z1ESWoO,
],
[
"src/assets/blog/ileopard/ileopard-2-0-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md",
__ASTRO_IMAGE_IMPORT_rrnp,
],
[
"src/assets/blog/gunpla/box.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md",
__ASTRO_IMAGE_IMPORT_Zi2DqH,
],
[
"src/assets/blog/gunpla/all-the-parts.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md",
__ASTRO_IMAGE_IMPORT_1OkzEl,
],
[
"src/assets/blog/gunpla/final.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md",
__ASTRO_IMAGE_IMPORT_FYQiW,
],
]);
+1 -1
View File
@@ -1 +1 @@
export default new Map(); export default new Map();
+58 -92
View File
@@ -1,22 +1,22 @@
declare module "astro:content" { declare module 'astro:content' {
interface Render { interface Render {
".mdx": Promise<{ '.mdx': Promise<{
Content: import("astro").MarkdownInstance<{}>["Content"]; Content: import('astro').MarkdownInstance<{}>['Content'];
headings: import("astro").MarkdownHeading[]; headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>; remarkPluginFrontmatter: Record<string, any>;
components: import("astro").MDXInstance<{}>["components"]; components: import('astro').MDXInstance<{}>['components'];
}>; }>;
} }
} }
declare module "astro:content" { declare module 'astro:content' {
export interface RenderResult { export interface RenderResult {
Content: import("astro/runtime/server/index.js").AstroComponentFactory; Content: import('astro/runtime/server/index.js').AstroComponentFactory;
headings: import("astro").MarkdownHeading[]; headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>; remarkPluginFrontmatter: Record<string, any>;
} }
interface Render { interface Render {
".md": Promise<RenderResult>; '.md': Promise<RenderResult>;
} }
export interface RenderedContent { export interface RenderedContent {
@@ -28,13 +28,11 @@ declare module "astro:content" {
} }
} }
declare module "astro:content" { declare module 'astro:content' {
type Flatten<T> = T extends { [K: string]: infer U } ? U : never; type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
export type CollectionKey = keyof AnyEntryMap; export type CollectionKey = keyof AnyEntryMap;
export type CollectionEntry<C extends CollectionKey> = Flatten< export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
AnyEntryMap[C]
>;
export type ContentCollectionKey = keyof ContentEntryMap; export type ContentCollectionKey = keyof ContentEntryMap;
export type DataCollectionKey = keyof DataEntryMap; export type DataCollectionKey = keyof DataEntryMap;
@@ -42,7 +40,7 @@ declare module "astro:content" {
type AllValuesOf<T> = T extends any ? T[keyof T] : never; type AllValuesOf<T> = T extends any ? T[keyof T] : never;
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf< type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
ContentEntryMap[C] ContentEntryMap[C]
>["slug"]; >['slug'];
export type ReferenceDataEntry< export type ReferenceDataEntry<
C extends CollectionKey, C extends CollectionKey,
@@ -58,9 +56,7 @@ declare module "astro:content" {
collection: C; collection: C;
slug: E; slug: E;
}; };
export type ReferenceLiveEntry< export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = {
C extends keyof LiveContentConfig["collections"],
> = {
collection: C; collection: C;
id: string; id: string;
}; };
@@ -78,15 +74,12 @@ declare module "astro:content" {
: Promise<CollectionEntry<C> | undefined>; : Promise<CollectionEntry<C> | undefined>;
/** @deprecated Use `getEntry` instead. */ /** @deprecated Use `getEntry` instead. */
export function getDataEntryById< export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
C extends keyof DataEntryMap, collection: C,
E extends keyof DataEntryMap[C], entryId: E,
>(collection: C, entryId: E): Promise<CollectionEntry<C>>; ): Promise<CollectionEntry<C>>;
export function getCollection< export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
C extends keyof AnyEntryMap,
E extends CollectionEntry<C>,
>(
collection: C, collection: C,
filter?: (entry: CollectionEntry<C>) => entry is E, filter?: (entry: CollectionEntry<C>) => entry is E,
): Promise<E[]>; ): Promise<E[]>;
@@ -95,16 +88,11 @@ declare module "astro:content" {
filter?: (entry: CollectionEntry<C>) => unknown, filter?: (entry: CollectionEntry<C>) => unknown,
): Promise<CollectionEntry<C>[]>; ): Promise<CollectionEntry<C>[]>;
export function getLiveCollection< export function getLiveCollection<C extends keyof LiveContentConfig['collections']>(
C extends keyof LiveContentConfig["collections"],
>(
collection: C, collection: C,
filter?: LiveLoaderCollectionFilterType<C>, filter?: LiveLoaderCollectionFilterType<C>,
): Promise< ): Promise<
import("astro").LiveDataCollectionResult< import('astro').LiveDataCollectionResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>
LiveLoaderDataType<C>,
LiveLoaderErrorType<C>
>
>; >;
export function getEntry< export function getEntry<
@@ -143,17 +131,10 @@ declare module "astro:content" {
? Promise<DataEntryMap[C][E]> | undefined ? Promise<DataEntryMap[C][E]> | undefined
: Promise<DataEntryMap[C][E]> : Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>; : Promise<CollectionEntry<C> | undefined>;
export function getLiveEntry< export function getLiveEntry<C extends keyof LiveContentConfig['collections']>(
C extends keyof LiveContentConfig["collections"],
>(
collection: C, collection: C,
filter: string | LiveLoaderEntryFilterType<C>, filter: string | LiveLoaderEntryFilterType<C>,
): Promise< ): Promise<import('astro').LiveDataEntryResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>>;
import("astro").LiveDataEntryResult<
LiveLoaderDataType<C>,
LiveLoaderErrorType<C>
>
>;
/** Resolve an array of entry references from the same collection */ /** Resolve an array of entry references from the same collection */
export function getEntries<C extends keyof ContentEntryMap>( export function getEntries<C extends keyof ContentEntryMap>(
@@ -169,8 +150,8 @@ declare module "astro:content" {
export function reference<C extends keyof AnyEntryMap>( export function reference<C extends keyof AnyEntryMap>(
collection: C, collection: C,
): import("astro/zod").ZodEffects< ): import('astro/zod').ZodEffects<
import("astro/zod").ZodString, import('astro/zod').ZodString,
C extends keyof ContentEntryMap C extends keyof ContentEntryMap
? ReferenceContentEntry<C, ValidContentEntrySlug<C>> ? ReferenceContentEntry<C, ValidContentEntrySlug<C>>
: ReferenceDataEntry<C, keyof DataEntryMap[C]> : ReferenceDataEntry<C, keyof DataEntryMap[C]>
@@ -180,72 +161,57 @@ declare module "astro:content" {
// Invalid collection names will be caught at build time. // Invalid collection names will be caught at build time.
export function reference<C extends string>( export function reference<C extends string>(
collection: C, collection: C,
): import("astro/zod").ZodEffects<import("astro/zod").ZodString, never>; ): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T; type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
type InferEntrySchema<C extends keyof AnyEntryMap> = type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
import("astro/zod").infer< ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
ReturnTypeOrOriginal<Required<ContentConfig["collections"][C]>["schema"]> >;
>;
type ContentEntryMap = {}; type ContentEntryMap = {
};
type DataEntryMap = { type DataEntryMap = {
blog: Record< "blog": Record<string, {
string, id: string;
{ body?: string;
id: string; collection: "blog";
body?: string; data: InferEntrySchema<"blog">;
collection: "blog"; rendered?: RenderedContent;
data: InferEntrySchema<"blog">; filePath?: string;
rendered?: RenderedContent; }>;
filePath?: string;
}
>;
}; };
type AnyEntryMap = ContentEntryMap & DataEntryMap; type AnyEntryMap = ContentEntryMap & DataEntryMap;
type ExtractLoaderTypes<T> = T extends import("astro/loaders").LiveLoader< type ExtractLoaderTypes<T> = T extends import('astro/loaders').LiveLoader<
infer TData, infer TData,
infer TEntryFilter, infer TEntryFilter,
infer TCollectionFilter, infer TCollectionFilter,
infer TError infer TError
> >
? { ? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
data: TData; : { data: never; entryFilter: never; collectionFilter: never; error: never };
entryFilter: TEntryFilter; type ExtractDataType<T> = ExtractLoaderTypes<T>['data'];
collectionFilter: TCollectionFilter; type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter'];
error: TError; type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter'];
} type ExtractErrorType<T> = ExtractLoaderTypes<T>['error'];
: {
data: never;
entryFilter: never;
collectionFilter: never;
error: never;
};
type ExtractDataType<T> = ExtractLoaderTypes<T>["data"];
type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>["entryFilter"];
type ExtractCollectionFilterType<T> =
ExtractLoaderTypes<T>["collectionFilter"];
type ExtractErrorType<T> = ExtractLoaderTypes<T>["error"];
type LiveLoaderDataType<C extends keyof LiveContentConfig["collections"]> = type LiveLoaderDataType<C extends keyof LiveContentConfig['collections']> =
LiveContentConfig["collections"][C]["schema"] extends undefined LiveContentConfig['collections'][C]['schema'] extends undefined
? ExtractDataType<LiveContentConfig["collections"][C]["loader"]> ? ExtractDataType<LiveContentConfig['collections'][C]['loader']>
: import("astro/zod").infer< : import('astro/zod').infer<
Exclude<LiveContentConfig["collections"][C]["schema"], undefined> Exclude<LiveContentConfig['collections'][C]['schema'], undefined>
>; >;
type LiveLoaderEntryFilterType< type LiveLoaderEntryFilterType<C extends keyof LiveContentConfig['collections']> =
C extends keyof LiveContentConfig["collections"], ExtractEntryFilterType<LiveContentConfig['collections'][C]['loader']>;
> = ExtractEntryFilterType<LiveContentConfig["collections"][C]["loader"]>; type LiveLoaderCollectionFilterType<C extends keyof LiveContentConfig['collections']> =
type LiveLoaderCollectionFilterType< ExtractCollectionFilterType<LiveContentConfig['collections'][C]['loader']>;
C extends keyof LiveContentConfig["collections"], type LiveLoaderErrorType<C extends keyof LiveContentConfig['collections']> = ExtractErrorType<
> = ExtractCollectionFilterType< LiveContentConfig['collections'][C]['loader']
LiveContentConfig["collections"][C]["loader"]
>; >;
type LiveLoaderErrorType<C extends keyof LiveContentConfig["collections"]> =
ExtractErrorType<LiveContentConfig["collections"][C]["loader"]>;
export type ContentConfig = typeof import("../src/content.config.js"); export type ContentConfig = typeof import("../src/content.config.js");
export type LiveContentConfig = never; export type LiveContentConfig = never;
+1 -2087
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,2 +1,2 @@
/// <reference types="astro/client" /> /// <reference types="astro/client" />
/// <reference path="content.d.ts" /> /// <reference path="content.d.ts" />
+14 -15
View File
@@ -1,31 +1,30 @@
--- ---
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import portrait from "../assets/portrait.jpg"; import portrait from "../assets/portrait.jpg";
interface Props { interface Props {
size: number; size: number;
} }
const { size } = Astro.props; const { size } = Astro.props;
--- ---
<Image <Image
src={portrait} src={portrait}
alt="Illustrated portrait of a person with glasses and a beard, wearing a red and black plaid shirt. The background is a solid green color." alt="Illustrated portrait of a person with glasses and a beard, wearing a red and black plaid shirt. The background is a solid green color."
class="portrait" class="portrait"
width={`${size}`} width={`${size}`}
height={`${size}`} height={`${size}`}
/> />
<style> <style>
.portrait { .portrait {
display: block; display: block;
margin: auto; margin: auto;
border-radius: 200px; border-radius: 200px;
} }
a > svg { a > svg {
transform: translateY(0.18rem); transform: translateY(0.18rem);
} }
</style> </style>
+23 -24
View File
@@ -1,44 +1,43 @@
--- ---
import { add, format } from "date-fns"; import { add, format } from "date-fns";
import CalendarIcon from "../assets/svg/calendar.svg"; import CalendarIcon from "../assets/svg/calendar.svg";
interface Props { interface Props {
title: string; title: string;
date: Date; date: Date;
slug?: string; slug?: string;
} }
const { title, date, slug } = Astro.props; const { title, date, slug } = Astro.props;
--- ---
<div class="blog-header"> <div class="blog-header">
{slug ? <a href={`/blog/${slug}`}>{title}</a> : <h1>{title}</h1>} {slug ? <a href={`/blog/${slug}`}>{title}</a> : <h1>{title}</h1>}
<div> <div>
<CalendarIcon class="calendar-icon" width={24} height={24} /> <CalendarIcon class="calendar-icon" width={24} height={24} />
<strong>{format(add(new Date(date), { hours: 6 }), 'MMM do, y')}</strong> <strong>{format(add(new Date(date), { hours: 6 }), "MMM do, y")}</strong>
</div> </div>
</div> </div>
<style> <style>
a { a {
font-size: 1.3rem; font-size: 1.3rem;
} }
strong { strong {
font-weight: bolder; font-weight: bolder;
} }
.blog-header { .blog-header {
margin: 1rem 0; margin: 1rem 0;
} }
.blog-header > h2 { .blog-header > h2 {
margin: 0; margin: 0;
} }
.calendar-icon { .calendar-icon {
transform: translateY(0.3rem); transform: translateY(0.3rem);
} }
</style> </style>
+39 -40
View File
@@ -1,55 +1,54 @@
--- ---
interface Props { interface Props {
title?: string; title?: string;
image?: ImageMetadata; image?: ImageMetadata;
links?: { links?: {
label: string; label: string;
href: string; href: string;
newWindow?: boolean; newWindow?: boolean;
}[]; }[];
} }
const { title, image, links } = Astro.props; const { title, image, links } = Astro.props;
--- ---
<div class="card"> <div class="card">
{image && <img src={image.src} alt="" />} {image && <img src={image.src} alt="" />}
<div> <div>
{title && <h2>{title}</h2>} {title && <h2>{title}</h2>}
<slot /> <slot />
{ {
links && links &&
links.map((link) => ( links.map((link) => (
<a <a
href={link.href} href={link.href}
target={link.newWindow ? '_blank' : '_self'} target={link.newWindow ? "_blank" : "_self"}
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{link.label} {link.label}
</a> </a>
)) ))
} }
</div> </div>
</div> </div>
<style> <style>
.card { .card {
border: var(--border); border: var(--border);
border-radius: var(--radius); border-radius: var(--radius);
overflow: hidden; overflow: hidden;
} }
.card > div { .card > div {
padding: 1rem; padding: 1rem;
} }
h2 { h2 {
margin: 0; margin: 0;
} }
img { img {
object-fit: cover; object-fit: cover;
max-width: 100%; max-width: 100%;
} }
</style> </style>
+46 -47
View File
@@ -1,5 +1,4 @@
--- ---
import MastodonIcon from "../assets/svg/mastodon.svg"; import MastodonIcon from "../assets/svg/mastodon.svg";
import RssIcon from "../assets/svg/rss.svg"; import RssIcon from "../assets/svg/rss.svg";
@@ -7,61 +6,61 @@ const year = new Date().getFullYear();
--- ---
<footer> <footer>
<div class="socials"> <div class="socials">
<a <a
href="https://mastodon.social/@ghalldev" href="https://mastodon.social/@ghalldev"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
><MastodonIcon class="mastodon-icon" width={20} height={20} /> Follow me on ><MastodonIcon class="mastodon-icon" width={20} height={20} /> Follow me on
Mastodon</a Mastodon</a
> >
<a href="/rss.xml" <a href="/rss.xml"
><RssIcon class="rss-icon" width={20} height={20} /> Subscribe with RSS</a ><RssIcon class="rss-icon" width={20} height={20} /> Subscribe with RSS</a
> >
</div> </div>
<p> <p>
Copyright 2022 - {year}, Graham Hall Copyright 2022 - {year}, Graham Hall
<br /> <br />
Built with <a href="https://astro.build">Astro</a> Built with <a href="https://astro.build">Astro</a>
</p> </p>
</footer> </footer>
<style> <style>
footer { footer {
margin: 2rem 0; margin: 2rem 0;
text-align: center; text-align: center;
border-top: var(--border); border-top: var(--border);
padding-top: 24px; padding-top: 24px;
} }
.rss-icon { .rss-icon {
transform: translateY(0.18rem); transform: translateY(0.18rem);
} }
.socials { .socials {
display: flex; display: flex;
gap: 24px; gap: 24px;
justify-content: center; justify-content: center;
} }
.socials a { .socials a {
color: var(--text); color: var(--text);
} }
.rss-icon { .rss-icon {
color: var(--orange); color: var(--orange);
} }
.mastodon-icon { .mastodon-icon {
color: var(--blue); color: var(--blue);
transform: translateY(4px); transform: translateY(4px);
} }
@media (max-width: 516px) { @media (max-width: 516px) {
.socials { .socials {
flex-direction: column; flex-direction: column;
} }
} }
</style> </style>
+79 -80
View File
@@ -1,5 +1,4 @@
--- ---
import BarsIcon from "../../assets/svg/bars.svg"; import BarsIcon from "../../assets/svg/bars.svg";
import CloseIcon from "../../assets/svg/xmark.svg"; import CloseIcon from "../../assets/svg/xmark.svg";
@@ -8,94 +7,94 @@ import { navLinks } from "../../data/nav-links";
<!-- drawerOpened is defined in /src/layouts/Layout.astro --> <!-- drawerOpened is defined in /src/layouts/Layout.astro -->
<div class="drawer-container" @keydown.escape="drawerOpen = false"> <div class="drawer-container" @keydown.escape="drawerOpen = false">
<button class="icon-button" @click="drawerOpen = !drawerOpen"> <button class="icon-button" @click="drawerOpen = !drawerOpen">
<BarsIcon width={24} height={24} /> <BarsIcon width={24} height={24} />
</button> </button>
<div <div
class="overlay" class="overlay"
@click="drawerOpen = !drawerOpen" @click="drawerOpen = !drawerOpen"
x-show="drawerOpen" x-show="drawerOpen"
x-transition:enter-start="hidden-overlay" x-transition:enter-start="hidden-overlay"
x-transition:enter-end="visible-overlay" x-transition:enter-end="visible-overlay"
x-transition:leave-start="visible-overlay" x-transition:leave-start="visible-overlay"
x-transition:leave-end="hidden-overlay" x-transition:leave-end="hidden-overlay"
> >
</div> </div>
<div <div
class="drawer" class="drawer"
x-show="drawerOpen" x-show="drawerOpen"
x-transition:enter-start="hidden-drawer" x-transition:enter-start="hidden-drawer"
x-transition:enter-end="visible-drawer" x-transition:enter-end="visible-drawer"
x-transition:leave-start="visible-drawer" x-transition:leave-start="visible-drawer"
x-transition:leave-end="hidden-drawer" x-transition:leave-end="hidden-drawer"
> >
<div> <div>
<button class="icon-button" @click="drawerOpen = !drawerOpen"> <button class="icon-button" @click="drawerOpen = !drawerOpen">
<CloseIcon width={24} height={24} /></button <CloseIcon width={24} height={24} /></button
> >
</div> </div>
<nav> <nav>
{navLinks.map((link) => <a href={`/${link.path}`}>{link.label}</a>)} {navLinks.map((link) => <a href={`/${link.path}`}>{link.label}</a>)}
</nav> </nav>
</div> </div>
</div> </div>
<style> <style>
:root { :root {
--transition: all 0.3s ease-in-out; --transition: all 0.3s ease-in-out;
} }
.drawer-container { .drawer-container {
display: none; display: none;
} }
.overlay { .overlay {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background-color: rgba(0, 0, 0, 0.6); background-color: rgba(0, 0, 0, 0.6);
z-index: 2; z-index: 2;
transition: var(--transition); transition: var(--transition);
} }
.hidden-overlay { .hidden-overlay {
opacity: 0; opacity: 0;
} }
.drawer { .drawer {
right: 0; right: 0;
top: 0; top: 0;
background-color: var(--background); background-color: var(--background);
position: absolute; position: absolute;
width: 75vw; width: 75vw;
height: 100vh; height: 100vh;
z-index: 3; z-index: 3;
transition: var(--transition); transition: var(--transition);
} }
.drawer > div { .drawer > div {
padding: 8px 6px 4px; padding: 8px 6px 4px;
border-bottom: var(--border); border-bottom: var(--border);
} }
nav { nav {
text-align: center; text-align: center;
padding: 12px; padding: 12px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
font-size: 1.2rem; font-size: 1.2rem;
} }
.hidden-drawer { .hidden-drawer {
transform: translateX(75vw); transform: translateX(75vw);
} }
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.drawer-container { .drawer-container {
display: block; display: block;
} }
} }
</style> </style>
+36 -37
View File
@@ -1,50 +1,49 @@
--- ---
import Drawer from "./Drawer.astro"; import Drawer from "./Drawer.astro";
import Nav from "./Nav.astro"; import Nav from "./Nav.astro";
--- ---
<header> <header>
<div class="container"> <div class="container">
<a href="/">ghall.space</a> <a href="/">ghall.space</a>
<Nav /> <Nav />
<Drawer /> <Drawer />
</div> </div>
</header> </header>
<style> <style>
header { header {
margin-bottom: 20px; margin-bottom: 20px;
height: 70px; height: 70px;
background-color: rgba(252, 252, 252, 90%); background-color: rgba(252, 252, 252, 90%);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
width: 100%; width: 100%;
top: 0; top: 0;
position: fixed; position: fixed;
z-index: 1; z-index: 1;
border-bottom: var(--border); border-bottom: var(--border);
} }
.container { .container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
max-width: var(--max-page-width); max-width: var(--max-page-width);
margin: auto; margin: auto;
padding: 0 16px; padding: 0 16px;
height: 100%; height: 100%;
} }
a { a {
font-size: 1.6rem; font-size: 1.6rem;
color: var(--text); color: var(--text);
font-weight: bold; font-weight: bold;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
header { header {
background-color: rgba(30, 30, 46, 90%); background-color: rgba(30, 30, 46, 90%);
} }
} }
</style> </style>
+48 -49
View File
@@ -1,65 +1,64 @@
--- ---
import { navLinks } from "../../data/nav-links";
import { navLinks } from '../../data/nav-links';
const { pathname } = Astro.url; const { pathname } = Astro.url;
const pathComponents = pathname.split('/').slice(1); const pathComponents = pathname.split("/").slice(1);
--- ---
<nav> <nav>
{ {
navLinks.map((link) => ( navLinks.map((link) => (
<li> <li>
<a <a
href={`/${link.path}`} href={`/${link.path}`}
class={pathComponents[0] === link.path ? 'selected' : null} class={pathComponents[0] === link.path ? "selected" : null}
> >
{link.label} {link.label}
</a> </a>
</li> </li>
)) ))
} }
</nav> </nav>
<style> <style>
nav { nav {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
list-style: none; list-style: none;
gap: 10px; gap: 10px;
font-size: 1.15rem; font-size: 1.15rem;
font-weight: 600; font-weight: 600;
} }
a { a {
color: var(--text); color: var(--text);
padding: 5px 10px; padding: 5px 10px;
border-radius: var(--radius); border-radius: var(--radius);
transition: background-color 0.2s ease-in-out; transition: background-color 0.2s ease-in-out;
} }
a:hover { a:hover {
background-color: rgba(0, 0, 0, 0.04); background-color: rgba(0, 0, 0, 0.04);
opacity: 1; opacity: 1;
} }
.underline { .underline {
height: 2px; height: 2px;
width: 100%; width: 100%;
background-color: var(--orange); background-color: var(--orange);
} }
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
nav { nav {
display: none; display: none;
} }
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
a:hover { a:hover {
background-color: rgba(231, 237, 255, 0.1); background-color: rgba(231, 237, 255, 0.1);
opacity: 1; opacity: 1;
} }
} }
</style> </style>
+24 -24
View File
@@ -1,5 +1,5 @@
<div <div
x-data="{ x-data="{
latestPost: null, latestPost: null,
fetchPost() { fetchPost() {
fetch('https://mastodon.social/@ghalldev.rss') fetch('https://mastodon.social/@ghalldev.rss')
@@ -21,32 +21,32 @@
}); });
}, },
}" }"
x-init="fetchPost()" x-init="fetchPost()"
> >
<template x-if="latestPost"> <template x-if="latestPost">
<div class="mastodon-post"> <div class="mastodon-post">
<h2>Latest Mastodon Post</h2> <h2>Latest Mastodon Post</h2>
<div x-html="latestPost.body"></div> <div x-html="latestPost.body"></div>
<a x-bind:href="latestPost.link" target="_blank" rel="noopener noreferrer" <a x-bind:href="latestPost.link" target="_blank" rel="noopener noreferrer"
>View Post</a >View Post</a
> >
</div> </div>
</template> </template>
<template x-if="!latestPost"> <template x-if="!latestPost">
<p>Loading...</p> <p>Loading...</p>
</template> </template>
</div> </div>
<style> <style>
.mastodon-post { .mastodon-post {
margin: 2rem auto; margin: 2rem auto;
padding: 1rem; padding: 1rem;
max-width: 600px; max-width: 600px;
border-radius: var(--radius); border-radius: var(--radius);
border: var(--border); border: var(--border);
} }
.mastodon-post > h2 { .mastodon-post > h2 {
margin-top: 0; margin-top: 0;
} }
</style> </style>
+9 -10
View File
@@ -1,27 +1,26 @@
--- ---
import type { CollectionEntry } from "astro:content"; import type { CollectionEntry } from "astro:content";
import BlogHeader from "@components/BlogHeader.astro"; import BlogHeader from "@components/BlogHeader.astro";
import Tags from "@components/Tags.astro"; import Tags from "@components/Tags.astro";
interface Props { interface Props {
post: CollectionEntry<"blog">; post: CollectionEntry<"blog">;
} }
const { post } = Astro.props; const { post } = Astro.props;
--- ---
<article> <article>
<BlogHeader title={post.data.title} date={post.data.pubDate} slug={post.id} /> <BlogHeader title={post.data.title} date={post.data.pubDate} slug={post.id} />
<Tags tags={post.data.tags} /> <Tags tags={post.data.tags} />
</article> </article>
<style> <style>
article { article {
padding-bottom: 20px; padding-bottom: 20px;
} }
article:not(:first-child) { article:not(:first-child) {
border-top: var(--border); border-top: var(--border);
} }
</style> </style>
+11 -12
View File
@@ -1,21 +1,20 @@
--- ---
interface Props { interface Props {
tags: string[]; tags: string[];
} }
const { tags } = Astro.props; const { tags } = Astro.props;
--- ---
<span> <span>
{ {
tags.sort().map((tag: string, index: number) => ( tags.sort().map((tag: string, index: number) => (
<> <>
<a class="tag" href={`/blog/tag/${tag}`}> <a class="tag" href={`/blog/tag/${tag}`}>
{tag} {tag}
</a> </a>
{index < tags.length - 1 ? ' | ' : ''} {index < tags.length - 1 ? " | " : ""}
</> </>
)) ))
} }
</span> </span>
-2241
View File
File diff suppressed because it is too large Load Diff
+34 -35
View File
@@ -1,5 +1,4 @@
--- ---
import { ClientRouter } from "astro:transitions"; import { ClientRouter } from "astro:transitions";
import "../global.css"; import "../global.css";
@@ -8,10 +7,10 @@ import Footer from "@components/Footer.astro";
import Header from "@components/Header/Header.astro"; import Header from "@components/Header/Header.astro";
export interface Props { export interface Props {
title: string; title: string;
frontmatter?: { frontmatter?: {
title: string; title: string;
}; };
} }
const title = Astro.props.title || Astro.props.frontmatter?.title || "Unknown"; const title = Astro.props.title || Astro.props.frontmatter?.title || "Unknown";
@@ -19,37 +18,37 @@ const title = Astro.props.title || Astro.props.frontmatter?.title || "Unknown";
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="robots" content="noindex" /><meta <meta name="robots" content="noindex" /><meta
name="description" name="description"
content="My little space on the World Wide Web." content="My little space on the World Wide Web."
/><link /><link
rel="alternate" rel="alternate"
type="application/rss+xml" type="application/rss+xml"
title="ghall.space - RSS" title="ghall.space - RSS"
href={`${Astro.site}rss.xml`} href={`${Astro.site}rss.xml`}
/><title>{`ghall.space - ${title}`}</title> /><title>{`ghall.space - ${title}`}</title>
<ClientRouter /> <ClientRouter />
</head> </head>
<body <body
class="layout-simple" class="layout-simple"
x-data="{ drawerOpen: false }" x-data="{ drawerOpen: false }"
:class="drawerOpen ? 'lock-scroll' : ''" :class="drawerOpen ? 'lock-scroll' : ''"
> >
<Header /> <Header />
<main transition:animate="initial"> <main transition:animate="initial">
<slot /> <slot />
</main> </main>
<Footer /> <Footer />
</body> </body>
</html> </html>
<style> <style>
/* Prevent scrolling when drawer (located in Header) is open */ /* Prevent scrolling when drawer (located in Header) is open */
.lock-scroll { .lock-scroll {
overflow: hidden; overflow: hidden;
} }
</style> </style>
+15 -16
View File
@@ -1,5 +1,4 @@
--- ---
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import Layout from "@layouts/Layout.astro"; import Layout from "@layouts/Layout.astro";
@@ -7,23 +6,23 @@ import DataGif from "../assets/it-does-not-exist.gif";
--- ---
<Layout title="Not Found"> <Layout title="Not Found">
<h2>404 - Page Not Found</h2> <h2>404 - Page Not Found</h2>
<Image <Image
class="gif" class="gif"
src={DataGif} src={DataGif}
alt="Data from Star Trek with the caption 'It does not exist'" alt="Data from Star Trek with the caption 'It does not exist'"
/> />
</Layout> </Layout>
<style> <style>
h2 { h2 {
text-align: center; text-align: center;
} }
.gif { .gif {
display: block; display: block;
margin: auto; margin: auto;
border-radius: var(--radius); border-radius: var(--radius);
box-shadow: var(--shadow); box-shadow: var(--shadow);
} }
</style> </style>
+27 -29
View File
@@ -1,16 +1,15 @@
--- ---
import { getCollection, render } from "astro:content"; import { getCollection, render } from "astro:content";
import BlogHeader from "@components/BlogHeader.astro"; import BlogHeader from "@components/BlogHeader.astro";
import Tags from "@components/Tags.astro"; import Tags from "@components/Tags.astro";
import Layout from "@layouts/Layout.astro"; import Layout from "@layouts/Layout.astro";
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await getCollection("blog"); const posts = await getCollection("blog");
return posts.map((post) => ({ return posts.map((post) => ({
params: { slug: post.id }, params: { slug: post.id },
props: { post }, props: { post },
})); }));
} }
const { post } = Astro.props; const { post } = Astro.props;
@@ -21,31 +20,30 @@ const { Content } = await render(post);
--- ---
<Layout title={data.title}> <Layout title={data.title}>
<h1></h1> <article>
<article> <BlogHeader title={data.title} date={data.pubDate} />
<BlogHeader title={data.title} date={data.pubDate} /> <Content />
<Content /> <a href="https://notbyai.fyi/">
<a href="https://notbyai.fyi/"> <i class="not-by-ai"></i>
<i class="not-by-ai"></i> </a>
</a> <Tags tags={data.tags} />
<Tags tags={data.tags} /> </article>
</article>
</Layout> </Layout>
<style> <style>
.not-by-ai { .not-by-ai {
display: block; display: block;
width: 134px; width: 134px;
height: 45px; height: 45px;
background-image: url(../../assets/svg/Written-By-Human-Not-By-AI-Badge-white.svg); background-image: url(../../assets/svg/Written-By-Human-Not-By-AI-Badge-white.svg);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
margin: 1rem 0; margin: 1rem 0;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.not-by-ai { .not-by-ai {
background-image: url(../../assets/svg/Written-By-Human-Not-By-AI-Badge-black.svg); background-image: url(../../assets/svg/Written-By-Human-Not-By-AI-Badge-black.svg);
} }
} }
</style> </style>
+42 -43
View File
@@ -1,65 +1,64 @@
--- ---
import { type CollectionEntry, getCollection } from "astro:content";
import PostPreview from "@components/PostPreview.astro";
import { type CollectionEntry, getCollection } from 'astro:content'; import Layout from "@layouts/Layout.astro";
import PostPreview from '@components/PostPreview.astro'; import type { Page } from "astro";
import Layout from '@layouts/Layout.astro';
import type { Page } from 'astro';
export async function getStaticPaths({ paginate }) { export async function getStaticPaths({ paginate }) {
const allPosts = await getCollection('blog'); const allPosts = await getCollection("blog");
allPosts.sort( allPosts.sort(
(a, b) => (a, b) =>
Date.parse(String(b.data.pubDate)) - Date.parse(String(a.data.pubDate)) Date.parse(String(b.data.pubDate)) - Date.parse(String(a.data.pubDate)),
); );
return paginate(allPosts, { pageSize: 10 }); return paginate(allPosts, { pageSize: 10 });
} }
interface Props { interface Props {
page: Page<CollectionEntry<'blog'>>; page: Page<CollectionEntry<"blog">>;
} }
const { page } = Astro.props; const { page } = Astro.props;
--- ---
<Layout title="Blog Archive"> <Layout title="Blog Archive">
{page.data.map((post) => <PostPreview post={post} />)} {page.data.map((post) => <PostPreview post={post} />)}
<div class="pagination"> <div class="pagination">
{ {
page.currentPage !== 1 ? ( page.currentPage !== 1 ? (
<a href={`/blog/page/${page.currentPage - 1}`}>Previous</a> <a href={`/blog/page/${page.currentPage - 1}`}>Previous</a>
) : ( ) : (
<span class="disabled">Previous</span> <span class="disabled">Previous</span>
) )
} }
{ {
page.currentPage !== page.lastPage ? ( page.currentPage !== page.lastPage ? (
<a href={`/blog/page/${page.currentPage + 1}`}>Next</a> <a href={`/blog/page/${page.currentPage + 1}`}>Next</a>
) : ( ) : (
<span class="disabled">Next</span> <span class="disabled">Next</span>
) )
} }
</div> </div>
</Layout> </Layout>
<style> <style>
ul { ul {
list-style: none; list-style: none;
} }
.pagination { .pagination {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin: auto; margin: auto;
margin-top: 2rem; margin-top: 2rem;
gap: 1rem; gap: 1rem;
} }
.disabled { .disabled {
color: var(--text); color: var(--text);
opacity: 0.5; opacity: 0.5;
} }
</style> </style>
+46 -47
View File
@@ -1,73 +1,72 @@
--- ---
import { getCollection } from "astro:content";
import { getCollection } from 'astro:content'; import Layout from "@layouts/Layout.astro";
import Layout from '@layouts/Layout.astro'; import { add, format } from "date-fns";
import { add, format } from 'date-fns';
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await getCollection('blog'); const posts = await getCollection("blog");
const tags = ['all']; const tags = ["all"];
posts.forEach((post) => { posts.forEach((post) => {
post.data.tags.forEach((tag) => { post.data.tags.forEach((tag) => {
if (!tags.includes(tag)) { if (!tags.includes(tag)) {
tags.push(tag); tags.push(tag);
} }
}); });
}); });
return tags.map((tag) => ({ return tags.map((tag) => ({
params: { tag }, params: { tag },
props: { tag }, props: { tag },
})); }));
} }
const { tag }: { tag?: string } = Astro.params; const { tag }: { tag?: string } = Astro.params;
const posts = await getCollection('blog', ({ data }) => { const posts = await getCollection("blog", ({ data }) => {
if (!tag) { if (!tag) {
return false; return false;
} }
if (tag === 'all') { if (tag === "all") {
return true; return true;
} }
return data.tags.includes(tag); return data.tags.includes(tag);
}); });
if (posts.length === 0) { if (posts.length === 0) {
return Astro.redirect('/404'); return Astro.redirect("/404");
} }
posts.sort( posts.sort(
(a, b) => (a, b) =>
Date.parse(String(b.data.pubDate)) - Date.parse(String(a.data.pubDate)) Date.parse(String(b.data.pubDate)) - Date.parse(String(a.data.pubDate)),
); );
--- ---
<Layout title={`Blog - ${tag}`}> <Layout title={`Blog - ${tag}`}>
<h1>All posts tagged: {tag}</h1> <h1>All posts tagged: {tag}</h1>
<ul> <ul>
{ {
posts.map((post) => ( posts.map((post) => (
<li> <li>
<a href={`/posts/${post.id}`}>{post.data.title}</a> - <a href={`/posts/${post.id}`}>{post.data.title}</a> -
<span> <span>
{format( {format(
add(new Date(post.data.pubDate), { hours: 6 }), add(new Date(post.data.pubDate), { hours: 6 }),
'MMM do, y' "MMM do, y",
)} )}
</span> </span>
</li> </li>
)) ))
} }
</ul> </ul>
</Layout> </Layout>
<style> <style>
ul { ul {
list-style: none; list-style: none;
} }
</style> </style>
+53 -54
View File
@@ -1,5 +1,4 @@
--- ---
export const prerender = true; export const prerender = true;
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
@@ -13,64 +12,64 @@ const iconSize = 16;
const posts = await getCollection("blog"); const posts = await getCollection("blog");
const latestPost = posts.sort( const latestPost = posts.sort(
(a, b) => (a, b) =>
new Date(b.data.pubDate).valueOf() - new Date(a.data.pubDate).valueOf(), new Date(b.data.pubDate).valueOf() - new Date(a.data.pubDate).valueOf(),
)[0]; )[0];
--- ---
<Layout title="Welcome"> <Layout title="Welcome">
<Avatar size={200} /> <Avatar size={200} />
<p> <p>
My name is <strong>Graham</strong> (he/him), a full-stack web developer, and My name is <strong>Graham</strong> (he/him), a full-stack web developer, and
tech enthusiast. tech enthusiast.
</p> </p>
<p> <p>
When I'm not writing code, I'm usually enjoying one of my other hobbies; When I'm not writing code, I'm usually enjoying one of my other hobbies;
video games, board games, music, photography, art, the list goes on... video games, board games, music, photography, art, the list goes on...
</p> </p>
<p> <p>
I also like writing about stuff, so I put together this site to share I also like writing about stuff, so I put together this site to share
whatever's on my mind about the world of tech, gaming, life, web whatever's on my mind about the world of tech, gaming, life, web
development, and whatever else strikes my fancy. development, and whatever else strikes my fancy.
</p> </p>
<p> <p>
Read my latest blog post, <a href={`blog/${latestPost.id}`} Read my latest blog post, <a href={`blog/${latestPost.id}`}
>{latestPost.data.title}</a >{latestPost.data.title}</a
>, posted on {new Date(latestPost.data.pubDate).toLocaleDateString()}. >, posted on {new Date(latestPost.data.pubDate).toLocaleDateString()}.
</p> </p>
<p> <p>
If your interested in checking out my web dev projects, check out <a If your interested in checking out my web dev projects, check out <a
href="https://ghall.dev/" href="https://ghall.dev/"
target="_blank" target="_blank"
rel="noopener noreferrer">my portfolio</a rel="noopener noreferrer">my portfolio</a
>. >.
</p> </p>
<p> <p>
If you want to get in touch, I'm on <a If you want to get in touch, I'm on <a
rel="me" rel="me"
href="https://mastodon.social/@ghalldev" href="https://mastodon.social/@ghalldev"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
><MastodonIcon width={iconSize} height={iconSize} />Mastodon</a ><MastodonIcon width={iconSize} height={iconSize} />Mastodon</a
> and <a > and <a
href="https://bsky.app/profile/ghalldev.bsky.social" href="https://bsky.app/profile/ghalldev.bsky.social"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
><BlueskyIcon width={iconSize} height={iconSize} />Bluesky</a ><BlueskyIcon width={iconSize} height={iconSize} />Bluesky</a
>. >.
</p> </p>
<p> <p>
My portrait was drawn by <a My portrait was drawn by <a
href="https://www.nataliavazquezgarcia.com" href="https://www.nataliavazquezgarcia.com"
target="_blank" target="_blank"
rel="noopener noreferrer">Natalia Vazquez</a rel="noopener noreferrer">Natalia Vazquez</a
>. >.
</p> </p>
<LatestPost /> <LatestPost />
</Layout> </Layout>
<style> <style>
a > svg { a > svg {
transform: translateY(0.1rem); transform: translateY(0.1rem);
} }
</style> </style>
+33 -34
View File
@@ -1,5 +1,4 @@
--- ---
import Card from "@components/Card.astro"; import Card from "@components/Card.astro";
import { projects } from "@data/projects"; import { projects } from "@data/projects";
import Layout from "@layouts/Layout.astro"; import Layout from "@layouts/Layout.astro";
@@ -8,41 +7,41 @@ const title = "Projects";
--- ---
<Layout title={title}> <Layout title={title}>
<h1>{title}</h1> <h1>{title}</h1>
<div class="projects-grid"> <div class="projects-grid">
{ {
projects.map((project) => ( projects.map((project) => (
<Card <Card
title={project.title} title={project.title}
image={project.image} image={project.image}
links={[ links={[
{ {
label: 'More...', label: "More...",
href: project.link, href: project.link,
newWindow: true, newWindow: true,
}, },
]} ]}
> >
<div class="project-content"> <div class="project-content">
<p>{project.description}</p> <p>{project.description}</p>
</div> </div>
</Card> </Card>
)) ))
} }
</div> </div>
</Layout> </Layout>
<style> <style>
.projects-grid { .projects-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem; gap: 1rem;
} }
.project-content { .project-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
height: 8rem; height: 8rem;
} }
</style> </style>