Compare commits

...

7 commits

76 changed files with 3183 additions and 3848 deletions

6
.gitignore vendored
View file

@ -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/

View file

@ -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`

View file

@ -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({});

View file

@ -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"
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +0,0 @@
// postcss.config.cjs
module.exports = {
plugins: {},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
public/assets/dcs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View file

@ -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>

View 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>

View file

@ -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 />
</>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -0,0 +1,69 @@
---
const currentYear = new Date().getFullYear();
---
<footer>
<div class="group">
<p>&copy; {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
View 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
View 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
View 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>

View 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>'
};

View file

@ -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>

View 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
View 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>

View file

@ -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
View 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>

View 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>

View file

@ -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>

View 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>

View 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>

View file

@ -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>

View file

@ -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
View 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(),
}),
}),
};

View file

@ -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.

View file

@ -0,0 +1,22 @@
---
author: Toastie
publishDate: 2024-12-28
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.

View file

@ -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).

View file

@ -1,8 +0,0 @@
import { defineCollection } from 'astro:content';
import { rssSchema } from '@astrojs/rss';
const blog = defineCollection({
schema: rssSchema,
});
export const collections = { blog };

View 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.

View 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

View 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 Dragons child studios which is where I host all my projects (even the one which this site is located at)

View 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
View file

@ -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;
}

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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;
};
}

View file

@ -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" },
];

View file

@ -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
View 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
View 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
View 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>

View file

@ -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;
.content :global(blockquote) {
font-size: var(--text-2xl);
}
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>

View file

@ -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>

View file

@ -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");
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 }),
},
],
];
timeline(sequence as TimelineDefinition);
</script>
<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]"
<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"
>
<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>
<div class="roles">
<Pill><Icon icon="code" size="1.33em" /> Developer</Pill>
</div>
</Hero>
</header>
<Skills />
</div>
<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>
<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>
</Layout>
<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
View 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>

View 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>

View file

@ -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_t0asts 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
View 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;
}
}

View file

@ -1,5 +0,0 @@
import { vitePreprocess } from '@astrojs/svelte';
export default {
preprocess: vitePreprocess(),
};

View file

@ -1,7 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js"
}
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}

View file

@ -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",
},
}),
],
});