style: auto-format codebase with Prettier

Applied formatting to 1487+ files using pnpm format:write
  - TypeScript/JavaScript files
  - Svelte components
  - Astro pages
  - JSON configs
  - Markdown docs

  13 files still need manual review (Astro JSX comments)
This commit is contained in:
Wuesteon 2025-11-27 18:33:16 +01:00
parent 0241f5554c
commit d36b321d9d
3952 changed files with 661498 additions and 739751 deletions

View file

@ -5,6 +5,7 @@ This file provides guidance to Claude Code when working with the BaunTown projec
## Project Overview
BaunTown is a community website for developers and creators with:
- Multilingual support (DE, EN, IT)
- Payment integration (Stripe, PayPal)
- Content collections for news, projects, tutorials, etc.
@ -56,6 +57,7 @@ pnpm --filter @bauntown/landing preview
## Environment Variables
Create `apps/bauntown/apps/landing/.env`:
```bash
STRIPE_SECRET_KEY=sk_...
STRIPE_WEBHOOK_SECRET=whsec_...
@ -65,31 +67,31 @@ PUBLIC_STRIPE_KEY=pk_...
## Technology Stack
| Component | Technology |
|-----------|------------|
| Framework | Astro 5.x |
| Styling | CSS/Tailwind |
| i18n | astro-i18n-aut (DE, EN, IT) |
| Payments | Stripe, PayPal |
| Analytics | Plausible (via Partytown) |
| APIs | Google Sheets/Docs |
| Deployment | Netlify (SSR + Functions) |
| Component | Technology |
| ---------- | --------------------------- |
| Framework | Astro 5.x |
| Styling | CSS/Tailwind |
| i18n | astro-i18n-aut (DE, EN, IT) |
| Payments | Stripe, PayPal |
| Analytics | Plausible (via Partytown) |
| APIs | Google Sheets/Docs |
| Deployment | Netlify (SSR + Functions) |
## Content Collections
BaunTown uses Astro Content Collections:
| Collection | Purpose |
|------------|---------|
| tools | Design, Development, Productivity tools |
| news | AI, Web, Design, Community news |
| models | AI models (Text, Image) |
| projects | Web, Mobile, Desktop projects |
| tutorials | Courses (UI/UX, Business, Marketing) |
| missions | Community challenges |
| vision | Long-term vision items |
| join | Join page content |
| members | Team members |
| Collection | Purpose |
| ---------- | --------------------------------------- |
| tools | Design, Development, Productivity tools |
| news | AI, Web, Design, Community news |
| models | AI models (Text, Image) |
| projects | Web, Mobile, Desktop projects |
| tutorials | Courses (UI/UX, Business, Marketing) |
| missions | Community challenges |
| vision | Long-term vision items |
| join | Join page content |
| members | Team members |
## Code Style Guidelines
@ -110,6 +112,7 @@ BaunTown uses Astro Content Collections:
## Deployment
Deployed via Netlify with `@astrojs/netlify` adapter:
- Static pages pre-rendered
- Dynamic routes use Netlify Functions
- Configuration in `netlify.toml`

View file

@ -67,4 +67,4 @@ Die Website unterstützt mehrere Sprachen mit einer URL-Struktur wie `example.co
- `de` - Deutsch (Standard)
- `en` - Englisch
- `it` - Italienisch
- `it` - Italienisch

View file

@ -67,12 +67,13 @@ npm install stripe @stripe/stripe-js @paypal/paypal-js
### 3. Erstellung der Frontend-Komponenten
#### PaymentForm.astro
```astro
---
import { useTranslations } from '../utils/i18n';
interface Props {
lang: string;
lang: string;
}
const { lang } = Astro.props;
@ -80,69 +81,74 @@ const t = useTranslations(lang);
---
<div class="payment-container">
<!-- Zahlungstyp-Auswahl -->
<div class="payment-type-selector">
<button id="one-time" class="payment-type-btn active">{t('support.onetime')}</button>
<button id="recurring" class="payment-type-btn">{t('support.recurring')}</button>
</div>
<!-- Kaffee-Größen -->
<div class="coffee-options">
<!-- ... -->
</div>
<!-- Zahlungsmethoden -->
<div class="payment-buttons">
<button id="stripe-button" class="payment-method-btn">
<!-- ... -->
<span>{t('support.payWithStripe')}</span>
</button>
<button id="paypal-button" class="payment-method-btn">
<!-- ... -->
<span>{t('support.payWithPayPal')}</span>
</button>
</div>
<!-- Zahlungstyp-Auswahl -->
<div class="payment-type-selector">
<button id="one-time" class="payment-type-btn active">{t('support.onetime')}</button>
<button id="recurring" class="payment-type-btn">{t('support.recurring')}</button>
</div>
<!-- Kaffee-Größen -->
<div class="coffee-options">
<!-- ... -->
</div>
<!-- Zahlungsmethoden -->
<div class="payment-buttons">
<button id="stripe-button" class="payment-method-btn">
<!-- ... -->
<span>{t('support.payWithStripe')}</span>
</button>
<button id="paypal-button" class="payment-method-btn">
<!-- ... -->
<span>{t('support.payWithPayPal')}</span>
</button>
</div>
</div>
<script>
import { loadStripe } from '@stripe/stripe-js';
document.addEventListener('DOMContentLoaded', async () => {
// Stripe-Instanz initialisieren
const stripePromise = loadStripe(import.meta.env.PUBLIC_STRIPE_PUBLISHABLE_KEY || 'pk_test_placeholder');
// Event-Listener für Stripe-Button
stripeBtn?.addEventListener('click', async () => {
try {
// Checkout-Session erstellen
const response = await fetch('/.netlify/functions/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount, isRecurring, priceId, coffeeSize
}),
});
const data = await response.json();
// Zur Checkout-Seite weiterleiten
if (data.url) {
window.location.href = data.url;
return;
}
// Alternativ: redirectToCheckout verwenden
if (data.sessionId || data.id) {
const stripe = await stripePromise;
await stripe.redirectToCheckout({
sessionId: data.sessionId || data.id,
});
}
} catch (error) {
console.error('Payment error:', error);
}
});
});
import { loadStripe } from '@stripe/stripe-js';
document.addEventListener('DOMContentLoaded', async () => {
// Stripe-Instanz initialisieren
const stripePromise = loadStripe(
import.meta.env.PUBLIC_STRIPE_PUBLISHABLE_KEY || 'pk_test_placeholder'
);
// Event-Listener für Stripe-Button
stripeBtn?.addEventListener('click', async () => {
try {
// Checkout-Session erstellen
const response = await fetch('/.netlify/functions/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount,
isRecurring,
priceId,
coffeeSize,
}),
});
const data = await response.json();
// Zur Checkout-Seite weiterleiten
if (data.url) {
window.location.href = data.url;
return;
}
// Alternativ: redirectToCheckout verwenden
if (data.sessionId || data.id) {
const stripe = await stripePromise;
await stripe.redirectToCheckout({
sessionId: data.sessionId || data.id,
});
}
} catch (error) {
console.error('Payment error:', error);
}
});
});
</script>
```
@ -154,50 +160,52 @@ const t = useTranslations(lang);
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
exports.handler = async (event, context) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "POST, OPTIONS"
};
try {
const { amount, isRecurring, coffeeSize } = JSON.parse(event.body || '{}');
const amountInCents = Math.round(amount * 100);
// Stripe Checkout Session erstellen
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: 'eur',
product_data: {
name: `BaunTown Kaffee - ${coffeeSize || 'Mittlerer Kaffee'}`,
description: isRecurring ? 'Monatliche Unterstützung' : 'Einmalige Unterstützung',
},
unit_amount: amountInCents,
recurring: isRecurring ? { interval: 'month' } : undefined,
},
quantity: 1,
}],
mode: isRecurring ? 'subscription' : 'payment',
success_url: `${process.env.URL || 'https://bauntown.com'}/support-success`,
cancel_url: `${process.env.URL || 'https://bauntown.com'}/support-cancel`,
});
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: session.url
})
};
} catch (error) {
return {
statusCode: 500,
headers,
body: JSON.stringify({ error: error.message })
};
}
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
try {
const { amount, isRecurring, coffeeSize } = JSON.parse(event.body || '{}');
const amountInCents = Math.round(amount * 100);
// Stripe Checkout Session erstellen
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'eur',
product_data: {
name: `BaunTown Kaffee - ${coffeeSize || 'Mittlerer Kaffee'}`,
description: isRecurring ? 'Monatliche Unterstützung' : 'Einmalige Unterstützung',
},
unit_amount: amountInCents,
recurring: isRecurring ? { interval: 'month' } : undefined,
},
quantity: 1,
},
],
mode: isRecurring ? 'subscription' : 'payment',
success_url: `${process.env.URL || 'https://bauntown.com'}/support-success`,
cancel_url: `${process.env.URL || 'https://bauntown.com'}/support-cancel`,
});
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: session.url,
}),
};
} catch (error) {
return {
statusCode: 500,
headers,
body: JSON.stringify({ error: error.message }),
};
}
};
```
@ -265,7 +273,7 @@ export function useTranslations(lang: keyof typeof ui) {
- Navigieren Sie zu Ihrem Netlify-Dashboard
- Gehen Sie zu "Site settings" > "Environment variables"
- Fügen Sie die folgenden Umgebungsvariablen hinzu:
- `PUBLIC_STRIPE_PUBLISHABLE_KEY` (muss mit PUBLIC_ beginnen für Frontend-Verwendung)
- `PUBLIC_STRIPE_PUBLISHABLE_KEY` (muss mit PUBLIC\_ beginnen für Frontend-Verwendung)
- `STRIPE_SECRET_KEY`
- `STRIPE_WEBHOOK_SECRET` (optional für Webhook-Verarbeitung)
@ -315,9 +323,9 @@ Die Netlify-Function enthält CORS-Header für die Anfrageverarbeitung:
```javascript
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "POST, OPTIONS"
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
```
@ -327,15 +335,15 @@ Die Implementation enthält einen Fallback-Mechanismus für Entwicklungs- und Te
```javascript
if (!process.env.STRIPE_SECRET_KEY) {
console.log("WARNUNG: STRIPE_SECRET_KEY fehlt - liefere Test-Antwort");
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: `${process.env.URL || 'https://bauntown.com'}/support-success?test=true`,
message: "Test mode - no Stripe key available"
})
};
console.log('WARNUNG: STRIPE_SECRET_KEY fehlt - liefere Test-Antwort');
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: `${process.env.URL || 'https://bauntown.com'}/support-success?test=true`,
message: 'Test mode - no Stripe key available',
}),
};
}
```
@ -346,9 +354,10 @@ if (!process.env.STRIPE_SECRET_KEY) {
Die Stripe-Integration für das "Buy Me a Coffee"-Feature wurde erfolgreich implementiert und ermöglicht sowohl einmalige als auch wiederkehrende Spenden. Durch die Verwendung von Netlify Functions wird die Sicherheit erhöht, da sensible Daten wie der Stripe Secret Key nicht im Frontend verfügbar sind.
Die wichtigsten Punkte für eine fehlerfreie Implementierung:
1. Verwenden Sie `PUBLIC_`-Präfix für Frontend-Umgebungsvariablen in Astro
2. Verwenden Sie den Stripe Checkout-Flow für eine einfache und sichere Zahlungsabwicklung
3. Implementieren Sie robuste Fehlerbehandlung für Übersetzungen und API-Aufrufe
4. Testen Sie sowohl im Entwicklungs- als auch im Produktionsmodus
Bei Fragen oder Problemen konsultieren Sie die [Stripe-Dokumentation](https://stripe.com/docs/checkout) oder öffnen Sie ein Issue im BaunTown-Repository.
Bei Fragen oder Problemen konsultieren Sie die [Stripe-Dokumentation](https://stripe.com/docs/checkout) oder öffnen Sie ein Issue im BaunTown-Repository.

View file

@ -4,155 +4,157 @@ const crypto = require('crypto');
// Überprüfe, ob alle erforderlichen Umgebungsvariablen vorhanden sind
const requiredEnvVars = [
'GOOGLE_SERVICE_ACCOUNT_EMAIL',
'GOOGLE_PRIVATE_KEY',
'GOOGLE_SHEET_ID',
'SITE_URL'
'GOOGLE_SERVICE_ACCOUNT_EMAIL',
'GOOGLE_PRIVATE_KEY',
'GOOGLE_SHEET_ID',
'SITE_URL',
];
function checkEnvVars() {
const missing = requiredEnvVars.filter(varName => !process.env[varName]);
if (missing.length > 0) {
throw new Error(`Fehlende Umgebungsvariablen: ${missing.join(', ')}`);
}
const missing = requiredEnvVars.filter((varName) => !process.env[varName]);
if (missing.length > 0) {
throw new Error(`Fehlende Umgebungsvariablen: ${missing.join(', ')}`);
}
}
const auth = new JWT({
email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const sheets = google.sheets({ version: 'v4', auth });
exports.handler = async function(event, context) {
console.log('Content submission request received');
// CORS Headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS'
};
exports.handler = async function (event, context) {
console.log('Content submission request received');
// Handle OPTIONS request
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: ''
};
}
// CORS Headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: 'Method Not Allowed' })
};
}
// Handle OPTIONS request
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: '',
};
}
try {
// Überprüfe Umgebungsvariablen
checkEnvVars();
// Parse request body
const { contentType, title, description, email } = JSON.parse(event.body);
if (!contentType || !title || !description || !email) {
throw new Error('Alle Felder müssen ausgefüllt werden');
}
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: 'Method Not Allowed' }),
};
}
console.log('Processing submission:', { contentType, title, email });
const timestamp = new Date().toISOString();
const id = crypto.randomUUID();
try {
// Überprüfe Umgebungsvariablen
checkEnvVars();
// Füge neue Zeile zum Sheet hinzu
console.log('Adding row to Google Sheet');
try {
// Prüfe zuerst, ob das Sheet existiert
const sheetInfo = await sheets.spreadsheets.get({
spreadsheetId: process.env.GOOGLE_SHEET_ID
});
// Prüfe, ob das Content-Submissions-Sheet existiert, wenn nicht, erstelle es
let sheetExists = false;
const sheetName = 'ContentSubmissions';
for (const sheet of sheetInfo.data.sheets) {
if (sheet.properties.title === sheetName) {
sheetExists = true;
break;
}
}
if (!sheetExists) {
// Erstelle ein neues Sheet für Content-Submissions
await sheets.spreadsheets.batchUpdate({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
requestBody: {
requests: [
{
addSheet: {
properties: {
title: sheetName,
gridProperties: {
rowCount: 1000,
columnCount: 6
}
}
}
}
]
}
});
// Füge Überschriften hinzu
await sheets.spreadsheets.values.update({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `${sheetName}!A1:F1`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [['Timestamp', 'ID', 'Content Type', 'Title', 'Description', 'Email']]
}
});
}
// Füge die neue Zeile hinzu
await sheets.spreadsheets.values.append({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `${sheetName}!A:F`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [[timestamp, id, contentType, title, description, email]]
}
});
console.log('Content submission added to sheet');
} catch (sheetError) {
console.error('Google Sheets error:', sheetError);
throw new Error(`Google Sheets Fehler: ${sheetError.message}. Bitte stellen Sie sicher, dass das Sheet existiert und der Service Account Zugriff hat.`);
}
// Parse request body
const { contentType, title, description, email } = JSON.parse(event.body);
console.log('Submission completed successfully');
return {
statusCode: 200,
headers,
body: JSON.stringify({
success: true,
message: 'Deine Einreichung wurde erfolgreich gespeichert.'
})
};
} catch (error) {
console.error('Content submission error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: error.message,
details: error.stack
})
};
}
};
if (!contentType || !title || !description || !email) {
throw new Error('Alle Felder müssen ausgefüllt werden');
}
console.log('Processing submission:', { contentType, title, email });
const timestamp = new Date().toISOString();
const id = crypto.randomUUID();
// Füge neue Zeile zum Sheet hinzu
console.log('Adding row to Google Sheet');
try {
// Prüfe zuerst, ob das Sheet existiert
const sheetInfo = await sheets.spreadsheets.get({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
});
// Prüfe, ob das Content-Submissions-Sheet existiert, wenn nicht, erstelle es
let sheetExists = false;
const sheetName = 'ContentSubmissions';
for (const sheet of sheetInfo.data.sheets) {
if (sheet.properties.title === sheetName) {
sheetExists = true;
break;
}
}
if (!sheetExists) {
// Erstelle ein neues Sheet für Content-Submissions
await sheets.spreadsheets.batchUpdate({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
requestBody: {
requests: [
{
addSheet: {
properties: {
title: sheetName,
gridProperties: {
rowCount: 1000,
columnCount: 6,
},
},
},
},
],
},
});
// Füge Überschriften hinzu
await sheets.spreadsheets.values.update({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `${sheetName}!A1:F1`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [['Timestamp', 'ID', 'Content Type', 'Title', 'Description', 'Email']],
},
});
}
// Füge die neue Zeile hinzu
await sheets.spreadsheets.values.append({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `${sheetName}!A:F`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [[timestamp, id, contentType, title, description, email]],
},
});
console.log('Content submission added to sheet');
} catch (sheetError) {
console.error('Google Sheets error:', sheetError);
throw new Error(
`Google Sheets Fehler: ${sheetError.message}. Bitte stellen Sie sicher, dass das Sheet existiert und der Service Account Zugriff hat.`
);
}
console.log('Submission completed successfully');
return {
statusCode: 200,
headers,
body: JSON.stringify({
success: true,
message: 'Deine Einreichung wurde erfolgreich gespeichert.',
}),
};
} catch (error) {
console.error('Content submission error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: error.message,
details: error.stack,
}),
};
}
};

View file

@ -2,112 +2,114 @@
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
exports.handler = async (event, context) => {
// CORS Headers
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "POST, OPTIONS"
};
// Handle OPTIONS request (CORS preflight)
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: ""
};
}
// CORS Headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
try {
// Parse request
const data = JSON.parse(event.body || '{}');
const { amount, isRecurring, priceId, coffeeSize } = data;
// Debug-Ausgabe
console.log("Request data:", { amount, isRecurring, priceId, coffeeSize });
console.log("Stripe key available:", !!process.env.STRIPE_SECRET_KEY);
// Fallback, wenn kein Stripe-Key verfügbar
if (!process.env.STRIPE_SECRET_KEY) {
console.log("WARNUNG: STRIPE_SECRET_KEY fehlt - liefere Test-Antwort");
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: `${process.env.URL || 'https://bauntown.com'}/support-success?test=true`,
isRecurring: isRecurring || false,
message: "Test mode - no Stripe key available"
})
};
}
// Betrag in Cent umrechnen für Stripe
const amountInCents = Math.round(amount * 100);
let sessionConfig = {
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: 'eur',
product_data: {
name: `BaunTown Kaffee - ${coffeeSize || 'Mittlerer Kaffee'}`,
description: isRecurring ? 'Monatliche Unterstützung' : 'Einmalige Unterstützung',
},
unit_amount: amountInCents,
recurring: isRecurring ? { interval: 'month' } : undefined,
},
quantity: 1,
}],
mode: isRecurring ? 'subscription' : 'payment',
success_url: `${process.env.URL || 'https://bauntown.com'}/support-success?amount=${amount}&type=${isRecurring ? 'recurring' : 'one-time'}&provider=stripe&coffeeSize=${encodeURIComponent(coffeeSize || 'Mittlerer Kaffee')}`,
cancel_url: `${process.env.URL || 'https://bauntown.com'}/support-cancel`,
metadata: {
isRecurring: isRecurring ? 'true' : 'false',
priceId: priceId || '',
coffeeSize: coffeeSize || 'Mittlerer Kaffee'
}
};
console.log("Creating checkout session with config:", JSON.stringify(sessionConfig));
try {
// Stripe Checkout Session erstellen
const session = await stripe.checkout.sessions.create(sessionConfig);
console.log("Session created:", session.id, "URL:", session.url);
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: session.url, // Die Redirect-URL hat Priorität
isRecurring: isRecurring || false
})
};
} catch (stripeError) {
console.error("Stripe error:", stripeError);
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: `Stripe error: ${stripeError.message}`,
stripeCode: stripeError.code || 'unknown'
}
})
};
}
} catch (error) {
console.error("Function error:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: {
message: error.message || "Unknown server error"
}
})
};
}
};
// Handle OPTIONS request (CORS preflight)
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: '',
};
}
try {
// Parse request
const data = JSON.parse(event.body || '{}');
const { amount, isRecurring, priceId, coffeeSize } = data;
// Debug-Ausgabe
console.log('Request data:', { amount, isRecurring, priceId, coffeeSize });
console.log('Stripe key available:', !!process.env.STRIPE_SECRET_KEY);
// Fallback, wenn kein Stripe-Key verfügbar
if (!process.env.STRIPE_SECRET_KEY) {
console.log('WARNUNG: STRIPE_SECRET_KEY fehlt - liefere Test-Antwort');
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: `${process.env.URL || 'https://bauntown.com'}/support-success?test=true`,
isRecurring: isRecurring || false,
message: 'Test mode - no Stripe key available',
}),
};
}
// Betrag in Cent umrechnen für Stripe
const amountInCents = Math.round(amount * 100);
let sessionConfig = {
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'eur',
product_data: {
name: `BaunTown Kaffee - ${coffeeSize || 'Mittlerer Kaffee'}`,
description: isRecurring ? 'Monatliche Unterstützung' : 'Einmalige Unterstützung',
},
unit_amount: amountInCents,
recurring: isRecurring ? { interval: 'month' } : undefined,
},
quantity: 1,
},
],
mode: isRecurring ? 'subscription' : 'payment',
success_url: `${process.env.URL || 'https://bauntown.com'}/support-success?amount=${amount}&type=${isRecurring ? 'recurring' : 'one-time'}&provider=stripe&coffeeSize=${encodeURIComponent(coffeeSize || 'Mittlerer Kaffee')}`,
cancel_url: `${process.env.URL || 'https://bauntown.com'}/support-cancel`,
metadata: {
isRecurring: isRecurring ? 'true' : 'false',
priceId: priceId || '',
coffeeSize: coffeeSize || 'Mittlerer Kaffee',
},
};
console.log('Creating checkout session with config:', JSON.stringify(sessionConfig));
try {
// Stripe Checkout Session erstellen
const session = await stripe.checkout.sessions.create(sessionConfig);
console.log('Session created:', session.id, 'URL:', session.url);
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: session.url, // Die Redirect-URL hat Priorität
isRecurring: isRecurring || false,
}),
};
} catch (stripeError) {
console.error('Stripe error:', stripeError);
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: `Stripe error: ${stripeError.message}`,
stripeCode: stripeError.code || 'unknown',
},
}),
};
}
} catch (error) {
console.error('Function error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: {
message: error.message || 'Unknown server error',
},
}),
};
}
};

View file

@ -5,140 +5,148 @@ const fetch = require('node-fetch');
// PayPal OAuth Token holen
async function getPayPalAccessToken() {
const auth = Buffer.from(`${process.env.PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`).toString('base64');
const response = await fetch('https://api-m.sandbox.paypal.com/v1/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${auth}`
},
body: 'grant_type=client_credentials'
});
const data = await response.json();
return data.access_token;
const auth = Buffer.from(
`${process.env.PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`
).toString('base64');
const response = await fetch('https://api-m.sandbox.paypal.com/v1/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${auth}`,
},
body: 'grant_type=client_credentials',
});
const data = await response.json();
return data.access_token;
}
// PayPal Order erstellen
async function createPayPalOrder(accessToken, amount, isRecurring, priceId, coffeeSize) {
const url = isRecurring
? 'https://api-m.sandbox.paypal.com/v1/billing/plans'
: 'https://api-m.sandbox.paypal.com/v2/checkout/orders';
if (isRecurring) {
// Vereinfachtes Beispiel für ein Abonnement
// In einer echten Anwendung sollten Sie Pläne vorab erstellen
const planData = {
name: `BaunTown Monthly ${coffeeSize}`,
description: `Monthly ${coffeeSize} support for BaunTown (${amount}€)`,
type: "FIXED",
payment_definitions: [
{
name: `Monthly ${coffeeSize}`,
type: "REGULAR",
frequency: "MONTH",
frequency_interval: "1",
amount: {
value: amount,
currency: "EUR"
},
cycles: "0"
}
],
merchant_preferences: {
return_url: `${process.env.URL || 'http://localhost:3000'}/support-success`,
cancel_url: `${process.env.URL || 'http://localhost:3000'}/support-cancel`
}
};
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify(planData)
});
return response.json();
} else {
// Normale einmalige Zahlung
const orderData = {
intent: "CAPTURE",
purchase_units: [
{
amount: {
currency_code: "EUR",
value: amount
},
description: `${coffeeSize} für BaunTown (${amount}€)`,
custom_id: priceId
}
],
application_context: {
return_url: `${process.env.URL || 'http://localhost:3000'}/support-success?amount=${amount}&type=${isRecurring ? 'recurring' : 'one-time'}&provider=paypal&coffeeSize=${encodeURIComponent(coffeeSize)}`,
cancel_url: `${process.env.URL || 'http://localhost:3000'}/support-cancel`
}
};
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify(orderData)
});
return response.json();
}
const url = isRecurring
? 'https://api-m.sandbox.paypal.com/v1/billing/plans'
: 'https://api-m.sandbox.paypal.com/v2/checkout/orders';
if (isRecurring) {
// Vereinfachtes Beispiel für ein Abonnement
// In einer echten Anwendung sollten Sie Pläne vorab erstellen
const planData = {
name: `BaunTown Monthly ${coffeeSize}`,
description: `Monthly ${coffeeSize} support for BaunTown (${amount}€)`,
type: 'FIXED',
payment_definitions: [
{
name: `Monthly ${coffeeSize}`,
type: 'REGULAR',
frequency: 'MONTH',
frequency_interval: '1',
amount: {
value: amount,
currency: 'EUR',
},
cycles: '0',
},
],
merchant_preferences: {
return_url: `${process.env.URL || 'http://localhost:3000'}/support-success`,
cancel_url: `${process.env.URL || 'http://localhost:3000'}/support-cancel`,
},
};
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(planData),
});
return response.json();
} else {
// Normale einmalige Zahlung
const orderData = {
intent: 'CAPTURE',
purchase_units: [
{
amount: {
currency_code: 'EUR',
value: amount,
},
description: `${coffeeSize} für BaunTown (${amount}€)`,
custom_id: priceId,
},
],
application_context: {
return_url: `${process.env.URL || 'http://localhost:3000'}/support-success?amount=${amount}&type=${isRecurring ? 'recurring' : 'one-time'}&provider=paypal&coffeeSize=${encodeURIComponent(coffeeSize)}`,
cancel_url: `${process.env.URL || 'http://localhost:3000'}/support-cancel`,
},
};
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(orderData),
});
return response.json();
}
}
exports.handler = async (event, context) => {
// CORS Headers
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "POST, OPTIONS"
};
// Handle OPTIONS request (CORS preflight)
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: ""
};
}
// CORS Headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
try {
// Request-Body parsen
const data = JSON.parse(event.body);
const { amount, isRecurring, priceId, coffeeSize } = data;
// PayPal Access Token holen
const accessToken = await getPayPalAccessToken();
// Order oder Plan erstellen mit zusätzlichen Metadaten
const paypalResponse = await createPayPalOrder(accessToken, amount, isRecurring, priceId, coffeeSize);
return {
statusCode: 200,
headers,
body: JSON.stringify(paypalResponse)
};
} catch (error) {
console.error('PayPal error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: {
message: error.message || 'Internal server error'
}
})
};
}
};
// Handle OPTIONS request (CORS preflight)
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: '',
};
}
try {
// Request-Body parsen
const data = JSON.parse(event.body);
const { amount, isRecurring, priceId, coffeeSize } = data;
// PayPal Access Token holen
const accessToken = await getPayPalAccessToken();
// Order oder Plan erstellen mit zusätzlichen Metadaten
const paypalResponse = await createPayPalOrder(
accessToken,
amount,
isRecurring,
priceId,
coffeeSize
);
return {
statusCode: 200,
headers,
body: JSON.stringify(paypalResponse),
};
} catch (error) {
console.error('PayPal error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: {
message: error.message || 'Internal server error',
},
}),
};
}
};

View file

@ -4,112 +4,114 @@ const crypto = require('crypto');
// Überprüfe, ob alle erforderlichen Umgebungsvariablen vorhanden sind
const requiredEnvVars = [
'GOOGLE_SERVICE_ACCOUNT_EMAIL',
'GOOGLE_PRIVATE_KEY',
'GOOGLE_SHEET_ID',
'SITE_URL'
'GOOGLE_SERVICE_ACCOUNT_EMAIL',
'GOOGLE_PRIVATE_KEY',
'GOOGLE_SHEET_ID',
'SITE_URL',
];
function checkEnvVars() {
const missing = requiredEnvVars.filter(varName => !process.env[varName]);
if (missing.length > 0) {
throw new Error(`Fehlende Umgebungsvariablen: ${missing.join(', ')}`);
}
const missing = requiredEnvVars.filter((varName) => !process.env[varName]);
if (missing.length > 0) {
throw new Error(`Fehlende Umgebungsvariablen: ${missing.join(', ')}`);
}
}
const auth = new JWT({
email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const sheets = google.sheets({ version: 'v4', auth });
exports.handler = async function(event, context) {
console.log('Newsletter subscription request received');
// CORS Headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS'
};
exports.handler = async function (event, context) {
console.log('Newsletter subscription request received');
// Handle OPTIONS request
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: ''
};
}
// CORS Headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: 'Method Not Allowed' })
};
}
// Handle OPTIONS request
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: '',
};
}
try {
// Überprüfe Umgebungsvariablen
checkEnvVars();
// Parse request body
const { email } = JSON.parse(event.body);
if (!email) {
throw new Error('Email is required');
}
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: 'Method Not Allowed' }),
};
}
console.log('Processing subscription for email:', email);
const timestamp = new Date().toISOString();
const unsubscribeToken = crypto.randomUUID();
try {
// Überprüfe Umgebungsvariablen
checkEnvVars();
// Füge neue Zeile zum Sheet hinzu
console.log('Adding row to Google Sheet');
try {
// Prüfe zuerst, ob das Sheet existiert und hole den ersten Tab-Namen
const sheetInfo = await sheets.spreadsheets.get({
spreadsheetId: process.env.GOOGLE_SHEET_ID
});
console.log('Sheet info:', sheetInfo.data);
// Hole den Namen des ersten Tabs
const firstSheetName = sheetInfo.data.sheets[0].properties.title;
console.log('Using sheet name:', firstSheetName);
// Füge die Zeile hinzu
await sheets.spreadsheets.values.append({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `${firstSheetName}!A:D`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [[timestamp, email, 'active', unsubscribeToken]]
}
});
} catch (sheetError) {
console.error('Google Sheets error:', sheetError);
throw new Error(`Google Sheets Fehler: ${sheetError.message}. Bitte stellen Sie sicher, dass das Sheet existiert und der Service Account Zugriff hat.`);
}
// Parse request body
const { email } = JSON.parse(event.body);
if (!email) {
throw new Error('Email is required');
}
console.log('Subscription completed successfully');
return {
statusCode: 200,
headers,
body: JSON.stringify({ success: true })
};
} catch (error) {
console.error('Newsletter subscription error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: error.message,
details: error.stack
})
};
}
};
console.log('Processing subscription for email:', email);
const timestamp = new Date().toISOString();
const unsubscribeToken = crypto.randomUUID();
// Füge neue Zeile zum Sheet hinzu
console.log('Adding row to Google Sheet');
try {
// Prüfe zuerst, ob das Sheet existiert und hole den ersten Tab-Namen
const sheetInfo = await sheets.spreadsheets.get({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
});
console.log('Sheet info:', sheetInfo.data);
// Hole den Namen des ersten Tabs
const firstSheetName = sheetInfo.data.sheets[0].properties.title;
console.log('Using sheet name:', firstSheetName);
// Füge die Zeile hinzu
await sheets.spreadsheets.values.append({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `${firstSheetName}!A:D`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [[timestamp, email, 'active', unsubscribeToken]],
},
});
} catch (sheetError) {
console.error('Google Sheets error:', sheetError);
throw new Error(
`Google Sheets Fehler: ${sheetError.message}. Bitte stellen Sie sicher, dass das Sheet existiert und der Service Account Zugriff hat.`
);
}
console.log('Subscription completed successfully');
return {
statusCode: 200,
headers,
body: JSON.stringify({ success: true }),
};
} catch (error) {
console.error('Newsletter subscription error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: error.message,
details: error.stack,
}),
};
}
};

View file

@ -2,66 +2,66 @@ const { google } = require('googleapis');
const { JWT } = require('google-auth-library');
const auth = new JWT({
email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const sheets = google.sheets({ version: 'v4', auth });
exports.handler = async function(event, context) {
if (event.httpMethod !== 'GET') {
return { statusCode: 405, body: 'Method Not Allowed' };
}
exports.handler = async function (event, context) {
if (event.httpMethod !== 'GET') {
return { statusCode: 405, body: 'Method Not Allowed' };
}
const { token } = event.queryStringParameters;
const { token } = event.queryStringParameters;
if (!token) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Token is required' })
};
}
if (!token) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Token is required' }),
};
}
try {
// Finde die Zeile mit dem Token
const response = await sheets.spreadsheets.values.get({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: 'Sheet1!A:D',
});
try {
// Finde die Zeile mit dem Token
const response = await sheets.spreadsheets.values.get({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: 'Sheet1!A:D',
});
const values = response.data.values;
const rowIndex = values.findIndex(row => row[3] === token);
const values = response.data.values;
const rowIndex = values.findIndex((row) => row[3] === token);
if (rowIndex === -1) {
return {
statusCode: 404,
body: JSON.stringify({ error: 'Token not found' })
};
}
if (rowIndex === -1) {
return {
statusCode: 404,
body: JSON.stringify({ error: 'Token not found' }),
};
}
// Aktualisiere den Status auf 'unsubscribed'
await sheets.spreadsheets.values.update({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `Sheet1!C${rowIndex + 1}`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [['unsubscribed']]
}
});
// Aktualisiere den Status auf 'unsubscribed'
await sheets.spreadsheets.values.update({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `Sheet1!C${rowIndex + 1}`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [['unsubscribed']],
},
});
// Redirect zur Bestätigungsseite
return {
statusCode: 302,
headers: {
Location: `${process.env.URL}/unsubscribe-success`
}
};
} catch (error) {
console.error('Newsletter unsubscribe error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: error.message })
};
}
};
// Redirect zur Bestätigungsseite
return {
statusCode: 302,
headers: {
Location: `${process.env.URL}/unsubscribe-success`,
},
};
} catch (error) {
console.error('Newsletter unsubscribe error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: error.message }),
};
}
};

View file

@ -1,10 +1,10 @@
{
"name": "bauntown-functions",
"version": "1.0.0",
"dependencies": {
"stripe": "^12.0.0",
"googleapis": "^128.0.0",
"google-auth-library": "^9.0.0",
"node-fetch": "^2.7.0"
}
}
"name": "bauntown-functions",
"version": "1.0.0",
"dependencies": {
"stripe": "^12.0.0",
"googleapis": "^128.0.0",
"google-auth-library": "^9.0.0",
"node-fetch": "^2.7.0"
}
}

View file

@ -1,145 +1,145 @@
// Netlify Function für Webhooks von Stripe und PayPal
exports.handler = async (event, context) => {
// CORS Headers
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "POST, OPTIONS"
};
// Handle OPTIONS request (CORS preflight)
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: ""
};
}
// CORS Headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
// Den Webhook-Source aus URL-Parameter oder Header bestimmen
const source = event.queryStringParameters.source || 'unknown';
try {
if (source === 'stripe') {
// Stripe Webhook verarbeiten
return processStripeWebhook(event, headers);
} else if (source === 'paypal') {
// PayPal Webhook verarbeiten
return processPayPalWebhook(event, headers);
} else {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: `Unknown webhook source: ${source}`
}
})
};
}
} catch (error) {
console.error(`Webhook error (${source}):`, error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: {
message: error.message || 'Internal server error'
}
})
};
}
// Handle OPTIONS request (CORS preflight)
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: '',
};
}
// Den Webhook-Source aus URL-Parameter oder Header bestimmen
const source = event.queryStringParameters.source || 'unknown';
try {
if (source === 'stripe') {
// Stripe Webhook verarbeiten
return processStripeWebhook(event, headers);
} else if (source === 'paypal') {
// PayPal Webhook verarbeiten
return processPayPalWebhook(event, headers);
} else {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: `Unknown webhook source: ${source}`,
},
}),
};
}
} catch (error) {
console.error(`Webhook error (${source}):`, error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: {
message: error.message || 'Internal server error',
},
}),
};
}
};
// Stripe Webhook verarbeiten
async function processStripeWebhook(event, headers) {
// In einer echten Anwendung würden Sie hier die Stripe Signatur überprüfen
// const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// const sig = event.headers['stripe-signature'];
// const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
try {
const data = JSON.parse(event.body);
// Verschiedene Ereignistypen verarbeiten
switch (data.type) {
case 'payment_intent.succeeded':
// Zahlung erfolgreich - hier könnte eine Datenbank aktualisiert werden
console.log('Payment succeeded:', data.data.object.id);
break;
case 'payment_intent.payment_failed':
// Zahlung fehlgeschlagen
console.log('Payment failed:', data.data.object.id);
break;
// Weitere Event-Typen hier verarbeiten
default:
console.log('Unhandled Stripe event type:', data.type);
}
return {
statusCode: 200,
headers,
body: JSON.stringify({ received: true })
};
} catch (error) {
console.error('Error processing Stripe webhook:', error);
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: error.message
}
})
};
}
// In einer echten Anwendung würden Sie hier die Stripe Signatur überprüfen
// const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// const sig = event.headers['stripe-signature'];
// const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
try {
const data = JSON.parse(event.body);
// Verschiedene Ereignistypen verarbeiten
switch (data.type) {
case 'payment_intent.succeeded':
// Zahlung erfolgreich - hier könnte eine Datenbank aktualisiert werden
console.log('Payment succeeded:', data.data.object.id);
break;
case 'payment_intent.payment_failed':
// Zahlung fehlgeschlagen
console.log('Payment failed:', data.data.object.id);
break;
// Weitere Event-Typen hier verarbeiten
default:
console.log('Unhandled Stripe event type:', data.type);
}
return {
statusCode: 200,
headers,
body: JSON.stringify({ received: true }),
};
} catch (error) {
console.error('Error processing Stripe webhook:', error);
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: error.message,
},
}),
};
}
}
// PayPal Webhook verarbeiten
async function processPayPalWebhook(event, headers) {
// In einer echten Anwendung würden Sie hier die PayPal-Signatur verifizieren
try {
const data = JSON.parse(event.body);
// Verschiedene Ereignistypen verarbeiten
switch (data.event_type) {
case 'PAYMENT.CAPTURE.COMPLETED':
// Zahlung erfolgreich - hier könnte eine Datenbank aktualisiert werden
console.log('PayPal payment completed:', data.resource.id);
break;
case 'PAYMENT.CAPTURE.DENIED':
// Zahlung abgelehnt
console.log('PayPal payment denied:', data.resource.id);
break;
// Weitere Event-Typen hier verarbeiten
default:
console.log('Unhandled PayPal event type:', data.event_type);
}
return {
statusCode: 200,
headers,
body: JSON.stringify({ received: true })
};
} catch (error) {
console.error('Error processing PayPal webhook:', error);
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: error.message
}
})
};
}
}
// In einer echten Anwendung würden Sie hier die PayPal-Signatur verifizieren
try {
const data = JSON.parse(event.body);
// Verschiedene Ereignistypen verarbeiten
switch (data.event_type) {
case 'PAYMENT.CAPTURE.COMPLETED':
// Zahlung erfolgreich - hier könnte eine Datenbank aktualisiert werden
console.log('PayPal payment completed:', data.resource.id);
break;
case 'PAYMENT.CAPTURE.DENIED':
// Zahlung abgelehnt
console.log('PayPal payment denied:', data.resource.id);
break;
// Weitere Event-Typen hier verarbeiten
default:
console.log('Unhandled PayPal event type:', data.event_type);
}
return {
statusCode: 200,
headers,
body: JSON.stringify({ received: true }),
};
} catch (error) {
console.error('Error processing PayPal webhook:', error);
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: error.message,
},
}),
};
}
}

View file

@ -1,31 +1,31 @@
{
"name": "@bauntown/landing",
"type": "module",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"type-check": "astro check",
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "^4.2.3",
"@astrojs/netlify": "^6.2.5",
"@astrojs/partytown": "^2.1.4",
"@astrojs/prefetch": "^0.4.1",
"@paypal/paypal-js": "^8.2.0",
"@stripe/stripe-js": "^6.1.0",
"astro": "^5.5.6",
"astro-i18n-aut": "^0.7.3",
"google-auth-library": "^9.15.1",
"googleapis": "^148.0.0",
"node-fetch": "^3.3.2",
"stripe": "^17.7.0"
},
"devDependencies": {
"@astrojs/check": "^0.9.0",
"typescript": "^5.7.2"
}
"name": "@bauntown/landing",
"type": "module",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"type-check": "astro check",
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "^4.2.3",
"@astrojs/netlify": "^6.2.5",
"@astrojs/partytown": "^2.1.4",
"@astrojs/prefetch": "^0.4.1",
"@paypal/paypal-js": "^8.2.0",
"@stripe/stripe-js": "^6.1.0",
"astro": "^5.5.6",
"astro-i18n-aut": "^0.7.3",
"google-auth-library": "^9.15.1",
"googleapis": "^148.0.0",
"node-fetch": "^3.3.2",
"stripe": "^17.7.0"
},
"devDependencies": {
"@astrojs/check": "^0.9.0",
"typescript": "^5.7.2"
}
}

View file

@ -1,218 +1,231 @@
---
interface Props {
title: string;
description?: string;
image?: string;
variant?: 'primary' | 'secondary' | 'tertiary';
imageAlt?: string;
href?: string;
buttonText?: string;
className?: string;
title: string;
description?: string;
image?: string;
variant?: 'primary' | 'secondary' | 'tertiary';
imageAlt?: string;
href?: string;
buttonText?: string;
className?: string;
}
const {
title,
description,
image,
variant = 'primary',
imageAlt = '',
href,
buttonText,
className = ''
title,
description,
image,
variant = 'primary',
imageAlt = '',
href,
buttonText,
className = '',
} = Astro.props;
---
{href ? (
<a href={href} class="card-link">
<article class:list={["card", `card-${variant}`, className]}>
{image && (
<div class="card-image-container">
<img src={image} alt={imageAlt} class="card-image" />
</div>
)}
<div class="card-content">
<h3 class="card-title">{title}</h3>
{description && <p class="card-description">{description}</p>}
{buttonText && (
<div class="card-footer">
<span class="card-button">
{buttonText} <span class="arrow">→</span>
</span>
</div>
)}
</div>
</article>
</a>
) : (
<article class:list={["card", `card-${variant}`, className]}>
{image && (
<div class="card-image-container">
<img src={image} alt={imageAlt} class="card-image" />
</div>
)}
<div class="card-content">
<h3 class="card-title">{title}</h3>
{description && <p class="card-description">{description}</p>}
</div>
</article>
)}
{
href ? (
<a href={href} class="card-link">
<article class:list={['card', `card-${variant}`, className]}>
{image && (
<div class="card-image-container">
<img src={image} alt={imageAlt} class="card-image" />
</div>
)}
<div class="card-content">
<h3 class="card-title">{title}</h3>
{description && <p class="card-description">{description}</p>}
{buttonText && (
<div class="card-footer">
<span class="card-button">
{buttonText} <span class="arrow">→</span>
</span>
</div>
)}
</div>
</article>
</a>
) : (
<article class:list={['card', `card-${variant}`, className]}>
{image && (
<div class="card-image-container">
<img src={image} alt={imageAlt} class="card-image" />
</div>
)}
<div class="card-content">
<h3 class="card-title">{title}</h3>
{description && <p class="card-description">{description}</p>}
</div>
</article>
)
}
<style>
.card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.card {
display: flex;
flex-direction: column;
border-radius: 1rem;
overflow: hidden;
height: 100%;
border: 2px solid rgba(255, 255, 255, 0.3);
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.card-link:hover .card {
transform: translateY(-2px);
box-shadow: 0 6px 12px -3px rgba(0, 0, 0, 0.1);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
}
.card {
display: flex;
flex-direction: column;
border-radius: 1rem;
overflow: hidden;
height: 100%;
border: 2px solid rgba(255, 255, 255, 0.3);
transition:
transform 0.2s ease,
box-shadow 0.2s ease,
border-color 0.2s ease;
}
/* Primary variant - accent background, white text */
.card-primary {
background-color: var(--accent-color);
color: white;
box-shadow: 0 4px 6px -1px rgba(249, 115, 22, 0.2), 0 2px 4px -2px rgba(249, 115, 22, 0.1);
}
.card-link:hover .card-primary {
box-shadow: 0 8px 10px -3px rgba(249, 115, 22, 0.25), 0 4px 6px -2px rgba(249, 115, 22, 0.15);
background-color: #ea580c; /* Slightly darker shade when hovered */
}
.card-primary .card-button {
background-color: white;
color: var(--accent-color);
display: inline-flex;
align-items: center;
}
.card-link:hover .card {
transform: translateY(-2px);
box-shadow: 0 6px 12px -3px rgba(0, 0, 0, 0.1);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
}
/* Secondary variant - white background, accent elements */
.card-secondary {
background-color: var(--card-bg);
color: var(--text-color);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.03);
}
.card-link:hover .card-secondary {
box-shadow: 0 6px 12px -2px rgba(0, 0, 0, 0.08), 0 3px 5px -2px rgba(0, 0, 0, 0.04);
}
.card-secondary .card-button {
background-color: var(--accent-color);
color: white;
display: inline-flex;
align-items: center;
}
/* Primary variant - accent background, white text */
.card-primary {
background-color: var(--accent-color);
color: white;
box-shadow:
0 4px 6px -1px rgba(249, 115, 22, 0.2),
0 2px 4px -2px rgba(249, 115, 22, 0.1);
}
/* Tertiary variant - minimal design */
.card-tertiary {
background-color: var(--card-bg);
box-shadow: none;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.card-link:hover .card-tertiary {
background-color: var(--card-bg);
}
.card-tertiary .card-button {
color: var(--accent-color);
display: inline-flex;
align-items: center;
}
.card-link:hover .card-primary {
box-shadow:
0 8px 10px -3px rgba(249, 115, 22, 0.25),
0 4px 6px -2px rgba(249, 115, 22, 0.15);
background-color: #ea580c; /* Slightly darker shade when hovered */
}
.card-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.card-primary .card-button {
background-color: white;
color: var(--accent-color);
display: inline-flex;
align-items: center;
}
.card-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
/* Secondary variant - white background, accent elements */
.card-secondary {
background-color: var(--card-bg);
color: var(--text-color);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 1px 2px -1px rgba(0, 0, 0, 0.03);
}
.card-content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.card-link:hover .card-secondary {
box-shadow:
0 6px 12px -2px rgba(0, 0, 0, 0.08),
0 3px 5px -2px rgba(0, 0, 0, 0.04);
}
.card-title {
font-size: 1.5rem;
font-weight: 700;
margin: 0 0 1rem 0;
line-height: 1.3;
}
.card-secondary .card-button {
background-color: var(--accent-color);
color: white;
display: inline-flex;
align-items: center;
}
.card-description {
font-size: 1rem;
line-height: 1.6;
margin: 0 0 1.5rem 0;
flex-grow: 1;
opacity: 0.85;
}
/* Tertiary variant - minimal design */
.card-tertiary {
background-color: var(--card-bg);
box-shadow: none;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.card-footer {
margin-top: auto;
display: flex;
justify-content: flex-start;
}
.card-link:hover .card-tertiary {
background-color: var(--card-bg);
}
.card-button {
display: inline-flex;
align-items: center;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
text-align: center;
transition: all 0.2s ease;
}
.arrow {
display: inline-block;
margin-left: 8px;
transition: transform 0.2s ease;
}
.card-link:hover .arrow {
transform: translateX(4px);
}
.card-tertiary .card-button {
color: var(--accent-color);
display: inline-flex;
align-items: center;
}
@media (max-width: 768px) {
.card-content {
padding: 1.25rem;
}
.card-title {
font-size: 1.25rem;
}
}
</style>
.card-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.card-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.card-content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.card-title {
font-size: 1.5rem;
font-weight: 700;
margin: 0 0 1rem 0;
line-height: 1.3;
}
.card-description {
font-size: 1rem;
line-height: 1.6;
margin: 0 0 1.5rem 0;
flex-grow: 1;
opacity: 0.85;
}
.card-footer {
margin-top: auto;
display: flex;
justify-content: flex-start;
}
.card-button {
display: inline-flex;
align-items: center;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
text-align: center;
transition: all 0.2s ease;
}
.arrow {
display: inline-block;
margin-left: 8px;
transition: transform 0.2s ease;
}
.card-link:hover .arrow {
transform: translateX(4px);
}
@media (max-width: 768px) {
.card-content {
padding: 1.25rem;
}
.card-title {
font-size: 1.25rem;
}
}
</style>

View file

@ -1,386 +1,360 @@
---
import { getLangFromUrl, useTranslations } from "../utils/i18n";
import { getLangFromUrl, useTranslations } from '../utils/i18n';
interface Props {
defaultType?: "mission" | "tutorial" | "vision";
showTitle?: boolean;
customTitle?: string;
customDescription?: string;
defaultType?: 'mission' | 'tutorial' | 'vision';
showTitle?: boolean;
customTitle?: string;
customDescription?: string;
}
const {
defaultType = "mission",
showTitle = true,
customTitle,
customDescription,
} = Astro.props;
const { defaultType = 'mission', showTitle = true, customTitle, customDescription } = Astro.props;
const lang = getLangFromUrl(Astro.url);
const t = useTranslations(lang);
---
<div class="content-submission">
{showTitle && <h2>{customTitle || t("join.submissionTitle")}</h2>}
<p class="submission-description">
{customDescription || t("join.submissionDesc")}
</p>
{showTitle && <h2>{customTitle || t('join.submissionTitle')}</h2>}
<p class="submission-description">
{customDescription || t('join.submissionDesc')}
</p>
<form class="submission-form" id="contentSubmissionForm">
<div class="form-group">
<label for="contentType">{t("join.submissionType")}</label>
<select id="contentType" name="contentType" required>
<option value="mission" selected={defaultType === "mission"}
>{t("join.submissionMission")}</option
>
<option value="tutorial" selected={defaultType === "tutorial"}
>{t("join.submissionTutorial")}</option
>
<option value="vision" selected={defaultType === "vision"}
>{t("join.submissionVision")}</option
>
</select>
</div>
<form class="submission-form" id="contentSubmissionForm">
<div class="form-group">
<label for="contentType">{t('join.submissionType')}</label>
<select id="contentType" name="contentType" required>
<option value="mission" selected={defaultType === 'mission'}
>{t('join.submissionMission')}</option
>
<option value="tutorial" selected={defaultType === 'tutorial'}
>{t('join.submissionTutorial')}</option
>
<option value="vision" selected={defaultType === 'vision'}
>{t('join.submissionVision')}</option
>
</select>
</div>
<div class="form-group">
<label for="title">{t("join.submissionTitle")}</label>
<input
type="text"
id="title"
name="title"
placeholder={t("join.submissionTitlePlaceholder")}
required
/>
</div>
<div class="form-group">
<label for="title">{t('join.submissionTitle')}</label>
<input
type="text"
id="title"
name="title"
placeholder={t('join.submissionTitlePlaceholder')}
required
/>
</div>
<div class="form-group">
<label for="description">{t("join.submissionDesc")}</label>
<textarea
id="description"
name="description"
placeholder={t("join.submissionDescPlaceholder")}
rows="4"
required></textarea>
</div>
<div class="form-group">
<label for="description">{t('join.submissionDesc')}</label>
<textarea
id="description"
name="description"
placeholder={t('join.submissionDescPlaceholder')}
rows="4"
required></textarea>
</div>
<div class="form-group">
<label for="email">{t("join.submissionEmail")}</label>
<input
type="email"
id="email"
name="email"
placeholder={t("join.submissionEmailPlaceholder")}
required
/>
</div>
<div class="form-group">
<label for="email">{t('join.submissionEmail')}</label>
<input
type="email"
id="email"
name="email"
placeholder={t('join.submissionEmailPlaceholder')}
required
/>
</div>
<button type="submit" class="submit-button"
>{t("join.submissionSubmit")}</button
>
</form>
<button type="submit" class="submit-button">{t('join.submissionSubmit')}</button>
</form>
<div id="submissionSuccess" class="submission-success" style="display: none;">
<p class="success-title">{t("join.submissionSuccessTitle")}</p>
<p class="success-message">{t("join.submissionSuccessMessage")}</p>
</div>
<div id="submissionSuccess" class="submission-success" style="display: none;">
<p class="success-title">{t('join.submissionSuccessTitle')}</p>
<p class="success-message">{t('join.submissionSuccessMessage')}</p>
</div>
</div>
<style>
.content-submission {
background-color: var(--card-bg);
border-radius: 1rem;
padding: 2.5rem;
border: 1px solid var(--border-color);
margin-bottom: 2rem;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.3s ease;
}
.content-submission {
background-color: var(--card-bg);
border-radius: 1rem;
padding: 2.5rem;
border: 1px solid var(--border-color);
margin-bottom: 2rem;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.3s ease;
}
/* Heller Modus */
:root:not(.dark) .content-submission {
background-color: var(--card-bg);
}
/* Heller Modus */
:root:not(.dark) .content-submission {
background-color: var(--card-bg);
}
.content-submission:hover {
box-shadow: 0 15px 30px -5px rgba(0, 0, 0, 0.08);
}
.content-submission:hover {
box-shadow: 0 15px 30px -5px rgba(0, 0, 0, 0.08);
}
h2 {
margin-top: 0;
margin-bottom: 1rem;
font-size: 1.75rem;
background: linear-gradient(90deg, var(--accent-color), #fb923c);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
display: inline-block;
position: relative;
}
h2 {
margin-top: 0;
margin-bottom: 1rem;
font-size: 1.75rem;
background: linear-gradient(90deg, var(--accent-color), #fb923c);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
display: inline-block;
position: relative;
}
h2::after {
content: "";
position: absolute;
left: 0;
bottom: -0.25rem;
width: 100%;
height: 3px;
background: linear-gradient(90deg, var(--accent-color), #fb923c);
border-radius: 1.5px;
}
h2::after {
content: '';
position: absolute;
left: 0;
bottom: -0.25rem;
width: 100%;
height: 3px;
background: linear-gradient(90deg, var(--accent-color), #fb923c);
border-radius: 1.5px;
}
.submission-description {
margin-bottom: 2rem;
color: var(--text-muted);
font-size: 1.1rem;
line-height: 1.6;
max-width: 600px;
}
.submission-description {
margin-bottom: 2rem;
color: var(--text-muted);
font-size: 1.1rem;
line-height: 1.6;
max-width: 600px;
}
.submission-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.submission-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
label {
font-weight: 600;
font-size: 0.95rem;
color: var(--text-color);
}
label {
font-weight: 600;
font-size: 0.95rem;
color: var(--text-color);
}
input,
select,
textarea {
padding: 0.9rem 1rem;
border-radius: 0.5rem;
border: 1.5px solid var(--border-color);
background-color: var(--background-color);
color: var(--text-color);
font-size: 1rem;
font-family: inherit;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
}
input,
select,
textarea {
padding: 0.9rem 1rem;
border-radius: 0.5rem;
border: 1.5px solid var(--border-color);
background-color: var(--background-color);
color: var(--text-color);
font-size: 1rem;
font-family: inherit;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
}
/* Heller Modus für Eingabefelder */
:root:not(.dark) input,
:root:not(.dark) select,
:root:not(.dark) textarea {
background-color: var(--background-color);
}
/* Heller Modus für Eingabefelder */
:root:not(.dark) input,
:root:not(.dark) select,
:root:not(.dark) textarea {
background-color: var(--background-color);
}
input:hover,
select:hover,
textarea:hover {
border-color: var(--accent-color);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.04);
}
input:hover,
select:hover,
textarea:hover {
border-color: var(--accent-color);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.04);
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(var(--accent-color-rgb), 0.15);
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(var(--accent-color-rgb), 0.15);
}
textarea {
min-height: 120px;
resize: vertical;
}
textarea {
min-height: 120px;
resize: vertical;
}
.submit-button {
background: linear-gradient(90deg, var(--accent-color), #fb923c);
color: white;
border: none;
border-radius: 0.5rem;
padding: 0.9rem 2rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
margin-top: 1rem;
align-self: flex-start;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.submit-button {
background: linear-gradient(90deg, var(--accent-color), #fb923c);
color: white;
border: none;
border-radius: 0.5rem;
padding: 0.9rem 2rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
margin-top: 1rem;
align-self: flex-start;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.submit-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15);
}
.submit-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15);
}
.submit-button:active {
transform: translateY(1px);
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1);
}
.submit-button:active {
transform: translateY(1px);
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1);
}
.submission-success {
background-color: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 0.75rem;
padding: 2rem;
margin-top: 2rem;
text-align: center;
animation: fadeIn 0.5s ease-in-out;
box-shadow: 0 4px 10px rgba(16, 185, 129, 0.1);
}
.submission-success {
background-color: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 0.75rem;
padding: 2rem;
margin-top: 2rem;
text-align: center;
animation: fadeIn 0.5s ease-in-out;
box-shadow: 0 4px 10px rgba(16, 185, 129, 0.1);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.submission-success .success-title {
color: #10b981;
margin: 0 0 1rem 0;
font-weight: 700;
font-size: 1.5rem;
}
.submission-success .success-title {
color: #10b981;
margin: 0 0 1rem 0;
font-weight: 700;
font-size: 1.5rem;
}
.submission-success .success-message {
color: var(--text-color);
margin: 0;
font-size: 1.05rem;
line-height: 1.7;
}
.submission-success .success-message {
color: var(--text-color);
margin: 0;
font-size: 1.05rem;
line-height: 1.7;
}
@media (max-width: 768px) {
.content-submission {
padding: 1.5rem;
}
@media (max-width: 768px) {
.content-submission {
padding: 1.5rem;
}
h2 {
font-size: 1.5rem;
}
h2 {
font-size: 1.5rem;
}
.submission-description {
font-size: 1rem;
}
.submission-description {
font-size: 1rem;
}
.submit-button {
width: 100%;
justify-content: center;
text-align: center;
display: flex;
}
}
.submit-button {
width: 100%;
justify-content: center;
text-align: center;
display: flex;
}
}
</style>
<script>
declare global {
interface Window {
plausible: (
eventName: string,
options?: { props?: Record<string, string> }
) => void;
}
}
declare global {
interface Window {
plausible: (eventName: string, options?: { props?: Record<string, string> }) => void;
}
}
document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById(
"contentSubmissionForm"
) as HTMLFormElement;
const successMessage = document.getElementById("submissionSuccess");
const contentType = document.getElementById(
"contentType"
) as HTMLSelectElement;
const title = document.getElementById("title") as HTMLInputElement;
const description = document.getElementById(
"description"
) as HTMLTextAreaElement;
const email = document.getElementById("email") as HTMLInputElement;
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('contentSubmissionForm') as HTMLFormElement;
const successMessage = document.getElementById('submissionSuccess');
const contentType = document.getElementById('contentType') as HTMLSelectElement;
const title = document.getElementById('title') as HTMLInputElement;
const description = document.getElementById('description') as HTMLTextAreaElement;
const email = document.getElementById('email') as HTMLInputElement;
if (
!form ||
!successMessage ||
!contentType ||
!title ||
!description ||
!email
)
return;
if (!form || !successMessage || !contentType || !title || !description || !email) return;
form.addEventListener("submit", async (e) => {
e.preventDefault();
form.addEventListener('submit', async (e) => {
e.preventDefault();
// Sammle Formulardaten
const formData = {
contentType: contentType.value,
title: title.value,
description: description.value,
email: email.value,
};
// Sammle Formulardaten
const formData = {
contentType: contentType.value,
title: title.value,
description: description.value,
email: email.value,
};
// Track content submission attempt
if (typeof window.plausible === "function") {
window.plausible("content-submit", {
props: {
type: contentType.value,
title: title.value,
email: email.value,
},
});
}
// Track content submission attempt
if (typeof window.plausible === 'function') {
window.plausible('content-submit', {
props: {
type: contentType.value,
title: title.value,
email: email.value,
},
});
}
try {
// Sende die Daten an die Netlify-Funktion
const response = await fetch("/.netlify/functions/content-submission", {
method: "POST",
body: JSON.stringify(formData),
});
try {
// Sende die Daten an die Netlify-Funktion
const response = await fetch('/.netlify/functions/content-submission', {
method: 'POST',
body: JSON.stringify(formData),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Submission failed");
}
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Submission failed');
}
// Bei erfolgreicher Übermittlung
form.reset();
successMessage.style.display = "block";
// Bei erfolgreicher Übermittlung
form.reset();
successMessage.style.display = 'block';
// Track successful submission
if (typeof window.plausible === "function") {
window.plausible("content-success", {
props: {
type: contentType.value,
title: title.value,
email: email.value,
},
});
}
// Track successful submission
if (typeof window.plausible === 'function') {
window.plausible('content-success', {
props: {
type: contentType.value,
title: title.value,
email: email.value,
},
});
}
// Erfolgs-Nachricht nach 8 Sekunden ausblenden
setTimeout(() => {
successMessage.style.display = "none";
}, 8000);
} catch (error: unknown) {
// Track error
if (typeof window.plausible === "function") {
window.plausible("content-error", {
props: {
type: contentType.value,
title: title.value,
email: email.value,
error: error instanceof Error ? error.message : String(error),
},
});
}
// Erfolgs-Nachricht nach 8 Sekunden ausblenden
setTimeout(() => {
successMessage.style.display = 'none';
}, 8000);
} catch (error: unknown) {
// Track error
if (typeof window.plausible === 'function') {
window.plausible('content-error', {
props: {
type: contentType.value,
title: title.value,
email: email.value,
error: error instanceof Error ? error.message : String(error),
},
});
}
// Fehlermeldung anzeigen
alert(
"Es gab einen Fehler bei der Übermittlung. Bitte versuche es später erneut."
);
}
});
});
// Fehlermeldung anzeigen
alert('Es gab einen Fehler bei der Übermittlung. Bitte versuche es später erneut.');
}
});
});
</script>

View file

@ -1,108 +1,122 @@
---
// Component for tracking content interactions
interface Props {
contentType: 'tutorial' | 'project' | 'tool' | 'model' | 'mission' | 'vision';
contentId?: string;
contentTitle?: string;
contentType: 'tutorial' | 'project' | 'tool' | 'model' | 'mission' | 'vision';
contentId?: string;
contentTitle?: string;
}
const { contentType, contentId, contentTitle } = Astro.props;
---
<script define:vars={{ contentType, contentId, contentTitle }}>
import { trackEvent, EVENTS } from '../scripts/analytics';
// Track page view for content
document.addEventListener('DOMContentLoaded', () => {
const eventMap = {
'tutorial': EVENTS.TUTORIAL_VIEW,
'project': EVENTS.PROJECT_VIEW,
'tool': EVENTS.TOOL_VIEW,
'model': EVENTS.MODEL_VIEW,
'mission': EVENTS.MISSION_VIEW,
'vision': 'vision-view'
};
const eventName = eventMap[contentType];
if (eventName) {
trackEvent(eventName, {
id: contentId || 'unknown',
title: contentTitle || document.title,
language: document.documentElement.lang
});
}
// Track video plays
document.querySelectorAll('iframe[src*="youtube"], iframe[src*="vimeo"]').forEach(video => {
// Create intersection observer to track when video comes into view
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const src = (entry.target as HTMLIFrameElement).src;
trackEvent(EVENTS.VIDEO_PLAY, {
source: src.includes('youtube') ? 'youtube' : 'vimeo',
url: src,
contentType,
contentId
});
observer.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
observer.observe(video);
});
// Track Figma embeds
document.querySelectorAll('iframe[src*="figma.com"]').forEach(embed => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
trackEvent(EVENTS.FIGMA_EMBED_VIEW, {
url: (entry.target as HTMLIFrameElement).src,
contentType,
contentId
});
observer.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
observer.observe(embed);
});
// Track code copy (if there are code blocks with copy buttons)
document.querySelectorAll('.copy-code-button, [data-copy]').forEach(button => {
button.addEventListener('click', () => {
trackEvent(EVENTS.CODE_COPY, {
contentType,
contentId,
location: window.location.pathname
});
});
});
// Track card clicks on listing pages
document.querySelectorAll('.tutorial-card, .project-card, .tool-card, .model-card, .mission-card').forEach(card => {
card.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const link = target.querySelector('a');
const title = target.querySelector('h3, h4')?.textContent?.trim() || '';
if (link) {
const cardType = target.className.includes('tutorial') ? 'tutorial' :
target.className.includes('project') ? 'project' :
target.className.includes('tool') ? 'tool' :
target.className.includes('model') ? 'model' :
target.className.includes('mission') ? 'mission' : 'unknown';
trackEvent(`${cardType}-card-click`, {
title,
destination: link.href,
fromPage: window.location.pathname
});
}
});
});
});
</script>
import { trackEvent, EVENTS } from '../scripts/analytics';
// Track page view for content
document.addEventListener('DOMContentLoaded', () => {
const eventMap = {
tutorial: EVENTS.TUTORIAL_VIEW,
project: EVENTS.PROJECT_VIEW,
tool: EVENTS.TOOL_VIEW,
model: EVENTS.MODEL_VIEW,
mission: EVENTS.MISSION_VIEW,
vision: 'vision-view',
};
const eventName = eventMap[contentType];
if (eventName) {
trackEvent(eventName, {
id: contentId || 'unknown',
title: contentTitle || document.title,
language: document.documentElement.lang,
});
}
// Track video plays
document.querySelectorAll('iframe[src*="youtube"], iframe[src*="vimeo"]').forEach((video) => {
// Create intersection observer to track when video comes into view
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const src = (entry.target as HTMLIFrameElement).src;
trackEvent(EVENTS.VIDEO_PLAY, {
source: src.includes('youtube') ? 'youtube' : 'vimeo',
url: src,
contentType,
contentId,
});
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.5 }
);
observer.observe(video);
});
// Track Figma embeds
document.querySelectorAll('iframe[src*="figma.com"]').forEach((embed) => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
trackEvent(EVENTS.FIGMA_EMBED_VIEW, {
url: (entry.target as HTMLIFrameElement).src,
contentType,
contentId,
});
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.5 }
);
observer.observe(embed);
});
// Track code copy (if there are code blocks with copy buttons)
document.querySelectorAll('.copy-code-button, [data-copy]').forEach((button) => {
button.addEventListener('click', () => {
trackEvent(EVENTS.CODE_COPY, {
contentType,
contentId,
location: window.location.pathname,
});
});
});
// Track card clicks on listing pages
document
.querySelectorAll('.tutorial-card, .project-card, .tool-card, .model-card, .mission-card')
.forEach((card) => {
card.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const link = target.querySelector('a');
const title = target.querySelector('h3, h4')?.textContent?.trim() || '';
if (link) {
const cardType = target.className.includes('tutorial')
? 'tutorial'
: target.className.includes('project')
? 'project'
: target.className.includes('tool')
? 'tool'
: target.className.includes('model')
? 'model'
: target.className.includes('mission')
? 'mission'
: 'unknown';
trackEvent(`${cardType}-card-click`, {
title,
destination: link.href,
fromPage: window.location.pathname,
});
}
});
});
});
</script>

View file

@ -1,61 +1,75 @@
---
interface Props {
figmaUrl: string;
title?: string;
height?: string;
figmaUrl: string;
title?: string;
height?: string;
}
const { figmaUrl, title = "Figma Design", height = "450px" } = Astro.props;
const { figmaUrl, title = 'Figma Design', height = '450px' } = Astro.props;
---
<div class="figma-container">
<h3 class="figma-title">{title}</h3>
<a href={figmaUrl} target="_blank" rel="noopener noreferrer" class="figma-button">
<svg width="20" height="20" viewBox="0 0 38 57" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 28.5C19 25.9804 20.0009 23.5641 21.7825 21.7825C23.5641 20.0009 25.9804 19 28.5 19C31.0196 19 33.4359 20.0009 35.2175 21.7825C36.9991 23.5641 38 25.9804 38 28.5C38 31.0196 36.9991 33.4359 35.2175 35.2175C33.4359 36.9991 31.0196 38 28.5 38C25.9804 38 23.5641 36.9991 21.7825 35.2175C20.0009 33.4359 19 31.0196 19 28.5Z" fill="#1ABCFE"/>
<path d="M0 47.5C0 44.9804 1.00089 42.5641 2.78249 40.7825C4.56408 39.0009 6.98044 38 9.5 38H19V47.5C19 50.0196 17.9991 52.4359 16.2175 54.2175C14.4359 55.9991 12.0196 57 9.5 57C6.98044 57 4.56408 55.9991 2.78249 54.2175C1.00089 52.4359 0 50.0196 0 47.5Z" fill="#0ACF83"/>
<path d="M19 0V19H28.5C31.0196 19 33.4359 17.9991 35.2175 16.2175C36.9991 14.4359 38 12.0196 38 9.5C38 6.98044 36.9991 4.56408 35.2175 2.78249C33.4359 1.00089 31.0196 0 28.5 0H19Z" fill="#FF7262"/>
<path d="M0 9.5C0 12.0196 1.00089 14.4359 2.78249 16.2175C4.56408 17.9991 6.98044 19 9.5 19H19V0H9.5C6.98044 0 4.56408 1.00089 2.78249 2.78249C1.00089 4.56408 0 6.98044 0 9.5Z" fill="#F24E1E"/>
<path d="M0 28.5C0 31.0196 1.00089 33.4359 2.78249 35.2175C4.56408 36.9991 6.98044 38 9.5 38H19V19H9.5C6.98044 19 4.56408 20.0009 2.78249 21.7825C1.00089 23.5641 0 25.9804 0 28.5Z" fill="#A259FF"/>
</svg>
Open in Figma
</a>
<h3 class="figma-title">{title}</h3>
<a href={figmaUrl} target="_blank" rel="noopener noreferrer" class="figma-button">
<svg width="20" height="20" viewBox="0 0 38 57" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M19 28.5C19 25.9804 20.0009 23.5641 21.7825 21.7825C23.5641 20.0009 25.9804 19 28.5 19C31.0196 19 33.4359 20.0009 35.2175 21.7825C36.9991 23.5641 38 25.9804 38 28.5C38 31.0196 36.9991 33.4359 35.2175 35.2175C33.4359 36.9991 31.0196 38 28.5 38C25.9804 38 23.5641 36.9991 21.7825 35.2175C20.0009 33.4359 19 31.0196 19 28.5Z"
fill="#1ABCFE"></path>
<path
d="M0 47.5C0 44.9804 1.00089 42.5641 2.78249 40.7825C4.56408 39.0009 6.98044 38 9.5 38H19V47.5C19 50.0196 17.9991 52.4359 16.2175 54.2175C14.4359 55.9991 12.0196 57 9.5 57C6.98044 57 4.56408 55.9991 2.78249 54.2175C1.00089 52.4359 0 50.0196 0 47.5Z"
fill="#0ACF83"></path>
<path
d="M19 0V19H28.5C31.0196 19 33.4359 17.9991 35.2175 16.2175C36.9991 14.4359 38 12.0196 38 9.5C38 6.98044 36.9991 4.56408 35.2175 2.78249C33.4359 1.00089 31.0196 0 28.5 0H19Z"
fill="#FF7262"></path>
<path
d="M0 9.5C0 12.0196 1.00089 14.4359 2.78249 16.2175C4.56408 17.9991 6.98044 19 9.5 19H19V0H9.5C6.98044 0 4.56408 1.00089 2.78249 2.78249C1.00089 4.56408 0 6.98044 0 9.5Z"
fill="#F24E1E"></path>
<path
d="M0 28.5C0 31.0196 1.00089 33.4359 2.78249 35.2175C4.56408 36.9991 6.98044 38 9.5 38H19V19H9.5C6.98044 19 4.56408 20.0009 2.78249 21.7825C1.00089 23.5641 0 25.9804 0 28.5Z"
fill="#A259FF"></path>
</svg>
Open in Figma
</a>
</div>
<style define:vars={{ height }}>
.figma-container {
margin: 2.5rem 0;
text-align: center;
}
.figma-title {
font-size: 1.25rem;
margin-bottom: 1rem;
color: var(--text-color);
}
.figma-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background-color: #0acf83;
color: white;
border-radius: 0.5rem;
font-weight: 600;
text-decoration: none;
transition: transform 0.2s, background-color 0.2s;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.figma-button:hover {
background-color: #09b975;
transform: translateY(-2px);
}
.figma-button svg {
width: 20px;
height: 20px;
}
</style>
.figma-container {
margin: 2.5rem 0;
text-align: center;
}
.figma-title {
font-size: 1.25rem;
margin-bottom: 1rem;
color: var(--text-color);
}
.figma-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background-color: #0acf83;
color: white;
border-radius: 0.5rem;
font-weight: 600;
text-decoration: none;
transition:
transform 0.2s,
background-color 0.2s;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.figma-button:hover {
background-color: #09b975;
transform: translateY(-2px);
}
.figma-button svg {
width: 20px;
height: 20px;
}
</style>

View file

@ -1,149 +1,176 @@
---
interface Props {
title: string;
description?: string;
image?: string;
imageAlt?: string;
title: string;
description?: string;
image?: string;
imageAlt?: string;
}
const {
title,
description,
image,
imageAlt = "Hero image"
} = Astro.props;
const { title, description, image, imageAlt = 'Hero image' } = Astro.props;
---
<div class="hero-section">
<div class="hero-text">
<svg width="60" height="60" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" class="hero-logo-svg" style="display:block;margin:0 auto 2rem auto;"><rect x="0.666667" y="7.33334" width="10.6667" height="2.66667" stroke="currentColor" stroke-width="0.8" fill="none"/><rect x="2" y="4.66666" width="8" height="2.66667" stroke="currentColor" stroke-width="0.8" fill="none"/><rect x="3.33333" y="2" width="5.33333" height="2.66667" stroke="currentColor" stroke-width="0.8" fill="none"/></svg>
<h1>{title}</h1>
{description && <p class="hero-description">{description}</p>}
<slot name="content" />
</div>
{image && (
<div class="hero-image">
<img src={image} alt={imageAlt} />
</div>
)}
<div class="hero-text">
<svg
width="60"
height="60"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="hero-logo-svg"
style="display:block;margin:0 auto 2rem auto;"
><rect
x="0.666667"
y="7.33334"
width="10.6667"
height="2.66667"
stroke="currentColor"
stroke-width="0.8"
fill="none"></rect><rect
x="2"
y="4.66666"
width="8"
height="2.66667"
stroke="currentColor"
stroke-width="0.8"
fill="none"></rect><rect
x="3.33333"
y="2"
width="5.33333"
height="2.66667"
stroke="currentColor"
stroke-width="0.8"
fill="none"></rect></svg
>
<h1>{title}</h1>
{description && <p class="hero-description">{description}</p>}
<slot name="content" />
</div>
{
image && (
<div class="hero-image">
<img src={image} alt={imageAlt} />
</div>
)
}
</div>
<style is:global>
/* Hero Section Styles */
.hero-section {
display: flex;
align-items: center;
justify-content: space-between;
margin: 120px 0 4rem;
padding: 0.5rem 0 0;
gap: 4rem;
}
/* Hero Section Styles */
.hero-section {
display: flex;
align-items: center;
justify-content: space-between;
margin: 120px 0 4rem;
padding: 0.5rem 0 0;
gap: 4rem;
}
.hero-section .hero-text {
flex: 1;
text-align: center;
}
.hero-logo {
margin-bottom: 2rem;
margin-top: 0;
display: block;
margin-left: auto;
margin-right: auto;
width: 60px;
height: 60px;
transition: filter 0.2s;
}
@media (prefers-color-scheme: dark) {
.hero-logo {
filter: invert(1) brightness(2);
}
}
.hero-section .hero-text {
flex: 1;
text-align: center;
}
.hero-logo {
margin-bottom: 2rem;
margin-top: 0;
display: block;
margin-left: auto;
margin-right: auto;
width: 60px;
height: 60px;
transition: filter 0.2s;
}
@media (prefers-color-scheme: dark) {
.hero-logo {
filter: invert(1) brightness(2);
}
}
.hero-section h1 {
font-size: 2.5rem;
line-height: 1.2;
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(90deg, var(--accent-color), #fb923c);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.hero-section h1 {
font-size: 2.5rem;
line-height: 1.2;
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(90deg, var(--accent-color), #fb923c);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.hero-section .hero-description {
font-size: 1.25rem;
color: var(--text-muted);
margin-bottom: 1.5rem;
line-height: 1.6;
}
.hero-section .hero-description {
font-size: 1.25rem;
color: var(--text-muted);
margin-bottom: 1.5rem;
line-height: 1.6;
}
.hero-section .hero-image {
flex: 0 0 400px;
height: 400px;
border-radius: 1.5rem;
overflow: hidden;
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.hero-section .hero-image {
flex: 0 0 400px;
height: 400px;
border-radius: 1.5rem;
overflow: hidden;
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.hero-section .hero-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.hero-section .hero-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.hero-section .hero-image img:hover {
transform: scale(1.05);
}
@media (max-width: 1024px) {
.hero-section {
gap: 2rem;
}
.hero-section .hero-image img:hover {
transform: scale(1.05);
}
.hero-section .hero-image {
flex: 0 0 250px;
height: auto;
aspect-ratio: 1/1;
}
@media (max-width: 1024px) {
.hero-section {
gap: 2rem;
}
.hero-section h1 {
font-size: 2.25rem;
}
.hero-section .hero-image {
flex: 0 0 250px;
height: auto;
aspect-ratio: 1/1;
}
.hero-section .hero-description {
font-size: 1.15rem;
}
}
.hero-section h1 {
font-size: 2.25rem;
}
@media (max-width: 768px) {
.hero-section {
flex-direction: column-reverse;
margin: 100px 0 3.5rem;
gap: 2rem;
}
.hero-section .hero-description {
font-size: 1.15rem;
}
}
.hero-section .hero-text {
width: 100%;
}
@media (max-width: 768px) {
.hero-section {
flex-direction: column-reverse;
margin: 100px 0 3.5rem;
gap: 2rem;
}
.hero-section .hero-text h1 {
font-size: 2rem;
}
.hero-section .hero-text {
width: 100%;
}
.hero-section .hero-description {
font-size: 1.1rem;
}
.hero-section .hero-text h1 {
font-size: 2rem;
}
.hero-section .hero-image {
width: 100%;
flex: 0 0 auto;
aspect-ratio: 3/2;
height: auto;
}
}
</style>
.hero-section .hero-description {
font-size: 1.1rem;
}
.hero-section .hero-image {
width: 100%;
flex: 0 0 auto;
aspect-ratio: 3/2;
height: auto;
}
}
</style>

View file

@ -6,351 +6,379 @@ const lang = getLangFromUrl(Astro.url);
const currentPath = Astro.url.pathname;
const getPathInLang = (targetLang: string) => {
// Split URL path into segments
const segments = currentPath.split('/').filter(Boolean);
// First segment is always the language code
if (segments.length === 0) {
// Homepage case
return `/${targetLang}`;
} else if (Object.keys(languages).includes(segments[0])) {
// Normal case: first segment is a language code
segments[0] = targetLang;
} else {
// Fallback for any non-language URLs
segments.unshift(targetLang);
}
return `/${segments.join('/')}`;
// Split URL path into segments
const segments = currentPath.split('/').filter(Boolean);
// First segment is always the language code
if (segments.length === 0) {
// Homepage case
return `/${targetLang}`;
} else if (Object.keys(languages).includes(segments[0])) {
// Normal case: first segment is a language code
segments[0] = targetLang;
} else {
// Fallback for any non-language URLs
segments.unshift(targetLang);
}
return `/${segments.join('/')}`;
};
// Props for the component
interface Props {
dropdownStyle?: boolean;
dropdownStyle?: boolean;
}
const { dropdownStyle = false } = Astro.props;
---
{
dropdownStyle ? (
<div class="language-dropdown">
<button class="dropdown-button">
<span class="current-lang">{languages[lang]}</span>
<svg class="dropdown-arrow" xmlns="http://www.w3.org/2000/svg" width="12" height="6" viewBox="0 0 12 6" fill="none">
<path d="M1 1L6 5L11 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<div class="dropdown-menu">
{Object.entries(languages).map(([code, name]) => (
<a
href={getPathInLang(code)}
class={lang === code ? 'active' : ''}
hreflang={code}
lang={code}
>
{name}
</a>
))}
</div>
</div>
) : (
<div class="language-selector">
{Object.entries(languages).map(([code, name]) => (
<a
href={getPathInLang(code)}
class={lang === code ? 'active' : ''}
hreflang={code}
lang={code}
>
{name}
</a>
))}
</div>
)
dropdownStyle ? (
<div class="language-dropdown">
<button class="dropdown-button">
<span class="current-lang">{languages[lang]}</span>
<svg
class="dropdown-arrow"
xmlns="http://www.w3.org/2000/svg"
width="12"
height="6"
viewBox="0 0 12 6"
fill="none"
>
<path
d="M1 1L6 5L11 1"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<div class="dropdown-menu">
{Object.entries(languages).map(([code, name]) => (
<a
href={getPathInLang(code)}
class={lang === code ? 'active' : ''}
hreflang={code}
lang={code}
>
{name}
</a>
))}
</div>
</div>
) : (
<div class="language-selector">
{Object.entries(languages).map(([code, name]) => (
<a
href={getPathInLang(code)}
class={lang === code ? 'active' : ''}
hreflang={code}
lang={code}
>
{name}
</a>
))}
</div>
)
}
<style>
/* Shared styles */
.language-selector,
.language-dropdown {
display: flex;
align-items: center;
}
.language-selector a,
.dropdown-menu a {
padding: 0.35rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
color: var(--text-color);
text-decoration: none;
transition: background-color 0.2s, color 0.2s;
}
.language-selector a:hover,
.dropdown-menu a:hover {
background-color: var(--hover-bg);
}
.language-selector a.active,
.dropdown-menu a.active {
font-weight: bold;
background-color: var(--hover-bg);
}
/* Horizontal selector styles */
.language-selector {
gap: 0.25rem;
}
/* Dropdown styles */
.language-dropdown {
position: relative;
z-index: 150;
}
.dropdown-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: none;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
font-size: 0.875rem;
color: var(--text-color);
cursor: pointer;
transition: background-color 0.2s;
}
/* Fix text color in dark mode */
:global(:root.dark) .dropdown-button {
color: #f9fafb;
}
.dropdown-button:hover {
background-color: var(--hover-bg);
}
.dropdown-arrow {
transition: transform 0.2s;
}
.dropdown-menu {
position: absolute;
top: calc(100% + 0.25rem);
right: 0;
display: flex;
flex-direction: column;
min-width: 120px;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 0.375rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
opacity: 0;
transform: translateY(-10px);
visibility: hidden;
transition: opacity 0.2s, transform 0.2s, visibility 0s 0.2s;
z-index: 200; /* Higher z-index to ensure it appears above other elements */
overflow: hidden;
}
/* Initial state for dropdowns */
.dropdown-menu {
opacity: 0;
visibility: hidden;
}
.dropdown-menu.show {
opacity: 1;
transform: translateY(0);
visibility: visible;
transition: opacity 0.2s, transform 0.2s;
}
.dropdown-menu a {
padding: 0.75rem 1rem;
border-radius: 0;
width: 100%;
text-align: left;
}
/* Fix link color in dark mode */
:global(:root.dark) .dropdown-menu a {
color: #f9fafb;
}
/* Only show dropdown on desktop hover, not mobile */
@media (min-width: 769px) {
.language-dropdown:hover .dropdown-menu,
.dropdown-button:focus-within .dropdown-menu {
opacity: 1;
transform: translateY(0);
visibility: visible;
transition: opacity 0.2s, transform 0.2s;
}
.language-dropdown:hover .dropdown-arrow,
.dropdown-button:focus-within .dropdown-arrow,
.dropdown-button[aria-expanded="true"] .dropdown-arrow {
transform: rotate(180deg);
}
}
/* Show dropdown in mobile when active */
.dropdown-menu.show {
opacity: 1;
transform: translateY(0);
visibility: visible;
transition: opacity 0.2s, transform 0.2s;
}
/* Standard animation for all dropdowns */
.mobile-menu-controls .dropdown-menu.show {
transform: none !important;
opacity: 1;
visibility: visible;
}
/* Mobile styles */
@media (max-width: 768px) {
.language-selector {
flex-direction: row;
width: 100%;
justify-content: flex-start;
gap: 1rem;
}
.language-selector a {
font-size: 1rem;
padding: 0.5rem 0.75rem;
}
.language-dropdown {
width: 100%;
position: relative; /* Keep relative positioning for proper dropdown placement */
}
.dropdown-button {
width: 100%;
justify-content: space-between;
padding: 1rem 1.25rem;
font-size: 1.1rem;
text-align: center;
}
/* Important: absolute positioning for mobile dropdown */
.dropdown-menu {
position: absolute;
width: 100%;
left: 0;
top: 100%; /* Show below the button */
z-index: 2000; /* Extra high z-index */
max-height: calc(100vh - 200px);
overflow-y: auto;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* Style for mobile menu dropdown - directly attached to button */
.mobile-menu-controls .dropdown-menu {
position: absolute;
width: 100%; /* Match button width */
left: 0;
top: 100%; /* Position below the button */
bottom: auto;
margin-top: 2px; /* Small gap */
max-height: 180px;
border-radius: 8px;
z-index: 2000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background-color: var(--card-bg);
}
.dropdown-menu a {
padding: 1rem;
font-size: 1rem;
}
}
/* Shared styles */
.language-selector,
.language-dropdown {
display: flex;
align-items: center;
}
.language-selector a,
.dropdown-menu a {
padding: 0.35rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
color: var(--text-color);
text-decoration: none;
transition:
background-color 0.2s,
color 0.2s;
}
.language-selector a:hover,
.dropdown-menu a:hover {
background-color: var(--hover-bg);
}
.language-selector a.active,
.dropdown-menu a.active {
font-weight: bold;
background-color: var(--hover-bg);
}
/* Horizontal selector styles */
.language-selector {
gap: 0.25rem;
}
/* Dropdown styles */
.language-dropdown {
position: relative;
z-index: 150;
}
.dropdown-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: none;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
font-size: 0.875rem;
color: var(--text-color);
cursor: pointer;
transition: background-color 0.2s;
}
/* Fix text color in dark mode */
:global(:root.dark) .dropdown-button {
color: #f9fafb;
}
.dropdown-button:hover {
background-color: var(--hover-bg);
}
.dropdown-arrow {
transition: transform 0.2s;
}
.dropdown-menu {
position: absolute;
top: calc(100% + 0.25rem);
right: 0;
display: flex;
flex-direction: column;
min-width: 120px;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 0.375rem;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
opacity: 0;
transform: translateY(-10px);
visibility: hidden;
transition:
opacity 0.2s,
transform 0.2s,
visibility 0s 0.2s;
z-index: 200; /* Higher z-index to ensure it appears above other elements */
overflow: hidden;
}
/* Initial state for dropdowns */
.dropdown-menu {
opacity: 0;
visibility: hidden;
}
.dropdown-menu.show {
opacity: 1;
transform: translateY(0);
visibility: visible;
transition:
opacity 0.2s,
transform 0.2s;
}
.dropdown-menu a {
padding: 0.75rem 1rem;
border-radius: 0;
width: 100%;
text-align: left;
}
/* Fix link color in dark mode */
:global(:root.dark) .dropdown-menu a {
color: #f9fafb;
}
/* Only show dropdown on desktop hover, not mobile */
@media (min-width: 769px) {
.language-dropdown:hover .dropdown-menu,
.dropdown-button:focus-within .dropdown-menu {
opacity: 1;
transform: translateY(0);
visibility: visible;
transition:
opacity 0.2s,
transform 0.2s;
}
.language-dropdown:hover .dropdown-arrow,
.dropdown-button:focus-within .dropdown-arrow,
.dropdown-button[aria-expanded='true'] .dropdown-arrow {
transform: rotate(180deg);
}
}
/* Show dropdown in mobile when active */
.dropdown-menu.show {
opacity: 1;
transform: translateY(0);
visibility: visible;
transition:
opacity 0.2s,
transform 0.2s;
}
/* Standard animation for all dropdowns */
.mobile-menu-controls .dropdown-menu.show {
transform: none !important;
opacity: 1;
visibility: visible;
}
/* Mobile styles */
@media (max-width: 768px) {
.language-selector {
flex-direction: row;
width: 100%;
justify-content: flex-start;
gap: 1rem;
}
.language-selector a {
font-size: 1rem;
padding: 0.5rem 0.75rem;
}
.language-dropdown {
width: 100%;
position: relative; /* Keep relative positioning for proper dropdown placement */
}
.dropdown-button {
width: 100%;
justify-content: space-between;
padding: 1rem 1.25rem;
font-size: 1.1rem;
text-align: center;
}
/* Important: absolute positioning for mobile dropdown */
.dropdown-menu {
position: absolute;
width: 100%;
left: 0;
top: 100%; /* Show below the button */
z-index: 2000; /* Extra high z-index */
max-height: calc(100vh - 200px);
overflow-y: auto;
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* Style for mobile menu dropdown - directly attached to button */
.mobile-menu-controls .dropdown-menu {
position: absolute;
width: 100%; /* Match button width */
left: 0;
top: 100%; /* Position below the button */
bottom: auto;
margin-top: 2px; /* Small gap */
max-height: 180px;
border-radius: 8px;
z-index: 2000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background-color: var(--card-bg);
}
.dropdown-menu a {
padding: 1rem;
font-size: 1rem;
}
}
</style>
<script>
// For accessibility - allow keyboard navigation
document.addEventListener('DOMContentLoaded', () => {
// Handle all dropdown buttons in the document, not just the first one
const dropdownButtons = document.querySelectorAll('.dropdown-button');
dropdownButtons.forEach(dropdownButton => {
if (!dropdownButton) return;
// Open/close dropdown on click
dropdownButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const isExpanded = dropdownButton.getAttribute('aria-expanded') === 'true';
dropdownButton.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
const menu = dropdownButton.nextElementSibling;
if (menu && menu.classList.contains('dropdown-menu')) {
menu.classList.toggle('show');
if (!isExpanded) {
// Focus the first menu item
const firstItem = menu.querySelector('a');
if (firstItem) firstItem.focus();
}
}
});
dropdownButton.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
dropdownButton.click();
}
// Close when escape is pressed
if (e.key === 'Escape') {
const menu = dropdownButton.nextElementSibling;
if (menu && menu.classList.contains('show')) {
menu.classList.remove('show');
dropdownButton.setAttribute('aria-expanded', 'false');
dropdownButton.focus();
}
}
});
// Initialize ARIA attributes
dropdownButton.setAttribute('aria-haspopup', 'true');
dropdownButton.setAttribute('aria-expanded', 'false');
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
dropdownButtons.forEach(dropdownButton => {
if (!dropdownButton.contains(e.target)) {
const menu = dropdownButton.nextElementSibling;
if (menu && menu.classList.contains('show')) {
menu.classList.remove('show');
dropdownButton.setAttribute('aria-expanded', 'false');
}
}
});
});
// Add click listener to all dropdown menu items
const dropdownMenuItems = document.querySelectorAll('.dropdown-menu a');
dropdownMenuItems.forEach(item => {
item.addEventListener('click', (e) => {
const menu = item.closest('.dropdown-menu');
if (menu) {
menu.classList.remove('show');
const button = menu.previousElementSibling;
if (button) {
button.setAttribute('aria-expanded', 'false');
}
}
});
});
});
</script>
// For accessibility - allow keyboard navigation
document.addEventListener('DOMContentLoaded', () => {
// Handle all dropdown buttons in the document, not just the first one
const dropdownButtons = document.querySelectorAll('.dropdown-button');
dropdownButtons.forEach((dropdownButton) => {
if (!dropdownButton) return;
// Open/close dropdown on click
dropdownButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const isExpanded = dropdownButton.getAttribute('aria-expanded') === 'true';
dropdownButton.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
const menu = dropdownButton.nextElementSibling;
if (menu && menu.classList.contains('dropdown-menu')) {
menu.classList.toggle('show');
if (!isExpanded) {
// Focus the first menu item
const firstItem = menu.querySelector('a');
if (firstItem) firstItem.focus();
}
}
});
dropdownButton.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
dropdownButton.click();
}
// Close when escape is pressed
if (e.key === 'Escape') {
const menu = dropdownButton.nextElementSibling;
if (menu && menu.classList.contains('show')) {
menu.classList.remove('show');
dropdownButton.setAttribute('aria-expanded', 'false');
dropdownButton.focus();
}
}
});
// Initialize ARIA attributes
dropdownButton.setAttribute('aria-haspopup', 'true');
dropdownButton.setAttribute('aria-expanded', 'false');
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
dropdownButtons.forEach((dropdownButton) => {
if (!dropdownButton.contains(e.target)) {
const menu = dropdownButton.nextElementSibling;
if (menu && menu.classList.contains('show')) {
menu.classList.remove('show');
dropdownButton.setAttribute('aria-expanded', 'false');
}
}
});
});
// Add click listener to all dropdown menu items
const dropdownMenuItems = document.querySelectorAll('.dropdown-menu a');
dropdownMenuItems.forEach((item) => {
item.addEventListener('click', (e) => {
const menu = item.closest('.dropdown-menu');
if (menu) {
menu.classList.remove('show');
const button = menu.previousElementSibling;
if (button) {
button.setAttribute('aria-expanded', 'false');
}
}
});
});
});
</script>

View file

@ -1,385 +1,381 @@
---
interface Props {
height?: string;
height?: string;
}
const { height = "85vh" } = Astro.props;
const { height = '85vh' } = Astro.props;
---
<div class="matrix-hero" style={`height: ${height};`}>
<div class="matrix-canvas-container">
<canvas id="matrixCanvas"></canvas>
<div class="fade-bottom"></div>
</div>
<div class="content">
<div class="title-container">
<p class="hero-subtitle">Custom build future</p>
<button id="scroll-down" class="scroll-down-btn" aria-label="Scroll down">
<svg
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="chevron-down"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
</div>
<div class="matrix-canvas-container">
<canvas id="matrixCanvas"></canvas>
<div class="fade-bottom"></div>
</div>
<div class="content">
<div class="title-container">
<p class="hero-subtitle">Custom build future</p>
<button id="scroll-down" class="scroll-down-btn" aria-label="Scroll down">
<svg
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="chevron-down"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
</div>
</div>
<style>
.matrix-hero {
position: relative;
width: 100vw;
margin-left: calc(-50vw + 50%);
margin-top: -69px; /* Match the navigation height to extend behind it */
padding-top: 69px; /* Add padding to ensure content starts below nav */
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
background-color: #000; /* Ensure black background */
}
.matrix-hero {
position: relative;
width: 100vw;
margin-left: calc(-50vw + 50%);
margin-top: -69px; /* Match the navigation height to extend behind it */
padding-top: 69px; /* Add padding to ensure content starts below nav */
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
background-color: #000; /* Ensure black background */
}
.matrix-canvas-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2; /* Higher z-index to place it above content */
pointer-events: auto; /* Allow interaction with matrix elements */
}
.matrix-canvas-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2; /* Higher z-index to place it above content */
pointer-events: auto; /* Allow interaction with matrix elements */
}
.fade-bottom {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 150px;
background: linear-gradient(
to top,
var(--background-color) 0%,
rgba(0, 0, 0, 0) 100%
);
z-index: 3; /* Above matrix canvas */
}
.fade-bottom {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 150px;
background: linear-gradient(to top, var(--background-color) 0%, rgba(0, 0, 0, 0) 100%);
z-index: 3; /* Above matrix canvas */
}
canvas {
width: 100%;
height: 100%;
display: block;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
.content {
position: relative;
z-index: 2; /* Same z-index as the matrix canvas to interleave */
text-align: center;
color: white;
padding: 0 1rem;
transform: translateY(-50px); /* Move content up */
}
.content {
position: relative;
z-index: 2; /* Same z-index as the matrix canvas to interleave */
text-align: center;
color: white;
padding: 0 1rem;
transform: translateY(-50px); /* Move content up */
}
.title-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.title-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.hero-subtitle {
font-size: 5rem;
font-weight: 600;
letter-spacing: 0.05em;
color: rgba(255, 255, 255, 0.95);
text-align: center;
text-transform: uppercase;
line-height: 1.2;
max-width: 80%;
text-shadow:
0 0 20px rgba(255, 255, 255, 0.25),
0 0 35px rgba(255, 165, 0, 0.18),
0 0 60px rgba(255, 255, 255, 0.12),
0 0 100px rgba(255, 255, 255, 0.08);
margin-bottom: 0.5rem;
}
.hero-subtitle {
font-size: 5rem;
font-weight: 600;
letter-spacing: 0.05em;
color: rgba(255, 255, 255, 0.95);
text-align: center;
text-transform: uppercase;
line-height: 1.2;
max-width: 80%;
text-shadow:
0 0 20px rgba(255, 255, 255, 0.25),
0 0 35px rgba(255, 165, 0, 0.18),
0 0 60px rgba(255, 255, 255, 0.12),
0 0 100px rgba(255, 255, 255, 0.08);
margin-bottom: 0.5rem;
}
.scroll-down-btn {
background: none;
border: none;
cursor: pointer;
padding: 0;
margin-top: 0.5rem;
color: rgba(255, 255, 255, 1);
transition: transform 0.3s ease;
}
.scroll-down-btn {
background: none;
border: none;
cursor: pointer;
padding: 0;
margin-top: 0.5rem;
color: rgba(255, 255, 255, 1);
transition: transform 0.3s ease;
}
.scroll-down-btn:hover {
transform: scale(1.1);
}
.scroll-down-btn:hover {
transform: scale(1.1);
}
.chevron-down {
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.3));
}
.chevron-down {
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.3));
}
.matrix-ball {
display: none; /* Hide the ball element instead of removing it completely */
}
.matrix-ball {
display: none; /* Hide the ball element instead of removing it completely */
}
@media (max-width: 768px) {
.matrix-hero {
margin-top: -57px; /* Adjust for smaller mobile nav height */
padding-top: 57px;
}
@media (max-width: 768px) {
.matrix-hero {
margin-top: -57px; /* Adjust for smaller mobile nav height */
padding-top: 57px;
}
.content {
transform: translateY(-30px); /* Less shift on mobile */
}
.content {
transform: translateY(-30px); /* Less shift on mobile */
}
.hero-subtitle {
font-size: 2.8rem;
max-width: 90%;
}
.hero-subtitle {
font-size: 2.8rem;
max-width: 90%;
}
.scroll-down-btn svg {
width: 48px;
height: 48px;
}
.scroll-down-btn svg {
width: 48px;
height: 48px;
}
.fade-bottom {
height: 100px;
}
}
.fade-bottom {
height: 100px;
}
}
</style>
<script defer>
// Defer the matrix animation initialization
function initMatrixAnimation() {
// Scroll to first card when chevron is clicked
const scrollBtn = document.getElementById("scroll-down");
if (scrollBtn) {
scrollBtn.addEventListener("click", () => {
// Find the first card section after the hero
const featureSection = document.querySelector(".feature-section");
if (featureSection && window.scrollToElement) {
// Use the global scroll helper
window.scrollToElement(featureSection, 20); // 20px additional offset
}
});
}
// Defer the matrix animation initialization
function initMatrixAnimation() {
// Scroll to first card when chevron is clicked
const scrollBtn = document.getElementById('scroll-down');
if (scrollBtn) {
scrollBtn.addEventListener('click', () => {
// Find the first card section after the hero
const featureSection = document.querySelector('.feature-section');
if (featureSection && window.scrollToElement) {
// Use the global scroll helper
window.scrollToElement(featureSection, 20); // 20px additional offset
}
});
}
// Matrix animation (optimized)
const canvas = document.getElementById("matrixCanvas");
if (!canvas) return;
// Matrix animation (optimized)
const canvas = document.getElementById('matrixCanvas');
if (!canvas) return;
const ctx = canvas.getContext("2d", { alpha: false }); // Optimize context by disabling alpha
if (!ctx) return;
const ctx = canvas.getContext('2d', { alpha: false }); // Optimize context by disabling alpha
if (!ctx) return;
// Make canvas full size of container but at lower resolution for performance
function resizeCanvas() {
const container = canvas.parentElement;
if (!container) return;
// Use a smaller canvas resolution for better performance
const scale = window.devicePixelRatio * 0.5; // Half the device pixel ratio
canvas.width = container.clientWidth * scale;
canvas.height = container.clientHeight * scale;
// Scale the context to make it look right
ctx.scale(scale, scale);
// Reset CSS dimensions
canvas.style.width = container.clientWidth + 'px';
canvas.style.height = container.clientHeight + 'px';
}
// Make canvas full size of container but at lower resolution for performance
function resizeCanvas() {
const container = canvas.parentElement;
if (!container) return;
// Initial setup
resizeCanvas();
const debouncedResize = debounce(resizeCanvas, 250);
window.addEventListener("resize", debouncedResize);
// Use a smaller canvas resolution for better performance
const scale = window.devicePixelRatio * 0.5; // Half the device pixel ratio
canvas.width = container.clientWidth * scale;
canvas.height = container.clientHeight * scale;
// Clear with solid black to start
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Scale the context to make it look right
ctx.scale(scale, scale);
// Pre-calculate some values for optimization
const codeSnippets = [
"const x = 0;",
"let y = true;",
"if (x > 0) {",
"function() {",
"export const",
"import { }",
];
// Pre-calculate colors for better performance
const colorPalette = [];
for (let i = 0; i < 20; i++) {
const hue = 20 + Math.floor(Math.random() * 20);
const saturation = 80 + Math.floor(Math.random() * 20);
const lightness = 50 + Math.floor(Math.random() * 20);
colorPalette.push(`hsl(${hue}, ${saturation}%, ${lightness}%)`);
}
// Reset CSS dimensions
canvas.style.width = container.clientWidth + 'px';
canvas.style.height = container.clientHeight + 'px';
}
// Matrix code effect - simplified for better performance
class Column {
constructor(x, fontSize, canvasHeight) {
this.x = x;
this.fontSize = fontSize;
this.canvasHeight = canvasHeight;
this.rises = [];
this.chars = [];
this.colorIndexes = [];
// Even slower speed for better performance
this.speed = 0.02 + Math.random() * 0.01;
// Initial setup
resizeCanvas();
const debouncedResize = debounce(resizeCanvas, 250);
window.addEventListener('resize', debouncedResize);
// Initialize with positions from bottom - use less characters
const snippet = codeSnippets[Math.floor(Math.random() * codeSnippets.length)];
for (let i = 0; i < snippet.length; i++) {
// Position strings at different positions below the canvas
const y = canvasHeight + Math.random() * 200;
this.rises.push(y);
this.chars.push(snippet[i]);
this.colorIndexes.push(Math.floor(Math.random() * colorPalette.length));
}
}
// Clear with solid black to start
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
draw(ctx) {
for (let i = 0; i < this.rises.length; i++) {
// Calculate positions for current character
const x = this.x + i * this.fontSize * 0.6;
const y = this.rises[i];
// Pre-calculate some values for optimization
const codeSnippets = [
'const x = 0;',
'let y = true;',
'if (x > 0) {',
'function() {',
'export const',
'import { }',
];
// Draw only if in visible area (performance optimization)
if (y > 0 && y < this.canvasHeight + 50) {
// Draw the current character
ctx.fillStyle = colorPalette[this.colorIndexes[i]];
ctx.font = `${this.fontSize}px 'Courier New', monospace`;
ctx.fillText(this.chars[i], x, y);
}
// Pre-calculate colors for better performance
const colorPalette = [];
for (let i = 0; i < 20; i++) {
const hue = 20 + Math.floor(Math.random() * 20);
const saturation = 80 + Math.floor(Math.random() * 20);
const lightness = 50 + Math.floor(Math.random() * 20);
colorPalette.push(`hsl(${hue}, ${saturation}%, ${lightness}%)`);
}
// Move the rise up slower
this.rises[i] -= this.fontSize * this.speed;
// Matrix code effect - simplified for better performance
class Column {
constructor(x, fontSize, canvasHeight) {
this.x = x;
this.fontSize = fontSize;
this.canvasHeight = canvasHeight;
this.rises = [];
this.chars = [];
this.colorIndexes = [];
// Even slower speed for better performance
this.speed = 0.02 + Math.random() * 0.01;
// Reset when off the top of the screen with random delay
if (this.rises[i] < -50 && Math.random() > 0.99) {
this.rises[i] = this.canvasHeight + this.fontSize * (10 + Math.random() * 20);
}
}
}
}
// Initialize with positions from bottom - use less characters
const snippet = codeSnippets[Math.floor(Math.random() * codeSnippets.length)];
for (let i = 0; i < snippet.length; i++) {
// Position strings at different positions below the canvas
const y = canvasHeight + Math.random() * 200;
this.rises.push(y);
this.chars.push(snippet[i]);
this.colorIndexes.push(Math.floor(Math.random() * colorPalette.length));
}
}
// Create columns of code - reduced number for better performance
let columns = [];
let animationFrameId;
let lastFrameTime = 0;
const targetFPS = 30; // Lower target FPS for better performance
const frameInterval = 1000 / targetFPS;
draw(ctx) {
for (let i = 0; i < this.rises.length; i++) {
// Calculate positions for current character
const x = this.x + i * this.fontSize * 0.6;
const y = this.rises[i];
function initColumns() {
columns = [];
const fontSize = canvas.width > 768 ? 16 : 12;
// Draw only if in visible area (performance optimization)
if (y > 0 && y < this.canvasHeight + 50) {
// Draw the current character
ctx.fillStyle = colorPalette[this.colorIndexes[i]];
ctx.font = `${this.fontSize}px 'Courier New', monospace`;
ctx.fillText(this.chars[i], x, y);
}
// Use much fewer columns for better performance
const maxColumns = canvas.width > 768 ? 15 : 8;
// Move the rise up slower
this.rises[i] -= this.fontSize * this.speed;
// Create columns at optimal positions
for (let i = 0; i < maxColumns; i++) {
const x = (i / maxColumns) * canvas.width;
columns.push(new Column(x, fontSize, canvas.height));
}
}
// Reset when off the top of the screen with random delay
if (this.rises[i] < -50 && Math.random() > 0.99) {
this.rises[i] = this.canvasHeight + this.fontSize * (10 + Math.random() * 20);
}
}
}
}
// Animation loop - optimized with frame limiting
function animate(currentTime) {
// Frame limiting for better performance
const elapsed = currentTime - lastFrameTime;
if (elapsed < frameInterval) {
animationFrameId = requestAnimationFrame(animate);
return;
}
lastFrameTime = currentTime - (elapsed % frameInterval);
// Create columns of code - reduced number for better performance
let columns = [];
let animationFrameId;
let lastFrameTime = 0;
const targetFPS = 30; // Lower target FPS for better performance
const frameInterval = 1000 / targetFPS;
// Use alpha for trail effect
ctx.fillStyle = "rgba(0, 0, 0, 0.08)"; // Slower fade for better effect
ctx.fillRect(0, 0, canvas.width, canvas.height);
function initColumns() {
columns = [];
const fontSize = canvas.width > 768 ? 16 : 12;
// Draw all columns
columns.forEach((column) => column.draw(ctx));
// Use much fewer columns for better performance
const maxColumns = canvas.width > 768 ? 15 : 8;
// Continue animation
animationFrameId = requestAnimationFrame(animate);
}
// Create columns at optimal positions
for (let i = 0; i < maxColumns; i++) {
const x = (i / maxColumns) * canvas.width;
columns.push(new Column(x, fontSize, canvas.height));
}
}
// Initialize columns after a short delay
setTimeout(() => {
initColumns();
lastFrameTime = performance.now();
animate(lastFrameTime);
}, 500); // 500ms delay to ensure other critical resources load first
// Animation loop - optimized with frame limiting
function animate(currentTime) {
// Frame limiting for better performance
const elapsed = currentTime - lastFrameTime;
if (elapsed < frameInterval) {
animationFrameId = requestAnimationFrame(animate);
return;
}
lastFrameTime = currentTime - (elapsed % frameInterval);
// Debounce function for resize event
function debounce(func, wait) {
let timeout;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
// Use alpha for trail effect
ctx.fillStyle = 'rgba(0, 0, 0, 0.08)'; // Slower fade for better effect
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Handle visibility change to pause animation when tab is not visible
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// Pause animation to save resources
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
} else if (!animationFrameId) {
// Resume animation
lastFrameTime = performance.now();
animationFrameId = requestAnimationFrame(animate);
}
});
// Draw all columns
columns.forEach((column) => column.draw(ctx));
// Reinitialize on resize, but debounced
window.addEventListener("resize", () => {
// Cancel existing animation to prevent memory leaks
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
// Continue animation
animationFrameId = requestAnimationFrame(animate);
}
// We already have a debounced resize for canvas dimensions
// Clear canvas
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Initialize columns after a short delay
setTimeout(() => {
initColumns();
lastFrameTime = performance.now();
animate(lastFrameTime);
}, 500); // 500ms delay to ensure other critical resources load first
// Reinitialize columns
initColumns();
lastFrameTime = performance.now();
animate(lastFrameTime);
});
}
// Debounce function for resize event
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
// Use requestIdleCallback or setTimeout to defer initialization
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
initMatrixAnimation();
});
} else {
// Fallback for browsers without requestIdleCallback
setTimeout(initMatrixAnimation, 1000); // Wait for 1 second after page load
}
// Handle visibility change to pause animation when tab is not visible
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// Pause animation to save resources
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
} else if (!animationFrameId) {
// Resume animation
lastFrameTime = performance.now();
animationFrameId = requestAnimationFrame(animate);
}
});
// Reinitialize on resize, but debounced
window.addEventListener('resize', () => {
// Cancel existing animation to prevent memory leaks
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
// We already have a debounced resize for canvas dimensions
// Clear canvas
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Reinitialize columns
initColumns();
lastFrameTime = performance.now();
animate(lastFrameTime);
});
}
// Use requestIdleCallback or setTimeout to defer initialization
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
initMatrixAnimation();
});
} else {
// Fallback for browsers without requestIdleCallback
setTimeout(initMatrixAnimation, 1000); // Wait for 1 second after page load
}
</script>

View file

@ -3,7 +3,7 @@ import { getLangFromUrl, useTranslations } from '../utils/i18n';
import type { CollectionEntry } from 'astro:content';
interface Props {
mission: CollectionEntry<'missions'>;
mission: CollectionEntry<'missions'>;
}
const { mission } = Astro.props;
@ -14,9 +14,9 @@ const t = useTranslations(lang);
// Format date based on language
const formattedDate = new Intl.DateTimeFormat(lang === 'de' ? 'de-DE' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(data.pubDate);
// Generate mission URL - always include language segment
@ -31,253 +31,262 @@ const statusKey = `missions.status.${data.status}` as const;
---
<a href={missionUrl} class="mission-card-link">
<article class:list={["mission-card", { "featured": data.featured }]}>
{data.image && (
<div class="mission-image-container">
<img src={data.image} alt={data.title} class="mission-image" />
<div class="status-badge" data-status={data.status}>
{t(statusKey)}
</div>
</div>
)}
<div class="content">
<div class="meta">
<span class="difficulty" data-difficulty={data.difficulty}>{t(difficultyKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<div class="details">
<div class="detail-item">
<span class="detail-label">{t('missions.duration')}</span>
<span class="detail-value">{data.duration}</span>
</div>
<div class="detail-item">
<span class="detail-label">{t('missions.skills')}</span>
<div class="skills-list">
{data.skills.map(skill => (
<span class="skill-tag">{skill}</span>
))}
</div>
</div>
</div>
<div class="footer">
{data.participants && data.participants.length > 0 && (
<div class="participants">
<span class="participants-count">{data.participants.length}</span>
</div>
)}
<span class="read-more">{t('missions.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
<article class:list={['mission-card', { featured: data.featured }]}>
{
data.image && (
<div class="mission-image-container">
<img src={data.image} alt={data.title} class="mission-image" />
<div class="status-badge" data-status={data.status}>
{t(statusKey)}
</div>
</div>
)
}
<div class="content">
<div class="meta">
<span class="difficulty" data-difficulty={data.difficulty}>{t(difficultyKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<div class="details">
<div class="detail-item">
<span class="detail-label">{t('missions.duration')}</span>
<span class="detail-value">{data.duration}</span>
</div>
<div class="detail-item">
<span class="detail-label">{t('missions.skills')}</span>
<div class="skills-list">
{data.skills.map((skill) => <span class="skill-tag">{skill}</span>)}
</div>
</div>
</div>
<div class="footer">
{
data.participants && data.participants.length > 0 && (
<div class="participants">
<span class="participants-count">{data.participants.length}</span>
</div>
)
}
<span class="read-more">{t('missions.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
</a>
<style>
.mission-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.mission-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
height: 100%;
}
.mission-card-link:hover .mission-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.mission-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.mission-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.mission-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.status-badge {
position: absolute;
top: 1rem;
right: 1rem;
padding: 0.35rem 0.75rem;
border-radius: 2rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
z-index: 2;
color: white;
}
.status-badge[data-status="active"] {
background-color: #10b981; /* Green */
}
.status-badge[data-status="completed"] {
background-color: #6366f1; /* Purple */
}
.status-badge[data-status="upcoming"] {
background-color: #f59e0b; /* Amber */
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.difficulty {
font-weight: 600;
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
}
.difficulty[data-difficulty="beginner"] {
background-color: #dcfce7; /* Light green */
color: #16a34a;
}
.difficulty[data-difficulty="intermediate"] {
background-color: #e0f2fe; /* Light blue */
color: #0284c7;
}
.difficulty[data-difficulty="advanced"] {
background-color: #fee2e2; /* Light red */
color: #dc2626;
}
.date {
color: var(--text-muted);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
margin-bottom: 1.5rem;
}
.details {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.detail-label {
font-size: 0.75rem;
color: var(--text-muted);
font-weight: 500;
}
.detail-value {
font-size: 0.875rem;
}
.skills-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.skill-tag {
font-size: 0.75rem;
background-color: rgba(var(--border-color-rgb), 0.3);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.participants {
display: flex;
align-items: center;
}
.participants-count {
font-size: 0.875rem;
color: var(--text-muted);
display: flex;
align-items: center;
}
.participants-count::before {
content: '👥 ';
margin-right: 0.25rem;
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.mission-card-link:hover .arrow {
transform: translateX(4px);
}
</style>
.mission-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.mission-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
height: 100%;
}
.mission-card-link:hover .mission-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.08),
0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.mission-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.mission-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.mission-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.status-badge {
position: absolute;
top: 1rem;
right: 1rem;
padding: 0.35rem 0.75rem;
border-radius: 2rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
z-index: 2;
color: white;
}
.status-badge[data-status='active'] {
background-color: #10b981; /* Green */
}
.status-badge[data-status='completed'] {
background-color: #6366f1; /* Purple */
}
.status-badge[data-status='upcoming'] {
background-color: #f59e0b; /* Amber */
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.difficulty {
font-weight: 600;
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
}
.difficulty[data-difficulty='beginner'] {
background-color: #dcfce7; /* Light green */
color: #16a34a;
}
.difficulty[data-difficulty='intermediate'] {
background-color: #e0f2fe; /* Light blue */
color: #0284c7;
}
.difficulty[data-difficulty='advanced'] {
background-color: #fee2e2; /* Light red */
color: #dc2626;
}
.date {
color: var(--text-muted);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
margin-bottom: 1.5rem;
}
.details {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.detail-label {
font-size: 0.75rem;
color: var(--text-muted);
font-weight: 500;
}
.detail-value {
font-size: 0.875rem;
}
.skills-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.skill-tag {
font-size: 0.75rem;
background-color: rgba(var(--border-color-rgb), 0.3);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.participants {
display: flex;
align-items: center;
}
.participants-count {
font-size: 0.875rem;
color: var(--text-muted);
display: flex;
align-items: center;
}
.participants-count::before {
content: '👥 ';
margin-right: 0.25rem;
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.mission-card-link:hover .arrow {
transform: translateX(4px);
}
</style>

View file

@ -2,150 +2,318 @@
import TableImproved from './TableImproved.astro';
const models = [
{ name: "o1", provider: "OpenAI", inputCost: 15, cachedInputCost: 7.5, outputCost: 60, inputLimit: 200000, outputLimit: 100000, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 45 },
{ name: "gpt 4.1", provider: "OpenAI", inputCost: 2, cachedInputCost: 0.5, outputCost: 8, inputLimit: 1000000, outputLimit: 32768, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "June 2024", totalCost: 6 },
{ name: "GPT-4o", provider: "OpenAI", inputCost: 2.5, cachedInputCost: 1.25, outputCost: 10, inputLimit: 128000, outputLimit: 16384, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 7.5 },
{ name: "gpt 4.1-mini", provider: "OpenAI", inputCost: 0.4, cachedInputCost: 0.1, outputCost: 1.6, inputLimit: 1000000, outputLimit: 32768, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "June 2024", totalCost: 1.2 },
{ name: "gpt 4.1-nano", provider: "OpenAI", inputCost: 0.1, cachedInputCost: 0.025, outputCost: 0.4, inputLimit: 1000000, outputLimit: 32768, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "June 2024", totalCost: 0.3 },
{ name: "o3-mini", provider: "OpenAI", inputCost: 1.1, cachedInputCost: 0.55, outputCost: 4.4, inputLimit: 200000, outputLimit: 100000, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 3.3 },
{ name: "Claude Sonnet 3.7", provider: "Antrophic", inputCost: 3, cachedInputCost: "Up to 90%", outputCost: 15, inputLimit: 200000, outputLimit: 8192, batchAPI: "50% for 24 hours", hoster: "Google, Amazon, Antrophic", trainingCutoff: "April 2024", totalCost: 10.5 },
{ name: "Claude Haiku 3.5", provider: "Antrophic", inputCost: 0.8, cachedInputCost: "Up to 90%", outputCost: 4, inputLimit: 200000, outputLimit: 8192, batchAPI: "50% for 24 hours", hoster: "Google, Amazon, Antrophic", trainingCutoff: "July 2024", totalCost: 2.8 },
{ name: "Deepseek R1", provider: "Deepseek", inputCost: 0.55, cachedInputCost: 0.14, outputCost: 2.19, inputLimit: 64000, outputLimit: 8192, batchAPI: "", hoster: "Deepseek", trainingCutoff: "July 2024", totalCost: 1.645 },
{ name: "Deepseek V3", provider: "Deepseek", inputCost: 0.27, cachedInputCost: 0.07, outputCost: 1.1, inputLimit: 64000, outputLimit: 8192, batchAPI: "", hoster: "Deepseek", trainingCutoff: "December 2024", totalCost: 0.82 },
{ name: "GPT-4o mini", provider: "OpenAI", inputCost: 0.15, cachedInputCost: 0.075, outputCost: 0.6, inputLimit: 128000, outputLimit: 16384, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 0.45 },
{ name: "Gemini 2.5 Pro", provider: "", inputCost: 1.25, cachedInputCost: "", outputCost: 10, inputLimit: 1048576, outputLimit: 65536, batchAPI: "", hoster: "Google", trainingCutoff: "January 2025", totalCost: 6.25 },
{ name: "Gemini 2.0 Flash", provider: "Google", inputCost: 0.1, cachedInputCost: 0.025, outputCost: 0.4, inputLimit: 1048576, outputLimit: 8192, batchAPI: "", hoster: "Google", trainingCutoff: "June 2024", totalCost: 0.3 },
{ name: "Gemini 2.0 Flash-Lite", provider: "Google", inputCost: 0.075, cachedInputCost: 0.01875, outputCost: 0.3, inputLimit: 1048576, outputLimit: 8192, batchAPI: "", hoster: "Google", trainingCutoff: "June 2024", totalCost: 0.225 },
{
name: 'o1',
provider: 'OpenAI',
inputCost: 15,
cachedInputCost: 7.5,
outputCost: 60,
inputLimit: 200000,
outputLimit: 100000,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 45,
},
{
name: 'gpt 4.1',
provider: 'OpenAI',
inputCost: 2,
cachedInputCost: 0.5,
outputCost: 8,
inputLimit: 1000000,
outputLimit: 32768,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'June 2024',
totalCost: 6,
},
{
name: 'GPT-4o',
provider: 'OpenAI',
inputCost: 2.5,
cachedInputCost: 1.25,
outputCost: 10,
inputLimit: 128000,
outputLimit: 16384,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 7.5,
},
{
name: 'gpt 4.1-mini',
provider: 'OpenAI',
inputCost: 0.4,
cachedInputCost: 0.1,
outputCost: 1.6,
inputLimit: 1000000,
outputLimit: 32768,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'June 2024',
totalCost: 1.2,
},
{
name: 'gpt 4.1-nano',
provider: 'OpenAI',
inputCost: 0.1,
cachedInputCost: 0.025,
outputCost: 0.4,
inputLimit: 1000000,
outputLimit: 32768,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'June 2024',
totalCost: 0.3,
},
{
name: 'o3-mini',
provider: 'OpenAI',
inputCost: 1.1,
cachedInputCost: 0.55,
outputCost: 4.4,
inputLimit: 200000,
outputLimit: 100000,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 3.3,
},
{
name: 'Claude Sonnet 3.7',
provider: 'Antrophic',
inputCost: 3,
cachedInputCost: 'Up to 90%',
outputCost: 15,
inputLimit: 200000,
outputLimit: 8192,
batchAPI: '50% for 24 hours',
hoster: 'Google, Amazon, Antrophic',
trainingCutoff: 'April 2024',
totalCost: 10.5,
},
{
name: 'Claude Haiku 3.5',
provider: 'Antrophic',
inputCost: 0.8,
cachedInputCost: 'Up to 90%',
outputCost: 4,
inputLimit: 200000,
outputLimit: 8192,
batchAPI: '50% for 24 hours',
hoster: 'Google, Amazon, Antrophic',
trainingCutoff: 'July 2024',
totalCost: 2.8,
},
{
name: 'Deepseek R1',
provider: 'Deepseek',
inputCost: 0.55,
cachedInputCost: 0.14,
outputCost: 2.19,
inputLimit: 64000,
outputLimit: 8192,
batchAPI: '',
hoster: 'Deepseek',
trainingCutoff: 'July 2024',
totalCost: 1.645,
},
{
name: 'Deepseek V3',
provider: 'Deepseek',
inputCost: 0.27,
cachedInputCost: 0.07,
outputCost: 1.1,
inputLimit: 64000,
outputLimit: 8192,
batchAPI: '',
hoster: 'Deepseek',
trainingCutoff: 'December 2024',
totalCost: 0.82,
},
{
name: 'GPT-4o mini',
provider: 'OpenAI',
inputCost: 0.15,
cachedInputCost: 0.075,
outputCost: 0.6,
inputLimit: 128000,
outputLimit: 16384,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 0.45,
},
{
name: 'Gemini 2.5 Pro',
provider: '',
inputCost: 1.25,
cachedInputCost: '',
outputCost: 10,
inputLimit: 1048576,
outputLimit: 65536,
batchAPI: '',
hoster: 'Google',
trainingCutoff: 'January 2025',
totalCost: 6.25,
},
{
name: 'Gemini 2.0 Flash',
provider: 'Google',
inputCost: 0.1,
cachedInputCost: 0.025,
outputCost: 0.4,
inputLimit: 1048576,
outputLimit: 8192,
batchAPI: '',
hoster: 'Google',
trainingCutoff: 'June 2024',
totalCost: 0.3,
},
{
name: 'Gemini 2.0 Flash-Lite',
provider: 'Google',
inputCost: 0.075,
cachedInputCost: 0.01875,
outputCost: 0.3,
inputLimit: 1048576,
outputLimit: 8192,
batchAPI: '',
hoster: 'Google',
trainingCutoff: 'June 2024',
totalCost: 0.225,
},
];
// Formatierungsfunktionen für verschiedene Spaltentypen
const formatCurrency = (value: number | undefined | null): string => {
if (value === undefined || value === null) return "-";
return `${value}$`;
if (value === undefined || value === null) return '-';
return `${value}$`;
};
const formatNumber = (value: number | undefined | null): string => {
if (value === undefined || value === null) return "-";
return value.toLocaleString();
if (value === undefined || value === null) return '-';
return value.toLocaleString();
};
// Spaltendefinitionen mit festen Breiten
const columns = [
{
key: 'name',
title: 'Model',
width: '150px',
sticky: true
},
{
key: 'provider',
title: 'Provider',
width: '120px'
},
{
key: 'inputCost',
title: 'Input Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right',
width: '120px'
},
{
key: 'cachedInputCost',
title: 'Cached Input Cost',
subtitle: 'per 1M tokens',
formatter: (value: any): string => typeof value === 'number' ? formatCurrency(value) : value,
align: 'right',
width: '150px'
},
{
key: 'outputCost',
title: 'Output Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right',
width: '120px'
},
{
key: 'inputLimit',
title: 'Input Token Limit',
formatter: formatNumber,
align: 'right',
width: '140px'
},
{
key: 'outputLimit',
title: 'Output Token Limit',
formatter: formatNumber,
align: 'right',
width: '140px'
},
{
key: 'batchAPI',
title: 'Batch API',
width: '150px'
},
{
key: 'hoster',
title: 'Hoster',
width: '200px'
},
{
key: 'trainingCutoff',
title: 'Training Cut-off',
width: '140px'
},
{
key: 'totalCost',
title: 'Total Cost',
subtitle: '1M Input + 0.5M Output',
formatter: formatCurrency,
align: 'right',
width: '120px'
}
{
key: 'name',
title: 'Model',
width: '150px',
sticky: true,
},
{
key: 'provider',
title: 'Provider',
width: '120px',
},
{
key: 'inputCost',
title: 'Input Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right',
width: '120px',
},
{
key: 'cachedInputCost',
title: 'Cached Input Cost',
subtitle: 'per 1M tokens',
formatter: (value: any): string => (typeof value === 'number' ? formatCurrency(value) : value),
align: 'right',
width: '150px',
},
{
key: 'outputCost',
title: 'Output Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right',
width: '120px',
},
{
key: 'inputLimit',
title: 'Input Token Limit',
formatter: formatNumber,
align: 'right',
width: '140px',
},
{
key: 'outputLimit',
title: 'Output Token Limit',
formatter: formatNumber,
align: 'right',
width: '140px',
},
{
key: 'batchAPI',
title: 'Batch API',
width: '150px',
},
{
key: 'hoster',
title: 'Hoster',
width: '200px',
},
{
key: 'trainingCutoff',
title: 'Training Cut-off',
width: '140px',
},
{
key: 'totalCost',
title: 'Total Cost',
subtitle: '1M Input + 0.5M Output',
formatter: formatCurrency,
align: 'right',
width: '120px',
},
];
---
<section class="models-comparison-table">
<h2>AI Models Comparison</h2>
<TableImproved
data={models}
columns={columns}
stickyHeader={true}
stickyFirstColumn={true}
maxHeight="70vh"
sortable={true}
searchable={true}
className="models-table"
/>
<h2>AI Models Comparison</h2>
<TableImproved
data={models}
columns={columns}
stickyHeader={true}
stickyFirstColumn={true}
maxHeight="70vh"
sortable={true}
searchable={true}
className="models-table"
/>
</section>
<style>
.models-comparison-table {
margin-bottom: 2.5rem;
background: var(--background-secondary);
border-radius: 1rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
padding: 2rem 0;
width: calc(100vw - 2rem);
position: relative;
left: 50%;
right: 50%;
margin-left: calc(-50vw + 1rem);
margin-right: calc(-50vw + 1rem);
overflow: hidden;
}
h2 {
text-align: center;
margin-bottom: 2rem;
}
/* Zusätzliche Anpassungen für die Tabelle */
:global(.models-table) {
--background-secondary: var(--background-secondary, #f8fafc);
--background-tertiary: var(--background-tertiary, #edf2f7);
--border-color: var(--border-color, #e2e8f0);
}
.models-comparison-table {
margin-bottom: 2.5rem;
background: var(--background-secondary);
border-radius: 1rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 2rem 0;
width: calc(100vw - 2rem);
position: relative;
left: 50%;
right: 50%;
margin-left: calc(-50vw + 1rem);
margin-right: calc(-50vw + 1rem);
overflow: hidden;
}
h2 {
text-align: center;
margin-bottom: 2rem;
}
/* Zusätzliche Anpassungen für die Tabelle */
:global(.models-table) {
--background-secondary: var(--background-secondary, #f8fafc);
--background-tertiary: var(--background-tertiary, #edf2f7);
--border-color: var(--border-color, #e2e8f0);
}
</style>

View file

@ -2,148 +2,316 @@
import Table from './Table.astro';
const models = [
{ name: "o1", provider: "OpenAI", inputCost: 15, cachedInputCost: 7.5, outputCost: 60, inputLimit: 200000, outputLimit: 100000, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 45 },
{ name: "gpt 4.1", provider: "OpenAI", inputCost: 2, cachedInputCost: 0.5, outputCost: 8, inputLimit: 1000000, outputLimit: 32768, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "June 2024", totalCost: 6 },
{ name: "GPT-4o", provider: "OpenAI", inputCost: 2.5, cachedInputCost: 1.25, outputCost: 10, inputLimit: 128000, outputLimit: 16384, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 7.5 },
{ name: "gpt 4.1-mini", provider: "OpenAI", inputCost: 0.4, cachedInputCost: 0.1, outputCost: 1.6, inputLimit: 1000000, outputLimit: 32768, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "June 2024", totalCost: 1.2 },
{ name: "gpt 4.1-nano", provider: "OpenAI", inputCost: 0.1, cachedInputCost: 0.025, outputCost: 0.4, inputLimit: 1000000, outputLimit: 32768, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "June 2024", totalCost: 0.3 },
{ name: "o3-mini", provider: "OpenAI", inputCost: 1.1, cachedInputCost: 0.55, outputCost: 4.4, inputLimit: 200000, outputLimit: 100000, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 3.3 },
{ name: "Claude Sonnet 3.7", provider: "Antrophic", inputCost: 3, cachedInputCost: "Up to 90%", outputCost: 15, inputLimit: 200000, outputLimit: 8192, batchAPI: "50% for 24 hours", hoster: "Google, Amazon, Antrophic", trainingCutoff: "April 2024", totalCost: 10.5 },
{ name: "Claude Haiku 3.5", provider: "Antrophic", inputCost: 0.8, cachedInputCost: "Up to 90%", outputCost: 4, inputLimit: 200000, outputLimit: 8192, batchAPI: "50% for 24 hours", hoster: "Google, Amazon, Antrophic", trainingCutoff: "July 2024", totalCost: 2.8 },
{ name: "Deepseek R1", provider: "Deepseek", inputCost: 0.55, cachedInputCost: 0.14, outputCost: 2.19, inputLimit: 64000, outputLimit: 8192, batchAPI: "", hoster: "Deepseek", trainingCutoff: "July 2024", totalCost: 1.645 },
{ name: "Deepseek V3", provider: "Deepseek", inputCost: 0.27, cachedInputCost: 0.07, outputCost: 1.1, inputLimit: 64000, outputLimit: 8192, batchAPI: "", hoster: "Deepseek", trainingCutoff: "December 2024", totalCost: 0.82 },
{ name: "GPT-4o mini", provider: "OpenAI", inputCost: 0.15, cachedInputCost: 0.075, outputCost: 0.6, inputLimit: 128000, outputLimit: 16384, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 0.45 },
{ name: "Gemini 2.5 Pro", provider: "", inputCost: 1.25, cachedInputCost: "", outputCost: 10, inputLimit: 1048576, outputLimit: 65536, batchAPI: "", hoster: "Google", trainingCutoff: "January 2025", totalCost: 6.25 },
{ name: "Gemini 2.0 Flash", provider: "Google", inputCost: 0.1, cachedInputCost: 0.025, outputCost: 0.4, inputLimit: 1048576, outputLimit: 8192, batchAPI: "", hoster: "Google", trainingCutoff: "June 2024", totalCost: 0.3 },
{ name: "Gemini 2.0 Flash-Lite", provider: "Google", inputCost: 0.075, cachedInputCost: 0.01875, outputCost: 0.3, inputLimit: 1048576, outputLimit: 8192, batchAPI: "", hoster: "Google", trainingCutoff: "June 2024", totalCost: 0.225 },
{
name: 'o1',
provider: 'OpenAI',
inputCost: 15,
cachedInputCost: 7.5,
outputCost: 60,
inputLimit: 200000,
outputLimit: 100000,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 45,
},
{
name: 'gpt 4.1',
provider: 'OpenAI',
inputCost: 2,
cachedInputCost: 0.5,
outputCost: 8,
inputLimit: 1000000,
outputLimit: 32768,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'June 2024',
totalCost: 6,
},
{
name: 'GPT-4o',
provider: 'OpenAI',
inputCost: 2.5,
cachedInputCost: 1.25,
outputCost: 10,
inputLimit: 128000,
outputLimit: 16384,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 7.5,
},
{
name: 'gpt 4.1-mini',
provider: 'OpenAI',
inputCost: 0.4,
cachedInputCost: 0.1,
outputCost: 1.6,
inputLimit: 1000000,
outputLimit: 32768,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'June 2024',
totalCost: 1.2,
},
{
name: 'gpt 4.1-nano',
provider: 'OpenAI',
inputCost: 0.1,
cachedInputCost: 0.025,
outputCost: 0.4,
inputLimit: 1000000,
outputLimit: 32768,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'June 2024',
totalCost: 0.3,
},
{
name: 'o3-mini',
provider: 'OpenAI',
inputCost: 1.1,
cachedInputCost: 0.55,
outputCost: 4.4,
inputLimit: 200000,
outputLimit: 100000,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 3.3,
},
{
name: 'Claude Sonnet 3.7',
provider: 'Antrophic',
inputCost: 3,
cachedInputCost: 'Up to 90%',
outputCost: 15,
inputLimit: 200000,
outputLimit: 8192,
batchAPI: '50% for 24 hours',
hoster: 'Google, Amazon, Antrophic',
trainingCutoff: 'April 2024',
totalCost: 10.5,
},
{
name: 'Claude Haiku 3.5',
provider: 'Antrophic',
inputCost: 0.8,
cachedInputCost: 'Up to 90%',
outputCost: 4,
inputLimit: 200000,
outputLimit: 8192,
batchAPI: '50% for 24 hours',
hoster: 'Google, Amazon, Antrophic',
trainingCutoff: 'July 2024',
totalCost: 2.8,
},
{
name: 'Deepseek R1',
provider: 'Deepseek',
inputCost: 0.55,
cachedInputCost: 0.14,
outputCost: 2.19,
inputLimit: 64000,
outputLimit: 8192,
batchAPI: '',
hoster: 'Deepseek',
trainingCutoff: 'July 2024',
totalCost: 1.645,
},
{
name: 'Deepseek V3',
provider: 'Deepseek',
inputCost: 0.27,
cachedInputCost: 0.07,
outputCost: 1.1,
inputLimit: 64000,
outputLimit: 8192,
batchAPI: '',
hoster: 'Deepseek',
trainingCutoff: 'December 2024',
totalCost: 0.82,
},
{
name: 'GPT-4o mini',
provider: 'OpenAI',
inputCost: 0.15,
cachedInputCost: 0.075,
outputCost: 0.6,
inputLimit: 128000,
outputLimit: 16384,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 0.45,
},
{
name: 'Gemini 2.5 Pro',
provider: '',
inputCost: 1.25,
cachedInputCost: '',
outputCost: 10,
inputLimit: 1048576,
outputLimit: 65536,
batchAPI: '',
hoster: 'Google',
trainingCutoff: 'January 2025',
totalCost: 6.25,
},
{
name: 'Gemini 2.0 Flash',
provider: 'Google',
inputCost: 0.1,
cachedInputCost: 0.025,
outputCost: 0.4,
inputLimit: 1048576,
outputLimit: 8192,
batchAPI: '',
hoster: 'Google',
trainingCutoff: 'June 2024',
totalCost: 0.3,
},
{
name: 'Gemini 2.0 Flash-Lite',
provider: 'Google',
inputCost: 0.075,
cachedInputCost: 0.01875,
outputCost: 0.3,
inputLimit: 1048576,
outputLimit: 8192,
batchAPI: '',
hoster: 'Google',
trainingCutoff: 'June 2024',
totalCost: 0.225,
},
];
// Formatierungsfunktionen für verschiedene Spaltentypen
const formatCurrency = (value) => {
if (value === undefined || value === null) return "-";
return `${value}$`;
if (value === undefined || value === null) return '-';
return `${value}$`;
};
const formatNumber = (value) => {
if (value === undefined || value === null) return "-";
return value.toLocaleString();
if (value === undefined || value === null) return '-';
return value.toLocaleString();
};
// Spaltendefinitionen
const columns = [
{
key: 'name',
title: 'Model',
width: '150px',
sticky: true
},
{
key: 'provider',
title: 'Provider'
},
{
key: 'inputCost',
title: 'Input Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right'
},
{
key: 'cachedInputCost',
title: 'Cached Input Cost',
subtitle: 'per 1M tokens',
formatter: (value) => typeof value === 'number' ? formatCurrency(value) : value,
align: 'right'
},
{
key: 'outputCost',
title: 'Output Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right'
},
{
key: 'inputLimit',
title: 'Input Token Limit',
formatter: formatNumber,
align: 'right'
},
{
key: 'outputLimit',
title: 'Output Token Limit',
formatter: formatNumber,
align: 'right'
},
{
key: 'batchAPI',
title: 'Batch API'
},
{
key: 'hoster',
title: 'Hoster'
},
{
key: 'trainingCutoff',
title: 'Training Cut-off'
},
{
key: 'totalCost',
title: 'Total Cost',
subtitle: '1M Input + 0.5M Output',
formatter: formatCurrency,
align: 'right'
}
{
key: 'name',
title: 'Model',
width: '150px',
sticky: true,
},
{
key: 'provider',
title: 'Provider',
},
{
key: 'inputCost',
title: 'Input Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right',
},
{
key: 'cachedInputCost',
title: 'Cached Input Cost',
subtitle: 'per 1M tokens',
formatter: (value) => (typeof value === 'number' ? formatCurrency(value) : value),
align: 'right',
},
{
key: 'outputCost',
title: 'Output Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right',
},
{
key: 'inputLimit',
title: 'Input Token Limit',
formatter: formatNumber,
align: 'right',
},
{
key: 'outputLimit',
title: 'Output Token Limit',
formatter: formatNumber,
align: 'right',
},
{
key: 'batchAPI',
title: 'Batch API',
},
{
key: 'hoster',
title: 'Hoster',
},
{
key: 'trainingCutoff',
title: 'Training Cut-off',
},
{
key: 'totalCost',
title: 'Total Cost',
subtitle: '1M Input + 0.5M Output',
formatter: formatCurrency,
align: 'right',
},
];
---
<section class="models-comparison-page">
<h1>AI Models Comparison</h1>
<Table
data={models}
columns={columns}
stickyHeader={true}
stickyFirstColumn={true}
maxHeight="70vh"
sortable={true}
searchable={true}
className="models-table"
/>
<div class="table-info">
<p>
<strong>Hinweis:</strong> Die Preise sind in US-Dollar angegeben und können sich ändern.
Die Gesamtkosten basieren auf 1 Million Input-Token und 0,5 Millionen Output-Token.
</p>
</div>
<h1>AI Models Comparison</h1>
<Table
data={models}
columns={columns}
stickyHeader={true}
stickyFirstColumn={true}
maxHeight="70vh"
sortable={true}
searchable={true}
className="models-table"
/>
<div class="table-info">
<p>
<strong>Hinweis:</strong> Die Preise sind in US-Dollar angegeben und können sich ändern. Die Gesamtkosten
basieren auf 1 Million Input-Token und 0,5 Millionen Output-Token.
</p>
</div>
</section>
<style>
.models-comparison-page {
margin: 2rem 0;
width: calc(100vw - 2rem);
position: relative;
left: 50%;
right: 50%;
margin-left: calc(-50vw + 1rem);
margin-right: calc(-50vw + 1rem);
}
h1 {
text-align: center;
margin-bottom: 2rem;
}
.table-info {
margin: 1.5rem 2rem;
font-size: 0.9rem;
color: var(--text-secondary);
}
/* Zusätzliche Anpassungen für die Tabelle */
:global(.models-table) {
--background-secondary: var(--color-background-secondary, #f8fafc);
--background-tertiary: var(--color-background-tertiary, #edf2f7);
--border-color: var(--color-border, #e2e8f0);
}
.models-comparison-page {
margin: 2rem 0;
width: calc(100vw - 2rem);
position: relative;
left: 50%;
right: 50%;
margin-left: calc(-50vw + 1rem);
margin-right: calc(-50vw + 1rem);
}
h1 {
text-align: center;
margin-bottom: 2rem;
}
.table-info {
margin: 1.5rem 2rem;
font-size: 0.9rem;
color: var(--text-secondary);
}
/* Zusätzliche Anpassungen für die Tabelle */
:global(.models-table) {
--background-secondary: var(--color-background-secondary, #f8fafc);
--background-tertiary: var(--color-background-tertiary, #edf2f7);
--border-color: var(--color-border, #e2e8f0);
}
</style>

File diff suppressed because it is too large Load diff

View file

@ -3,93 +3,92 @@
---
<script>
import { trackEvent, EVENTS } from '../scripts/analytics';
document.addEventListener('DOMContentLoaded', () => {
// Track navigation clicks
document.querySelectorAll('.nav-links a, .mobile-tab-bar a').forEach(link => {
link.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLAnchorElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
trackEvent(EVENTS.NAV_CLICK, {
label: text,
destination: href
});
});
});
// Track footer navigation clicks
document.querySelectorAll('footer a').forEach(link => {
link.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLAnchorElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
// Skip external links (they're tracked separately)
if (!href.startsWith('http')) {
trackEvent(EVENTS.FOOTER_CLICK, {
label: text,
destination: href
});
}
});
});
// Track language switcher
document.querySelectorAll('.language-switcher a, .language-option').forEach(link => {
link.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const lang = target.getAttribute('href')?.split('/')[1] ||
target.dataset.lang || '';
trackEvent(EVENTS.LANGUAGE_SWITCH, {
from: document.documentElement.lang,
to: lang
});
});
});
// Track theme toggle
const themeToggle = document.querySelector('.theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.classList.contains('dark') ? 'light' : 'dark';
trackEvent(EVENTS.THEME_TOGGLE, {
theme: currentTheme
});
});
}
// Track CTA buttons
document.querySelectorAll('a[href*="/join"], .cta-button').forEach(button => {
button.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
trackEvent(EVENTS.JOIN_CLICK, {
label: text,
destination: href,
location: target.closest('section')?.id || 'unknown'
});
});
});
// Track support/coffee buttons
document.querySelectorAll('a[href*="/support"], .support-link').forEach(button => {
button.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
trackEvent(EVENTS.SUPPORT_CLICK, {
label: text,
destination: href,
location: target.closest('footer') ? 'footer' : 'page'
});
});
});
});
</script>
import { trackEvent, EVENTS } from '../scripts/analytics';
document.addEventListener('DOMContentLoaded', () => {
// Track navigation clicks
document.querySelectorAll('.nav-links a, .mobile-tab-bar a').forEach((link) => {
link.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLAnchorElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
trackEvent(EVENTS.NAV_CLICK, {
label: text,
destination: href,
});
});
});
// Track footer navigation clicks
document.querySelectorAll('footer a').forEach((link) => {
link.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLAnchorElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
// Skip external links (they're tracked separately)
if (!href.startsWith('http')) {
trackEvent(EVENTS.FOOTER_CLICK, {
label: text,
destination: href,
});
}
});
});
// Track language switcher
document.querySelectorAll('.language-switcher a, .language-option').forEach((link) => {
link.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const lang = target.getAttribute('href')?.split('/')[1] || target.dataset.lang || '';
trackEvent(EVENTS.LANGUAGE_SWITCH, {
from: document.documentElement.lang,
to: lang,
});
});
});
// Track theme toggle
const themeToggle = document.querySelector('.theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.classList.contains('dark') ? 'light' : 'dark';
trackEvent(EVENTS.THEME_TOGGLE, {
theme: currentTheme,
});
});
}
// Track CTA buttons
document.querySelectorAll('a[href*="/join"], .cta-button').forEach((button) => {
button.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
trackEvent(EVENTS.JOIN_CLICK, {
label: text,
destination: href,
location: target.closest('section')?.id || 'unknown',
});
});
});
// Track support/coffee buttons
document.querySelectorAll('a[href*="/support"], .support-link').forEach((button) => {
button.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
trackEvent(EVENTS.SUPPORT_CLICK, {
label: text,
destination: href,
location: target.closest('footer') ? 'footer' : 'page',
});
});
});
});
</script>

View file

@ -1,12 +1,12 @@
---
import { getLangFromUrl, useTranslations } from "../utils/i18n";
import type { CollectionEntry } from "astro:content";
import { getLangFromUrl, useTranslations } from '../utils/i18n';
import type { CollectionEntry } from 'astro:content';
// Define allowed collections
type AllowedCollections = "news" | "models";
type AllowedCollections = 'news' | 'models';
interface Props {
news: CollectionEntry<AllowedCollections>;
news: CollectionEntry<AllowedCollections>;
}
const { news } = Astro.props;
@ -17,17 +17,17 @@ const t = useTranslations(lang);
// Format date based on language
const formattedDate = new Intl.DateTimeFormat(
lang === "de" ? "de-DE" : lang === "it" ? "it-IT" : "en-US",
{
year: "numeric",
month: "long",
day: "numeric",
}
lang === 'de' ? 'de-DE' : lang === 'it' ? 'it-IT' : 'en-US',
{
year: 'numeric',
month: 'long',
day: 'numeric',
}
).format(data.pubDate);
// Generate URL with language prefix
let pageUrl;
const slugParts = news.slug.split("/");
const slugParts = news.slug.split('/');
const fileName = slugParts[slugParts.length - 1];
// Determine collection for URL
const collection = news.collection as AllowedCollections;
@ -38,168 +38,166 @@ const categoryKey = `${collection}.categories.${data.category}` as const;
---
<a href={pageUrl} class="news-card-link">
<article class:list={["news-card", { featured: data.featured }]}>
{
data.image && (
<div class="news-image-container">
<img src={data.image} alt={data.title} class="news-image" />
</div>
)
}
<article class:list={['news-card', { featured: data.featured }]}>
{
data.image && (
<div class="news-image-container">
<img src={data.image} alt={data.title} class="news-image" />
</div>
)
}
<div class="content">
<div class="meta">
<span class="category">{t(categoryKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<div class="content">
<div class="meta">
<span class="category">{t(categoryKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<h3>{data.title}</h3>
<p>{data.description}</p>
<div class="footer">
<span class="author">{data.author}</span>
<span class="read-more"
>{t(`${collection}.readMore`)} <span class="arrow">→</span></span
>
</div>
</div>
</article>
<div class="footer">
<span class="author">{data.author}</span>
<span class="read-more">{t(`${collection}.readMore`)} <span class="arrow">→</span></span>
</div>
</div>
</article>
</a>
<style>
.news-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.news-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.news-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
height: 100%;
}
.news-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
height: 100%;
}
.news-card-link:hover .news-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.08),
0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.news-card-link:hover .news-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.08),
0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.news-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.news-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.news-image-container {
width: 100%;
padding-top: 56.25%; /* Aspect ratio 16:9 */
position: relative;
overflow: hidden;
}
.news-image-container {
width: 100%;
padding-top: 56.25%; /* Aspect ratio 16:9 */
position: relative;
overflow: hidden;
}
.news-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.news-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.category {
color: var(--accent-color);
font-weight: 600;
}
.category {
color: var(--accent-color);
font-weight: 600;
}
.date {
color: var(--text-muted);
}
.date {
color: var(--text-muted);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
flex-grow: 1;
margin-bottom: 1.5rem;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
flex-grow: 1;
margin-bottom: 1.5rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.author {
font-size: 0.875rem;
color: var(--text-muted);
}
.author {
font-size: 0.875rem;
color: var(--text-muted);
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.news-card-link:hover .arrow {
transform: translateX(4px);
}
.news-card-link:hover .arrow {
transform: translateX(4px);
}
/* Tags */
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.75rem;
}
/* Tags */
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.75rem;
}
.tag {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
color: var(--accent-color);
border-radius: 0.25rem;
}
.tag {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
color: var(--accent-color);
border-radius: 0.25rem;
}
</style>

View file

@ -1,9 +1,9 @@
---
import { getLangFromUrl, useTranslations } from "../utils/i18n";
import { getLangFromUrl, useTranslations } from '../utils/i18n';
interface Props {
customTitle?: string;
customDescription?: string;
customTitle?: string;
customDescription?: string;
}
const { customTitle, customDescription } = Astro.props;
@ -12,145 +12,145 @@ const t = useTranslations(lang);
---
<div class="newsletter-container">
<h2>{customTitle || t("join.newsletterTitle")}</h2>
<p class="newsletter-description">
{customDescription || t("join.newsletterDesc")}
</p>
<h2>{customTitle || t('join.newsletterTitle')}</h2>
<p class="newsletter-description">
{customDescription || t('join.newsletterDesc')}
</p>
<form class="newsletter-form" id="newsletterForm">
<input
type="email"
id="newsletterEmail"
name="email"
placeholder={t("join.emailPlaceholder")}
required
/>
<button type="submit">{t("join.subscribe")}</button>
</form>
<form class="newsletter-form" id="newsletterForm">
<input
type="email"
id="newsletterEmail"
name="email"
placeholder={t('join.emailPlaceholder')}
required
/>
<button type="submit">{t('join.subscribe')}</button>
</form>
<div id="subscribeSuccess" class="subscribe-success" style="display: none;">
<p>{t("join.success")}</p>
</div>
<div id="subscribeSuccess" class="subscribe-success" style="display: none;">
<p>{t('join.success')}</p>
</div>
</div>
<style>
.newsletter-container {
background-color: var(--card-bg);
border-radius: 0.75rem;
padding: 2rem;
border: 1px solid var(--border-color);
margin-bottom: 2rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.newsletter-container {
background-color: var(--card-bg);
border-radius: 0.75rem;
padding: 2rem;
border: 1px solid var(--border-color);
margin-bottom: 2rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
/* Heller Modus */
:root:not(.dark) .newsletter-container {
background-color: var(--card-bg);
}
/* Heller Modus */
:root:not(.dark) .newsletter-container {
background-color: var(--card-bg);
}
h2 {
margin-top: 0;
margin-bottom: 1rem;
font-size: 1.5rem;
}
h2 {
margin-top: 0;
margin-bottom: 1rem;
font-size: 1.5rem;
}
.newsletter-description {
margin-bottom: 1.5rem;
color: var(--text-muted);
}
.newsletter-description {
margin-bottom: 1.5rem;
color: var(--text-muted);
}
.newsletter-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.newsletter-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
input {
padding: 0.75rem;
border-radius: 0.375rem;
border: 1px solid var(--border-color);
background-color: var(--background-color);
color: var(--text-color);
font-size: 1rem;
transition: border-color 0.2s ease;
width: 100%;
}
input {
padding: 0.75rem;
border-radius: 0.375rem;
border: 1px solid var(--border-color);
background-color: var(--background-color);
color: var(--text-color);
font-size: 1rem;
transition: border-color 0.2s ease;
width: 100%;
}
/* Heller Modus für Eingabefelder */
:root:not(.dark) input {
background-color: var(--background-color);
}
/* Heller Modus für Eingabefelder */
:root:not(.dark) input {
background-color: var(--background-color);
}
input:focus {
outline: none;
border-color: var(--accent-color);
}
input:focus {
outline: none;
border-color: var(--accent-color);
}
button {
background-color: var(--accent-color);
color: white;
border: none;
border-radius: 0.375rem;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
width: max-content;
}
button {
background-color: var(--accent-color);
color: white;
border: none;
border-radius: 0.375rem;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
width: max-content;
}
button:hover {
background-color: var(--accent-hover);
}
button:hover {
background-color: var(--accent-hover);
}
.subscribe-success {
background-color: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 0.375rem;
padding: 1rem;
margin-top: 1rem;
}
.subscribe-success {
background-color: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 0.375rem;
padding: 1rem;
margin-top: 1rem;
}
.subscribe-success p {
color: #10b981;
margin: 0;
font-weight: 500;
}
.subscribe-success p {
color: #10b981;
margin: 0;
font-weight: 500;
}
/* Responsive layout for larger screens */
@media (min-width: 640px) {
.newsletter-form {
flex-direction: row;
}
/* Responsive layout for larger screens */
@media (min-width: 640px) {
.newsletter-form {
flex-direction: row;
}
input {
flex: 1;
}
input {
flex: 1;
}
button {
flex-shrink: 0;
}
}
button {
flex-shrink: 0;
}
}
</style>
<script>
document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById("newsletterForm");
const successMessage = document.getElementById("subscribeSuccess");
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('newsletterForm');
const successMessage = document.getElementById('subscribeSuccess');
form.addEventListener("submit", (e) => {
e.preventDefault();
form.addEventListener('submit', (e) => {
e.preventDefault();
// Simulate form submission (in a real app, this would send data to a server)
setTimeout(() => {
form.reset();
successMessage.style.display = "block";
// Simulate form submission (in a real app, this would send data to a server)
setTimeout(() => {
form.reset();
successMessage.style.display = 'block';
// Hide success message after 5 seconds
setTimeout(() => {
successMessage.style.display = "none";
}, 5000);
}, 1000);
});
});
// Hide success message after 5 seconds
setTimeout(() => {
successMessage.style.display = 'none';
}, 5000);
}, 1000);
});
});
</script>

View file

@ -1,8 +1,8 @@
---
import { useTranslations } from "../utils/i18n";
import { useTranslations } from '../utils/i18n';
interface Props {
lang: "de" | "en" | "it";
lang: 'de' | 'en' | 'it';
}
const { lang } = Astro.props;
@ -10,79 +10,75 @@ const t = useTranslations(lang);
---
<div class="payment-container">
<div class="payment-options">
<div class="payment-type-selector">
<button id="one-time" class="payment-type-btn active"
>{t("support.onetime")}</button
>
<button id="recurring" class="payment-type-btn"
>{t("support.recurring")}</button
>
</div>
<div class="payment-options">
<div class="payment-type-selector">
<button id="one-time" class="payment-type-btn active">{t('support.onetime')}</button>
<button id="recurring" class="payment-type-btn">{t('support.recurring')}</button>
</div>
<div class="coffee-options">
<h3>{t("support.buyMeACoffee")}</h3>
<div class="coffee-selector">
<div
class="coffee-option"
data-price="3"
data-price-id-onetime="price_1R8xkmAZjQCYS0ZJk6PZtJUm"
data-price-id-recurring="price_1R8xkmAZjQCYS0ZJSBkFXcE1"
>
<div class="coffee-icon small-coffee">☕</div>
<div class="coffee-details">
<h4>Kleiner Kaffee</h4>
<p>3€</p>
</div>
</div>
<div
class="coffee-option active"
data-price="5"
data-price-id-onetime="price_1R8xmlAZjQCYS0ZJ4QCSZFFN"
data-price-id-recurring="price_1R8xn3AZjQCYS0ZJIXddpl0O"
>
<div class="coffee-icon medium-coffee">☕</div>
<div class="coffee-details">
<h4>Mittlerer Kaffee</h4>
<p>5€</p>
</div>
</div>
<div
class="coffee-option"
data-price="8"
data-price-id-onetime="price_1R8xp4AZjQCYS0ZJCkTA6gCs"
data-price-id-recurring="price_1R8xpIAZjQCYS0ZJ93nqMjop"
>
<div class="coffee-icon large-coffee">☕</div>
<div class="coffee-details">
<h4>Großer Kaffee</h4>
<p>8€</p>
</div>
</div>
</div>
</div>
<div class="coffee-options">
<h3>{t('support.buyMeACoffee')}</h3>
<div class="coffee-selector">
<div
class="coffee-option"
data-price="3"
data-price-id-onetime="price_1R8xkmAZjQCYS0ZJk6PZtJUm"
data-price-id-recurring="price_1R8xkmAZjQCYS0ZJSBkFXcE1"
>
<div class="coffee-icon small-coffee">☕</div>
<div class="coffee-details">
<h4>Kleiner Kaffee</h4>
<p>3€</p>
</div>
</div>
<div
class="coffee-option active"
data-price="5"
data-price-id-onetime="price_1R8xmlAZjQCYS0ZJ4QCSZFFN"
data-price-id-recurring="price_1R8xn3AZjQCYS0ZJIXddpl0O"
>
<div class="coffee-icon medium-coffee">☕</div>
<div class="coffee-details">
<h4>Mittlerer Kaffee</h4>
<p>5€</p>
</div>
</div>
<div
class="coffee-option"
data-price="8"
data-price-id-onetime="price_1R8xp4AZjQCYS0ZJCkTA6gCs"
data-price-id-recurring="price_1R8xpIAZjQCYS0ZJ93nqMjop"
>
<div class="coffee-icon large-coffee">☕</div>
<div class="coffee-details">
<h4>Großer Kaffee</h4>
<p>8€</p>
</div>
</div>
</div>
</div>
<div class="payment-methods">
<p>{t("support.paymentMethod")}</p>
<div class="payment-buttons">
<button id="stripe-button" class="payment-method-btn">
<svg
viewBox="0 0 60 25"
xmlns="http://www.w3.org/2000/svg"
width="60"
height="25"
class="UserLogo variant--"
>
<title>Stripe logo</title>
<path
fill="var(--text-color)"
d="M59.64 14.28h-8.06c.19 1.93 1.6 2.55 3.2 2.55 1.64 0 2.96-.37 4.05-.95v3.32a8.33 8.33 0 0 1-4.56 1.1c-4.01 0-6.83-2.5-6.83-7.48 0-4.19 2.39-7.52 6.3-7.52 3.92 0 5.96 3.28 5.96 7.5 0 .4-.04 1.26-.06 1.48zm-5.92-5.62c-1.03 0-2.17.73-2.17 2.58h4.25c0-1.85-1.07-2.58-2.08-2.58zM40.95 20.3c-1.44 0-2.32-.6-2.9-1.04l-.02 4.63-4.12.87V5.57h3.76l.08 1.02a4.7 4.7 0 0 1 3.23-1.29c2.9 0 5.62 2.6 5.62 7.4 0 5.23-2.7 7.6-5.65 7.6zM40 8.95c-.95 0-1.54.34-1.97.81l.02 6.12c.4.44.98.78 1.95.78 1.52 0 2.54-1.65 2.54-3.87 0-2.15-1.04-3.84-2.54-3.84zM28.24 5.57h4.13v14.44h-4.13V5.57zm0-4.7L32.37 0v3.36l-4.13.88V.88zm-4.32 9.35v9.79H19.8V5.57h3.7l.12 1.22c1-1.77 3.07-1.41 3.62-1.22v3.79c-.52-.17-2.29-.43-3.32.86zm-8.55 4.72c0 2.43 2.6 1.68 3.12 1.46v3.36c-.55.3-1.54.54-2.89.54a4.15 4.15 0 0 1-4.27-4.24l.02-13.17 4.02-.86v3.54h3.14V9.1h-3.14v5.85zm-4.91.7c0 2.97-2.31 4.66-5.73 4.66a11.2 11.2 0 0 1-4.46-.93v-3.93c1.38.75 3.1 1.31 4.46 1.31.92 0 1.53-.24 1.53-1C6.26 13.77 0 14.51 0 9.95 0 7.04 2.28 5.3 5.62 5.3c1.36 0 2.72.2 4.09.75v3.88a9.23 9.23 0 0 0-4.1-1.06c-.86 0-1.44.25-1.44.9 0 1.85 6.29.97 6.29 5.88z"
fill-rule="evenodd"></path>
</svg>
<span>{t("support.payWithStripe")}</span>
</button>
{
/* PayPal Button wird später implementiert
<div class="payment-methods">
<p>{t('support.paymentMethod')}</p>
<div class="payment-buttons">
<button id="stripe-button" class="payment-method-btn">
<svg
viewBox="0 0 60 25"
xmlns="http://www.w3.org/2000/svg"
width="60"
height="25"
class="UserLogo variant--"
>
<title>Stripe logo</title>
<path
fill="var(--text-color)"
d="M59.64 14.28h-8.06c.19 1.93 1.6 2.55 3.2 2.55 1.64 0 2.96-.37 4.05-.95v3.32a8.33 8.33 0 0 1-4.56 1.1c-4.01 0-6.83-2.5-6.83-7.48 0-4.19 2.39-7.52 6.3-7.52 3.92 0 5.96 3.28 5.96 7.5 0 .4-.04 1.26-.06 1.48zm-5.92-5.62c-1.03 0-2.17.73-2.17 2.58h4.25c0-1.85-1.07-2.58-2.08-2.58zM40.95 20.3c-1.44 0-2.32-.6-2.9-1.04l-.02 4.63-4.12.87V5.57h3.76l.08 1.02a4.7 4.7 0 0 1 3.23-1.29c2.9 0 5.62 2.6 5.62 7.4 0 5.23-2.7 7.6-5.65 7.6zM40 8.95c-.95 0-1.54.34-1.97.81l.02 6.12c.4.44.98.78 1.95.78 1.52 0 2.54-1.65 2.54-3.87 0-2.15-1.04-3.84-2.54-3.84zM28.24 5.57h4.13v14.44h-4.13V5.57zm0-4.7L32.37 0v3.36l-4.13.88V.88zm-4.32 9.35v9.79H19.8V5.57h3.7l.12 1.22c1-1.77 3.07-1.41 3.62-1.22v3.79c-.52-.17-2.29-.43-3.32.86zm-8.55 4.72c0 2.43 2.6 1.68 3.12 1.46v3.36c-.55.3-1.54.54-2.89.54a4.15 4.15 0 0 1-4.27-4.24l.02-13.17 4.02-.86v3.54h3.14V9.1h-3.14v5.85zm-4.91.7c0 2.97-2.31 4.66-5.73 4.66a11.2 11.2 0 0 1-4.46-.93v-3.93c1.38.75 3.1 1.31 4.46 1.31.92 0 1.53-.24 1.53-1C6.26 13.77 0 14.51 0 9.95 0 7.04 2.28 5.3 5.62 5.3c1.36 0 2.72.2 4.09.75v3.88a9.23 9.23 0 0 0-4.1-1.06c-.86 0-1.44.25-1.44.9 0 1.85 6.29.97 6.29 5.88z"
fill-rule="evenodd"></path>
</svg>
<span>{t('support.payWithStripe')}</span>
</button>
{
/* PayPal Button wird später implementiert
<button id="paypal-button" class="payment-method-btn">
<svg
width="101"
@ -99,462 +95,449 @@ const t = useTranslations(lang);
<span>{t("support.payWithPayPal")}</span>
</button>
*/
}
</div>
</div>
}
</div>
</div>
<div id="payment-message"></div>
</div>
<div id="payment-message"></div>
</div>
</div>
<style>
.payment-container {
background-color: var(--card-bg);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
padding: 2.5rem;
width: 100%;
max-width: 550px;
transition: all 0.3s ease;
border: 1px solid rgba(var(--border-color-rgb), 0.1);
}
.payment-container {
background-color: var(--card-bg);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
padding: 2.5rem;
width: 100%;
max-width: 550px;
transition: all 0.3s ease;
border: 1px solid rgba(var(--border-color-rgb), 0.1);
}
.payment-options {
display: flex;
flex-direction: column;
gap: 2rem;
}
.payment-options {
display: flex;
flex-direction: column;
gap: 2rem;
}
.payment-type-selector {
display: flex;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(var(--border-color-rgb), 0.2);
}
.payment-type-selector {
display: flex;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(var(--border-color-rgb), 0.2);
}
.payment-type-btn {
flex: 1;
padding: 1.2rem;
border: none;
background: var(--card-bg);
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
color: var(--text-color);
font-size: 1.1rem;
position: relative;
overflow: hidden;
}
.payment-type-btn {
flex: 1;
padding: 1.2rem;
border: none;
background: var(--card-bg);
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
color: var(--text-color);
font-size: 1.1rem;
position: relative;
overflow: hidden;
}
.payment-type-btn:after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background-color: transparent;
transition: all 0.3s ease;
}
.payment-type-btn:after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background-color: transparent;
transition: all 0.3s ease;
}
.payment-type-btn.active {
background-color: rgba(var(--accent-color-rgb), 0.1);
color: var(--accent-color);
}
.payment-type-btn.active {
background-color: rgba(var(--accent-color-rgb), 0.1);
color: var(--accent-color);
}
.payment-type-btn.active:after {
background-color: var(--accent-color);
}
.payment-type-btn.active:after {
background-color: var(--accent-color);
}
/* Coffee Options */
.coffee-options {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Coffee Options */
.coffee-options {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.coffee-options h3 {
text-align: center;
margin-bottom: 0.5rem;
font-size: 1.3rem;
color: var(--text-color);
}
.coffee-options h3 {
text-align: center;
margin-bottom: 0.5rem;
font-size: 1.3rem;
color: var(--text-color);
}
.coffee-selector {
display: flex;
gap: 1.2rem;
justify-content: space-between;
}
.coffee-selector {
display: flex;
gap: 1.2rem;
justify-content: space-between;
}
.coffee-option {
flex: 1;
border: 2px solid transparent;
background-color: rgba(var(--border-color-rgb), 0.05);
border-radius: 12px;
padding: 1.2rem 0.8rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.coffee-option {
flex: 1;
border: 2px solid transparent;
background-color: rgba(var(--border-color-rgb), 0.05);
border-radius: 12px;
padding: 1.2rem 0.8rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.coffee-option:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
}
.coffee-option:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
}
.coffee-option.active {
border-color: var(--accent-color);
background-color: rgba(var(--accent-color-rgb), 0.08);
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.08);
}
.coffee-option.active {
border-color: var(--accent-color);
background-color: rgba(var(--accent-color-rgb), 0.08);
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.08);
}
.coffee-icon {
margin-bottom: 0.8rem;
transition: transform 0.3s ease;
}
.coffee-icon {
margin-bottom: 0.8rem;
transition: transform 0.3s ease;
}
.coffee-option:hover .coffee-icon {
transform: scale(1.1);
}
.coffee-option:hover .coffee-icon {
transform: scale(1.1);
}
.small-coffee {
font-size: 1.5rem;
}
.small-coffee {
font-size: 1.5rem;
}
.medium-coffee {
font-size: 2rem;
}
.medium-coffee {
font-size: 2rem;
}
.large-coffee {
font-size: 2.5rem;
}
.large-coffee {
font-size: 2.5rem;
}
.coffee-details h4 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: var(--text-color);
}
.coffee-details h4 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: var(--text-color);
}
.coffee-details p {
margin: 0.25rem 0 0 0;
font-weight: 600;
color: var(--accent-color);
font-size: 1.2rem;
}
.coffee-details p {
margin: 0.25rem 0 0 0;
font-weight: 600;
color: var(--accent-color);
font-size: 1.2rem;
}
.payment-methods {
display: flex;
flex-direction: column;
gap: 1rem;
}
.payment-methods {
display: flex;
flex-direction: column;
gap: 1rem;
}
.payment-methods p {
text-align: center;
font-weight: 500;
color: var(--text-muted);
margin: 0 0 0.5rem 0;
}
.payment-methods p {
text-align: center;
font-weight: 500;
color: var(--text-muted);
margin: 0 0 0.5rem 0;
}
.payment-buttons {
display: flex;
flex-direction: column;
gap: 1rem;
}
.payment-buttons {
display: flex;
flex-direction: column;
gap: 1rem;
}
.payment-method-btn {
padding: 1rem 1.5rem;
border-radius: 12px;
border: 1px solid rgba(var(--border-color-rgb), 0.15);
background-color: var(--background-color);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
transition: all 0.3s ease;
color: var(--text-color);
font-weight: 500;
font-size: 1.1rem;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
.payment-method-btn {
padding: 1rem 1.5rem;
border-radius: 12px;
border: 1px solid rgba(var(--border-color-rgb), 0.15);
background-color: var(--background-color);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
transition: all 0.3s ease;
color: var(--text-color);
font-weight: 500;
font-size: 1.1rem;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
.payment-method-btn:hover {
background-color: var(--hover-bg);
transform: translateY(-2px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.08);
}
.payment-method-btn:hover {
background-color: var(--hover-bg);
transform: translateY(-2px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.08);
}
.payment-method-btn span {
font-weight: 600;
}
.payment-method-btn span {
font-weight: 600;
}
#payment-message {
color: var(--accent-color);
text-align: center;
padding: 1.2rem;
margin-top: 1rem;
border-radius: 12px;
display: none;
font-weight: 500;
}
#payment-message {
color: var(--accent-color);
text-align: center;
padding: 1.2rem;
margin-top: 1rem;
border-radius: 12px;
display: none;
font-weight: 500;
}
#payment-message.success {
background-color: rgba(var(--accent-color-rgb), 0.1);
border: 1px solid var(--accent-color);
display: block;
}
#payment-message.success {
background-color: rgba(var(--accent-color-rgb), 0.1);
border: 1px solid var(--accent-color);
display: block;
}
#payment-message.error {
background-color: rgba(255, 0, 0, 0.1);
border: 1px solid red;
color: red;
display: block;
}
#payment-message.error {
background-color: rgba(255, 0, 0, 0.1);
border: 1px solid red;
color: red;
display: block;
}
@media (max-width: 480px) {
.payment-container {
padding: 1.5rem;
}
@media (max-width: 480px) {
.payment-container {
padding: 1.5rem;
}
.coffee-selector {
flex-direction: column;
gap: 0.8rem;
}
.coffee-selector {
flex-direction: column;
gap: 0.8rem;
}
.coffee-option {
padding: 1rem;
}
.coffee-option {
padding: 1rem;
}
.payment-type-btn {
padding: 1rem;
font-size: 1rem;
}
}
.payment-type-btn {
padding: 1rem;
font-size: 1rem;
}
}
</style>
<script>
// Import Stripe and PayPal libraries
import { loadStripe } from "@stripe/stripe-js";
// import { loadScript } from "@paypal/paypal-js"; // Wird später benötigt
import { trackEvent, EVENTS } from '../scripts/analytics';
// Import Stripe and PayPal libraries
import { loadStripe } from '@stripe/stripe-js';
// import { loadScript } from "@paypal/paypal-js"; // Wird später benötigt
import { trackEvent, EVENTS } from '../scripts/analytics';
// TypeScript-Definitionen für Plausible (legacy)
declare global {
interface Window {
plausible?: (
eventName: string,
options?: { props?: Record<string, any> }
) => void;
}
}
// TypeScript-Definitionen für Plausible (legacy)
declare global {
interface Window {
plausible?: (eventName: string, options?: { props?: Record<string, any> }) => void;
}
}
// Interface für Coffee Option Element
interface CoffeeOptionElement extends Element {
dataset: {
price: string;
priceIdOnetime: string;
priceIdRecurring: string;
};
}
// Interface für Coffee Option Element
interface CoffeeOptionElement extends Element {
dataset: {
price: string;
priceIdOnetime: string;
priceIdRecurring: string;
};
}
document.addEventListener("DOMContentLoaded", async () => {
const oneTimeBtn = document.getElementById("one-time");
const recurringBtn = document.getElementById("recurring");
const stripeBtn = document.getElementById("stripe-button");
// const paypalBtn = document.getElementById("paypal-button"); // Wird später benötigt
const coffeeOptions =
document.querySelectorAll<CoffeeOptionElement>(".coffee-option");
const paymentMessage = document.getElementById("payment-message");
document.addEventListener('DOMContentLoaded', async () => {
const oneTimeBtn = document.getElementById('one-time');
const recurringBtn = document.getElementById('recurring');
const stripeBtn = document.getElementById('stripe-button');
// const paypalBtn = document.getElementById("paypal-button"); // Wird später benötigt
const coffeeOptions = document.querySelectorAll<CoffeeOptionElement>('.coffee-option');
const paymentMessage = document.getElementById('payment-message');
if (!paymentMessage) {
console.error("Payment message element not found");
return;
}
if (!paymentMessage) {
console.error('Payment message element not found');
return;
}
// Kaffeegrößen-Auswahl
let selectedCoffeeOption = document.querySelector<CoffeeOptionElement>(
".coffee-option.active"
);
let selectedAmount = Number(selectedCoffeeOption?.dataset.price || 5);
let selectedPriceId = selectedCoffeeOption?.dataset.priceIdOnetime;
// Kaffeegrößen-Auswahl
let selectedCoffeeOption = document.querySelector<CoffeeOptionElement>('.coffee-option.active');
let selectedAmount = Number(selectedCoffeeOption?.dataset.price || 5);
let selectedPriceId = selectedCoffeeOption?.dataset.priceIdOnetime;
// Event-Listener für Kaffee-Optionen
coffeeOptions.forEach((option) => {
option.addEventListener("click", () => {
// Vorherige Auswahl zurücksetzen
coffeeOptions.forEach((opt) => opt.classList.remove("active"));
// Event-Listener für Kaffee-Optionen
coffeeOptions.forEach((option) => {
option.addEventListener('click', () => {
// Vorherige Auswahl zurücksetzen
coffeeOptions.forEach((opt) => opt.classList.remove('active'));
// Neue Auswahl aktivieren
option.classList.add("active");
selectedCoffeeOption = option;
selectedAmount = Number(option.dataset.price);
// Neue Auswahl aktivieren
option.classList.add('active');
selectedCoffeeOption = option;
selectedAmount = Number(option.dataset.price);
// Preis-ID basierend auf dem Zahlungstyp setzen
if (recurringBtn?.classList.contains("active")) {
selectedPriceId = option.dataset.priceIdRecurring;
} else {
selectedPriceId = option.dataset.priceIdOnetime;
}
// Preis-ID basierend auf dem Zahlungstyp setzen
if (recurringBtn?.classList.contains('active')) {
selectedPriceId = option.dataset.priceIdRecurring;
} else {
selectedPriceId = option.dataset.priceIdOnetime;
}
// Plausible Event für Kaffeegrößen-Auswahl
const coffeeTitle = option.querySelector("h4");
if (coffeeTitle) {
trackEvent(EVENTS.COFFEE_SELECT, {
size: coffeeTitle.textContent,
amount: selectedAmount,
type: recurringBtn?.classList.contains("active") ? "recurring" : "one-time"
});
}
});
});
// Plausible Event für Kaffeegrößen-Auswahl
const coffeeTitle = option.querySelector('h4');
if (coffeeTitle) {
trackEvent(EVENTS.COFFEE_SELECT, {
size: coffeeTitle.textContent,
amount: selectedAmount,
type: recurringBtn?.classList.contains('active') ? 'recurring' : 'one-time',
});
}
});
});
// Toggle zwischen Zahlungstypen
oneTimeBtn?.addEventListener("click", () => {
oneTimeBtn.classList.add("active");
recurringBtn?.classList.remove("active");
// Toggle zwischen Zahlungstypen
oneTimeBtn?.addEventListener('click', () => {
oneTimeBtn.classList.add('active');
recurringBtn?.classList.remove('active');
// Preis-ID aktualisieren
if (selectedCoffeeOption) {
selectedPriceId = selectedCoffeeOption.dataset.priceIdOnetime;
}
// Preis-ID aktualisieren
if (selectedCoffeeOption) {
selectedPriceId = selectedCoffeeOption.dataset.priceIdOnetime;
}
// Plausible Event für Einmalzahlung
trackEvent(EVENTS.PAYMENT_TYPE_CHANGE, {
type: "one-time",
amount: selectedAmount
});
});
// Plausible Event für Einmalzahlung
trackEvent(EVENTS.PAYMENT_TYPE_CHANGE, {
type: 'one-time',
amount: selectedAmount,
});
});
recurringBtn?.addEventListener("click", () => {
recurringBtn.classList.add("active");
oneTimeBtn?.classList.remove("active");
recurringBtn?.addEventListener('click', () => {
recurringBtn.classList.add('active');
oneTimeBtn?.classList.remove('active');
// Preis-ID aktualisieren
if (selectedCoffeeOption) {
selectedPriceId = selectedCoffeeOption.dataset.priceIdRecurring;
}
// Preis-ID aktualisieren
if (selectedCoffeeOption) {
selectedPriceId = selectedCoffeeOption.dataset.priceIdRecurring;
}
// Plausible Event für wiederkehrende Zahlung
trackEvent(EVENTS.PAYMENT_TYPE_CHANGE, {
type: "recurring",
amount: selectedAmount
});
});
// Plausible Event für wiederkehrende Zahlung
trackEvent(EVENTS.PAYMENT_TYPE_CHANGE, {
type: 'recurring',
amount: selectedAmount,
});
});
// Dieser Stripe Key wird aus den Netlify Umgebungsvariablen geladen
const stripePromise = loadStripe(
import.meta.env.PUBLIC_STRIPE_PUBLISHABLE_KEY || "pk_test_placeholder"
);
// Dieser Stripe Key wird aus den Netlify Umgebungsvariablen geladen
const stripePromise = loadStripe(
import.meta.env.PUBLIC_STRIPE_PUBLISHABLE_KEY || 'pk_test_placeholder'
);
// Stripe payment processing
stripeBtn?.addEventListener("click", async () => {
const amount = selectedAmount;
const isRecurring = recurringBtn?.classList.contains("active");
const priceId = selectedPriceId;
// Stripe payment processing
stripeBtn?.addEventListener('click', async () => {
const amount = selectedAmount;
const isRecurring = recurringBtn?.classList.contains('active');
const priceId = selectedPriceId;
paymentMessage.textContent = "Verarbeite Zahlung...";
paymentMessage.classList.remove("success", "error");
paymentMessage.style.display = "block";
paymentMessage.textContent = 'Verarbeite Zahlung...';
paymentMessage.classList.remove('success', 'error');
paymentMessage.style.display = 'block';
// Plausible Event für Stripe-Zahlungsstart
const coffeeTitle = selectedCoffeeOption?.querySelector("h4");
trackEvent(EVENTS.CHECKOUT_START, {
provider: "stripe",
amount: amount,
type: isRecurring ? "recurring" : "one-time",
coffeeSize: coffeeTitle?.textContent || "Mittlerer Kaffee",
priceId: selectedPriceId
});
// Plausible Event für Stripe-Zahlungsstart
const coffeeTitle = selectedCoffeeOption?.querySelector('h4');
trackEvent(EVENTS.CHECKOUT_START, {
provider: 'stripe',
amount: amount,
type: isRecurring ? 'recurring' : 'one-time',
coffeeSize: coffeeTitle?.textContent || 'Mittlerer Kaffee',
priceId: selectedPriceId,
});
try {
// Create a payment intent on the server using Netlify Functions
const response = await fetch(
"/.netlify/functions/create-payment-intent",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
amount,
isRecurring,
priceId,
coffeeSize: coffeeTitle?.textContent || "Mittlerer Kaffee",
}),
}
);
try {
// Create a payment intent on the server using Netlify Functions
const response = await fetch('/.netlify/functions/create-payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount,
isRecurring,
priceId,
coffeeSize: coffeeTitle?.textContent || 'Mittlerer Kaffee',
}),
});
if (!response.ok) {
throw new Error("Netzwerkantwort nicht ok");
}
if (!response.ok) {
throw new Error('Netzwerkantwort nicht ok');
}
const data = await response.json();
const data = await response.json();
console.log("Payment response data:", data);
console.log('Payment response data:', data);
// Option 1: Direkt zur Stripe URL weiterleiten
if (data.url) {
console.log("Redirecting to checkout URL:", data.url);
// Kurze Verzögerung für UX
setTimeout(() => {
window.location.href = data.url;
}, 500);
return; // Wichtig: Return hier, damit der Rest nicht ausgeführt wird
}
// Option 1: Direkt zur Stripe URL weiterleiten
if (data.url) {
console.log('Redirecting to checkout URL:', data.url);
// Kurze Verzögerung für UX
setTimeout(() => {
window.location.href = data.url;
}, 500);
return; // Wichtig: Return hier, damit der Rest nicht ausgeführt wird
}
// Wenn keine URL vorhanden ist, versuchen wir es mit redirectToCheckout
if (data.sessionId || data.id) {
console.log(
"Using redirectToCheckout with session ID:",
data.sessionId || data.id
);
// Wenn keine URL vorhanden ist, versuchen wir es mit redirectToCheckout
if (data.sessionId || data.id) {
console.log('Using redirectToCheckout with session ID:', data.sessionId || data.id);
// Initialisiere Stripe
const stripe = await stripePromise;
if (!stripe) {
throw new Error("Stripe konnte nicht initialisiert werden");
}
// Initialisiere Stripe
const stripe = await stripePromise;
if (!stripe) {
throw new Error('Stripe konnte nicht initialisiert werden');
}
const { error } = await stripe.redirectToCheckout({
sessionId: data.sessionId || data.id,
});
const { error } = await stripe.redirectToCheckout({
sessionId: data.sessionId || data.id,
});
if (error) {
console.error("Checkout redirect error:", error);
throw new Error(error.message);
}
} else {
throw new Error("Keine Checkout-URL oder Session-ID erhalten");
}
if (error) {
console.error('Checkout redirect error:', error);
throw new Error(error.message);
}
} else {
throw new Error('Keine Checkout-URL oder Session-ID erhalten');
}
// Erfolgreiche Zahlung
paymentMessage.textContent = `Zahlung über ${amount}€ ${isRecurring ? "(regelmäßig)" : "(einmalig)"} erfolgreich!`;
paymentMessage.classList.add("success");
paymentMessage.classList.remove("error");
} catch (error: unknown) {
console.error("Zahlungsfehler:", error);
const errorMessage =
error instanceof Error ? error.message : "Unbekannter Fehler";
paymentMessage.textContent = `Fehler: ${errorMessage}`;
paymentMessage.classList.add("error");
paymentMessage.classList.remove("success");
// Erfolgreiche Zahlung
paymentMessage.textContent = `Zahlung über ${amount}€ ${isRecurring ? '(regelmäßig)' : '(einmalig)'} erfolgreich!`;
paymentMessage.classList.add('success');
paymentMessage.classList.remove('error');
} catch (error: unknown) {
console.error('Zahlungsfehler:', error);
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
paymentMessage.textContent = `Fehler: ${errorMessage}`;
paymentMessage.classList.add('error');
paymentMessage.classList.remove('success');
// Plausible Event für Zahlungsfehler
trackEvent('payment-error', {
provider: "stripe",
error: errorMessage
});
}
});
// Plausible Event für Zahlungsfehler
trackEvent('payment-error', {
provider: 'stripe',
error: errorMessage,
});
}
});
// PayPal payment processing wird später implementiert
/*
// PayPal payment processing wird später implementiert
/*
paypalBtn?.addEventListener("click", async () => {
const amount = selectedAmount;
const isRecurring = recurringBtn?.classList.contains("active");
@ -619,5 +602,5 @@ const t = useTranslations(lang);
}
});
*/
});
});
</script>

View file

@ -3,7 +3,7 @@ import { getLangFromUrl, useTranslations } from '../utils/i18n';
import type { CollectionEntry } from 'astro:content';
interface Props {
project: CollectionEntry<'projects'>;
project: CollectionEntry<'projects'>;
}
const { project } = Astro.props;
@ -13,11 +13,14 @@ const lang = getLangFromUrl(Astro.url);
const t = useTranslations(lang);
// Format date based on language
const formattedDate = new Intl.DateTimeFormat(lang === 'de' ? 'de-DE' : lang === 'it' ? 'it-IT' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(data.pubDate);
const formattedDate = new Intl.DateTimeFormat(
lang === 'de' ? 'de-DE' : lang === 'it' ? 'it-IT' : 'en-US',
{
year: 'numeric',
month: 'long',
day: 'numeric',
}
).format(data.pubDate);
// Generate project URL with language prefix
let projectUrl;
@ -31,182 +34,193 @@ const statusKey = `projects.status.${data.status}` as const;
---
<a href={projectUrl} class="project-card-link">
<article class:list={["project-card", { "featured": data.featured }]}>
{data.image && (
<div class="project-image-container">
<img src={data.image} alt={data.title} class="project-image" />
</div>
)}
<div class="content">
<div class="meta">
<span class="category">{t(categoryKey)}</span>
<span class="status" class:list={[data.status]}>{t(statusKey)}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
{data.technologies && data.technologies.length > 0 && (
<div class="technologies">
{data.technologies.map(tech => (
<span class="tech">{tech}</span>
))}
</div>
)}
<div class="footer">
<span class="author">{data.author}</span>
<span class="read-more">{t('projects.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
<article class:list={['project-card', { featured: data.featured }]}>
{
data.image && (
<div class="project-image-container">
<img src={data.image} alt={data.title} class="project-image" />
</div>
)
}
<div class="content">
<div class="meta">
<span class="category">{t(categoryKey)}</span>
<span class="status" class:list={[data.status]}>{t(statusKey)}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
{
data.technologies && data.technologies.length > 0 && (
<div class="technologies">
{data.technologies.map((tech) => (
<span class="tech">{tech}</span>
))}
</div>
)
}
<div class="footer">
<span class="author">{data.author}</span>
<span class="read-more">{t('projects.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
</a>
<style>
.project-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.project-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
height: 100%;
}
.project-card-link:hover .project-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.project-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.project-image-container {
width: 100%;
padding-top: 56.25%; /* Aspect ratio 16:9 */
position: relative;
overflow: hidden;
}
.project-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.category {
color: var(--accent-color);
font-weight: 600;
}
.status {
padding: 0.125rem 0.5rem;
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 500;
}
.status.active {
background-color: rgba(16, 185, 129, 0.2);
color: rgb(16, 185, 129);
}
.status.completed {
background-color: rgba(79, 70, 229, 0.2);
color: rgb(79, 70, 229);
}
.status.archived {
background-color: rgba(107, 114, 128, 0.2);
color: rgb(107, 114, 128);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
flex-grow: 1;
margin-bottom: 1.5rem;
}
.technologies {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.tech {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
color: var(--accent-color);
border-radius: 0.25rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.author {
font-size: 0.875rem;
color: var(--text-muted);
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.project-card-link:hover .arrow {
transform: translateX(4px);
}
</style>
.project-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.project-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
height: 100%;
}
.project-card-link:hover .project-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.08),
0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.project-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.project-image-container {
width: 100%;
padding-top: 56.25%; /* Aspect ratio 16:9 */
position: relative;
overflow: hidden;
}
.project-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.category {
color: var(--accent-color);
font-weight: 600;
}
.status {
padding: 0.125rem 0.5rem;
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 500;
}
.status.active {
background-color: rgba(16, 185, 129, 0.2);
color: rgb(16, 185, 129);
}
.status.completed {
background-color: rgba(79, 70, 229, 0.2);
color: rgb(79, 70, 229);
}
.status.archived {
background-color: rgba(107, 114, 128, 0.2);
color: rgb(107, 114, 128);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
flex-grow: 1;
margin-bottom: 1.5rem;
}
.technologies {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.tech {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
color: var(--accent-color);
border-radius: 0.25rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.author {
font-size: 0.875rem;
color: var(--text-muted);
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.project-card-link:hover .arrow {
transform: translateX(4px);
}
</style>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -4,9 +4,9 @@ import { getCollection } from 'astro:content';
import type { CollectionEntry } from 'astro:content';
interface Props {
maxMembers?: number;
showLinks?: boolean;
compact?: boolean;
maxMembers?: number;
showLinks?: boolean;
compact?: boolean;
}
const { maxMembers = 5, showLinks = true, compact = false } = Astro.props;
@ -17,187 +17,267 @@ const t = useTranslations(lang);
// Get members in current language
const allMembers = await getCollection('members', ({ id }) => {
return id.startsWith(lang + '/');
return id.startsWith(lang + '/');
});
// Sort by order, then by name
const sortedMembers = allMembers
.sort((a, b) => a.data.order - b.data.order || a.data.name.localeCompare(b.data.name))
.filter((member, index) => index < maxMembers);
.sort((a, b) => a.data.order - b.data.order || a.data.name.localeCompare(b.data.name))
.filter((member, index) => index < maxMembers);
---
<div class:list={["team-members", { compact }]}>
{sortedMembers.map((member) => (
<div class="member-card">
<div class="member-avatar">
{member.data.image ? (
<img src={member.data.image} alt={member.data.name} />
) : (
<div class="avatar-placeholder">{member.data.name.charAt(0)}</div>
)}
</div>
<div class="member-info">
<h3 class="member-name">{member.data.name}</h3>
<p class="member-role">{member.data.role}</p>
{!compact && <p class="member-bio">{member.data.bio}</p>}
{showLinks && (
<div class="member-links">
{member.data.github && (
<a href={`https://github.com/${member.data.github}`} target="_blank" rel="noopener noreferrer" class="social-link github" title="GitHub">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>
</a>
)}
{member.data.twitter && (
<a href={`https://twitter.com/${member.data.twitter}`} target="_blank" rel="noopener noreferrer" class="social-link twitter" title="Twitter">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path></svg>
</a>
)}
{member.data.linkedin && (
<a href={`https://linkedin.com/in/${member.data.linkedin}`} target="_blank" rel="noopener noreferrer" class="social-link linkedin" title="LinkedIn">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg>
</a>
)}
{member.data.website && (
<a href={member.data.website} target="_blank" rel="noopener noreferrer" class="social-link website" title="Website">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
</a>
)}
</div>
)}
</div>
</div>
))}
<div class:list={['team-members', { compact }]}>
{
sortedMembers.map((member) => (
<div class="member-card">
<div class="member-avatar">
{member.data.image ? (
<img src={member.data.image} alt={member.data.name} />
) : (
<div class="avatar-placeholder">{member.data.name.charAt(0)}</div>
)}
</div>
<div class="member-info">
<h3 class="member-name">{member.data.name}</h3>
<p class="member-role">{member.data.role}</p>
{!compact && <p class="member-bio">{member.data.bio}</p>}
{showLinks && (
<div class="member-links">
{member.data.github && (
<a
href={`https://github.com/${member.data.github}`}
target="_blank"
rel="noopener noreferrer"
class="social-link github"
title="GitHub"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" />
</svg>
</a>
)}
{member.data.twitter && (
<a
href={`https://twitter.com/${member.data.twitter}`}
target="_blank"
rel="noopener noreferrer"
class="social-link twitter"
title="Twitter"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z" />
</svg>
</a>
)}
{member.data.linkedin && (
<a
href={`https://linkedin.com/in/${member.data.linkedin}`}
target="_blank"
rel="noopener noreferrer"
class="social-link linkedin"
title="LinkedIn"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" />
<rect x="2" y="9" width="4" height="12" />
<circle cx="4" cy="4" r="2" />
</svg>
</a>
)}
{member.data.website && (
<a
href={member.data.website}
target="_blank"
rel="noopener noreferrer"
class="social-link website"
title="Website"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="2" y1="12" x2="22" y2="12" />
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
</svg>
</a>
)}
</div>
)}
</div>
</div>
))
}
</div>
<style>
.team-members {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
margin: 2rem 0;
}
.team-members.compact {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
.member-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 1.5rem;
border-radius: 0.75rem;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.1);
transition: transform 0.2s ease, border-color 0.2s ease;
}
.member-card:hover {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.2);
}
.member-avatar {
width: 90px;
height: 90px;
border-radius: 50%;
overflow: hidden;
margin-bottom: 1rem;
background-color: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.compact .member-avatar {
width: 70px;
height: 70px;
}
.member-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
font-weight: 700;
color: var(--accent-color);
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
}
.member-info {
text-align: center;
width: 100%;
}
.member-name {
font-size: 1.1rem;
font-weight: 600;
margin: 0.5rem 0 0.25rem;
}
.compact .member-name {
font-size: 1rem;
}
.member-role {
font-size: 0.9rem;
color: var(--accent-color);
margin: 0 0 0.75rem;
font-weight: 500;
}
.compact .member-role {
font-size: 0.8rem;
margin-bottom: 0.5rem;
}
.member-bio {
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-muted);
margin-bottom: 1rem;
}
.member-links {
display: flex;
justify-content: center;
gap: 0.75rem;
margin-top: 0.75rem;
}
.social-link {
color: var(--text-muted);
transition: color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.social-link:hover {
color: var(--accent-color);
}
@media (max-width: 768px) {
.team-members {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
.team-members.compact {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
}
</style>
.team-members {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
margin: 2rem 0;
}
.team-members.compact {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
.member-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 1.5rem;
border-radius: 0.75rem;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.1);
transition:
transform 0.2s ease,
border-color 0.2s ease;
}
.member-card:hover {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.2);
}
.member-avatar {
width: 90px;
height: 90px;
border-radius: 50%;
overflow: hidden;
margin-bottom: 1rem;
background-color: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.compact .member-avatar {
width: 70px;
height: 70px;
}
.member-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
font-weight: 700;
color: var(--accent-color);
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
}
.member-info {
text-align: center;
width: 100%;
}
.member-name {
font-size: 1.1rem;
font-weight: 600;
margin: 0.5rem 0 0.25rem;
}
.compact .member-name {
font-size: 1rem;
}
.member-role {
font-size: 0.9rem;
color: var(--accent-color);
margin: 0 0 0.75rem;
font-weight: 500;
}
.compact .member-role {
font-size: 0.8rem;
margin-bottom: 0.5rem;
}
.member-bio {
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-muted);
margin-bottom: 1rem;
}
.member-links {
display: flex;
justify-content: center;
gap: 0.75rem;
margin-top: 0.75rem;
}
.social-link {
color: var(--text-muted);
transition: color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.social-link:hover {
color: var(--accent-color);
}
@media (max-width: 768px) {
.team-members {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
.team-members.compact {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
}
</style>

View file

@ -6,86 +6,94 @@ const t = useTranslations(lang);
---
<button id="themeToggle" aria-label={t('nav.darkMode')}>
<svg width="20" height="20" viewBox="0 0 24 24">
<path
class="sun"
fill-rule="evenodd"
d="M12 17.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0 1.5a7 7 0 1 0 0-14 7 7 0 0 0 0 14zm12-7a.8.8 0 0 1-.8.8h-2.4a.8.8 0 0 1 0-1.6h2.4a.8.8 0 0 1 .8.8zM4 12a.8.8 0 0 1-.8.8H.8a.8.8 0 0 1 0-1.6h2.5a.8.8 0 0 1 .8.8zm16.5-8.5a.8.8 0 0 1 0 1l-1.8 1.8a.8.8 0 0 1-1-1l1.7-1.8a.8.8 0 0 1 1.1 0zM6.3 17.7a.8.8 0 0 1 0 1l-1.7 1.8a.8.8 0 1 1-1.1-1l1.7-1.8a.8.8 0 0 1 1.1 0zM12 0a.8.8 0 0 1 .8.8v2.5a.8.8 0 0 1-1.6 0V.8A.8.8 0 0 1 12 0zm0 20a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-1.6 0v-2.4a.8.8 0 0 1 .8-.8zM3.5 3.5a.8.8 0 0 1 1 0l1.8 1.8a.8.8 0 1 1-1 1L3.5 4.6a.8.8 0 0 1 0-1.1zm14.2 14.2a.8.8 0 0 1 1 0l1.8 1.7a.8.8 0 0 1-1 1l-1.8-1.7a.8.8 0 0 1 0-1z"
></path>
<path
class="moon"
fill-rule="evenodd"
d="M16.5 6A10.5 10.5 0 0 1 4.7 16.4 8.5 8.5 0 1 0 16.4 4.7l.1 1.3zm-1.7-2a9 9 0 0 1 .2 2 9 9 0 0 1-11 8.8 9.4 9.4 0 0 1-.8-.3c-.4 0-.8.3-.7.7a10 10 0 0 0 .3.8 10 10 0 0 0 9.2 6 10 10 0 0 0 4-19.2 9.7 9.7 0 0 0-.9-.3c-.3-.1-.7.3-.6.7a9 9 0 0 1 .3.8z"
></path>
</svg>
<svg width="20" height="20" viewBox="0 0 24 24">
<path
class="sun"
fill-rule="evenodd"
d="M12 17.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0 1.5a7 7 0 1 0 0-14 7 7 0 0 0 0 14zm12-7a.8.8 0 0 1-.8.8h-2.4a.8.8 0 0 1 0-1.6h2.4a.8.8 0 0 1 .8.8zM4 12a.8.8 0 0 1-.8.8H.8a.8.8 0 0 1 0-1.6h2.5a.8.8 0 0 1 .8.8zm16.5-8.5a.8.8 0 0 1 0 1l-1.8 1.8a.8.8 0 0 1-1-1l1.7-1.8a.8.8 0 0 1 1.1 0zM6.3 17.7a.8.8 0 0 1 0 1l-1.7 1.8a.8.8 0 1 1-1.1-1l1.7-1.8a.8.8 0 0 1 1.1 0zM12 0a.8.8 0 0 1 .8.8v2.5a.8.8 0 0 1-1.6 0V.8A.8.8 0 0 1 12 0zm0 20a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-1.6 0v-2.4a.8.8 0 0 1 .8-.8zM3.5 3.5a.8.8 0 0 1 1 0l1.8 1.8a.8.8 0 1 1-1 1L3.5 4.6a.8.8 0 0 1 0-1.1zm14.2 14.2a.8.8 0 0 1 1 0l1.8 1.7a.8.8 0 0 1-1 1l-1.8-1.7a.8.8 0 0 1 0-1z"
></path>
<path
class="moon"
fill-rule="evenodd"
d="M16.5 6A10.5 10.5 0 0 1 4.7 16.4 8.5 8.5 0 1 0 16.4 4.7l.1 1.3zm-1.7-2a9 9 0 0 1 .2 2 9 9 0 0 1-11 8.8 9.4 9.4 0 0 1-.8-.3c-.4 0-.8.3-.7.7a10 10 0 0 0 .3.8 10 10 0 0 0 9.2 6 10 10 0 0 0 4-19.2 9.7 9.7 0 0 0-.9-.3c-.3-.1-.7.3-.6.7a9 9 0 0 1 .3.8z"
></path>
</svg>
</button>
<style>
#themeToggle {
border: 0;
background: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
position: relative;
}
#themeToggle:hover {
background-color: var(--hover-bg);
}
#themeToggle {
border: 0;
background: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
position: relative;
}
.sun { fill: var(--text-color); }
.moon { fill: transparent; }
:global(.dark) .sun { fill: transparent; }
:global(.dark) .moon { fill: var(--text-color); }
@media (max-width: 768px) {
#themeToggle {
padding: 0.75rem;
width: 100%;
justify-content: flex-start;
}
#themeToggle::after {
content: attr(aria-label);
margin-left: 0.75rem;
font-size: 1rem;
color: var(--text-color);
}
}
#themeToggle:hover {
background-color: var(--hover-bg);
}
.sun {
fill: var(--text-color);
}
.moon {
fill: transparent;
}
:global(.dark) .sun {
fill: transparent;
}
:global(.dark) .moon {
fill: var(--text-color);
}
@media (max-width: 768px) {
#themeToggle {
padding: 0.75rem;
width: 100%;
justify-content: flex-start;
}
#themeToggle::after {
content: attr(aria-label);
margin-left: 0.75rem;
font-size: 1rem;
color: var(--text-color);
}
}
</style>
<script>
const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
})();
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
})();
window.localStorage.setItem('theme', theme);
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
const handleToggleClick = () => {
const element = document.documentElement;
element.classList.toggle('dark');
const isDark = element.classList.contains('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
};
window.localStorage.setItem('theme', theme);
document.getElementById('themeToggle').addEventListener('click', handleToggleClick);
</script>
const handleToggleClick = () => {
const element = document.documentElement;
element.classList.toggle('dark');
const isDark = element.classList.contains('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
};
document.getElementById('themeToggle').addEventListener('click', handleToggleClick);
</script>

View file

@ -1,10 +1,10 @@
---
import type { CollectionEntry } from "astro:content";
import { getLangFromUrl, useTranslations } from "../utils/i18n";
import type { CollectionEntry } from 'astro:content';
import { getLangFromUrl, useTranslations } from '../utils/i18n';
interface Props {
tool: CollectionEntry<"tools">;
isFeatured?: boolean;
tool: CollectionEntry<'tools'>;
isFeatured?: boolean;
}
const { tool, isFeatured = false } = Astro.props;
@ -13,7 +13,7 @@ const lang = getLangFromUrl(Astro.url);
const t = useTranslations(lang);
// Generate URL based on lang and only the filename part of the slug
const slugParts = slug.split("/");
const slugParts = slug.split('/');
const fileName = slugParts[slugParts.length - 1];
const url = `/${lang}/tools/${fileName}`;
@ -22,138 +22,141 @@ const categoryKey = `tools.categories.${data.category}` as const;
const pricingKey = `tools.pricing.${data.pricing}` as const;
// Default image if none is provided
const imageSrc = data.image ?? "/images/bauntown-codespilling.png";
const imageSrc = data.image ?? '/images/bauntown-codespilling.png';
---
<article class={`tool-card ${isFeatured ? "featured" : ""}`}>
<a href={url} class="card-link">
<div class="card-image-container">
<img src={imageSrc} alt={data.title} class="card-image" loading="lazy" />
</div>
<div class="card-content">
<div class="card-meta">
<span class="card-category">{t(categoryKey)}</span>
<span class="card-pricing">{t(pricingKey)}</span>
</div>
<h3 class="card-title">{data.title}</h3>
<p class="card-description">{data.description}</p>
</div>
</a>
<article class={`tool-card ${isFeatured ? 'featured' : ''}`}>
<a href={url} class="card-link">
<div class="card-image-container">
<img src={imageSrc} alt={data.title} class="card-image" loading="lazy" />
</div>
<div class="card-content">
<div class="card-meta">
<span class="card-category">{t(categoryKey)}</span>
<span class="card-pricing">{t(pricingKey)}</span>
</div>
<h3 class="card-title">{data.title}</h3>
<p class="card-description">{data.description}</p>
</div>
</a>
</article>
<style>
.tool-card {
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
transition: transform 0.2s ease, box-shadow 0.2s ease;
height: 100%;
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
}
.tool-card {
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
height: 100%;
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
}
.tool-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 30px -15px var(--shadow-color);
filter: brightness(1.1);
}
.tool-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 30px -15px var(--shadow-color);
filter: brightness(1.1);
}
.tool-card.featured {
border: 1px solid var(--accent-color);
}
.tool-card.featured {
border: 1px solid var(--accent-color);
}
.card-link {
display: flex;
flex-direction: column;
height: 100%;
color: var(--text-color);
text-decoration: none;
}
.card-link {
display: flex;
flex-direction: column;
height: 100%;
color: var(--text-color);
text-decoration: none;
}
.card-image-container {
aspect-ratio: 16 / 9;
overflow: hidden;
}
.card-image-container {
aspect-ratio: 16 / 9;
overflow: hidden;
}
.card-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.card-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.card-content {
padding: 1.5rem;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.card-content {
padding: 1.5rem;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.card-meta {
display: flex;
gap: 0.75rem;
margin-bottom: 0.75rem;
flex-wrap: wrap;
}
.card-meta {
display: flex;
gap: 0.75rem;
margin-bottom: 0.75rem;
flex-wrap: wrap;
}
.card-category, .card-pricing {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-weight: 500;
}
.card-category,
.card-pricing {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-weight: 500;
}
.card-category {
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
color: var(--accent-color);
}
.card-category {
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
color: var(--accent-color);
}
.card-pricing {
background-color: var(--surface-bg);
color: var(--text-muted);
}
.card-pricing {
background-color: var(--surface-bg);
color: var(--text-muted);
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin-top: 0.5rem;
margin-bottom: 0rem;
line-height: 1.4;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin-top: 0.5rem;
margin-bottom: 0rem;
line-height: 1.4;
}
.card-description {
font-size: 0.875rem;
color: var(--text-muted);
flex-grow: 1;
line-height: 1.6;
margin-top: 0.25rem;
margin-bottom: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-description {
font-size: 0.875rem;
color: var(--text-muted);
flex-grow: 1;
line-height: 1.6;
margin-top: 0.25rem;
margin-bottom: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-footer {
margin-top: auto;
display: flex;
align-items: center;
justify-content: space-between;
}
.card-footer {
margin-top: auto;
display: flex;
align-items: center;
justify-content: space-between;
}
.read-more {
color: var(--accent-color);
font-size: 0.875rem;
font-weight: 500;
}
.read-more {
color: var(--accent-color);
font-size: 0.875rem;
font-weight: 500;
}
.tool-card:hover .read-more {
text-decoration: underline;
}
.tool-card:hover .read-more {
text-decoration: underline;
}
@media (max-width: 768px) {
.card-image-container {
aspect-ratio: 3 / 2;
}
}
</style>
@media (max-width: 768px) {
.card-image-container {
aspect-ratio: 3 / 2;
}
}
</style>

View file

@ -3,7 +3,7 @@ import { getLangFromUrl, useTranslations } from '../utils/i18n';
import type { CollectionEntry } from 'astro:content';
interface Props {
tutorial: CollectionEntry<'tutorials'>;
tutorial: CollectionEntry<'tutorials'>;
}
const { tutorial } = Astro.props;
@ -14,9 +14,9 @@ const t = useTranslations(lang);
// Format date based on language
const formattedDate = new Intl.DateTimeFormat(lang === 'de' ? 'de-DE' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(data.pubDate);
// Generate tutorial URL with language prefix
@ -31,141 +31,150 @@ const categoryKey = `tutorials.categories.${data.category}` as const;
---
<a href={tutorialUrl} class="tutorial-card-link">
<article class:list={["tutorial-card", { "featured": data.featured }]}>
{data.image && (
<div class="tutorial-image-container">
<img src={data.image} alt={data.title} class="tutorial-image" />
</div>
)}
<div class="content">
<div class="meta">
<span class="category">{t(categoryKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<div class="footer">
<span class="author">{data.author}</span>
<span class="read-more">{t('tutorials.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
<article class:list={['tutorial-card', { featured: data.featured }]}>
{
data.image && (
<div class="tutorial-image-container">
<img src={data.image} alt={data.title} class="tutorial-image" />
</div>
)
}
<div class="content">
<div class="meta">
<span class="category">{t(categoryKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<div class="footer">
<span class="author">{data.author}</span>
<span class="read-more">{t('tutorials.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
</a>
<style>
.tutorial-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.tutorial-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
height: 100%;
}
.tutorial-card-link:hover .tutorial-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.tutorial-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.tutorial-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.tutorial-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.category {
color: var(--accent-color);
font-weight: 600;
}
.date {
color: var(--text-muted);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
flex-grow: 1;
margin-bottom: 1.5rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.author {
font-size: 0.875rem;
color: var(--text-muted);
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.tutorial-card-link:hover .arrow {
transform: translateX(4px);
}
</style>
.tutorial-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.tutorial-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
height: 100%;
}
.tutorial-card-link:hover .tutorial-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.08),
0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.tutorial-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.tutorial-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.tutorial-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.category {
color: var(--accent-color);
font-weight: 600;
}
.date {
color: var(--text-muted);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
flex-grow: 1;
margin-bottom: 1.5rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.author {
font-size: 0.875rem;
color: var(--text-muted);
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.tutorial-card-link:hover .arrow {
transform: translateX(4px);
}
</style>

View file

@ -3,7 +3,7 @@ import { getLangFromUrl, useTranslations } from '../utils/i18n';
import type { CollectionEntry } from 'astro:content';
interface Props {
vision: CollectionEntry<'vision'>;
vision: CollectionEntry<'vision'>;
}
const { vision } = Astro.props;
@ -14,9 +14,9 @@ const t = useTranslations(lang);
// Format date based on language
const formattedDate = new Intl.DateTimeFormat(lang === 'de' ? 'de-DE' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(data.pubDate);
// Generate vision URL with language prefix
@ -32,233 +32,244 @@ const statusKey = `vision.status.${data.status}` as const;
---
<a href={visionUrl} class="vision-card-link">
<article class:list={["vision-card", { "featured": data.featured }]}>
{data.image && (
<div class="vision-image-container">
<img src={data.image} alt={data.title} class="vision-image" />
<div class="status-badge" data-status={data.status}>
{t(statusKey)}
</div>
</div>
)}
<div class="content">
<div class="meta">
<span class="category" data-category={data.category}>{t(categoryKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<div class="details">
<div class="timeline">
<span class="timeline-icon">⏱️</span>
<span class="timeline-text">{data.timeline}</span>
</div>
</div>
<div class="footer">
{data.contributors && data.contributors.length > 0 && (
<div class="contributors">
<span class="contributors-count">{data.contributors.length}</span>
</div>
)}
<span class="read-more">{t('vision.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
<article class:list={['vision-card', { featured: data.featured }]}>
{
data.image && (
<div class="vision-image-container">
<img src={data.image} alt={data.title} class="vision-image" />
<div class="status-badge" data-status={data.status}>
{t(statusKey)}
</div>
</div>
)
}
<div class="content">
<div class="meta">
<span class="category" data-category={data.category}>{t(categoryKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<div class="details">
<div class="timeline">
<span class="timeline-icon">⏱️</span>
<span class="timeline-text">{data.timeline}</span>
</div>
</div>
<div class="footer">
{
data.contributors && data.contributors.length > 0 && (
<div class="contributors">
<span class="contributors-count">{data.contributors.length}</span>
</div>
)
}
<span class="read-more">{t('vision.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
</a>
<style>
.vision-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.vision-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
height: 100%;
}
.vision-card-link:hover .vision-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.vision-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.vision-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.vision-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.status-badge {
position: absolute;
top: 1rem;
right: 1rem;
padding: 0.35rem 0.75rem;
border-radius: 2rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
z-index: 2;
color: white;
}
.status-badge[data-status="current"] {
background-color: #10b981; /* Green */
}
.status-badge[data-status="planned"] {
background-color: #6366f1; /* Purple */
}
.status-badge[data-status="exploring"] {
background-color: #f59e0b; /* Amber */
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.category {
font-weight: 600;
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
}
.category[data-category="product"] {
background-color: #dcfce7; /* Light green */
color: #16a34a;
}
.category[data-category="technology"] {
background-color: #e0f2fe; /* Light blue */
color: #0284c7;
}
.category[data-category="community"] {
background-color: #fef3c7; /* Light yellow */
color: #b45309;
}
.category[data-category="future"] {
background-color: #ede9fe; /* Light purple */
color: #7c3aed;
}
.date {
color: var(--text-muted);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
margin-bottom: 1.5rem;
}
.details {
margin-bottom: 1.5rem;
}
.timeline {
display: flex;
align-items: center;
gap: 0.5rem;
}
.timeline-icon {
font-size: 1rem;
}
.timeline-text {
font-size: 0.875rem;
color: var(--text-color);
font-weight: 500;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.contributors {
display: flex;
align-items: center;
}
.contributors-count {
font-size: 0.875rem;
color: var(--text-muted);
display: flex;
align-items: center;
}
.contributors-count::before {
content: '👥 ';
margin-right: 0.25rem;
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.vision-card-link:hover .arrow {
transform: translateX(4px);
}
</style>
.vision-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.vision-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
height: 100%;
}
.vision-card-link:hover .vision-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.08),
0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.vision-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.vision-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.vision-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.status-badge {
position: absolute;
top: 1rem;
right: 1rem;
padding: 0.35rem 0.75rem;
border-radius: 2rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
z-index: 2;
color: white;
}
.status-badge[data-status='current'] {
background-color: #10b981; /* Green */
}
.status-badge[data-status='planned'] {
background-color: #6366f1; /* Purple */
}
.status-badge[data-status='exploring'] {
background-color: #f59e0b; /* Amber */
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.category {
font-weight: 600;
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
}
.category[data-category='product'] {
background-color: #dcfce7; /* Light green */
color: #16a34a;
}
.category[data-category='technology'] {
background-color: #e0f2fe; /* Light blue */
color: #0284c7;
}
.category[data-category='community'] {
background-color: #fef3c7; /* Light yellow */
color: #b45309;
}
.category[data-category='future'] {
background-color: #ede9fe; /* Light purple */
color: #7c3aed;
}
.date {
color: var(--text-muted);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
margin-bottom: 1.5rem;
}
.details {
margin-bottom: 1.5rem;
}
.timeline {
display: flex;
align-items: center;
gap: 0.5rem;
}
.timeline-icon {
font-size: 1rem;
}
.timeline-text {
font-size: 0.875rem;
color: var(--text-color);
font-weight: 500;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.contributors {
display: flex;
align-items: center;
}
.contributors-count {
font-size: 0.875rem;
color: var(--text-muted);
display: flex;
align-items: center;
}
.contributors-count::before {
content: '👥 ';
margin-right: 0.25rem;
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.vision-card-link:hover .arrow {
transform: translateX(4px);
}
</style>

View file

@ -1,40 +1,41 @@
---
interface Props {
videoId: string;
title?: string;
aspectRatio?: string;
videoId: string;
title?: string;
aspectRatio?: string;
}
const { videoId, title = "YouTube video", aspectRatio = "16/9" } = Astro.props;
const { videoId, title = 'YouTube video', aspectRatio = '16/9' } = Astro.props;
---
<div class="youtube-video-container">
<iframe
src={`https://www.youtube.com/embed/${videoId}`}
title={title}
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen="true"
></iframe>
<iframe
src={`https://www.youtube.com/embed/${videoId}`}
title={title}
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen="true"></iframe>
</div>
<style define:vars={{ aspectRatio }}>
.youtube-video-container {
position: relative;
width: 100%;
aspect-ratio: var(--aspectRatio);
margin: 2rem 0;
border-radius: 0.75rem;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.youtube-video-container {
position: relative;
width: 100%;
aspect-ratio: var(--aspectRatio);
margin: 2rem 0;
border-radius: 0.75rem;
overflow: hidden;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
</style>
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
</style>

View file

@ -1,181 +1,199 @@
import { defineCollection, z } from 'astro:content';
const toolsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['Design', 'Development', 'Productivity', 'Hosting']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
website: z.string().optional(),
pricing: z.enum(['Free', 'Freemium', 'Paid']).default('Freemium'),
tags: z.array(z.string()).optional(),
externalLinks: z.array(z.object({
title: z.string(),
url: z.string(),
})).optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['Design', 'Development', 'Productivity', 'Hosting']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
website: z.string().optional(),
pricing: z.enum(['Free', 'Freemium', 'Paid']).default('Freemium'),
tags: z.array(z.string()).optional(),
externalLinks: z
.array(
z.object({
title: z.string(),
url: z.string(),
})
)
.optional(),
}),
});
const newsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['AI', 'Web', 'Development', 'Design', 'Community', 'Product']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
tags: z.array(z.string()).optional(),
externalLinks: z.array(z.object({
title: z.string(),
url: z.string(),
})).optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['AI', 'Web', 'Development', 'Design', 'Community', 'Product']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
tags: z.array(z.string()).optional(),
externalLinks: z
.array(
z.object({
title: z.string(),
url: z.string(),
})
)
.optional(),
}),
});
const modelsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['Text', 'Bild']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
tags: z.array(z.string()).optional(),
externalLinks: z.array(z.object({
title: z.string(),
url: z.string(),
})).optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['Text', 'Bild']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
tags: z.array(z.string()).optional(),
externalLinks: z
.array(
z.object({
title: z.string(),
url: z.string(),
})
)
.optional(),
}),
});
const projectsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['Web', 'Mobile', 'Desktop', 'IoT', 'AI', 'Design']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
status: z.enum(['active', 'completed', 'archived']).default('active'),
tags: z.array(z.string()).optional(),
githubUrl: z.string().optional(),
demoUrl: z.string().optional(),
technologies: z.array(z.string()).optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['Web', 'Mobile', 'Desktop', 'IoT', 'AI', 'Design']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
status: z.enum(['active', 'completed', 'archived']).default('active'),
tags: z.array(z.string()).optional(),
githubUrl: z.string().optional(),
demoUrl: z.string().optional(),
technologies: z.array(z.string()).optional(),
}),
});
const tutorialsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['UI & UX', 'Business', 'Users', 'Branding', 'Marketing', 'Vibecoding']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
// Course related fields
course: z.string().optional(),
courseName: z.string().optional(),
lessonNumber: z.number().optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['UI & UX', 'Business', 'Users', 'Branding', 'Marketing', 'Vibecoding']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
// Course related fields
course: z.string().optional(),
courseName: z.string().optional(),
lessonNumber: z.number().optional(),
}),
});
const missionsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
difficulty: z.enum(['beginner', 'intermediate', 'advanced']),
duration: z.string(), // e.g. "2-3 hours", "1 day"
skills: z.array(z.string()),
image: z.string().optional(),
featured: z.boolean().default(false),
category: z.enum(['UI & UX', 'Business', 'Users', 'Branding', 'Marketing', 'Vibecoding']).default('UI & UX'),
status: z.enum(['active', 'completed', 'upcoming']).default('active'),
participants: z.array(z.string()).optional(),
githubRepo: z.string().optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
difficulty: z.enum(['beginner', 'intermediate', 'advanced']),
duration: z.string(), // e.g. "2-3 hours", "1 day"
skills: z.array(z.string()),
image: z.string().optional(),
featured: z.boolean().default(false),
category: z
.enum(['UI & UX', 'Business', 'Users', 'Branding', 'Marketing', 'Vibecoding'])
.default('UI & UX'),
status: z.enum(['active', 'completed', 'upcoming']).default('active'),
participants: z.array(z.string()).optional(),
githubRepo: z.string().optional(),
}),
});
const visionCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
image: z.string().optional(),
featured: z.boolean().default(false),
category: z.enum(['product', 'technology', 'community', 'future']),
timeline: z.string(), // e.g. "2025-2030", "Long-term"
status: z.enum(['current', 'planned', 'exploring']),
contributors: z.array(z.string()).optional(),
relatedLinks: z.array(z.object({
title: z.string(),
url: z.string(),
})).optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
image: z.string().optional(),
featured: z.boolean().default(false),
category: z.enum(['product', 'technology', 'community', 'future']),
timeline: z.string(), // e.g. "2025-2030", "Long-term"
status: z.enum(['current', 'planned', 'exploring']),
contributors: z.array(z.string()).optional(),
relatedLinks: z
.array(
z.object({
title: z.string(),
url: z.string(),
})
)
.optional(),
}),
});
const joinCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
image: z.string().optional(),
heroTitle: z.string().optional(),
heroDescription: z.string().optional(),
newsletterTitle: z.string().optional(),
newsletterDescription: z.string().optional(),
submissionTitle: z.string().optional(),
submissionDescription: z.string().optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
image: z.string().optional(),
heroTitle: z.string().optional(),
heroDescription: z.string().optional(),
newsletterTitle: z.string().optional(),
newsletterDescription: z.string().optional(),
submissionTitle: z.string().optional(),
submissionDescription: z.string().optional(),
}),
});
const membersCollection = defineCollection({
type: 'content',
schema: z.object({
name: z.string(),
role: z.string(),
bio: z.string(),
image: z.string().optional(),
github: z.string().optional(),
twitter: z.string().optional(),
linkedin: z.string().optional(),
website: z.string().optional(),
featured: z.boolean().default(false),
order: z.number().default(999),
}),
type: 'content',
schema: z.object({
name: z.string(),
role: z.string(),
bio: z.string(),
image: z.string().optional(),
github: z.string().optional(),
twitter: z.string().optional(),
linkedin: z.string().optional(),
website: z.string().optional(),
featured: z.boolean().default(false),
order: z.number().default(999),
}),
});
export const collections = {
'news': newsCollection,
'models': modelsCollection,
'projects': projectsCollection,
'tutorials': tutorialsCollection,
'missions': missionsCollection,
'vision': visionCollection,
'join': joinCollection,
'members': membersCollection,
'tools': toolsCollection,
};
news: newsCollection,
models: modelsCollection,
projects: projectsCollection,
tutorials: tutorialsCollection,
missions: missionsCollection,
vision: visionCollection,
join: joinCollection,
members: membersCollection,
tools: toolsCollection,
};

View file

@ -26,4 +26,4 @@ Als Open-Source-Projekt lebt BaunTown von den Beiträgen der Community. Egal ob
4. Mache deine Änderungen und committe sie
5. Pushe deinen Branch und erstelle einen Pull Request
Wir freuen uns auf deine Beiträge!
Wir freuen uns auf deine Beiträge!

View file

@ -23,4 +23,4 @@ Schau dir die Highlights und Projekte unserer vergangenen Events an und hol dir
### Eigenes Event organisieren
Möchtest du ein lokales BaunTown-Meetup in deiner Stadt organisieren? Wir unterstützen dich gerne dabei! Kontaktiere uns für weitere Informationen und Ressourcen.
Möchtest du ein lokales BaunTown-Meetup in deiner Stadt organisieren? Wir unterstützen dich gerne dabei! Kontaktiere uns für weitere Informationen und Ressourcen.

View file

@ -25,4 +25,4 @@ BaunTown ist ein Projekt, das von Leidenschaft und Community-Engagement angetrie
- **Einmalige Spende**: Auch einmalige Spenden sind willkommen.
- **Unternehmenssponsoring**: Interesse an Unternehmenssponsoring? Kontaktiere uns direkt.
Jeder Beitrag zählt und hilft uns, BaunTown weiterzuentwickeln!
Jeder Beitrag zählt und hilft uns, BaunTown weiterzuentwickeln!

View file

@ -26,4 +26,4 @@ As an open-source project, BaunTown thrives on community contributions. Whether
4. Make your changes and commit them
5. Push your branch and create a pull request
We look forward to your contributions!
We look forward to your contributions!

View file

@ -23,4 +23,4 @@ Check out the highlights and projects from our past events and get inspiration f
### Organize Your Own Event
Would you like to organize a local BaunTown meetup in your city? We're happy to support you! Contact us for more information and resources.
Would you like to organize a local BaunTown meetup in your city? We're happy to support you! Contact us for more information and resources.

View file

@ -25,4 +25,4 @@ BaunTown is a project fueled by passion and community engagement. To advance our
- **One-time Donations**: One-time donations are also welcome.
- **Corporate Sponsorship**: Interested in corporate sponsorship? Contact us directly.
Every contribution counts and helps us continue developing BaunTown!
Every contribution counts and helps us continue developing BaunTown!

View file

@ -26,4 +26,4 @@ Come progetto open-source, BaunTown prospera grazie ai contributi della comunit
4. Fai le tue modifiche e committale
5. Pusha il tuo branch e crea una pull request
Aspettiamo con ansia i tuoi contributi!
Aspettiamo con ansia i tuoi contributi!

View file

@ -23,4 +23,4 @@ Dai un'occhiata ai momenti salienti e ai progetti dei nostri eventi passati e tr
### Organizza il Tuo Evento
Vorresti organizzare un meetup locale di BaunTown nella tua città? Siamo felici di supportarti! Contattaci per maggiori informazioni e risorse.
Vorresti organizzare un meetup locale di BaunTown nella tua città? Siamo felici di supportarti! Contattaci per maggiori informazioni e risorse.

View file

@ -33,4 +33,4 @@ Il tuo feedback ci aiuta a migliorare la piattaforma. Facci sapere cosa ti piace
### Contribuisci al Codice
BaunTown è un progetto open source. Se hai competenze tecniche, puoi contribuire direttamente al codice o aiutare con la documentazione.
BaunTown è un progetto open source. Se hai competenze tecniche, puoi contribuire direttamente al codice o aiutare con la documentazione.

View file

@ -25,4 +25,4 @@ BaunTown è un progetto alimentato dalla passione e dal coinvolgimento della com
- **Donazioni Una Tantum**: Anche le donazioni una tantum sono benvenute.
- **Sponsorizzazione Aziendale**: Interessato alla sponsorizzazione aziendale? Contattaci direttamente.
Ogni contributo conta e ci aiuta a continuare a sviluppare BaunTown!
Ogni contributo conta e ci aiuta a continuare a sviluppare BaunTown!

View file

@ -1,10 +1,10 @@
---
name: "Nils Weiser"
role: "Backend-Entwickler"
bio: "Spezialist für Serverarchitektur und Datenbanken mit Schwerpunkt auf skalierbaren Lösungen. Nils ist verantwortlich für die robuste technische Infrastruktur von BaunTown."
image: "/images/members/avatar-placeholder.png"
github: "nilsweiser"
linkedin: "nils-weiser"
name: 'Nils Weiser'
role: 'Backend-Entwickler'
bio: 'Spezialist für Serverarchitektur und Datenbanken mit Schwerpunkt auf skalierbaren Lösungen. Nils ist verantwortlich für die robuste technische Infrastruktur von BaunTown.'
image: '/images/members/avatar-placeholder.png'
github: 'nilsweiser'
linkedin: 'nils-weiser'
featured: true
order: 3
---
---

View file

@ -1,11 +1,11 @@
---
name: "Till Schneider"
role: "Gründer & Entwickler"
bio: "Full-Stack Entwickler mit Fokus auf Web-Technologien und KI-Integration. Till hat BaunTown gegründet, um eine Plattform für offenes, kollaboratives Lernen zu schaffen."
image: "/images/members/avatar-placeholder.png"
github: "tillschneider"
linkedin: "tillschneider"
website: "https://bauntown.com"
name: 'Till Schneider'
role: 'Gründer & Entwickler'
bio: 'Full-Stack Entwickler mit Fokus auf Web-Technologien und KI-Integration. Till hat BaunTown gegründet, um eine Plattform für offenes, kollaboratives Lernen zu schaffen.'
image: '/images/members/avatar-placeholder.png'
github: 'tillschneider'
linkedin: 'tillschneider'
website: 'https://bauntown.com'
featured: true
order: 1
---
---

View file

@ -1,11 +1,11 @@
---
name: "Tobias Müller"
role: "Design Lead"
bio: "UI/UX Designer mit Erfahrung in der Gestaltung intuitiver Benutzeroberflächen. Tobias bringt seine Expertise für visuelles Design und Benutzererfahrung in das BaunTown-Team ein."
image: "/images/members/avatar-placeholder.png"
github: "tobiasmdesign"
twitter: "tobiasmdesign"
linkedin: "tobias-mueller-design"
name: 'Tobias Müller'
role: 'Design Lead'
bio: 'UI/UX Designer mit Erfahrung in der Gestaltung intuitiver Benutzeroberflächen. Tobias bringt seine Expertise für visuelles Design und Benutzererfahrung in das BaunTown-Team ein.'
image: '/images/members/avatar-placeholder.png'
github: 'tobiasmdesign'
twitter: 'tobiasmdesign'
linkedin: 'tobias-mueller-design'
featured: true
order: 2
---
---

View file

@ -1,10 +1,10 @@
---
name: "Nils Weiser"
role: "Backend Developer"
name: 'Nils Weiser'
role: 'Backend Developer'
bio: "Specialist in server architecture and databases with a focus on scalable solutions. Nils is responsible for BaunTown's robust technical infrastructure."
image: "/images/members/avatar-placeholder.png"
github: "nilsweiser"
linkedin: "nils-weiser"
image: '/images/members/avatar-placeholder.png'
github: 'nilsweiser'
linkedin: 'nils-weiser'
featured: true
order: 3
---
---

View file

@ -1,11 +1,11 @@
---
name: "Till Schneider"
role: "Founder & Developer"
bio: "Full-stack developer with a focus on web technologies and AI integration. Till founded BaunTown to create a platform for open, collaborative learning."
image: "/images/members/avatar-placeholder.png"
github: "tillschneider"
linkedin: "tillschneider"
website: "https://bauntown.com"
name: 'Till Schneider'
role: 'Founder & Developer'
bio: 'Full-stack developer with a focus on web technologies and AI integration. Till founded BaunTown to create a platform for open, collaborative learning.'
image: '/images/members/avatar-placeholder.png'
github: 'tillschneider'
linkedin: 'tillschneider'
website: 'https://bauntown.com'
featured: true
order: 1
---
---

View file

@ -1,11 +1,11 @@
---
name: "Tobias Müller"
role: "Design Lead"
bio: "UI/UX designer with experience crafting intuitive user interfaces. Tobias brings his expertise in visual design and user experience to the BaunTown team."
image: "/images/members/avatar-placeholder.png"
github: "tobiasmdesign"
twitter: "tobiasmdesign"
linkedin: "tobias-mueller-design"
name: 'Tobias Müller'
role: 'Design Lead'
bio: 'UI/UX designer with experience crafting intuitive user interfaces. Tobias brings his expertise in visual design and user experience to the BaunTown team.'
image: '/images/members/avatar-placeholder.png'
github: 'tobiasmdesign'
twitter: 'tobiasmdesign'
linkedin: 'tobias-mueller-design'
featured: true
order: 2
---
---

View file

@ -1,10 +1,10 @@
---
name: "Nils Weiser"
role: "Sviluppatore Backend"
bio: "Specialista in architettura server e database con focus su soluzioni scalabili. Nils è responsabile della robusta infrastruttura tecnica di BaunTown."
image: "/images/members/avatar-placeholder.png"
github: "nilsweiser"
linkedin: "nils-weiser"
name: 'Nils Weiser'
role: 'Sviluppatore Backend'
bio: 'Specialista in architettura server e database con focus su soluzioni scalabili. Nils è responsabile della robusta infrastruttura tecnica di BaunTown.'
image: '/images/members/avatar-placeholder.png'
github: 'nilsweiser'
linkedin: 'nils-weiser'
featured: true
order: 3
---
---

View file

@ -1,11 +1,11 @@
---
name: "Till Schneider"
role: "Fondatore & Sviluppatore"
bio: "Sviluppatore full-stack con focus su tecnologie web e integrazione AI. Till ha fondato BaunTown per creare una piattaforma di apprendimento aperto e collaborativo."
image: "/images/members/avatar-placeholder.png"
github: "tillschneider"
linkedin: "tillschneider"
website: "https://bauntown.com"
name: 'Till Schneider'
role: 'Fondatore & Sviluppatore'
bio: 'Sviluppatore full-stack con focus su tecnologie web e integrazione AI. Till ha fondato BaunTown per creare una piattaforma di apprendimento aperto e collaborativo.'
image: '/images/members/avatar-placeholder.png'
github: 'tillschneider'
linkedin: 'tillschneider'
website: 'https://bauntown.com'
featured: true
order: 1
---
---

View file

@ -1,11 +1,11 @@
---
name: "Tobias Müller"
role: "Lead Designer"
bio: "Designer UI/UX con esperienza nella creazione di interfacce utente intuitive. Tobias porta la sua competenza in design visivo ed esperienza utente al team di BaunTown."
image: "/images/members/avatar-placeholder.png"
github: "tobiasmdesign"
twitter: "tobiasmdesign"
linkedin: "tobias-mueller-design"
name: 'Tobias Müller'
role: 'Lead Designer'
bio: 'Designer UI/UX con esperienza nella creazione di interfacce utente intuitive. Tobias porta la sua competenza in design visivo ed esperienza utente al team di BaunTown.'
image: '/images/members/avatar-placeholder.png'
github: 'tobiasmdesign'
twitter: 'tobiasmdesign'
linkedin: 'tobias-mueller-design'
featured: true
order: 2
---
---

View file

@ -1,17 +1,17 @@
---
title: "Google Gemini 2.5 Pro: Ein Durchbruch in der KI-Technologie"
description: "Google hat kürzlich das Gemini 2.5 Pro-Modell vorgestellt, das als das fortschrittlichste KI-Modell des Unternehmens gilt und mit seiner enormen Kontextfenstergröße neue Maßstäbe setzt."
title: 'Google Gemini 2.5 Pro: Ein Durchbruch in der KI-Technologie'
description: 'Google hat kürzlich das Gemini 2.5 Pro-Modell vorgestellt, das als das fortschrittlichste KI-Modell des Unternehmens gilt und mit seiner enormen Kontextfenstergröße neue Maßstäbe setzt.'
pubDate: 2025-03-25
category: "Text"
category: 'Text'
featured: true
author: "BaunTown"
tags: ["Google", "Gemini", "KI", "Multimodal", "Kontextfenster"]
image: "/images/models/google-gemini-2.5pro-bauntown.png"
author: 'BaunTown'
tags: ['Google', 'Gemini', 'KI', 'Multimodal', 'Kontextfenster']
image: '/images/models/google-gemini-2.5pro-bauntown.png'
externalLinks:
- title: "Offizielle Google Ankündigung"
url: "https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/#gemini-2-5-thinking"
- title: "Gemini - Ausprobieren"
url: "https://gemini.google.com"
- title: 'Offizielle Google Ankündigung'
url: 'https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/#gemini-2-5-thinking'
- title: 'Gemini - Ausprobieren'
url: 'https://gemini.google.com'
---
Google hat kürzlich das Gemini 2.5 Pro-Modell vorgestellt, das als das fortschrittlichste KI-Modell des Unternehmens gilt. Mit bahnbrechenden Funktionen wie multimodaler Verarbeitung, verbesserter Problemlösungsfähigkeit und einer enormen Kontextfenstergröße von bis zu 2 Millionen Tokens setzt dieses Modell neue Maßstäbe in der KI-Entwicklung. Die Reaktionen auf diese Innovation sind weltweit vielfältig und spiegeln sowohl Begeisterung als auch kritische Reflexion wider.

View file

@ -1,19 +1,19 @@
---
title: "Revolution der KI-Bildgenerierung: OpenAIs GPT-4o setzt neue Maßstäbe"
description: "OpenAI hat kürzlich sein neuestes Modell, GPT-4o, vorgestellt, das die Bildgenerierung direkt integriert und damit einen bedeutenden Fortschritt in der KI-Technologie markiert."
title: 'Revolution der KI-Bildgenerierung: OpenAIs GPT-4o setzt neue Maßstäbe'
description: 'OpenAI hat kürzlich sein neuestes Modell, GPT-4o, vorgestellt, das die Bildgenerierung direkt integriert und damit einen bedeutenden Fortschritt in der KI-Technologie markiert.'
pubDate: 2025-03-25
category: "Bild"
category: 'Bild'
featured: true
author: "BaunTown"
tags: ["GPT-4o", "OpenAI", "KI", "Multimodal", "Bildgenerierung"]
image: "/images/models/openai-gpt4o-imagemode-bauntown.png"
author: 'BaunTown'
tags: ['GPT-4o', 'OpenAI', 'KI', 'Multimodal', 'Bildgenerierung']
image: '/images/models/openai-gpt4o-imagemode-bauntown.png'
externalLinks:
- title: "Offizielle OpenAI Ankündigung"
url: "https://openai.com/index/introducing-4o-image-generation/"
- title: "ChatGPT - Bilder erstellen"
url: "https://chat.openai.com"
- title: 'Offizielle OpenAI Ankündigung'
url: 'https://openai.com/index/introducing-4o-image-generation/'
- title: 'ChatGPT - Bilder erstellen'
url: 'https://chat.openai.com'
- title: "Sora - OpenAI's Video & Bild Plattform"
url: "https://sora.com"
url: 'https://sora.com'
---
OpenAI hat kürzlich sein neuestes Modell, GPT-4o, vorgestellt, das die Bildgenerierung direkt integriert und damit einen bedeutenden Fortschritt in der KI-Technologie markiert. Dieses Modell kann Bilder basierend auf Textaufforderungen erstellen, hochgeladene Bilder bearbeiten und sogar mehrere Objekte präzise darstellen. Es zeichnet sich durch eine verbesserte Textdarstellung in Bildern aus, ein Bereich, in dem frühere Modelle wie DALL-E oft Schwierigkeiten hatten.

View file

@ -1,17 +1,17 @@
---
title: "Google Gemini 2.5 Pro: A Breakthrough in AI Technology"
title: 'Google Gemini 2.5 Pro: A Breakthrough in AI Technology'
description: "Google recently unveiled the Gemini 2.5 Pro model, considered the company's most advanced AI model, setting new standards with its massive context window."
pubDate: 2025-03-25
category: "Text"
category: 'Text'
featured: true
author: "BaunTown"
tags: ["Google", "Gemini", "AI", "Multimodal", "Context Window"]
image: "/images/models/google-gemini-2.5pro-bauntown.png"
author: 'BaunTown'
tags: ['Google', 'Gemini', 'AI', 'Multimodal', 'Context Window']
image: '/images/models/google-gemini-2.5pro-bauntown.png'
externalLinks:
- title: "Official Google Announcement"
url: "https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/#gemini-2-5-thinking"
- title: "Try Gemini"
url: "https://gemini.google.com"
- title: 'Official Google Announcement'
url: 'https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/#gemini-2-5-thinking'
- title: 'Try Gemini'
url: 'https://gemini.google.com'
---
Google recently unveiled the Gemini 2.5 Pro model, considered the company's most advanced AI model. With groundbreaking features such as multimodal processing, improved problem-solving capabilities, and an enormous context window of up to 2 million tokens, this model sets new standards in AI development. Reactions to this innovation are diverse worldwide, reflecting both enthusiasm and critical reflection.

View file

@ -1,19 +1,19 @@
---
title: "AI Image Generation Revolution: OpenAI's GPT-4o Sets New Standards"
description: "OpenAI recently unveiled its latest model, GPT-4o, which directly integrates image generation, marking a significant advancement in AI technology."
description: 'OpenAI recently unveiled its latest model, GPT-4o, which directly integrates image generation, marking a significant advancement in AI technology.'
pubDate: 2025-03-25
category: "Bild"
category: 'Bild'
featured: true
author: "BaunTown"
tags: ["GPT-4o", "OpenAI", "AI", "Multimodal", "Image Generation"]
image: "/images/models/openai-gpt4o-imagemode-bauntown.png"
author: 'BaunTown'
tags: ['GPT-4o', 'OpenAI', 'AI', 'Multimodal', 'Image Generation']
image: '/images/models/openai-gpt4o-imagemode-bauntown.png'
externalLinks:
- title: "Official OpenAI Announcement"
url: "https://openai.com/index/introducing-4o-image-generation/"
- title: "ChatGPT - Create Images"
url: "https://chat.openai.com"
- title: 'Official OpenAI Announcement'
url: 'https://openai.com/index/introducing-4o-image-generation/'
- title: 'ChatGPT - Create Images'
url: 'https://chat.openai.com'
- title: "Sora - OpenAI's Video & Image Platform"
url: "https://sora.com"
url: 'https://sora.com'
---
OpenAI recently unveiled its latest model, GPT-4o, which directly integrates image generation, marking a significant advancement in AI technology. This model can create images based on text prompts, edit uploaded images, and even accurately represent multiple objects. It stands out for its improved text rendering in images, an area where previous models like DALL-E often struggled.

View file

@ -1,17 +1,17 @@
---
title: "Google Gemini 2.5 Pro: Una svolta nella tecnologia AI"
title: 'Google Gemini 2.5 Pro: Una svolta nella tecnologia AI'
description: "Google ha recentemente presentato il modello Gemini 2.5 Pro, considerato il modello AI più avanzato dell'azienda, stabilendo nuovi standard con la sua enorme finestra di contesto."
pubDate: 2025-03-25
category: "Text"
category: 'Text'
featured: true
author: "BaunTown"
tags: ["Google", "Gemini", "IA", "Multimodale", "Finestra di contesto"]
image: "/images/models/google-gemini-2.5pro-bauntown.png"
author: 'BaunTown'
tags: ['Google', 'Gemini', 'IA', 'Multimodale', 'Finestra di contesto']
image: '/images/models/google-gemini-2.5pro-bauntown.png'
externalLinks:
- title: "Annuncio ufficiale di Google"
url: "https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/#gemini-2-5-thinking"
- title: "Prova Gemini"
url: "https://gemini.google.com"
- title: 'Annuncio ufficiale di Google'
url: 'https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/#gemini-2-5-thinking'
- title: 'Prova Gemini'
url: 'https://gemini.google.com'
---
Google ha recentemente presentato il modello Gemini 2.5 Pro, considerato il modello di intelligenza artificiale più avanzato dell'azienda. Con caratteristiche rivoluzionarie come l'elaborazione multimodale, migliori capacità di risoluzione dei problemi e un'enorme finestra di contesto fino a 2 milioni di token, questo modello stabilisce nuovi standard nello sviluppo dell'IA. Le reazioni a questa innovazione sono diverse in tutto il mondo, riflettendo sia entusiasmo che riflessione critica.

View file

@ -1,19 +1,19 @@
---
title: "Rivoluzione nella generazione di immagini AI: OpenAI GPT-4o stabilisce nuovi standard"
description: "OpenAI ha recentemente svelato il suo ultimo modello, GPT-4o, che integra direttamente la generazione di immagini, segnando un significativo avanzamento nella tecnologia AI."
title: 'Rivoluzione nella generazione di immagini AI: OpenAI GPT-4o stabilisce nuovi standard'
description: 'OpenAI ha recentemente svelato il suo ultimo modello, GPT-4o, che integra direttamente la generazione di immagini, segnando un significativo avanzamento nella tecnologia AI.'
pubDate: 2025-03-25
category: "Bild"
category: 'Bild'
featured: true
author: "BaunTown"
tags: ["GPT-4o", "OpenAI", "AI", "Multimodale", "Generazione di immagini"]
image: "/images/models/openai-gpt4o-imagemode-bauntown.png"
author: 'BaunTown'
tags: ['GPT-4o', 'OpenAI', 'AI', 'Multimodale', 'Generazione di immagini']
image: '/images/models/openai-gpt4o-imagemode-bauntown.png'
externalLinks:
- title: "Annuncio ufficiale di OpenAI"
url: "https://openai.com/index/introducing-4o-image-generation/"
- title: "ChatGPT - Crea immagini"
url: "https://chat.openai.com"
- title: "Sora - Piattaforma video e immagini di OpenAI"
url: "https://sora.com"
- title: 'Annuncio ufficiale di OpenAI'
url: 'https://openai.com/index/introducing-4o-image-generation/'
- title: 'ChatGPT - Crea immagini'
url: 'https://chat.openai.com'
- title: 'Sora - Piattaforma video e immagini di OpenAI'
url: 'https://sora.com'
---
OpenAI ha recentemente presentato il suo ultimo modello, GPT-4o, che integra direttamente la generazione di immagini, segnando un importante progresso nella tecnologia dell'IA. Questo modello può creare immagini basate su prompt testuali, modificare immagini caricate e persino rappresentare con precisione oggetti multipli. Si distingue per la sua migliore rappresentazione del testo nelle immagini, un'area in cui modelli precedenti come DALL-E spesso avevano difficoltà.

View file

@ -1,15 +1,15 @@
---
title: "BaunTown"
description: "Eine Community-Plattform zum Teilen von Wissen, Tools und Ressourcen, um Entwicklern zu helfen, gemeinsam bessere digitale Produkte zu erstellen."
title: 'BaunTown'
description: 'Eine Community-Plattform zum Teilen von Wissen, Tools und Ressourcen, um Entwicklern zu helfen, gemeinsam bessere digitale Produkte zu erstellen.'
pubDate: 2024-04-04
category: "Web"
category: 'Web'
featured: true
status: "active"
technologies: ["Astro", "TypeScript", "CSS", "Markdown", "Netlify", "Stripe"]
image: "/images/projects/bauntown-project-bauntown-platform.png"
author: "Till Schneider"
githubUrl: "https://github.com/bauntown/website"
demoUrl: "https://baun.town"
status: 'active'
technologies: ['Astro', 'TypeScript', 'CSS', 'Markdown', 'Netlify', 'Stripe']
image: '/images/projects/bauntown-project-bauntown-platform.png'
author: 'Till Schneider'
githubUrl: 'https://github.com/bauntown/website'
demoUrl: 'https://baun.town'
---
# BaunTown Plattform
@ -19,6 +19,7 @@ BaunTown ist unsere zentrale Wissensaustausch-Plattform, die geschaffen wurde, u
## Die Herausforderung
Nach Workshops und Schulungen standen wir immer wieder vor denselben Problemen:
- Teilnehmer fragten nach Assets, Dateien und Dokumentation
- Kein zentraler Ort zur Speicherung und gemeinsamen Nutzung von Materialien
- Schwierigkeiten bei der Nachverfolgung, wer Zugang zu welchen Ressourcen hat
@ -39,6 +40,7 @@ BaunTown bietet:
## Technische Umsetzung
Die Plattform nutzt einen modernen Tech-Stack:
- **Astro** für statische Site-Generierung mit minimalem JavaScript
- **Content Collections** für strukturierte, typsichere Inhalte
- **Markdown/MDX** für einfache Inhaltserstellung
@ -50,10 +52,11 @@ Die Plattform nutzt einen modernen Tech-Stack:
## Zukunftsvision
BaunTown wird sich weiterentwickeln mit:
- Erweiterter Tutorial-Bibliothek zu Entwicklungs- und Design-Themen
- Detailliertere Projektvorstellungen und Fallstudien
- Mitgliederprofile für Mitwirkende
- Verbesserte Community-Funktionen und Diskussionsmöglichkeiten
- Integration mit unseren anderen Produkten und Dienstleistungen
Die Plattform verkörpert unsere Werte des gemeinschaftlichen Lernens, Bauens und Verdienens - sie bietet eine Grundlage für den Wissensaustausch und demonstriert gleichzeitig die modernen Entwicklungsansätze, für die wir eintreten.
Die Plattform verkörpert unsere Werte des gemeinschaftlichen Lernens, Bauens und Verdienens - sie bietet eine Grundlage für den Wissensaustausch und demonstriert gleichzeitig die modernen Entwicklungsansätze, für die wir eintreten.

View file

@ -1,15 +1,16 @@
---
title: "Memoro"
description: "KI-gestützte mobile Anwendung zur Umwandlung von Gesprächen und Gedanken in strukturierte Dokumente."
title: 'Memoro'
description: 'KI-gestützte mobile Anwendung zur Umwandlung von Gesprächen und Gedanken in strukturierte Dokumente.'
pubDate: 2024-04-08
category: "Mobile"
category: 'Mobile'
featured: true
status: "active"
technologies: ["React Native", "Expo", "Supabase", "Azure OpenAI", "Azure Speech", "RevenueCat", "PostHog"]
image: "/images/projects/memoro-project-bauntown-app.png"
author: "Till Schneider"
githubUrl: "https://github.com/bauntown/memoro"
demoUrl: "https://www.memoro.ai"
status: 'active'
technologies:
['React Native', 'Expo', 'Supabase', 'Azure OpenAI', 'Azure Speech', 'RevenueCat', 'PostHog']
image: '/images/projects/memoro-project-bauntown-app.png'
author: 'Till Schneider'
githubUrl: 'https://github.com/bauntown/memoro'
demoUrl: 'https://www.memoro.ai'
---
# Memoro App
@ -77,4 +78,4 @@ Unsere Entwicklung konzentriert sich auf mehrere Schlüsselbereiche:
Wir verbessern außerdem unsere kontextbezogenen Verständnisfähigkeiten, um die Qualität der automatisierten Dokumentation in spezialisierten Domänen und technischen Bereichen weiter zu optimieren.
Memoro repräsentiert unsere Vision für Technologie, die in den Hintergrund tritt und gleichzeitig die menschliche Produktivität steigert - damit Menschen sich auf Gespräche und Ideen konzentrieren können, anstatt auf die Mechanik der Dokumentation.
Memoro repräsentiert unsere Vision für Technologie, die in den Hintergrund tritt und gleichzeitig die menschliche Produktivität steigert - damit Menschen sich auf Gespräche und Ideen konzentrieren können, anstatt auf die Mechanik der Dokumentation.

View file

@ -1,15 +1,15 @@
---
title: "BaunTown"
description: "A community platform for sharing knowledge, tools, and resources to help builders create better digital products together."
title: 'BaunTown'
description: 'A community platform for sharing knowledge, tools, and resources to help builders create better digital products together.'
pubDate: 2024-04-04
category: "Web"
category: 'Web'
featured: true
status: "active"
technologies: ["Astro", "TypeScript", "CSS", "Markdown", "Netlify", "Stripe"]
image: "/images/projects/bauntown-project-bauntown-platform.png"
author: "Till Schneider"
githubUrl: "https://github.com/bauntown/website"
demoUrl: "https://baun.town"
status: 'active'
technologies: ['Astro', 'TypeScript', 'CSS', 'Markdown', 'Netlify', 'Stripe']
image: '/images/projects/bauntown-project-bauntown-platform.png'
author: 'Till Schneider'
githubUrl: 'https://github.com/bauntown/website'
demoUrl: 'https://baun.town'
---
# BaunTown Platform
@ -19,6 +19,7 @@ BaunTown is our central knowledge-sharing platform, created to solve the recurri
## The Challenge
After hosting workshops and training sessions, we consistently faced the same problems:
- Participants asking for assets, files, and documentation
- No centralized place to store and share materials
- Difficulty tracking who has access to what resources
@ -39,6 +40,7 @@ BaunTown provides:
## Technical Implementation
The platform uses a modern tech stack:
- **Astro** for static site generation with minimal JavaScript
- **Content Collections** for structured, type-safe content
- **Markdown/MDX** for easy content creation
@ -50,10 +52,11 @@ The platform uses a modern tech stack:
## Future Vision
BaunTown will continue to evolve with:
- Expanded tutorial library covering development and design topics
- More detailed project showcases and case studies
- Member profiles for contributors
- Enhanced community features and discussion capabilities
- Integration with our other products and services
The platform embodies our values of collaborative learning, building, and earning - providing a foundation for sharing knowledge while demonstrating the modern development approaches we advocate for.
The platform embodies our values of collaborative learning, building, and earning - providing a foundation for sharing knowledge while demonstrating the modern development approaches we advocate for.

View file

@ -1,15 +1,16 @@
---
title: "Memoro"
description: "AI-powered mobile application for transforming conversations and thoughts into structured documents."
title: 'Memoro'
description: 'AI-powered mobile application for transforming conversations and thoughts into structured documents.'
pubDate: 2024-04-08
category: "Mobile"
category: 'Mobile'
featured: true
status: "active"
technologies: ["React Native", "Expo", "Supabase", "Azure OpenAI", "Azure Speech", "RevenueCat", "PostHog"]
image: "/images/projects/memoro-project-bauntown-app.png"
author: "Till Schneider"
githubUrl: "https://github.com/bauntown/memoro"
demoUrl: "https://www.memoro.ai"
status: 'active'
technologies:
['React Native', 'Expo', 'Supabase', 'Azure OpenAI', 'Azure Speech', 'RevenueCat', 'PostHog']
image: '/images/projects/memoro-project-bauntown-app.png'
author: 'Till Schneider'
githubUrl: 'https://github.com/bauntown/memoro'
demoUrl: 'https://www.memoro.ai'
---
# Memoro App
@ -77,4 +78,4 @@ Our development focuses on several key areas:
We're also enhancing our contextual understanding capabilities to further improve the quality of automated documentation across specialized domains and technical fields.
Memoro represents our vision for technology that fades into the background while amplifying human productivity - letting people focus on conversations and ideas rather than the mechanics of documentation.
Memoro represents our vision for technology that fades into the background while amplifying human productivity - letting people focus on conversations and ideas rather than the mechanics of documentation.

View file

@ -1,15 +1,15 @@
---
title: "BaunTown"
description: "Una piattaforma comunitaria per condividere conoscenze, strumenti e risorse per aiutare i costruttori a creare insieme prodotti digitali migliori."
title: 'BaunTown'
description: 'Una piattaforma comunitaria per condividere conoscenze, strumenti e risorse per aiutare i costruttori a creare insieme prodotti digitali migliori.'
pubDate: 2024-04-04
category: "Web"
category: 'Web'
featured: true
status: "active"
technologies: ["Astro", "TypeScript", "CSS", "Markdown", "Netlify", "Stripe"]
image: "/images/projects/bauntown-project-bauntown-platform.png"
author: "Till Schneider"
githubUrl: "https://github.com/bauntown/website"
demoUrl: "https://baun.town"
status: 'active'
technologies: ['Astro', 'TypeScript', 'CSS', 'Markdown', 'Netlify', 'Stripe']
image: '/images/projects/bauntown-project-bauntown-platform.png'
author: 'Till Schneider'
githubUrl: 'https://github.com/bauntown/website'
demoUrl: 'https://baun.town'
---
# Piattaforma BaunTown
@ -19,6 +19,7 @@ BaunTown è la nostra piattaforma centrale per la condivisione delle conoscenze,
## La Sfida
Dopo aver ospitato workshop e sessioni di formazione, abbiamo costantemente affrontato gli stessi problemi:
- Partecipanti che chiedevano assets, file e documentazione
- Nessun luogo centralizzato per archiviare e condividere materiali
- Difficoltà nel tracciare chi ha accesso a quali risorse
@ -39,6 +40,7 @@ BaunTown offre:
## Implementazione Tecnica
La piattaforma utilizza uno stack tecnologico moderno:
- **Astro** per la generazione di siti statici con JavaScript minimo
- **Content Collections** per contenuti strutturati e type-safe
- **Markdown/MDX** per una facile creazione di contenuti
@ -50,10 +52,11 @@ La piattaforma utilizza uno stack tecnologico moderno:
## Visione Futura
BaunTown continuerà a evolversi con:
- Libreria di tutorial ampliata che copre argomenti di sviluppo e design
- Vetrine di progetti più dettagliate e casi di studio
- Profili dei membri per i contributori
- Funzionalità comunitarie migliorate e capacità di discussione
- Integrazione con i nostri altri prodotti e servizi
La piattaforma incarna i nostri valori di apprendimento collaborativo, costruzione e guadagno - fornendo una base per la condivisione delle conoscenze mentre dimostra gli approcci di sviluppo moderni che sosteniamo.
La piattaforma incarna i nostri valori di apprendimento collaborativo, costruzione e guadagno - fornendo una base per la condivisione delle conoscenze mentre dimostra gli approcci di sviluppo moderni che sosteniamo.

View file

@ -1,15 +1,16 @@
---
title: "Memoro"
description: "Applicazione mobile basata su IA per trasformare conversazioni e pensieri in documenti strutturati."
title: 'Memoro'
description: 'Applicazione mobile basata su IA per trasformare conversazioni e pensieri in documenti strutturati.'
pubDate: 2024-04-08
category: "Mobile"
category: 'Mobile'
featured: true
status: "active"
technologies: ["React Native", "Expo", "Supabase", "Azure OpenAI", "Azure Speech", "RevenueCat", "PostHog"]
image: "/images/projects/memoro-project-bauntown-app.png"
author: "Till Schneider"
githubUrl: "https://github.com/bauntown/memoro"
demoUrl: "https://www.memoro.ai"
status: 'active'
technologies:
['React Native', 'Expo', 'Supabase', 'Azure OpenAI', 'Azure Speech', 'RevenueCat', 'PostHog']
image: '/images/projects/memoro-project-bauntown-app.png'
author: 'Till Schneider'
githubUrl: 'https://github.com/bauntown/memoro'
demoUrl: 'https://www.memoro.ai'
---
# App Memoro
@ -77,4 +78,4 @@ Il nostro sviluppo si concentra su diverse aree chiave:
Stiamo anche migliorando le nostre capacità di comprensione contestuale per migliorare ulteriormente la qualità della documentazione automatizzata in domini specializzati e campi tecnici.
Memoro rappresenta la nostra visione di una tecnologia che scompare sullo sfondo mentre amplifica la produttività umana - permettendo alle persone di concentrarsi sulle conversazioni e le idee piuttosto che sulla meccanica della documentazione.
Memoro rappresenta la nostra visione di una tecnologia che scompare sullo sfondo mentre amplifica la produttività umana - permettendo alle persone di concentrarsi sulle conversazioni e le idee piuttosto che sulla meccanica della documentazione.

View file

@ -82,4 +82,4 @@ Claude Code kann je nach Nutzungsintensität zu erheblichen Kosten führen:
Obwohl Claude Code noch in der Vorschauphase ist, zeigt es großes Potenzial für die Transformation der Softwareentwicklung. Es ermöglicht Entwicklern nicht nur Zeitersparnis, sondern auch eine tiefere Integration von KI in den Entwicklungsprozess. Mit zukünftigen Verbesserungen könnte es sogar möglich sein, komplette Projekte autonom zu generieren.
Claude Code markiert einen bedeutenden Schritt in Richtung einer neuen Ära der Softwareentwicklung einer Ära, in der Entwickler ihre Arbeit durch intelligente Automatisierung auf ein neues Niveau heben können. Trotz der höheren Kosten im Vergleich zu anderen KI-Codierungswerkzeugen bietet es in bestimmten Szenarien einen klaren Mehrwert, der die Investition rechtfertigt.
Claude Code markiert einen bedeutenden Schritt in Richtung einer neuen Ära der Softwareentwicklung einer Ära, in der Entwickler ihre Arbeit durch intelligente Automatisierung auf ein neues Niveau heben können. Trotz der höheren Kosten im Vergleich zu anderen KI-Codierungswerkzeugen bietet es in bestimmten Szenarien einen klaren Mehrwert, der die Investition rechtfertigt.

View file

@ -73,4 +73,4 @@ Claude hat sich zu einem unschätzbaren Werkzeug in unserem Workflow entwickelt
- Die nahtlose Integration in unsere bestehenden Tools und Workflows durch die API
- Die Fähigkeit, zeitaufwändige Aufgaben zu automatisieren und dadurch mehr Raum für kreative und strategische Arbeit zu schaffen
Claude ermöglicht es uns, effizienter zu arbeiten, während wir gleichzeitig die Qualität unserer Ausgaben verbessern, was ihn zu einem unverzichtbaren Teil unseres Toolsets macht.
Claude ermöglicht es uns, effizienter zu arbeiten, während wir gleichzeitig die Qualität unserer Ausgaben verbessern, was ihn zu einem unverzichtbaren Teil unseres Toolsets macht.

View file

@ -68,4 +68,4 @@ Als Entwickler haben wir festgestellt, dass Cursor unsere Produktivität erhebli
- Der Fehlersuche, da die KI oft Probleme erkennt, die leicht zu übersehen sind
- Der Dokumentation und dem Verständnis von vorhandenem Code
Cursor hat die Art und Weise verändert, wie wir Code schreiben, und ermöglicht es uns, uns mehr auf die Problemlösung und weniger auf die Syntax zu konzentrieren.
Cursor hat die Art und Weise verändert, wie wir Code schreiben, und ermöglicht es uns, uns mehr auf die Problemlösung und weniger auf die Syntax zu konzentrieren.

View file

@ -73,4 +73,4 @@ Discord hat sich als unverzichtbares Tool für unsere Community-Bildung und Team
- Die Fähigkeit, Interessengruppen innerhalb derselben Plattform zu verwalten
- Die kontinuierliche Entwicklung und Hinzufügung neuer Funktionen
Discord hat uns geholfen, eine lebendige, engagierte Community aufzubauen und gleichzeitig unsere internen Kommunikationsabläufe zu optimieren.
Discord hat uns geholfen, eine lebendige, engagierte Community aufzubauen und gleichzeitig unsere internen Kommunikationsabläufe zu optimieren.

View file

@ -70,4 +70,4 @@ Expo hat unsere mobile Anwendungsentwicklung revolutioniert aus mehreren Gründe
- **OTA-Updates**: Die Möglichkeit, Anwendungen zu aktualisieren, ohne durch App Store-Überprüfungen zu gehen, beschleunigt unseren Fehlerbehebungs- und Funktionsbereitstellungszyklus.
- **Vielseitigkeit**: Die Unterstützung für "Bare Workflow" bedeutet, dass wir bei Bedarf native Code-Erweiterungen hinzufügen können, was Flexibilität für komplexere Anwendungen bietet.
Für Teams, die schnell mobile Anwendungen entwickeln und bereitstellen möchten, besonders wenn sie bereits mit dem React-Ökosystem vertraut sind, ist Expo eine ausgezeichnete Wahl, die viele der komplexen Aspekte der nativen App-Entwicklung abstrahiert.
Für Teams, die schnell mobile Anwendungen entwickeln und bereitstellen möchten, besonders wenn sie bereits mit dem React-Ökosystem vertraut sind, ist Expo eine ausgezeichnete Wahl, die viele der komplexen Aspekte der nativen App-Entwicklung abstrahiert.

View file

@ -65,4 +65,4 @@ Figma bietet verschiedene Preisstufen an:
Figma hat unseren Design-Workflow revolutioniert durch seine Zugänglichkeit (browserbasiert, plattformübergreifend), kollaborative Funktionen und die nahtlose Integration in den Entwicklungsprozess. Es ermöglicht es uns, schneller zu iterieren und engere Zusammenarbeit zwischen Design und Entwicklung zu fördern.
Die intuitive Benutzeroberfläche und die umfangreiche Community mit Vorlagen und Plugins machen Figma zu einem unverzichtbaren Werkzeug in unserem Entwicklungsprozess.
Die intuitive Benutzeroberfläche und die umfangreiche Community mit Vorlagen und Plugins machen Figma zu einem unverzichtbaren Werkzeug in unserem Entwicklungsprozess.

View file

@ -76,4 +76,4 @@ GitHub ist mehr als nur ein Tool es ist ein Ökosystem, das moderne Software
- **Kontinuierliche Innovation**: Microsoft's Unterstützung hat das Innovationstempo beschleunigt, mit regelmäßigen neuen Funktionen, die die Plattform kontinuierlich verbessern.
- **Community**: Die riesige Nutzerbasis bedeutet, dass Unterstützung, Lösungen und Beispiele für fast jedes Problem leicht zu finden sind.
Für Teams jeder Größe bietet GitHub die nötige Infrastruktur, um effizient zusammenzuarbeiten, qualitativ hochwertige Software zu erstellen und kontinuierlich zu liefern.
Für Teams jeder Größe bietet GitHub die nötige Infrastruktur, um effizient zusammenzuarbeiten, qualitativ hochwertige Software zu erstellen und kontinuierlich zu liefern.

View file

@ -73,4 +73,4 @@ Google Workspace hat sich für uns als unverzichtbar erwiesen, besonders wegen:
- Der starken Mobil-Unterstützung, die es ermöglicht, von überall aus zu arbeiten
- Der umfassenden Sicherheitsfunktionen zum Schutz sensibler Daten
Google Workspace hat unsere teamübergreifende Kommunikation und Zusammenarbeit erheblich verbessert und bildet das Rückgrat unserer täglichen Arbeitsabläufe.
Google Workspace hat unsere teamübergreifende Kommunikation und Zusammenarbeit erheblich verbessert und bildet das Rückgrat unserer täglichen Arbeitsabläufe.

View file

@ -95,4 +95,4 @@ Memoro wurde von einem erfahrenen Team entwickelt:
## Zukunftsaussichten
Memoro entwickelt sich kontinuierlich weiter. Geplant ist unter anderem die Entwicklung einer Self-Service-Plattform, die es Kunden ermöglicht, eigene Dokumentationsvorlagen zu erstellen und zu teilen, sowie erweiterte Enterprise-Funktionen mit CRM/ERP-Integration.
Memoro entwickelt sich kontinuierlich weiter. Geplant ist unter anderem die Entwicklung einer Self-Service-Plattform, die es Kunden ermöglicht, eigene Dokumentationsvorlagen zu erstellen und zu teilen, sowie erweiterte Enterprise-Funktionen mit CRM/ERP-Integration.

View file

@ -71,4 +71,4 @@ Netlify hat unseren Deployment-Workflow erheblich vereinfacht. Zu den Vorteilen,
- **Serverlose Architektur**: Wir können dynamische Funktionen ohne eigene Server implementieren, was die Wartungskosten reduziert.
- **Skalierbarkeit**: Netlify wächst mit unseren Projekten und bietet zuverlässige Performance auch bei hohem Verkehrsaufkommen.
Ob für kleine persönliche Projekte oder komplexe Unternehmenswebsites - Netlify hat sich als zuverlässige und leistungsstarke Hosting-Lösung bewährt.
Ob für kleine persönliche Projekte oder komplexe Unternehmenswebsites - Netlify hat sich als zuverlässige und leistungsstarke Hosting-Lösung bewährt.

View file

@ -78,4 +78,4 @@ Wir empfehlen Plausible aus mehreren Gründen:
- **Transparenz**: Als Open-Source-Plattform bietet Plausible Einblick in die Funktionsweise, was Vertrauen schafft.
- **Ethische Alternative**: Plausible stellt eine ethischere Alternative zu den datengetriebenen, werbezentrierten Modellen der großen Tech-Unternehmen dar.
Für Website-Betreiber, die eine datenschutzfreundliche, einfache und effektive Analyseplattform suchen, ist Plausible eine ausgezeichnete Wahl, die technische Effizienz mit ethischen Grundsätzen verbindet.
Für Website-Betreiber, die eine datenschutzfreundliche, einfache und effektive Analyseplattform suchen, ist Plausible eine ausgezeichnete Wahl, die technische Effizienz mit ethischen Grundsätzen verbindet.

View file

@ -76,4 +76,4 @@ RevenueCat hat sich für uns aus mehreren Gründen als unverzichtbar erwiesen:
- **Flexibilität bei der Produktgestaltung**: Die Entitlements-API ermöglicht es uns, Produktangebote anzupassen und zu experimentieren, ohne App-Updates einreichen zu müssen.
- **Skalierbarkeit**: Die Plattform wächst mit unseren Apps und bietet robuste Lösungen sowohl für kleine als auch für große Anwendungen.
Für Entwickler, die In-App-Käufe oder Abonnements in ihre mobilen Apps implementieren möchten, bietet RevenueCat einen erheblichen Mehrwert und kann den Entwicklungsaufwand dramatisch reduzieren, während es gleichzeitig Einblicke und Flexibilität bietet, die für die Optimierung der App-Monetarisierung entscheidend sind.
Für Entwickler, die In-App-Käufe oder Abonnements in ihre mobilen Apps implementieren möchten, bietet RevenueCat einen erheblichen Mehrwert und kann den Entwicklungsaufwand dramatisch reduzieren, während es gleichzeitig Einblicke und Flexibilität bietet, die für die Optimierung der App-Monetarisierung entscheidend sind.

View file

@ -66,4 +66,4 @@ TestFlight hat sich als unverzichtbares Werkzeug in unserem iOS-Entwicklungsproz
- **Zuverlässige Bereitstellung**: Das System von Apple stellt sicher, dass Updates zuverlässig an Tester verteilt werden, mit minimalem Verwaltungsaufwand für das Entwicklungsteam.
- **Compliance und Sicherheit**: Die Plattform erfüllt die strengen Sicherheits- und Datenschutzanforderungen von Apple, was für die Verteilung von Software an externe Tester wichtig ist.
Für jedes Team, das iOS-Apps entwickelt, ist TestFlight zu einem Standard-Tool geworden, das den Übergang von der Entwicklung zur Produktion vereinfacht und sicherstellt, dass Apps gründlich getestet werden, bevor sie im App Store für die Öffentlichkeit zugänglich gemacht werden.
Für jedes Team, das iOS-Apps entwickelt, ist TestFlight zu einem Standard-Tool geworden, das den Übergang von der Entwicklung zur Produktion vereinfacht und sicherstellt, dass Apps gründlich getestet werden, bevor sie im App Store für die Öffentlichkeit zugänglich gemacht werden.

View file

@ -64,11 +64,11 @@ Windsurf bietet eine großzügige kostenlose Version mit erweiterten Funktionen
Windsurf hebt sich durch seine Fähigkeit hervor, sowohl als Copilot als auch als autonomer Agent zu agieren, was es von anderen IDEs wie Cursor unterscheidet.
| Feature | Windsurf Editor | Cursor | VS Code (mit Plugins) |
|---------------------------|---|---|---|
| KI-Agenten-Unterstützung | ✅ Ja | ❌ Nein | ❌ Nein |
| Multi-Datei-Bearbeitung | ✅ Ja | ✅ Ja | ❌ Nein |
| Vollständige Kontextbewusstheit | ✅ Ja | ❌ Nein | ❌ Nein |
| Feature | Windsurf Editor | Cursor | VS Code (mit Plugins) |
| ------------------------------- | --------------- | ------- | --------------------- |
| KI-Agenten-Unterstützung | ✅ Ja | ❌ Nein | ❌ Nein |
| Multi-Datei-Bearbeitung | ✅ Ja | ✅ Ja | ❌ Nein |
| Vollständige Kontextbewusstheit | ✅ Ja | ❌ Nein | ❌ Nein |
## Anwendungsfälle
@ -98,4 +98,4 @@ Windsurf AI bietet verschiedene Preisoptionen:
## Fazit
Windsurf AI ist mehr als nur ein Werkzeug es ist ein intelligenter Partner für Entwickler. Mit seinen innovativen Funktionen wie Cascade und Memories sowie seiner Fähigkeit zur tiefen Kontextanalyse revolutioniert es die Art und Weise, wie Software entwickelt wird. Für Anfänger ebenso wie für erfahrene Entwickler bietet Windsurf eine intuitive Benutzeroberfläche und eine Vielzahl von Funktionen, die den Workflow optimieren.
Windsurf AI ist mehr als nur ein Werkzeug es ist ein intelligenter Partner für Entwickler. Mit seinen innovativen Funktionen wie Cascade und Memories sowie seiner Fähigkeit zur tiefen Kontextanalyse revolutioniert es die Art und Weise, wie Software entwickelt wird. Für Anfänger ebenso wie für erfahrene Entwickler bietet Windsurf eine intuitive Benutzeroberfläche und eine Vielzahl von Funktionen, die den Workflow optimieren.

View file

@ -2,4 +2,4 @@
// You can run it with Node.js
// The problem might be with how Astro loads content collections
console.log("Debug file for tools collection");
console.log('Debug file for tools collection');

View file

@ -82,4 +82,4 @@ Claude Code can lead to significant costs depending on usage intensity:
Although Claude Code is still in the preview phase, it shows great potential for transforming software development. It not only saves developers time but also enables deeper integration of AI into the development process. With future improvements, it might even be possible to autonomously generate complete projects.
Claude Code marks a significant step towards a new era of software development an era in which developers can take their work to a new level through intelligent automation. Despite the higher costs compared to other AI coding tools, it offers clear added value in certain scenarios that justifies the investment.
Claude Code marks a significant step towards a new era of software development an era in which developers can take their work to a new level through intelligent automation. Despite the higher costs compared to other AI coding tools, it offers clear added value in certain scenarios that justifies the investment.

View file

@ -73,4 +73,4 @@ Claude has become an invaluable tool in our workflow for several reasons:
- The seamless integration into our existing tools and workflows through the API
- The ability to automate time-consuming tasks, thus creating more space for creative and strategic work
Claude enables us to work more efficiently while improving the quality of our outputs, making it an essential part of our toolset.
Claude enables us to work more efficiently while improving the quality of our outputs, making it an essential part of our toolset.

View file

@ -68,4 +68,4 @@ As developers, we've found that Cursor significantly increases our productivity,
- Debugging, as the AI often identifies problems that are easy to overlook
- Documenting and understanding existing code
Cursor has changed the way we write code, allowing us to focus more on problem-solving and less on syntax.
Cursor has changed the way we write code, allowing us to focus more on problem-solving and less on syntax.

View file

@ -73,4 +73,4 @@ Discord has proven to be an indispensable tool for our community building and te
- The capability to manage interest groups within the same platform
- The continuous development and addition of new features
Discord has helped us build a vibrant, engaged community while also streamlining our internal communication workflows.
Discord has helped us build a vibrant, engaged community while also streamlining our internal communication workflows.

View file

@ -70,4 +70,4 @@ Expo has revolutionized our mobile application development for several reasons:
- **OTA Updates**: The ability to update applications without going through app store reviews accelerates our bug fixing and feature deployment cycle.
- **Versatility**: The support for "bare workflow" means we can add native code extensions when needed, providing flexibility for more complex applications.
For teams looking to quickly develop and deploy mobile applications, especially if they're already familiar with the React ecosystem, Expo is an excellent choice that abstracts away many of the complex aspects of native app development.
For teams looking to quickly develop and deploy mobile applications, especially if they're already familiar with the React ecosystem, Expo is an excellent choice that abstracts away many of the complex aspects of native app development.

View file

@ -65,4 +65,4 @@ Figma offers various pricing tiers:
Figma has revolutionized our design workflow through its accessibility (browser-based, cross-platform), collaborative features, and seamless integration into the development process. It enables us to iterate faster and foster closer collaboration between design and development.
The intuitive user interface and extensive community with templates and plugins make Figma an indispensable tool in our development process.
The intuitive user interface and extensive community with templates and plugins make Figma an indispensable tool in our development process.

View file

@ -76,4 +76,4 @@ GitHub is more than just a tool it's an ecosystem that enables and promotes
- **Continuous Innovation**: Microsoft's support has accelerated the pace of innovation, with regular new features that continuously improve the platform.
- **Community**: The huge user base means that support, solutions, and examples for almost any problem are easy to find.
For teams of any size, GitHub provides the necessary infrastructure to collaborate efficiently, create high-quality software, and deliver continuously.
For teams of any size, GitHub provides the necessary infrastructure to collaborate efficiently, create high-quality software, and deliver continuously.

View file

@ -73,4 +73,4 @@ Google Workspace has proven essential for us, especially because of:
- The strong mobile support that enables working from anywhere
- The comprehensive security features to protect sensitive data
Google Workspace has significantly improved our cross-team communication and collaboration and forms the backbone of our daily workflows.
Google Workspace has significantly improved our cross-team communication and collaboration and forms the backbone of our daily workflows.

View file

@ -95,4 +95,4 @@ Memoro was developed by an experienced team:
## Future Outlook
Memoro continues to evolve. Plans include developing a self-service platform that allows customers to create and share their own documentation templates, as well as extended enterprise features with CRM/ERP integration.
Memoro continues to evolve. Plans include developing a self-service platform that allows customers to create and share their own documentation templates, as well as extended enterprise features with CRM/ERP integration.

View file

@ -71,4 +71,4 @@ Netlify has significantly simplified our deployment workflow. The benefits we pa
- **Serverless architecture**: We can implement dynamic features without our own servers, reducing maintenance costs.
- **Scalability**: Netlify grows with our projects and provides reliable performance even with high traffic volumes.
Whether for small personal projects or complex enterprise websites, Netlify has proven to be a reliable and powerful hosting solution.
Whether for small personal projects or complex enterprise websites, Netlify has proven to be a reliable and powerful hosting solution.

View file

@ -78,4 +78,4 @@ We recommend Plausible for several reasons:
- **Transparency**: As an open-source platform, Plausible offers insight into how it works, which builds trust.
- **Ethical Alternative**: Plausible represents a more ethical alternative to the data-driven, advertising-centric models of big tech companies.
For website operators looking for a privacy-friendly, simple, and effective analytics platform, Plausible is an excellent choice that combines technical efficiency with ethical principles.
For website operators looking for a privacy-friendly, simple, and effective analytics platform, Plausible is an excellent choice that combines technical efficiency with ethical principles.

View file

@ -76,4 +76,4 @@ RevenueCat has proven indispensable to us for several reasons:
- **Flexibility in Product Design**: The Entitlements API allows us to customize product offerings and experiment without having to submit app updates.
- **Scalability**: The platform grows with our apps and provides robust solutions for both small and large applications.
For developers looking to implement in-app purchases or subscriptions in their mobile apps, RevenueCat offers significant added value and can dramatically reduce development effort while providing insights and flexibility that are crucial for optimizing app monetization.
For developers looking to implement in-app purchases or subscriptions in their mobile apps, RevenueCat offers significant added value and can dramatically reduce development effort while providing insights and flexibility that are crucial for optimizing app monetization.

View file

@ -66,4 +66,4 @@ TestFlight has proven to be an indispensable tool in our iOS development process
- **Reliable Delivery**: Apple's system ensures updates are reliably distributed to testers, with minimal administrative overhead for the development team.
- **Compliance and Security**: The platform meets Apple's stringent security and privacy requirements, which is important for distributing software to external testers.
For any team developing iOS apps, TestFlight has become a standard tool that streamlines the transition from development to production and ensures apps are thoroughly tested before they're made available to the public on the App Store.
For any team developing iOS apps, TestFlight has become a standard tool that streamlines the transition from development to production and ensures apps are thoroughly tested before they're made available to the public on the App Store.

View file

@ -64,11 +64,11 @@ Windsurf offers a generous free version with advanced features such as unlimited
Windsurf stands out for its ability to act as both a copilot and an autonomous agent, distinguishing it from other IDEs like Cursor.
| Feature | Windsurf Editor | Cursor | VS Code (with plugins) |
|---------------------------|---|---|---|
| AI Agent Support | ✅ Yes | ❌ No | ❌ No |
| Multi-File Editing | ✅ Yes | ✅ Yes | ❌ No |
| Full Context Awareness | ✅ Yes | ❌ No | ❌ No |
| Feature | Windsurf Editor | Cursor | VS Code (with plugins) |
| ---------------------- | --------------- | ------ | ---------------------- |
| AI Agent Support | ✅ Yes | ❌ No | ❌ No |
| Multi-File Editing | ✅ Yes | ✅ Yes | ❌ No |
| Full Context Awareness | ✅ Yes | ❌ No | ❌ No |
## Use Cases
@ -98,4 +98,4 @@ Windsurf AI offers various pricing options:
## Conclusion
Windsurf AI is more than just a tool it's an intelligent partner for developers. With its innovative features like Cascade and Memories, as well as its ability for deep context analysis, it revolutionizes the way software is developed. For beginners and experienced developers alike, Windsurf offers an intuitive user interface and a variety of features that optimize workflow.
Windsurf AI is more than just a tool it's an intelligent partner for developers. With its innovative features like Cascade and Memories, as well as its ability for deep context analysis, it revolutionizes the way software is developed. For beginners and experienced developers alike, Windsurf offers an intuitive user interface and a variety of features that optimize workflow.

View file

@ -82,4 +82,4 @@ Claude Code può comportare costi significativi a seconda dell'intensità di uti
Sebbene Claude Code sia ancora in fase di anteprima, mostra un grande potenziale per trasformare lo sviluppo software. Non solo fa risparmiare tempo agli sviluppatori, ma consente anche una più profonda integrazione dell'AI nel processo di sviluppo. Con miglioramenti futuri, potrebbe essere possibile generare autonomamente progetti completi.
Claude Code segna un passo significativo verso una nuova era dello sviluppo software un'era in cui gli sviluppatori possono portare il loro lavoro a un nuovo livello attraverso l'automazione intelligente. Nonostante i costi più elevati rispetto ad altri strumenti di codifica AI, offre un chiaro valore aggiunto in determinati scenari che giustifica l'investimento.
Claude Code segna un passo significativo verso una nuova era dello sviluppo software un'era in cui gli sviluppatori possono portare il loro lavoro a un nuovo livello attraverso l'automazione intelligente. Nonostante i costi più elevati rispetto ad altri strumenti di codifica AI, offre un chiaro valore aggiunto in determinati scenari che giustifica l'investimento.

View file

@ -73,4 +73,4 @@ Claude è diventato uno strumento inestimabile nel nostro flusso di lavoro per d
- L'integrazione perfetta nei nostri strumenti e flussi di lavoro esistenti attraverso l'API
- La capacità di automatizzare attività dispendiose in termini di tempo, creando così più spazio per lavoro creativo e strategico
Claude ci permette di lavorare in modo più efficiente migliorando al contempo la qualità dei nostri output, rendendolo una parte essenziale del nostro set di strumenti.
Claude ci permette di lavorare in modo più efficiente migliorando al contempo la qualità dei nostri output, rendendolo una parte essenziale del nostro set di strumenti.

Some files were not shown because too many files have changed in this diff Show more