Inital commit of Astro Website
This commit is contained in:
41
src/pages/blog/[...slug].astro
Normal file
41
src/pages/blog/[...slug].astro
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import "katex/dist/katex.min.css";
|
||||
import "rehype-callouts/theme/obsidian"
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const blogEntries = await getCollection('blog');
|
||||
return blogEntries.map(entry => ({
|
||||
params: { slug: entry.id }, props: { entry },
|
||||
}));
|
||||
}
|
||||
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await render(entry);
|
||||
---
|
||||
<BaseLayout title={entry.data.title} theme="blog">
|
||||
<div class="glass-container header">
|
||||
<h1>{entry.data.title}</h1>
|
||||
<div class="meta">
|
||||
<time>{entry.data.pubDate.toLocaleDateString()}</time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-container content prose">
|
||||
<Content />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.meta {
|
||||
opacity: 0.7;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.content {
|
||||
line-height: 1.8;
|
||||
}
|
||||
</style>
|
||||
47
src/pages/blog/index.astro
Normal file
47
src/pages/blog/index.astro
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
|
||||
const posts = await getCollection('blog');
|
||||
posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
||||
---
|
||||
<BaseLayout title="Blog" theme="blog" description="Gedanken über Technologie, Design und das Leben von c0ntroller.de.">
|
||||
<div class="glass-container header">
|
||||
<h1>Artikel & Gedanken</h1>
|
||||
<p>Gedanken über Technologie, Design und das Leben.</p>
|
||||
</div>
|
||||
|
||||
<div class="list">
|
||||
{posts.map(post => (
|
||||
<a href={`/blog/${post.id}`} class="glass-container link-card">
|
||||
<h2>{post.data.title}</h2>
|
||||
<p class="date">{post.data.pubDate.toLocaleDateString()}</p>
|
||||
<p>{post.data.summary}</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<style>
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
.link-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.link-card h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.date {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.7;
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
112
src/pages/book/[...slug].astro
Normal file
112
src/pages/book/[...slug].astro
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import BookNavigation from '../../components/BookNavigation.astro';
|
||||
import "katex/dist/katex.min.css";
|
||||
import "rehype-callouts/theme/obsidian";
|
||||
import "../../styles/md-custom.css";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const pages = await getCollection('book');
|
||||
pages.sort((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
return pages.map((page, index) => {
|
||||
return {
|
||||
params: { slug: page.id },
|
||||
props: {
|
||||
page,
|
||||
prevPage: index > 0 ? pages[index - 1] : null,
|
||||
nextPage: index < pages.length - 1 ? pages[index + 1] : null,
|
||||
allPages: pages
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const { page, prevPage, nextPage, allPages } = Astro.props;
|
||||
const { Content } = await render(page);
|
||||
|
||||
const chapters = allPages.reduce((acc, p) => {
|
||||
const [chapter] = p.id.split('/');
|
||||
if (!acc[chapter]) acc[chapter] = [];
|
||||
acc[chapter].push(p);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof allPages>);
|
||||
---
|
||||
<BaseLayout title={page.data.title} theme="book">
|
||||
<div class="glass-container content prose">
|
||||
<h1>{page.data.title}</h1>
|
||||
<Content />
|
||||
</div>
|
||||
|
||||
<BookNavigation
|
||||
prevUrl={prevPage ? `/book/${prevPage.id}` : undefined}
|
||||
prevTitle={prevPage?.data.title}
|
||||
nextUrl={nextPage ? `/book/${nextPage.id}` : undefined}
|
||||
nextTitle={nextPage?.data.title}
|
||||
>
|
||||
<div slot="toc" class="mini-toc">
|
||||
{Object.entries(chapters).map(([chapter, items]) => (
|
||||
<div class="mini-chapter">
|
||||
<h4>{chapter.replace(/-/g, ' ').toUpperCase()}</h4>
|
||||
<ul>
|
||||
{items.map(item => (
|
||||
<li>
|
||||
<a
|
||||
href={`/book/${item.id}`}
|
||||
class={item.id === page.id ? 'active' : ''}
|
||||
>
|
||||
{item.data.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</BookNavigation>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
line-height: 1.8;
|
||||
padding: 2rem 3rem;
|
||||
}
|
||||
.mini-toc {
|
||||
text-align: left;
|
||||
}
|
||||
.mini-chapter {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.mini-chapter h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.7;
|
||||
color: var(--text-main);
|
||||
}
|
||||
.mini-chapter ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.mini-chapter li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.mini-chapter a {
|
||||
display: block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
.mini-chapter a:hover {
|
||||
background: rgba(var(--accent-base), 0.1);
|
||||
color: rgb(var(--accent-base));
|
||||
}
|
||||
.mini-chapter a.active {
|
||||
background: rgba(var(--accent-base), 0.2);
|
||||
color: rgb(var(--accent-base));
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
44
src/pages/book/index.astro
Normal file
44
src/pages/book/index.astro
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
|
||||
const pages = await getCollection('book');
|
||||
pages.sort((a, b) => a.data.part - b.data.part);
|
||||
|
||||
const chapters = pages.reduce((acc, page) => {
|
||||
const chapter = page.data.chapter["id"];
|
||||
if (!acc[chapter]) acc[chapter] = [];
|
||||
acc[chapter].push(page);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof pages>);
|
||||
---
|
||||
<BaseLayout title="Das Buch" theme="book" description="Ein fortlaufendes Werk über Software, Design und Architektur von c0ntroller.de.">
|
||||
<div class="glass-container header">
|
||||
<h1>Das Buch</h1>
|
||||
<p>Ein fortlaufendes Werk über Software, Design und Architektur.</p>
|
||||
</div>
|
||||
|
||||
<div class="glass-container toc">
|
||||
<h2>Inhaltsverzeichnis</h2>
|
||||
{Object.entries(chapters).map(([chapter, items]) => (
|
||||
<div class="chapter">
|
||||
<h3>{chapter.replace(/-/g, ' ').toUpperCase()}</h3>
|
||||
<ul class="page-list">
|
||||
{items.map(item => (
|
||||
<li><a href={`/book/${item.id}`}>{item.data.title}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<style>
|
||||
.header { margin-bottom: 2rem; }
|
||||
.toc { padding: 2rem; }
|
||||
.chapter { margin-bottom: 1.5rem; }
|
||||
.chapter h3 { margin-top: 0; margin-bottom: 0.5rem; font-size: 1.1rem; opacity: 0.8; }
|
||||
.page-list { list-style: none; padding-left: 0; margin: 0; }
|
||||
.page-list li { margin-bottom: 0.5rem; }
|
||||
.page-list a { display: block; padding: 0.5rem; border-radius: 0.25rem; transition: background 0.2s; }
|
||||
.page-list a:hover { background: rgba(59, 130, 246, 0.1); }
|
||||
</style>
|
||||
226
src/pages/index.astro
Normal file
226
src/pages/index.astro
Normal file
@@ -0,0 +1,226 @@
|
||||
---
|
||||
import WelcomeTypewriter from '../components/WelcomeTypewriter.astro';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import socials from "../data/socials.json";
|
||||
---
|
||||
<BaseLayout title="c0ntroller.de" theme="default">
|
||||
<div class="landing-wrapper">
|
||||
<div class="hero-section">
|
||||
<div class="socials fade-in-up shadow-glow glass-bg glass-container">
|
||||
{socials.map(({ name, url, icon }) => (
|
||||
<a href={url} target="_blank" rel="noreferrer" class="social-link">
|
||||
<Icon name={icon} desc={name} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<h1 class="gradient-text reveal-text">Willkommen auf meiner Seite.</h1>
|
||||
<p class="subtitle fade-in-up">
|
||||
Hier geht es unter anderem um:
|
||||
</p>
|
||||
<WelcomeTypewriter class="fade-in-up" />
|
||||
</div>
|
||||
|
||||
<div class="bento-grid">
|
||||
<a href="/portfolio" class="bento-card portfolio shadow-glow">
|
||||
<div class="card-content glass-bg">
|
||||
<div class="icon"><Icon name="lucide:sparkles" /></div>
|
||||
<h2>Portfolio</h2>
|
||||
<p>Meine neuesten Projekte und Experimente.</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/blog" class="bento-card blog shadow-glow">
|
||||
<div class="card-content glass-bg">
|
||||
<div class="icon"><Icon name="lucide:pen-tool" /></div>
|
||||
<h2>Blog</h2>
|
||||
<p>Einige planlose Gedanken über Technologie und Design.</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/book" class="bento-card book shadow-glow">
|
||||
<div class="card-content glass-bg">
|
||||
<div class="icon"><Icon name="lucide:book-open" /></div>
|
||||
<h2>Das Buch</h2>
|
||||
<p>Ein subjektiver Guide in die Welt der Programmierung.</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.landing-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 80vh;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
text-align: center;
|
||||
max-width: 750px;
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
font-size: clamp(3rem, 7vw, 5rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #94a3b8 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 1.5rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.socials {
|
||||
border-radius: 2rem;
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: .5rem;
|
||||
margin: 0 auto 2rem auto;
|
||||
padding: 0.5rem 1rem;
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
}
|
||||
|
||||
.social-link {
|
||||
color: rgb(var(--accent-base));
|
||||
font-size: 2rem;
|
||||
margin: 0 0.5rem;
|
||||
filter: drop-shadow(0px 0px 5px transparent);
|
||||
transition: filter 0.5s ease;
|
||||
}
|
||||
|
||||
.social-link:hover svg {
|
||||
filter: drop-shadow(0px 0px 5px rgba(var(--accent-base), 0.4));
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: clamp(1.1rem, 2.5vw, 1.4rem);
|
||||
color: var(--text-muted);
|
||||
line-height: 1.6;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.bento-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.bento-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.bento-card {
|
||||
text-decoration: none;
|
||||
border-radius: 1.5rem;
|
||||
position: relative;
|
||||
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.bento-card:hover {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: 1.4rem;
|
||||
padding: 2.5rem 2rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.bento-card:hover .card-content {
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 2.5rem;
|
||||
margin: 0 auto 1rem auto;
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 1rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bento-card h2 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.5rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bento-card p {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.portfolio { background: linear-gradient(135deg, rgba(var(--accent-base--portfolio), 0.5) 0%, rgba(var(--accent-base--portfolio), 0.15) 100%); }
|
||||
.blog { background: linear-gradient(135deg, rgba(var(--accent-base--blog), 0.8) 0%, rgba(var(--accent-base--blog), 0.15) 100%); }
|
||||
.book { background: linear-gradient(135deg, rgba(var(--accent-base--book), 0.8) 0%, rgba(var(--accent-base--book), 0.15) 100%); }
|
||||
|
||||
.portfolio:hover { box-shadow: 0 10px 40px -10px rgba(var(--accent-base--portfolio), 1); }
|
||||
.blog:hover { box-shadow: 0 10px 40px -10px rgba(var(--accent-base--blog), 0.4); }
|
||||
.book:hover { box-shadow: 0 10px 40px -10px rgba(var(--accent-base--book), 0.4); }
|
||||
|
||||
.portfolio .icon { color: rgb(var(--accent-base--portfolio)); }
|
||||
.blog .icon { color: rgb(var(--accent-base--blog)); }
|
||||
.book .icon { color: rgb(var(--accent-base--book)); }
|
||||
|
||||
@keyframes fadeUp {
|
||||
from { opacity: 0; transform: translateY(30px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.reveal-text {
|
||||
animation: fadeUp 0.8s ease-out forwards;
|
||||
}
|
||||
|
||||
.fade-in-up {
|
||||
animation: fadeUp 1s ease-out forwards;
|
||||
opacity: 0;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
/* Stagger grid items */
|
||||
.bento-card {
|
||||
animation: fadeUp 0.8s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
.portfolio { animation-delay: 0.4s; }
|
||||
.blog { animation-delay: 0.6s; }
|
||||
.book { animation-delay: 0.8s; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bento-card, .fade-in-up, .reveal-text {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
.bento-card:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
52
src/pages/portfolio/[...slug].astro
Normal file
52
src/pages/portfolio/[...slug].astro
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import "katex/dist/katex.min.css";
|
||||
import "rehype-callouts/theme/obsidian"
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const portfolioEntries = await getCollection('portfolio');
|
||||
return portfolioEntries.map(entry => ({
|
||||
params: { slug: entry.id }, props: { entry },
|
||||
}));
|
||||
}
|
||||
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await render(entry);
|
||||
---
|
||||
<BaseLayout title={entry.data.title} theme="portfolio">
|
||||
<div class="glass-container header">
|
||||
<h1>{entry.data.title}</h1>
|
||||
<p>{entry.data.summary}</p>
|
||||
{entry.data.tags && (
|
||||
<div class="tags">
|
||||
{entry.data.tags.map(tech => <span class="tag">{tech}</span>)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="glass-container content prose">
|
||||
<Content />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tag {
|
||||
font-size: 0.8rem;
|
||||
background: rgba(var(--accent-base), 0.2);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.content {
|
||||
line-height: 1.8;
|
||||
}
|
||||
</style>
|
||||
55
src/pages/portfolio/index.astro
Normal file
55
src/pages/portfolio/index.astro
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
|
||||
const projects = await getCollection('portfolio');
|
||||
projects.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
|
||||
---
|
||||
<BaseLayout title="Portfolio" theme="portfolio" description="Eine Auswahl aktueller Projekte und Experimente von c0ntroller.de.">
|
||||
<div class="glass-container">
|
||||
<h1>Meine Arbeiten</h1>
|
||||
<p>Eine Auswahl aktueller Projekte und Experimente.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
{projects.map(project => (
|
||||
<a href={`/portfolio/${project.id}`} class="glass-container link-card">
|
||||
<h2>{project.data.title}</h2>
|
||||
<p>{project.data.summary}</p>
|
||||
{project.data.tags && (
|
||||
<div class="tags">
|
||||
{project.data.tags?.map(tech => <span class="tag">{tech}</span>)}
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<style>
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.link-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.link-card h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tag {
|
||||
font-size: 0.8rem;
|
||||
background: rgba(var(--accent-base), 0.2);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user