Merge dev into main. (#1)
6
.gitignore
vendored
|
@ -1,14 +1,11 @@
|
|||
# build output
|
||||
dist/
|
||||
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
.netlify/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
@ -21,3 +18,6 @@ pnpm-debug.log*
|
|||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
2
.vscode/extensions.json
vendored
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
}
|
2
.vscode/launch.json
vendored
|
@ -8,4 +8,4 @@
|
|||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
21
README.md
|
@ -4,23 +4,6 @@
|
|||
|
||||
To view my portfolio **[click here](https://toastiet0ast.com)**
|
||||
|
||||
## Features
|
||||
|
||||
- Modern and Minimal bento-like, sleek UI Design
|
||||
- All in one page (almost)
|
||||
- Fully Responsive
|
||||
- Performances and SEO optimizations
|
||||
- Blog
|
||||
- RSS support (your-domain/rss.xml)
|
||||
- Cool 3d globe
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- [Astro](https://astro.build)
|
||||
- [unocss](https://unocss.dev/)
|
||||
- [motion](https://motion.dev/)
|
||||
- [d3](https://d3js.org/)
|
||||
|
||||
# Steps ▶️
|
||||
|
||||
```bash
|
||||
|
@ -46,7 +29,3 @@ $ pnpm run dev
|
|||
or
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
# Configuration
|
||||
|
||||
remember to replace the `site` and other properties with your data in `astro.config.mjs`
|
||||
|
|
|
@ -1,31 +1,5 @@
|
|||
import { defineConfig } from "astro/config";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import robotsTxt from "astro-robots-txt";
|
||||
import UnoCSS from "@unocss/astro";
|
||||
import icon from "astro-icon";
|
||||
|
||||
import solidJs from "@astrojs/solid-js";
|
||||
import { remarkReadingTime } from "./src/lib/ remark-reading-time.mjs";
|
||||
|
||||
import svelte from "@astrojs/svelte";
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: "https://toastiet0ast.com/",
|
||||
integrations: [
|
||||
sitemap(),
|
||||
robotsTxt({
|
||||
sitemap: [
|
||||
"https://toastiet0ast.com/sitemap-index.xml",
|
||||
"https://toastiet0ast.com/sitemap-0.xml",
|
||||
],
|
||||
}),
|
||||
solidJs(),
|
||||
UnoCSS({ injectReset: true }),
|
||||
icon(),
|
||||
svelte(),
|
||||
],
|
||||
markdown: {
|
||||
remarkPlugins: [remarkReadingTime],
|
||||
}
|
||||
});
|
||||
export default defineConfig({});
|
38
package.json
|
@ -1,45 +1,15 @@
|
|||
{
|
||||
"name": "astro-bento-portfolio",
|
||||
"name": "toastie-site",
|
||||
"type": "module",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/rss": "^4.0.9",
|
||||
"@astrojs/sitemap": "^3.2.1",
|
||||
"@astrojs/solid-js": "^4.4.2",
|
||||
"@astrojs/svelte": "^5.7.2",
|
||||
"@iconify-json/ri": "^1.2.1",
|
||||
"@rive-app/canvas": "^2.21.6",
|
||||
"astro": "^4.16.5",
|
||||
"astro-icon": "^1.1.1",
|
||||
"astro-robots-txt": "^1.0.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"d3": "^7.9.0",
|
||||
"gsap": "^3.12.5",
|
||||
"lenis": "^1.1.14",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"motion": "^10.18.0",
|
||||
"reading-time": "^1.5.0",
|
||||
"solid-js": "^1.9.2",
|
||||
"svelte": "^4.2.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/d3": "^7.4.3",
|
||||
"@unocss/astro": "^0.63.4",
|
||||
"@unocss/postcss": "^0.63.4",
|
||||
"@unocss/preset-uno": "^0.63.4",
|
||||
"@unocss/reset": "^0.63.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"markdown-it": "^14.1.0",
|
||||
"motion": "^10.18.0",
|
||||
"sanitize-html": "^2.13.1",
|
||||
"typescript": "^5.6.3",
|
||||
"unocss": "^0.63.4"
|
||||
"astro": "^5.1.1"
|
||||
}
|
||||
}
|
||||
|
|
3700
pnpm-lock.yaml
|
@ -1,4 +0,0 @@
|
|||
// postcss.config.cjs
|
||||
module.exports = {
|
||||
plugins: {},
|
||||
};
|
BIN
public/assets/backgrounds/bg-main-dark-800w.jpg
Normal file
After Width: | Height: | Size: 12 KiB |
1
public/assets/backgrounds/bg-main-dark.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg height="640" width="1440" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a"><stop offset=".58" stop-opacity="0"/><stop offset="1"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="793.5" x2="759.5" xlink:href="#a" y1="261.5" y2="149.5"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="644.19" x2="645.54" xlink:href="#a" y1="398.02" y2="267.7"/><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="547" x2="522.36" xlink:href="#a" y1="457.27" y2="342.85"/><g clip-rule="evenodd" fill-rule="evenodd" opacity=".15"><path d="m439.57 249.55a2149.47 2149.47 0 0 1 1193.87-182.45l-12.48 93.17a2055.46 2055.46 0 0 0 -1141.66 174.47l-454.24 211.86-39.73-85.2z" fill="url(#b)"/><path d="m272.3 266.93a2393.36 2393.36 0 0 1 1328.96 205.6l-44.42 94.78a2288.7 2288.7 0 0 0 -1270.84-196.61l-553.29 73.05-13.7-103.77z" fill="url(#c)" opacity=".56"/><path d="m195.26 416.13a2149.46 2149.46 0 0 1 1204.86-83.21l-20.13 91.82a2055.46 2055.46 0 0 0 -1152.17 79.56l-470.18 173.62-32.56-88.18 470.18-173.62z" fill="url(#d)"/></g><path d="m-258.15 719.56 1743.12-517.56 182.93 616.12-1743.1 517.56z" fill="#090b11"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/assets/backgrounds/bg-main-light-800w.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
1
public/assets/backgrounds/bg-main-light.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1440" height="640"><g opacity=".15"><path fill="url(#a)" d="M439.57 249.55A2149.47 2149.47 0 0 1 1633.44 67.1l-12.48 93.17A2055.46 2055.46 0 0 0 479.3 334.74L25.06 546.6l-39.73-85.2z"/><path fill="url(#b)" d="M272.3 265.93a2393.36 2393.36 0 0 1 1328.96 205.6l-44.42 94.78A2288.7 2288.7 0 0 0 286 369.7l-553.29 73.05-13.7-103.77z" opacity=".56"/><path fill="url(#c)" d="M195.26 416.13a2149.47 2149.47 0 0 1 1204.86-83.21l-20.13 91.82A2055.46 2055.46 0 0 0 227.82 504.3l-470.18 173.62-32.56-88.18 470.18-173.62z"/></g><path fill="#fff" d="M-258 718.56 1485.12 201l182.93 616.12-1743.11 517.56z"/><defs><linearGradient id="d"><stop offset=".58" stop-opacity="0"/><stop offset="1"/></linearGradient><linearGradient xlink:href="#d" id="a" x1="793.5" x2="759.5" y1="261.5" y2="149.5" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#d" id="b" x1="644.19" x2="645.54" y1="397.02" y2="266.7" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#d" id="c" x1="547" x2="522.36" y1="457.27" y2="342.85" gradientUnits="userSpaceOnUse"/></defs></svg>
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/assets/backgrounds/noise.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
public/assets/dcs.png
Normal file
After Width: | Height: | Size: 114 KiB |
BIN
public/assets/ellie-casey.png
Normal file
After Width: | Height: | Size: 273 KiB |
BIN
public/assets/toastielab.png
Normal file
After Width: | Height: | Size: 346 KiB |
BIN
public/assets/valkyriecoms.png
Normal file
After Width: | Height: | Size: 89 KiB |
|
@ -1,13 +0,0 @@
|
|||
---
|
||||
import Card from "./Card/index.astro";
|
||||
---
|
||||
|
||||
<Card colSpan="md:col-span-1" rowSpan="md:row-span-6" title="About me">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-sm font-light">
|
||||
<p>
|
||||
Hi, I'm Toastie_t0ast, a software dev & systems administrator from New Zealand.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
|
@ -1,29 +0,0 @@
|
|||
---
|
||||
import { formatDate } from "../../lib/helpers";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
date: Date;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const { title, date, url } = Astro.props;
|
||||
---
|
||||
|
||||
<li class="w-full text-neutral-100 hover:text-neutral-400 ease-in-out transition-colors border-b-neutral-400 border-dashed border-b-1 my-2">
|
||||
<a
|
||||
class="text-sm md:text-base decoration-none flex justify-between"
|
||||
href={`/blog/${url}`}
|
||||
>
|
||||
<p class="inline-block whitespace-nowrap">
|
||||
{title}
|
||||
</p>
|
||||
<time
|
||||
class="text-right tabular-nums"
|
||||
datetime={date.toISOString()}
|
||||
data-date={date.toISOString()}
|
||||
>
|
||||
{formatDate(date)}
|
||||
</time>
|
||||
</a>
|
||||
</li>
|
56
src/components/BlogHero.astro
Normal file
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
tagline?: string;
|
||||
author?: string;
|
||||
align?: 'start' | 'center';
|
||||
}
|
||||
|
||||
const { align = 'center', tagline, title, author } = Astro.props;
|
||||
---
|
||||
|
||||
<div class:list={['hero stack gap-4', align]}>
|
||||
<div class="stack gap-2">
|
||||
<h1 class="title">{title}</h1>
|
||||
<p class="author">{author}</p>
|
||||
{tagline && <p class="tagline">{tagline}</p>}
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
font-size: var(--text-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title,
|
||||
.tagline {
|
||||
max-width: 37ch;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--text-3xl);
|
||||
color: var(--gray-0);
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.hero {
|
||||
font-size: var(--text-xl);
|
||||
}
|
||||
|
||||
.start {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.start .title,
|
||||
.start .tagline {
|
||||
margin-inline: unset;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--text-5xl);
|
||||
}
|
||||
}
|
||||
</style>
|
63
src/components/BlogPreview.astro
Normal file
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
|
||||
interface Props {
|
||||
blog: CollectionEntry<'blog'>;
|
||||
}
|
||||
|
||||
const { data, id } = Astro.props.blog;
|
||||
---
|
||||
|
||||
<a class="card" href={`/blog/${id}`}>
|
||||
<span class="title">{data.title}</span>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
display: grid;
|
||||
grid-template: auto 1fr / auto 1fr;
|
||||
height: 11rem;
|
||||
background: var(--gradient-subtle);
|
||||
border: 1px solid var(--gray-800);
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-sm);
|
||||
text-decoration: none;
|
||||
font-family: var(--font-brand);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 500;
|
||||
transition: box-shadow var(--theme-transition);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.title {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
z-index: 1;
|
||||
margin: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--gray-999);
|
||||
color: var(--gray-200);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
img {
|
||||
grid-area: 1 / 1 / 3 / 3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.card {
|
||||
height: 22rem;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
border-radius: 0.9375rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,15 +0,0 @@
|
|||
---
|
||||
interface Props {
|
||||
rounded?: boolean;
|
||||
}
|
||||
|
||||
const { rounded } = Astro.props;
|
||||
---
|
||||
|
||||
<button
|
||||
class={`custom-btn text-xl max-h-[50px] shadow-custom shadow-primary-500 active:shadow-none active:translate-x-[3px] active:translate-y-[3px] text-gray-200 px-5 py-2 border border-primary-500 hover:text-primary-500 transition-colors duration-100 ease-in-out bg-gray-900 cursor-pointer ${
|
||||
rounded ? "rounded-full" : "rounded-lg"
|
||||
}`}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
56
src/components/CallToAction.astro
Normal file
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
interface Props {
|
||||
href: string;
|
||||
}
|
||||
|
||||
const { href } = Astro.props;
|
||||
---
|
||||
|
||||
<a href={href}><slot /></a>
|
||||
|
||||
<style>
|
||||
a {
|
||||
position: relative;
|
||||
display: flex;
|
||||
place-content: center;
|
||||
text-align: center;
|
||||
padding: 0.56em 2em;
|
||||
gap: 0.8em;
|
||||
color: var(--accent-text-over);
|
||||
text-decoration: none;
|
||||
line-height: 1.1;
|
||||
border-radius: 999rem;
|
||||
overflow: hidden;
|
||||
background: var(--gradient-accent-orange);
|
||||
box-shadow: var(--shadow-md);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (min-width: 20em) {
|
||||
a {
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Overlay for hover effects. */
|
||||
a::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
transition: background-color var(--theme-transition);
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
a:focus::after,
|
||||
a:hover::after {
|
||||
background-color: hsla(var(--gray-999-basis), 0.3);
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
a {
|
||||
padding: 1.125rem 2.5rem;
|
||||
font-size: var(--text-xl);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,14 +0,0 @@
|
|||
---
|
||||
interface Props {
|
||||
title?: string;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
const { title, body } = Astro.props;
|
||||
---
|
||||
|
||||
<>
|
||||
{title && <h2 class="text-xl font-bold m-0 z-20">{title}</h2>}
|
||||
{body && <p class="m-0 font-light text-base">{body}</p>}
|
||||
<slot />
|
||||
</>
|
|
@ -1,43 +0,0 @@
|
|||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import Content from "./Content.astro";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
body?: string;
|
||||
colSpan?: string;
|
||||
rowSpan?: string;
|
||||
href?: string;
|
||||
colorText?: string;
|
||||
height?: string;
|
||||
width?: string;
|
||||
}
|
||||
|
||||
const { title, body, colSpan, rowSpan, href, colorText, height } = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
class={`card h-max sm:h-auto group overflow-hidden transform-y-[-40%] bg-darkslate-500 shadow-lg rounded-lg p-6 border border-darkslate-100 hover:border-primary-500 align-start flex-none ${
|
||||
height || "h-full"
|
||||
} justify-start relative transform perspective-1200 w-full transition duration-75 ease-in-out col-span-1 ${
|
||||
colSpan || "md:col-span-2"
|
||||
} ${rowSpan || ""}`}
|
||||
>
|
||||
{
|
||||
href ? (
|
||||
<a href={href} class={`h-full w-full ${colorText || " "}`}>
|
||||
<Icon
|
||||
name="ri:arrow-right-up-line"
|
||||
class="h-6 float-right group-hover:text-primary-500 group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform ease-in-out duration-100 z-20"
|
||||
/>
|
||||
<Content title={title} body={body}>
|
||||
<slot />
|
||||
</Content>
|
||||
</a>
|
||||
) : (
|
||||
<Content title={title} body={body}>
|
||||
<slot />
|
||||
</Content>
|
||||
)
|
||||
}
|
||||
</div>
|
46
src/components/ContactCTA.astro
Normal file
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
import CallToAction from './CallToAction.astro';
|
||||
import Icon from './Icon.astro';
|
||||
---
|
||||
|
||||
<aside>
|
||||
<h2>Need to contact me?</h2>
|
||||
<CallToAction href="mailto:hello@toastiet0ast.com">
|
||||
Send Me an Email
|
||||
<Icon icon="paper-plane-tilt" size="1.2em" />
|
||||
</CallToAction>
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
aside {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 3rem;
|
||||
border-top: 1px solid var(--gray-800);
|
||||
border-bottom: 1px solid var(--gray-800);
|
||||
padding: 5rem 1.5rem;
|
||||
background-color: var(--gray-999_40);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-xl);
|
||||
text-align: center;
|
||||
max-width: 15ch;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
aside {
|
||||
padding: 7.5rem;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-3xl);
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,32 +0,0 @@
|
|||
---
|
||||
import Card from "./Card/index.astro";
|
||||
import { LINKS } from "../lib/constants";
|
||||
---
|
||||
|
||||
<Card colSpan="md:col-span-1" rowSpan="md:row-span-4">
|
||||
<div class="h-full">
|
||||
<header class="flex items-center">
|
||||
<h1 class="text-white text-xl font-bold">
|
||||
Let's start working together!
|
||||
</h1>
|
||||
</header>
|
||||
<address class="flex flex-col mt-4">
|
||||
<h2 class="text-gray-500">Contact Details</h2>
|
||||
<p>hello@toastiet0ast.com</p>
|
||||
</address>
|
||||
<div class="flex flex-col mt-4 w-fit">
|
||||
<h2 class="text-gray-500">Socials</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href={LINKS.toastielab} target="_blank">Toastielab</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={LINKS.discord} target="_blank">Discord</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={LINKS.valkyriecoms} target="_blank">Valkyriecoms</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
70
src/components/Footer.astro
Normal file
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
import Icon from './Icon.astro';
|
||||
const currentYear = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<footer>
|
||||
<div class="group">
|
||||
<p>© {currentYear} Toastie_t0ast</p>
|
||||
</div>
|
||||
<p class="socials">
|
||||
<a href="https://valkyriecoms.com/@toastie"> Valkyriecoms</a>
|
||||
<a href="https://toastielab.dev/toastie_t0ast"> Toastielab</a>
|
||||
<a href="https://discord.gg/3qvVNTk6sa"> Discord</a>
|
||||
</p>
|
||||
</footer>
|
||||
<style>
|
||||
footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3rem;
|
||||
margin-top: auto;
|
||||
padding: 3rem 2rem 3rem;
|
||||
text-align: center;
|
||||
color: var(--gray-400);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--gray-400);
|
||||
text-decoration: 1px solid underline transparent;
|
||||
text-underline-offset: 0.25em;
|
||||
transition: text-decoration-color var(--theme-transition);
|
||||
}
|
||||
|
||||
footer a:hover,
|
||||
footer a:focus {
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.socials {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
footer {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 2.5rem 5rem;
|
||||
}
|
||||
|
||||
.group {
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.socials {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
65
src/components/Grid.astro
Normal file
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
interface Props {
|
||||
variant?: 'offset' | 'small'
|
||||
}
|
||||
|
||||
const { variant } = Astro.props;
|
||||
---
|
||||
|
||||
<ul class:list={['grid', { offset: variant === 'offset', small: variant === 'small' }]}>
|
||||
<slot />
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-auto-rows: 1fr;
|
||||
gap: 1rem;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.grid.small {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* If last row contains only one item, make it span both columns. */
|
||||
.grid.small > :global(:last-child:nth-child(odd)) {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.grid.offset {
|
||||
--row-offset: 7.5rem;
|
||||
padding-bottom: var(--row-offset);
|
||||
}
|
||||
|
||||
/* Shift first item in each row vertically to create staggered effect. */
|
||||
.grid.offset > :global(:nth-child(odd)) {
|
||||
transform: translateY(var(--row-offset));
|
||||
}
|
||||
|
||||
/* If last row contains only one item, display it in the second column. */
|
||||
.grid.offset > :global(:last-child:nth-child(odd)) {
|
||||
grid-column: 2 / 3;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.grid.small {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.grid.small > :global(*) {
|
||||
flex-basis: 20rem;
|
||||
}
|
||||
}
|
||||
</style>
|
54
src/components/Hero.astro
Normal file
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
tagline?: string;
|
||||
align?: 'start' | 'center';
|
||||
}
|
||||
|
||||
const { align = 'center', tagline, title } = Astro.props;
|
||||
---
|
||||
|
||||
<div class:list={['hero stack gap-4', align]}>
|
||||
<div class="stack gap-2">
|
||||
<h1 class="title">{title}</h1>
|
||||
{tagline && <p class="tagline">{tagline}</p>}
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
font-size: var(--text-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title,
|
||||
.tagline {
|
||||
max-width: 37ch;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--text-3xl);
|
||||
color: var(--gray-0);
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.hero {
|
||||
font-size: var(--text-xl);
|
||||
}
|
||||
|
||||
.start {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.start .title,
|
||||
.start .tagline {
|
||||
margin-inline: unset;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--text-5xl);
|
||||
}
|
||||
}
|
||||
</style>
|
56
src/components/Icon.astro
Normal file
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
import type { HTMLAttributes } from 'astro/types';
|
||||
import { iconPaths } from './IconPaths';
|
||||
|
||||
interface Props {
|
||||
icon: keyof typeof iconPaths;
|
||||
color?: string;
|
||||
gradient?: boolean;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
const { color = 'currentcolor', gradient, icon, size } = Astro.props;
|
||||
const iconPath = iconPaths[icon];
|
||||
|
||||
const attrs: HTMLAttributes<'svg'> = {};
|
||||
if (size) attrs.style = { '--size': size };
|
||||
|
||||
const gradientId = 'icon-gradient-' + Math.round(Math.random() * 10e12).toString(36);
|
||||
---
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 256 256"
|
||||
aria-hidden="true"
|
||||
stroke={gradient ? `url(#${gradientId})` : color}
|
||||
fill={gradient ? `url(#${gradientId})` : color}
|
||||
{...attrs}
|
||||
>
|
||||
<g set:html={iconPath} />
|
||||
{
|
||||
gradient && (
|
||||
<linearGradient
|
||||
id={gradientId}
|
||||
x1="23"
|
||||
x2="235"
|
||||
y1="43"
|
||||
y2="202"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--gradient-stop-1)" />
|
||||
<stop offset=".5" stop-color="var(--gradient-stop-2)" />
|
||||
<stop offset="1" stop-color="var(--gradient-stop-3)" />
|
||||
</linearGradient>
|
||||
)
|
||||
}
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
width: var(--size, 1em);
|
||||
height: var(--size, 1em);
|
||||
}
|
||||
</style>
|
39
src/components/IconPaths.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* Icons adapted from https://phosphoricons.com/
|
||||
*
|
||||
* Want to add more?
|
||||
* 1. Find the icon you want on Phosphor Icons.
|
||||
* 2. Click “Copy SVG”.
|
||||
* 3. Paste the SVG code in your editor.
|
||||
* 4. Remove the `<svg>` wrapper so you only have elements like `<path>`, `<circle>`, `<rect>` etc.
|
||||
* 5. Remove any `stroke="#000000"` attributes
|
||||
* 6. Replace any `fill="#000000"` attributes with `stroke="none"`
|
||||
* (or add `stroke="none"` on shapes with no `fill` or `stroke` specified).
|
||||
*/
|
||||
export const iconPaths = {
|
||||
'terminal-window': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m80 96 40 32-40 32m56 0h40"/><rect width="192" height="160" x="32" y="48" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16.97" rx="8.5"/>`,
|
||||
trophy: `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M56 56v55.1c0 39.7 31.8 72.6 71.5 72.9a72 72 0 0 0 72.5-72V56a8 8 0 0 0-8-8H64a8 8 0 0 0-8 8Zm40 168h64m-32-40v40"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M198.2 128h9.8a32 32 0 0 0 32-32V80a8 8 0 0 0-8-8h-32M58 128H47.9a32 32 0 0 1-32-32V80a8 8 0 0 1 8-8h32"/>`,
|
||||
strategy: `<circle cx="68" cy="188" r="28" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m40 72 40 40m0-40-40 40m136 56 40 40m0-40-40 40M136 80V40h40"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m136 40 16 16c40 40 8 88-24 96"/>`,
|
||||
'paper-plane-tilt': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M210.3 35.9 23.9 88.4a8 8 0 0 0-1.2 15l85.6 40.5a7.8 7.8 0 0 1 3.8 3.8l40.5 85.6a8 8 0 0 0 15-1.2l52.5-186.4a7.9 7.9 0 0 0-9.8-9.8Zm-99.4 109.2 45.2-45.2"/>`,
|
||||
'arrow-right': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M40 128h176m-72-72 72 72-72 72"/>`,
|
||||
'arrow-left': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M216 128H40m72-72-72 72 72 72"/>`,
|
||||
code: `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m64 88-48 40 48 40m128-80 48 40-48 40M160 40 96 216"/>`,
|
||||
'microphone-stage': `<circle cx="168" cy="88" r="64" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m213.3 133.3-90.6-90.6M100 156l-12 12m16.8-70.1L28.1 202.5a7.9 7.9 0 0 0 .8 10.4l14.2 14.2a7.9 7.9 0 0 0 10.4.8l104.6-76.7"/>`,
|
||||
'pencil-line': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M96 216H48a8 8 0 0 1-8-8v-44.7a7.9 7.9 0 0 1 2.3-5.6l120-120a8 8 0 0 1 11.4 0l44.6 44.6a8 8 0 0 1 0 11.4Zm40-152 56 56"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M216 216H96l-55.5-55.5M164 92l-96 96"/>`,
|
||||
'rocket-launch': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M94.1 184.6c-11.4 33.9-56.6 33.9-56.6 33.9s0-45.2 33.9-56.6m124.5-56.5L128 173.3 82.7 128l67.9-67.9C176.3 34.4 202 34.7 213 36.3a7.8 7.8 0 0 1 6.7 6.7c1.6 11 1.9 36.7-23.8 62.4Z"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M184.6 116.7v64.6a8 8 0 0 1-2.4 5.6l-32.3 32.4a8 8 0 0 1-13.5-4.1l-8.4-41.9m11.3-101.9H74.7a8 8 0 0 0-5.6 2.4l-32.4 32.3a8 8 0 0 0 4.1 13.5l41.9 8.4"/>`,
|
||||
list: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M40 128h176M40 64h176M40 192h176"/>`,
|
||||
heart: `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M128 216S28 160 28 92a52 52 0 0 1 100-20h0a52 52 0 0 1 100 20c0 68-100 124-100 124Z"/>`,
|
||||
'moon-stars': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M216 112V64m24 24h-48m-24-64v32m16-16h-32m65 113A92 92 0 0 1 103 39h0a92 92 0 1 0 114 114Z"/>`,
|
||||
sun: `<circle cx="128" cy="128" r="60" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M128 36V16M63 63 49 49m-13 79H16m47 65-14 14m79 13v20m65-47 14 14m13-79h20m-47-65 14-14"/>`,
|
||||
'twitter-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M128 88c0-22 18.5-40.3 40.5-40a40 40 0 0 1 36.2 24H240l-32.3 32.3A127.9 127.9 0 0 1 80 224c-32 0-40-12-40-12s32-12 48-36c0 0-64-32-48-120 0 0 40 40 88 48Z"/>`,
|
||||
'codepen-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m232 101-104 59-104-59 100.1-56.8a8.3 8.3 0 0 1 7.8 0Z"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m232 165-100.1 56.8a8.3 8.3 0 0 1-7.8 0L24 165l104-59Zm0-64v64M24 101v64m104-5v62.8m0-179.6V106"/>`,
|
||||
'git-branch': `<path d="M232,64a32,32,0,1,0-40,31v17a8,8,0,0,1-8,8H96a23.84,23.84,0,0,0-8,1.38V95a32,32,0,1,0-16,0v66a32,32,0,1,0,16,0V144a8,8,0,0,1,8-8h88a24,24,0,0,0,24-24V95A32.06,32.06,0,0,0,232,64ZM64,64A16,16,0,1,1,80,80,16,16,0,0,1,64,64ZM96,192a16,16,0,1,1-16-16A16,16,0,0,1,96,192ZM200,80a16,16,0,1,1,16-16A16,16,0,0,1,200,80Z"></path>`,
|
||||
'twitch-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M165 200h-42a8 8 0 0 0-5 2l-46 38v-40H48a8 8 0 0 1-8-8V48a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8v108a8 8 0 0 1-3 6l-43 36a8 8 0 0 1-5 2Zm3-112v48m-48-48v48"/>`,
|
||||
'youtube-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m160 128-48-32v64l48-32z"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M24 128c0 30 3 47 5 56a16 16 0 0 0 10 11c34 13 89 13 89 13s56 0 89-13a16 16 0 0 0 10-11c2-9 5-26 5-56s-3-47-5-56a16 16 0 0 0-10-11c-33-13-89-13-89-13s-55 0-89 13a16 16 0 0 0-10 11c-2 9-5 26-5 56Z"/>`,
|
||||
'dribbble-logo': `<circle cx="128" cy="128" r="96" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M71 205a160 160 0 0 1 137-77l16 1m-36-76a160 160 0 0 1-124 59 165 165 0 0 1-30-3"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M86 42a161 161 0 0 1 74 177"/>`,
|
||||
'discord-logo': `<circle stroke="none" cx="96" cy="144" r="12"/><circle stroke="none" cx="160" cy="144" r="12"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M74 80a175 175 0 0 1 54-8 175 175 0 0 1 54 8m0 96a175 175 0 0 1-54 8 175 175 0 0 1-54-8"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m155 182 12 24a8 8 0 0 0 9 4c25-6 46-16 61-30a8 8 0 0 0 3-8L206 59a8 8 0 0 0-5-5 176 176 0 0 0-30-9 8 8 0 0 0-9 5l-8 24m-53 108-12 24a8 8 0 0 1-9 4c-25-6-46-16-61-30a8 8 0 0 1-3-8L50 59a8 8 0 0 1 5-5 176 176 0 0 1 30-9 8 8 0 0 1 9 5l8 24"/>`,
|
||||
'linkedin-logo': `<rect width="184" height="184" x="36" y="36" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" rx="8"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M120 112v64m-32-64v64m32-36a28 28 0 0 1 56 0v36"/><circle stroke="none" cx="88" cy="80" r="12"/>`,
|
||||
'instagram-logo': `<circle cx="128" cy="128" r="40" fill="none" stroke-miterlimit="10" stroke-width="16"/><rect width="184" height="184" x="36" y="36" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" rx="48"/><circle cx="180" cy="76" r="12" stroke="none" />`,
|
||||
'tiktok-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M168 106a96 96 0 0 0 56 18V84a56 56 0 0 1-56-56h-40v128a28 28 0 1 1-40-25V89a68 68 0 1 0 80 67Z"/>`,
|
||||
'fediverse-logo': '<path d="M212,96a27.84,27.84,0,0,0-10.51,2L171,59.94A28,28,0,1,0,120,44a28.65,28.65,0,0,0,.15,2.94L73.68,66.3a28,28,0,1,0-28.6,44.83l1.85,46.38a28,28,0,1,0,32.74,41.42L128,212.47a28,28,0,1,0,49.13-18.79l27.21-42.75A28,28,0,1,0,212,96Zm-56,88-.89,0-16.18-48.53,46.65-2.22a27.94,27.94,0,0,0,5.28,9l-27.21,42.75A28,28,0,0,0,156,184ZM62.92,156.87l-1.85-46.38a28,28,0,0,0,10.12-6.13L113.72,129,72.26,161.22A28,28,0,0,0,62.92,156.87ZM149.57,72a27.8,27.8,0,0,0,8.94-2L189,108.06a27.86,27.86,0,0,0-4.18,9.22l-46.57,2.22ZM82.09,173.85,124,141.26l15.94,47.83a28.2,28.2,0,0,0-7.6,8L84,183.53A28,28,0,0,0,82.09,173.85ZM148,32a12,12,0,1,1-12,12A12,12,0,0,1,148,32ZM126.32,61.7A28.44,28.44,0,0,0,134,68.24l-11.3,47.45L79.23,90.52A28,28,0,0,0,80,84a28.65,28.65,0,0,0-.15-2.94ZM40,84A12,12,0,1,1,52,96,12,12,0,0,1,40,84ZM56,196a12,12,0,1,1,12-12A12,12,0,0,1,56,196Zm100,28a12,12,0,1,1,12-12A12,12,0,0,1,156,224Zm56-88a12,12,0,1,1,12-12A12,12,0,0,1,212,136Z"></path>'
|
||||
};
|
|
@ -1,36 +0,0 @@
|
|||
---
|
||||
import Card from "./Card/index.astro";
|
||||
import Button from "./Button.astro";
|
||||
import { LINKS } from "../lib/constants";
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import Tooltip from "./Tooltip/index";
|
||||
---
|
||||
|
||||
<Card colSpan="md:col-span-3" rowSpan="md:row-span-4">
|
||||
<div class="flex w-full h-full">
|
||||
<div class="flex flex-col justify-between md:max-h-[300px] gap-4">
|
||||
<div class="flex flex-col h-full">
|
||||
<h6 class="text-sm font-light m-0 text-gray-500">welcome</h6>
|
||||
<p class="m-0 font-light text-xl">
|
||||
Hi, I'm <b class="font-bold">Toastie_t0ast</b>, I like to make
|
||||
Discord Bots as well as work on game servers through my host
|
||||
Dragon's child hosting.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<a href={LINKS.toastielab} aria-label="toastielab profile" target="_blank">
|
||||
<Button aria-label="toastielab profile">
|
||||
<Icon name="ri:github-fill" class="h-6" />
|
||||
<span class="sr-only">Toastielab Profile</span>
|
||||
</Button>
|
||||
</a>
|
||||
<Tooltip client:visible>
|
||||
<Button aria-label="easter egg btn">
|
||||
<Icon name="ri:emotion-laugh-line" class="h-6" />
|
||||
<span class="sr-only">Easter egg button</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
47
src/components/MainHead.astro
Normal file
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
import '../styles/global.css';
|
||||
|
||||
interface Props {
|
||||
title?: string | undefined;
|
||||
description?: string | undefined;
|
||||
}
|
||||
|
||||
const {
|
||||
title = 'Toastie_t0ast',
|
||||
description = 'A random dev',
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="description" property="og:description" content={description} />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,400;0,700;1,400&family=Rubik:wght@500;600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script is:inline>
|
||||
// This code is inlined in the head to make dark mode instant & blocking.
|
||||
const getThemePreference = () => {
|
||||
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
||||
return localStorage.getItem('theme');
|
||||
}
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
};
|
||||
const isDark = getThemePreference() === 'dark';
|
||||
document.documentElement.classList[isDark ? 'add' : 'remove']('theme-dark');
|
||||
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
// Watch the document element and persist user preference when it changes.
|
||||
const observer = new MutationObserver(() => {
|
||||
const isDark = document.documentElement.classList.contains('theme-dark');
|
||||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||
});
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||
}
|
||||
</script>
|
353
src/components/Nav.astro
Normal file
|
@ -0,0 +1,353 @@
|
|||
---
|
||||
import Icon from './Icon.astro';
|
||||
import ThemeToggle from './ThemeToggle.astro';
|
||||
import type { iconPaths } from './IconPaths';
|
||||
|
||||
/** Main menu items */
|
||||
const textLinks: { label: string; href: string }[] = [
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'Projects', href: '/projects/' },
|
||||
{ label: 'Blog', href: "/blog/" },
|
||||
{ label: 'About', href: '/about/' },
|
||||
];
|
||||
|
||||
/** Icon links to social media — edit these with links to your profiles! */
|
||||
const iconLinks: { label: string; href: string; icon: keyof typeof iconPaths }[] = [
|
||||
{ label: 'Valkyriecoms', href: 'https://valkyriecoms.com/@toastie', icon: 'fediverse-logo' },
|
||||
{ label: 'Twitch', href: 'https://twitch.tv/toastie_t0ast', icon: 'twitch-logo' },
|
||||
{ label: 'Toastielab', href: 'https://toastielab.dev/toastie_t0ast', icon: 'git-branch' },
|
||||
{ label: 'Discord', href: 'https://discord.gg/3qvVNTk6sa', icon: 'discord-logo' },
|
||||
{ label: 'YouTube', href: 'https://www.youtube.com/@toastie_t0ast', icon: 'youtube-logo' },
|
||||
];
|
||||
|
||||
/** Test if a link is pointing to the current page. */
|
||||
const isCurrentPage = (href: string) => {
|
||||
let pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, '');
|
||||
if (pathname.at(0) !== '/') pathname = '/' + pathname;
|
||||
if (pathname.at(-1) !== '/') pathname += '/';
|
||||
return pathname === href || (href !== '/' && pathname.startsWith(href));
|
||||
};
|
||||
---
|
||||
|
||||
<nav>
|
||||
<div class="menu-header">
|
||||
<a href="/" class="site-title">
|
||||
<Icon icon="terminal-window" color="var(--accent-regular)" size="1.6em" gradient />
|
||||
Toastie_t0ast
|
||||
</a>
|
||||
<menu-button>
|
||||
<template>
|
||||
<button class="menu-button" aria-expanded="false">
|
||||
<span class="sr-only">Menu</span>
|
||||
<Icon icon="list" />
|
||||
</button>
|
||||
</template>
|
||||
</menu-button>
|
||||
</div>
|
||||
<noscript>
|
||||
<ul class="nav-items">
|
||||
{
|
||||
textLinks.map(({ label, href }) => (
|
||||
<li>
|
||||
<a aria-current={isCurrentPage(href) ? 'page' : null} class="link" href={href}>
|
||||
{label}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</noscript>
|
||||
<noscript>
|
||||
<div class="menu-footer">
|
||||
<div class="socials">
|
||||
{
|
||||
iconLinks.map(({ href, icon, label }) => (
|
||||
<a href={href} class="social">
|
||||
<span class="sr-only">{label}</span>
|
||||
<Icon icon={icon} />
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="menu-content" hidden>
|
||||
<ul class="nav-items">
|
||||
{
|
||||
textLinks.map(({ label, href }) => (
|
||||
<li>
|
||||
<a aria-current={isCurrentPage(href) ? 'page' : null} class="link" href={href}>
|
||||
{label}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<div class="menu-footer">
|
||||
<div class="socials">
|
||||
{
|
||||
iconLinks.map(({ href, icon, label }) => (
|
||||
<a href={href} class="social">
|
||||
<span class="sr-only">{label}</span>
|
||||
<Icon icon={icon} />
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="theme-toggle">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
class MenuButton extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Inject menu toggle button when JS runs.
|
||||
this.appendChild(this.querySelector('template')!.content.cloneNode(true));
|
||||
const btn = this.querySelector('button')!;
|
||||
|
||||
// Hide menu (shown by default to support no-JS browsers).
|
||||
const menu = document.getElementById('menu-content')!;
|
||||
menu.hidden = true;
|
||||
// Add "menu-content" class in JS to avoid covering content in non-JS browsers.
|
||||
menu.classList.add('menu-content');
|
||||
|
||||
/** Set whether the menu is currently expanded or collapsed. */
|
||||
const setExpanded = (expand: boolean) => {
|
||||
btn.setAttribute('aria-expanded', expand ? 'true' : 'false');
|
||||
menu.hidden = !expand;
|
||||
};
|
||||
|
||||
// Toggle menu visibility when the menu button is clicked.
|
||||
btn.addEventListener('click', () => setExpanded(menu.hidden));
|
||||
|
||||
// Hide menu button for large screens.
|
||||
const handleViewports = (e: MediaQueryList | MediaQueryListEvent) => {
|
||||
setExpanded(e.matches);
|
||||
btn.hidden = e.matches;
|
||||
};
|
||||
const mediaQueries = window.matchMedia('(min-width: 50em)');
|
||||
handleViewports(mediaQueries);
|
||||
mediaQueries.addEventListener('change', handleViewports);
|
||||
}
|
||||
}
|
||||
customElements.define('menu-button', MenuButton);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
nav {
|
||||
z-index: 9999;
|
||||
position: relative;
|
||||
font-family: var(--font-brand);
|
||||
font-weight: 500;
|
||||
margin-bottom: 3.5rem;
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
line-height: 1.1;
|
||||
color: var(--gray-0);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
border: 0;
|
||||
border-radius: 999rem;
|
||||
padding: 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
color: var(--gray-300);
|
||||
background: radial-gradient(var(--gray-900), var(--gray-800) 150%);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.menu-button[aria-expanded='true'] {
|
||||
color: var(--gray-0);
|
||||
background: linear-gradient(180deg, var(--gray-600), transparent),
|
||||
radial-gradient(var(--gray-900), var(--gray-800) 150%);
|
||||
}
|
||||
|
||||
.menu-button[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-button::before {
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
content: '';
|
||||
background: var(--gradient-stroke);
|
||||
border-radius: 999rem;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.nav-items {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
font-size: var(--text-md);
|
||||
line-height: 1.2;
|
||||
list-style: none;
|
||||
padding: 2rem;
|
||||
background-color: var(--gray-999);
|
||||
border-bottom: 1px solid var(--gray-800);
|
||||
}
|
||||
|
||||
.link {
|
||||
display: inline-block;
|
||||
color: var(--gray-300);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link[aria-current] {
|
||||
color: var(--gray-0);
|
||||
}
|
||||
|
||||
.menu-footer {
|
||||
--icon-size: var(--text-xl);
|
||||
--icon-padding: 0.5rem;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem 2rem 1.5rem 1.5rem;
|
||||
background-color: var(--gray-999);
|
||||
border-radius: 0 0 0.75rem 0.75rem;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.socials {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.625rem;
|
||||
font-size: var(--icon-size);
|
||||
}
|
||||
|
||||
.social {
|
||||
display: flex;
|
||||
padding: var(--icon-padding);
|
||||
text-decoration: none;
|
||||
color: var(--accent-dark);
|
||||
transition: color var(--theme-transition);
|
||||
}
|
||||
|
||||
.social:hover,
|
||||
.social:focus {
|
||||
color: var(--accent-text-over);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: calc(var(--icon-size) + 2 * var(--icon-padding));
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
nav {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
padding: 2.5rem 5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.nav-items {
|
||||
position: relative;
|
||||
flex-direction: row;
|
||||
font-size: var(--text-sm);
|
||||
border-radius: 999rem;
|
||||
border: 0;
|
||||
padding: 0.5rem 0.5625rem;
|
||||
background: radial-gradient(var(--gray-900), var(--gray-800) 150%);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.nav-items::before {
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
content: '';
|
||||
background: var(--gradient-stroke);
|
||||
border-radius: 999rem;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.link {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 999rem;
|
||||
transition:
|
||||
color var(--theme-transition),
|
||||
background-color var(--theme-transition);
|
||||
}
|
||||
|
||||
.link:hover,
|
||||
.link:focus {
|
||||
color: var(--gray-100);
|
||||
background-color: var(--accent-subtle-overlay);
|
||||
}
|
||||
|
||||
.link[aria-current='page'] {
|
||||
color: var(--accent-text-over);
|
||||
background-color: var(--accent-regular);
|
||||
}
|
||||
|
||||
.menu-footer {
|
||||
--icon-padding: 0.375rem;
|
||||
|
||||
justify-self: flex-end;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.socials {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 60em) {
|
||||
.socials {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
.link[aria-current='page'] {
|
||||
color: SelectedItem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,17 +0,0 @@
|
|||
---
|
||||
import Card from "./Card/index.astro";
|
||||
import Pulse from "./Pulse.astro";
|
||||
---
|
||||
|
||||
<Card colSpan="md:col-span-1" rowSpan="md:row-span-2">
|
||||
<div class="flex justify-between w-full items-start mb-2">
|
||||
<div class="flex flex-col">
|
||||
<h2>Now</h2>
|
||||
<a href="https://sive.rs/nowff" target="_blank">
|
||||
<span class="text-xs text-gray-500 cursor-pointer">what's that ?</span>
|
||||
</a>
|
||||
</div>
|
||||
<Pulse />
|
||||
</div>
|
||||
<p class="text-xs">Currently studying</p>
|
||||
</Card>
|
16
src/components/Pill.astro
Normal file
|
@ -0,0 +1,16 @@
|
|||
<div class="pill"><slot /></div>
|
||||
|
||||
<style>
|
||||
.pill {
|
||||
display: flex;
|
||||
padding: 0.5rem 1rem;
|
||||
gap: 0.5rem;
|
||||
color: var(--accent-text-over);
|
||||
border: 1px solid var(--accent-regular);
|
||||
background-color: var(--accent-regular);
|
||||
border-radius: 999rem;
|
||||
font-size: var(--text-md);
|
||||
line-height: 1.35;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
64
src/components/PortfolioPreview.astro
Normal file
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
|
||||
interface Props {
|
||||
project: CollectionEntry<'projects'>;
|
||||
}
|
||||
|
||||
const { data, id } = Astro.props.project;
|
||||
---
|
||||
|
||||
<a class="card" href={`/projects/${id}`}>
|
||||
<span class="title">{data.title}</span>
|
||||
<img src={data.img} alt={data.img_alt || ''} loading="lazy" decoding="async" />
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
display: grid;
|
||||
grid-template: auto 1fr / auto 1fr;
|
||||
height: 11rem;
|
||||
background: var(--gradient-subtle);
|
||||
border: 1px solid var(--gray-800);
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-sm);
|
||||
text-decoration: none;
|
||||
font-family: var(--font-brand);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 500;
|
||||
transition: box-shadow var(--theme-transition);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.title {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
z-index: 1;
|
||||
margin: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--gray-999);
|
||||
color: var(--gray-200);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
img {
|
||||
grid-area: 1 / 1 / 3 / 3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.card {
|
||||
height: 22rem;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
border-radius: 0.9375rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,6 +0,0 @@
|
|||
<span class="relative flex h-3 w-3">
|
||||
<span
|
||||
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
||||
></span>
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
|
||||
</span>
|
57
src/components/Skills.astro
Normal file
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
import Icon from './Icon.astro';
|
||||
---
|
||||
|
||||
<section class="box skills">
|
||||
<div class="stack gap-2 lg:gap-4">
|
||||
<Icon icon="terminal-window" color="var(--accent-regular)" size="2.5rem" gradient />
|
||||
<h2>Discord bot dev</h2>
|
||||
<p>I have been working on a Discord bot called Ellie since 2018.</p>
|
||||
</div>
|
||||
<div class="stack gap-2 lg:gap-4">
|
||||
<Icon icon="trophy" color="var(--accent-regular)" size="2.5rem" gradient />
|
||||
<h2>Systems administrator</h2>
|
||||
<p>I am a systems administrator and have been working on my skills since 2021.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.box {
|
||||
border: 1px solid var(--gray-800);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
background-color: var(--gray-999_40);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.skills {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.skills h2 {
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.skills p {
|
||||
color: var(--gray-400);
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.box {
|
||||
border-radius: 1.5rem;
|
||||
padding: 2.5rem;
|
||||
}
|
||||
|
||||
.skills {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 5rem;
|
||||
}
|
||||
|
||||
.skills h2 {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
}
|
||||
</style>
|
95
src/components/ThemeToggle.astro
Normal file
|
@ -0,0 +1,95 @@
|
|||
---
|
||||
import Icon from './Icon.astro';
|
||||
---
|
||||
|
||||
<theme-toggle>
|
||||
<button>
|
||||
<span class="sr-only">Dark theme</span>
|
||||
<span class="icon light"><Icon icon="sun" /></span>
|
||||
<span class="icon dark"><Icon icon="moon-stars" /></span>
|
||||
</button>
|
||||
</theme-toggle>
|
||||
|
||||
<style>
|
||||
button {
|
||||
display: flex;
|
||||
border: 0;
|
||||
border-radius: 999rem;
|
||||
padding: 0;
|
||||
background-color: var(--gray-999);
|
||||
box-shadow: inset 0 0 0 1px var(--accent-overlay);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 0.5rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
font-size: 1rem;
|
||||
color: var(--accent-overlay);
|
||||
}
|
||||
|
||||
.icon.light::before {
|
||||
content: '';
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--accent-regular);
|
||||
border-radius: 999rem;
|
||||
}
|
||||
|
||||
:global(.theme-dark) .icon.light::before {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
:global(.theme-dark) .icon.dark,
|
||||
:global(html:not(.theme-dark)) .icon.light,
|
||||
button[aria-pressed='false'] .icon.light {
|
||||
color: var(--accent-text-over);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.icon,
|
||||
.icon.light::before {
|
||||
transition:
|
||||
transform var(--theme-transition),
|
||||
color var(--theme-transition);
|
||||
}
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.icon.light::before {
|
||||
background-color: SelectedItem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
class ThemeToggle extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const button = this.querySelector('button')!;
|
||||
|
||||
/** Set the theme to dark/light mode. */
|
||||
const setTheme = (dark: boolean) => {
|
||||
document.documentElement.classList[dark ? 'add' : 'remove']('theme-dark');
|
||||
button.setAttribute('aria-pressed', String(dark));
|
||||
};
|
||||
|
||||
// Toggle the theme when a user clicks the button.
|
||||
button.addEventListener('click', () => setTheme(!this.isDark()));
|
||||
|
||||
// Initialize button state to reflect current theme.
|
||||
setTheme(this.isDark());
|
||||
}
|
||||
|
||||
isDark() {
|
||||
return document.documentElement.classList.contains('theme-dark');
|
||||
}
|
||||
}
|
||||
customElements.define('theme-toggle', ThemeToggle);
|
||||
</script>
|
|
@ -1,39 +0,0 @@
|
|||
---
|
||||
import { getCurrentTimeInItaly, formatTimeForItaly } from "../lib/helpers";
|
||||
import Card from "./Card/index.astro";
|
||||
---
|
||||
|
||||
<script>
|
||||
import { onCleanup, onMount } from "solid-js";
|
||||
import { formatTimeForItaly } from "../lib/helpers";
|
||||
|
||||
let interval: ReturnType<typeof setInterval>;
|
||||
|
||||
function updateClock() {
|
||||
const timeDisplay = document.getElementById("timeDisplay");
|
||||
const now = new Date();
|
||||
|
||||
if (timeDisplay) {
|
||||
timeDisplay.textContent = formatTimeForItaly(now);
|
||||
timeDisplay.setAttribute("datetime", now.toISOString());
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
interval = setInterval(updateClock, 1000);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Card colSpan="lg:col-span-2" rowSpan="md:row-span-2" title="Time zone">
|
||||
<time
|
||||
datetime=""
|
||||
id="timeDisplay"
|
||||
class="text-2xl xl:text-5xl xl:whitespace-nowrap w-50 xl:w-100 h-[calc(100%-28px)] font-serif flex justify-center items-center"
|
||||
>
|
||||
{formatTimeForItaly(getCurrentTimeInItaly())}
|
||||
</time>
|
||||
</Card>
|
|
@ -1,79 +0,0 @@
|
|||
import { type JSX, Show, createSignal } from "solid-js";
|
||||
|
||||
type Props = {
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
function Tooltip(props: Props) {
|
||||
const [isVisible, setIsVisible] = createSignal(false);
|
||||
const [clickCount, setClickCount] = createSignal(0);
|
||||
|
||||
const messages = [
|
||||
"Hi there!",
|
||||
"Clicked again?",
|
||||
"Still here?",
|
||||
"Persistent, aren't you?",
|
||||
"What's up?",
|
||||
"Again? Really?",
|
||||
"You're curious!",
|
||||
"Not cool!",
|
||||
"Give it a break!",
|
||||
"That's annoying!",
|
||||
"Hands off!",
|
||||
"No more clicks!",
|
||||
"Seriously?!",
|
||||
"Ouch! That hurts!",
|
||||
"You're persistent!",
|
||||
"Why the curiosity?",
|
||||
"I'm getting tired!",
|
||||
"I'm bored!",
|
||||
"Enough's enough!",
|
||||
"Find another hobby!",
|
||||
"Stop, please!",
|
||||
"Okay, last one!",
|
||||
"That's it, I'm done!",
|
||||
];
|
||||
|
||||
const currentMessage = () => {
|
||||
const count = clickCount();
|
||||
if (count >= messages.length) {
|
||||
return messages[messages.length - 1];
|
||||
}
|
||||
return messages[count];
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="relative inline-block">
|
||||
<div
|
||||
onMouseDown={() => {
|
||||
setIsVisible(!isVisible());
|
||||
if (isVisible()) {
|
||||
setClickCount((count) => count + 1);
|
||||
}
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
setIsVisible(false);
|
||||
}}
|
||||
onTouchStart={() => {
|
||||
setIsVisible(!isVisible());
|
||||
if (isVisible()) {
|
||||
setClickCount((count) => count + 1);
|
||||
}
|
||||
}}
|
||||
onTouchEnd={() => {
|
||||
setIsVisible(false);
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
<Show when={isVisible()}>
|
||||
<div class="absolute left-1/2 -translate-x-1/2 -translate-y-24 mt-1 w-auto max-h-[70px] p-2 bg-black text-white text-center rounded-lg z-10 shadow-custom shadow-primary-500 border border-primary-500 whitespace-normal after:content-[''] after:block after:rotate-45 after:w-4 after:h-4 after:shadow-custom after:shadow-primary-500 after:absolute after:-bottom-2 after:-translate-x-1/2 after:left-1/2 after:bg-black after:z-20">
|
||||
<p class="w-max">{currentMessage()}</p>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tooltip;
|
30
src/content.config.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { glob } from 'astro/loaders';
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
export const collections = {
|
||||
projects: defineCollection({
|
||||
// Load Markdown files in the src/content/projects directory.
|
||||
loader: glob({ base: './src/content/projects', pattern: '**/*.md', }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
publishDate: z.coerce.date(),
|
||||
tags: z.array(z.string()),
|
||||
img: z.string(),
|
||||
img_alt: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
|
||||
blog: defineCollection({
|
||||
loader: glob({ base: './src/content/blog', pattern: '**/*.md', }),
|
||||
schema: z.object({
|
||||
author: z.string(),
|
||||
publishDate: z.date(),
|
||||
title: z.string(),
|
||||
postSlug: z.string().optional(),
|
||||
tags: z.array(z.string()).default(["others"]),
|
||||
description: z.string(),
|
||||
canonicalURL: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
};
|
|
@ -1,9 +1,10 @@
|
|||
---
|
||||
layout: ../../layouts/LayoutBlogPost.astro
|
||||
title: "New projects"
|
||||
description: "The projects I have started."
|
||||
pubDate: 2023-12-09
|
||||
category: "updates"
|
||||
author: Toastie
|
||||
publishDate: 2023-12-09
|
||||
title: New projects
|
||||
description: The projects I have started.
|
||||
tags:
|
||||
- project-update
|
||||
---
|
||||
|
||||
Hi there, it has been a while.
|
||||
|
@ -36,4 +37,4 @@ I am going to include a screenshot of Toastielab below (our icons a little bit b
|
|||
|
||||
- https://valkyriecoms.com
|
||||
- https://dragonschildhosting.net
|
||||
- https://toastielab.dev
|
||||
- https://toastielab.dev
|
22
src/content/blog/starting-fresh.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
author: Toastie
|
||||
publishDate: 2024-12-18
|
||||
title: Starting fresh
|
||||
description: A small update on my life.
|
||||
tags:
|
||||
- life-update
|
||||
---
|
||||
|
||||
Hi there it has been a while,
|
||||
|
||||
I have been quite busy in my day to day life with education as well as my work on both the Dragon's child studios and EllieBot projects as well as updating this website to look a little bit nicer.
|
||||
|
||||
This year has been a crazy one with the release of EllieBot v5 as well as other EllieBot related stuff including the EllieHub updater/launcher, other things I have been working on is Valkyriecoms, Toastielab as well as the other projects located in the projects tab on this site.
|
||||
|
||||
Over the next year I plan to continue making things I like and enjoy making and using so please keep an eye out on the projects tab to see what I post there next.
|
||||
|
||||
Thanks for letting me ramble and thank you for an amazing 2024.
|
||||
|
||||
See you in 2025 with whatever happens then.
|
||||
|
||||
Toastie.
|
|
@ -1,9 +1,10 @@
|
|||
---
|
||||
layout: ../../layouts/LayoutBlogPost.astro
|
||||
title: "Update on things"
|
||||
description: "Where I have been among other things."
|
||||
pubDate: 2023-06-21
|
||||
category: "updates"
|
||||
author: Toastie
|
||||
publishDate: 2023-06-21
|
||||
title: Update on things
|
||||
description: Where I have been among other things.
|
||||
tags:
|
||||
- update
|
||||
---
|
||||
|
||||
Hey guys it has been a while and I have not streamed in a little while and I want to update you on why this has happened, First of all I have been having somputer issues as well as some IRL stuff taking over a lot, Secondly I have started some major projects, one with a friend and another to support me first project. These projects are as follows [Valkyriecoms](https://valkyriecoms.com), [Toastielab](https://toastielab.dev) and last but not least a small group stared by me and a good friend who you may know by Elearu called [Dragon's Child Studios](https://dragonschildstudios.com).
|
||||
|
@ -12,4 +13,4 @@ As you can see I have been quite busy but I hope I will be able to return to str
|
|||
|
||||
Thanks for taking the time to read this,
|
||||
|
||||
Toastie_t0ast
|
||||
Toastie_t0ast
|
|
@ -1,8 +0,0 @@
|
|||
import { defineCollection } from 'astro:content';
|
||||
import { rssSchema } from '@astrojs/rss';
|
||||
|
||||
const blog = defineCollection({
|
||||
schema: rssSchema,
|
||||
});
|
||||
|
||||
export const collections = { blog };
|
26
src/content/projects/dcs.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
title: Dragon's child studios
|
||||
publishDate: 2024-12-01 00:00:00
|
||||
img: /assets/dcs.png
|
||||
img_alt: A picture of my the Dragon's child studios cover logo.
|
||||
description: |
|
||||
A small group of people who like to make stuff.
|
||||
tags:
|
||||
- Game dev
|
||||
- Services
|
||||
---
|
||||
|
||||
Dragon's child studios is a group of friends who came together through our love of games and we want to try and make games that people will love.
|
||||
|
||||
Our founders are myself and [Azrael Indrason](http://www.twitch.tv/azrael_indrason)
|
||||
|
||||
Our values are:
|
||||
|
||||
- Privacy Based:
|
||||
We do not like our data being sold so why should we sell your data off to other companies?
|
||||
|
||||
- Have fun making games:
|
||||
We try to have fun in all that we do. We love games and we love to play them. So why should we make games that arent fun?
|
||||
|
||||
- Honesty and transparency:
|
||||
If we have any privacy issues we will make sure that they are resolved and that a transparency report is made.
|
13
src/content/projects/ellie-bot.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
title: EllieBot
|
||||
publishDate: 2024-12-01 00:00:00
|
||||
img: /assets/ellie-casey.png
|
||||
img_alt: A picture of my OC Ellie who also acts as the mascot for the EllieBot project.
|
||||
description: |
|
||||
A small little Discord bot written in C#
|
||||
tags:
|
||||
- Dev
|
||||
- Discord
|
||||
---
|
||||
|
||||
EllieBot is a Discord bot that I have been working on since 2018 and she is currently on version 5.3.2 and her code is currently located at https://toastielab.dev/Emotions-stuff/elliebot
|
13
src/content/projects/toastielab.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
title: Toastielab
|
||||
publishDate: 2024-12-01 00:00:00
|
||||
img: /assets/toastielab.png
|
||||
img_alt: A picture of my the Toastielab cover logo.
|
||||
description: |
|
||||
A small git hosting site.
|
||||
tags:
|
||||
- Development
|
||||
- Services
|
||||
---
|
||||
|
||||
Toastielab is a small git platform that I operate wit the help of Dragon’s child studios which is where I host all my projects (even the one which this site is located at)
|
13
src/content/projects/valkyriecoms.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
title: Valkyriecoms
|
||||
publishDate: 2024-12-01 00:00:00
|
||||
img: /assets/valkyriecoms.png
|
||||
img_alt: A picture of my the valkyriecoms cover logo.
|
||||
description: |
|
||||
A safe place for people to hang out with others.
|
||||
tags:
|
||||
- Social media
|
||||
- Services
|
||||
---
|
||||
|
||||
Valkyriecoms is a social site I have been working on since December 2022.
|
12
src/env.d.ts
vendored
|
@ -1,12 +0,0 @@
|
|||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
interface ImportMetaEnv {
|
||||
readonly CONTENTFUL_SPACE_ID: string;
|
||||
readonly CONTENTFUL_DELIVERY_TOKEN: string;
|
||||
readonly CONTENTFUL_PREVIEW_TOKEN: string;
|
||||
}
|
||||
|
||||
declare module "*.riv" {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
84
src/layouts/BaseLayout.astro
Normal file
|
@ -0,0 +1,84 @@
|
|||
---
|
||||
// Learn about using Astro layouts:
|
||||
// https://docs.astro.build/en/core-concepts/layouts/
|
||||
|
||||
// Component Imports
|
||||
import MainHead from '../components/MainHead.astro';
|
||||
import Nav from '../components/Nav.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
|
||||
interface Props {
|
||||
title?: string | undefined;
|
||||
description?: string | undefined;
|
||||
}
|
||||
|
||||
const { title, description } = Astro.props;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<MainHead title={title} description={description} />
|
||||
</head>
|
||||
<body>
|
||||
<div class="stack backgrounds">
|
||||
<Nav />
|
||||
<slot />
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add “loaded” class once the document has completely loaded.
|
||||
addEventListener('load', () => document.documentElement.classList.add('loaded'));
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--_placeholder-bg: linear-gradient(transparent, transparent);
|
||||
--bg-image-main: url('/assets/backgrounds/bg-main-light-800w.jpg');
|
||||
--bg-image-main-curves: url('/assets/backgrounds/bg-main-light.svg');
|
||||
--bg-image-subtle-1: var(--_placeholder-bg);
|
||||
--bg-image-subtle-2: var(--_placeholder-bg);
|
||||
--bg-image-footer: var(--_placeholder-bg);
|
||||
--bg-svg-blend-mode: overlay;
|
||||
--bg-blend-mode: darken;
|
||||
--bg-image-aspect-ratio: 2.25;
|
||||
--bg-scale: 1.68;
|
||||
--bg-aspect-ratio: calc(var(--bg-image-aspect-ratio) / var(--bg-scale));
|
||||
--bg-gradient-size: calc(var(--bg-scale) * 100%);
|
||||
}
|
||||
|
||||
:root.theme-dark {
|
||||
--bg-image-main: url('/assets/backgrounds/bg-main-dark-800w.jpg');
|
||||
--bg-image-main-curves: url('/assets/backgrounds/bg-main-dark.svg');
|
||||
--bg-svg-blend-mode: darken;
|
||||
--bg-blend-mode: lighten;
|
||||
}
|
||||
|
||||
.backgrounds {
|
||||
min-height: 100%;
|
||||
isolation: isolate;
|
||||
background:
|
||||
/*noise*/
|
||||
url('/assets/backgrounds/noise.png') top center/220px repeat,
|
||||
/*footer*/ var(--bg-image-footer) bottom center/var(--bg-gradient-size) no-repeat,
|
||||
/*header1*/ var(--bg-image-main-curves) top center/var(--bg-gradient-size) no-repeat,
|
||||
/*header2*/ var(--bg-image-main) top center/var(--bg-gradient-size) no-repeat,
|
||||
/*base*/ var(--gray-999);
|
||||
background-blend-mode: /*noise*/
|
||||
overlay,
|
||||
/*footer*/ var(--bg-blend-mode),
|
||||
/*header1*/ var(--bg-svg-blend-mode),
|
||||
/*header2*/ normal,
|
||||
/*base*/ normal;
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
/* Deactivate custom backgrounds for high contrast users. */
|
||||
.backgrounds {
|
||||
background: none;
|
||||
background-blend-mode: none;
|
||||
--bg-gradient-size: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
|
@ -1,141 +0,0 @@
|
|||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
page?: "blog";
|
||||
slug?: string | undefined;
|
||||
frontmatter?: {
|
||||
file: string;
|
||||
url: string | undefined;
|
||||
} & {
|
||||
title: string;
|
||||
description: string;
|
||||
pubDate: string;
|
||||
minutesRead: string;
|
||||
};
|
||||
}
|
||||
|
||||
const { title, description, page, slug, frontmatter } = Astro.props;
|
||||
|
||||
const schema =
|
||||
page !== "blog"
|
||||
? {
|
||||
"@context": "http://schema.org",
|
||||
"@type": "Person",
|
||||
name: "Toastiet0ast",
|
||||
url: "https://toastiet0ast.com",
|
||||
sameAs: [
|
||||
"https://toastielab.dev/toastie_t0ast",
|
||||
],
|
||||
jobTitle: "Freelance Systems Administrator",
|
||||
worksFor: {
|
||||
"@type": "Organization",
|
||||
name: "Self-Employed",
|
||||
address: {
|
||||
"@type": "PostalAddress",
|
||||
addressLocality: "New Zealand",
|
||||
addressCountry: "NZ",
|
||||
},
|
||||
},
|
||||
nationality: {
|
||||
"@type": "Country",
|
||||
name: "New Zealand",
|
||||
},
|
||||
}
|
||||
: {
|
||||
"@context": "http://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
mainEntityOfPage: {
|
||||
"@type": "WebPage",
|
||||
"@id": `https://toastiet0ast.com/blog/${slug}`,
|
||||
},
|
||||
headline: frontmatter?.title || title,
|
||||
description: frontmatter?.description || title,
|
||||
author: {
|
||||
"@type": "Person",
|
||||
name: "Toastie_t0ast",
|
||||
url: "https://toastiet0ast.com",
|
||||
sameAs: [
|
||||
"https://toastielab.dev/toastie_t0ast",
|
||||
],
|
||||
},
|
||||
publisher: {
|
||||
"@type": "Organization",
|
||||
name: "toastiet0ast",
|
||||
},
|
||||
datePublished: frontmatter?.pubDate || new Date().toISOString(),
|
||||
dateModified: frontmatter?.pubDate || new Date().toISOString(),
|
||||
};
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta name="robots" content="/favicon/sitemap-index.xml" />
|
||||
<!-- Basic OG tags for sharing your website's content on platforms like Facebook and LinkedIn -->
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={Astro.url.origin} />
|
||||
<!-- Basic Twitter Card tags -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
|
||||
<link rel="preconnect" href="https://cdn.fontshare.com" />
|
||||
<script type="application/ld+json" set:html={JSON.stringify(schema)} />
|
||||
</head>
|
||||
<body
|
||||
class="bg-darkslate-700 md:h-screen flex flex-col justify-center items-center"
|
||||
>
|
||||
<slot name="loader" />
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style is:global>
|
||||
@font-face {
|
||||
font-family: "CabinetGrotesk";
|
||||
src: url("/fonts/CabinetGrotesk-Variable.ttf") format("truetype-variations");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 100 1000;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Satoshi";
|
||||
src: url("/fonts/Satoshi-Variable.ttf") format("truetype-variations");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 100 1000;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Satoshi", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: "Cabinet Grotesk", sans-serif;
|
||||
}
|
||||
p {
|
||||
font-family: "Satoshi";
|
||||
}
|
||||
|
||||
b {
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
</style>
|
|
@ -1,21 +0,0 @@
|
|||
---
|
||||
import BasicLayout from "./BasicLayout.astro";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
page?: "travel";
|
||||
fullScreen?: string;
|
||||
}
|
||||
|
||||
const { title, description, page, fullScreen } = Astro.props;
|
||||
---
|
||||
|
||||
<BasicLayout title={title} description={description}>
|
||||
<div
|
||||
slot="loader"
|
||||
class="loader bg-darkslate-700 text-neutral-50 text-3xl font-black uppercase flex justify-center items-center w-screen h-screen z-50 fixed top-0 bottom-0 right-0 left-0"
|
||||
>
|
||||
</div>
|
||||
<slot />
|
||||
</BasicLayout>
|
|
@ -1,41 +0,0 @@
|
|||
---
|
||||
import { formatDate } from "../lib/helpers";
|
||||
import type { MarkdownLayoutProps } from "astro";
|
||||
import BasicLayout from "./BasicLayout.astro";
|
||||
|
||||
type Props = MarkdownLayoutProps<{
|
||||
title: string;
|
||||
description: string;
|
||||
pubDate: string;
|
||||
minutesRead: string;
|
||||
}>;
|
||||
|
||||
const { slug } = Astro.params;
|
||||
const { frontmatter } = Astro.props;
|
||||
---
|
||||
|
||||
<BasicLayout
|
||||
frontmatter={frontmatter}
|
||||
slug={slug}
|
||||
page="blog"
|
||||
title={`Toastie_t0ast - ${frontmatter.title}`}
|
||||
description={frontmatter.description}
|
||||
>
|
||||
<main class="mx-auto max-w-3xl w-full h-screen p-8 text-neutral-100">
|
||||
<a
|
||||
href="/blog"
|
||||
class="text-white bg-neutral-900 hover:bg-neutral-800 px-4 py-2 border-1 border-solid border-neutral-600 rounded-lg mb-8"
|
||||
>Back</a
|
||||
>
|
||||
<div class="my-10">
|
||||
<h1 class="text-5xl font-semibold">{frontmatter.title}</h1>
|
||||
<div class="flex justify-between pt-4 text-gray-500 text-sm">
|
||||
<p>{formatDate(new Date(frontmatter.pubDate))}</p>
|
||||
<p>{frontmatter.minutesRead}</p>
|
||||
</div>
|
||||
</div>
|
||||
<article class="prose prose-p:text-red-500 prose-slate prose-invert">
|
||||
<slot />
|
||||
</article>
|
||||
</main>
|
||||
</BasicLayout>
|
|
@ -1,12 +0,0 @@
|
|||
import getReadingTime from "reading-time";
|
||||
import { toString } from "mdast-util-to-string";
|
||||
|
||||
export function remarkReadingTime() {
|
||||
return function (tree, { data }) {
|
||||
const textOnPage = toString(tree);
|
||||
const readingTime = getReadingTime(textOnPage);
|
||||
// readingTime.text will give us minutes read as a friendly string,
|
||||
// i.e. "3 min read"
|
||||
data.astro.frontmatter.minutesRead = readingTime.text;
|
||||
};
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
export const LINKS = {
|
||||
toastielab: "https://toastielab.dev/toastie_t0ast",
|
||||
discord: "https://discordapp.com/users/234542843732033537",
|
||||
valkyriecoms: "https://valkyriecoms.com/@toastie",
|
||||
};
|
||||
|
||||
export const loaderAnimation = [
|
||||
".loader",
|
||||
{ opacity: [1, 0], pointerEvents: "none" },
|
||||
{ easing: "ease-out" },
|
||||
];
|
|
@ -1,40 +0,0 @@
|
|||
export function trimText(input: string, maxLength: number = 100): string {
|
||||
if (input.length <= maxLength) return input;
|
||||
return input.substring(0, maxLength - 3) + "...";
|
||||
}
|
||||
export function getCurrentTimeInItaly(): Date {
|
||||
// Create a date object with the current UTC time
|
||||
const now = new Date();
|
||||
|
||||
// Convert the UTC time to Italy's time
|
||||
const offsetItaly = 2; // Italy is in Central European Summer Time (UTC+2), but you might need to adjust this based on Daylight Saving Time
|
||||
now.setHours(now.getUTCHours() + offsetItaly);
|
||||
|
||||
return now;
|
||||
}
|
||||
|
||||
export function formatTimeForItaly(date: Date): string {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: true, // This will format the time in 12-hour format with AM/PM
|
||||
timeZone: "Pacific/Auckland",
|
||||
};
|
||||
|
||||
let formattedTime = new Intl.DateTimeFormat("en-US", options).format(date);
|
||||
|
||||
// Append the time zone abbreviation. You can automate this with libraries like `moment-timezone`.
|
||||
// For simplicity, here I'm just appending "CET", but do remember that Italy switches between CET and CEST.
|
||||
formattedTime += " NZST";
|
||||
|
||||
return formattedTime;
|
||||
}
|
||||
|
||||
export function formatDate(date: Date): string {
|
||||
return date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
8
src/pages/404.astro
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
import Hero from '../components/Hero.astro';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Not Found" description="404 Error — this page was not found">
|
||||
<Hero title="Page Not Found" tagline="Not found" />
|
||||
</BaseLayout>
|
99
src/pages/about.astro
Normal file
|
@ -0,0 +1,99 @@
|
|||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
|
||||
import ContactCTA from '../components/ContactCTA.astro';
|
||||
import Hero from '../components/Hero.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="About | Toastie_t0ast" description="About Toastie_t0ast">
|
||||
<div class="stack gap-20">
|
||||
<main class="wrapper about">
|
||||
<Hero
|
||||
title="About"
|
||||
tagline="Thanks for stopping by. Read below to learn more about myself and my background."
|
||||
>
|
||||
</Hero>
|
||||
|
||||
<section>
|
||||
<h2 class="section-title">Background</h2>
|
||||
<div class="content">
|
||||
<p>
|
||||
Hi there I am Toastie a software developer from New Zealand I mostly write Discord bots and some other things.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="section-title">Education</h2>
|
||||
<div class="content">
|
||||
<p>Studied at Learner Me here in NZ.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="section-title">Skills</h2>
|
||||
<div class="content">
|
||||
<p>Discord bot development and systems administration</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<ContactCTA />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.about {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3.5rem;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-top: 1.5rem;
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
color: var(--gray-200);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
grid-column-start: 1;
|
||||
font-size: var(--text-xl);
|
||||
color: var(--gray-0);
|
||||
}
|
||||
|
||||
.content {
|
||||
grid-column: 2 / 4;
|
||||
}
|
||||
|
||||
.content :global(a) {
|
||||
text-decoration: 1px solid underline transparent;
|
||||
text-underline-offset: 0.25em;
|
||||
transition: text-decoration-color var(--theme-transition);
|
||||
}
|
||||
|
||||
.content :global(a:hover),
|
||||
.content :global(a:focus) {
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.about {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 60% 1fr;
|
||||
}
|
||||
|
||||
.about > :global(:first-child) {
|
||||
grid-column-start: 2;
|
||||
}
|
||||
|
||||
section {
|
||||
display: contents;
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
}
|
||||
</style>
|
39
src/pages/blog.astro
Normal file
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
|
||||
import ContactCTA from '../components/ContactCTA.astro';
|
||||
import BlogPreview from '../components/BlogPreview.astro';
|
||||
import Hero from '../components/Hero.astro';
|
||||
import Grid from '../components/Grid.astro';
|
||||
|
||||
const blog = (await getCollection('blog')).sort(
|
||||
(a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf(),
|
||||
);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="My Blog | Toastie_t0ast"
|
||||
description="Learn about what Toastie_t0ast is up to."
|
||||
>
|
||||
<div class="stack gap-20">
|
||||
<main class="wrapper stack gap-8">
|
||||
<Hero
|
||||
title="Blog"
|
||||
tagline="Here are some posts that I have made."
|
||||
align="start"
|
||||
/>
|
||||
<Grid variant="offset">
|
||||
{
|
||||
blog.map((blog) => (
|
||||
<li>
|
||||
<BlogPreview blog={blog} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
</main>
|
||||
<ContactCTA />
|
||||
</div>
|
||||
</BaseLayout>
|
|
@ -1,129 +1,152 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
|
||||
export const prerender = true;
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
|
||||
import ContactCTA from '../../components/ContactCTA.astro';
|
||||
import BlogHero from '../../components/BlogHero.astro';
|
||||
import Icon from '../../components/Icon.astro';
|
||||
import Pill from '../../components/Pill.astro';
|
||||
import { render } from 'astro:content';
|
||||
|
||||
interface Props {
|
||||
entry: CollectionEntry<'blog'>;
|
||||
}
|
||||
|
||||
// This is a dynamic route that generates a page for every Markdown file in src/content/
|
||||
// Read more about dynamic routes and this `getStaticPaths` function in the Astro docs:
|
||||
// https://docs.astro.build/en/core-concepts/routing/#dynamic-routes
|
||||
export async function getStaticPaths() {
|
||||
return (await getCollection("blog")).map(({ slug }) => ({
|
||||
params: { slug },
|
||||
}));
|
||||
const blog = await getCollection('blog');
|
||||
return blog.map((entry) => ({
|
||||
params: { slug: entry.id },
|
||||
props: { entry },
|
||||
}));
|
||||
}
|
||||
|
||||
const { slug } = Astro.params;
|
||||
|
||||
if (slug === undefined) {
|
||||
throw new Error("slug is missing");
|
||||
}
|
||||
|
||||
const posts = (await getCollection("blog")).sort(
|
||||
(blogEntryA, blogEntryB) =>
|
||||
(blogEntryB.data.pubDate || new Date()).getTime() -
|
||||
(blogEntryA.data.pubDate || new Date()).getTime()
|
||||
);
|
||||
|
||||
const entry = posts.find((entry) => entry.slug === slug);
|
||||
if (entry === undefined) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
|
||||
const { Content } = await entry.render();
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await render(entry);
|
||||
---
|
||||
|
||||
<Content />
|
||||
<BaseLayout title={entry.data.title} description={entry.data.description}>
|
||||
<div class="stack gap-20">
|
||||
<div class="stack gap-15">
|
||||
<header>
|
||||
<div class="wrapper stack gap-2">
|
||||
<a class="back-link" href="/blog/"><Icon icon="arrow-left" /> blog</a>
|
||||
<BlogHero title={entry.data.title} align="start">
|
||||
<p>Author: {entry.data.author}</p>
|
||||
<div class="details">
|
||||
<div class="tags">
|
||||
{entry.data.tags.map((t) => <Pill>{t}</Pill>)}
|
||||
</div>
|
||||
<p class="description">{entry.data.description}</p>
|
||||
</div>
|
||||
</BlogHero>
|
||||
</div>
|
||||
</header>
|
||||
<main class="wrapper">
|
||||
<div class="stack gap-10 content">
|
||||
<div class="content">
|
||||
<Content />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<ContactCTA />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<style is:global>
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
<style>
|
||||
header {
|
||||
padding-bottom: 2.5rem;
|
||||
border-bottom: 1px solid var(--gray-800);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
.back-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 28px;
|
||||
}
|
||||
.details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem;
|
||||
gap: 1.5rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
}
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 20px;
|
||||
}
|
||||
.description {
|
||||
font-size: var(--text-lg);
|
||||
max-width: 54ch;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 18px;
|
||||
}
|
||||
.content {
|
||||
max-width: 65ch;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 16px;
|
||||
}
|
||||
.content > :global(* + *) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
.content :global(h1),
|
||||
.content :global(h2),
|
||||
.content :global(h3),
|
||||
.content :global(h4),
|
||||
.content :global(h5) {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #1a0dab;
|
||||
text-decoration: none;
|
||||
}
|
||||
.content :global(img) {
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
background: var(--gradient-subtle);
|
||||
border: 1px solid var(--gray-800);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.content :global(blockquote) {
|
||||
font-size: var(--text-lg);
|
||||
font-family: var(--font-brand);
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
padding-inline-start: 1.5rem;
|
||||
border-inline-start: 0.25rem solid var(--accent-dark);
|
||||
color: var(--gray-0);
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-left: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.back-link,
|
||||
.content :global(a) {
|
||||
text-decoration: 1px solid underline transparent;
|
||||
text-underline-offset: 0.25em;
|
||||
transition: text-decoration-color var(--theme-transition);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 20px 0;
|
||||
padding-left: 15px;
|
||||
border-left: 5px solid #ccc;
|
||||
}
|
||||
.back-link:hover,
|
||||
.back-link:focus,
|
||||
.content :global(a:hover),
|
||||
.content :global(a:focus) {
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
font-family: "Courier New", monospace;
|
||||
background-color: #f4f4f4;
|
||||
border-radius: 5px;
|
||||
}
|
||||
@media (min-width: 50em) {
|
||||
.back-link {
|
||||
display: block;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.details {
|
||||
flex-direction: row;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 4px;
|
||||
font-size: 90%;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
</style>
|
||||
.content :global(blockquote) {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,47 +0,0 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import PostRow from "../../components/Blog/PostRow.astro";
|
||||
|
||||
const posts = (await getCollection("blog"))?.sort(
|
||||
(blogEntryA, blogEntryB) =>
|
||||
(blogEntryB.data.pubDate || new Date()).getTime() -
|
||||
(blogEntryA.data.pubDate || new Date()).getTime()
|
||||
);
|
||||
---
|
||||
|
||||
<script>
|
||||
import { timeline, type TimelineDefinition } from "motion";
|
||||
import { loaderAnimation } from "../../lib/constants";
|
||||
|
||||
const sequence = [loaderAnimation];
|
||||
|
||||
timeline(sequence as TimelineDefinition);
|
||||
</script>
|
||||
|
||||
<Layout
|
||||
title="Toastie_t0ast - Blog"
|
||||
description="I'm a developer based in New Zealand, I like to make Discord Bots as well as work on game servers through my host https://dragonschildhosting.net."
|
||||
>
|
||||
<main
|
||||
class="w-screen h-screen flex flex-col justify-start items-start max-w-3xl mx-auto p-8"
|
||||
>
|
||||
<a
|
||||
href="/"
|
||||
class="text-white bg-neutral-900 hover:bg-neutral-800 px-4 py-2 mb-8 border-1 border-solid border-neutral-600 rounded-lg"
|
||||
>Back</a
|
||||
>
|
||||
<h1 class="text-4xl font-bold mb-4 text-neutral-100">Posts</h1>
|
||||
<ul class="w-full">
|
||||
{
|
||||
posts?.map((post) => (
|
||||
<PostRow
|
||||
title={post.data.title || "No title"}
|
||||
date={post.data.pubDate || new Date()}
|
||||
url={post.slug}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</main>
|
||||
</Layout>
|
|
@ -1,61 +1,225 @@
|
|||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import Card from "../components/Card/index.astro";
|
||||
import IntroCard from "../components/IntroCard.astro";
|
||||
import ContactsCard from "../components/ContactsCard.astro";
|
||||
import TimeZone from "../components/TimeZoneCard.astro";
|
||||
import AboutMe from "../components/AboutMe.astro";
|
||||
import Now from "../components/Now.astro";
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
// Layout import — provides basic page elements: <head>, <nav>, <footer> etc.
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
|
||||
// Component Imports
|
||||
import CallToAction from '../components/CallToAction.astro';
|
||||
import Grid from '../components/Grid.astro';
|
||||
import Hero from '../components/Hero.astro';
|
||||
import Icon from '../components/Icon.astro';
|
||||
import Pill from '../components/Pill.astro';
|
||||
import PortfolioPreview from '../components/PortfolioPreview.astro';
|
||||
|
||||
// Page section components
|
||||
import ContactCTA from '../components/ContactCTA.astro';
|
||||
import Skills from '../components/Skills.astro';
|
||||
|
||||
// Content Fetching: List four most recent projects
|
||||
const projects = (await getCollection('projects'))
|
||||
.sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf())
|
||||
.slice(0, 4);
|
||||
|
||||
const blog = (await getCollection('blog'))
|
||||
.sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf())
|
||||
.slice(9, 4);
|
||||
|
||||
// Full Astro Component Syntax:
|
||||
// https://docs.astro.build/basics/astro-components/
|
||||
---
|
||||
|
||||
<script>
|
||||
import { stagger, spring, timeline, type TimelineDefinition } from "motion";
|
||||
import { loaderAnimation } from "../lib/constants";
|
||||
const cards = document.querySelectorAll(".card");
|
||||
<BaseLayout>
|
||||
<div class="stack gap-20 lg:gap-48">
|
||||
<div class="wrapper stack gap-8 lg:gap-20">
|
||||
<header class="hero">
|
||||
<Hero
|
||||
title="Hello, my name is Toastie_t0ast"
|
||||
tagline="I am a systems admin and a Discord bot developer."
|
||||
align="start"
|
||||
>
|
||||
<div class="roles">
|
||||
<Pill><Icon icon="code" size="1.33em" /> Developer</Pill>
|
||||
</div>
|
||||
</Hero>
|
||||
</header>
|
||||
|
||||
const sequence = [
|
||||
loaderAnimation,
|
||||
[
|
||||
cards,
|
||||
{ y: ["40%", "0%"], opacity: [0, 1] },
|
||||
{
|
||||
at: "-0.1",
|
||||
duration: 0.4,
|
||||
delay: stagger(0.3),
|
||||
easing: spring({ velocity: 100, stiffness: 50, damping: 10 }),
|
||||
},
|
||||
],
|
||||
];
|
||||
<Skills />
|
||||
</div>
|
||||
|
||||
timeline(sequence as TimelineDefinition);
|
||||
</script>
|
||||
<main class="wrapper stack gap-20 lg:gap-48">
|
||||
<section class="section with-background with-cta">
|
||||
<header class="section-header stack gap-2 lg:gap-4">
|
||||
<h3>Selected Projects</h3>
|
||||
<p>Take a look below at some of the projects I have worked on over the past few years.</p>
|
||||
</header>
|
||||
|
||||
<Layout
|
||||
title="Toastie_t0ast - A random dev"
|
||||
description="I'm a developer based in New Zealand, I like to make Discord Bots as well as work on game servers through my host https://dragonschildhosting.net."
|
||||
>
|
||||
<main
|
||||
class="text-white m-auto p-2 grid gap-2 max-w-6xl overflow-hidden relative w-full sm:p-4 sm:gap-2 md:grid-cols-2 md:gap-3 md:p-6 lg:h-screen lg:grid-rows-8 lg:grid-cols-4 lg:gap-4 lg:max-h-[800px]"
|
||||
>
|
||||
<IntroCard />
|
||||
<AboutMe />
|
||||
<ContactsCard />
|
||||
<TimeZone />
|
||||
<Now />
|
||||
<Card
|
||||
colSpan="md:col-span-1"
|
||||
rowSpan="md:row-span-2 flex gap-4"
|
||||
title="Blog"
|
||||
href="/blog"
|
||||
/>
|
||||
<Card colSpan="md:col-span-1" rowSpan="md:row-span-1">
|
||||
<p class="text-xs">
|
||||
© 2024 · Crafted with ♥️ using <a
|
||||
href="https://astro.build/"
|
||||
target="_blank"
|
||||
class="text-red-500">Astro</a
|
||||
> by Toastie_t0ast.
|
||||
</p>
|
||||
</Card>
|
||||
</main>
|
||||
</Layout>
|
||||
<div class="gallery">
|
||||
<Grid variant="offset">
|
||||
{
|
||||
projects.map((project) => (
|
||||
<li>
|
||||
<PortfolioPreview project={project} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
<div class="cta">
|
||||
<CallToAction href="/projects/">
|
||||
View All
|
||||
<Icon icon="arrow-right" size="1.2em" />
|
||||
</CallToAction>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<ContactCTA />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.roles {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hero img {
|
||||
aspect-ratio: 5 / 4;
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: 6fr 4fr;
|
||||
padding-inline: 2.5rem;
|
||||
gap: 3.75rem;
|
||||
}
|
||||
|
||||
.roles {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.hero img {
|
||||
aspect-ratio: 3 / 4;
|
||||
border-radius: 4.5rem;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
/* ====================================================== */
|
||||
|
||||
.section {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.with-background {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.with-background::before {
|
||||
--hero-bg: var(--bg-image-subtle-2);
|
||||
|
||||
content: '';
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
left: 50%;
|
||||
width: 100vw;
|
||||
aspect-ratio: calc(2.25 / var(--bg-scale));
|
||||
top: 0;
|
||||
transform: translateY(-75%) translateX(-50%);
|
||||
background:
|
||||
url('/assets/backgrounds/noise.png') top center/220px repeat,
|
||||
var(--hero-bg) center center / var(--bg-gradient-size) no-repeat,
|
||||
var(--gray-999);
|
||||
background-blend-mode: overlay, normal, normal, normal;
|
||||
mix-blend-mode: var(--bg-blend-mode);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.with-background.bg-variant::before {
|
||||
--hero-bg: var(--bg-image-subtle-1);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
justify-self: center;
|
||||
text-align: center;
|
||||
max-width: 50ch;
|
||||
font-size: var(--text-md);
|
||||
color: var(--gray-300);
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.section {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-areas: 'header header header header' 'gallery gallery gallery gallery';
|
||||
gap: 5rem;
|
||||
}
|
||||
|
||||
.section.with-cta {
|
||||
grid-template-areas: 'header header header cta' 'gallery gallery gallery gallery';
|
||||
}
|
||||
|
||||
.section-header {
|
||||
grid-area: header;
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
font-size: var(--text-4xl);
|
||||
}
|
||||
|
||||
.with-cta .section-header {
|
||||
justify-self: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.gallery {
|
||||
grid-area: gallery;
|
||||
}
|
||||
|
||||
.cta {
|
||||
grid-area: cta;
|
||||
}
|
||||
}
|
||||
|
||||
/* ====================================================== */
|
||||
|
||||
.mention-card {
|
||||
display: flex;
|
||||
height: 7rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
border: 1px solid var(--gray-800);
|
||||
border-radius: 1.5rem;
|
||||
color: var(--gray-300);
|
||||
background: var(--gradient-subtle);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.mention-card {
|
||||
border-radius: 1.5rem;
|
||||
height: 9.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
39
src/pages/projects.astro
Normal file
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
|
||||
import ContactCTA from '../components/ContactCTA.astro';
|
||||
import PortfolioPreview from '../components/PortfolioPreview.astro';
|
||||
import Hero from '../components/Hero.astro';
|
||||
import Grid from '../components/Grid.astro';
|
||||
|
||||
const projects = (await getCollection('projects')).sort(
|
||||
(a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf(),
|
||||
);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="My Projects | Toastie_t0ast"
|
||||
description="Learn about Toastie_t0ast's most recent projects"
|
||||
>
|
||||
<div class="stack gap-20">
|
||||
<main class="wrapper stack gap-8">
|
||||
<Hero
|
||||
title="My Projects"
|
||||
tagline="See my most recent projects below to get an idea of my past experience."
|
||||
align="start"
|
||||
/>
|
||||
<Grid variant="offset">
|
||||
{
|
||||
projects.map((project) => (
|
||||
<li>
|
||||
<PortfolioPreview project={project} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
</main>
|
||||
<ContactCTA />
|
||||
</div>
|
||||
</BaseLayout>
|
152
src/pages/projects/[...slug].astro
Normal file
|
@ -0,0 +1,152 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
|
||||
import ContactCTA from '../../components/ContactCTA.astro';
|
||||
import Hero from '../../components/Hero.astro';
|
||||
import Icon from '../../components/Icon.astro';
|
||||
import Pill from '../../components/Pill.astro';
|
||||
import { render } from 'astro:content';
|
||||
|
||||
interface Props {
|
||||
entry: CollectionEntry<'projects'>;
|
||||
}
|
||||
|
||||
// This is a dynamic route that generates a page for every Markdown file in src/content/
|
||||
// Read more about dynamic routes and this `getStaticPaths` function in the Astro docs:
|
||||
// https://docs.astro.build/en/core-concepts/routing/#dynamic-routes
|
||||
export async function getStaticPaths() {
|
||||
const projects = await getCollection('projects');
|
||||
return projects.map((entry) => ({
|
||||
params: { slug: entry.id },
|
||||
props: { entry },
|
||||
}));
|
||||
}
|
||||
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await render(entry);
|
||||
---
|
||||
|
||||
<BaseLayout title={entry.data.title} description={entry.data.description}>
|
||||
<div class="stack gap-20">
|
||||
<div class="stack gap-15">
|
||||
<header>
|
||||
<div class="wrapper stack gap-2">
|
||||
<a class="back-link" href="/projects/"><Icon icon="arrow-left" /> projects</a>
|
||||
<Hero title={entry.data.title} align="start">
|
||||
<div class="details">
|
||||
<div class="tags">
|
||||
{entry.data.tags.map((t) => <Pill>{t}</Pill>)}
|
||||
</div>
|
||||
<p class="description">{entry.data.description}</p>
|
||||
</div>
|
||||
</Hero>
|
||||
</div>
|
||||
</header>
|
||||
<main class="wrapper">
|
||||
<div class="stack gap-10 content">
|
||||
{entry.data.img && <img src={entry.data.img} alt={entry.data.img_alt || ''} />}
|
||||
<div class="content">
|
||||
<Content />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<ContactCTA />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
header {
|
||||
padding-bottom: 2.5rem;
|
||||
border-bottom: 1px solid var(--gray-800);
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem;
|
||||
gap: 1.5rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: var(--text-lg);
|
||||
max-width: 54ch;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 65ch;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.content > :global(* + *) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.content :global(h1),
|
||||
.content :global(h2),
|
||||
.content :global(h3),
|
||||
.content :global(h4),
|
||||
.content :global(h5) {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.content :global(img) {
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
background: var(--gradient-subtle);
|
||||
border: 1px solid var(--gray-800);
|
||||
}
|
||||
|
||||
.content :global(blockquote) {
|
||||
font-size: var(--text-lg);
|
||||
font-family: var(--font-brand);
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
padding-inline-start: 1.5rem;
|
||||
border-inline-start: 0.25rem solid var(--accent-dark);
|
||||
color: var(--gray-0);
|
||||
}
|
||||
|
||||
.back-link,
|
||||
.content :global(a) {
|
||||
text-decoration: 1px solid underline transparent;
|
||||
text-underline-offset: 0.25em;
|
||||
transition: text-decoration-color var(--theme-transition);
|
||||
}
|
||||
|
||||
.back-link:hover,
|
||||
.back-link:focus,
|
||||
.content :global(a:hover),
|
||||
.content :global(a:focus) {
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.back-link {
|
||||
display: block;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex-direction: row;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
.content :global(blockquote) {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,23 +0,0 @@
|
|||
import rss from "@astrojs/rss";
|
||||
import { getCollection } from "astro:content";
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
const parser = new MarkdownIt();
|
||||
|
||||
export async function GET(context) {
|
||||
const blog = await getCollection("blog");
|
||||
return rss({
|
||||
title: "Toastie_t0ast’s Blog",
|
||||
description: "my blog",
|
||||
site: context.site,
|
||||
items: blog.map((post) => ({
|
||||
title: post.data.title,
|
||||
pubDate: post.data.pubDate,
|
||||
description: post.data.description,
|
||||
content: sanitizeHtml(parser.render(post.body)),
|
||||
// Compute RSS link from post `slug`
|
||||
// This example assumes all posts are rendered as `/blog/[slug]` routes
|
||||
link: `/blog/${post.slug}/`,
|
||||
})),
|
||||
});
|
||||
}
|
255
src/styles/global.css
Normal file
|
@ -0,0 +1,255 @@
|
|||
/* Global variables */
|
||||
:root {
|
||||
/* Colors */
|
||||
--gray-0: #090b11;
|
||||
--gray-50: #141925;
|
||||
--gray-100: #283044;
|
||||
--gray-200: #3d4663;
|
||||
--gray-300: #505d84;
|
||||
--gray-400: #6474a2;
|
||||
--gray-500: #8490b5;
|
||||
--gray-600: #a3acc8;
|
||||
--gray-700: #c3cadb;
|
||||
--gray-800: #e3e6ee;
|
||||
--gray-900: #f3f4f7;
|
||||
--gray-999-basis: 0, 0%, 100%;
|
||||
--gray-999_40: hsla(var(--gray-999-basis), 0.4);
|
||||
--gray-999: #ffffff;
|
||||
|
||||
--accent-light: #c561f6;
|
||||
--accent-regular: #7611a6;
|
||||
--accent-dark: #1c0056;
|
||||
--accent-overlay: hsla(280, 89%, 67%, 0.33);
|
||||
--accent-subtle-overlay: var(--accent-overlay);
|
||||
--accent-text-over: var(--gray-999);
|
||||
|
||||
--link-color: var(--accent-regular);
|
||||
|
||||
/* Gradients */
|
||||
--gradient-stop-1: var(--accent-light);
|
||||
--gradient-stop-2: var(--accent-regular);
|
||||
--gradient-stop-3: var(--accent-dark);
|
||||
--gradient-subtle: linear-gradient(150deg, var(--gray-900) 19%, var(--gray-999) 150%);
|
||||
--gradient-accent: linear-gradient(
|
||||
150deg,
|
||||
var(--gradient-stop-1),
|
||||
var(--gradient-stop-2),
|
||||
var(--gradient-stop-3)
|
||||
);
|
||||
--gradient-accent-orange: linear-gradient(
|
||||
150deg,
|
||||
#ca7879,
|
||||
var(--accent-regular),
|
||||
var(--accent-dark)
|
||||
);
|
||||
--gradient-stroke: linear-gradient(180deg, var(--gray-900), var(--gray-700));
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0px 6px 3px rgba(9, 11, 17, 0.01), 0px 4px 2px rgba(9, 11, 17, 0.01),
|
||||
0px 2px 2px rgba(9, 11, 17, 0.02), 0px 0px 1px rgba(9, 11, 17, 0.03);
|
||||
--shadow-md: 0px 28px 11px rgba(9, 11, 17, 0.01), 0px 16px 10px rgba(9, 11, 17, 0.03),
|
||||
0px 7px 7px rgba(9, 11, 17, 0.05), 0px 2px 4px rgba(9, 11, 17, 0.06);
|
||||
--shadow-lg: 0px 62px 25px rgba(9, 11, 17, 0.01), 0px 35px 21px rgba(9, 11, 17, 0.05),
|
||||
0px 16px 16px rgba(9, 11, 17, 0.1), 0px 4px 9px rgba(9, 11, 17, 0.12);
|
||||
|
||||
/* Text Sizes */
|
||||
--text-sm: 0.875rem;
|
||||
--text-base: 1rem;
|
||||
--text-md: 1.125rem;
|
||||
--text-lg: 1.25rem;
|
||||
--text-xl: 1.625rem;
|
||||
--text-2xl: 2.125rem;
|
||||
--text-3xl: 2.625rem;
|
||||
--text-4xl: 3.5rem;
|
||||
--text-5xl: 4.5rem;
|
||||
|
||||
/* Fonts */
|
||||
--font-system: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
--font-body: 'Public Sans', var(--font-system);
|
||||
--font-brand: Rubik, var(--font-system);
|
||||
|
||||
/* Transitions */
|
||||
--theme-transition: 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
:root.theme-dark {
|
||||
--gray-0: #ffffff;
|
||||
--gray-50: #f3f4f7;
|
||||
--gray-100: #e3e6ee;
|
||||
--gray-200: #c3cadb;
|
||||
--gray-300: #a3acc8;
|
||||
--gray-400: #8490b5;
|
||||
--gray-500: #6474a2;
|
||||
--gray-600: #505d84;
|
||||
--gray-700: #3d4663;
|
||||
--gray-800: #283044;
|
||||
--gray-900: #141925;
|
||||
--gray-999-basis: 225, 31%, 5%;
|
||||
--gray-999: #090b11;
|
||||
|
||||
--accent-light: #1c0056;
|
||||
--accent-regular: #7611a6;
|
||||
--accent-dark: #c561f6;
|
||||
--accent-overlay: hsla(280, 89%, 67%, 0.33);
|
||||
--accent-subtle-overlay: hsla(281, 81%, 36%, 0.33);
|
||||
--accent-text-over: var(--gray-0);
|
||||
|
||||
--link-color: var(--accent-dark);
|
||||
|
||||
--gradient-stop-1: #4c11c6;
|
||||
--gradient-subtle: linear-gradient(150deg, var(--gray-900) 19%, var(--gray-999) 81%);
|
||||
--gradient-accent-orange: linear-gradient(
|
||||
150deg,
|
||||
#ca7879,
|
||||
var(--accent-regular),
|
||||
var(--accent-light)
|
||||
);
|
||||
--gradient-stroke: linear-gradient(180deg, var(--gray-600), var(--gray-800));
|
||||
|
||||
--shadow-sm: 0px 6px 3px rgba(255, 255, 255, 0.01), 0px 4px 2px rgba(255, 255, 255, 0.01),
|
||||
0px 2px 2px rgba(255, 255, 255, 0.02), 0px 0px 1px rgba(255, 255, 255, 0.03);
|
||||
--shadow-md: 0px 28px 11px rgba(255, 255, 255, 0.01), 0px 16px 10px rgba(255, 255, 255, 0.03),
|
||||
0px 7px 7px rgba(255, 255, 255, 0.05), 0px 2px 4px rgba(255, 255, 255, 0.06);
|
||||
--shadow-lg: 0px 62px 25px rgba(255, 255, 255, 0.01), 0px 35px 21px rgba(255, 255, 255, 0.05),
|
||||
0px 16px 16px rgba(255, 255, 255, 0.1), 0px 4px 9px rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--gray-999);
|
||||
color: var(--gray-200);
|
||||
font-family: var(--font-body);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
*,
|
||||
*::after,
|
||||
*::before {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
line-height: 1.1;
|
||||
font-family: var(--font-brand);
|
||||
font-weight: 600;
|
||||
color: var(--gray-100);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-5xl);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-4xl);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--text-3xl);
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: var(--text-xl);
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
max-width: 83rem;
|
||||
margin-inline: auto;
|
||||
padding-inline: 1.5rem;
|
||||
}
|
||||
|
||||
.stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
.gap-8 {
|
||||
gap: 2rem;
|
||||
}
|
||||
.gap-10 {
|
||||
gap: 2.5rem;
|
||||
}
|
||||
.gap-15 {
|
||||
gap: 3.75rem;
|
||||
}
|
||||
.gap-20 {
|
||||
gap: 5rem;
|
||||
}
|
||||
.gap-30 {
|
||||
gap: 7.5rem;
|
||||
}
|
||||
.gap-48 {
|
||||
gap: 12rem;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.lg\:gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.lg\:gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
.lg\:gap-8 {
|
||||
gap: 2rem;
|
||||
}
|
||||
.lg\:gap-10 {
|
||||
gap: 2.5rem;
|
||||
}
|
||||
.lg\:gap-15 {
|
||||
gap: 3.75rem;
|
||||
}
|
||||
.lg\:gap-20 {
|
||||
gap: 5rem;
|
||||
}
|
||||
.lg\:gap-30 {
|
||||
gap: 7.5rem;
|
||||
}
|
||||
.lg\:gap-48 {
|
||||
gap: 12rem;
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import { vitePreprocess } from '@astrojs/svelte';
|
||||
|
||||
export default {
|
||||
preprocess: vitePreprocess(),
|
||||
};
|
|
@ -1,7 +1,5 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js"
|
||||
}
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
// uno.config.ts
|
||||
import { defineConfig, presetUno, presetWebFonts } from "unocss";
|
||||
|
||||
export default defineConfig({
|
||||
content: {
|
||||
filesystem: ["**/*.{html,js,ts,jsx,tsx,vue,svelte,astro}"],
|
||||
},
|
||||
theme: {
|
||||
boxShadow: {
|
||||
custom: `2px 2px 0`,
|
||||
"custom-hover": `1px 1px 0`,
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["CabinetGrotesk", "Satoshi"],
|
||||
},
|
||||
gridTemplateRows: {
|
||||
"auto-250": "repeat(auto-fill, 250px)",
|
||||
},
|
||||
gridTemplateColumns: {
|
||||
"4-minmax": "repeat(4, minmax(150px, 1fr))",
|
||||
},
|
||||
colors: {
|
||||
gray: {
|
||||
50: "#FAFAFA",
|
||||
100: "#F5F5F5",
|
||||
200: "#E5E5E5",
|
||||
300: "#D4D4D4",
|
||||
400: "#A3A3A3",
|
||||
500: "#737373",
|
||||
600: "#525252",
|
||||
700: "#404040",
|
||||
800: "#262626",
|
||||
900: "#171717",
|
||||
},
|
||||
darkslate: {
|
||||
50: "#3D3D3D",
|
||||
100: "#2C2C2C",
|
||||
200: "#262626",
|
||||
300: "#202020",
|
||||
400: "#1A1A1A",
|
||||
500: "#171717" /* Exactly your example for the background */,
|
||||
600: "#141414",
|
||||
700: "#111111",
|
||||
800: "#0E0E0E",
|
||||
900: "#0B0B0B" /* Deeper and darker */,
|
||||
},
|
||||
primary: {
|
||||
100: "#F9CDD3",
|
||||
200: "#F3A3AA",
|
||||
300: "#EC7981",
|
||||
400: "#E64F59",
|
||||
500: "#E63946",
|
||||
600: "#CF2F3D",
|
||||
700: "#B82534",
|
||||
800: "#A01B2B",
|
||||
900: "#891321",
|
||||
},
|
||||
},
|
||||
},
|
||||
presets: [
|
||||
presetUno(),
|
||||
presetWebFonts({
|
||||
provider: "fontshare",
|
||||
fonts: {
|
||||
sans: ["Cabinet Grotesk", "Satoshi"],
|
||||
serif: "Zodiak",
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|