Compare commits
3 Commits
main
..
mkl.gg-main
| Author | SHA1 | Date | |
|---|---|---|---|
| 256c6af932 | |||
| 896243fcee | |||
| 66fcfa31e8 |
+3
-4
@@ -1,10 +1,9 @@
|
||||
import { defineConfig } from "astro/config";
|
||||
import mdx from "@astrojs/mdx";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
import mdx from "@astrojs/mdx";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: "https://astro-nano-demo.vercel.app",
|
||||
integrations: [sitemap(), tailwind(), mdx()]
|
||||
});
|
||||
integrations: [mdx(), sitemap(), tailwind()],
|
||||
});
|
||||
|
||||
Generated
+3044
-2525
File diff suppressed because it is too large
Load Diff
+8
-13
@@ -13,30 +13,25 @@
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/mdx": "^4.0.6",
|
||||
"@astrojs/check": "^0.5.9",
|
||||
"@astrojs/mdx": "^2.2.0",
|
||||
"@astrojs/rss": "^4.0.5",
|
||||
"@astrojs/sitemap": "^3.2.1",
|
||||
"@astrojs/tailwind": "^5.1.4",
|
||||
"@astrojs/sitemap": "^3.1.1",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@fontsource/inter": "^5.0.17",
|
||||
"@fontsource/lora": "^5.0.16",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||
"@typescript-eslint/parser": "^7.3.1",
|
||||
"astro": "^5.1.7",
|
||||
"astro-embed": "^0.9.0",
|
||||
"astro-light-box": "^0.1.1",
|
||||
"astro": "^4.5.6",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"clsx": "^2.1.0",
|
||||
"devicons-astro": "^0.3.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-astro": "^0.32.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"gsap": "^3.13.0",
|
||||
"rehype": "^13.0.2",
|
||||
"sharp": "^0.33.3",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.3"
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 173 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 140 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 97 KiB |
@@ -1,120 +0,0 @@
|
||||
---
|
||||
// Contact Form Component
|
||||
---
|
||||
<div id="form-message" class="hidden mb-4"></div>
|
||||
|
||||
<form id="contact-form" class="space-y-6 contact-form-wrapper">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label for="name" class="block text-sm text-black/70 dark:text-white/60">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
required
|
||||
class="w-full px-4 py-3 rounded-md border border-black/5 dark:border-white/5 bg-transparent text-black dark:text-white placeholder-black/30 dark:placeholder-white/30 focus:outline-none focus:border-black/20 dark:focus:border-white/20 transition-all duration-300"
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="email" class="block text-sm text-black/70 dark:text-white/60">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
class="w-full px-4 py-3 rounded-md border border-black/5 dark:border-white/5 bg-transparent text-black dark:text-white placeholder-black/30 dark:placeholder-white/30 focus:outline-none focus:border-black/20 dark:focus:border-white/20 transition-all duration-300"
|
||||
placeholder="your.email@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="message" class="block text-sm text-black/70 dark:text-white/60">
|
||||
Message
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
required
|
||||
rows="5"
|
||||
class="w-full px-4 py-3 rounded-md border border-black/5 dark:border-white/5 bg-transparent text-black dark:text-white placeholder-black/30 dark:placeholder-white/30 focus:outline-none focus:border-black/20 dark:focus:border-white/20 transition-all duration-300 resize-none"
|
||||
placeholder="Your message..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-8 py-3 rounded-md bg-black/90 dark:bg-white/90 text-white dark:text-black font-medium hover:bg-black dark:hover:bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black/50 dark:focus:ring-white/50 transition-all duration-300 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span id="button-text">Send Message</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const form = document.getElementById('contact-form') as HTMLFormElement;
|
||||
const buttonText = document.getElementById('button-text') as HTMLSpanElement;
|
||||
const formMessage = document.getElementById('form-message') as HTMLDivElement;
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Get form data
|
||||
const formData = new FormData(form);
|
||||
const name = formData.get('name') as string;
|
||||
const email = formData.get('email') as string;
|
||||
const message = formData.get('message') as string;
|
||||
|
||||
// Disable button and show loading state
|
||||
const submitButton = form.querySelector('button[type="submit"]') as HTMLButtonElement;
|
||||
submitButton.disabled = true;
|
||||
buttonText.textContent = 'Sending...';
|
||||
formMessage.className = 'hidden';
|
||||
|
||||
try {
|
||||
// Send to API
|
||||
const response = await fetch('https://notify.api.standard.nz/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
email,
|
||||
message,
|
||||
destination: 'michael'
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Success
|
||||
formMessage.textContent = 'Message sent successfully! I\'ll get back to you soon.';
|
||||
formMessage.className = 'text-sm px-4 py-2.5 rounded-md bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border border-green-200 dark:border-green-800/30';
|
||||
form.style.display = 'none';
|
||||
} else {
|
||||
// Error response
|
||||
throw new Error('Failed to send message');
|
||||
}
|
||||
} catch (error) {
|
||||
// Network or other error
|
||||
formMessage.textContent = 'Failed to send message. Please try again or email me directly.';
|
||||
formMessage.className = 'text-sm px-4 py-2.5 rounded-md bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 border border-red-200 dark:border-red-800/30';
|
||||
} finally {
|
||||
// Re-enable button
|
||||
submitButton.disabled = false;
|
||||
buttonText.textContent = 'Send Message';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
<div class="mx-auto max-w-screen-sm px-5">
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
|
||||
</script>
|
||||
|
||||
<span class="text-5xl" id="emoji">👋</span>
|
||||
<span class="text-4xl" id="emoji">👋</span>
|
||||
|
||||
+16
-15
@@ -1,23 +1,24 @@
|
||||
---
|
||||
import Container from "@components/Container.astro";
|
||||
import Link from "@components/Link.astro";
|
||||
import { SITE, SOCIALS } from "@consts";
|
||||
|
||||
// Check if we're on the homepage
|
||||
const isHomepage = Astro.url.pathname === '/';
|
||||
import { SITE } from "@consts";
|
||||
---
|
||||
|
||||
<header>
|
||||
<Container>
|
||||
<div class={`flex flex-wrap gap-y-2 ${isHomepage ? 'justify-end' : 'justify-between'}`}>
|
||||
{!isHomepage && (
|
||||
<Link href="/" underline={false}>
|
||||
<div class="font-semibold header-name">
|
||||
{SITE.NAME}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
<div class="flex flex-wrap gap-y-2 justify-between">
|
||||
<Link href="/" underline={false}>
|
||||
<div class="font-semibold">
|
||||
{SITE.NAME}
|
||||
</div>
|
||||
</Link>
|
||||
<nav class="flex gap-1">
|
||||
<Link href="/blog">
|
||||
blog
|
||||
</Link>
|
||||
<span>
|
||||
{`/`}
|
||||
</span>
|
||||
<Link href="/work">
|
||||
work
|
||||
</Link>
|
||||
@@ -30,9 +31,9 @@ const isHomepage = Astro.url.pathname === '/';
|
||||
<span>
|
||||
{`/`}
|
||||
</span>
|
||||
<Link href={SOCIALS.find(social => social.NAME === "cv")?.HREF || ""} external={true}>
|
||||
cv
|
||||
</Link>
|
||||
<Link href="/lab">
|
||||
lab
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -6,7 +6,6 @@ type Props = {
|
||||
external?: boolean;
|
||||
underline?: boolean;
|
||||
confetti?: boolean;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { href, external, underline = true, confetti, ...rest } = Astro.props;
|
||||
|
||||
@@ -9,19 +9,19 @@ type Props = {
|
||||
const { heading, subheading, href, target="" } = Astro.props;
|
||||
---
|
||||
|
||||
<a href={href} target={target} class="relative group flex flex-nowrap py-4 px-5 pr-12 rounded-xl modern-card grain-texture hover:text-black dark:hover:text-white transition-all duration-300 ease-in-out">
|
||||
<div class="flex flex-col flex-1 truncate relative z-10">
|
||||
<div class="font-semibold text-base">
|
||||
<a href={href} target={target} class="relative group flex flex-nowrap py-3 px-4 pr-10 rounded-lg border border-black/15 dark:border-white/20 hover:bg-black/5 dark:hover:bg-white/5 hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out">
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
<div class="font-semibold">
|
||||
{heading}
|
||||
</div>
|
||||
<div class="text-sm opacity-75 mt-1">
|
||||
<div class="text-sm">
|
||||
{subheading}
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="absolute top-1/2 right-4 -translate-y-1/2 size-5 stroke-2 fill-none stroke-current z-10">
|
||||
class="absolute top-1/2 right-2 -translate-y-1/2 size-5 stroke-2 fill-none stroke-current">
|
||||
<line x1="5" y1="12" x2="19" y2="12" class="translate-x-3 group-hover:translate-x-0 scale-x-0 group-hover:scale-x-100 transition-transform duration-300 ease-in-out" />
|
||||
<polyline points="12 5 19 12 12 19" class="-translate-x-1 group-hover:translate-x-0 transition-transform duration-300 ease-in-out" />
|
||||
</svg>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
---
|
||||
|
||||
<div class="mx-auto max-w-screen-xl px-5">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import LinkCard from "./LinkCard.astro";
|
||||
import { dateRange } from "@lib/utils";
|
||||
|
||||
type Props = {
|
||||
entry: CollectionEntry<"work">;
|
||||
}
|
||||
|
||||
const { entry } = Astro.props;
|
||||
const href = entry.data.linkedinURL || "#";
|
||||
const heading = entry.data.company;
|
||||
const subheading = `${entry.data.role} • ${dateRange(entry.data.dateStart, entry.data.dateEnd)}`;
|
||||
---
|
||||
|
||||
<LinkCard href={href} heading={heading} subheading={subheading} target="_blank">
|
||||
|
||||
</LinkCard>
|
||||
+1
-5
@@ -2,7 +2,7 @@ import type { Site, Metadata, Socials } from "@types";
|
||||
|
||||
export const SITE: Site = {
|
||||
NAME: "Michael Rausch",
|
||||
EMAIL: "m@michaelraus.ch",
|
||||
EMAIL: "michael@rausch.nz",
|
||||
NUM_POSTS_ON_HOMEPAGE: 3,
|
||||
NUM_WORKS_ON_HOMEPAGE: 2,
|
||||
NUM_PROJECTS_ON_HOMEPAGE: 3,
|
||||
@@ -36,9 +36,5 @@ export const SOCIALS: Socials = [
|
||||
{
|
||||
NAME: "linkedin",
|
||||
HREF: "https://www.linkedin.com/in/michael-rausch-13445b8a/",
|
||||
},
|
||||
{
|
||||
NAME: "cv",
|
||||
HREF: "https://standardresume.co/r/sQUNyo7W9NsmFFG8ZvU_B",
|
||||
}
|
||||
];
|
||||
|
||||
@@ -17,7 +17,6 @@ const work = defineCollection({
|
||||
role: z.string(),
|
||||
dateStart: z.coerce.date(),
|
||||
dateEnd: z.union([z.coerce.date(), z.string()]),
|
||||
linkedinURL: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
title: "Tile Direct EasySample"
|
||||
description: "An AR cloud content management system"
|
||||
date: "2023"
|
||||
demoURL: ""
|
||||
---
|
||||
|
||||
TileDirect EasySample is a platform built to simplify how tile samples are managed and tracked. It lets sales teams check samples out to customers effortlessly, keeps tabs on inventory to prevent loss, and helps them follow up with customers at the right time—knowing exactly which tiles caught their attention. Customers can scan samples to explore more details online, see how tiles look in augmented reality, and get automated reminders for returns or care instructions.
|
||||
|
||||
The front end is built with Next.js, using shadcn/ui for styling and modern React Redux to manage state. On the backend, I used .NET Core with a PostgreSQL database, structured around an event-driven architecture. It’s hosted on Vercel, fly.io and Amazon AWS.
|
||||
|
||||
<br/>
|
||||
|
||||

|
||||
|
||||
<br/>
|
||||
|
||||

|
||||
|
||||
<br/>
|
||||
|
||||

|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: "QuickView Cloud"
|
||||
description: "An AR cloud content management system"
|
||||
date: "2024"
|
||||
demoURL: "https://actuality.nz"
|
||||
---
|
||||
|
||||

|
||||
|
||||
## Technologies
|
||||
- NodeJS
|
||||
- Firebase
|
||||
- React
|
||||
- UIKit
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
title: "QuickView Cloud"
|
||||
description: "An AR cloud content management system"
|
||||
date: "2024"
|
||||
demoURL: "/quickviewdemo"
|
||||
---
|
||||
import { Tweet, Vimeo, YouTube, LinkPreview } from 'astro-embed';
|
||||
import DevIcon from 'devicons-astro';
|
||||
|
||||
<YouTube id="tRZPLgZB_II" />
|
||||
|
||||
QuickView transforms product information into interactive augmented reality (AR) experiences that can be seamlessly embedded in websites, apps, and marketing materials, allowing users to visualize products in their own spaces.
|
||||
|
||||
QuickView is built with a modern tech stack, including React, Redux, Node.js, Go, Firebase, and Pixar Universal Scene Description (USDZ).
|
||||
|
||||

|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
company: "Standard"
|
||||
role: "Software Engineer / Director"
|
||||
dateStart: "01/01/2019"
|
||||
dateEnd: "Mothballed"
|
||||
linkedinURL: "https://www.linkedin.com/company/standard-nz"
|
||||
---
|
||||
|
||||
@@ -3,6 +3,5 @@ company: "University of Canterbury"
|
||||
role: "Software Engineer"
|
||||
dateStart: "08/01/2023"
|
||||
dateEnd: "present"
|
||||
linkedinURL: "https://www.linkedin.com/company/university-of-canterbury"
|
||||
---
|
||||
|
||||
|
||||
@@ -3,6 +3,5 @@ company: "Actuality"
|
||||
role: "Software Engineer"
|
||||
dateStart: "02/11/2022"
|
||||
dateEnd: "05/05/2023"
|
||||
linkedinURL: "https://www.linkedin.com/company/actuality"
|
||||
---
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ const { title, description, redirect } = Astro.props;
|
||||
<head>
|
||||
<Head title={`${title} | ${SITE.NAME}`} description={description} redirect={redirect}/>
|
||||
</head>
|
||||
<body>
|
||||
<Header />
|
||||
<body>
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
@@ -14,7 +14,6 @@ export function formatDate(date: Date) {
|
||||
}
|
||||
|
||||
export function readingTime(html: string) {
|
||||
if (!html) return "1 min read";
|
||||
const textOnly = html.replace(/<[^>]+>/g, "");
|
||||
const wordCount = textOnly.split(/\s+/).length;
|
||||
const readingTimeMinutes = ((wordCount / 200) + 1).toFixed();
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import PageLayout from "@layouts/PageLayout.astro";
|
||||
import Container from "@components/Container.astro";
|
||||
import FormattedDate from "@components/FormattedDate.astro";
|
||||
import { readingTime } from "@lib/utils";
|
||||
import BackToPrev from "@components/BackToPrev.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = (await getCollection("blog"))
|
||||
.filter(post => !post.data.draft)
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.slug },
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
type Props = CollectionEntry<"blog">;
|
||||
|
||||
const post = Astro.props;
|
||||
const { Content } = await post.render();
|
||||
---
|
||||
|
||||
<PageLayout title={post.data.title} description={post.data.description}>
|
||||
<Container>
|
||||
<div class="animate">
|
||||
<BackToPrev href="/blog">
|
||||
Back to blog
|
||||
</BackToPrev>
|
||||
</div>
|
||||
<div class="space-y-1 my-10">
|
||||
<div class="animate flex items-center gap-1.5">
|
||||
<div class="font-base text-sm">
|
||||
<FormattedDate date={post.data.date} />
|
||||
</div>
|
||||
•
|
||||
<div class="font-base text-sm">
|
||||
{readingTime(post.body)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="animate text-2xl font-semibold text-black dark:text-white">
|
||||
{post.data.title}
|
||||
</div>
|
||||
</div>
|
||||
<article class="animate">
|
||||
<Content />
|
||||
</article>
|
||||
</Container>
|
||||
</PageLayout>
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import PageLayout from "@layouts/PageLayout.astro";
|
||||
import Container from "@components/Container.astro";
|
||||
import ArrowCard from "@components/ArrowCard.astro";
|
||||
import { BLOG } from "@consts";
|
||||
|
||||
const data = (await getCollection("blog"))
|
||||
.filter(post => !post.data.draft)
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||
|
||||
type Acc = {
|
||||
[year: string]: CollectionEntry<"blog">[];
|
||||
}
|
||||
|
||||
const posts = data.reduce((acc: Acc, post) => {
|
||||
const year = post.data.date.getFullYear().toString();
|
||||
if (!acc[year]) {
|
||||
acc[year] = [];
|
||||
}
|
||||
acc[year].push(post);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const years = Object.keys(posts).sort((a, b) => parseInt(b) - parseInt(a));
|
||||
---
|
||||
|
||||
<PageLayout title={BLOG.TITLE} description={BLOG.DESCRIPTION}>
|
||||
<Container>
|
||||
<div class="space-y-10">
|
||||
<div class="animate font-semibold text-black dark:text-white text-4xl font-serif">
|
||||
Blog
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
{years.map(year => (
|
||||
<section class="animate space-y-4">
|
||||
<div class="font-semibold text-black dark:text-white">
|
||||
{year}
|
||||
</div>
|
||||
<div>
|
||||
<ul class="flex flex-col gap-4">
|
||||
{
|
||||
posts[year].map((post) => (
|
||||
<li>
|
||||
<ArrowCard entry={post}/>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</PageLayout>
|
||||
+16
-131
@@ -1,141 +1,26 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import Container from "@components/Container.astro";
|
||||
import PageLayout from "@layouts/PageLayout.astro";
|
||||
import ArrowCard from "@components/ArrowCard.astro";
|
||||
import WorkCard from "@components/WorkCard.astro";
|
||||
import Link from "@components/Link.astro";
|
||||
import ContactForm from "@components/ContactForm.astro";
|
||||
import { dateRange } from "@lib/utils";
|
||||
import { SITE, HOME, SOCIALS } from "@consts";
|
||||
import { HOME } from "@consts";
|
||||
import EmojiScroller from "@components/EmojiScroller.astro";
|
||||
|
||||
const blog = (await getCollection("blog"))
|
||||
.filter(post => !post.data.draft)
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||
.slice(0,SITE.NUM_POSTS_ON_HOMEPAGE);
|
||||
|
||||
const projects = (await getCollection("projects"))
|
||||
.filter(project => !project.data.draft)
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||
.slice(0,SITE.NUM_PROJECTS_ON_HOMEPAGE);
|
||||
|
||||
const allwork = (await getCollection("work"))
|
||||
.sort((a, b) => new Date(b.data.dateStart).valueOf() - new Date(a.data.dateStart).valueOf())
|
||||
.slice(0,SITE.NUM_WORKS_ON_HOMEPAGE);
|
||||
|
||||
const work = await Promise.all(
|
||||
allwork.map(async (item) => {
|
||||
const { Content } = await item.render();
|
||||
return { ...item, Content };
|
||||
})
|
||||
);
|
||||
|
||||
import LinkCard from "@components/LinkCard.astro";
|
||||
import Link from "@components/Link.astro";
|
||||
---
|
||||
|
||||
|
||||
<PageLayout title={HOME.TITLE} description={HOME.DESCRIPTION}>
|
||||
<Container>
|
||||
<h1 class="animate font-bold text-black dark:text-white text-4xl md:text-5xl font-serif leading-tight mb-5">
|
||||
Hi, I'm Michael <EmojiScroller/>
|
||||
</h1>
|
||||
<div class="space-y-16">
|
||||
<section>
|
||||
<article class="space-y-4">
|
||||
<p class="animate text-lg">
|
||||
I'm a software engineer at the University of Canterbury, working on <Link href="https://uconline.ac.nz" external class="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">Tuihono UC | UC Online</Link>.
|
||||
|
||||
I specialize in full-stack development. While my primary expertise is in software engineering, I also have a strong interest in cloud infrastructure and DevOps. Based in Christchurch, New Zealand
|
||||
<br/><br/>
|
||||
Feel free to connect with me through any of the social media links at the bottom of this page!
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
{ blog.length > 1 &&
|
||||
<section class="animate space-y-6">
|
||||
<div class="flex flex-wrap gap-y-2 items-center justify-between">
|
||||
<h4 class="font-semibold text-black dark:text-white text-xl">
|
||||
Latest posts
|
||||
</h4>
|
||||
<Link href="/blog">
|
||||
See all posts
|
||||
</Link>
|
||||
</div>
|
||||
<ul class="flex flex-col gap-4">
|
||||
{blog.map(post => (
|
||||
<li>
|
||||
<ArrowCard entry={post} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
}
|
||||
|
||||
|
||||
<section class="animate space-y-6">
|
||||
<div class="flex flex-wrap gap-y-2 items-center justify-between">
|
||||
<h4 class="font-semibold text-black dark:text-white text-xl">
|
||||
Work Experience
|
||||
</h4>
|
||||
<Link href="/work">
|
||||
See all work
|
||||
</Link>
|
||||
</div>
|
||||
<ul class="flex flex-col gap-4">
|
||||
{work.map(entry => (
|
||||
<li>
|
||||
<WorkCard entry={entry} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="animate space-y-6">
|
||||
<div class="flex flex-wrap gap-y-2 items-center justify-between">
|
||||
<h4 class="font-semibold text-black dark:text-white text-xl">
|
||||
Recent projects
|
||||
</h4>
|
||||
<Link href="/projects">
|
||||
See all projects
|
||||
</Link>
|
||||
</div>
|
||||
<ul class="flex flex-col gap-4">
|
||||
{projects.map(project => (
|
||||
<li>
|
||||
<ArrowCard entry={project} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="animate space-y-4">
|
||||
<h4 class="font-semibold text-black dark:text-white text-xl">
|
||||
Let's Connect
|
||||
</h4>
|
||||
<article>
|
||||
<p>
|
||||
If you want to get in touch with me about something or just to say hi,
|
||||
reach out on social media or send me an email.
|
||||
</p>
|
||||
</article>
|
||||
<ContactForm />
|
||||
<ul class="flex flex-wrap gap-2 pt-5">
|
||||
{SOCIALS.map(SOCIAL => (
|
||||
<li class="flex gap-x-2 text-nowrap">
|
||||
<Link href={SOCIAL.HREF} external aria-label={`${SITE.NAME} on ${SOCIAL.NAME}`}>
|
||||
{SOCIAL.NAME}
|
||||
</Link>
|
||||
{"/"}
|
||||
</li>
|
||||
))}
|
||||
<li class="line-clamp-1">
|
||||
<Link href={`mailto:${SITE.EMAIL}`} aria-label={`Email ${SITE.NAME}`} confetti>
|
||||
{SITE.EMAIL}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<h4 class="animate font-semibold text-black dark:text-white text-4xl font-serif">
|
||||
Welcome <EmojiScroller/>
|
||||
</h4>
|
||||
<div class="py-3">
|
||||
</div>
|
||||
<p>If you've landed here, you're probably not in the right place.</p>
|
||||
|
||||
<div class="pt-1">
|
||||
<Link href="https://michaelraus.ch">Head to my website -></Link>
|
||||
</div>
|
||||
|
||||
<p class="pt-12 text-xs">
|
||||
Recieved an email from this address? All emails sent to the @mkl.gg domain are forwarded to me. I may have provided you with a unique address, such as yourcompany@mkl.gg. These addresses are legitimate and help me organize and prioritize my emails while minimizing spam.
|
||||
</p>
|
||||
</Container>
|
||||
</PageLayout>
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import PageLayout from "@layouts/PageLayout.astro";
|
||||
import Container from "@components/Container.astro";
|
||||
import FormattedDate from "@components/FormattedDate.astro";
|
||||
import { readingTime } from "@lib/utils";
|
||||
import BackToPrev from "@components/BackToPrev.astro";
|
||||
import Link from "@components/Link.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const projects = (await getCollection("projects"))
|
||||
.filter(post => !post.data.draft)
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||
return projects.map((project) => ({
|
||||
params: { slug: project.slug },
|
||||
props: project,
|
||||
}));
|
||||
}
|
||||
type Props = CollectionEntry<"projects">;
|
||||
|
||||
const project = Astro.props;
|
||||
const { Content } = await project.render();
|
||||
---
|
||||
|
||||
<PageLayout title={project.data.title} description={project.data.description}>
|
||||
<Container>
|
||||
<div class="animate">
|
||||
<BackToPrev href="/projects">
|
||||
Back to projects
|
||||
</BackToPrev>
|
||||
</div>
|
||||
<div class="space-y-1 my-10">
|
||||
<div class="animate flex items-center gap-1.5">
|
||||
<div class="font-base text-sm">
|
||||
<FormattedDate date={project.data.date} />
|
||||
</div>
|
||||
•
|
||||
<div class="font-base text-sm">
|
||||
{readingTime(project.body)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="animate text-2xl font-semibold text-black dark:text-white">
|
||||
{project.data.title}
|
||||
</div>
|
||||
{(project.data.demoURL || project.data.repoURL) && (
|
||||
<nav class="animate flex gap-1">
|
||||
{project.data.demoURL && (
|
||||
<Link href={project.data.demoURL} external>
|
||||
live site
|
||||
</Link>
|
||||
)}
|
||||
{project.data.demoURL && project.data.repoURL && (
|
||||
<span>/</span>
|
||||
)}
|
||||
{project.data.repoURL && (
|
||||
<Link href={project.data.repoURL} external>
|
||||
repo
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
<article class="animate">
|
||||
<Content />
|
||||
</article>
|
||||
</Container>
|
||||
</PageLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import PageLayout from "@layouts/PageLayout.astro";
|
||||
import Container from "@components/Container.astro";
|
||||
import ArrowCard from "@components/ArrowCard.astro";
|
||||
import { PROJECTS } from "@consts";
|
||||
|
||||
const projects = (await getCollection("projects"))
|
||||
.filter(project => !project.data.draft)
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||
---
|
||||
|
||||
<PageLayout title={PROJECTS.TITLE} description={PROJECTS.DESCRIPTION}>
|
||||
<Container>
|
||||
<div class="space-y-10">
|
||||
<div class="animate font-semibold text-black dark:text-white text-5xl md:text-6xl font-serif">
|
||||
Projects
|
||||
</div>
|
||||
<ul class="animate flex flex-col gap-4">
|
||||
{
|
||||
projects.map((project) => (
|
||||
<li>
|
||||
<ArrowCard entry={project}/>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</Container>
|
||||
</PageLayout>
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
import PageLayout from "@layouts/PageLayout.astro";
|
||||
import WideContainer from "@components/WideContainer.astro";
|
||||
---
|
||||
|
||||
<PageLayout title={"QuickView Demo"} description={""}>
|
||||
<WideContainer>
|
||||
<iframe src="https://embedv1.quickview.co/?pid=dLFJz4mIGby5aV4kFORL" style="border: 1px solid #f0f0f0; width: 100%; min-height: 700px; border-radius: 20px;"></iframe>
|
||||
</WideContainer>
|
||||
</PageLayout>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import PageLayout from "@layouts/PageLayout.astro";
|
||||
import Container from "@components/Container.astro";
|
||||
import { dateRange } from "@lib/utils";
|
||||
import { WORK } from "@consts";
|
||||
|
||||
const collection = (await getCollection("work"))
|
||||
.sort((a, b) => new Date(b.data.dateStart).valueOf() - new Date(a.data.dateStart).valueOf());
|
||||
|
||||
const work = await Promise.all(
|
||||
collection.map(async (item) => {
|
||||
const { Content } = await item.render();
|
||||
return { ...item, Content };
|
||||
})
|
||||
);
|
||||
---
|
||||
|
||||
<PageLayout title={WORK.TITLE} description={WORK.DESCRIPTION}>
|
||||
<Container>
|
||||
<div class="space-y-10">
|
||||
<div class="animate font-semibold text-black dark:text-white text-5xl md:text-6xl font-serif">
|
||||
Work
|
||||
</div>
|
||||
<ul class="flex flex-col space-y-4">
|
||||
{
|
||||
work.map(entry => (
|
||||
<li class="animate">
|
||||
<div class="text-sm opacity-75">
|
||||
{dateRange(entry.data.dateStart, entry.data.dateEnd)}
|
||||
</div>
|
||||
<div class="font-semibold text-black dark:text-white">
|
||||
{entry.data.company}
|
||||
</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{entry.data.role}
|
||||
</div>
|
||||
<article>
|
||||
<entry.Content />
|
||||
</article>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<!--
|
||||
<ul class="animate flex flex-col gap-4">
|
||||
|
||||
</ul> -->
|
||||
</div>
|
||||
</Container>
|
||||
</PageLayout>
|
||||
+2
-136
@@ -14,35 +14,21 @@ html.dark {
|
||||
html,
|
||||
body {
|
||||
@apply size-full;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply font-sans antialiased;
|
||||
@apply flex flex-col;
|
||||
@apply bg-stone-100;
|
||||
@apply bg-stone-100 dark:bg-black;
|
||||
@apply text-black dark:text-white/75;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
background: #0f0f0f;
|
||||
background-attachment: fixed;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
@apply fixed top-0 left-0 right-0 z-50 py-5;
|
||||
@apply bg-stone-100/75;
|
||||
@apply bg-stone-100/75 dark:bg-black/25;
|
||||
@apply backdrop-blur-sm saturate-200;
|
||||
}
|
||||
|
||||
.dark header {
|
||||
background: transparent !important;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
main {
|
||||
@apply flex-1 py-32;
|
||||
}
|
||||
@@ -84,124 +70,4 @@ html #back-to-top {
|
||||
|
||||
html.scrolled #back-to-top {
|
||||
@apply opacity-100 pointer-events-auto;
|
||||
}
|
||||
|
||||
/* Grain texture effect */
|
||||
.grain-texture {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.grain-texture::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.15'/%3E%3C/svg%3E");
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
.grain-texture:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Modern card hover effects */
|
||||
.modern-card {
|
||||
position: relative !important;
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
.modern-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, #1a0a0a 0%, transparent 40%),
|
||||
radial-gradient(circle at 10% 20%, #ff8c42 0%, transparent 50%),
|
||||
radial-gradient(circle at 60% 50%, #4a9fd8 0%, transparent 60%),
|
||||
radial-gradient(circle at 100% 100%, #1a4d7a 0%, transparent 50%),
|
||||
linear-gradient(135deg, #2a1a1a 0%, #1a3a5a 100%);
|
||||
filter: blur(40px);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 0;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.modern-card:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Different color variations for each card */
|
||||
li:nth-child(1) .modern-card::after {
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, #1a0a0a 0%, transparent 40%),
|
||||
radial-gradient(circle at 10% 20%, #ff8c42 0%, transparent 50%),
|
||||
radial-gradient(circle at 60% 50%, #4a9fd8 0%, transparent 60%),
|
||||
radial-gradient(circle at 100% 100%, #1a4d7a 0%, transparent 50%),
|
||||
linear-gradient(135deg, #2a1a1a 0%, #1a3a5a 100%);
|
||||
}
|
||||
|
||||
li:nth-child(2) .modern-card::after {
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, #0a1a0a 0%, transparent 40%),
|
||||
radial-gradient(circle at 10% 20%, #42ff8c 0%, transparent 50%),
|
||||
radial-gradient(circle at 60% 50%, #4ad89f 0%, transparent 60%),
|
||||
radial-gradient(circle at 100% 100%, #1a7a4d 0%, transparent 50%),
|
||||
linear-gradient(135deg, #1a2a1a 0%, #1a5a3a 100%);
|
||||
}
|
||||
|
||||
li:nth-child(3) .modern-card::after {
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, #1a0a1a 0%, transparent 40%),
|
||||
radial-gradient(circle at 10% 20%, #ff42d8 0%, transparent 50%),
|
||||
radial-gradient(circle at 60% 50%, #9f4ad8 0%, transparent 60%),
|
||||
radial-gradient(circle at 100% 100%, #7a1a6d 0%, transparent 50%),
|
||||
linear-gradient(135deg, #2a1a2a 0%, #5a1a4a 100%);
|
||||
}
|
||||
|
||||
li:nth-child(4) .modern-card::after {
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, #1a1a0a 0%, transparent 40%),
|
||||
radial-gradient(circle at 10% 20%, #ffd842 0%, transparent 50%),
|
||||
radial-gradient(circle at 60% 50%, #d8b84a 0%, transparent 60%),
|
||||
radial-gradient(circle at 100% 100%, #7a6d1a 0%, transparent 50%),
|
||||
linear-gradient(135deg, #2a2a1a 0%, #5a4a1a 100%);
|
||||
}
|
||||
|
||||
li:nth-child(5) .modern-card::after {
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, #0a0a1a 0%, transparent 40%),
|
||||
radial-gradient(circle at 10% 20%, #8c42ff 0%, transparent 50%),
|
||||
radial-gradient(circle at 60% 50%, #6a4ad8 0%, transparent 60%),
|
||||
radial-gradient(circle at 100% 100%, #4d1a7a 0%, transparent 50%),
|
||||
linear-gradient(135deg, #1a1a2a 0%, #3a1a5a 100%);
|
||||
}
|
||||
|
||||
.modern-card:hover {
|
||||
border-color: rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
.dark .modern-card {
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.dark .modern-card:hover {
|
||||
border-color: rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
/* Header name animation */
|
||||
.header-name {
|
||||
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
Reference in New Issue
Block a user