mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:21:08 +02:00
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:
parent
0241f5554c
commit
d36b321d9d
3952 changed files with 661498 additions and 739751 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
---
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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à.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue