Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed src/assets/images/avatar.jpeg
Binary file not shown.
Binary file added src/assets/images/avatar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 24 additions & 13 deletions src/components/partials/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,37 @@ const pathname = Astro.url.pathname;

const isHomePage = pathname === '/';
const isWritingPage = pathname.startsWith('/writing');

---

<Container as='header' class="w-full max-w-full flex justify-center items-center">
<div class="w-max fixed top-0 mt-5 bg-muted-foreground/40 backdrop-blur-md border border-border rounded-full p-1">
<Container
as="header"
class="w-full max-w-full flex justify-center items-center"
>
<div
class="w-max fixed top-0 mt-5 bg-muted-foreground/40 backdrop-blur-3xl border border-border rounded-full p-1"
>
<nav class="flex items-center">
<ul class="flex items-center gap-1">
<li>
<a href="/" class:list={[
'font-medium transition-colors block px-5 py-2',
'hover:text-headings',
isHomePage && 'text-headings bg-muted-foreground/40 rounded-full',
]}>Home</a>
<a
href="/"
class:list={[
'font-medium transition-colors block px-5 py-2',
'hover:text-headings',
isHomePage && 'text-headings bg-muted-foreground/40 rounded-full',
]}>Home</a
>
</li>
<li>
<a href="/writing" class:list={[
'font-medium transition-colors block px-5 py-2',
'hover:text-headings',
isWritingPage && 'text-headings bg-muted-foreground/40 rounded-full',
]}>Writing</a>
<a
href="/writing"
class:list={[
'font-medium transition-colors block px-5 py-2',
'hover:text-headings',
isWritingPage &&
'text-headings bg-muted-foreground/40 rounded-full',
]}>Writing</a
>
</li>
</ul>
</nav>
Expand Down
60 changes: 32 additions & 28 deletions src/components/ui/WorkExperience.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface Props {
const { entry } = Astro.props;
const { Content } = await render(entry);

const images = entry.data.images
const images = entry.data.images
? entry.data.images.map((img) => ({
src: img.src,
alt: entry.data.title,
Expand All @@ -19,7 +19,9 @@ const images = entry.data.images
<li class="py-0.5">
<div class="flex gap-5">
<div class="relative min-w-28 shrink-0">
<span class="text-muted-foreground text-sm">{entry.data.from} - {entry.data.to}</span>
<span class="text-muted-foreground text-sm"
>{entry.data.from} - {entry.data.to ?? 'Present'}</span
>
</div>
<div class="flex flex-col gap-3">
<div class="flex flex-col">
Expand All @@ -29,24 +31,26 @@ const images = entry.data.images
<div class="prose dark:prose-invert prose-sm">
<Content />
</div>

{images.length > 0 && (
<div class="flex gap-2 flex-wrap mt-2">
{images.map((image, idx) => (
<button
class="work-image-thumb overflow-hidden rounded-lg border border-border hover:opacity-80 transition-opacity"
data-images={JSON.stringify(images)}
data-index={idx}
>
<img
src={image.src}
alt={image.alt}
class="w-20 h-20 object-cover"
/>
</button>
))}
</div>
)}

{
images.length > 0 && (
<div class="flex gap-2 flex-wrap mt-2">
{images.map((image, idx) => (
<button
class="work-image-thumb overflow-hidden rounded-lg border border-border hover:opacity-80 transition-opacity"
data-images={JSON.stringify(images)}
data-index={idx}
>
<img
src={image.src}
alt={image.alt}
class="w-20 h-20 object-cover"
/>
</button>
))}
</div>
)
}
</div>
</div>
</li>
Expand All @@ -57,19 +61,19 @@ const images = entry.data.images
import { createElement } from 'react';

const container = document.querySelector('.flex.gap-2.flex-wrap.mt-2');

if (container) {
const clickHandler = (event: Event) => {
const thumb = (event.target as HTMLElement).closest('.work-image-thumb');
if (!thumb || !container.contains(thumb)) return;

const images = JSON.parse(thumb.getAttribute('data-images') || '[]');
const index = parseInt(thumb.getAttribute('data-index') || '0');

const lightboxContainer = document.createElement('div');
document.body.appendChild(lightboxContainer);
const root = createRoot(lightboxContainer);

const closeHandler = () => {
root.unmount();
try {
Expand All @@ -78,17 +82,17 @@ const images = entry.data.images
// Container may have already been removed; ignore error
}
};

root.render(
createElement(ImageLightbox, {
images,
isOpen: true,
initialIndex: index,
onClose: closeHandler,
})
}),
);
};

container.addEventListener('click', clickHandler);
}
</script>
</script>
2 changes: 1 addition & 1 deletion src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const jobCollection = defineCollection({
company: z.string(),
location: z.string(),
from: z.number(),
to: z.number().or(z.enum(['Now'])),
to: z.number().optional(), // optional if currently working
url: z.string(),
images: z.array(image()).optional(),
}),
Expand Down
7 changes: 7 additions & 0 deletions src/content/jobs/coderdiaz-studio.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: Engineering & Design
company: Coderdiaz Studio
location: Mexico City
from: 2022
url: https://coderdiaz.com
---
2 changes: 1 addition & 1 deletion src/content/jobs/reboot-studio/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Product Engineer
company: Reboot Studio
location: Mexico City
from: 2022
to: 2022 # Use 'Now' if the job is still active
to: 2022
url: https://reboot.studio
images:
- ./image-1.jpg
Expand Down
2 changes: 1 addition & 1 deletion src/content/jobs/sofia/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Product Engineer
company: Sofia
location: Mexico City
from: 2025
to: Now # Use 'Now' if the job is still active
to: 2025
url: https://sofiasalud.com
---

Expand Down
19 changes: 10 additions & 9 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { z } from 'astro/zod';
import MetaDefaultImage from '@/assets/images/meta-default.jpg';
import avatar from '@/assets/images/avatar.jpeg';
import avatar from '@/assets/images/avatar.png';
import type { seoSchemaWithoutImage } from '@/content.config';
import astroConfig from 'astro.config.mjs';

Expand All @@ -11,17 +11,17 @@ export type AuthorInfo = {
username?: string;
location?: string;
pronouns?: string;
}
};

export type Seo = z.infer<typeof seoSchemaWithoutImage> & {
image?: any;
}
};

type DefaultConfigurationType = {
baseUrl: string,
baseUrl: string;
author: AuthorInfo;
seo: Seo;
}
};

export const DEFAULT_CONFIGURATION: DefaultConfigurationType = {
baseUrl: astroConfig.site || 'https://getcvfolio.com',
Expand All @@ -35,12 +35,13 @@ export const DEFAULT_CONFIGURATION: DefaultConfigurationType = {
},
seo: {
title: 'CV Folio — An Astro template inspired on Read.cv',
description: 'Clean and aesthetic portfolio website for developers and designers',
description:
'Clean and aesthetic portfolio website for developers and designers',
type: 'website',
image: MetaDefaultImage,
twitter: {
creator: '@cvfolio'
creator: '@cvfolio',
},
robots: 'noindex, nofollow',
}
};
},
};
44 changes: 33 additions & 11 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { DEFAULT_CONFIGURATION } from './constants';
import type { CollectionEntry } from 'astro:content';

export const formatDate = (date: Date) => {
const formatter = new Intl.DateTimeFormat('en-US', {
Expand All @@ -23,18 +22,41 @@ export const includeDraft = (draft: boolean) => {
return draft !== true;
};

export const sortJobsByDate = (jobs: CollectionEntry<'jobs'>[]) => {
// Convert "Now" to current year, otherwise returns the year as is
const getEndYear = (job: CollectionEntry<'jobs'>) =>
job.data.to === 'Now' ? new Date().getFullYear() : job.data.to;

return jobs.sort((current, next) => {
// Compare end years first, then fall back to start years if end years are equal
const [currentEnd, nextEnd] = [getEndYear(current), getEndYear(next)];
/*
* Generic function for sorting items with start and optional end years
* - Items without an end year are considered ongoing and are sorted accordingly
* - Items are sorted by end year descending, then by start year descending
* - If end year is not present, current year is used for comparison
*/
export const sortByDateRange = <
T extends { data: { from: number; to?: number } },
>(
items: T[],
) => {
const getCurrentYear = () => new Date().getFullYear();

return items.sort((current, next) => {
// Prioritize ongoing jobs (no 'to' field) first
const currentIsOngoing = current.data.to === undefined;
const nextIsOngoing = next.data.to === undefined;

// If one is ongoing and the other isn't, ongoing comes first
if (currentIsOngoing && !nextIsOngoing) return -1;
if (!currentIsOngoing && nextIsOngoing) return 1;

// If both are ongoing or both have end dates, sort by end year then start year
const currentEnd = current.data.to ?? getCurrentYear();
const nextEnd = next.data.to ?? getCurrentYear();
return nextEnd - currentEnd || next.data.from - current.data.from;
});
};

export const sortTalksByYear = (talks: CollectionEntry<'talks'>[]) => {
return talks.sort((a, b) => b.data.year - a.data.year);
/*
* Generic function for sorting items by year in descending order
* - Items are sorted by year descending
*/
export const sortByYear = <T extends { data: { year: number } }>(
items: T[],
) => {
return items.sort((a, b) => b.data.year - a.data.year);
};
8 changes: 5 additions & 3 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Author from '@/components/ui/Author.astro';
import { DEFAULT_CONFIGURATION } from '@/lib/constants';
import WorkExperience from '@/components/ui/WorkExperience.astro';
import Talk from '@/components/ui/Talk.astro';
import { sortJobsByDate, sortTalksByYear } from '@/lib/utils';
import { sortByDateRange, sortByYear } from '@/lib/utils';

const entry = await getEntry('pages', 'homepage');

Expand All @@ -17,10 +17,12 @@ if (!entry) {
const { Content } = await render(entry);

const links = await getCollection('links');

const jobs = await getCollection('jobs');
const sortedJobs = sortJobsByDate(jobs);
const sortedJobs = sortByDateRange(jobs);

const talks = await getCollection('talks');
const sortedTalks = sortTalksByYear(talks);
const sortedTalks = sortByYear(talks);
---

<BaseLayout seo={entry.data.seo}>
Expand Down