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 modified bun.lockb
Binary file not shown.
14 changes: 14 additions & 0 deletions config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Site Configuration - Control visibility of sections
sections:
about: true
workExperience: true
talks: true
writing: true
socialLinks: true

# Individual elements
elements:
avatar: true
themeSwitch: true
header: true
footer: true
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
"@astrojs/sitemap": "3.6.0",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.16",
"@types/js-yaml": "^4.0.9",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"astro": "5.15.2",
"framer-motion": "^12.23.24",
"js-yaml": "^4.1.0",
"lucide-react": "^0.548.0",
"mdast-util-to-string": "^4.0.0",
"react": "^18.3.1",
Expand Down
15 changes: 10 additions & 5 deletions src/components/partials/Footer.astro
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
---
import Container from '@/components/Container.astro';
import { getSiteConfig } from '@/lib/config';

const config = getSiteConfig();
---

<Container as='footer' class='pt-24'>
<p class="text-center text-muted-foreground text-sm">
&copy; {new Date().getFullYear()}. Powered by <a href="https://astro.build" target="_blank" rel="noopener noreferrer">Astro</a> and CVfolio.
</p>
</Container>
{config.elements.footer && (
<Container as='footer' class='pt-24'>
<p class="text-center text-muted-foreground text-sm">
&copy; {new Date().getFullYear()}. Powered by <a href="https://astro.build" target="_blank" rel="noopener noreferrer">Astro</a> and CVfolio.
</p>
</Container>
)}
74 changes: 40 additions & 34 deletions src/components/partials/Header.astro
Original file line number Diff line number Diff line change
@@ -1,43 +1,49 @@
---
import Container from '@/components/Container.astro';
import { getSiteConfig } from '@/lib/config';

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-3xl border border-border rounded-full p-1"
const config = getSiteConfig();
---
{config.elements.header && (
<Container
as="header"
class="w-full max-w-full flex justify-center items-center"
>
<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
>
</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
>
</li>
</ul>
</nav>
</div>
</Container>
<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
>
</li>
{config.sections.writing && (
<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
>
</li>
)}
</ul>
</nav>
</div>
</Container>
)}
6 changes: 5 additions & 1 deletion src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import Header from '@/components/partials/Header.astro';
import Footer from '@/components/partials/Footer.astro';
import Head from '@/components/partials/Head.astro';
import SwitchTheme from '@/components/SwitchTheme.tsx';
import { getSiteConfig } from '@/lib/config';

interface Props {
seo?: Seo
}

const { seo } = Astro.props;
const config = getSiteConfig();
---

<html lang="en">
Expand All @@ -22,6 +24,8 @@ const { seo } = Astro.props;
<slot />
</main>
<Footer />
<SwitchTheme client:only="react" />
{config.elements.themeSwitch && (
<SwitchTheme client:only="react" />
)}
</body>
</html>
101 changes: 101 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import fs from 'fs';
import path from 'path';
import yaml from 'js-yaml';
import { z } from 'astro:content';

const SiteConfigSchema = z.object({
sections: z.object({
about: z.boolean(),
workExperience: z.boolean(),
talks: z.boolean(),
writing: z.boolean(),
socialLinks: z.boolean(),
}),
elements: z.object({
avatar: z.boolean(),
themeSwitch: z.boolean(),
header: z.boolean(),
footer: z.boolean(),
}),
});

type SiteConfig = z.infer<typeof SiteConfigSchema>;

const defaultConfig: SiteConfig = {
sections: {
about: true,
workExperience: true,
talks: true,
writing: true,
socialLinks: true,
},
elements: {
avatar: true,
themeSwitch: true,
footer: true,
header: true,
},
};

function deepMerge(target: any, source: any): any {
// Recursively merge source into target
for (const key of Object.keys(source)) {
if (
source[key] &&
typeof source[key] === 'object' &&
!Array.isArray(source[key])
) {
if (!target[key] || typeof target[key] !== 'object') {
target[key] = {};
}
deepMerge(target[key], source[key]);
} else {
if (target[key] === undefined) {
target[key] = source[key];
}
}
}
return target;
}

function isValidConfig(config: SiteConfig): config is SiteConfig {
return SiteConfigSchema.safeParse(config).success;
}

export function getSiteConfig(): SiteConfig {
const configPath = path.join(process.cwd(), 'config.yml');
let loadedConfig: any = {};
try {
const fileContents = fs.readFileSync(configPath, 'utf8');
loadedConfig = yaml.load(fileContents);
if (!loadedConfig || typeof loadedConfig !== 'object') {
console.warn(
'config.yml is empty or not a valid YAML object, using defaults',
);
return defaultConfig;
}
// Merge loaded config with defaults
const mergedConfig = deepMerge(loadedConfig, defaultConfig);
if (!isValidConfig(mergedConfig)) {
console.warn(
'config.yml is malformed or missing required fields, using defaults',
);
return defaultConfig;
}
return mergedConfig as SiteConfig;
} catch (error: any) {
if (error.code === 'ENOENT') {
console.warn('config.yml not found, using defaults');
} else if (
error.name === 'YAMLException' ||
error instanceof yaml.YAMLException
) {
console.warn('config.yml is malformed YAML, using defaults');
} else {
console.warn(
`Error loading config.yml: ${error.message}, using defaults`,
);
}
return defaultConfig;
}
}
37 changes: 22 additions & 15 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DEFAULT_CONFIGURATION } from '@/lib/constants';
import WorkExperience from '@/components/ui/WorkExperience.astro';
import Talk from '@/components/ui/Talk.astro';
import { sortByDateRange, sortByYear } from '@/lib/utils';
import { getSiteConfig } from '@/lib/config';

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

Expand All @@ -23,25 +24,31 @@ const sortedJobs = sortByDateRange(jobs);

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

const config = getSiteConfig();
---

<BaseLayout seo={entry.data.seo}>
<Container as="section" class="py-6">
<Author {...DEFAULT_CONFIGURATION.author} />
</Container>
{config.elements.avatar && (
<Container as="section" class="py-6">
<Author {...DEFAULT_CONFIGURATION.author} />
</Container>
)}

<Container as="section" class="py-6">
<div class="flex flex-col gap-6">
<div class="flex items-center">
<span class="text-headings">About</span>
</div>
<div class="prose dark:prose-invert">
<Content />
{config.sections.about && (
<Container as="section" class="py-6">
<div class="flex flex-col gap-6">
<div class="flex items-center">
<span class="text-headings">About</span>
</div>
<div class="prose dark:prose-invert">
<Content />
</div>
</div>
</div>
</Container>
</Container>
)}
{
links.length > 0 && (
config.sections.socialLinks && links.length > 0 && (
<Container as="section" class="py-8">
<div class="flex flex-col gap-5">
<span class="text-headings">Contact</span>
Expand Down Expand Up @@ -69,7 +76,7 @@ const sortedTalks = sortByYear(talks);
)
}
{
sortedJobs.length > 0 && (
config.sections.workExperience && sortedJobs.length > 0 && (
<Container as="section" class="py-6">
<div class="flex flex-col gap-5">
<span class="text-headings">Work Experience</span>
Expand All @@ -83,7 +90,7 @@ const sortedTalks = sortByYear(talks);
)
}
{
talks.length > 0 && (
config.sections.talks && sortedTalks.length > 0 && (
<Container as="section" class="py-6">
<div class="flex flex-col gap-5">
<span class="text-headings">Speaking</span>
Expand Down