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:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
4
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
||||||
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
44
README.md
44
README.md
@@ -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
18
astro.config.mjs
Normal 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
5288
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 655 B |
4
public/favicon.svg
Normal file
4
public/favicon.svg
Normal 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 |
82
src/components/About.astro
Normal file
82
src/components/About.astro
Normal 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>
|
||||||
57
src/components/Contact.astro
Normal file
57
src/components/Contact.astro
Normal 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>
|
||||||
36
src/components/Footer.astro
Normal file
36
src/components/Footer.astro
Normal 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
28
src/components/Hero.astro
Normal 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>
|
||||||
29
src/components/LanguageSwitcher.astro
Normal file
29
src/components/LanguageSwitcher.astro
Normal 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>
|
||||||
85
src/components/Navbar.astro
Normal file
85
src/components/Navbar.astro
Normal 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>
|
||||||
99
src/components/Projects.astro
Normal file
99
src/components/Projects.astro
Normal 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>
|
||||||
92
src/components/Services.astro
Normal file
92
src/components/Services.astro
Normal 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
17
src/i18n/de.json
Normal 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
92
src/i18n/en.json
Normal 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
17
src/i18n/es.json
Normal 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
15
src/i18n/utils.ts
Normal 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
33
src/layouts/Layout.astro
Normal 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
23
src/pages/de/index.astro
Normal 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
28
src/pages/en/index.astro
Normal 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
23
src/pages/es/index.astro
Normal 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
4
src/pages/index.astro
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
// Redirect to /en/
|
||||||
|
return Astro.redirect('/en/', 301);
|
||||||
|
---
|
||||||
56
src/styles/global.css
Normal file
56
src/styles/global.css
Normal 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
5
tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
|
"exclude": ["dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user