Compare commits

...

2 Commits
0.1.7 ... 0.3.0

Author SHA1 Message Date
f6c05b8a0c change work to projects 2024-08-23 21:01:26 -05:00
b8efef1a00 polishing pass 2024-08-23 20:46:46 -05:00
21 changed files with 34 additions and 78 deletions

1
.gitignore vendored
View File

@@ -23,4 +23,5 @@ pnpm-debug.log*
# jetbrains setting folder # jetbrains setting folder
.idea/ .idea/
# vscode workspace
site-profile.code-workspace site-profile.code-workspace

View File

@@ -1,6 +1,6 @@
FROM node:20.16.0-alpine3.20 AS base FROM node:20.16.0-alpine3.20 AS base
LABEL version="0.1.7" LABEL version="0.2.0"
LABEL description="Astro based website to use as a profile" LABEL description="Astro based website to use as a profile"
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"

View File

@@ -2,7 +2,6 @@ import { defineConfig } from 'astro/config';
import node from "@astrojs/node"; import node from "@astrojs/node";
// https://astro.build/config
export default defineConfig({ export default defineConfig({
output: "hybrid", output: "hybrid",
adapter: node({ adapter: node({

View File

@@ -1,7 +1,7 @@
{ {
"name": "site-profile", "name": "site-profile",
"type": "module", "type": "module",
"version": "0.1.7", "version": "0.2.0",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"start": "astro dev", "start": "astro dev",

View File

@@ -32,7 +32,6 @@ const { href } = Astro.props;
} }
} }
/* Overlay for hover effects. */
a::after { a::after {
content: ''; content: '';
position: absolute; position: absolute;

View File

@@ -16,6 +16,7 @@ const currentYear = new Date().getFullYear();
<a href="https://www.linkedin.com/in/alexanderlebens"> LinkedIn</a> <a href="https://www.linkedin.com/in/alexanderlebens"> LinkedIn</a>
</p> </p>
</footer> </footer>
<style> <style>
footer { footer {
display: flex; display: flex;

View File

@@ -24,7 +24,6 @@ const { variant } = Astro.props;
gap: 1.5rem; gap: 1.5rem;
} }
/* If last row contains only one item, make it span both columns. */
.grid.small > :global(:last-child:nth-child(odd)) { .grid.small > :global(:last-child:nth-child(odd)) {
grid-column: 1 / 3; grid-column: 1 / 3;
} }
@@ -40,12 +39,10 @@ const { variant } = Astro.props;
padding-bottom: var(--row-offset); padding-bottom: var(--row-offset);
} }
/* Shift first item in each row vertically to create staggered effect. */
.grid.offset > :global(:nth-child(odd)) { .grid.offset > :global(:nth-child(odd)) {
transform: translateY(var(--row-offset)); 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.offset > :global(:last-child:nth-child(odd)) {
grid-column: 2 / 3; grid-column: 2 / 3;
transform: none; transform: none;

View File

@@ -7,7 +7,7 @@ interface Props {
} }
const { const {
title = 'Alex Lebens: Personal Site', title = 'Alex Lebens',
description = 'The personal site of Alex Lebens', description = 'The personal site of Alex Lebens',
} = Astro.props; } = Astro.props;
--- ---
@@ -26,7 +26,6 @@ const {
rel="stylesheet" rel="stylesheet"
/> />
<script is:inline> <script is:inline>
// This code is inlined in the head to make dark mode instant & blocking.
const getThemePreference = () => { const getThemePreference = () => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) { if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme'); return localStorage.getItem('theme');
@@ -37,7 +36,6 @@ const {
document.documentElement.classList[isDark ? 'add' : 'remove']('theme-dark'); document.documentElement.classList[isDark ? 'add' : 'remove']('theme-dark');
if (typeof localStorage !== 'undefined') { if (typeof localStorage !== 'undefined') {
// Watch the document element and persist user preference when it changes.
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
const isDark = document.documentElement.classList.contains('theme-dark'); const isDark = document.documentElement.classList.contains('theme-dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light'); localStorage.setItem('theme', isDark ? 'dark' : 'light');

View File

@@ -3,17 +3,15 @@ import Icon from './Icon.astro';
import ThemeToggle from './ThemeToggle.astro'; import ThemeToggle from './ThemeToggle.astro';
import type { iconPaths } from './IconPaths'; import type { iconPaths } from './IconPaths';
/** Main menu items */
const textLinks: { label: string; href: string }[] = [ const textLinks: { label: string; href: string }[] = [
{ label: 'Home', href: '/' }, { label: 'Home', href: '/' },
{ label: 'Work', href: '/work/' }, { label: 'Projects', href: '/projects/' },
{ label: 'About', href: '/about/' }, { 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 }[] = [ const iconLinks: { label: string; href: string; icon: keyof typeof iconPaths }[] = [
{ label: 'GitHub', href: 'https://github.com/alexlebens', icon: 'github-logo' }, { label: 'GitHub', href: 'https://github.com/alexlebens', icon: 'github-logo' },
{ label: 'LinkedIn', href: 'https://www.linkedin.com/in/alexanderlebens', icon: 'codepen-logo' }, { label: 'LinkedIn', href: 'https://www.linkedin.com/in/alexanderlebens', icon: 'linkedin-logo' },
]; ];
--- ---
@@ -117,26 +115,20 @@ const iconLinks: { label: string; href: string; icon: keyof typeof iconPaths }[]
constructor() { constructor() {
super(); super();
// Inject menu toggle button when JS runs.
this.appendChild(this.querySelector('template')!.content.cloneNode(true)); this.appendChild(this.querySelector('template')!.content.cloneNode(true));
const btn = this.querySelector('button')!; const btn = this.querySelector('button')!;
// Hide menu (shown by default to support no-JS browsers).
const menu = document.getElementById('menu-content')!; const menu = document.getElementById('menu-content')!;
menu.hidden = true; menu.hidden = true;
// Add "menu-content" class in JS to avoid covering content in non-JS browsers.
menu.classList.add('menu-content'); menu.classList.add('menu-content');
/** Set whether the menu is currently expanded or collapsed. */
const setExpanded = (expand: boolean) => { const setExpanded = (expand: boolean) => {
btn.setAttribute('aria-expanded', expand ? 'true' : 'false'); btn.setAttribute('aria-expanded', expand ? 'true' : 'false');
menu.hidden = !expand; menu.hidden = !expand;
}; };
// Toggle menu visibility when the menu button is clicked.
btn.addEventListener('click', () => setExpanded(menu.hidden)); btn.addEventListener('click', () => setExpanded(menu.hidden));
// Hide menu button for large screens.
const handleViewports = (e: MediaQueryList | MediaQueryListEvent) => { const handleViewports = (e: MediaQueryList | MediaQueryListEvent) => {
setExpanded(e.matches); setExpanded(e.matches);
btn.hidden = e.matches; btn.hidden = e.matches;

View File

@@ -2,13 +2,13 @@
import type { CollectionEntry } from 'astro:content'; import type { CollectionEntry } from 'astro:content';
interface Props { interface Props {
project: CollectionEntry<'work'>; project: CollectionEntry<'projects'>;
} }
const { data, slug } = Astro.props.project; const { data, slug } = Astro.props.project;
--- ---
<a class="card" href={`/work/${slug}`}> <a class="card" href={`/projects/${slug}`}>
<span class="title">{data.title}</span> <span class="title">{data.title}</span>
<img src={data.img} alt={data.img_alt || ''} loading="lazy" decoding="async" /> <img src={data.img} alt={data.img_alt || ''} loading="lazy" decoding="async" />
</a> </a>

View File

@@ -74,16 +74,13 @@ import Icon from './Icon.astro';
const button = this.querySelector('button')!; const button = this.querySelector('button')!;
/** Set the theme to dark/light mode. */
const setTheme = (dark: boolean) => { const setTheme = (dark: boolean) => {
document.documentElement.classList[dark ? 'add' : 'remove']('theme-dark'); document.documentElement.classList[dark ? 'add' : 'remove']('theme-dark');
button.setAttribute('aria-pressed', String(dark)); button.setAttribute('aria-pressed', String(dark));
}; };
// Toggle the theme when a user clicks the button.
button.addEventListener('click', () => setTheme(!this.isDark())); button.addEventListener('click', () => setTheme(!this.isDark()));
// Initialize button state to reflect current theme.
setTheme(this.isDark()); setTheme(this.isDark());
} }

View File

@@ -1,7 +1,7 @@
import { defineCollection, z } from 'astro:content'; import { defineCollection, z } from 'astro:content';
export const collections = { export const collections = {
work: defineCollection({ projects: defineCollection({
type: 'content', type: 'content',
schema: z.object({ schema: z.object({
title: z.string(), title: z.string(),

View File

@@ -1,8 +1,4 @@
--- ---
// Learn about using Astro layouts:
// https://docs.astro.build/en/core-concepts/layouts/
// Component Imports
import MainHead from '../components/MainHead.astro'; import MainHead from '../components/MainHead.astro';
import Nav from '../components/Nav.astro'; import Nav from '../components/Nav.astro';
import Footer from '../components/Footer.astro'; import Footer from '../components/Footer.astro';
@@ -27,7 +23,6 @@ const { title, description } = Astro.props;
</div> </div>
<script> <script>
// Add “loaded” class once the document has completely loaded.
addEventListener('load', () => document.documentElement.classList.add('loaded')); addEventListener('load', () => document.documentElement.classList.add('loaded'));
</script> </script>
@@ -54,13 +49,12 @@ const { title, description } = Astro.props;
--bg-blend-mode: lighten; --bg-blend-mode: lighten;
} }
/* These backgrounds are displayed below the fold, so we lazy load them
once the `.loaded` class has been set. */
:root.loaded { :root.loaded {
--bg-image-subtle-1: url('/assets/backgrounds/bg-subtle-1-light-800w.jpg'); --bg-image-subtle-1: url('/assets/backgrounds/bg-subtle-1-light-800w.jpg');
--bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-light-800w.jpg'); --bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-light-800w.jpg');
--bg-image-footer: url('/assets/backgrounds/bg-footer-light-800w.jpg'); --bg-image-footer: url('/assets/backgrounds/bg-footer-light-800w.jpg');
} }
:root.loaded.theme-dark { :root.loaded.theme-dark {
--bg-image-subtle-1: url('/assets/backgrounds/bg-subtle-1-dark-800w.jpg'); --bg-image-subtle-1: url('/assets/backgrounds/bg-subtle-1-dark-800w.jpg');
--bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-dark-800w.jpg'); --bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-dark-800w.jpg');
@@ -72,6 +66,7 @@ const { title, description } = Astro.props;
--bg-scale: 1; --bg-scale: 1;
--bg-image-main: url('/assets/backgrounds/bg-main-light-1440w.jpg'); --bg-image-main: url('/assets/backgrounds/bg-main-light-1440w.jpg');
} }
:root.theme-dark { :root.theme-dark {
--bg-image-main: url('/assets/backgrounds/bg-main-dark-1440w.jpg'); --bg-image-main: url('/assets/backgrounds/bg-main-dark-1440w.jpg');
} }
@@ -81,6 +76,7 @@ const { title, description } = Astro.props;
--bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-light-1440w.jpg'); --bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-light-1440w.jpg');
--bg-image-footer: url('/assets/backgrounds/bg-footer-light-1440w.jpg'); --bg-image-footer: url('/assets/backgrounds/bg-footer-light-1440w.jpg');
} }
:root.loaded.theme-dark { :root.loaded.theme-dark {
--bg-image-subtle-1: url('/assets/backgrounds/bg-subtle-1-dark-1440w.jpg'); --bg-image-subtle-1: url('/assets/backgrounds/bg-subtle-1-dark-1440w.jpg');
--bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-dark-1440w.jpg'); --bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-dark-1440w.jpg');
@@ -92,21 +88,20 @@ const { title, description } = Astro.props;
min-height: 100%; min-height: 100%;
isolation: isolate; isolation: isolate;
background: background:
/*noise*/
url('/assets/backgrounds/noise.png') top center/220px repeat, url('/assets/backgrounds/noise.png') top center/220px repeat,
/*footer*/ var(--bg-image-footer) bottom center/var(--bg-gradient-size) no-repeat, 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, 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, var(--bg-image-main) top center/var(--bg-gradient-size) no-repeat,
/*base*/ var(--gray-999); var(--gray-999);
background-blend-mode: /*noise*/ background-blend-mode:
overlay, overlay,
/*footer*/ var(--bg-blend-mode), var(--bg-blend-mode),
/*header1*/ var(--bg-svg-blend-mode), var(--bg-svg-blend-mode),
/*header2*/ normal, normal,
/*base*/ normal; normal;
} }
@media (forced-colors: active) { @media (forced-colors: active) {
/* Deactivate custom backgrounds for high contrast users. */
.backgrounds { .backgrounds {
background: none; background: none;
background-blend-mode: none; background-blend-mode: none;

View File

@@ -1,10 +1,8 @@
--- ---
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
// Layout import — provides basic page elements: <head>, <nav>, <footer> etc.
import BaseLayout from '../layouts/BaseLayout.astro'; import BaseLayout from '../layouts/BaseLayout.astro';
// Component Imports
import CallToAction from '../components/CallToAction.astro'; import CallToAction from '../components/CallToAction.astro';
import Grid from '../components/Grid.astro'; import Grid from '../components/Grid.astro';
import Hero from '../components/Hero.astro'; import Hero from '../components/Hero.astro';
@@ -12,17 +10,12 @@ import Icon from '../components/Icon.astro';
import Pill from '../components/Pill.astro'; import Pill from '../components/Pill.astro';
import PortfolioPreview from '../components/PortfolioPreview.astro'; import PortfolioPreview from '../components/PortfolioPreview.astro';
// Page section components
import ContactCTA from '../components/ContactCTA.astro'; import ContactCTA from '../components/ContactCTA.astro';
import Skills from '../components/Skills.astro'; import Skills from '../components/Skills.astro';
// Content Fetching: List four most recent work projects const projects = (await getCollection('projects'))
const projects = (await getCollection('work'))
.sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()) .sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf())
.slice(0, 4); .slice(0, 4);
// Full Astro Component Syntax:
// https://docs.astro.build/basics/astro-components/
--- ---
<BaseLayout> <BaseLayout>
@@ -55,8 +48,8 @@ const projects = (await getCollection('work'))
<main class="wrapper stack gap-20 lg:gap-48"> <main class="wrapper stack gap-20 lg:gap-48">
<section class="section with-background with-cta"> <section class="section with-background with-cta">
<header class="section-header stack gap-2 lg:gap-4"> <header class="section-header stack gap-2 lg:gap-4">
<h3>Selected Work</h3> <h3>Selected Projects</h3>
<p>Take a look below at some of my featured work from the past few years.</p> <p>Take a look below at some of my featured projects from the past few years.</p>
</header> </header>
<div class="gallery"> <div class="gallery">
@@ -72,7 +65,7 @@ const projects = (await getCollection('work'))
</div> </div>
<div class="cta"> <div class="cta">
<CallToAction href="/work/"> <CallToAction href="/projects/">
View All View All
<Icon icon="arrow-right" size="1.2em" /> <Icon icon="arrow-right" size="1.2em" />
</CallToAction> </CallToAction>
@@ -147,8 +140,6 @@ const projects = (await getCollection('work'))
} }
} }
/* ====================================================== */
.section { .section {
display: grid; display: grid;
gap: 2rem; gap: 2rem;
@@ -228,8 +219,6 @@ const projects = (await getCollection('work'))
} }
} }
/* ====================================================== */
.mention-card { .mention-card {
display: flex; display: flex;
height: 7rem; height: 7rem;

View File

@@ -8,19 +8,19 @@ import PortfolioPreview from '../components/PortfolioPreview.astro';
import Hero from '../components/Hero.astro'; import Hero from '../components/Hero.astro';
import Grid from '../components/Grid.astro'; import Grid from '../components/Grid.astro';
const projects = (await getCollection('work')).sort( const projects = (await getCollection('projects')).sort(
(a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf(), (a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf(),
); );
--- ---
<BaseLayout <BaseLayout
title="My Work | Alex Lebens" title="My Projects | Alex Lebens"
description="Learn about Alex Lebens's most recent projects" description="Learn about Alex Lebens's most recent projects"
> >
<div class="stack gap-20"> <div class="stack gap-20">
<main class="wrapper stack gap-8"> <main class="wrapper stack gap-8">
<Hero <Hero
title="My Work" title="My Projects"
tagline="See my most recent projects below to get an idea of my past experience." tagline="See my most recent projects below to get an idea of my past experience."
align="start" align="start"
/> />

View File

@@ -9,15 +9,12 @@ import Icon from '../../components/Icon.astro';
import Pill from '../../components/Pill.astro'; import Pill from '../../components/Pill.astro';
interface Props { interface Props {
entry: CollectionEntry<'work'>; 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() { export async function getStaticPaths() {
const work = await getCollection('work'); const projects = await getCollection('projects');
return work.map((entry) => ({ return projects.map((entry) => ({
params: { slug: entry.slug }, params: { slug: entry.slug },
props: { entry }, props: { entry },
})); }));
@@ -32,7 +29,7 @@ const { Content } = await entry.render();
<div class="stack gap-15"> <div class="stack gap-15">
<header> <header>
<div class="wrapper stack gap-2"> <div class="wrapper stack gap-2">
<a class="back-link" href="/work/"><Icon icon="arrow-left" /> Work</a> <a class="back-link" href="/projects/"><Icon icon="arrow-left" /> Projects</a>
<Hero title={entry.data.title} align="start"> <Hero title={entry.data.title} align="start">
<div class="details"> <div class="details">
<div class="tags"> <div class="tags">

View File

@@ -1,6 +1,4 @@
/* Global variables */
:root { :root {
/* Colors */
--gray-0: #090b11; --gray-0: #090b11;
--gray-50: #141925; --gray-50: #141925;
--gray-100: #283044; --gray-100: #283044;
@@ -25,7 +23,6 @@
--link-color: var(--accent-regular); --link-color: var(--accent-regular);
/* Gradients */
--gradient-stop-1: var(--accent-light); --gradient-stop-1: var(--accent-light);
--gradient-stop-2: var(--accent-regular); --gradient-stop-2: var(--accent-regular);
--gradient-stop-3: var(--accent-dark); --gradient-stop-3: var(--accent-dark);
@@ -44,7 +41,6 @@
); );
--gradient-stroke: linear-gradient(180deg, var(--gray-900), var(--gray-700)); --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), --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); 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), --shadow-md: 0px 28px 11px rgba(9, 11, 17, 0.01), 0px 16px 10px rgba(9, 11, 17, 0.03),
@@ -52,7 +48,6 @@
--shadow-lg: 0px 62px 25px rgba(9, 11, 17, 0.01), 0px 35px 21px rgba(9, 11, 17, 0.05), --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); 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-sm: 0.875rem;
--text-base: 1rem; --text-base: 1rem;
--text-md: 1.125rem; --text-md: 1.125rem;
@@ -63,13 +58,11 @@
--text-4xl: 3.5rem; --text-4xl: 3.5rem;
--text-5xl: 4.5rem; --text-5xl: 4.5rem;
/* Fonts */
--font-system: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, --font-system: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--font-body: 'Public Sans', var(--font-system); --font-body: 'Public Sans', var(--font-system);
--font-brand: Rubik, var(--font-system); --font-brand: Rubik, var(--font-system);
/* Transitions */
--theme-transition: 0.2s ease-in-out; --theme-transition: 0.2s ease-in-out;
} }
@@ -176,8 +169,6 @@ h5 {
font-size: var(--text-xl); font-size: var(--text-xl);
} }
/* Utilities */
.sr-only { .sr-only {
position: absolute; position: absolute;
width: 1px; width: 1px;