Build portfolio site - Astro + Tailwind, trilingual structure

- Astro 5.x + Tailwind CSS 4.x
- Dark theme with warm orange accent (#f97316)
- i18n routing: /en/, /de/, /es/ (English content complete, DE/ES placeholders)
- Components: Navbar, Hero, Services (4 cards), Projects (4 case studies), About, Contact, Footer
- Fade-in scroll animations
- Mobile-responsive with hamburger menu
- All content from content/*.md integrated
- SEO meta tags, Open Graph tags
- Clean build with no errors
This commit is contained in:
2026-03-22 12:27:36 +00:00
parent e9403188c4
commit fba9133d51
28 changed files with 6231 additions and 2 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

4
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View File

@@ -1,3 +1,43 @@
# portfolio # Astro Starter Kit: Minimal
Public portfolio site — Astro + Tailwind CSS ```sh
npm create astro@latest -- --template minimal
```
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

18
astro.config.mjs Normal file
View File

@@ -0,0 +1,18 @@
// @ts-check
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
// https://astro.build/config
export default defineConfig({
i18n: {
defaultLocale: 'en',
locales: ['en', 'de', 'es'],
routing: {
prefixDefaultLocale: true
}
},
vite: {
plugins: [tailwindcss()]
}
});

5288
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "tender-shell",
"type": "module",
"version": "0.0.1",
"engines": {
"node": ">=22.12.0"
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@tailwindcss/vite": "^4.2.2",
"astro": "^6.0.8",
"tailwindcss": "^4.2.2"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

4
public/favicon.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" fill="#f97316"/>
<text x="50" y="70" font-size="60" text-anchor="middle" fill="white" font-family="sans-serif" font-weight="bold">P</text>
</svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@@ -0,0 +1,82 @@
---
import type { Locale } from '../i18n/utils';
import { getTranslations } from '../i18n/utils';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const t = getTranslations(locale);
const skills = [
t.about.automation,
t.about.ai,
t.about.documents,
t.about.web,
t.about.infra,
t.about.integrations
];
const languages = [
t.about.english,
t.about.german,
t.about.spanish
];
---
<section id="about" class="py-20 px-4 sm:px-6 lg:px-8">
<div class="max-w-5xl mx-auto">
<h2 class="text-4xl sm:text-5xl font-bold text-center mb-16">
{t.about.title}
</h2>
<div class="fade-in-scroll bg-[#111] border border-[#222] rounded-xl p-8 mb-12">
<p class="text-xl text-[#a1a1a1] leading-relaxed mb-6">
{t.about.bio}
</p>
<div class="space-y-4 text-[#a1a1a1] leading-relaxed">
{t.about.bioLong.split('\n\n').map((paragraph: string) => (
<p>{paragraph}</p>
))}
</div>
</div>
<div class="fade-in-scroll mb-12">
<h3 class="text-2xl font-bold mb-6">{t.about.skills}</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{skills.map((skill: string) => (
<div class="bg-[#111] border border-[#222] rounded-lg p-4 hover:border-[#f97316] transition-colors">
<p class="text-[#a1a1a1]">{skill}</p>
</div>
))}
</div>
</div>
<div class="fade-in-scroll">
<h3 class="text-2xl font-bold mb-6">{t.about.languages}</h3>
<div class="flex flex-wrap gap-3">
{languages.map((lang: string) => (
<span class="px-4 py-2 bg-[#111] border border-[#222] rounded-lg text-[#a1a1a1] font-medium">
{lang}
</span>
))}
</div>
</div>
</div>
</section>
<script>
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.fade-in-scroll').forEach((el) => {
observer.observe(el);
});
</script>

View File

@@ -0,0 +1,57 @@
---
import type { Locale } from '../i18n/utils';
import { getTranslations } from '../i18n/utils';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const t = getTranslations(locale);
---
<section id="contact" class="py-20 px-4 sm:px-6 lg:px-8 bg-[#0d0d0d]">
<div class="max-w-3xl mx-auto text-center">
<h2 class="text-4xl sm:text-5xl font-bold mb-6">
{t.contact.title}
</h2>
<p class="text-xl text-[#a1a1a1] mb-12">
{t.contact.description}
</p>
<div class="flex flex-col sm:flex-row items-center justify-center gap-6">
<a
href="mailto:contact@example.com"
class="inline-flex items-center gap-3 bg-[#f97316] hover:bg-[#ea580c] text-white px-8 py-4 rounded-lg text-lg font-medium transition-all hover:shadow-lg hover:shadow-[#f97316]/20"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
{t.contact.email}
</a>
<!-- Placeholder for LinkedIn/GitHub -->
<div class="flex gap-4">
<a
href="#"
class="w-12 h-12 flex items-center justify-center bg-[#111] border border-[#222] rounded-lg hover:border-[#f97316] transition-colors"
aria-label="LinkedIn"
>
<svg class="w-6 h-6 text-[#a1a1a1]" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</a>
<a
href="#"
class="w-12 h-12 flex items-center justify-center bg-[#111] border border-[#222] rounded-lg hover:border-[#f97316] transition-colors"
aria-label="GitHub"
>
<svg class="w-6 h-6 text-[#a1a1a1]" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,36 @@
---
import type { Locale } from '../i18n/utils';
import { getTranslations } from '../i18n/utils';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const t = getTranslations(locale);
---
<footer class="py-8 px-4 sm:px-6 lg:px-8 border-t border-[#222]">
<div class="max-w-7xl mx-auto">
<div class="flex flex-col sm:flex-row justify-between items-center gap-4">
<p class="text-[#a1a1a1] text-sm">
{t.footer.copyright}
</p>
<div class="flex gap-6">
<a href={`/${locale}/#services`} class="text-[#a1a1a1] hover:text-white text-sm transition-colors">
{t.nav.services}
</a>
<a href={`/${locale}/#projects`} class="text-[#a1a1a1] hover:text-white text-sm transition-colors">
{t.nav.projects}
</a>
<a href={`/${locale}/#about`} class="text-[#a1a1a1] hover:text-white text-sm transition-colors">
{t.nav.about}
</a>
<a href={`/${locale}/#contact`} class="text-[#a1a1a1] hover:text-white text-sm transition-colors">
{t.nav.contact}
</a>
</div>
</div>
</div>
</footer>

28
src/components/Hero.astro Normal file
View File

@@ -0,0 +1,28 @@
---
import type { Locale } from '../i18n/utils';
import { getTranslations } from '../i18n/utils';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const t = getTranslations(locale);
---
<section class="pt-32 pb-20 px-4 sm:px-6 lg:px-8">
<div class="max-w-4xl mx-auto text-center">
<h1 class="text-5xl sm:text-6xl lg:text-7xl font-bold mb-6 bg-gradient-to-r from-white to-[#a1a1a1] bg-clip-text text-transparent">
{t.hero.title}
</h1>
<p class="text-xl sm:text-2xl text-[#a1a1a1] mb-8 max-w-2xl mx-auto">
{t.hero.subtitle}
</p>
<a
href={`/${locale}/#services`}
class="inline-block bg-[#f97316] hover:bg-[#ea580c] text-white px-8 py-4 rounded-lg text-lg font-medium transition-all hover:shadow-lg hover:shadow-[#f97316]/20"
>
{t.hero.cta}
</a>
</div>
</section>

View File

@@ -0,0 +1,29 @@
---
import type { Locale } from '../i18n/utils';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const locales: { code: Locale; label: string }[] = [
{ code: 'en', label: 'EN' },
{ code: 'de', label: 'DE' },
{ code: 'es', label: 'ES' }
];
---
<div class="flex items-center space-x-2">
{locales.map(({ code, label }) => (
<a
href={`/${code}/`}
class={`px-2 py-1 rounded text-sm font-medium transition-colors ${
code === locale
? 'text-[#f97316] font-semibold'
: 'text-[#a1a1a1] hover:text-white'
}`}
>
{label}
</a>
))}
</div>

View File

@@ -0,0 +1,85 @@
---
import LanguageSwitcher from './LanguageSwitcher.astro';
import type { Locale } from '../i18n/utils';
import { getTranslations } from '../i18n/utils';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const t = getTranslations(locale);
---
<nav class="fixed top-0 left-0 right-0 z-50 bg-[#0a0a0a]/90 backdrop-blur-md border-b border-[#222]">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<!-- Logo/Name -->
<div class="flex-shrink-0">
<a href={`/${locale}/`} class="text-xl font-bold text-white hover:text-[#f97316] transition-colors">
Portfolio
</a>
</div>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8">
<a href={`/${locale}/#services`} class="text-[#a1a1a1] hover:text-white transition-colors">
{t.nav.services}
</a>
<a href={`/${locale}/#projects`} class="text-[#a1a1a1] hover:text-white transition-colors">
{t.nav.projects}
</a>
<a href={`/${locale}/#about`} class="text-[#a1a1a1] hover:text-white transition-colors">
{t.nav.about}
</a>
<a href={`/${locale}/#contact`} class="text-[#a1a1a1] hover:text-white transition-colors">
{t.nav.contact}
</a>
<LanguageSwitcher locale={locale} />
<a href={`/${locale}/#contact`} class="bg-[#f97316] hover:bg-[#ea580c] text-white px-4 py-2 rounded-lg transition-colors font-medium">
{t.nav.cta}
</a>
</div>
<!-- Mobile menu button -->
<div class="md:hidden flex items-center space-x-4">
<LanguageSwitcher locale={locale} />
<button id="mobile-menu-button" class="text-[#a1a1a1] hover:text-white">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile menu -->
<div id="mobile-menu" class="hidden md:hidden bg-[#111] border-t border-[#222]">
<div class="px-4 py-4 space-y-3">
<a href={`/${locale}/#services`} class="block text-[#a1a1a1] hover:text-white transition-colors">
{t.nav.services}
</a>
<a href={`/${locale}/#projects`} class="block text-[#a1a1a1] hover:text-white transition-colors">
{t.nav.projects}
</a>
<a href={`/${locale}/#about`} class="block text-[#a1a1a1] hover:text-white transition-colors">
{t.nav.about}
</a>
<a href={`/${locale}/#contact`} class="block text-[#a1a1a1] hover:text-white transition-colors">
{t.nav.contact}
</a>
<a href={`/${locale}/#contact`} class="block bg-[#f97316] hover:bg-[#ea580c] text-white px-4 py-2 rounded-lg transition-colors font-medium text-center">
{t.nav.cta}
</a>
</div>
</div>
</nav>
<script>
const button = document.getElementById('mobile-menu-button');
const menu = document.getElementById('mobile-menu');
button?.addEventListener('click', () => {
menu?.classList.toggle('hidden');
});
</script>

View File

@@ -0,0 +1,99 @@
---
import type { Locale } from '../i18n/utils';
import { getTranslations } from '../i18n/utils';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const t = getTranslations(locale);
const projects = [
{
title: t.projects.schedule.title,
type: t.projects.schedule.type,
description: t.projects.schedule.description,
highlights: t.projects.schedule.highlights,
stack: ['n8n', 'DeepSeek', 'Google Calendar', 'Gmail']
},
{
title: t.projects.pdf.title,
type: t.projects.pdf.type,
description: t.projects.pdf.description,
highlights: t.projects.pdf.highlights,
stack: ['Node.js', 'Express', 'pdf-lib', 'Docker']
},
{
title: t.projects.docx.title,
type: t.projects.docx.type,
description: t.projects.docx.description,
highlights: t.projects.docx.highlights,
stack: ['Node.js', 'Express', 'docxtemplater', 'Docker']
},
{
title: t.projects.approval.title,
type: t.projects.approval.type,
description: t.projects.approval.description,
highlights: t.projects.approval.highlights,
stack: ['n8n', 'Gmail', 'ntfy']
}
];
---
<section id="projects" class="py-20 px-4 sm:px-6 lg:px-8 bg-[#0d0d0d]">
<div class="max-w-7xl mx-auto">
<h2 class="text-4xl sm:text-5xl font-bold text-center mb-16">
{t.projects.title}
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
{projects.map((project) => (
<div class="fade-in-scroll bg-[#111] border border-[#222] rounded-xl p-8 hover:border-[#f97316] transition-all duration-300">
<div class="mb-4">
<h3 class="text-2xl font-bold mb-2">{project.title}</h3>
<p class="text-sm text-[#f97316] font-medium">{project.type}</p>
</div>
<p class="text-[#a1a1a1] mb-6 leading-relaxed">
{project.description}
</p>
<div class="mb-6">
<h4 class="text-sm font-semibold text-white mb-3">Highlights:</h4>
<ul class="text-sm text-[#a1a1a1] space-y-2">
{project.highlights.split(' • ').map((highlight: string) => (
<li class="flex items-start">
<span class="text-[#f97316] mr-2">•</span>
<span>{highlight}</span>
</li>
))}
</ul>
</div>
<div class="flex flex-wrap gap-2 pt-4 border-t border-[#222]">
{project.stack.map((tech: string) => (
<span class="px-3 py-1 bg-[#1a1a1a] text-[#a1a1a1] text-sm rounded-full border border-[#222] font-mono">
{tech}
</span>
))}
</div>
</div>
))}
</div>
</div>
</section>
<script>
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.fade-in-scroll').forEach((el) => {
observer.observe(el);
});
</script>

View File

@@ -0,0 +1,92 @@
---
import type { Locale } from '../i18n/utils';
import { getTranslations } from '../i18n/utils';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const t = getTranslations(locale);
const services = [
{
title: t.services.workflow.title,
description: t.services.workflow.description,
examples: t.services.workflow.examples,
tools: t.services.workflow.tools,
icon: '⚙️'
},
{
title: t.services.ai.title,
description: t.services.ai.description,
examples: t.services.ai.examples,
tools: t.services.ai.tools,
icon: '🤖'
},
{
title: t.services.documents.title,
description: t.services.documents.description,
examples: t.services.documents.examples,
tools: t.services.documents.tools,
icon: '📄'
},
{
title: t.services.web.title,
description: t.services.web.description,
examples: t.services.web.examples,
tools: t.services.web.tools,
icon: '💻'
}
];
---
<section id="services" class="py-20 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto">
<h2 class="text-4xl sm:text-5xl font-bold text-center mb-16">
{t.services.title}
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
{services.map((service) => (
<div class="fade-in-scroll bg-[#111] border border-[#222] rounded-xl p-8 hover:border-[#f97316] transition-all duration-300">
<div class="text-4xl mb-4">{service.icon}</div>
<h3 class="text-2xl font-bold mb-4">{service.title}</h3>
<p class="text-[#a1a1a1] mb-6 leading-relaxed">
{service.description}
</p>
<div class="mb-6">
<h4 class="text-sm font-semibold text-[#f97316] mb-2">What this looks like:</h4>
<ul class="text-sm text-[#a1a1a1] space-y-2">
{service.examples.split(' • ').map((example: string) => (
<li class="flex items-start">
<span class="text-[#f97316] mr-2">•</span>
<span>{example}</span>
</li>
))}
</ul>
</div>
<div class="pt-4 border-t border-[#222]">
<p class="text-sm text-[#888] font-mono">{service.tools}</p>
</div>
</div>
))}
</div>
</div>
</section>
<script>
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.fade-in-scroll').forEach((el) => {
observer.observe(el);
});
</script>

17
src/i18n/de.json Normal file
View File

@@ -0,0 +1,17 @@
{
"nav": {
"services": "Services",
"projects": "Projects",
"about": "About",
"contact": "Contact",
"cta": "Get in Touch"
},
"hero": {
"title": "Build Automation That Works",
"subtitle": "I turn repetitive tasks into automated pipelines for SMBs and agencies.",
"cta": "See What I Do"
},
"footer": {
"copyright": "© 2026 Automation & AI Tooling. All rights reserved."
}
}

92
src/i18n/en.json Normal file
View File

@@ -0,0 +1,92 @@
{
"nav": {
"services": "Services",
"projects": "Projects",
"about": "About",
"contact": "Contact",
"cta": "Get in Touch"
},
"hero": {
"title": "Build Automation That Works",
"subtitle": "I turn repetitive tasks into automated pipelines for SMBs and agencies.",
"cta": "See What I Do"
},
"services": {
"title": "What I Build",
"workflow": {
"title": "Workflow Automation",
"description": "I build automated pipelines that replace repetitive manual work. From form submissions to multi-step approval flows, I connect your existing tools (Google Workspace, email, chat, notifications) into workflows that run themselves.",
"examples": "Multi-step approval workflows with email and push notifications • Scheduled reports and notifications across multiple channels • Data sync between internal systems • Form-based intake that triggers automated processing",
"tools": "n8n, Node.js, REST APIs, Google Workspace APIs, Webhooks"
},
"ai": {
"title": "AI-Powered Internal Tools",
"description": "I integrate LLMs into practical business tools — not chatbots, but agents that do real work. Natural language interfaces for scheduling, document drafting, and data processing.",
"examples": "AI agent that reads calendars, drafts emails, and plans schedules from plain-text requests • Document classification and routing • Smart data extraction from unstructured inputs",
"tools": "OpenAI, Anthropic, DeepSeek, LangChain, tool-calling agents"
},
"documents": {
"title": "Document Processing",
"description": "I build services that handle PDF and Word document manipulation at scale. Template-based generation, splitting, merging, metadata extraction — exposed as APIs your other systems can call.",
"examples": "DOCX generation from templates (contracts, letters, forms) • PDF splitting and renaming based on content • Batch document processing pipelines",
"tools": "Node.js, pdf-lib, docxtemplater, Express, REST APIs"
},
"web": {
"title": "Custom Web Applications",
"description": "I build focused web apps that solve specific operational problems. Clean UI, fast, deployed and ready to use.",
"examples": "Employee-facing internal tools • Data validation and reporting dashboards • Absence and scheduling systems",
"tools": "Next.js, React, TypeScript, Tailwind CSS"
}
},
"projects": {
"title": "Case Studies",
"schedule": {
"title": "AI Schedule Planner",
"type": "AI-powered workflow automation",
"description": "An AI agent that handles schedule planning through natural language. Managers describe what they need in plain text — the agent checks calendar availability, generates a plan, and drafts emails with proper formatting. Supports multilingual interaction.",
"highlights": "Natural language input → structured schedule assignments • Calendar integration for conflict detection • Auto-generated email drafts ready to send • Multilingual support (German/English)"
},
"pdf": {
"title": "PDF Manager",
"type": "Document processing API",
"description": "A REST API service for PDF manipulation. Upload documents, split them by page ranges, and rename output files based on extracted metadata. Built for integration into larger document processing pipelines.",
"highlights": "Split PDFs by page range or content markers • Automatic file naming from document metadata • Batch processing support • Dockerized for easy deployment"
},
"docx": {
"title": "DocxTemplater Server",
"type": "Document generation API",
"description": "A template-based Word document generation service. Upload a .docx template with placeholder variables, send data via API, get a finished document back. Used for contracts, letters, HR forms — anything with a repeating structure.",
"highlights": "Variable substitution in Word templates • Supports tables, loops, and conditional sections • REST API — integrates with any system • Dockerized for easy deployment"
},
"approval": {
"title": "Approval Workflow Engine",
"type": "Workflow automation",
"description": "A scheduled workflow that handles recurring procurement with a human-in-the-loop approval step. Runs on a configurable schedule, sends an approval request to the relevant team, and on approval automatically emails the supplier. Confirmation sent via push notification.",
"highlights": "Configurable scheduled triggers • Email-based approval workflow • Auto-generated supplier/vendor emails • Push notification confirmations"
}
},
"about": {
"title": "About Me",
"bio": "Automation engineer based in Berlin. I build internal tools, workflow automations, and AI-powered systems that replace manual busywork for small and medium businesses.",
"bioLong": "I specialize in turning repetitive operational tasks into automated pipelines. I've built tools for HR processes, scheduling, document generation, and internal communications — systems that run smoothly without constant manual intervention.\n\nI work across the full stack: from n8n workflows connecting Google Workspace, email, and chat systems, to custom web apps in Next.js, to AI agents that handle scheduling and document drafting through natural language.\n\nIf your team spends hours on tasks that follow predictable patterns, I can probably automate most of it.",
"skills": "What I Work With",
"automation": "Automation: n8n, custom Node.js pipelines, REST APIs, webhooks",
"ai": "AI/LLM: OpenAI, Anthropic, DeepSeek — tool-calling agents, not just chatbots",
"documents": "Document Processing: PDF manipulation, Word template generation",
"web": "Web Apps: Next.js, React, TypeScript, Tailwind CSS",
"infra": "Infrastructure: Docker, Linux, self-hosted deployments",
"integrations": "Integrations: Google Workspace, Telegram, email, ntfy, any system with an API",
"languages": "Languages",
"english": "English (fluent)",
"german": "German (professional)",
"spanish": "Spanish (native)"
},
"contact": {
"title": "Get in Touch",
"description": "Interested in working together? Send me an email.",
"email": "Email"
},
"footer": {
"copyright": "© 2026 Automation & AI Tooling. All rights reserved."
}
}

17
src/i18n/es.json Normal file
View File

@@ -0,0 +1,17 @@
{
"nav": {
"services": "Services",
"projects": "Projects",
"about": "About",
"contact": "Contact",
"cta": "Get in Touch"
},
"hero": {
"title": "Build Automation That Works",
"subtitle": "I turn repetitive tasks into automated pipelines for SMBs and agencies.",
"cta": "See What I Do"
},
"footer": {
"copyright": "© 2026 Automation & AI Tooling. All rights reserved."
}
}

15
src/i18n/utils.ts Normal file
View File

@@ -0,0 +1,15 @@
import en from './en.json';
import de from './de.json';
import es from './es.json';
const translations = {
en,
de,
es
} as const;
export type Locale = keyof typeof translations;
export function getTranslations(locale: Locale) {
return translations[locale] || translations.en;
}

33
src/layouts/Layout.astro Normal file
View File

@@ -0,0 +1,33 @@
---
interface Props {
title: string;
description: string;
locale: string;
}
const { title, description, locale } = Astro.props;
import '../styles/global.css';
---
<!doctype html>
<html lang={locale}>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<meta name="generator" content={Astro.generator} />
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<!-- Favicon placeholder -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>

23
src/pages/de/index.astro Normal file
View File

@@ -0,0 +1,23 @@
---
import Layout from '../../layouts/Layout.astro';
import Navbar from '../../components/Navbar.astro';
import Hero from '../../components/Hero.astro';
import Footer from '../../components/Footer.astro';
const locale = 'de' as const;
---
<Layout
title="Automation & AI Tooling | Portfolio"
description="I build workflow automations, AI-powered tools, and custom web applications for SMBs and agencies."
locale={locale}
>
<Navbar locale={locale} />
<main>
<Hero locale={locale} />
<div class="py-20 px-4 text-center">
<p class="text-xl text-[#a1a1a1]">German translation coming soon...</p>
</div>
</main>
<Footer locale={locale} />
</Layout>

28
src/pages/en/index.astro Normal file
View File

@@ -0,0 +1,28 @@
---
import Layout from '../../layouts/Layout.astro';
import Navbar from '../../components/Navbar.astro';
import Hero from '../../components/Hero.astro';
import Services from '../../components/Services.astro';
import Projects from '../../components/Projects.astro';
import About from '../../components/About.astro';
import Contact from '../../components/Contact.astro';
import Footer from '../../components/Footer.astro';
const locale = 'en' as const;
---
<Layout
title="Automation & AI Tooling | Portfolio"
description="I build workflow automations, AI-powered tools, and custom web applications for SMBs and agencies. Turn repetitive tasks into automated pipelines."
locale={locale}
>
<Navbar locale={locale} />
<main>
<Hero locale={locale} />
<Services locale={locale} />
<Projects locale={locale} />
<About locale={locale} />
<Contact locale={locale} />
</main>
<Footer locale={locale} />
</Layout>

23
src/pages/es/index.astro Normal file
View File

@@ -0,0 +1,23 @@
---
import Layout from '../../layouts/Layout.astro';
import Navbar from '../../components/Navbar.astro';
import Hero from '../../components/Hero.astro';
import Footer from '../../components/Footer.astro';
const locale = 'es' as const;
---
<Layout
title="Automation & AI Tooling | Portfolio"
description="I build workflow automations, AI-powered tools, and custom web applications for SMBs and agencies."
locale={locale}
>
<Navbar locale={locale} />
<main>
<Hero locale={locale} />
<div class="py-20 px-4 text-center">
<p class="text-xl text-[#a1a1a1]">Spanish translation coming soon...</p>
</div>
</main>
<Footer locale={locale} />
</Layout>

4
src/pages/index.astro Normal file
View File

@@ -0,0 +1,4 @@
---
// Redirect to /en/
return Astro.redirect('/en/', 301);
---

56
src/styles/global.css Normal file
View File

@@ -0,0 +1,56 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
@import "tailwindcss";
:root {
--bg-primary: #0a0a0a;
--bg-card: #111111;
--text-primary: #fafafa;
--text-muted: #a1a1a1;
--border: #222222;
--accent: #f97316; /* warm orange */
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', system-ui, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
}
html {
scroll-behavior: smooth;
}
/* Fade-in animation */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeInUp 0.3s ease-out;
}
/* Scroll-triggered fade-in (handled via Intersection Observer in components) */
.fade-in-scroll {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.3s ease-out, transform 0.3s ease-out;
}
.fade-in-scroll.visible {
opacity: 1;
transform: translateY(0);
}

5
tsconfig.json Normal file
View File

@@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}