7 Commits

Author SHA1 Message Date
Michael Rausch 1a40e6e718 UI updates 2025-10-12 00:58:56 +13:00
Michael Rausch 237668fd31 UI updates 2025-10-11 21:06:21 +13:00
Michael Rausch facc608d98 Added CV redirect 2025-01-16 01:27:57 +13:00
Michael Rausch c85dc91466 Added CV redirect 2025-01-16 01:26:15 +13:00
Michael Rausch d35c906e50 Added CV 2025-01-16 01:23:09 +13:00
Michael Rausch 589553883a Added work portfolio 2025-01-16 00:57:44 +13:00
Michael Rausch c0c5a71b0a Added work portfolio 2025-01-16 00:42:01 +13:00
33 changed files with 5517 additions and 5137 deletions
+3 -2
View File
@@ -1,9 +1,10 @@
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: [mdx(), sitemap(), tailwind()],
integrations: [sitemap(), tailwind(), mdx()]
});
+2521 -3040
View File
File diff suppressed because it is too large Load Diff
+13 -8
View File
@@ -13,25 +13,30 @@
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@astrojs/check": "^0.5.9",
"@astrojs/mdx": "^2.2.0",
"@astrojs/check": "^0.9.4",
"@astrojs/mdx": "^4.0.6",
"@astrojs/rss": "^4.0.5",
"@astrojs/sitemap": "^3.1.1",
"@astrojs/tailwind": "^5.1.0",
"@astrojs/sitemap": "^3.2.1",
"@astrojs/tailwind": "^5.1.4",
"@fontsource/inter": "^5.0.17",
"@fontsource/lora": "^5.0.16",
"@tailwindcss/typography": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"astro": "^4.5.6",
"astro": "^5.1.7",
"astro-embed": "^0.9.0",
"astro-light-box": "^0.1.1",
"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.1",
"typescript": "^5.4.2"
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

+120
View File
@@ -0,0 +1,120 @@
---
// 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
View File
@@ -1,5 +1,4 @@
---
---
<div class="mx-auto max-w-screen-sm px-5">
+1 -1
View File
@@ -17,4 +17,4 @@
</script>
<span class="text-4xl" id="emoji">👋</span>
<span class="text-5xl" id="emoji">👋</span>
+10 -11
View File
@@ -1,24 +1,23 @@
---
import Container from "@components/Container.astro";
import Link from "@components/Link.astro";
import { SITE } from "@consts";
import { SITE, SOCIALS } from "@consts";
// Check if we're on the homepage
const isHomepage = Astro.url.pathname === '/';
---
<header>
<Container>
<div class="flex flex-wrap gap-y-2 justify-between">
<div class={`flex flex-wrap gap-y-2 ${isHomepage ? 'justify-end' : 'justify-between'}`}>
{!isHomepage && (
<Link href="/" underline={false}>
<div class="font-semibold">
<div class="font-semibold header-name">
{SITE.NAME}
</div>
</Link>
)}
<nav class="flex gap-1">
<Link href="/blog">
blog
</Link>
<span>
{`/`}
</span>
<Link href="/work">
work
</Link>
@@ -31,8 +30,8 @@ import { SITE } from "@consts";
<span>
{`/`}
</span>
<Link href="/lab">
lab
<Link href={SOCIALS.find(social => social.NAME === "cv")?.HREF || ""} external={true}>
cv
</Link>
</nav>
</div>
+1
View File
@@ -6,6 +6,7 @@ type Props = {
external?: boolean;
underline?: boolean;
confetti?: boolean;
class?: string;
}
const { href, external, underline = true, confetti, ...rest } = Astro.props;
+5 -5
View File
@@ -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-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">
<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">
{heading}
</div>
<div class="text-sm">
<div class="text-sm opacity-75 mt-1">
{subheading}
</div>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute top-1/2 right-2 -translate-y-1/2 size-5 stroke-2 fill-none stroke-current">
class="absolute top-1/2 right-4 -translate-y-1/2 size-5 stroke-2 fill-none stroke-current z-10">
<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>
+6
View File
@@ -0,0 +1,6 @@
---
---
<div class="mx-auto max-w-screen-xl px-5">
<slot />
</div>
+18
View File
@@ -0,0 +1,18 @@
---
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>
+5 -1
View File
@@ -2,7 +2,7 @@ import type { Site, Metadata, Socials } from "@types";
export const SITE: Site = {
NAME: "Michael Rausch",
EMAIL: "michael@rausch.nz",
EMAIL: "m@michaelraus.ch",
NUM_POSTS_ON_HOMEPAGE: 3,
NUM_WORKS_ON_HOMEPAGE: 2,
NUM_PROJECTS_ON_HOMEPAGE: 3,
@@ -36,5 +36,9 @@ export const SOCIALS: Socials = [
{
NAME: "linkedin",
HREF: "https://www.linkedin.com/in/michael-rausch-13445b8a/",
},
{
NAME: "cv",
HREF: "https://standardresume.co/r/sQUNyo7W9NsmFFG8ZvU_B",
}
];
+1
View File
@@ -17,6 +17,7 @@ const work = defineCollection({
role: z.string(),
dateStart: z.coerce.date(),
dateEnd: z.union([z.coerce.date(), z.string()]),
linkedinURL: z.string().optional(),
}),
});
+22
View File
@@ -0,0 +1,22 @@
---
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. Its hosted on Vercel, fly.io and Amazon AWS.
<br/>
![EasySample](/content/es1.png)
<br/>
![EasySample](/content/es2.png)
<br/>
![EasySample](/content/es3.png)
-14
View File
@@ -1,14 +0,0 @@
---
title: "QuickView Cloud"
description: "An AR cloud content management system"
date: "2024"
demoURL: "https://actuality.nz"
---
![UC Online](/content/quickview.png)
## Technologies
- NodeJS
- Firebase
- React
- UIKit
+16
View File
@@ -0,0 +1,16 @@
---
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).
![QuickView](/content/quickview.png)
+8
View File
@@ -0,0 +1,8 @@
---
company: "Standard"
role: "Software Engineer / Director"
dateStart: "01/01/2019"
dateEnd: "Mothballed"
linkedinURL: "https://www.linkedin.com/company/standard-nz"
---
@@ -3,5 +3,6 @@ company: "University of Canterbury"
role: "Software Engineer"
dateStart: "08/01/2023"
dateEnd: "present"
linkedinURL: "https://www.linkedin.com/company/university-of-canterbury"
---
+1
View File
@@ -3,5 +3,6 @@ company: "Actuality"
role: "Software Engineer"
dateStart: "02/11/2022"
dateEnd: "05/05/2023"
linkedinURL: "https://www.linkedin.com/company/actuality"
---
+1
View File
@@ -19,6 +19,7 @@ const { title, description, redirect } = Astro.props;
<Head title={`${title} | ${SITE.NAME}`} description={description} redirect={redirect}/>
</head>
<body>
<Header />
<main>
<slot />
</main>
+1
View File
@@ -14,6 +14,7 @@ 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();
+49
View File
@@ -0,0 +1,49 @@
---
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>
&bull;
<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>
+56
View File
@@ -0,0 +1,56 @@
---
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>
+130 -15
View File
@@ -1,26 +1,141 @@
---
import { getCollection } from "astro:content";
import Container from "@components/Container.astro";
import PageLayout from "@layouts/PageLayout.astro";
import { HOME } from "@consts";
import EmojiScroller from "@components/EmojiScroller.astro";
import LinkCard from "@components/LinkCard.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 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 };
})
);
---
<PageLayout title={HOME.TITLE} description={HOME.DESCRIPTION}>
<Container>
<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>
<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>.
<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.
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>
</div>
</Container>
</PageLayout>
+67
View File
@@ -0,0 +1,67 @@
---
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>
&bull;
<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>
+30
View File
@@ -0,0 +1,30 @@
---
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>
+11
View File
@@ -0,0 +1,11 @@
---
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>
+51
View File
@@ -0,0 +1,51 @@
---
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>
+136 -2
View File
@@ -14,21 +14,35 @@ html.dark {
html,
body {
@apply size-full;
min-height: 100vh;
}
body {
@apply font-sans antialiased;
@apply flex flex-col;
@apply bg-stone-100 dark:bg-black;
@apply bg-stone-100;
@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 dark:bg-black/25;
@apply bg-stone-100/75;
@apply backdrop-blur-sm saturate-200;
}
.dark header {
background: transparent !important;
backdrop-filter: none;
}
main {
@apply flex-1 py-32;
}
@@ -71,3 +85,123 @@ 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);
}
+2225 -2029
View File
File diff suppressed because it is too large Load Diff