feat(quote): integrate Quote app into monorepo

- Add complete Quote app with mobile (Expo), web (SvelteKit), landing (Astro), and backend (NestJS)
- Create NestJS backend with Drizzle ORM for PostgreSQL
- Add API endpoints for favorites and user lists
- Add database schema for favorites and user_lists tables
- Update root package.json with quote dev scripts
- Add Quote environment variables to generate-env.mjs
- Add missing toast.ts store for web app
- Configure hybrid content strategy (static + API)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-27 14:44:33 +01:00
parent 3a8d6bcf94
commit ea3285dcbb
285 changed files with 645599 additions and 8 deletions

View file

@ -38,7 +38,7 @@ JWT_ACCESS_TOKEN_EXPIRY=15m
JWT_REFRESH_TOKEN_EXPIRY=7d
JWT_ISSUER=manacore
JWT_AUDIENCE=manacore
CORS_ORIGINS=http://localhost:3000,http://localhost:3002,http://localhost:5173,http://localhost:8081
CORS_ORIGINS=http://localhost:3000,http://localhost:3002,http://localhost:5173,http://localhost:5177,http://localhost:8081
CREDITS_SIGNUP_BONUS=150
CREDITS_DAILY_FREE=5
RATE_LIMIT_TTL=60
@ -113,9 +113,17 @@ MANADECK_SUPABASE_ANON_KEY=your-supabase-anon-key
# PICTURE PROJECT
# ============================================
PICTURE_BACKEND_URL=http://localhost:3003
PICTURE_SUPABASE_URL=https://your-picture-project.supabase.co
PICTURE_SUPABASE_ANON_KEY=your-supabase-anon-key
# OAuth (optional - leave empty to disable)
PICTURE_GOOGLE_CLIENT_ID=
PICTURE_APPLE_CLIENT_ID=
# ============================================
# QUOTE PROJECT
# ============================================
QUOTE_BACKEND_PORT=3007
QUOTE_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/quote

160
apps/quote/CLAUDE.md Normal file
View file

@ -0,0 +1,160 @@
# Quote Project Guide
## Project Structure
```
apps/quote/
├── apps/
│ ├── backend/ # NestJS API server (@quote/backend)
│ ├── landing/ # Astro marketing landing page (@quote/landing)
│ ├── web/ # SvelteKit web application (@quote/web)
│ └── mobile/ # Expo/React Native mobile app (@quote/mobile)
├── packages/
│ ├── shared/ # Shared types, utils, configs (@quote/shared)
│ ├── content/ # Quote data and content (@quote/content)
│ └── web-ui/ # Shared Svelte components (@quote/web-ui)
└── package.json
```
## Commands
### Root Level (from monorepo root)
```bash
pnpm quote:dev # Run all quote apps
pnpm dev:quote:mobile # Start mobile app
pnpm dev:quote:web # Start web app
pnpm dev:quote:landing # Start landing page
pnpm dev:quote:backend # Start backend server
pnpm dev:quote:app # Start web + backend together
```
### Mobile App (apps/quote/apps/mobile)
```bash
pnpm dev # Start Expo dev server
pnpm ios # Run on iOS simulator
pnpm android # Run on Android emulator
```
### Backend (apps/quote/apps/backend)
```bash
pnpm dev # Start with hot reload
pnpm build # Build for production
pnpm start:prod # Start production server
pnpm db:push # Push schema to database
pnpm db:studio # Open Drizzle Studio
```
### Web App (apps/quote/apps/web)
```bash
pnpm dev # Start dev server
pnpm build # Build for production
pnpm preview # Preview production build
```
### Landing Page (apps/quote/apps/landing)
```bash
pnpm dev # Start dev server
pnpm build # Build for production
```
## Technology Stack
- **Mobile**: React Native 0.81 + Expo SDK 54, NativeWind, Expo Router, Zustand
- **Web**: SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS
- **Landing**: Astro 5.x, Tailwind CSS
- **Backend**: NestJS 10, Drizzle ORM, PostgreSQL
- **Types**: TypeScript 5.x
## Architecture
### Content Delivery (Hybrid)
- **Static Content**: Quotes and authors are bundled in `@quote/content` package for offline access
- **Backend API**: User-specific data (favorites, lists) are stored in PostgreSQL via backend API
### Backend API Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/health` | GET | Health check |
| `/api/favorites` | GET | Get user's favorites |
| `/api/favorites` | POST | Add quote to favorites |
| `/api/favorites/:quoteId` | DELETE | Remove from favorites |
| `/api/lists` | GET | Get user's lists |
| `/api/lists` | POST | Create new list |
| `/api/lists/:id` | GET | Get list details |
| `/api/lists/:id` | PUT | Update list |
| `/api/lists/:id` | DELETE | Delete list |
| `/api/lists/:id/quotes` | POST | Add quote to list |
| `/api/lists/:id/quotes/:quoteId` | DELETE | Remove quote from list |
### Database Schema
**favorites** - User favorite quotes
- `id` (UUID) - Primary key
- `user_id` (UUID) - User reference
- `quote_id` (VARCHAR) - Reference to static quote ID
- `created_at` (TIMESTAMP)
**user_lists** - Custom user lists
- `id` (UUID) - Primary key
- `user_id` (UUID) - User reference
- `name` (TEXT) - List name
- `description` (TEXT) - Optional description
- `quote_ids` (JSONB) - Array of quote IDs
- `created_at` (TIMESTAMP)
- `updated_at` (TIMESTAMP)
### Environment Variables
#### Backend (.env)
```
NODE_ENV=development
PORT=3007
DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/quote
MANA_CORE_AUTH_URL=http://localhost:3001
CORS_ORIGINS=http://localhost:5173,http://localhost:5177,http://localhost:8081
```
#### Mobile (.env)
```
EXPO_PUBLIC_BACKEND_URL=http://localhost:3007
EXPO_PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
```
#### Web (.env)
```
PUBLIC_BACKEND_URL=http://localhost:3007
PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
```
## Shared Packages
### @quote/shared
- Types: `ContentItem`, `ContentAuthor`, `Quote`, `QuoteMetadata`
- Utils: Search, filter, random selection functions
- Configs: App configuration
### @quote/content
- Static quote data (German and English)
- Author information with biographies
- Export functions for data access
### @quote/web-ui
- Shared Svelte 5 components
- Styling utilities
- Stores
## Code Style Guidelines
- **TypeScript**: Strict typing with interfaces
- **Mobile**: Functional components with hooks, Zustand for state
- **Web**: Svelte 5 runes mode (`$state`, `$derived`, `$effect`)
- **Styling**: Tailwind CSS / NativeWind
- **Formatting**: Prettier with project config
## Important Notes
1. **Offline First**: Static content works without backend
2. **Authentication**: Uses Mana Core Auth (JWT in Authorization header)
3. **Database**: PostgreSQL with Drizzle ORM
4. **Port**: Backend runs on port 3007 by default

View file

@ -0,0 +1,12 @@
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'postgresql',
schema: './src/db/schema/index.ts',
out: './src/db/migrations',
dbCredentials: {
url: process.env.DATABASE_URL || 'postgresql://manacore:devpassword@localhost:5432/quote',
},
verbose: true,
strict: true,
});

View file

@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"assets": [],
"watchAssets": false
}
}

View file

@ -0,0 +1,52 @@
{
"name": "@quote/backend",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "nest build",
"start": "nest start",
"dev": "nest start --watch",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"type-check": "tsc --noEmit",
"migration:generate": "drizzle-kit generate",
"migration:run": "tsx src/db/migrate.ts",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:seed": "tsx src/db/seed.ts"
},
"dependencies": {
"@nestjs/common": "^10.4.15",
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.4.15",
"@nestjs/platform-express": "^10.4.15",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dotenv": "^16.4.7",
"drizzle-kit": "^0.30.2",
"drizzle-orm": "^0.38.3",
"postgres": "^3.4.5",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/cli": "^10.4.9",
"@nestjs/schematics": "^10.2.3",
"@types/express": "^5.0.0",
"@types/node": "^22.10.2",
"@typescript-eslint/eslint-plugin": "^8.18.1",
"@typescript-eslint/parser": "^8.18.1",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.19.2",
"typescript": "^5.7.2"
}
}

View file

@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './db/database.module';
import { FavoriteModule } from './favorite/favorite.module';
import { ListModule } from './list/list.module';
import { HealthModule } from './health/health.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
DatabaseModule,
FavoriteModule,
ListModule,
HealthModule,
],
})
export class AppModule {}

View file

@ -0,0 +1,38 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import * as schema from './schema';
// Use require for postgres to avoid ESM/CommonJS interop issues
// eslint-disable-next-line @typescript-eslint/no-var-requires
const postgres = require('postgres');
let connection: ReturnType<typeof postgres> | null = null;
let db: ReturnType<typeof drizzle> | null = null;
export function getConnection(databaseUrl: string) {
if (!connection) {
connection = postgres(databaseUrl, {
max: 10,
idle_timeout: 20,
connect_timeout: 10,
});
}
return connection;
}
export function getDb(databaseUrl: string) {
if (!db) {
const conn = getConnection(databaseUrl);
db = drizzle(conn, { schema });
}
return db;
}
export async function closeConnection() {
if (connection) {
await connection.end();
connection = null;
db = null;
}
}
export type Database = ReturnType<typeof getDb>;

View file

@ -0,0 +1,28 @@
import { Module, Global, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { getDb, closeConnection, type Database } from './connection';
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
@Global()
@Module({
providers: [
{
provide: DATABASE_CONNECTION,
useFactory: (configService: ConfigService): Database => {
const databaseUrl = configService.get<string>('DATABASE_URL');
if (!databaseUrl) {
throw new Error('DATABASE_URL environment variable is not set');
}
return getDb(databaseUrl);
},
inject: [ConfigService],
},
],
exports: [DATABASE_CONNECTION],
})
export class DatabaseModule implements OnModuleDestroy {
async onModuleDestroy() {
await closeConnection();
}
}

View file

@ -0,0 +1,29 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import * as dotenv from 'dotenv';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const postgres = require('postgres');
dotenv.config();
async function runMigrations() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error('DATABASE_URL environment variable is not set');
}
console.log('Running migrations...');
const sql = postgres(databaseUrl, { max: 1 });
const db = drizzle(sql);
await migrate(db, { migrationsFolder: './src/db/migrations' });
await sql.end();
console.log('Migrations completed successfully!');
}
runMigrations().catch(console.error);

View file

@ -0,0 +1,13 @@
import { pgTable, uuid, timestamp, unique, varchar } from 'drizzle-orm/pg-core';
export const favorites = pgTable('favorites', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull(),
quoteId: varchar('quote_id', { length: 100 }).notNull(), // References static quote ID from shared package
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => ({
uniqueUserQuote: unique().on(table.userId, table.quoteId),
}));
export type Favorite = typeof favorites.$inferSelect;
export type NewFavorite = typeof favorites.$inferInsert;

View file

@ -0,0 +1,2 @@
export * from './favorites.schema';
export * from './user-lists.schema';

View file

@ -0,0 +1,14 @@
import { pgTable, uuid, text, timestamp, jsonb } from 'drizzle-orm/pg-core';
export const userLists = pgTable('user_lists', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull(),
name: text('name').notNull(),
description: text('description'),
quoteIds: jsonb('quote_ids').$type<string[]>().default([]), // References static quote IDs from shared package
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
});
export type UserList = typeof userLists.$inferSelect;
export type NewUserList = typeof userLists.$inferInsert;

View file

@ -0,0 +1,79 @@
import {
Controller,
Get,
Post,
Delete,
Body,
Param,
Headers,
UnauthorizedException,
ConflictException,
} from '@nestjs/common';
import { FavoriteService } from './favorite.service';
import { IsString, IsNotEmpty } from 'class-validator';
class CreateFavoriteDto {
@IsString()
@IsNotEmpty()
quoteId!: string;
}
// Simple JWT extraction - in production, use proper auth middleware
function extractUserId(authHeader?: string): string {
if (!authHeader?.startsWith('Bearer ')) {
throw new UnauthorizedException('Missing or invalid authorization header');
}
try {
const token = authHeader.substring(7);
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
if (!payload.sub) {
throw new UnauthorizedException('Invalid token payload');
}
return payload.sub;
} catch {
throw new UnauthorizedException('Invalid token');
}
}
@Controller('favorites')
export class FavoriteController {
constructor(private readonly favoriteService: FavoriteService) {}
@Get()
async findAll(@Headers('authorization') authHeader: string) {
const userId = extractUserId(authHeader);
const favorites = await this.favoriteService.findByUserId(userId);
return { favorites };
}
@Post()
async create(
@Headers('authorization') authHeader: string,
@Body() dto: CreateFavoriteDto,
) {
const userId = extractUserId(authHeader);
// Check if already favorited
const exists = await this.favoriteService.exists(userId, dto.quoteId);
if (exists) {
throw new ConflictException('Quote already in favorites');
}
const favorite = await this.favoriteService.create({
userId,
quoteId: dto.quoteId,
});
return { favorite };
}
@Delete(':quoteId')
async delete(
@Headers('authorization') authHeader: string,
@Param('quoteId') quoteId: string,
) {
const userId = extractUserId(authHeader);
await this.favoriteService.delete(userId, quoteId);
return { success: true };
}
}

View file

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { FavoriteController } from './favorite.controller';
import { FavoriteService } from './favorite.service';
@Module({
controllers: [FavoriteController],
providers: [FavoriteService],
exports: [FavoriteService],
})
export class FavoriteModule {}

View file

@ -0,0 +1,33 @@
import { Injectable, Inject } from '@nestjs/common';
import { eq, and } from 'drizzle-orm';
import { DATABASE_CONNECTION } from '../db/database.module';
import { type Database } from '../db/connection';
import { favorites, type Favorite, type NewFavorite } from '../db/schema';
@Injectable()
export class FavoriteService {
constructor(@Inject(DATABASE_CONNECTION) private db: Database) {}
async findByUserId(userId: string): Promise<Favorite[]> {
return this.db.select().from(favorites).where(eq(favorites.userId, userId));
}
async create(data: NewFavorite): Promise<Favorite> {
const [favorite] = await this.db.insert(favorites).values(data).returning();
return favorite;
}
async delete(userId: string, quoteId: string): Promise<void> {
await this.db
.delete(favorites)
.where(and(eq(favorites.userId, userId), eq(favorites.quoteId, quoteId)));
}
async exists(userId: string, quoteId: string): Promise<boolean> {
const result = await this.db
.select()
.from(favorites)
.where(and(eq(favorites.userId, userId), eq(favorites.quoteId, quoteId)));
return result.length > 0;
}
}

View file

@ -0,0 +1,13 @@
import { Controller, Get } from '@nestjs/common';
@Controller('health')
export class HealthController {
@Get()
check() {
return {
status: 'ok',
service: 'quote-backend',
timestamp: new Date().toISOString(),
};
}
}

View file

@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
@Module({
controllers: [HealthController],
})
export class HealthModule {}

View file

@ -0,0 +1,141 @@
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Headers,
UnauthorizedException,
} from '@nestjs/common';
import { ListService } from './list.service';
import { IsString, IsNotEmpty, IsOptional, IsArray } from 'class-validator';
class CreateListDto {
@IsString()
@IsNotEmpty()
name!: string;
@IsString()
@IsOptional()
description?: string;
}
class UpdateListDto {
@IsString()
@IsOptional()
name?: string;
@IsString()
@IsOptional()
description?: string;
@IsArray()
@IsString({ each: true })
@IsOptional()
quoteIds?: string[];
}
class AddQuoteDto {
@IsString()
@IsNotEmpty()
quoteId!: string;
}
// Simple JWT extraction - in production, use proper auth middleware
function extractUserId(authHeader?: string): string {
if (!authHeader?.startsWith('Bearer ')) {
throw new UnauthorizedException('Missing or invalid authorization header');
}
try {
const token = authHeader.substring(7);
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
if (!payload.sub) {
throw new UnauthorizedException('Invalid token payload');
}
return payload.sub;
} catch {
throw new UnauthorizedException('Invalid token');
}
}
@Controller('lists')
export class ListController {
constructor(private readonly listService: ListService) {}
@Get()
async findAll(@Headers('authorization') authHeader: string) {
const userId = extractUserId(authHeader);
const lists = await this.listService.findByUserId(userId);
return { lists };
}
@Get(':id')
async findOne(
@Headers('authorization') authHeader: string,
@Param('id') id: string,
) {
const userId = extractUserId(authHeader);
const list = await this.listService.findById(userId, id);
return { list };
}
@Post()
async create(
@Headers('authorization') authHeader: string,
@Body() dto: CreateListDto,
) {
const userId = extractUserId(authHeader);
const list = await this.listService.create({
userId,
name: dto.name,
description: dto.description,
});
return { list };
}
@Put(':id')
async update(
@Headers('authorization') authHeader: string,
@Param('id') id: string,
@Body() dto: UpdateListDto,
) {
const userId = extractUserId(authHeader);
const list = await this.listService.update(userId, id, dto);
return { list };
}
@Delete(':id')
async delete(
@Headers('authorization') authHeader: string,
@Param('id') id: string,
) {
const userId = extractUserId(authHeader);
await this.listService.delete(userId, id);
return { success: true };
}
@Post(':id/quotes')
async addQuote(
@Headers('authorization') authHeader: string,
@Param('id') id: string,
@Body() dto: AddQuoteDto,
) {
const userId = extractUserId(authHeader);
const list = await this.listService.addQuoteToList(userId, id, dto.quoteId);
return { list };
}
@Delete(':id/quotes/:quoteId')
async removeQuote(
@Headers('authorization') authHeader: string,
@Param('id') id: string,
@Param('quoteId') quoteId: string,
) {
const userId = extractUserId(authHeader);
const list = await this.listService.removeQuoteFromList(userId, id, quoteId);
return { list };
}
}

View file

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ListController } from './list.controller';
import { ListService } from './list.service';
@Module({
controllers: [ListController],
providers: [ListService],
exports: [ListService],
})
export class ListModule {}

View file

@ -0,0 +1,75 @@
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
import { eq, and } from 'drizzle-orm';
import { DATABASE_CONNECTION } from '../db/database.module';
import { type Database } from '../db/connection';
import { userLists, type UserList, type NewUserList } from '../db/schema';
@Injectable()
export class ListService {
constructor(@Inject(DATABASE_CONNECTION) private db: Database) {}
async findByUserId(userId: string): Promise<UserList[]> {
return this.db.select().from(userLists).where(eq(userLists.userId, userId));
}
async findById(userId: string, listId: string): Promise<UserList> {
const [list] = await this.db
.select()
.from(userLists)
.where(and(eq(userLists.id, listId), eq(userLists.userId, userId)));
if (!list) {
throw new NotFoundException('List not found');
}
return list;
}
async create(data: NewUserList): Promise<UserList> {
const [list] = await this.db.insert(userLists).values(data).returning();
return list;
}
async update(
userId: string,
listId: string,
data: Partial<Pick<UserList, 'name' | 'description' | 'quoteIds'>>,
): Promise<UserList> {
const [list] = await this.db
.update(userLists)
.set({ ...data, updatedAt: new Date() })
.where(and(eq(userLists.id, listId), eq(userLists.userId, userId)))
.returning();
if (!list) {
throw new NotFoundException('List not found');
}
return list;
}
async delete(userId: string, listId: string): Promise<void> {
const result = await this.db
.delete(userLists)
.where(and(eq(userLists.id, listId), eq(userLists.userId, userId)));
if (!result) {
throw new NotFoundException('List not found');
}
}
async addQuoteToList(userId: string, listId: string, quoteId: string): Promise<UserList> {
const list = await this.findById(userId, listId);
const quoteIds = list.quoteIds || [];
if (!quoteIds.includes(quoteId)) {
quoteIds.push(quoteId);
}
return this.update(userId, listId, { quoteIds });
}
async removeQuoteFromList(userId: string, listId: string, quoteId: string): Promise<UserList> {
const list = await this.findById(userId, listId);
const quoteIds = (list.quoteIds || []).filter((id) => id !== quoteId);
return this.update(userId, listId, { quoteIds });
}
}

View file

@ -0,0 +1,38 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable CORS for mobile and web apps
app.enableCors({
origin: [
'http://localhost:3000',
'http://localhost:5173',
'http://localhost:5177',
'http://localhost:8081',
'exp://localhost:8081',
'http://localhost:3001', // Mana Core Auth
],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
credentials: true,
});
// Enable validation
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);
// Set global prefix for API routes
app.setGlobalPrefix('api');
const port = process.env.PORT || 3007;
await app.listen(port);
console.log(`Quote backend running on http://localhost:${port}`);
}
bootstrap();

View file

@ -0,0 +1,25 @@
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"rootDir": "./src",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,9 @@
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
// https://astro.build/config
export default defineConfig({
site: 'https://quotes.app',
integrations: [tailwind()],
output: 'static',
});

View file

@ -0,0 +1,19 @@
{
"name": "@quote/landing",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"astro": "^5.3.4"
},
"devDependencies": {
"@astrojs/tailwind": "^6.0.2",
"tailwindcss": "^3.4.0",
"typescript": "^5.0.0"
}
}

View file

@ -0,0 +1,72 @@
---
interface Props {
title: string;
description?: string;
}
const { title, description = 'Discover inspiring quotes from great minds' } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="description" content={description} />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
</head>
<body>
<nav class="bg-gradient-to-r from-purple-600 to-indigo-600 text-white">
<div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<a href="/" class="text-2xl font-bold">📖 Quotes</a>
<div class="flex gap-6">
<a href="/#features" class="hover:opacity-80">Features</a>
<a href="/#about" class="hover:opacity-80">About</a>
<a href="/app" class="bg-white text-purple-600 px-4 py-2 rounded-lg font-semibold hover:opacity-90">
Open App
</a>
</div>
</div>
</div>
</nav>
<slot />
<footer class="bg-gray-900 text-white py-12">
<div class="container mx-auto px-6 text-center">
<p>&copy; 2025 Quotes App. All rights reserved.</p>
<div class="mt-4 flex justify-center gap-6">
<a href="/privacy" class="hover:opacity-80">Privacy</a>
<a href="/terms" class="hover:opacity-80">Terms</a>
<a href="/contact" class="hover:opacity-80">Contact</a>
</div>
</div>
</footer>
</body>
</html>
<style is:global>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-family: system-ui, sans-serif;
background: #ffffff;
}
body {
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
</style>

View file

@ -0,0 +1,120 @@
---
import Layout from '../layouts/Layout.astro';
---
<Layout title="Quotes - Inspiring Words from Great Minds">
<main>
<!-- Hero Section -->
<section class="bg-gradient-to-br from-purple-50 to-indigo-100 py-20">
<div class="container mx-auto px-6 text-center">
<h1 class="text-5xl md:text-6xl font-bold text-gray-900 mb-6">
Discover Inspiring Quotes
</h1>
<p class="text-xl md:text-2xl text-gray-700 mb-8 max-w-2xl mx-auto">
Explore wisdom from history's greatest thinkers. Available on mobile, web, and everywhere you need inspiration.
</p>
<div class="flex gap-4 justify-center flex-wrap">
<a href="/app" class="bg-purple-600 text-white px-8 py-4 rounded-lg text-lg font-semibold hover:bg-purple-700 transition">
Open Web App
</a>
<a href="#features" class="bg-white text-purple-600 px-8 py-4 rounded-lg text-lg font-semibold hover:bg-gray-50 transition border-2 border-purple-600">
Learn More
</a>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="py-20">
<div class="container mx-auto px-6">
<h2 class="text-4xl font-bold text-center mb-12 text-gray-900">Features</h2>
<div class="grid md:grid-cols-3 gap-8">
<div class="text-center p-6">
<div class="text-5xl mb-4">📱</div>
<h3 class="text-xl font-bold mb-2">Mobile App</h3>
<p class="text-gray-600">
Native iOS and Android app with offline support and beautiful design.
</p>
</div>
<div class="text-center p-6">
<div class="text-5xl mb-4">🌐</div>
<h3 class="text-xl font-bold mb-2">Web Access</h3>
<p class="text-gray-600">
Access your favorite quotes from any browser, anywhere.
</p>
</div>
<div class="text-center p-6">
<div class="text-5xl mb-4">📚</div>
<h3 class="text-xl font-bold mb-2">1000+ Quotes</h3>
<p class="text-gray-600">
Curated collection from philosophers, scientists, and leaders.
</p>
</div>
<div class="text-center p-6">
<div class="text-5xl mb-4">⭐</div>
<h3 class="text-xl font-bold mb-2">Favorites</h3>
<p class="text-gray-600">
Save your favorite quotes and access them instantly.
</p>
</div>
<div class="text-center p-6">
<div class="text-5xl mb-4">🔍</div>
<h3 class="text-xl font-bold mb-2">Search & Filter</h3>
<p class="text-gray-600">
Find quotes by author, category, or keyword.
</p>
</div>
<div class="text-center p-6">
<div class="text-5xl mb-4">🎨</div>
<h3 class="text-xl font-bold mb-2">Beautiful Design</h3>
<p class="text-gray-600">
Elegant interface that puts content first.
</p>
</div>
</div>
</div>
</section>
<!-- About Section -->
<section id="about" class="bg-gray-50 py-20">
<div class="container mx-auto px-6">
<div class="max-w-3xl mx-auto text-center">
<h2 class="text-4xl font-bold mb-6 text-gray-900">About Quotes</h2>
<p class="text-xl text-gray-700 mb-6">
Quotes is your daily source of inspiration and wisdom. We've carefully curated
over 1000 quotes from history's most influential thinkers, philosophers, scientists,
and leaders.
</p>
<p class="text-lg text-gray-600">
Whether you're looking for motivation, wisdom, or just a moment of reflection,
Quotes brings you timeless words that inspire and enlighten.
</p>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="bg-gradient-to-r from-purple-600 to-indigo-600 text-white py-20">
<div class="container mx-auto px-6 text-center">
<h2 class="text-4xl font-bold mb-6">Ready to Get Inspired?</h2>
<p class="text-xl mb-8 max-w-2xl mx-auto">
Start exploring thousands of inspiring quotes today.
</p>
<div class="flex gap-4 justify-center flex-wrap">
<a href="/app" class="bg-white text-purple-600 px-8 py-4 rounded-lg text-lg font-semibold hover:bg-gray-100 transition">
Launch Web App
</a>
<a href="#" class="bg-purple-800 text-white px-8 py-4 rounded-lg text-lg font-semibold hover:bg-purple-900 transition">
Download Mobile App
</a>
</div>
</div>
</section>
</main>
</Layout>

View file

@ -0,0 +1,7 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}

34
apps/quote/apps/mobile/.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
node_modules/
.expo/
dist/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
# expo router
expo-env.d.ts
# Environment variables
.env
.env.local
.env.*.local
ios
android
# macOS
.DS_Store
# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*
# Vim swap files
*.swp
*.swo
*~

2
apps/quote/apps/mobile/app-env.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
// @ts-ignore
/// <reference types="nativewind/types" />

View file

@ -0,0 +1,69 @@
{
"expo": {
"name": "Zitare",
"slug": "quote",
"version": "1.0.0",
"scheme": "zitare",
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/favicon.png"
},
"plugins": [
"expo-router",
"expo-web-browser",
"expo-font",
"@bacons/apple-targets",
[
"expo-document-picker",
{
"iCloudContainerEnvironment": "Production"
}
]
],
"experiments": {
"typedRoutes": true,
"tsconfigPaths": true
},
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.memoro.zitare",
"icon": "./assets/zitare.icon",
"usesIcloudStorage": true,
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false,
"NSDocumentsFolderUsageDescription": "Diese App benötigt Zugriff auf den Dokumentenordner, um Backups zu speichern und wiederherzustellen.",
"UISupportsDocumentBrowser": true
},
"entitlements": {
"com.apple.security.application-groups": [
"group.com.memoro.zitare.widget"
]
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.memoro.zitare"
},
"extra": {
"router": {},
"eas": {
"projectId": "8b630a7d-8b83-4847-81b0-e7922bfba178"
}
}
}
}

View file

@ -0,0 +1,116 @@
/**
* Native Tabs Implementation für iOS 18+ mit Liquid Glass Effekt
* Basierend auf der offiziellen Expo Router Native Tabs Dokumentation
*/
import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs';
import { DynamicColorIOS, Platform } from 'react-native';
import { useTranslation } from 'react-i18next';
export default function NativeTabLayout() {
const { t } = useTranslation();
// Monochrome Farben für Liquid Glass - passen sich automatisch an hell/dunkel an
const liquidGlassColors = Platform.OS === 'ios' ? {
// Text Farbe
color: DynamicColorIOS({
dark: 'white',
light: 'black',
}),
// Icon Farbe (selected)
tintColor: DynamicColorIOS({
dark: 'white',
light: 'black',
}),
} : {
color: '#ffffff',
tintColor: '#ffffff',
};
return (
<NativeTabs
// Farben für Liquid Glass
labelStyle={liquidGlassColors}
>
{/* Zitate Tab */}
<NativeTabs.Trigger
name="quotes"
// Optional: Scroll-to-top beim erneuten Tippen
disableScrollToTop={false}
>
<Label>{t('navigation.quotes')}</Label>
<Icon
// SF Symbols für iOS (beste native Integration)
sf={{
default: "book",
selected: "book.fill"
}}
// Fallback für Android
drawable="ic_book"
/>
</NativeTabs.Trigger>
{/* Autoren Tab */}
<NativeTabs.Trigger
name="authors"
disableScrollToTop={false}
>
<Label>{t('navigation.authors')}</Label>
<Icon
sf={{
default: "person.2",
selected: "person.2.fill"
}}
drawable="ic_people"
/>
</NativeTabs.Trigger>
{/* Listen Tab */}
<NativeTabs.Trigger
name="liste"
disableScrollToTop={false}
>
<Label>Listen</Label>
<Icon
sf={{
default: "list.bullet",
selected: "list.bullet.rectangle.fill"
}}
drawable="ic_list"
/>
</NativeTabs.Trigger>
{/* Meine Zitate Tab */}
<NativeTabs.Trigger
name="myquotes"
disableScrollToTop={false}
>
<Label>{t('navigation.myQuotes')}</Label>
<Icon
sf={{
default: "square.and.pencil",
selected: "square.and.pencil.fill"
}}
drawable="ic_create"
/>
</NativeTabs.Trigger>
{/* Search Tab - iOS 18+: Separate search tab für native Suche */}
<NativeTabs.Trigger
name="search"
role="search"
disableScrollToTop={false}
>
<Label>{t('navigation.search')}</Label>
<Icon
sf={{
default: "magnifyingglass",
selected: "magnifyingglass.circle.fill"
}}
drawable="ic_search"
/>
</NativeTabs.Trigger>
</NativeTabs>
);
}

View file

@ -0,0 +1,189 @@
/**
* Enhanced Tab Layout mit automatischer Native Tabs Detection
* Nutzt Native Tabs auf iOS 18+ für Liquid Glass Effekt
* Fallback zu Blur-basierten Tabs auf älteren Versionen
*/
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { View, Platform } from 'react-native';
import { BlurView } from 'expo-blur';
import { useTranslation } from 'react-i18next';
// Versuche Native Tabs zu laden
let NativeTabLayout: any = null;
const ENABLE_NATIVE_TABS = true; // Setze auf false zum Deaktivieren
if (ENABLE_NATIVE_TABS && Platform.OS === 'ios') {
try {
// Prüfe ob Native Tabs verfügbar sind
const nativeTabsModule = require('expo-router/unstable-native-tabs');
if (nativeTabsModule && nativeTabsModule.NativeTabs) {
// Lade unsere Native Tabs Implementation
NativeTabLayout = require('./NativeTabsLayout').default;
}
} catch (error) {
}
}
export default function TabLayout() {
const { t } = useTranslation();
// Wenn Native Tabs verfügbar sind, nutze sie
if (NativeTabLayout) {
return <NativeTabLayout />;
}
// Fallback: Enhanced Blur Tabs für ältere iOS Versionen
const isIOS = Platform.OS === 'ios';
return (
<Tabs
initialRouteName="quotes"
screenOptions={{
tabBarActiveTintColor: '#ffffff',
tabBarInactiveTintColor: '#6b7280',
tabBarStyle: isIOS ? {
// iOS: Transparenter Hintergrund für Blur-Effekt
position: 'absolute',
backgroundColor: 'transparent',
borderTopWidth: 0,
elevation: 0,
height: 95,
paddingTop: 10,
paddingBottom: 35,
} : {
// Android: Standard Style
backgroundColor: '#0f0f0f',
borderTopColor: '#1a1a1a',
height: 90,
paddingTop: 10,
paddingBottom: 30,
},
tabBarBackground: () => isIOS ? (
<BlurView
intensity={100}
tint="dark"
style={{
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
}}
/>
) : null,
tabBarLabelStyle: {
fontSize: 12,
fontWeight: '600',
},
}}>
<Tabs.Screen
name="quotes"
options={{
title: t('navigation.quotes'),
tabBarLabel: t('navigation.quotes'),
tabBarIcon: ({ color, focused }) => (
<View>
{focused && (
<View
className="absolute -inset-2 rounded-full bg-white/10"
style={{ width: 48, height: 48 }}
/>
)}
<Ionicons
name={focused ? "book" : "book-outline"}
size={28}
color={color}
/>
</View>
),
}}
/>
<Tabs.Screen
name="authors"
options={{
title: t('navigation.authors'),
tabBarIcon: ({ color, focused }) => (
<View>
{focused && (
<View
className="absolute -inset-2 rounded-full bg-white/10"
style={{ width: 48, height: 48 }}
/>
)}
<Ionicons
name={focused ? "people" : "people-outline"}
size={28}
color={color}
/>
</View>
),
}}
/>
<Tabs.Screen
name="liste"
options={{
title: t('lists.lists'),
tabBarIcon: ({ color, focused }) => (
<View>
{focused && (
<View
className="absolute -inset-2 rounded-full bg-white/10"
style={{ width: 48, height: 48 }}
/>
)}
<Ionicons
name={focused ? "list" : "list-outline"}
size={28}
color={color}
/>
</View>
),
}}
/>
<Tabs.Screen
name="myquotes"
options={{
title: t('navigation.myQuotes'),
tabBarIcon: ({ color, focused }) => (
<View>
{focused && (
<View
className="absolute -inset-2 rounded-full bg-white/10"
style={{ width: 48, height: 48 }}
/>
)}
<Ionicons
name={focused ? "create" : "create-outline"}
size={28}
color={color}
/>
</View>
),
}}
/>
<Tabs.Screen
name="search"
options={{
title: t('navigation.search'),
tabBarIcon: ({ color, focused }) => (
<View>
{focused && (
<View
className="absolute -inset-2 rounded-full bg-white/10"
style={{ width: 48, height: 48 }}
/>
)}
<Ionicons
name={focused ? "search" : "search-outline"}
size={28}
color={color}
/>
</View>
),
}}
/>
</Tabs>
);
}

View file

@ -0,0 +1,14 @@
import { Stack } from 'expo-router';
export default function AuthorsLayout() {
return (
<Stack>
<Stack.Screen
name="index"
options={{
headerShown: false,
}}
/>
</Stack>
);
}

View file

@ -0,0 +1,544 @@
import React, { useEffect, useState, useMemo, useRef } from 'react';
import { View, Text, FlatList, Pressable, TouchableOpacity, Dimensions, ActivityIndicator } from 'react-native';
import BottomSheet from '@gorhom/bottom-sheet';
import { LinearGradient } from 'expo-linear-gradient';
import { Icon } from '~/components/Icon';
import { useQuotesStore } from '~/store/quotesStore';
import { useRouter, useLocalSearchParams, Stack } from 'expo-router';
import { LoadingScreen } from '~/components/common/LoadingScreen';
import { useIsDarkMode } from '~/store/settingsStore';
import * as Haptics from 'expo-haptics';
import { GlassTabSelector } from '~/components/common/GlassTabSelector';
import Animated, {
FadeInDown,
useSharedValue,
useAnimatedScrollHandler,
useAnimatedStyle,
withSpring
} from 'react-native-reanimated';
import { useTranslation } from 'react-i18next';
import type { Author } from '@quote/shared';
import { LIST_ITEM_CLASSES, LIST_CONTAINER_PADDING } from '~/constants/layout';
import AuthorCard from '~/components/AuthorCard';
import { useTheme } from '~/hooks/useTheme';
import { useShare } from '~/hooks/useShare';
import { AuthorFilterBottomSheet, AuthorFilters } from '~/components/authors/AuthorFilterBottomSheet';
import { ActiveFilterChips } from '~/components/authors/ActiveFilterChips';
import { filterAuthors } from '~/utils/authorFilters';
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
const { height: screenHeight } = Dimensions.get('window');
// Reimplement AuthorCardItem to use new AuthorCard component
function AuthorCardItem({
item,
index,
isDarkMode,
isAuthorFavorite,
toggleAuthorFavorite,
router,
getAuthorGradient,
t
}: {
item: Author;
index: number;
isDarkMode: boolean;
isAuthorFavorite: (id: string) => boolean;
toggleAuthorFavorite: (author: Author) => void;
router: any;
getAuthorGradient: (index: number) => string[];
t: any;
}) {
const { shareAuthor, copyAuthorToClipboard } = useShare();
const quoteCount = item.quoteIds?.length || 0;
const isFavorite = isAuthorFavorite(item.id);
const likeScale = useSharedValue(1);
const handleFavorite = (e: any) => {
e.stopPropagation();
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
likeScale.value = withSpring(1.3, {}, () => {
likeScale.value = withSpring(1);
});
toggleAuthorFavorite(item);
};
const handleShare = (e: any) => {
e.stopPropagation();
shareAuthor(item);
};
const handleCopyToClipboard = (e: any) => {
e.stopPropagation();
copyAuthorToClipboard(item);
};
const likeAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: likeScale.value }]
}));
return (
<Animated.View
entering={FadeInDown.delay(index * 50).duration(400)}
className={LIST_ITEM_CLASSES.wrapper}
>
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.push(`/author/${item.id}`);
}}
>
{/* Gradient border like QuoteCard */}
<LinearGradient
colors={getAuthorGradient(index)}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={{
borderRadius: 24,
padding: 1
}}
>
<View className="bg-black/40 rounded-3xl backdrop-blur-xl">
<View className="p-5">
{/* Name and Profession */}
<View className="mb-3">
<Text
style={{
fontFamily: 'Georgia',
fontSize: 22,
lineHeight: 28,
color: 'white',
fontWeight: '400'
}}
>
{item.name}
</Text>
{item.profession && item.profession.length > 0 && (
<Text className="text-white/60 text-sm mt-1">
{item.profession.join(' · ')}
</Text>
)}
</View>
{/* Biography - larger and more prominent */}
{(item.biography?.short || item.biography?.long) && (
<Text
style={{
fontSize: 15,
lineHeight: 22,
color: 'rgba(255,255,255,0.7)',
marginBottom: 12
}}
numberOfLines={3}
>
{item.biography.short || item.biography.long}
</Text>
)}
{/* Bottom section with quote count and favorite button */}
<View className="flex-row items-center justify-between">
<View className="flex-row items-center">
<Icon
name="document-text-outline"
size={20}
color="rgba(255,255,255,0.4)"
/>
<Text className="text-white/40 text-sm ml-1">
{t('common.quotes_count', { count: quoteCount })}
</Text>
{item.era && (
<>
<Text className="text-white/20 text-sm mx-2">·</Text>
<Text className="text-white/40 text-sm">
{item.era}
</Text>
</>
)}
</View>
{/* Action Buttons */}
<View className="flex-row items-center gap-3">
{/* Copy Button */}
<Pressable
onPress={handleCopyToClipboard}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Icon
name="copy-outline"
size={22}
color="rgba(255,255,255,0.7)"
/>
</Pressable>
{/* Share Button */}
<Pressable
onPress={handleShare}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Icon
name="share-outline"
size={22}
color="rgba(255,255,255,0.7)"
/>
</Pressable>
{/* Favorite Button */}
<Pressable
onPress={handleFavorite}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Animated.View style={likeAnimatedStyle}>
<Icon
name={isFavorite ? 'heart' : 'heart-outline'}
size={24}
color={isFavorite ? '#ff6b6b' : 'rgba(255,255,255,0.8)'}
/>
</Animated.View>
</Pressable>
</View>
</View>
</View>
</View>
</LinearGradient>
</Pressable>
</Animated.View>
);
}
export default function AuthorsScreen() {
const router = useRouter();
const params = useLocalSearchParams();
const { q: searchQuery } = params;
const { t } = useTranslation();
const { authors, initializeStore, isLoading, isInitialized, toggleAuthorFavorite, isAuthorFavorite, getFavoriteAuthors } = useQuotesStore();
const isDarkMode = useIsDarkMode();
const { colors } = useTheme();
const [sortBy, setSortBy] = useState<'name' | 'quotes'>('name');
const [activeFilter, setActiveFilter] = useState<'all' | 'favorites'>('all');
const [viewMode, setViewMode] = useState<'card' | 'list'>('list');
const [filters, setFilters] = useState<AuthorFilters>({
epochs: [],
professions: [],
nationalities: [],
quoteCount: [],
special: []
});
const scrollY = useSharedValue(0);
const bottomSheetRef = useRef<BottomSheet>(null);
// useShare hook must be called at component level, not in render functions
const { shareAuthor, copyAuthorToClipboard } = useShare();
useEffect(() => {
initializeStore();
}, [initializeStore]);
// Filter authors based on search query, active filter, and advanced filters, then sort
const filteredAndSortedAuthors = useMemo(() => {
if (!authors) return [];
let filtered = authors;
// Apply active filter first (all vs favorites)
if (activeFilter === 'favorites') {
const favoriteAuthors = getFavoriteAuthors();
filtered = favoriteAuthors;
}
// Apply search filter if active
if (searchQuery && searchQuery.trim()) {
const query = searchQuery.toLowerCase();
filtered = filtered.filter(author =>
author.name.toLowerCase().includes(query) ||
(author.profession && author.profession.some(p => p.toLowerCase().includes(query))) ||
(author.biography?.short && author.biography.short.toLowerCase().includes(query)) ||
(author.biography?.long && author.biography.long.toLowerCase().includes(query))
);
}
// Apply advanced filters
filtered = filterAuthors(filtered, filters);
// Sort filtered results
const sorted = [...filtered];
sorted.sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name);
} else {
const aCount = a.quoteIds?.length || 0;
const bCount = b.quoteIds?.length || 0;
return bCount - aCount; // Absteigend nach Anzahl
}
});
return sorted;
}, [authors, sortBy, searchQuery, filters, activeFilter, getFavoriteAuthors]);
const handleRemoveFilter = (category: keyof AuthorFilters, value: string) => {
setFilters(prev => ({
...prev,
[category]: prev[category].filter(v => v !== value)
}));
};
const handleClearAllFilters = () => {
setFilters({
epochs: [],
professions: [],
nationalities: [],
quoteCount: [],
special: []
});
};
const hasActiveFilters = Object.values(filters).some(arr => arr.length > 0);
// Get favorite authors count
const favoriteAuthors = getFavoriteAuthors();
// Tab handling for segmented control
const tabs = [
{ key: 'all', label: t('common.all'), count: authors?.length || 0 },
{ key: 'favorites', label: t('navigation.favorites'), count: favoriteAuthors?.length || 0 }
];
const handleTabChange = (tabKey: string) => {
setActiveFilter(tabKey as 'all' | 'favorites');
};
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
});
// Get gradient colors based on author index or name
const getAuthorGradient = (index: number) => {
const gradients = isDarkMode ? [
['#9333EA', '#7C3AED'], // Purple
['#EC4899', '#F472B6'], // Pink
['#3B82F6', '#0EA5E9'], // Blue
['#10B981', '#34D399'], // Green
['#F59E0B', '#F97316'], // Amber
['#06B6D4', '#14B8A6'], // Cyan
] : [
['#7C3AED', '#6D28D9'], // Purple (darker for light mode)
['#DB2777', '#BE185D'], // Pink (darker for light mode)
['#2563EB', '#1D4ED8'], // Blue (darker for light mode)
['#059669', '#047857'], // Green (darker for light mode)
['#D97706', '#B45309'], // Amber (darker for light mode)
['#0891B2', '#0E7490'], // Cyan (darker for light mode)
];
return gradients[index % gradients.length];
};
// Calculate card height for vertical mode
const TAB_BAR_HEIGHT = 80;
const STATUS_BAR_HEIGHT = 44;
const CARD_HEIGHT = screenHeight - STATUS_BAR_HEIGHT - TAB_BAR_HEIGHT - 100;
// Render einzelner Autor als Card
const renderAuthorItem = ({ item, index }: { item: Author; index: number }) => {
// Handle actions
const handleShare = (e?: any) => {
if (e) e.stopPropagation();
shareAuthor(item);
};
const handleCopy = (e?: any) => {
if (e) e.stopPropagation();
copyAuthorToClipboard(item);
};
if (viewMode === 'card') {
return (
<AuthorCard
author={item}
index={index}
variant="vertical"
isFavorite={isAuthorFavorite(item.id)}
onToggleFavorite={() => toggleAuthorFavorite(item)}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.push(`/author/${item.id}`);
}}
scrollY={scrollY}
cardHeight={CARD_HEIGHT}
/>
);
}
// List view - use the original AuthorCardItem
return (
<AuthorCardItem
item={item}
index={index}
isDarkMode={isDarkMode}
isAuthorFavorite={isAuthorFavorite}
toggleAuthorFavorite={toggleAuthorFavorite}
router={router}
getAuthorGradient={getAuthorGradient}
t={t}
/>
);
};
if (isLoading) {
return <LoadingScreen message={t('authors.loadingAuthors')} />;
}
return (
<>
<Stack.Screen
options={{
title: t('navigation.authors'),
headerShown: true,
headerTransparent: true,
headerBlurEffect: isDarkMode ? 'dark' : 'light',
headerStyle: {
backgroundColor: 'transparent',
},
headerTintColor: isDarkMode ? '#ffffff' : '#000000',
headerShadowVisible: false,
headerRight: () => (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<TouchableOpacity
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setViewMode(viewMode === 'card' ? 'list' : 'card');
}}
style={{
justifyContent: 'center',
alignItems: 'center',
width: 44,
height: 44,
marginTop: -4,
}}
>
<Icon
name={viewMode === 'list' ? 'grid-outline' : 'list-outline'}
size={24}
color={isDarkMode ? '#ffffff' : '#000000'}
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
bottomSheetRef.current?.snapToIndex(0);
}}
style={{
justifyContent: 'center',
alignItems: 'center',
width: 44,
height: 44,
marginTop: -4,
}}
>
<Icon
name="filter-outline"
size={24}
color={hasActiveFilters ? '#7c3aed' : (isDarkMode ? '#ffffff' : '#000000')}
/>
{hasActiveFilters && (
<View style={{
position: 'absolute',
top: 8,
right: 8,
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: '#7c3aed'
}} />
)}
</TouchableOpacity>
</View>
),
headerRightContainerStyle: {
paddingRight: 16,
},
}}
/>
<View style={{ flex: 1, backgroundColor: colors.background }}>
{/* Loading State */}
{(!isInitialized || isLoading) ? (
<View className="flex-1 justify-center items-center">
<ActivityIndicator size="large" color={isDarkMode ? '#ffffff' : '#000000'} />
<Text className={`${isDarkMode ? 'text-white/60' : 'text-black/60'} mt-4`}>
{t('common.loading')}
</Text>
</View>
) : (
<>
{/* Active Filter Chips */}
<ActiveFilterChips
filters={filters}
onRemoveFilter={handleRemoveFilter}
onClearAll={handleClearAllFilters}
/>
{/* Authors List */}
{filteredAndSortedAuthors.length === 0 ? (
<View className="flex-1 justify-center items-center px-6" style={{ paddingTop: 100 }}>
<Icon
name="people-outline"
size={64}
color={isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)"}
/>
<Text className={`${isDarkMode ? 'text-white/60' : 'text-black/60'} text-lg mt-4 text-center font-semibold`}>
{searchQuery && searchQuery.trim() ? t('authors.noSearchResults') : t('authors.noAuthors')}
</Text>
{searchQuery && searchQuery.trim() && (
<Text className={`${isDarkMode ? 'text-white/40' : 'text-black/40'} text-sm mt-2 text-center`}>
No authors found for &quot;{searchQuery}&quot;
</Text>
)}
</View>
) : (
<AnimatedFlatList
data={filteredAndSortedAuthors}
renderItem={renderAuthorItem}
keyExtractor={(item) => item.id}
showsVerticalScrollIndicator={false}
contentContainerStyle={{
paddingTop: LIST_CONTAINER_PADDING.top,
paddingBottom: viewMode === 'list' ? LIST_CONTAINER_PADDING.bottom + 80 : CARD_HEIGHT * 0.1
}}
onScroll={scrollHandler}
scrollEventThrottle={16}
pagingEnabled={viewMode === 'card'}
snapToInterval={viewMode === 'card' ? CARD_HEIGHT : undefined}
snapToAlignment={viewMode === 'card' ? "start" : undefined}
decelerationRate={viewMode === 'card' ? "fast" : "normal"}
getItemLayout={viewMode === 'card' ? (data, index) => ({
length: CARD_HEIGHT,
offset: CARD_HEIGHT * index,
index
}) : undefined}
/>
)}
{/* Glass Tab Selector at bottom - positioned above tab bar */}
<View className="absolute bottom-24 left-0 right-0 z-10">
<GlassTabSelector
tabs={tabs}
activeTab={activeFilter}
onTabChange={handleTabChange}
/>
</View>
</>
)}
</View>
{/* Filter Bottom Sheet */}
<AuthorFilterBottomSheet
bottomSheetRef={bottomSheetRef}
filters={filters}
onFiltersChange={setFilters}
onClearAll={handleClearAllFilters}
/>
</>
);
}

View file

@ -0,0 +1,14 @@
import { Stack } from 'expo-router';
export default function ListeLayout() {
return (
<Stack>
<Stack.Screen
name="index"
options={{
headerShown: false,
}}
/>
</Stack>
);
}

View file

@ -0,0 +1,546 @@
import { Stack, useRouter } from 'expo-router';
import { View, Text, FlatList, Pressable, TextInput, ScrollView, Alert, TouchableOpacity, Dimensions } from 'react-native';
import { useListStore, LIST_COLORS, List } from '~/store/listStore';
import React, { useEffect, useState } from 'react';
import { Icon } from '~/components/Icon';
import * as Haptics from 'expo-haptics';
import usePremiumStore from '~/store/premiumStore';
import { useListCreation } from '~/hooks/useListCreation';
import { PremiumLimitDialog } from '~/components/PremiumLimitDialog';
import { useIsDarkMode } from '~/store/settingsStore';
import Animated, {
FadeInDown,
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
runOnJS,
useAnimatedScrollHandler,
interpolate,
Extrapolate
} from 'react-native-reanimated';
import { LinearGradient } from 'expo-linear-gradient';
import { BlurView } from 'expo-blur';
import { LIST_ITEM_CLASSES, LIST_CONTAINER_PADDING } from '~/constants/layout';
import { useTranslation } from 'react-i18next';
import { GlassFAB } from '~/components/common/GlassFAB';
import { useTheme } from '~/hooks/useTheme';
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
const { height: screenHeight } = Dimensions.get('window');
export default function Liste() {
const router = useRouter();
const isDarkMode = useIsDarkMode();
const { colors } = useTheme();
const { t } = useTranslation();
const [viewMode, setViewMode] = useState<'card' | 'list'>('list');
const scrollY = useSharedValue(0);
const [showLimitDialog, setShowLimitDialog] = useState(false);
// Premium store
const { canCreateCollection, getRemainingCollections, MAX_WEEKLY_COLLECTIONS } = usePremiumStore();
// Inline list creation state
const [isCreatingNew, setIsCreatingNew] = useState(false);
const [newListData, setNewListData] = useState({
name: '',
description: '',
color: LIST_COLORS[0]
});
const newListScale = useSharedValue(0.98);
const newListOpacity = useSharedValue(0);
const {
lists,
initializeLists,
updateList,
deleteList,
duplicateList,
getListStats
} = useListStore();
const { createList, canCreateList } = useListCreation();
useEffect(() => {
initializeLists();
}, []);
// Inline list creation functions
const startInlineCreation = () => {
// Check if user can create list
if (!canCreateList()) {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
setShowLimitDialog(true);
return;
}
setNewListData({
name: '',
description: '',
color: LIST_COLORS[Math.floor(Math.random() * LIST_COLORS.length)]
});
setIsCreatingNew(true);
// Animate new list appearance - very subtle
newListScale.value = withSpring(1, { damping: 25, stiffness: 300, mass: 0.8 });
newListOpacity.value = withTiming(1, { duration: 100 });
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
};
const handleInlineNameChange = (name: string) => {
setNewListData(prev => ({ ...prev, name }));
};
const handleInlineDescriptionChange = (description: string) => {
setNewListData(prev => ({ ...prev, description }));
};
const handleInlineColorChange = (color: string) => {
setNewListData(prev => ({ ...prev, color }));
};
const saveInlineList = () => {
if (!newListData.name.trim()) return;
const result = createList(newListData.name, newListData.description, newListData.color);
// Check if creation was successful
if (!result.success) {
setShowLimitDialog(true);
return;
}
// Animate out and reset
newListScale.value = withSpring(0.8, {}, () => {
newListScale.value = withSpring(1);
});
setIsCreatingNew(false);
setNewListData({
name: '',
description: '',
color: LIST_COLORS[0]
});
// Reset animation values
newListScale.value = 0.98;
newListOpacity.value = 0;
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
};
// Helper function to reset state safely
const resetCreationState = () => {
setIsCreatingNew(false);
setNewListData({
name: '',
description: '',
color: LIST_COLORS[0]
});
// Reset animation values
newListScale.value = 0.98;
newListOpacity.value = 0;
};
const cancelInlineCreation = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
// Animate out before removing - subtle and fast
newListScale.value = withTiming(0.95, { duration: 100 });
newListOpacity.value = withTiming(0, { duration: 150 }, (finished) => {
// Only reset if animation completed successfully
if (finished) {
runOnJS(resetCreationState)();
}
});
};
const handleDeleteList = (list: List) => {
if (list.isDefault) {
Alert.alert(t('lists.notice'), t('lists.cannotDeleteDefault'));
return;
}
Alert.alert(
t('lists.deleteList'),
t('lists.deleteListConfirm', { name: list.name }),
[
{ text: t('common.cancel'), style: 'cancel' },
{
text: t('common.delete'),
style: 'destructive',
onPress: () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
deleteList(list.id);
},
},
]
);
};
const handleDuplicateList = (list: List) => {
const newName = `${list.name} (Kopie)`;
duplicateList(list.id, newName);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
};
// Create empty list for inline creation
const createEmptyList = React.useCallback((): List => ({
id: 'new-list-temp',
name: newListData.name,
description: newListData.description,
color: newListData.color,
quoteIds: [],
isDefault: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}), [newListData]);
// Create display data including potential new list at top
const displayLists = React.useMemo(() => {
if (isCreatingNew) {
const emptyList = createEmptyList();
return [emptyList, ...lists];
}
return lists;
}, [isCreatingNew, lists, createEmptyList]);
// Calculate card height for vertical mode
const TAB_BAR_HEIGHT = 80;
const STATUS_BAR_HEIGHT = 44;
const CARD_HEIGHT = screenHeight - STATUS_BAR_HEIGHT - TAB_BAR_HEIGHT - 100;
const renderListCard = ({ item, index }: { item: List; index: number }) => {
const stats = getListStats(item.id);
const isEditing = item.id === 'new-list-temp';
const wrapperStyle = viewMode === 'card' ? {
height: CARD_HEIGHT,
justifyContent: 'center',
paddingHorizontal: 20,
width: '100%'
} : {};
return (
<View
style={wrapperStyle}
className={viewMode === 'list' ? LIST_ITEM_CLASSES.wrapper : ''}
>
{!isEditing ? (
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.push(`/list/${item.id}`);
}}
onLongPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
if (!item.isDefault) {
Alert.alert(
item.name,
t('lists.listActions'),
[
{ text: t('common.cancel'), style: 'cancel' },
{ text: t('lists.duplicate'), onPress: () => handleDuplicateList(item) },
{
text: t('common.delete'),
style: 'destructive',
onPress: () => handleDeleteList(item)
},
]
);
}
}}
>
{/* Gradient border like QuoteCard */}
<LinearGradient
colors={isDarkMode ? [item.color, item.color + 'CC'] : [item.color + 'CC', item.color + '99']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={{
borderRadius: 24,
padding: 1
}}
>
<View className="bg-black/40 rounded-3xl backdrop-blur-xl">
<View className="p-6">
{/* Main content */}
<Text
className="mb-4"
style={{
fontFamily: 'Georgia',
fontSize: 22,
lineHeight: 30,
color: 'white',
fontWeight: '300',
letterSpacing: 0.3
}}
>
{item.name}
</Text>
{item.description && (
<Text
className="text-white/60 mb-4"
style={{ fontSize: 14, lineHeight: 20 }}
>
{item.description}
</Text>
)}
{/* Bottom divider section like QuoteCard */}
<View className={`border-t border-white/10 pt-3`}>
<View className="flex-row items-center justify-between">
<Text className="text-white/60 text-sm">
{t('common.quotes_count', { count: stats.totalQuotes })}
</Text>
{item.isDefault && (
<Text className="text-white/40 text-xs uppercase tracking-wider">
System
</Text>
)}
</View>
</View>
</View>
</View>
</LinearGradient>
</Pressable>
) : (
// Edit Mode - Inline Creation Card
<LinearGradient
colors={isDarkMode ? [newListData.color, newListData.color + 'CC'] : [newListData.color + 'CC', newListData.color + '99']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={{
borderRadius: 24,
padding: 1
}}
>
<View className="bg-black/40 rounded-3xl backdrop-blur-xl">
<View className="p-6">
{/* Name Input */}
<TextInput
value={newListData.name}
onChangeText={handleInlineNameChange}
placeholder="Listen Name..."
placeholderTextColor="rgba(255,255,255,0.4)"
style={{
fontFamily: 'Georgia',
fontSize: 22,
lineHeight: 30,
color: 'white',
fontWeight: '300',
letterSpacing: 0.3,
paddingVertical: 0,
marginBottom: 16
}}
autoFocus
/>
{/* Description Input */}
<TextInput
value={newListData.description}
onChangeText={handleInlineDescriptionChange}
placeholder={t('lists.descriptionPlaceholder')}
placeholderTextColor="rgba(255,255,255,0.3)"
multiline
style={{
fontSize: 14,
lineHeight: 20,
color: 'rgba(255,255,255,0.6)',
paddingVertical: 0,
marginBottom: 16,
minHeight: 40
}}
/>
{/* Color Selection */}
<View className="mb-4">
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View className="flex-row">
{LIST_COLORS.slice(0, 12).map((color) => (
<Pressable
key={color}
onPress={() => handleInlineColorChange(color)}
className="mr-3"
>
<View
style={{ backgroundColor: color }}
className={`w-8 h-8 rounded-full ${
newListData.color === color ? 'border-2 border-white' : ''
}`}
>
{newListData.color === color && (
<View className="flex-1 items-center justify-center">
<Icon name="checkmark" size={16} color="#ffffff" />
</View>
)}
</View>
</Pressable>
))}
</View>
</ScrollView>
</View>
{/* Bottom divider section with action buttons */}
<View className={`border-t border-white/10 pt-3`}>
<View className="flex-row items-center justify-between">
<Text className="text-white/40 text-sm">
{t('lists.newList')}
</Text>
{/* Action Buttons */}
<View className="flex-row items-center gap-4">
{/* Cancel Button */}
<Pressable
onPress={cancelInlineCreation}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Icon
name="close"
size={22}
color="rgba(255,255,255,0.8)"
/>
</Pressable>
{/* Save Button */}
<Pressable
onPress={saveInlineList}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
disabled={!newListData.name.trim()}
>
<Icon
name="checkmark"
size={22}
color={newListData.name.trim() ? "rgba(255,255,255,0.8)" : "rgba(255,255,255,0.3)"}
/>
</Pressable>
</View>
</View>
</View>
</View>
</View>
</LinearGradient>
)}
</View>
);
};
return (
<>
<Stack.Screen
options={{
title: t('lists.lists'),
headerShown: true,
headerTransparent: true,
headerBlurEffect: isDarkMode ? 'dark' : 'light',
headerStyle: {
backgroundColor: 'transparent',
},
headerTintColor: isDarkMode ? '#ffffff' : '#000000',
headerShadowVisible: false,
headerRight: () => (
<TouchableOpacity
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setViewMode(viewMode === 'card' ? 'list' : 'card');
}}
style={{
justifyContent: 'center',
alignItems: 'center',
width: 44,
height: 44,
marginTop: -4,
}}
>
<Icon
name={viewMode === 'list' ? 'grid-outline' : 'list-outline'}
size={24}
color={isDarkMode ? '#ffffff' : '#000000'}
/>
</TouchableOpacity>
),
headerRightContainerStyle: {
paddingRight: 16,
},
}}
/>
<View style={{ flex: 1, backgroundColor: colors.background }}>
{/* Lists */}
{displayLists.length === 0 ? (
<View className="flex-1 justify-center items-center px-6" style={{ paddingTop: 100 }}>
<Icon
name="albums-outline"
size={64}
color={isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)"}
/>
<Text className={`${isDarkMode ? 'text-white/60' : 'text-black/60'} text-lg mt-4 text-center font-semibold`}>
{t('lists.noLists')}
</Text>
<Text className={`${isDarkMode ? 'text-white/40' : 'text-black/40'} text-sm mt-2 text-center`}>
{t('lists.createFirst')}
</Text>
<Pressable
onPress={startInlineCreation}
className={`${isDarkMode ? 'bg-white' : 'bg-black'} rounded-full px-6 py-3 mt-6`}
>
<Text className={`${isDarkMode ? 'text-black' : 'text-white'} font-semibold`}>
{t('lists.createNew')}
</Text>
</Pressable>
</View>
) : (
<AnimatedFlatList
data={displayLists}
renderItem={renderListCard}
keyExtractor={(item) => item.id}
showsVerticalScrollIndicator={false}
contentContainerStyle={{
paddingBottom: viewMode === 'list' ? LIST_CONTAINER_PADDING.bottom : CARD_HEIGHT * 0.1,
paddingTop: LIST_CONTAINER_PADDING.top
}}
onScroll={useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
})}
scrollEventThrottle={16}
pagingEnabled={viewMode === 'card'}
snapToInterval={viewMode === 'card' ? CARD_HEIGHT : undefined}
snapToAlignment={viewMode === 'card' ? "start" : undefined}
decelerationRate={viewMode === 'card' ? "fast" : "normal"}
getItemLayout={viewMode === 'card' ? (data, index) => ({
length: CARD_HEIGHT,
offset: CARD_HEIGHT * index,
index
}) : undefined}
/>
)}
{/* Floating Action Button for Lists */}
{displayLists.length > 0 && !isCreatingNew && (
<GlassFAB
onPress={startInlineCreation}
icon="add"
size="medium"
position="bottom-right"
/>
)}
{/* Premium Limit Dialog */}
<PremiumLimitDialog
visible={showLimitDialog}
onClose={() => setShowLimitDialog(false)}
limitType="collections"
remaining={getRemainingCollections()}
max={MAX_WEEKLY_COLLECTIONS}
/>
</View>
</>
);
}

View file

@ -0,0 +1,14 @@
import { Stack } from 'expo-router';
export default function MyQuotesLayout() {
return (
<Stack>
<Stack.Screen
name="index"
options={{
headerShown: false,
}}
/>
</Stack>
);
}

View file

@ -0,0 +1,491 @@
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, Pressable, TouchableOpacity } from 'react-native';
import { Stack, useRouter } from 'expo-router';
import { useQuotesStore, UserQuote, EnhancedQuote } from '~/store/quotesStore';
import { Icon } from '~/components/Icon';
import { useIsDarkMode, useSettingsStore } from '~/store/settingsStore';
import QuoteCard from '~/components/QuoteCard';
import * as Haptics from 'expo-haptics';
import { BlurView } from 'expo-blur';
import { GlassFAB } from '~/components/common/GlassFAB';
// DateTimePicker, Host, Button removed - no longer needed for inline editing
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
runOnJS
} from 'react-native-reanimated';
import { LIST_CONTAINER_PADDING, LIST_ITEM_CLASSES } from '~/constants/layout';
import { useTranslation } from 'react-i18next';
import { useTheme } from '~/hooks/useTheme';
export default function MyQuotes() {
const router = useRouter();
const isDarkMode = useIsDarkMode();
const { colors } = useTheme();
const { userName } = useSettingsStore();
const { t } = useTranslation();
const {
getUserQuotes,
addUserQuote,
updateUserQuote,
deleteUserQuote,
toggleUserQuoteFavorite,
initializeStore
} = useQuotesStore();
// Note: Modal-based editing removed in favor of inline editing
// View mode state
const [viewMode, setViewMode] = useState<'card' | 'list'>('list');
// Inline editing states
const [isCreatingNew, setIsCreatingNew] = useState(false);
const [newQuoteData, setNewQuoteData] = useState({
text: '',
author: userName || '',
categories: '',
});
// Inline edit states for existing quotes
const [editingQuoteId, setEditingQuoteId] = useState<string | null>(null);
const [editQuoteData, setEditQuoteData] = useState({
text: '',
author: '',
categories: '',
});
// showDatePicker removed - no longer needed for inline editing
const fabScale = useSharedValue(1);
const newQuoteScale = useSharedValue(0.98);
const newQuoteOpacity = useSharedValue(0);
const editQuoteScale = useSharedValue(1);
const editQuoteOpacity = useSharedValue(1);
useEffect(() => {
initializeStore();
}, []);
const userQuotes = getUserQuotes();
// Convert UserQuote to EnhancedQuote format for QuoteCard
const convertUserQuoteToEnhanced = React.useCallback((userQuote: UserQuote): EnhancedQuote => ({
id: userQuote.id,
text: userQuote.text,
author: {
id: `user-author-${userQuote.id}`,
name: userQuote.author,
profession: []
},
authorId: `user-author-${userQuote.id}`,
categories: userQuote.categories || [],
tags: userQuote.categories || [],
isFavorite: userQuote.isFavorite || false,
source: userQuote.quoteDate ? new Date(userQuote.quoteDate).toLocaleDateString('de-DE') : undefined,
year: userQuote.quoteDate ? new Date(userQuote.quoteDate).getFullYear().toString() : undefined,
category: userQuote.categories?.[0] || 'personal'
}), []);
// Create empty quote for inline creation
const createEmptyQuote = React.useCallback((): EnhancedQuote => ({
id: 'new-quote-temp',
text: newQuoteData.text,
author: {
id: 'new-author-temp',
name: newQuoteData.author,
profession: []
},
authorId: 'new-author-temp',
categories: newQuoteData.categories.split(',').map(c => c.trim()).filter(c => c.length > 0),
tags: [],
isFavorite: false,
source: undefined,
year: undefined,
category: 'personal'
}), [newQuoteData]);
// Create display data including potential new quote at top
const displayQuotes = React.useMemo(() => {
if (isCreatingNew) {
const emptyQuote = createEmptyQuote();
return [emptyQuote, ...userQuotes.map(convertUserQuoteToEnhanced)];
}
return userQuotes.map(convertUserQuoteToEnhanced);
}, [isCreatingNew, userQuotes, createEmptyQuote, convertUserQuoteToEnhanced]);
// Note: Modal-based editor functions removed in favor of inline editing
const handleDelete = (id: string) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
deleteUserQuote(id);
};
// Inline editing functions
const startInlineCreation = () => {
setNewQuoteData({
text: '',
author: userName || '',
categories: '',
});
setIsCreatingNew(true);
// Animate new quote appearance - very subtle
newQuoteScale.value = withSpring(1, { damping: 25, stiffness: 300, mass: 0.8 });
newQuoteOpacity.value = withTiming(1, { duration: 100 });
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
};
const handleInlineTextChange = (text: string) => {
setNewQuoteData(prev => ({ ...prev, text }));
};
const handleInlineAuthorChange = (author: string) => {
setNewQuoteData(prev => ({ ...prev, author }));
};
const handleInlineCategoryChange = (categories: string) => {
setNewQuoteData(prev => ({ ...prev, categories }));
};
const saveInlineQuote = () => {
if (!newQuoteData.text.trim()) return;
const categories = newQuoteData.categories
.split(',')
.map(c => c.trim())
.filter(c => c.length > 0);
const finalAuthor = newQuoteData.author || userName || 'Ich';
addUserQuote({
text: newQuoteData.text,
author: finalAuthor,
categories,
quoteDate: new Date().toISOString()
});
// Animate out and reset
newQuoteScale.value = withSpring(0.8, {}, () => {
newQuoteScale.value = withSpring(1);
});
setIsCreatingNew(false);
setNewQuoteData({
text: '',
author: userName || '',
categories: '',
});
// Reset animation values
newQuoteScale.value = 0.98;
newQuoteOpacity.value = 0;
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
};
// Helper function to reset state safely
const resetCreationState = () => {
setIsCreatingNew(false);
setNewQuoteData({
text: '',
author: userName || '',
categories: '',
});
// Reset animation values
newQuoteScale.value = 0.98;
newQuoteOpacity.value = 0;
};
const cancelInlineCreation = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
// Animate out before removing - subtle and fast
newQuoteScale.value = withTiming(0.95, { duration: 100 });
newQuoteOpacity.value = withTiming(0, { duration: 150 }, (finished) => {
// Only reset if animation completed successfully
if (finished) {
runOnJS(resetCreationState)();
}
});
};
// Inline edit functions for existing quotes
const startInlineEdit = (quote: UserQuote) => {
setEditingQuoteId(quote.id);
setEditQuoteData({
text: quote.text,
author: quote.author,
categories: quote.categories?.join(', ') || '',
});
// Subtle animation feedback
editQuoteScale.value = withSpring(0.98, { damping: 25, stiffness: 300, mass: 0.8 });
editQuoteOpacity.value = withTiming(0.95, { duration: 100 });
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
};
const handleEditTextChange = (text: string) => {
setEditQuoteData(prev => ({ ...prev, text }));
};
const handleEditAuthorChange = (author: string) => {
setEditQuoteData(prev => ({ ...prev, author }));
};
const handleEditCategoryChange = (categories: string) => {
setEditQuoteData(prev => ({ ...prev, categories }));
};
const saveInlineEdit = () => {
if (!editQuoteData.text.trim() || !editingQuoteId) return;
const categories = editQuoteData.categories
.split(',')
.map(c => c.trim())
.filter(c => c.length > 0);
const finalAuthor = editQuoteData.author || userName || 'Ich';
updateUserQuote(editingQuoteId, {
text: editQuoteData.text,
author: finalAuthor,
categories,
quoteDate: userQuotes.find(q => q.id === editingQuoteId)?.quoteDate || new Date().toISOString()
});
// Animate completion
editQuoteScale.value = withSpring(1.02, {}, () => {
editQuoteScale.value = withSpring(1);
});
editQuoteOpacity.value = withTiming(1, { duration: 150 });
setEditingQuoteId(null);
setEditQuoteData({
text: '',
author: '',
categories: '',
});
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
};
const cancelInlineEdit = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
// Animate back to normal
editQuoteScale.value = withSpring(1, { damping: 25, stiffness: 300 });
editQuoteOpacity.value = withTiming(1, { duration: 150 });
setEditingQuoteId(null);
setEditQuoteData({
text: '',
author: '',
categories: '',
});
};
// fabAnimatedStyle removed - not needed for current FAB implementation
const newQuoteAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: newQuoteScale.value }],
opacity: newQuoteOpacity.value,
}));
const editQuoteAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: editQuoteScale.value }],
opacity: editQuoteOpacity.value,
}));
const renderQuote = ({ item, index }: { item: EnhancedQuote; index: number }) => {
const isNewQuote = item.id === 'new-quote-temp';
const isBeingEdited = editingQuoteId === item.id;
if (isNewQuote) {
return (
<Animated.View style={newQuoteAnimatedStyle} className="mb-5">
<QuoteCard
quote={item}
variant="edit"
editMode={true}
onToggleFavorite={() => {}}
onTextChange={handleInlineTextChange}
onAuthorChange={handleInlineAuthorChange}
onCategoryChange={handleInlineCategoryChange}
onSave={saveInlineQuote}
onCancel={cancelInlineCreation}
/>
</Animated.View>
);
}
// Find original UserQuote for existing quotes
const originalQuote = userQuotes.find(q => q.id === item.id);
if (!originalQuote) return null;
// If this quote is being edited inline, show edit mode
if (isBeingEdited) {
const editQuote: EnhancedQuote = {
...item,
text: editQuoteData.text,
author: {
...item.author,
name: editQuoteData.author
},
categories: editQuoteData.categories.split(',').map(c => c.trim()).filter(c => c.length > 0)
};
return (
<Animated.View style={editQuoteAnimatedStyle} className="mb-5">
<QuoteCard
quote={editQuote}
variant="edit"
editMode={true}
onToggleFavorite={() => {}}
onTextChange={handleEditTextChange}
onAuthorChange={handleEditAuthorChange}
onCategoryChange={handleEditCategoryChange}
onSave={saveInlineEdit}
onCancel={cancelInlineEdit}
/>
</Animated.View>
);
}
// Use different variant based on viewMode
if (viewMode === 'card') {
return (
<View style={{ position: 'relative', width: '100%' }}>
<QuoteCard
quote={item}
variant="vertical"
index={index}
cardHeight={600} // Adjust height as needed
onToggleFavorite={() => toggleUserQuoteFavorite(originalQuote.id)}
onAuthorPress={() => {}}
onEdit={() => startInlineEdit(originalQuote)}
onDelete={() => handleDelete(originalQuote.id)}
/>
</View>
);
}
// List view (default)
return (
<View className="mb-5">
<QuoteCard
quote={item}
onToggleFavorite={() => toggleUserQuoteFavorite(originalQuote.id)}
onAuthorPress={() => {}} // Remove author press to avoid modal
onEdit={() => startInlineEdit(originalQuote)}
onDelete={() => handleDelete(originalQuote.id)}
/>
</View>
);
};
return (
<>
<Stack.Screen
options={{
title: t('myQuotes.title'),
headerShown: true,
headerTransparent: true,
headerBlurEffect: isDarkMode ? 'dark' : 'light',
headerStyle: {
backgroundColor: 'transparent',
},
headerTintColor: isDarkMode ? '#ffffff' : '#000000',
headerShadowVisible: false,
headerRight: () => (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<TouchableOpacity
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setViewMode(viewMode === 'card' ? 'list' : 'card');
}}
style={{
justifyContent: 'center',
alignItems: 'center',
width: 44,
height: 44,
marginTop: -4,
}}
>
<Icon
name={viewMode === 'list' ? 'grid-outline' : 'list-outline'}
size={24}
color={isDarkMode ? '#ffffff' : '#000000'}
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() => router.push('/settings')}
style={{
justifyContent: 'center',
alignItems: 'center',
width: 44,
height: 44,
marginTop: -4,
}}
>
<Icon
name="settings-outline"
size={24}
color={isDarkMode ? '#ffffff' : '#000000'}
/>
</TouchableOpacity>
</View>
),
headerRightContainerStyle: {
paddingRight: 16,
},
}}
/>
<View style={{ flex: 1, backgroundColor: colors.background }}>
{userQuotes.length === 0 && !isCreatingNew ? (
<View className="flex-1 justify-center items-center px-6" style={{ paddingTop: 100 }}>
<Icon
name="create-outline"
size={64}
color={isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)"}
/>
<Text className={`${isDarkMode ? 'text-white/60' : 'text-black/60'} text-lg mt-4 text-center font-semibold`}>
{t('myQuotes.noQuotes')}
</Text>
<Text className={`${isDarkMode ? 'text-white/40' : 'text-black/40'} text-sm mt-2 text-center`}>
{t('myQuotes.createFirst')}
</Text>
</View>
) : (
<FlatList
data={displayQuotes}
renderItem={renderQuote}
keyExtractor={(item) => item.id}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: LIST_CONTAINER_PADDING.bottom, paddingTop: LIST_CONTAINER_PADDING.top }}
pagingEnabled={viewMode === 'card'}
snapToInterval={viewMode === 'card' ? 600 : undefined}
snapToAlignment={viewMode === 'card' ? "start" : undefined}
decelerationRate={viewMode === 'card' ? "fast" : "normal"}
/>
)}
{/* Floating Action Button */}
{!isCreatingNew && (
<GlassFAB
onPress={startInlineCreation}
icon="add"
size="medium"
position="bottom-right"
/>
)}
{/* Modal-based editing removed - now using inline editing */}
</View>
</>
);
}

View file

@ -0,0 +1,14 @@
import { Stack } from 'expo-router';
export default function QuotesLayout() {
return (
<Stack>
<Stack.Screen
name="index"
options={{
headerShown: false,
}}
/>
</Stack>
);
}

View file

@ -0,0 +1,490 @@
import { useRouter, useLocalSearchParams, Stack } from 'expo-router';
import { View, Text, ActivityIndicator, Dimensions, FlatList, TouchableOpacity } from 'react-native';
import { useQuotesStore } from '~/store/quotesStore';
import QuoteCard from '~/components/QuoteCard';
import * as Haptics from 'expo-haptics';
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { Icon } from '~/components/Icon';
import { useIsDarkMode } from '~/store/settingsStore';
import { useTranslation } from 'react-i18next';
import { GlassTabSelector } from '~/components/common/GlassTabSelector';
import Animated, {
useSharedValue,
useAnimatedScrollHandler
} from 'react-native-reanimated';
import { LIST_CONTAINER_PADDING } from '~/constants/layout';
import { useTheme } from '~/hooks/useTheme';
import { QuoteFilterSheet } from '~/components/quotes/QuoteFilterSheet';
import { ActiveQuoteFilterChips } from '~/components/quotes/ActiveQuoteFilterChips';
import { filterQuotes, QuoteFilters, hasActiveFilters as checkHasActiveFilters } from '~/utils/quoteFilters';
import BottomSheet from '@gorhom/bottom-sheet';
const { height: screenHeight } = Dimensions.get('window');
const STATUS_BAR_HEIGHT = 44; // iOS status bar
const TAB_BAR_HEIGHT = 80; // Tab bar height
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
export default function Home() {
const router = useRouter();
const params = useLocalSearchParams();
const { widgetQuoteId, q: searchQuery } = params;
const { t } = useTranslation();
const isDarkMode = useIsDarkMode();
const { colors } = useTheme();
const [displayedQuotes, setDisplayedQuotes] = useState([]);
const [activeFilter, setActiveFilter] = useState<'recommended' | 'favorites'>('recommended');
const [viewMode, setViewMode] = useState<'card' | 'list'>('card');
const [filters, setFilters] = useState<QuoteFilters>({
timePeriods: [],
sourceTypes: [],
categories: [],
authorEras: [],
special: []
});
const flatListRef = useRef(null);
const bottomSheetRef = useRef<BottomSheet>(null);
const usedQuoteIds = useRef(new Set());
const scrollY = useSharedValue(0);
const {
quotes,
toggleFavorite,
getFavorites,
isLoading,
isInitialized
} = useQuotesStore();
// Filter quotes based on active filter, advanced filters, and search query
const filteredQuotes = useMemo(() => {
// Safety check: ensure quotes array exists
if (!quotes || !Array.isArray(quotes)) {
return [];
}
let quotesToFilter = activeFilter === 'favorites' ? getFavorites() : quotes;
// Apply advanced filters
quotesToFilter = filterQuotes(quotesToFilter, filters);
// Then apply search query if present
if (searchQuery && typeof searchQuery === 'string' && searchQuery.trim()) {
const query = searchQuery.toLowerCase().trim();
quotesToFilter = quotesToFilter.filter(quote =>
quote.text.toLowerCase().includes(query) ||
(quote.author?.name && quote.author.name.toLowerCase().includes(query)) ||
(quote.tags && quote.tags.some((tag: string) => tag.toLowerCase().includes(query)))
);
}
return quotesToFilter;
}, [quotes, activeFilter, getFavorites, searchQuery, filters]);
// Height calculations
const TOTAL_TOP_HEIGHT = STATUS_BAR_HEIGHT;
const TAB_SELECTOR_HEIGHT = 60; // Height of TabSelector component
const AVAILABLE_HEIGHT = screenHeight - TOTAL_TOP_HEIGHT - TAB_BAR_HEIGHT - TAB_SELECTOR_HEIGHT;
const CARD_HEIGHT = AVAILABLE_HEIGHT * 0.9; // Cards take 90% of available height
const VERTICAL_PADDING = (AVAILABLE_HEIGHT - CARD_HEIGHT) / 2; // Center padding
// Handle widget deep link
useEffect(() => {
if (widgetQuoteId && quotes.length > 0 && displayedQuotes.length > 0) {
// Find quote by hash value (matching Swift's hashValue)
const targetQuote = quotes.find(q => {
// Simple hash calculation that should be more consistent
const hash = Math.abs(
q.text.split('').reduce((a: number, b: string) => {
a = ((a << 5) - a) + b.charCodeAt(0);
return a & a;
}, 0)
);
return String(hash) === String(widgetQuoteId);
});
if (targetQuote) {
// Check if quote is already in displayed quotes
const existingIndex = displayedQuotes.findIndex((q: any) => q.id === targetQuote.id);
if (existingIndex !== -1) {
// Quote already displayed, scroll to it
setTimeout(() => {
flatListRef.current?.scrollToIndex({
index: existingIndex,
animated: true
});
}, 500);
} else {
// Add quote to beginning of list (after daily quote)
setDisplayedQuotes(prev => [targetQuote, ...prev]);
usedQuoteIds.current.add(targetQuote.id);
// Scroll to the new quote
setTimeout(() => {
flatListRef.current?.scrollToIndex({
index: 0,
animated: true
});
}, 500);
}
// Haptic feedback
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
}
}, [widgetQuoteId, quotes, displayedQuotes]);
// Initialize with random quotes when quotes are loaded
useEffect(() => {
// Only run once when initialized and quotes are available
if (!isInitialized || !quotes || quotes.length === 0 || displayedQuotes.length > 0) {
return;
}
// Add initial set of random quotes
const initialQuotes = [];
const availableQuotes = [...quotes]; // Create a copy to avoid mutating the original
// Pre-load 5 random quotes
for (let i = 0; i < Math.min(5, availableQuotes.length); i++) {
const randomIndex = Math.floor(Math.random() * availableQuotes.length);
const quote = availableQuotes[randomIndex];
if (!usedQuoteIds.current.has(quote.id)) {
initialQuotes.push(quote);
usedQuoteIds.current.add(quote.id);
availableQuotes.splice(randomIndex, 1);
}
}
if (initialQuotes.length > 0) {
setDisplayedQuotes(initialQuotes);
}
}, [isInitialized, quotes.length]); // Only depend on isInitialized and quotes.length, not quotes array
// Update displayedQuotes when quotes change (for favorites)
useEffect(() => {
if (displayedQuotes.length > 0 && quotes.length > 0 && isInitialized) {
setDisplayedQuotes(prevDisplayed =>
prevDisplayed
.filter(displayedQuote => displayedQuote && displayedQuote.id) // Filter out invalid quotes
.map(displayedQuote => {
const updatedQuote = quotes.find(q => q.id === displayedQuote.id);
return updatedQuote || displayedQuote;
})
);
}
}, [quotes, displayedQuotes.length, isInitialized]);
const loadMoreQuotes = useCallback(() => {
if (!quotes || quotes.length === 0) return;
const availableQuotes = quotes.filter(q => !usedQuoteIds.current.has(q.id));
if (availableQuotes.length === 0) {
// Reset and start over
usedQuoteIds.current.clear();
// Restart with new quotes
const newQuotes = [];
const freshQuotes = quotes;
for (let i = 0; i < Math.min(3, freshQuotes.length); i++) {
const randomIndex = Math.floor(Math.random() * freshQuotes.length);
const quote = freshQuotes[randomIndex];
if (!usedQuoteIds.current.has(quote.id)) {
newQuotes.push(quote);
usedQuoteIds.current.add(quote.id);
freshQuotes.splice(randomIndex, 1);
}
}
setDisplayedQuotes(prev => [...prev, ...newQuotes]);
return;
}
// Add 3 more random quotes
const newQuotes = [];
for (let i = 0; i < Math.min(3, availableQuotes.length); i++) {
const randomIndex = Math.floor(Math.random() * availableQuotes.length);
const quote = availableQuotes[randomIndex];
if (!usedQuoteIds.current.has(quote.id)) {
newQuotes.push(quote);
usedQuoteIds.current.add(quote.id);
availableQuotes.splice(randomIndex, 1);
}
}
if (newQuotes.length > 0) {
setDisplayedQuotes(prev => [...prev, ...newQuotes]);
}
}, [quotes]);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
});
const handleEndReached = () => {
loadMoreQuotes();
};
// Author press handler - reusable
const handleAuthorPress = useCallback((quote: EnhancedQuote) => {
const authorId = quote.authorId || quote.author?.id;
if (authorId) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.push(`/author/${authorId}`);
}
}, [router]);
// Get favorites count
const favoriteQuotes = getFavorites();
// Tab handling
const tabs = [
{ key: 'recommended', label: t('common.recommended') },
{ key: 'favorites', label: t('navigation.favorites'), count: favoriteQuotes?.length || 0 }
];
const handleTabChange = (tabKey: string) => {
setActiveFilter(tabKey as 'recommended' | 'favorites');
// Scroll to top when switching tabs
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
};
// Filter handlers
const handleRemoveFilter = (category: keyof QuoteFilters, value: string) => {
setFilters(prev => ({
...prev,
[category]: prev[category].filter(v => v !== value)
}));
};
const handleClearAllFilters = () => {
setFilters({
timePeriods: [],
sourceTypes: [],
categories: [],
authorEras: [],
special: []
});
};
const hasActiveFilters = checkHasActiveFilters(filters);
// Use filtered quotes when searching, when favorites filter is active, or when filters are applied
const quotesToShow = (searchQuery && searchQuery.trim()) || activeFilter === 'favorites' || hasActiveFilters ? filteredQuotes : displayedQuotes;
const allQuotes = quotesToShow;
// Optimierung für FlatList mit getItemLayout
const getItemLayout = useCallback((data: any, index: number) => {
return {
length: CARD_HEIGHT,
offset: CARD_HEIGHT * index,
index
};
}, [CARD_HEIGHT]);
const renderQuote = ({ item, index }: { item: EnhancedQuote; index: number }) => {
// Use list view if viewMode is 'list'
if (viewMode === 'list') {
return (
<View className="mb-5">
<QuoteCard
quote={item}
onToggleFavorite={toggleFavorite}
onAuthorPress={() => handleAuthorPress(item)}
/>
</View>
);
}
// Otherwise use vertical card view
return (
<View style={{ position: 'relative', width: '100%' }}>
<QuoteCard
quote={item}
variant="vertical"
index={index}
scrollY={scrollY}
cardHeight={CARD_HEIGHT}
onToggleFavorite={toggleFavorite}
onAuthorPress={() => handleAuthorPress(item)}
/>
</View>
);
};
// Show loading state while store is initializing
if (!isInitialized || isLoading) {
return (
<View style={{ flex: 1, backgroundColor: colors.background, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" color={colors.primary} />
<Text className={`${isDarkMode ? 'text-white' : 'text-black'} mt-4`}>{t('quotes.loadingQuotes')}</Text>
</View>
);
}
// Show loading state while store is initializing
if (!isInitialized || isLoading) {
return (
<View style={{ flex: 1, backgroundColor: colors.background, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" color={isDarkMode ? '#ffffff' : '#000000'} />
<Text className={`${isDarkMode ? 'text-gray-400' : 'text-gray-600'} mt-4`}>{t('common.loading')}</Text>
</View>
);
}
// Check if quotes are loaded after initialization
if (!quotes || !Array.isArray(quotes) || quotes.length === 0) {
return (
<View style={{ flex: 1, backgroundColor: colors.background, justifyContent: 'center', alignItems: 'center' }}>
<Text className={isDarkMode ? 'text-gray-400' : 'text-gray-600'}>{t('quotes.noQuotes')}</Text>
</View>
);
}
return (
<>
<Stack.Screen
options={{
title: activeFilter === 'favorites' ? t('navigation.favorites') : t('navigation.quotes'),
headerShown: true,
headerTransparent: true,
headerBlurEffect: isDarkMode ? 'dark' : 'light',
headerStyle: {
backgroundColor: 'transparent',
},
headerTintColor: isDarkMode ? '#ffffff' : '#000000',
headerShadowVisible: false,
headerTitleAlign: 'center',
headerRight: () => (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<TouchableOpacity
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
bottomSheetRef.current?.snapToIndex(0);
}}
style={{
justifyContent: 'center',
alignItems: 'center',
width: 44,
height: 44,
marginTop: -4,
}}
>
<Icon
name="filter-outline"
size={24}
color={hasActiveFilters ? '#7c3aed' : (isDarkMode ? '#ffffff' : '#000000')}
/>
{hasActiveFilters && (
<View style={{
position: 'absolute',
top: 8,
right: 8,
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: '#7c3aed'
}} />
)}
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setViewMode(viewMode === 'card' ? 'list' : 'card');
}}
style={{
justifyContent: 'center',
alignItems: 'center',
width: 44,
height: 44,
marginTop: -4,
}}
>
<Icon
name={viewMode === 'card' ? 'list-outline' : 'grid-outline'}
size={24}
color={isDarkMode ? '#ffffff' : '#000000'}
/>
</TouchableOpacity>
</View>
),
headerRightContainerStyle: {
paddingRight: 16,
},
}}
/>
<View style={{ flex: 1, backgroundColor: colors.background }}>
{/* Active Filter Chips */}
{hasActiveFilters && (
<ActiveQuoteFilterChips
filters={filters}
onRemoveFilter={handleRemoveFilter}
onClearAll={handleClearAllFilters}
/>
)}
{/* Main Content */}
<AnimatedFlatList
ref={flatListRef}
data={allQuotes}
renderItem={renderQuote}
keyExtractor={(item, index) => `${item.id}-${index}`}
horizontal={false}
pagingEnabled={false}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
onEndReached={handleEndReached}
onEndReachedThreshold={0.5}
onScroll={scrollHandler}
scrollEventThrottle={16}
contentContainerStyle={{
paddingTop: hasActiveFilters ? 24 : LIST_CONTAINER_PADDING.top,
paddingBottom: viewMode === 'list' ? LIST_CONTAINER_PADDING.bottom + 80 : AVAILABLE_HEIGHT - VERTICAL_PADDING
}}
snapToInterval={viewMode === 'list' ? undefined : CARD_HEIGHT}
snapToAlignment={viewMode === 'list' ? undefined : "start"}
decelerationRate={viewMode === 'list' ? "normal" : "fast"}
getItemLayout={viewMode === 'list' ? undefined : getItemLayout}
removeClippedSubviews={true}
initialNumToRender={3}
maxToRenderPerBatch={5}
windowSize={10}
ListFooterComponent={
allQuotes.length > 0 && activeFilter === 'recommended' ? (
<View className="py-8">
<ActivityIndicator size="small" color={isDarkMode ? '#ffffff' : '#000000'} />
<Text className={`${isDarkMode ? 'text-white/30' : 'text-black/30'} text-xs mt-2`}>
{t('quotes.loadMore')}
</Text>
</View>
) : null
}
/>
{/* Glass Tab Selector at bottom - positioned above tab bar */}
<View className="absolute bottom-24 left-0 right-0 z-10">
<GlassTabSelector
tabs={tabs}
activeTab={activeFilter}
onTabChange={handleTabChange}
/>
</View>
</View>
{/* Filter Sheet */}
<QuoteFilterSheet
bottomSheetRef={bottomSheetRef}
filters={filters}
onFiltersChange={setFilters}
onClearAll={handleClearAllFilters}
/>
</>
);
}

View file

@ -0,0 +1,89 @@
import { Stack, useRouter } from 'expo-router';
import { useState, useRef } from 'react';
import usePremiumStore from '~/store/premiumStore';
import { Alert } from 'react-native';
export default function SearchLayout() {
const router = useRouter();
const [showingLimitAlert, setShowingLimitAlert] = useState(false);
const lastSearchText = useRef('');
const lastCountedSearch = useRef('');
const { canSearch, useSearch, getRemainingSearches, MAX_DAILY_SEARCHES } = usePremiumStore();
// Live update search query without counting
const handleTextChange = (text: string) => {
// Always update the search query for live filtering
router.setParams({ q: text || '' });
lastSearchText.current = text;
};
// Count search only when user submits or leaves the search bar
const handleSearchSubmit = (text: string) => {
// Don't count empty searches or clearing
if (!text || text.length === 0) {
lastCountedSearch.current = '';
return;
}
// If same as last counted search, don't count again
if (text === lastCountedSearch.current) {
return;
}
// Check if user can search
if (!canSearch()) {
if (!showingLimitAlert) {
setShowingLimitAlert(true);
const remaining = getRemainingSearches();
Alert.alert(
'Such-Limit erreicht',
`Du hast deine ${MAX_DAILY_SEARCHES} täglichen Suchen aufgebraucht.\n\nMit Zitare Premium kannst du unbegrenzt suchen!`,
[
{ text: 'Später', style: 'cancel', onPress: () => setShowingLimitAlert(false) },
{
text: 'Premium werden',
onPress: () => {
setShowingLimitAlert(false);
router.push('/paywall');
}
}
]
);
}
// Reset to last valid search
router.setParams({ q: lastCountedSearch.current });
lastSearchText.current = lastCountedSearch.current;
return;
}
// Count this search
if (useSearch()) {
lastCountedSearch.current = text;
}
};
return (
<Stack>
<Stack.Screen
name="index"
options={{
headerShown: true, // Must be true for search bar to show
title: '', // Empty title for clean look
headerTransparent: true, // Make header transparent
headerSearchBarOptions: {
placement: 'automatic',
placeholder: 'Search quotes, authors...',
// Update search query live as user types (no counting)
onChangeText: (event) => {
handleTextChange(event.nativeEvent.text);
},
// Count search ONLY when user submits (presses Enter/Search button)
onSearchButtonPress: (event) => {
handleSearchSubmit(event.nativeEvent.text);
},
},
}}
/>
</Stack>
);
}

View file

@ -0,0 +1,241 @@
import { useLocalSearchParams, useRouter, Stack } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';
import { View, Text, FlatList, TouchableOpacity, Dimensions } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useEffect, useMemo, useState } from 'react';
import { Icon } from '~/components/Icon';
import * as Haptics from 'expo-haptics';
import Animated, {
useSharedValue,
useAnimatedScrollHandler,
useAnimatedStyle,
interpolate,
Extrapolate
} from 'react-native-reanimated';
import { useQuotesStore } from '~/store/quotesStore';
import { useIsDarkMode } from '~/store/settingsStore';
import QuoteCard from '~/components/QuoteCard';
import { useTranslation } from 'react-i18next';
import { LIST_CONTAINER_PADDING } from '~/constants/layout';
import { useTheme } from '~/hooks/useTheme';
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
const { height: screenHeight } = Dimensions.get('window');
export default function SearchIndex() {
const { t } = useTranslation();
const router = useRouter();
const isDarkMode = useIsDarkMode();
const { colors } = useTheme();
const params = useLocalSearchParams();
const { q: searchQuery } = params;
const [viewMode, setViewMode] = useState<'card' | 'list'>('list');
const scrollY = useSharedValue(0);
// Calculate card height for vertical mode
const TAB_BAR_HEIGHT = 80;
const STATUS_BAR_HEIGHT = 44;
const CARD_HEIGHT = screenHeight - STATUS_BAR_HEIGHT - TAB_BAR_HEIGHT - 100;
const { quotes, authors, toggleFavorite, initializeStore, isLoading } = useQuotesStore();
useEffect(() => {
initializeStore();
}, []);
// Filter quotes based on search query
const filteredQuotes = useMemo(() => {
if (!searchQuery || typeof searchQuery !== 'string') {
return quotes; // Show all quotes when no search query
}
const query = searchQuery.toLowerCase().trim();
if (!query) {
return quotes;
}
return quotes.filter(quote =>
quote.text.toLowerCase().includes(query) ||
(quote.author?.name && quote.author.name.toLowerCase().includes(query)) ||
(quote.tags && quote.tags.some(tag => tag.toLowerCase().includes(query))) ||
(quote.categories && quote.categories.some(cat => cat.toLowerCase().includes(query)))
);
}, [quotes, searchQuery]);
const renderQuote = ({ item, index }) => {
if (viewMode === 'card') {
return (
<View style={{ position: 'relative', width: '100%' }}>
<QuoteCard
quote={item}
variant="vertical"
index={index}
scrollY={scrollY}
cardHeight={CARD_HEIGHT}
onToggleFavorite={toggleFavorite}
onAuthorPress={() => {
if (item?.authorId) {
router.push(`/author/${item.authorId}`);
}
}}
/>
</View>
);
}
// List view
return (
<View className="mb-5">
<QuoteCard
quote={item}
onToggleFavorite={toggleFavorite}
onAuthorPress={() => {
if (item?.authorId) {
router.push(`/author/${item.authorId}`);
}
}}
/>
</View>
);
};
const renderEmptyState = () => {
if (searchQuery && typeof searchQuery === 'string' && searchQuery.trim()) {
// No results for search
return (
<View className="flex-1 justify-center items-center py-20 px-8">
<Ionicons
name="search-outline"
size={64}
color={isDarkMode ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.3)'}
/>
<Text className={`${isDarkMode ? 'text-white' : 'text-black'} text-lg font-semibold text-center mt-4`}>
{t('search.noResults', { defaultValue: 'No Results Found' })}
</Text>
<Text className={`${isDarkMode ? 'text-white/60' : 'text-black/60'} text-base text-center mt-2`}>
No quotes found for "{searchQuery}"
</Text>
<Text className={`${isDarkMode ? 'text-white/40' : 'text-black/40'} text-sm text-center mt-4`}>
Try searching for different keywords or authors
</Text>
</View>
);
}
// Welcome state when no search
return (
<View className="flex-1 justify-center items-center py-20 px-8">
<Ionicons
name="search"
size={80}
color={isDarkMode ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'}
/>
<Text className={`${isDarkMode ? 'text-white' : 'text-black'} text-xl font-bold text-center mt-6`}>
{t('search.title', { defaultValue: 'Search Quotes' })}
</Text>
<Text className={`${isDarkMode ? 'text-white/60' : 'text-black/60'} text-base text-center mt-2`}>
{t('search.description', { defaultValue: 'Use the search bar above to find quotes by text, author, or topic' })}
</Text>
</View>
);
};
if (isLoading) {
return (
<View style={{ flex: 1, backgroundColor: colors.background }}>
<View className="flex-1 justify-center items-center">
<Text className={`${isDarkMode ? 'text-white' : 'text-black'}`}>
{t('quotes.loadingQuotes', { defaultValue: 'Loading quotes...' })}
</Text>
</View>
</View>
);
}
return (
<>
<Stack.Screen
options={{
title: t('navigation.search'),
headerShown: true,
headerTransparent: true,
headerBlurEffect: isDarkMode ? 'dark' : 'light',
headerStyle: {
backgroundColor: 'transparent',
},
headerTintColor: isDarkMode ? '#ffffff' : '#000000',
headerShadowVisible: false,
headerTitleAlign: 'center',
headerRight: () => (
<TouchableOpacity
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setViewMode(viewMode === 'card' ? 'list' : 'card');
}}
style={{
justifyContent: 'center',
alignItems: 'center',
width: 44,
height: 44,
marginTop: -4,
}}
>
<Icon
name={viewMode === 'list' ? 'grid-outline' : 'list-outline'}
size={24}
color={isDarkMode ? '#ffffff' : '#000000'}
/>
</TouchableOpacity>
),
headerRightContainerStyle: {
paddingRight: 16,
},
}}
/>
<View style={{ flex: 1, backgroundColor: colors.background }}>
<AnimatedFlatList
data={filteredQuotes}
renderItem={renderQuote}
keyExtractor={(item) => item.id}
showsVerticalScrollIndicator={false}
contentContainerStyle={
filteredQuotes.length === 0
? { flex: 1 }
: {
paddingTop: LIST_CONTAINER_PADDING.top,
paddingBottom: viewMode === 'list' ? LIST_CONTAINER_PADDING.bottom : CARD_HEIGHT * 0.1
}
}
ListHeaderComponent={
searchQuery && filteredQuotes.length > 0 ? (
<View className="items-center mb-4">
<View className={`${isDarkMode ? 'bg-white/10 backdrop-blur-md border border-white/20' : 'bg-black/10 backdrop-blur-md border border-black/20'} px-4 py-2 rounded-full`}>
<Text className={`${isDarkMode ? 'text-white/90' : 'text-black/90'} text-sm font-medium`}>
{filteredQuotes.length} {filteredQuotes.length === 1 ? 'result' : 'results'}
</Text>
</View>
</View>
) : null
}
ListEmptyComponent={renderEmptyState}
onScroll={useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
})}
scrollEventThrottle={16}
pagingEnabled={viewMode === 'card'}
snapToInterval={viewMode === 'card' ? CARD_HEIGHT : undefined}
snapToAlignment={viewMode === 'card' ? "start" : undefined}
decelerationRate={viewMode === 'card' ? "fast" : "normal"}
getItemLayout={viewMode === 'card' ? (data, index) => ({
length: CARD_HEIGHT,
offset: CARD_HEIGHT * index,
index
}) : undefined}
/>
</View>
</>
);
}

View file

@ -0,0 +1,46 @@
import { ScrollViewStyleReset } from 'expo-router/html';
// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
{/*
This viewport disables scaling which makes the mobile website act more like a native app.
However this does reduce built-in accessibility. If you want to enable scaling, use this instead:
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
*/}
<meta
name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1.00001,viewport-fit=cover"
/>
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
);
}
const responsiveBackground = `
body {
background-color: #fff;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
}`;

View file

@ -0,0 +1,26 @@
import { Link, Stack } from 'expo-router';
import { Text, View } from 'react-native';
import { useTranslation } from 'react-i18next';
export default function NotFoundScreen() {
const { t } = useTranslation();
return (
<>
<Stack.Screen options={{ title: t('errors.oops') }} />
<View className={styles.container}>
<Text className={styles.title}>{t('errors.pageNotFound')}</Text>
<Link href="/" className={styles.link}>
<Text className={styles.linkText}>{t('errors.goHome')}</Text>
</Link>
</View>
</>
);
}
const styles = {
container: `items-center flex-1 justify-center p-5`,
title: `text-xl font-bold`,
link: `mt-4 pt-4`,
linkText: `text-base text-[#2e78b7]`,
};

View file

@ -0,0 +1,176 @@
import '../global.css';
import '../i18n/config';
import { Stack , router } from 'expo-router';
import { useEffect, useState } from 'react';
import { View, ActivityIndicator } from 'react-native';
import * as Linking from 'expo-linking';
import { ErrorBoundary } from '~/components/ErrorBoundary';
import RevenueCat from '~/services/RevenueCat';
import usePremiumStore from '~/store/premiumStore';
import { useOnboardingStore } from '~/store/onboardingStore';
import { useQuotesStore } from '~/store/quotesStore';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { DataBackupService } from '~/services/dataBackup';
export const unstable_settings = {
// Ensure that reloading on `/modal` keeps a back button present.
initialRouteName: '(tabs)',
};
export default function RootLayout() {
const [isStoreReady, setIsStoreReady] = useState(false);
const { checkPremiumStatus, checkAndResetLimits } = usePremiumStore();
const { shouldShowOnboarding } = useOnboardingStore();
const { initializeStore } = useQuotesStore();
// Wait for stores to be ready
useEffect(() => {
const initStores = async () => {
try {
// WICHTIG: Warte auf Zustand Rehydration, bevor wir initialisieren
// Dies verhindert Race Conditions auf echten Geräten
console.log('[App] Waiting for store hydration...');
// Poll für hasHydrated flag (max 5 Sekunden)
let attempts = 0;
const maxAttempts = 50; // 50 * 100ms = 5 Sekunden
while (!useQuotesStore.getState().hasHydrated && attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 100));
attempts++;
}
if (attempts >= maxAttempts) {
console.warn('[App] Hydration timeout, proceeding anyway');
} else {
console.log('[App] Store hydrated successfully');
}
// WICHTIG: Backup-Check NACH Rehydration, damit persistierte Daten verfügbar sind
console.log('[App] Checking for data backup...');
await DataBackupService.checkAndRestoreIfNeeded();
// Initialize quotes store
console.log('[App] Initializing quotes store...');
await initializeStore();
console.log('[App] Quotes store initialized');
// Give AsyncStorage time to fully settle
await new Promise(resolve => setTimeout(resolve, 50));
setIsStoreReady(true);
} catch (error) {
console.error('Error initializing stores:', error);
// Retry initialization after a delay instead of proceeding with uninitialized state
setTimeout(() => {
console.log('[App] Retrying initialization...');
initStores();
}, 1000);
}
};
initStores();
}, []);
useEffect(() => {
if (!isStoreReady) return;
// Initialize RevenueCat
const initRevenueCat = async () => {
try {
console.log('[App] Configuring RevenueCat...');
await RevenueCat.configure();
console.log('[App] RevenueCat configured successfully');
await checkPremiumStatus();
} catch (error) {
console.error('[App] Error initializing RevenueCat:', error);
}
};
initRevenueCat();
checkAndResetLimits();
// Check onboarding after a small delay to ensure navigation is ready
const timer = setTimeout(() => {
if (shouldShowOnboarding()) {
router.replace('/onboarding');
}
}, 100);
return () => clearTimeout(timer);
}, [isStoreReady, checkPremiumStatus, checkAndResetLimits, shouldShowOnboarding]);
useEffect(() => {
// Handle deep links from widgets
const handleDeepLink = (url: string) => {
try {
// Parse the URL - expecting format: zitare://widget/{quoteId}
if (url.includes('zitare://widget/')) {
const quoteHash = url.split('zitare://widget/')[1];
if (quoteHash) {
// Navigate to main tab with quote parameter
router.replace({
pathname: '/(tabs)/quotes',
params: { widgetQuoteId: quoteHash }
});
}
}
} catch (error) {
console.error('Error handling deep link:', error);
// Fallback: just open the app normally
router.replace('/');
}
};
// Listen for incoming links
const subscription = Linking.addEventListener('url', ({ url }) => {
handleDeepLink(url);
});
// Check if app was opened from a link
Linking.getInitialURL().then(url => {
if (url) {
handleDeepLink(url);
}
});
return () => subscription.remove();
}, []);
// Show loading screen while stores are initializing
if (!isStoreReady) {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }}>
<ActivityIndicator size="large" color="#fff" />
</View>
</GestureHandlerRootView>
);
}
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<ErrorBoundary
onError={(error, errorInfo) => {
// Hier könnte man Error-Logging an einen Service senden
console.error('App Error:', error, errorInfo);
}}
>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="onboarding" options={{ headerShown: false }} />
<Stack.Screen name="author/[id]" options={{ headerShown: false }} />
<Stack.Screen name="settings" options={{ headerShown: false }} />
<Stack.Screen name="list/[id]" options={{ headerShown: false }} />
<Stack.Screen name="paywall" options={{
headerShown: false,
presentation: 'modal',
animation: 'slide_from_bottom'
}} />
</Stack>
</ErrorBoundary>
</GestureHandlerRootView>
);
}

View file

@ -0,0 +1,390 @@
import React, { useEffect, useState } from 'react';
import { View, ScrollView, Pressable, ActivityIndicator } from 'react-native';
import { useLocalSearchParams, useRouter, Stack } from 'expo-router';
import { Icon } from '~/components/Icon';
import Text from '~/components/Text';
import { useQuotesStore } from '~/store/quotesStore';
import { useIsDarkMode } from '~/store/settingsStore';
import { AuthorAvatar } from '~/components/authors/AuthorAvatar';
import QuoteCard from '~/components/QuoteCard';
import { GlassTabSelector } from '~/components/common/GlassTabSelector';
import * as Haptics from 'expo-haptics';
import { useTranslation } from 'react-i18next';
import Animated, {
FadeIn,
FadeInDown,
useAnimatedStyle,
useSharedValue,
withSpring
} from 'react-native-reanimated';
import { useTheme } from '~/hooks/useTheme';
export default function AuthorDetailScreen() {
const { id } = useLocalSearchParams();
const { t } = useTranslation();
const router = useRouter();
const isDarkMode = useIsDarkMode();
const { colors } = useTheme();
const likeScale = useSharedValue(1);
const {
authors,
quotes,
initializeStore,
isLoading,
toggleFavorite,
toggleAuthorFavorite,
isAuthorFavorite
} = useQuotesStore();
const [activeTab, setActiveTab] = useState<'quotes' | 'bio'>('quotes');
useEffect(() => {
initializeStore();
}, []);
const author = authors?.find(a => a.id === id);
const authorQuotes = quotes?.filter(q => q.authorId === id) || [];
const isFavorite = author ? isAuthorFavorite(author.id) : false;
const tabs = [
{ key: 'quotes', label: t('authors.quotes'), count: authorQuotes.length },
{ key: 'bio', label: t('authors.biography') }
];
const likeAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: likeScale.value }]
}));
const handleFavoriteToggle = () => {
if (author) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
likeScale.value = withSpring(1.3, {}, () => {
likeScale.value = withSpring(1);
});
toggleAuthorFavorite(author.id);
}
};
if (isLoading) {
return (
<View style={{ flex: 1, backgroundColor: colors.background }}>
<View className="flex-1 justify-center items-center">
<ActivityIndicator size="large" color={isDarkMode ? '#ffffff' : '#000000'} />
</View>
</View>
);
}
if (!author) {
return (
<View style={{ flex: 1, backgroundColor: colors.background }}>
<View className="flex-1 justify-center items-center">
<Text variant="body" color="secondary">{t('authors.notFound')}</Text>
<Pressable
onPress={() => router.back()}
className="mt-4 px-4 py-2 bg-blue-500 rounded-full"
>
<Text variant="body" color="primary">{t('common.back')}</Text>
</Pressable>
</View>
</View>
);
}
const getLifeYears = () => {
if (!author.lifespan) return null;
const birth = author.lifespan.birth?.substring(0, 4);
const death = author.lifespan.death?.substring(0, 4);
if (birth && death) {
const birthYear = parseInt(birth);
const deathYear = parseInt(death);
const age = deathYear - birthYear;
return `${birth} ${death} (${age} ${t('authors.years')})`;
}
if (birth) {
const birthYear = parseInt(birth);
const currentYear = new Date().getFullYear();
const age = currentYear - birthYear;
return `${t('authors.born')} ${birth} (${age} ${t('authors.yearsOld')})`;
}
return null;
};
return (
<>
<Stack.Screen
options={{
title: author.name,
headerShown: true,
headerTransparent: true,
headerBlurEffect: isDarkMode ? 'dark' : 'light',
headerStyle: {
backgroundColor: 'transparent',
},
headerTintColor: isDarkMode ? '#ffffff' : '#000000',
headerShadowVisible: false,
headerBackTitle: 'Autoren',
}}
/>
<View style={{ flex: 1, backgroundColor: colors.background }}>
<ScrollView
className="flex-1"
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingTop: 100, paddingBottom: 100 }}
>
{/* Author Info - with max width constraint */}
<Animated.View entering={FadeIn.duration(600)} className="pb-4 px-6">
<Animated.View entering={FadeInDown.delay(100).duration(600)} className="items-center">
<AuthorAvatar
name={author.name}
imageUrl={author.imageUrl}
size="large"
/>
<Text variant="title" color="primary" weight="bold" className="mt-4 text-center" numberOfLines={2}>
{author.name}
</Text>
{getLifeYears() && (
<Text variant="body" color="secondary" className="mt-1">
{getLifeYears()}
</Text>
)}
{/* Professions - with max width and proper wrapping */}
{author.profession && author.profession.length > 0 && (
<View className="flex-row flex-wrap justify-center mt-2.5 px-4" style={{ maxWidth: 300 }}>
{author.profession.slice(0, 2).map((prof, idx) => (
<View
key={idx}
className={`px-2.5 py-1 rounded-full mr-1.5 mb-1.5 ${
isDarkMode ? 'bg-white/10' : 'bg-black/10'
}`}
style={{ maxWidth: 100 }}
>
<Text
variant="caption"
color="secondary"
numberOfLines={1}
style={{ fontSize: 10 }}
>
{prof.length > 12 ? prof.substring(0, 12) + '...' : prof}
</Text>
</View>
))}
{author.profession.length > 2 && (
<View
className={`px-2.5 py-1 rounded-full mb-1.5 ${
isDarkMode ? 'bg-white/10' : 'bg-black/10'
}`}
>
<Text variant="caption" color="secondary" style={{ fontSize: 10 }}>
+{author.profession.length - 2}
</Text>
</View>
)}
</View>
)}
{/* Favorite Button - compact and responsive */}
<View className="mt-3 mb-2 items-center">
<Pressable
onPress={handleFavoriteToggle}
className={`${isDarkMode ? 'bg-white/10' : 'bg-black/10'} px-3.5 py-2 rounded-full`}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Animated.View style={likeAnimatedStyle}>
<Icon
name={isFavorite ? 'heart' : 'heart-outline'}
size={22}
color={isFavorite ? '#ef4444' : (isDarkMode ? 'rgba(255,255,255,0.8)' : 'rgba(0,0,0,0.6)')}
/>
</Animated.View>
</Pressable>
</View>
{/* Stats - with proper spacing */}
<View className="flex-row mt-3">
<View className="items-center px-3">
<Text variant="bodyLarge" color="primary" weight="bold">
{authorQuotes.length}
</Text>
<Text variant="caption" color="secondary" style={{ fontSize: 11 }}>
{t('navigation.quotes')}
</Text>
</View>
{author.nationality && (
<View className={`items-center px-3 border-l ${isDarkMode ? 'border-white/20' : 'border-black/20'}`}>
<Text variant="body" color="primary" numberOfLines={1} style={{ fontSize: 14 }}>
{Array.isArray(author.nationality)
? author.nationality[0]
: (typeof author.nationality === 'string' && author.nationality.length > 12
? author.nationality.substring(0, 12) + '...'
: author.nationality)}
</Text>
<Text variant="caption" color="secondary" style={{ fontSize: 11 }}>
Nationalität
</Text>
</View>
)}
</View>
</Animated.View>
</Animated.View>
{/* Tabs */}
<View className="mb-2">
<GlassTabSelector
tabs={tabs}
activeTab={activeTab}
onTabChange={(tab) => setActiveTab(tab as 'quotes' | 'bio')}
animationDelay={200}
/>
</View>
{/* Content */}
<Animated.View entering={FadeInDown.delay(300).duration(600)} className="pt-4">
{activeTab === 'quotes' ? (
<View>
{authorQuotes.length > 0 ? (
authorQuotes.map((quote, index) => (
<View key={quote.id} className="mb-5">
<QuoteCard
quote={quote}
onToggleFavorite={toggleFavorite}
onAuthorPress={() => {}}
showDate={true}
/>
</View>
))
) : (
<View className="py-12">
<Text variant="body" color="secondary" className="text-center">
{t('authors.noQuotes')}
</Text>
</View>
)}
</View>
) : (
<View className="px-6">
{/* Biography */}
{(author.biography?.long || author.biography?.short || author.biography?.sections) ? (
<View>
{/* Main Biography Text */}
{(author.biography.long || author.biography.short) && (
<View className={`p-3 rounded-2xl mb-6 ${isDarkMode ? 'bg-white/5' : 'bg-black/5'}`}>
<Text variant="body" color="primary" className="leading-relaxed">
{author.biography.long || author.biography.short}
</Text>
</View>
)}
{/* Biography Sections */}
{author.biography.sections && (
<View className="mb-6">
{Object.entries(author.biography.sections).map(([key, section]: [string, any]) => (
<View key={key} className="mb-6">
<Text variant="bodyLarge" color="primary" weight="semibold" className="mb-3">
{section.title}
</Text>
<View className={`p-3 rounded-2xl ${isDarkMode ? 'bg-white/5' : 'bg-black/5'}`}>
<Text variant="body" color="primary" className="leading-relaxed">
{section.content}
</Text>
</View>
</View>
))}
</View>
)}
{/* Key Achievements */}
{author.biography.keyAchievements && author.biography.keyAchievements.length > 0 && (
<View className="mb-6">
<Text variant="bodyLarge" color="primary" weight="semibold" className="mb-3">
{t('authors.keyAchievements', { defaultValue: 'Wichtige Errungenschaften' })}
</Text>
<View className={`p-3 rounded-2xl ${isDarkMode ? 'bg-white/5' : 'bg-black/5'}`}>
{author.biography.keyAchievements.map((achievement: string, idx: number) => (
<View key={idx} className="flex-row mb-2">
<Text variant="body" color="secondary" className="mr-2"></Text>
<Text variant="body" color="primary" className="flex-1">
{achievement}
</Text>
</View>
))}
</View>
</View>
)}
{/* Famous Quote */}
{author.biography.famousQuote && (
<View className={`p-3 rounded-2xl mb-6 ${isDarkMode ? 'bg-white/5' : 'bg-black/5'}`}>
<View className="flex-row mb-2">
<Icon name="quote" size={20} color={isDarkMode ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)'} />
</View>
<Text variant="bodyLarge" color="primary" className="leading-relaxed italic">
"{author.biography.famousQuote}"
</Text>
</View>
)}
</View>
) : (
<View className="py-12">
<Text variant="body" color="secondary" className="text-center">
{t('authors.noBiography')}
</Text>
</View>
)}
{/* Links Section */}
{author.links && (author.links.wikipedia || author.links.website) && (
<View>
<Text variant="bodyLarge" color="primary" weight="semibold" className="mb-3">
{t('authors.moreInfo')}
</Text>
{author.links.wikipedia && (
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
// Open Wikipedia link
}}
className={`flex-row items-center py-3 px-4 rounded-xl mb-2 ${
isDarkMode ? 'bg-white/10' : 'bg-black/10'
}`}
>
<Icon name="globe-outline" size={20} color={isDarkMode ? 'white' : 'black'} />
<Text variant="body" color="primary" className="ml-3">
{t('authors.wikipedia')}
</Text>
</Pressable>
)}
{author.links?.website && (
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
// Open website
}}
className={`flex-row items-center py-3 px-4 rounded-xl ${
isDarkMode ? 'bg-white/10' : 'bg-black/10'
}`}
>
<Icon name="link-outline" size={20} color={isDarkMode ? 'white' : 'black'} />
<Text variant="body" color="primary" className="ml-3">
{t('authors.website')}
</Text>
</Pressable>
)}
</View>
)}
</View>
)}
</Animated.View>
</ScrollView>
</View>
</>
);
}

View file

@ -0,0 +1,363 @@
import { Stack, useRouter, useLocalSearchParams } from 'expo-router';
import {
View,
Text,
FlatList,
Dimensions,
Pressable,
Modal,
Alert,
ScrollView,
TouchableOpacity
} from 'react-native';
import { useQuotesStore, EnhancedQuote } from '~/store/quotesStore';
import { useListStore, List } from '~/store/listStore';
import { useEffect, useState } from 'react';
import { Icon } from '~/components/Icon';
import * as Haptics from 'expo-haptics';
import { Host, Picker } from '@expo/ui/swift-ui';
import { useIsDarkMode } from '~/store/settingsStore';
import QuoteCard from '~/components/QuoteCard';
import Animated, {
FadeInRight,
FadeInDown,
FadeOutUp,
useAnimatedScrollHandler,
useSharedValue
} from 'react-native-reanimated';
import { useTranslation } from 'react-i18next';
import DraggableFlatList, {
ScaleDecorator,
RenderItemParams
} from 'react-native-draggable-flatlist';
import { LIST_ITEM_CLASSES, LIST_CONTAINER_PADDING } from '~/constants/layout';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
const { width: screenWidth } = Dimensions.get('window');
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
export default function ListDetail() {
const router = useRouter();
const { id } = useLocalSearchParams();
const { t } = useTranslation();
const isDarkMode = useIsDarkMode();
const [isEditing, setIsEditing] = useState(false);
const [viewMode, setViewMode] = useState<'card' | 'list'>('list');
const scrollY = useSharedValue(0);
const {
quotes,
toggleFavorite,
initializeStore
} = useQuotesStore();
const {
getList,
getListQuotes,
removeQuoteFromList,
reorderListItems,
updateList,
sortList
} = useListStore();
const [list, setList] = useState<List | undefined>(undefined);
const [listQuotes, setListQuotes] = useState<EnhancedQuote[]>([]);
useEffect(() => {
initializeStore();
}, []);
useEffect(() => {
if (id && quotes.length > 0) {
const pl = getList(id as string);
setList(pl);
if (pl) {
const pQuotes = getListQuotes(pl.id, quotes);
setListQuotes(pQuotes);
}
}
}, [id, quotes, getList, getListQuotes]);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
});
const handleRemoveQuote = (quoteId: string) => {
Alert.alert(
t('lists.removeQuote'),
t('lists.removeQuoteConfirm'),
[
{ text: t('common.cancel'), style: 'cancel' },
{
text: t('common.delete'),
style: 'destructive',
onPress: () => {
if (list) {
removeQuoteFromList(list.id, quoteId);
const updatedQuotes = listQuotes.filter(q => q.id !== quoteId);
setListQuotes(updatedQuotes);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
}
}
}
]
);
};
const handleSortChange = (sortMode: List['sortMode']) => {
if (!list) return;
sortList(list.id, sortMode);
updateList(list.id, { sortMode });
// Reload quotes with new sort
const pl = getList(list.id);
if (pl) {
const pQuotes = getListQuotes(pl.id, quotes);
setListQuotes(pQuotes);
setList(pl);
}
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
};
const handleDragEnd = ({ data }) => {
if (!list) return;
// Update positions based on new order
data.forEach((quote, index) => {
const itemIndex = list.items.findIndex(item => item.quoteId === quote.id);
if (itemIndex !== -1) {
reorderListItems(list.id, itemIndex, index);
}
});
setListQuotes(data);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
};
const renderQuote = ({ item, index }) => {
if (viewMode === 'card') {
const CARD_HEIGHT = Dimensions.get('window').height - 250;
return (
<View style={{ position: 'relative', width: '100%' }}>
<QuoteCard
quote={item}
variant="vertical"
index={index}
scrollY={scrollY}
cardHeight={CARD_HEIGHT}
onToggleFavorite={toggleFavorite}
onAuthorPress={() => {
if (item?.authorId) {
router.push(`/author/${item.authorId}`);
} else if (item?.author?.id) {
router.push(`/author/${item.author.id}`);
}
}}
onDelete={isEditing ? () => handleRemoveQuote(item.id) : undefined}
/>
</View>
);
}
return (
<View className="mb-5">
<QuoteCard
quote={item}
onToggleFavorite={toggleFavorite}
onAuthorPress={() => {
if (item?.authorId) {
router.push(`/author/${item.authorId}`);
} else if (item?.author?.id) {
router.push(`/author/${item.author.id}`);
}
}}
onDelete={isEditing ? () => handleRemoveQuote(item.id) : undefined}
/>
</View>
);
};
const renderDraggableQuote = ({ item, drag, isActive }: RenderItemParams<EnhancedQuote>) => {
return (
<ScaleDecorator>
<Pressable
onLongPress={drag}
disabled={!isEditing}
className={`${isDarkMode ? 'bg-white/10' : 'bg-black/10'} rounded-2xl p-4 ${LIST_ITEM_CLASSES.wrapper} ${
isActive ? 'opacity-80' : ''
}`}
>
<View className="flex-row items-center">
{isEditing && (
<Icon
name="reorder-three"
size={24}
color={isDarkMode ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.3)'}
/>
)}
<View className="flex-1 ml-3">
<Text className={`${isDarkMode ? 'text-white' : 'text-black'} text-base`} numberOfLines={2}>
{item.text}
</Text>
<Text className={`${isDarkMode ? 'text-white/60' : 'text-black/60'} text-sm mt-1`}>
{item.author?.name}
</Text>
</View>
{isEditing && (
<Pressable
onPress={() => handleRemoveQuote(item.id)}
className="ml-3"
>
<Icon
name="close-circle"
size={24}
color={isDarkMode ? 'rgba(255,0,0,0.6)' : 'rgba(255,0,0,0.6)'}
/>
</Pressable>
)}
</View>
</Pressable>
</ScaleDecorator>
);
};
if (!list) {
return (
<View className={`flex-1 ${isDarkMode ? 'bg-black' : 'bg-white'} justify-center items-center`}>
<Text className={isDarkMode ? 'text-white' : 'text-black'}>Liste nicht gefunden</Text>
</View>
);
}
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Stack.Screen
options={{
title: list.name,
headerShown: true,
headerTransparent: true,
headerBlurEffect: isDarkMode ? 'dark' : 'light',
headerStyle: {
backgroundColor: 'transparent',
},
headerTintColor: isDarkMode ? '#ffffff' : '#000000',
headerShadowVisible: false,
headerBackTitle: t('lists.lists'),
headerRight: () => (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<TouchableOpacity
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setViewMode(viewMode === 'card' ? 'list' : 'card');
}}
style={{
justifyContent: 'center',
alignItems: 'center',
width: 44,
height: 44,
marginTop: -4,
}}
>
<Icon
name={viewMode === 'list' ? 'grid-outline' : 'list-outline'}
size={24}
color={isDarkMode ? '#ffffff' : '#000000'}
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setIsEditing(!isEditing)}
style={{
justifyContent: 'center',
alignItems: 'center',
width: 44,
height: 44,
marginTop: -4,
}}
>
<Icon
name={isEditing ? "checkmark-circle" : "create-outline"}
size={24}
color={isDarkMode ? '#ffffff' : '#000000'}
/>
</TouchableOpacity>
</View>
),
headerRightContainerStyle: {
paddingRight: 16,
},
}}
/>
<View className={`flex-1 ${isDarkMode ? 'bg-black' : 'bg-white'}`}>
{/* Content */}
{listQuotes.length === 0 ? (
<View className="flex-1 justify-center items-center px-6" style={{ paddingTop: 100 }}>
<Icon
name="book-outline"
size={64}
color={isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)"}
/>
<Text className={`${isDarkMode ? 'text-white/60' : 'text-black/60'} text-lg mt-4 text-center font-semibold`}>
{t('lists.noQuotesInList')}
</Text>
<Text className={`${isDarkMode ? 'text-white/40' : 'text-black/40'} text-sm mt-2 text-center`}>
{t('lists.emptyListHint')}
</Text>
</View>
) : isEditing && list.sortMode === 'manual' ? (
<DraggableFlatList
data={listQuotes}
onDragEnd={handleDragEnd}
keyExtractor={(item) => item.id}
renderItem={renderDraggableQuote}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: LIST_CONTAINER_PADDING.bottom, paddingTop: LIST_CONTAINER_PADDING.top }}
/>
) : (
<AnimatedFlatList
data={listQuotes}
renderItem={renderQuote}
keyExtractor={(item) => item.id}
showsVerticalScrollIndicator={false}
onScroll={scrollHandler}
scrollEventThrottle={16}
contentContainerStyle={{
paddingTop: LIST_CONTAINER_PADDING.top,
paddingBottom: viewMode === 'list' ? LIST_CONTAINER_PADDING.bottom : 100
}}
pagingEnabled={viewMode === 'card'}
snapToInterval={viewMode === 'card' ? Dimensions.get('window').height - 250 : undefined}
snapToAlignment={viewMode === 'card' ? "start" : undefined}
decelerationRate={viewMode === 'card' ? "fast" : "normal"}
/>
)}
{/* Segmented Control at bottom */}
{!isEditing && list.sortMode && (
<View className="absolute bottom-0 left-0 right-0 pb-28 px-4">
<Host matchContents style={{ width: '100%' }}>
<Picker
options={['Manuell', 'A-Z', 'Autor', 'Datum', 'Zufällig']}
selectedIndex={['manual', 'alphabetical', 'author', 'date', 'random'].indexOf(list.sortMode)}
onOptionSelected={({ nativeEvent: { index } }) => {
const modes: List['sortMode'][] = ['manual', 'alphabetical', 'author', 'date', 'random'];
handleSortChange(modes[index]);
}}
variant="segmented"
/>
</Host>
</View>
)}
</View>
</GestureHandlerRootView>
);
}

View file

@ -0,0 +1,6 @@
import React from 'react';
import AppleStyleOnboarding from '~/components/onboarding/AppleStyleOnboarding';
export default function OnboardingScreen() {
return <AppleStyleOnboarding />;
}

View file

@ -0,0 +1,326 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
ScrollView,
TouchableOpacity,
ActivityIndicator,
Alert,
Platform,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import RevenueCat from '~/services/RevenueCat';
import usePremiumStore from '~/store/premiumStore';
// Remove direct import since we handle it conditionally in RevenueCat service
export default function PaywallScreen() {
const [packages, setPackages] = useState<any[]>([]);
const [selectedPackage, setSelectedPackage] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const [isPurchasing, setIsPurchasing] = useState(false);
const { setPremium } = usePremiumStore();
useEffect(() => {
loadOfferings();
}, []);
const loadOfferings = async () => {
setIsLoading(true);
console.log('[Paywall] Loading offerings...');
try {
const offerings = await RevenueCat.getOfferings();
console.log('[Paywall] Offerings received:', offerings);
if (offerings && offerings.availablePackages) {
console.log('[Paywall] Available packages:', offerings.availablePackages.length);
offerings.availablePackages.forEach((pkg: any) => {
console.log(`[Paywall] Package: ${pkg.identifier}, Price: ${pkg.product?.priceString}`);
});
setPackages(offerings.availablePackages);
// Standardmäßig Yearly auswählen (meist in der Mitte)
const yearlyPackage = offerings.availablePackages.find((p: any) =>
p.identifier.includes('yearly') || p.packageType === 'ANNUAL'
);
setSelectedPackage(yearlyPackage || offerings.availablePackages[0]);
console.log('[Paywall] Selected package:', yearlyPackage?.identifier || offerings.availablePackages[0]?.identifier);
} else {
console.log('[Paywall] No packages found in offerings');
Alert.alert('Fehler', 'Keine Abos verfügbar. Bitte versuche es später erneut.');
}
} catch (error) {
console.error('[Paywall] Error loading offerings:', error);
Alert.alert('Fehler', 'Konnte Abos nicht laden. Bitte versuche es später erneut.');
} finally {
setIsLoading(false);
}
};
const handlePurchase = async () => {
if (!selectedPackage) {
console.log('[Paywall] No package selected');
return;
}
console.log('[Paywall] Starting purchase for:', selectedPackage.identifier);
setIsPurchasing(true);
try {
const success = await RevenueCat.purchasePackage(selectedPackage);
console.log('[Paywall] Purchase result:', success);
if (success) {
setPremium(true);
Alert.alert(
'Erfolgreich!',
'Willkommen bei Zitare Premium! 🎉',
[{ text: 'OK', onPress: () => router.back() }]
);
} else {
console.log('[Paywall] Purchase failed or was cancelled');
Alert.alert('Abgebrochen', 'Der Kauf wurde nicht abgeschlossen.');
}
} catch (error: any) {
console.error('[Paywall] Purchase error:', error);
if (!error.userCancelled) {
Alert.alert(
'Fehler',
`Kauf konnte nicht abgeschlossen werden.\n${error.message || 'Unbekannter Fehler'}`
);
}
} finally {
setIsPurchasing(false);
}
};
const handleRestore = async () => {
setIsPurchasing(true);
try {
const success = await RevenueCat.restorePurchases();
if (success) {
setPremium(true);
Alert.alert(
'Wiederhergestellt!',
'Deine Premium-Mitgliedschaft wurde wiederhergestellt.',
[{ text: 'OK', onPress: () => router.back() }]
);
} else {
Alert.alert('Keine Käufe gefunden', 'Es wurden keine früheren Käufe gefunden.');
}
} catch (error) {
Alert.alert('Fehler', 'Wiederherstellung fehlgeschlagen.');
} finally {
setIsPurchasing(false);
}
};
const getPackageDetails = (pkg: any) => {
const identifier = pkg.identifier.toLowerCase();
const product = pkg.product;
if (identifier.includes('lifetime')) {
return {
title: 'Lifetime',
subtitle: 'Einmalig, für immer',
price: product.priceString,
badge: 'BEST VALUE',
gradient: ['#FFD700', '#FFA500'],
};
} else if (identifier.includes('yearly') || pkg.packageType === 'ANNUAL') {
const monthlyPrice = (product.price / 12).toFixed(2);
return {
title: 'Jährlich',
subtitle: `Nur ${monthlyPrice}€/Monat`,
price: product.priceString + '/Jahr',
badge: 'SPARE 33%',
gradient: ['#8B5CF6', '#6366F1'],
};
} else {
return {
title: 'Monatlich',
subtitle: 'Flexibel kündbar',
price: product.priceString + '/Monat',
badge: null,
gradient: ['#10B981', '#059669'],
};
}
};
if (isLoading) {
return (
<SafeAreaView className="flex-1 bg-gray-50">
<View className="flex-1 items-center justify-center">
<ActivityIndicator size="large" color="#6366F1" />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView className="flex-1 bg-white">
<ScrollView className="flex-1" showsVerticalScrollIndicator={false}>
{/* Header */}
<View className="px-6 pt-4 pb-2">
<TouchableOpacity
onPress={() => router.back()}
className="self-start p-2 -ml-2"
>
<Ionicons name="close" size={28} color="#000" />
</TouchableOpacity>
</View>
{/* Title */}
<View className="px-6 pb-6">
<Text className="text-3xl font-bold text-center mb-2">
Zitare Premium
</Text>
<Text className="text-lg text-gray-600 text-center">
Unbegrenzte Inspiration wartet
</Text>
</View>
{/* Features */}
<View className="px-6 pb-4">
<FeatureRow icon="heart" text="Unlimited Favoriten" subtext="statt 5/Tag" />
<FeatureRow icon="folder" text="Unlimited Sammlungen" subtext="statt 1/Woche" />
<FeatureRow icon="color-palette" text="50+ Premium Themes" />
<FeatureRow icon="flash" text="Keine Wartezeiten" />
<FeatureRow icon="share-social" text="Premium Sharing" />
<FeatureRow icon="stats-chart" text="Persönliche Statistiken" />
</View>
{/* Price Options */}
<View className="px-6 pb-4">
{packages.map((pkg, index) => {
const details = getPackageDetails(pkg);
const isSelected = selectedPackage?.identifier === pkg.identifier;
return (
<TouchableOpacity
key={pkg.identifier}
onPress={() => setSelectedPackage(pkg)}
className={`mb-2.5 rounded-2xl overflow-hidden ${
isSelected ? 'border-2 border-indigo-500' : 'border border-gray-200'
}`}
>
<LinearGradient
colors={isSelected ? details.gradient : ['#FFFFFF', '#FFFFFF']}
className="p-4"
>
<View className="flex-row items-center justify-between">
<View className="flex-1">
<View className="flex-row items-center">
<Text className={`text-lg font-semibold ${
isSelected ? 'text-white' : 'text-gray-900'
}`}>
{details.title}
</Text>
{details.badge && (
<View className={`ml-2 px-2 py-1 rounded-full ${
isSelected ? 'bg-white/20' : 'bg-indigo-100'
}`}>
<Text className={`text-xs font-bold ${
isSelected ? 'text-white' : 'text-indigo-600'
}`}>
{details.badge}
</Text>
</View>
)}
</View>
<Text className={`text-sm mt-1 ${
isSelected ? 'text-white/80' : 'text-gray-500'
}`}>
{details.subtitle}
</Text>
</View>
<View className="items-end">
<Text className={`text-xl font-bold ${
isSelected ? 'text-white' : 'text-gray-900'
}`}>
{details.price}
</Text>
</View>
<View className="ml-3">
<View className={`w-6 h-6 rounded-full border-2 ${
isSelected
? 'border-white bg-white'
: 'border-gray-300'
}`}>
{isSelected && (
<View className="flex-1 m-1 rounded-full bg-indigo-500" />
)}
</View>
</View>
</View>
</LinearGradient>
</TouchableOpacity>
);
})}
</View>
{/* Purchase Button */}
<View className="px-6 pb-4">
<TouchableOpacity
onPress={handlePurchase}
disabled={isPurchasing || !selectedPackage}
className="bg-indigo-600 rounded-2xl py-4 shadow-lg"
style={{ opacity: isPurchasing ? 0.5 : 1 }}
>
{isPurchasing ? (
<ActivityIndicator color="white" />
) : (
<Text className="text-white text-center text-lg font-semibold">
Weiter mit Premium
</Text>
)}
</TouchableOpacity>
</View>
{/* Restore & Terms */}
<View className="px-6 pb-6">
<TouchableOpacity
onPress={handleRestore}
disabled={isPurchasing}
className="py-2"
>
<Text className="text-center text-indigo-600 font-medium">
Käufe wiederherstellen
</Text>
</TouchableOpacity>
<Text className="text-center text-xs text-gray-400 mt-4">
Mit dem Kauf stimmst du unseren AGB und Datenschutzbestimmungen zu.
{'\n'}Abos verlängern sich automatisch, können aber jederzeit gekündigt werden.
</Text>
</View>
</ScrollView>
</SafeAreaView>
);
}
function FeatureRow({
icon,
text,
subtext
}: {
icon: string;
text: string;
subtext?: string;
}) {
return (
<View className="flex-row items-center py-2">
<View className="w-9 h-9 rounded-full bg-indigo-100 items-center justify-center mr-3">
<Ionicons name={icon as any} size={18} color="#6366F1" />
</View>
<View className="flex-1">
<Text className="text-sm font-medium text-gray-900">{text}</Text>
{subtext && (
<Text className="text-xs text-gray-500">{subtext}</Text>
)}
</View>
<Ionicons name="checkmark-circle" size={22} color="#10B981" />
</View>
);
}

View file

@ -0,0 +1,547 @@
import { Stack, useRouter } from 'expo-router';
import { View, ScrollView, Switch, Pressable, TextInput, TouchableOpacity, Platform, Alert } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import * as Haptics from 'expo-haptics';
import { useState, useEffect } from 'react';
import { useSettingsStore, useIsDarkMode, ThemeType } from '~/store/settingsStore';
import { useQuotesStore } from '~/store/quotesStore';
import { useTranslation } from 'react-i18next';
import Text from '~/components/Text';
import { LinearGradient } from 'expo-linear-gradient';
import { themeDisplayNames, themeDescriptions } from '~/themes/definitions';
import usePremiumStore from '~/store/premiumStore';
import RevenueCat from '~/services/RevenueCat';
import { CloudSyncButton } from '~/components/CloudSyncButton';
import { useOnboardingStore } from '~/store/onboardingStore';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useTheme } from '~/hooks/useTheme';
export default function Settings() {
const { t } = useTranslation();
const router = useRouter();
const isDarkMode = useIsDarkMode();
const { colors } = useTheme();
const changeQuoteLanguage = useQuotesStore(state => state.changeLanguage);
const {
themeMode,
setThemeMode,
themeType,
setThemeType,
enableHaptics,
setHaptics,
dailyQuoteNotification,
setDailyNotification,
language,
setLanguage,
userName,
setUserName
} = useSettingsStore();
const {
isPremium,
premiumType,
checkPremiumStatus,
getRemainingFavorites,
getRemainingSearches,
getRemainingCollections
} = usePremiumStore();
const [notifications, setNotifications] = useState(true);
const [soundEffects, setSoundEffects] = useState(true);
useEffect(() => {
checkPremiumStatus();
}, []);
const toggleSwitch = (setter: (value: boolean) => void, value: boolean) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setter(!value);
};
const handleResetAllData = async () => {
Alert.alert(
t('settings.resetAllData', { defaultValue: 'Alle Daten zurücksetzen' }),
t('settings.resetAllDataConfirm', { defaultValue: 'Möchtest du wirklich ALLE Daten löschen? Dies umfasst Favoriten, Playlists, Einstellungen und alle anderen App-Daten. Diese Aktion kann nicht rückgängig gemacht werden!' }),
[
{
text: t('common.cancel'),
style: 'cancel'
},
{
text: t('settings.reset', { defaultValue: 'Zurücksetzen' }),
style: 'destructive',
onPress: async () => {
try {
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
// Clear AsyncStorage completely
await AsyncStorage.clear();
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
// Reload the app by navigating to index
// This will cause all stores to re-initialize with empty data
if (Platform.OS === 'web') {
window.location.reload();
} else {
router.replace('/(tabs)/');
}
} catch (error) {
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
Alert.alert(
t('common.error', { defaultValue: 'Fehler' }),
t('settings.resetError', { defaultValue: 'Daten konnten nicht zurückgesetzt werden.' })
);
}
}
}
]
);
};
return (
<>
<Stack.Screen
options={{
title: t('headers.settings'),
headerShown: true,
headerTransparent: true,
headerBlurEffect: isDarkMode ? 'dark' : 'light',
headerStyle: {
backgroundColor: 'transparent',
},
headerTintColor: isDarkMode ? '#ffffff' : '#000000',
headerShadowVisible: false,
headerBackTitle: t('common.back'),
}}
/>
<View style={{ flex: 1, backgroundColor: colors.background }}>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 120, paddingTop: 116 }}
>
{/* Settings Options */}
<View className="px-6">
{/* Premium Status Section */}
<View className="mb-8">
<Text variant="label" color="secondary" className="mb-4">
{t('settings.subscription')}
</Text>
<TouchableOpacity
onPress={() => {
if (!isPremium) {
router.push('/paywall');
}
}}
activeOpacity={isPremium ? 1 : 0.7}
>
<View className={`${isDarkMode ? 'bg-white/10' : 'bg-black/10'} rounded-2xl p-4 ${isPremium ? 'border-2' : ''}`}
style={isPremium ? { borderColor: '#8B5CF6' } : {}}>
<View className="flex-row items-center justify-between mb-3">
<View className="flex-row items-center">
<View className={`w-10 h-10 rounded-full ${isPremium ? 'bg-purple-500/20' : isDarkMode ? 'bg-white/10' : 'bg-black/10'} items-center justify-center mr-3`}>
<Ionicons
name={isPremium ? "star" : "star-outline"}
size={20}
color={isPremium ? "#8B5CF6" : isDarkMode ? "#fff" : "#000"}
/>
</View>
<View>
<Text variant="bodyLarge" weight="bold" color="primary">
{isPremium ? 'Zitare Premium' : 'Zitare Free'}
</Text>
{isPremium && (
<Text variant="caption" color="secondary">
{premiumType === 'lifetime' ? 'Lifetime' :
premiumType === 'yearly' ? 'Jahresabo' : 'Monatsabo'}
</Text>
)}
</View>
</View>
{!isPremium && (
<View className={`${isDarkMode ? 'bg-white/20' : 'bg-black/20'} px-3 py-1 rounded-full`}>
<Text variant="caption" weight="semibold" color="primary">
Upgrade
</Text>
</View>
)}
</View>
{isPremium ? (
<View>
<Text variant="bodySmall" color="secondary" className="mb-2">
Du genießt alle Premium-Features ohne Limits!
</Text>
<View className="flex-row flex-wrap">
<View className="flex-row items-center mr-4 mb-1">
<Ionicons name="checkmark-circle" size={16} color="#8B5CF6" />
<Text variant="caption" color="secondary" className="ml-1">
Unlimited Favoriten
</Text>
</View>
<View className="flex-row items-center mr-4 mb-1">
<Ionicons name="checkmark-circle" size={16} color="#8B5CF6" />
<Text variant="caption" color="secondary" className="ml-1">
Unlimited Suche
</Text>
</View>
<View className="flex-row items-center mb-1">
<Ionicons name="checkmark-circle" size={16} color="#8B5CF6" />
<Text variant="caption" color="secondary" className="ml-1">
Unlimited Sammlungen
</Text>
</View>
</View>
</View>
) : (
<View>
<Text variant="bodySmall" color="secondary" className="mb-3">
Deine aktuellen Limits:
</Text>
<View className="space-y-2">
<View className="flex-row items-center justify-between mb-2">
<View className="flex-row items-center">
<Ionicons name="heart-outline" size={16} color={isDarkMode ? "#fff" : "#000"} />
<Text variant="caption" color="secondary" className="ml-2">
Favoriten heute
</Text>
</View>
<Text variant="caption" weight="semibold" color="primary">
{getRemainingFavorites() === -1 ? '∞' : `${getRemainingFavorites()} / 3`}
</Text>
</View>
<View className="flex-row items-center justify-between mb-2">
<View className="flex-row items-center">
<Ionicons name="search-outline" size={16} color={isDarkMode ? "#fff" : "#000"} />
<Text variant="caption" color="secondary" className="ml-2">
Suchen heute
</Text>
</View>
<Text variant="caption" weight="semibold" color="primary">
{getRemainingSearches() === -1 ? '∞' : `${getRemainingSearches()} / 3`}
</Text>
</View>
<View className="flex-row items-center justify-between">
<View className="flex-row items-center">
<Ionicons name="folder-outline" size={16} color={isDarkMode ? "#fff" : "#000"} />
<Text variant="caption" color="secondary" className="ml-2">
Sammlung diese Woche
</Text>
</View>
<Text variant="caption" weight="semibold" color="primary">
{getRemainingCollections() === -1 ? '∞' : `${getRemainingCollections()} / 1`}
</Text>
</View>
</View>
<View className={`mt-3 pt-3 border-t ${isDarkMode ? 'border-white/10' : 'border-black/10'}`}>
<Text variant="caption" weight="semibold" style={{ color: '#8B5CF6' }} className="text-center">
Tippe für unbegrenzte Features
</Text>
</View>
</View>
)}
</View>
</TouchableOpacity>
</View>
{/* User Section */}
<View className="mb-8">
<Text variant="label" color="secondary" className="mb-4">
{t('settings.personal')}
</Text>
<View className={`${isDarkMode ? 'bg-white/10' : 'bg-black/10'} rounded-2xl p-4`}>
<Text variant="bodyLarge" weight="semibold" color="primary" className="mb-2">
{t('settings.yourName')}
</Text>
<TextInput
value={userName}
onChangeText={setUserName}
placeholder={t('settings.yourNamePlaceholder')}
placeholderTextColor={isDarkMode ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.3)'}
className={`${isDarkMode ? 'bg-white/10 text-white' : 'bg-black/10 text-black'} rounded-xl px-4 py-2.5`}
style={{ fontSize: 16 }}
/>
<Text variant="caption" color="secondary" className="mt-2">
{t('settingsDesc.defaultAuthor')}
</Text>
</View>
</View>
{/* Theme Section */}
<View className="mb-8">
<Text variant="label" color="secondary" className="mb-4">
{t('settings.appearance')}
</Text>
{/* Dark Mode Toggle */}
<View className={`${isDarkMode ? 'bg-white/10' : 'bg-black/10'} rounded-2xl p-4 mb-4`}>
<View className="flex-row justify-between items-center">
<View className="flex-1 mr-4">
<Text variant="bodyLarge" weight="semibold" color="primary">{t('settings.darkMode')}</Text>
<Text variant="bodySmall" color="secondary" className="mt-1">{t('settings.darkModeDesc')}</Text>
</View>
<Switch
trackColor={{ false: '#3e3e3e', true: '#4c4c4c' }}
thumbColor={isDarkMode ? '#ffffff' : '#f4f3f4'}
ios_backgroundColor="#3e3e3e"
onValueChange={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setThemeMode(isDarkMode ? 'light' : 'dark');
}}
value={isDarkMode}
/>
</View>
</View>
{/* Theme Selection */}
<View className={`${isDarkMode ? 'bg-white/10' : 'bg-black/10'} rounded-2xl overflow-hidden`}>
<View className="p-4 pb-2">
<Text variant="bodyLarge" weight="semibold" color="primary">
{t('settings.colorScheme')}
</Text>
</View>
<View className="px-3 pb-3 gap-2.5">
{(['default', 'colorful', 'nature'] as ThemeType[]).map((theme) => (
<Pressable
key={theme}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setThemeType(theme);
}}
>
<View className="relative rounded-2xl overflow-hidden"
style={{
borderWidth: themeType === theme ? 2 : 1,
borderColor: themeType === theme
? (theme === 'default' ? '#64748b' : theme === 'colorful' ? '#e11d48' : '#16a34a')
: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
height: 68
}}>
<LinearGradient
colors={
theme === 'default'
? isDarkMode ? ['#1e293b', '#334155'] : ['#94a3b8', '#cbd5e1']
: theme === 'colorful'
? isDarkMode ? ['#be185d', '#e11d48'] : ['#fb7185', '#fbbf24']
: isDarkMode ? ['#16a34a', '#22c55e'] : ['#86efac', '#34d399']
}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, opacity: 0.3 }}
/>
<View className="absolute inset-0 flex-row items-center px-4">
{/* Theme Preview Circles */}
<View className="flex-row mr-2.5">
<View
className="w-6 h-6 rounded-full"
style={{
backgroundColor: theme === 'default'
? '#64748b'
: theme === 'colorful'
? '#e11d48'
: '#16a34a',
marginRight: -5
}}
/>
<View
className="w-6 h-6 rounded-full border"
style={{
backgroundColor: theme === 'default'
? '#94a3b8'
: theme === 'colorful'
? '#f59e0b'
: '#22c55e',
borderColor: isDarkMode ? '#000' : '#fff'
}}
/>
</View>
{/* Theme Name and Description */}
<View className="flex-1 mr-1.5">
<Text
variant="body"
weight={themeType === theme ? "bold" : "semibold"}
color="primary"
numberOfLines={1}
>
{themeDisplayNames[theme]}
</Text>
<Text
variant="caption"
color="secondary"
numberOfLines={1}
style={{ fontSize: 10 }}
>
{themeDescriptions[theme]}
</Text>
</View>
{/* Selection Indicator */}
{themeType === theme && (
<View>
<Ionicons
name="checkmark-circle"
size={22}
color={theme === 'default' ? '#64748b' : theme === 'colorful' ? '#e11d48' : '#16a34a'}
/>
</View>
)}
</View>
</View>
</Pressable>
))}
</View>
</View>
</View>
{/* Language Section */}
<View className="mb-8">
<Text variant="label" color="secondary" className="mb-4">
{t('settings.language')}
</Text>
<View className={`${isDarkMode ? 'bg-white/10' : 'bg-black/10'} rounded-2xl p-4`}>
<View className="flex-row justify-between items-center">
<View className="flex-1 mr-4">
<Text variant="bodyLarge" weight="semibold" color="primary">
{t('settings.language')}
</Text>
<Text variant="bodySmall" color="secondary" className="mt-1">
{t('settings.languageDesc')}
</Text>
</View>
<View className="flex-row">
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setLanguage('de');
changeQuoteLanguage('de');
}}
className={`${language === 'de' ? (isDarkMode ? 'bg-white/20' : 'bg-black/20') : (isDarkMode ? 'bg-white/10' : 'bg-black/10')} px-3 py-1.5 rounded-l-full`}
>
<Text className={`${isDarkMode ? 'text-white' : 'text-black'} text-sm font-medium`}>
DE
</Text>
</Pressable>
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setLanguage('en');
changeQuoteLanguage('en');
}}
className={`${language === 'en' ? (isDarkMode ? 'bg-white/20' : 'bg-black/20') : (isDarkMode ? 'bg-white/10' : 'bg-black/10')} px-3 py-1.5 rounded-r-full`}
>
<Text className={`${isDarkMode ? 'text-white' : 'text-black'} text-sm font-medium`}>
EN
</Text>
</Pressable>
</View>
</View>
</View>
</View>
{/* About Section */}
<View className="mb-8">
<Text variant="label" color="secondary" className="mb-4">
{t('settings.about')}
</Text>
<View className={`${isDarkMode ? 'bg-white/10' : 'bg-black/10'} rounded-2xl p-4`}>
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
className="flex-row justify-between items-center mb-4"
>
<Text variant="bodyLarge" color="primary">{t('settings.version')}</Text>
<Text variant="body" color="secondary">1.0.0</Text>
</Pressable>
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
className="flex-row justify-between items-center mb-4"
>
<Text variant="bodyLarge" color="primary">{t('settings.rateUs')}</Text>
<Ionicons name="star-outline" size={20} color={isDarkMode ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'} />
</Pressable>
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
className="flex-row justify-between items-center mb-4"
>
<Text variant="bodyLarge" color="primary">{t('settings.sendFeedback')}</Text>
<Ionicons name="mail-outline" size={20} color={isDarkMode ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'} />
</Pressable>
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
className="flex-row justify-between items-center"
>
<Text variant="bodyLarge" color="primary">{t('settings.privacy')}</Text>
<Ionicons name="shield-checkmark-outline" size={20} color={isDarkMode ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'} />
</Pressable>
</View>
</View>
{/* Cloud Sync Section */}
<View className="mb-8">
<Text variant="label" color="secondary" className="mb-4">
{Platform.OS === 'ios' ? 'iCloud' : 'Cloud'} Backup
</Text>
<CloudSyncButton />
</View>
{/* Danger Zone */}
<View className="mb-8">
<Text variant="label" color="secondary" className="mb-4">
{t('settings.data')}
</Text>
<View className={`${isDarkMode ? 'bg-white/10' : 'bg-black/10'} rounded-2xl`}>
<Pressable
onPress={handleResetAllData}
className="flex-row justify-between items-center p-4 border-b border-white/5"
>
<View className="flex-1">
<Text variant="bodyLarge" color="danger">
{t('settings.resetAllData', { defaultValue: 'Alle Daten zurücksetzen' })}
</Text>
<Text variant="caption" color="secondary" className="mt-1">
{t('settings.resetAllDataDesc', { defaultValue: 'Löscht Favoriten, Playlists und Einstellungen' })}
</Text>
</View>
<Ionicons name="trash-outline" size={20} color="#f87171" />
</Pressable>
{/* Reset Onboarding for Testing */}
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
const { resetOnboarding } = useOnboardingStore.getState();
resetOnboarding();
router.replace('/onboarding');
}}
className="flex-row justify-between items-center p-4"
>
<View className="flex-1">
<Text variant="bodyLarge" color="primary">Onboarding zurücksetzen</Text>
<Text variant="caption" color="secondary">Zeigt die Einführung erneut an</Text>
</View>
<Ionicons name="refresh-outline" size={20} color={isDarkMode ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'} />
</Pressable>
</View>
</View>
</View>
</ScrollView>
</View>
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

View file

@ -0,0 +1,3 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M357.154 224L467.722 359.888C446.34 369.454 427.208 380.426 410.328 392.805C394.01 405.185 381.349 420.94 372.346 440.071C363.344 458.64 358.842 482.835 358.842 512.657L174 508.437C174 453.294 183.847 406.873 203.541 369.173C223.798 330.91 247.993 299.962 276.127 276.33C304.824 252.697 331.833 235.254 357.154 224ZM460.969 508.437V760.802H174V508.437H460.969ZM739.499 224L850.066 359.888C829.247 369.454 810.397 380.426 793.516 392.805C776.636 405.185 763.694 420.94 754.691 440.071C745.688 458.64 741.187 482.835 741.187 512.657L556.345 508.437C556.345 453.294 566.192 406.873 585.886 369.173C606.142 330.91 630.338 299.962 658.472 276.33C687.169 252.697 714.178 235.254 739.499 224ZM843.314 508.437V760.802H556.345V508.437H843.314Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 871 B

View file

@ -0,0 +1,30 @@
{
"fill" : "system-dark",
"groups" : [
{
"layers" : [
{
"fill" : {
"solid" : "extended-srgb:0.00000,0.53333,1.00000,1.00000"
},
"image-name" : "Zitare App Icon 02.svg",
"name" : "Zitare App Icon 02"
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

View file

@ -0,0 +1,451 @@
import React from 'react';
import { View, Pressable, Share, Platform, Alert } from 'react-native';
import Text from '~/components/Text';
import { LinearGradient } from 'expo-linear-gradient';
import { Icon } from '~/components/Icon';
import { useRouter } from 'expo-router';
import * as Haptics from 'expo-haptics';
import * as Clipboard from 'expo-clipboard';
import type { Author } from '@quote/shared';
import { useThemeStore, useIsDarkMode } from '~/store/settingsStore';
import { useTheme } from '~/hooks/useTheme';
import { AuthorAvatar } from '~/components/authors/AuthorAvatar';
import { useQuotesStore } from '~/store/quotesStore';
import FavoriteButton from '~/components/common/FavoriteButton';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
FadeInRight,
FadeInUp,
interpolate,
Extrapolate,
SharedValue
} from 'react-native-reanimated';
import { useTranslation } from 'react-i18next';
export type AuthorCardVariant = 'simple' | 'enhanced' | 'vertical';
interface AuthorCardProps {
author: Author;
onPress?: (author: Author) => void;
index?: number;
variant?: AuthorCardVariant;
isFavorite?: boolean;
onToggleFavorite?: (authorId: string) => void;
// For vertical variant
scrollY?: SharedValue<number>;
cardHeight?: number;
}
function AuthorCard({
author,
onPress,
index = 0,
variant = 'simple',
isFavorite,
onToggleFavorite,
scrollY,
cardHeight = 300
}: AuthorCardProps) {
const router = useRouter();
const { t } = useTranslation();
const { enableHaptics } = useThemeStore();
const isDarkMode = useIsDarkMode();
const { getCategoryGradient } = useTheme();
const { toggleAuthorFavorite, isAuthorFavorite } = useQuotesStore();
const scale = useSharedValue(1);
// Use prop or store value for favorite status
const favoriteStatus = isFavorite !== undefined ? isFavorite : isAuthorFavorite(author.id);
const handleFavoriteToggle = onToggleFavorite || (() => toggleAuthorFavorite(author.id));
const handlePressIn = () => {
if (variant === 'enhanced') {
scale.value = withSpring(0.98);
}
};
const handlePressOut = () => {
if (variant === 'enhanced') {
scale.value = withSpring(1);
}
};
const handlePress = () => {
if (enableHaptics) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
if (onPress) {
onPress(author);
} else {
router.push(`/author/${author.id}`);
}
};
const handleFavoritePress = () => {
handleFavoriteToggle(author.id);
};
const handleShare = async () => {
try {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
const authorInfo = `${author.name}${author.lifeYears ? ` (${author.lifeYears})` : ''}\n${author.profession?.join(', ') || ''}\n\n${author.bio || ''}`;
const result = await Share.share({
message: authorInfo,
title: author.name,
});
if (result.action === Share.sharedAction) {
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
} catch (error) {
Alert.alert(t('common.shareError'), t('common.shareErrorMessage'));
}
};
const handleCopyToClipboard = async () => {
try {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
const authorInfo = `${author.name}${author.lifeYears ? ` (${author.lifeYears})` : ''}\n${author.profession?.join(', ') || ''}`;
await Clipboard.setStringAsync(authorInfo);
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
if (Platform.OS === 'ios') {
Alert.alert(t('common.copied'), '', [{ text: 'OK' }], {
userInterfaceStyle: 'dark'
});
}
} catch (error) {
Alert.alert(t('common.copyError'), t('common.copyErrorMessage'));
}
};
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }]
}));
const verticalAnimatedStyle = useAnimatedStyle(() => {
if (variant !== 'vertical' || !scrollY) {
return {};
}
const cardPosition = index * cardHeight;
const inputRange = [
cardPosition - cardHeight,
cardPosition,
cardPosition + cardHeight,
];
const opacity = interpolate(
scrollY.value,
inputRange,
[0.3, 1, 0.3],
Extrapolate.CLAMP
);
const cardScale = interpolate(
scrollY.value,
inputRange,
[0.85, 1, 0.85],
Extrapolate.CLAMP
);
return {
opacity,
transform: [{ scale: cardScale }],
};
});
const getLifeYears = () => {
if (!author.lifespan) return null;
const birth = author.lifespan.birth?.substring(0, 4);
const death = author.lifespan.death?.substring(0, 4);
if (birth && death) {
return `${birth} ${death}`;
}
if (birth) {
return variant === 'enhanced' ? `${t('authors.born')} ${birth}` : birth;
}
return null;
};
// Gradient basierend auf Featured-Status oder Profession
const getGradientColors = () => {
if (author.featured) {
return ['#f59e0b', '#ef4444']; // Amber to Red for featured
}
// Use profession to determine gradient
const profession = author.profession?.[0]?.toLowerCase() || '';
if (profession.includes('philosoph')) {
return ['#9333ea', '#6366f1']; // Purple to Indigo
} else if (profession.includes('dichter') || profession.includes('poet')) {
return ['#ec4899', '#f43f5e']; // Pink to Rose
} else if (profession.includes('wissenschaft')) {
return ['#3b82f6', '#06b6d4']; // Blue to Cyan
} else if (profession.includes('schrift')) {
return ['#10b981', '#14b8a6']; // Emerald to Teal
}
return ['#6366f1', '#8b5cf6']; // Default: Indigo to Violet
};
const renderSimpleCard = () => (
<Animated.View
entering={FadeInRight.delay(index * 50).duration(600).springify()}
className="px-6 mb-4"
>
<Pressable
onPress={handlePress}
>
<LinearGradient
colors={isDarkMode ? ['#1e293b', '#334155'] : ['#334155', '#1e293b']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={{
borderRadius: 16,
padding: 1
}}
>
<View className="bg-black/40 rounded-2xl backdrop-blur-xl">
<View className="p-4">
<View className="flex-row items-center">
<AuthorAvatar
name={author.name}
imageUrl={author.image?.thumbnail || author.image?.full}
size="medium"
/>
<View className="flex-1 ml-4">
<View className="flex-row items-center justify-between">
<View className="flex-1 mr-2">
<Text variant="bodyLarge" weight="semibold" style={{ color: 'white' }}>
{author.name}
</Text>
{author.lifespan && (
<Text variant="caption" className="mt-0.5" style={{ color: 'rgba(255,255,255,0.6)' }}>
{getLifeYears()}
</Text>
)}
{author.profession && author.profession.length > 0 && (
<Text variant="caption" className="mt-1" style={{ color: 'rgba(255,255,255,0.6)' }}>
{author.profession[0]}
</Text>
)}
</View>
{/* Action Buttons */}
<View className="flex-row items-center gap-3">
{/* Copy Button */}
<Pressable
onPress={handleCopyToClipboard}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Icon
name="copy-outline"
size={22}
color="rgba(255,255,255,0.8)"
/>
</Pressable>
{/* Share Button */}
<Pressable
onPress={handleShare}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Icon
name="share-outline"
size={22}
color="rgba(255,255,255,0.8)"
/>
</Pressable>
{/* Favorite Button */}
<FavoriteButton
isFavorite={favoriteStatus}
onToggle={handleFavoritePress}
size={24}
/>
</View>
</View>
</View>
</View>
</View>
</View>
</LinearGradient>
</Pressable>
</Animated.View>
);
const renderEnhancedCard = () => (
<Animated.View style={animatedStyle}>
<Pressable
onPress={handlePress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
>
<LinearGradient
colors={getGradientColors()}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={{
borderRadius: 24,
padding: 1.5
}}
>
<View className="bg-black/40 rounded-3xl backdrop-blur-xl">
<View className="p-5">
{/* Main Content */}
<View className="flex-row items-center">
{/* Avatar */}
<View className="mr-4">
<AuthorAvatar
name={author.name}
imageUrl={author.image?.thumbnail}
size="medium"
/>
</View>
{/* Author Info */}
<View className="flex-1">
<Text variant="h4" weight="medium" className="mb-1" style={{ color: 'white' }}>
{author.name}
</Text>
{/* Lebensjahre */}
{getLifeYears() && (
<Text variant="bodySmall" style={{ color: 'rgba(255,255,255,0.7)' }}>
{getLifeYears()}
</Text>
)}
</View>
{/* Arrow */}
<View className="ml-2">
<Icon
name="chevron-forward"
size={25}
color="rgba(255,255,255,0.5)"
/>
</View>
</View>
{/* Bio wenn vorhanden */}
{author.biography?.short && (
<View className="border-t border-white/10 mt-4 pt-3">
<Text
variant="bodySmall"
className="leading-relaxed"
numberOfLines={2}
style={{ color: 'rgba(255,255,255,0.7)' }}
>
{author.biography.short}
</Text>
</View>
)}
{/* Professions und Action Buttons */}
<View className="flex-row items-end justify-between mt-3">
{/* Professions links */}
<View className="flex-1 flex-row flex-wrap">
{author.profession && author.profession.length > 0 && (
<>
{author.profession.slice(0, 2).map((prof, idx) => (
<View
key={idx}
className="bg-white/10 px-2.5 py-1 rounded-full mr-2 mb-2"
>
<Text variant="caption" className="opacity-70" style={{ color: 'white' }}>
{prof}
</Text>
</View>
))}
{author.profession.length > 2 && (
<Text variant="caption" className="self-center" style={{ color: 'rgba(255,255,255,0.5)' }}>
+{author.profession.length - 2}
</Text>
)}
</>
)}
</View>
{/* Action Buttons rechts */}
<View className="flex-row items-center gap-3">
{/* Copy Button */}
<Pressable
onPress={handleCopyToClipboard}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Icon
name="copy-outline"
size={22}
color="rgba(255,255,255,0.7)"
/>
</Pressable>
{/* Share Button */}
<Pressable
onPress={handleShare}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Icon
name="share-outline"
size={22}
color="rgba(255,255,255,0.7)"
/>
</Pressable>
{/* Favorite Button */}
<FavoriteButton
isFavorite={favoriteStatus}
onToggle={handleFavoritePress}
size={24}
/>
</View>
</View>
</View>
</View>
</LinearGradient>
</Pressable>
</Animated.View>
);
if (variant === 'vertical') {
return (
<View style={{
height: cardHeight,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 16
}}>
<Animated.View style={[verticalAnimatedStyle, { width: '100%', maxWidth: 400 }]}>
{renderEnhancedCard()}
</Animated.View>
</View>
);
}
if (variant === 'enhanced') {
return renderEnhancedCard();
}
return renderSimpleCard();
}
// Optimierung mit React.memo
export default React.memo(AuthorCard, (prevProps, nextProps) => {
return (
prevProps.author.id === nextProps.author.id &&
prevProps.variant === nextProps.variant &&
prevProps.isFavorite === nextProps.isFavorite
);
});

View file

@ -0,0 +1,191 @@
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, Alert, ActivityIndicator, Platform, Pressable } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useQuotesStore } from '../store/quotesStore';
import { CloudSyncService } from '../services/cloudSync/cloudSyncService';
import { useIsDarkMode } from '~/store/settingsStore';
import * as Haptics from 'expo-haptics';
interface CloudSyncButtonProps {
className?: string;
}
export const CloudSyncButton: React.FC<CloudSyncButtonProps> = ({ className = '' }) => {
const [isLoading, setIsLoading] = useState(false);
const { exportToCloud, importFromCloud, lastSyncDate, isSyncing } = useQuotesStore();
const isDarkMode = useIsDarkMode();
const formatLastSyncDate = () => {
if (!lastSyncDate) return 'Noch nie synchronisiert';
const date = new Date(lastSyncDate);
const now = new Date();
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / 60000);
if (diffInMinutes < 1) return 'Gerade eben';
if (diffInMinutes < 60) return `Vor ${diffInMinutes} Minuten`;
if (diffInMinutes < 1440) return `Vor ${Math.floor(diffInMinutes / 60)} Stunden`;
return date.toLocaleDateString('de-DE');
};
const handleExport = async () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setIsLoading(true);
try {
const authenticated = await CloudSyncService.authenticate();
if (!authenticated && Platform.OS === 'android') {
Alert.alert(
'Authentifizierung fehlgeschlagen',
'Bitte überprüfe deine Kontoeinstellungen'
);
return;
}
const success = await exportToCloud();
if (Platform.OS === 'ios') {
// On iOS, the share sheet handles the feedback
return;
}
Alert.alert(
success ? 'Export erfolgreich' : 'Export fehlgeschlagen',
success
? 'Deine Daten wurden erfolgreich gesichert'
: 'Backup konnte nicht erstellt werden. Bitte versuche es erneut.'
);
} catch (error) {
Alert.alert('Fehler', 'Ein Fehler ist beim Backup aufgetreten');
} finally {
setIsLoading(false);
}
};
const handleImport = async () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
Alert.alert(
'Daten wiederherstellen',
'Möchtest du deine gespeicherten Daten wiederherstellen? Neue Favoriten werden hinzugefügt, bestehende bleiben erhalten.',
[
{
text: 'Abbrechen',
style: 'cancel'
},
{
text: 'Wiederherstellen',
onPress: async () => {
setIsLoading(true);
try {
const success = await importFromCloud();
Alert.alert(
success ? 'Import erfolgreich' : 'Import fehlgeschlagen',
success
? 'Deine Daten wurden erfolgreich wiederhergestellt'
: 'Daten konnten nicht wiederhergestellt werden. Bitte wähle eine gültige Backup-Datei.'
);
} catch (error) {
Alert.alert('Fehler', 'Ein Fehler ist bei der Wiederherstellung aufgetreten');
} finally {
setIsLoading(false);
}
}
}
]
);
};
return (
<View className={className}>
{/* Main Backup Actions */}
<View className={`${isDarkMode ? 'bg-white/10' : 'bg-black/10'} rounded-2xl`}>
{/* Export/Backup Button */}
<Pressable
onPress={handleExport}
disabled={isLoading || isSyncing}
className={`flex-row justify-between items-center p-4 ${
(isLoading || isSyncing) ? 'opacity-50' : ''
} border-b ${isDarkMode ? 'border-white/5' : 'border-black/5'}`}
>
<View className="flex-1">
<View className="flex-row items-center">
<Ionicons
name={Platform.OS === 'ios' ? 'cloud-upload' : 'cloud-upload-outline'}
size={20}
color={isDarkMode ? '#60A5FA' : '#3B82F6'}
style={{ marginRight: 12 }}
/>
<View className="flex-1">
<Text className={`${isDarkMode ? 'text-white' : 'text-black'} text-base font-medium`}>
Backup erstellen
</Text>
<Text className={`${isDarkMode ? 'text-white/60' : 'text-black/60'} text-xs mt-0.5`}>
Sichere deine Daten in {Platform.OS === 'ios' ? 'iCloud' : 'Google Drive'}
</Text>
</View>
</View>
</View>
{(isLoading || isSyncing) ? (
<ActivityIndicator size="small" color={isDarkMode ? '#60A5FA' : '#3B82F6'} />
) : (
<Ionicons
name="chevron-forward"
size={20}
color={isDarkMode ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.3)'}
/>
)}
</Pressable>
{/* Import/Restore Button */}
<Pressable
onPress={handleImport}
disabled={isLoading || isSyncing}
className={`flex-row justify-between items-center p-4 ${
(isLoading || isSyncing) ? 'opacity-50' : ''
}`}
>
<View className="flex-1">
<View className="flex-row items-center">
<Ionicons
name={Platform.OS === 'ios' ? 'cloud-download' : 'cloud-download-outline'}
size={20}
color={isDarkMode ? '#10B981' : '#059669'}
style={{ marginRight: 12 }}
/>
<View className="flex-1">
<Text className={`${isDarkMode ? 'text-white' : 'text-black'} text-base font-medium`}>
Wiederherstellen
</Text>
<Text className={`${isDarkMode ? 'text-white/60' : 'text-black/60'} text-xs mt-0.5`}>
Lade dein letztes Backup
</Text>
</View>
</View>
</View>
{(isLoading || isSyncing) ? (
<ActivityIndicator size="small" color={isDarkMode ? '#10B981' : '#059669'} />
) : (
<Ionicons
name="chevron-forward"
size={20}
color={isDarkMode ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.3)'}
/>
)}
</Pressable>
</View>
{/* Last Sync Info */}
{lastSyncDate && (
<View className="mt-3 flex-row items-center justify-center">
<Ionicons
name="checkmark-circle"
size={14}
color={isDarkMode ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)'}
/>
<Text className={`${isDarkMode ? 'text-white/40' : 'text-black/40'} text-xs ml-1.5`}>
Letzte Synchronisation: {formatLastSyncDate()}
</Text>
</View>
)}
</View>
);
};

View file

@ -0,0 +1,164 @@
/**
* Error Boundary Component
* Fängt JavaScript-Fehler in der Komponenten-Hierarchie ab
*/
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { View, Text, Pressable, ScrollView } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Icon } from './Icon';
import * as Haptics from 'expo-haptics';
import { withTranslation, WithTranslation } from 'react-i18next';
interface Props extends WithTranslation {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
class ErrorBoundaryClass extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null
};
}
static getDerivedStateFromError(error: Error): State {
return {
hasError: true,
error,
errorInfo: null
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('=================================');
console.error('ErrorBoundary caught an error:');
console.error('Error:', error);
console.error('Error Message:', error.message);
console.error('Error Stack:', error.stack);
console.error('Component Stack:', errorInfo.componentStack);
console.error('=================================');
this.setState({
error,
errorInfo
});
// Callback für externes Error-Logging
if (this.props.onError) {
this.props.onError(error, errorInfo);
}
}
handleReset = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
this.setState({
hasError: false,
error: null,
errorInfo: null
});
};
render() {
if (this.state.hasError) {
// Custom fallback UI wenn vorhanden
if (this.props.fallback) {
return this.props.fallback;
}
// Standard Error UI
return (
<SafeAreaView className="flex-1 bg-white dark:bg-black">
<ScrollView
className="flex-1"
contentContainerStyle={{
flexGrow: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20
}}
>
<View className="items-center max-w-sm">
<View className="bg-red-100 dark:bg-red-900/20 p-4 rounded-full mb-6">
<Icon name="warning-outline" size={48} color="#ef4444" />
</View>
<Text className="text-2xl font-bold text-black dark:text-white mb-2 text-center">
{this.props.t('errors.somethingWrong') || 'Ups, etwas ist schiefgelaufen!'}
</Text>
<Text className="text-gray-600 dark:text-gray-400 text-center mb-6">
{this.props.t('errors.unexpectedError') || 'Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut oder starte die App neu.'}
</Text>
{/* Error Details (nur in Development) */}
{__DEV__ && this.state.error && (
<View className="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg mb-6 w-full">
<Text className="text-red-600 dark:text-red-400 font-mono text-xs mb-2">
{this.state.error.toString()}
</Text>
{this.state.errorInfo && (
<Text className="text-gray-600 dark:text-gray-400 font-mono text-xs">
{this.state.errorInfo.componentStack}
</Text>
)}
</View>
)}
<Pressable
onPress={this.handleReset}
className="bg-black dark:bg-white px-6 py-3 rounded-full"
>
<Text className="text-white dark:text-black font-semibold">
{this.props.t('common.retry') || 'Erneut versuchen'}
</Text>
</Pressable>
</View>
</ScrollView>
</SafeAreaView>
);
}
return this.props.children;
}
}
// Hook für funktionale Komponenten
export const useErrorHandler = () => {
const [error, setError] = React.useState<Error | null>(null);
React.useEffect(() => {
if (error) {
throw error;
}
}, [error]);
const resetError = () => setError(null);
const throwError = (error: Error) => setError(error);
return { throwError, resetError };
};
export const ErrorBoundary = withTranslation()(ErrorBoundaryClass);
// HOC für Error Boundary
export function withErrorBoundary<P extends object>(
Component: React.ComponentType<P>,
fallback?: ReactNode
) {
return (props: P) => (
<ErrorBoundary fallback={fallback}>
<Component {...props} />
</ErrorBoundary>
);
}

View file

@ -0,0 +1,198 @@
import React from 'react';
import { Ionicons } from '@expo/vector-icons';
import { StyleProp, ViewStyle } from 'react-native';
// Definiere alle verwendeten Icons zentral
export type IconName =
// Navigation & UI
| 'chevron-back'
| 'chevron-forward'
| 'close'
| 'search'
| 'filter'
| 'settings'
| 'settings-outline'
| 'menu'
| 'apps'
| 'swap-vertical-outline'
| 'create'
| 'create-outline'
| 'checkmark'
| 'checkmark-outline'
| 'list-outline'
| 'grid-outline'
// Actions
| 'heart'
| 'heart-outline'
| 'star'
| 'star-outline'
| 'share'
| 'share-outline'
| 'copy'
| 'copy-outline'
| 'trash'
| 'trash-outline'
| 'refresh'
| 'shuffle'
| 'play-circle'
| 'notifications'
| 'add'
| 'add-outline'
| 'add-circle'
| 'add-circle-outline'
// Content
| 'book'
| 'book-outline'
| 'person'
| 'person-outline'
| 'people'
| 'people-outline'
| 'text'
| 'text-outline'
| 'stats-chart-outline'
// Weather & Time
| 'sunny'
| 'moon'
| 'time'
| 'calendar'
// Status & Info
| 'checkmark-circle'
| 'information-circle'
| 'warning'
| 'alert-circle'
// Media & Communication
| 'phone-portrait'
| 'globe'
| 'globe-outline'
| 'link'
| 'link-outline'
// Categories (custom mapping)
| 'bulb'
| 'bulb-outline'
| 'rocket'
| 'trophy'
| 'leaf'
| 'happy'
| 'happy-outline'
| 'flask'
| 'color-palette'
| 'flash'
| 'flower'
| 'shield'
| 'pricetag';
interface IconProps {
name: IconName;
size?: number;
color?: string;
style?: StyleProp<ViewStyle>;
focused?: boolean;
}
// Standard-Farben
const colors = {
white: '#ffffff',
black: '#000000',
gray: {
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
},
red: '#ef4444',
yellow: '#fbbf24',
pink: '#ec4899',
transparent: {
white: {
60: 'rgba(255,255,255,0.6)',
70: 'rgba(255,255,255,0.7)',
80: 'rgba(255,255,255,0.8)',
},
black: {
60: 'rgba(0,0,0,0.6)',
}
}
};
export const Icon: React.FC<IconProps> = ({
name,
size = 24,
color = colors.white,
style,
focused = false
}) => {
// Only use focused logic when explicitly provided
// Otherwise, use the icon name exactly as provided
let iconName: any = name;
if (focused !== undefined && focused !== false) {
// If focused and icon has -outline, remove it
if (focused && name.includes('-outline')) {
iconName = name.replace('-outline', '') as any;
}
}
return (
<Ionicons
name={iconName}
size={size}
color={color}
style={style}
/>
);
};
// Helper-Funktion um zu prüfen ob eine Outline-Variante existiert
function hasOutlineVariant(name: string): boolean {
const outlineVariants = [
'heart', 'star', 'share', 'copy', 'trash',
'book', 'person', 'people', 'settings',
'text', 'bulb', 'happy', 'globe', 'link'
];
return outlineVariants.includes(name);
}
// Export der Farben für konsistente Verwendung
export { colors as IconColors };
// Spezielle Icon-Sets für Kategorien
export const getCategoryIcon = (category: string): IconName => {
const categoryIcons: Record<string, IconName> = {
'wisdom': 'bulb',
'love': 'heart',
'motivation': 'rocket',
'success': 'trophy',
'life': 'leaf',
'happiness': 'happy',
'philosophy': 'book',
'science': 'flask',
'creativity': 'color-palette',
'humor': 'happy-outline',
'inspiration': 'star',
'leadership': 'people',
'innovation': 'flash',
'dreams': 'moon',
'courage': 'shield',
'mindfulness': 'flower'
};
return categoryIcons[category] || 'pricetag';
};
// Tab-Icon-Helper
export const getTabIcon = (routeName: string, focused: boolean): IconName => {
const tabIcons: Record<string, IconName> = {
'index': focused ? 'book' : 'book-outline',
'search': focused ? 'search' : 'search',
'authors': focused ? 'people' : 'people-outline',
'favorites': focused ? 'heart' : 'heart-outline',
'settings': focused ? 'settings' : 'settings-outline',
};
return tabIcons[routeName] || 'apps';
};
export default Icon;

View file

@ -0,0 +1,212 @@
import React from 'react';
import {
Modal,
View,
Text,
TouchableOpacity,
TouchableWithoutFeedback,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import { LinearGradient } from 'expo-linear-gradient';
import { useIsDarkMode } from '~/store/settingsStore';
interface PremiumLimitDialogProps {
visible: boolean;
onClose: () => void;
limitType: 'favorites' | 'search' | 'collections';
remaining?: number;
max?: number;
}
export function PremiumLimitDialog({
visible,
onClose,
limitType,
remaining = 0,
max = 5,
}: PremiumLimitDialogProps) {
const isDarkMode = useIsDarkMode();
const getLimitInfo = () => {
switch (limitType) {
case 'favorites':
return {
icon: 'heart',
title: 'Favoriten-Limit erreicht',
description: `Du hast deine ${max} täglichen Favoriten aufgebraucht.`,
benefit: 'Unbegrenzte Favoriten mit Premium',
};
case 'search':
return {
icon: 'search',
title: 'Such-Limit erreicht',
description: `Du hast deine ${max} täglichen Suchen aufgebraucht.`,
benefit: 'Unbegrenzte Suchen mit Premium',
};
case 'collections':
return {
icon: 'folder',
title: 'Listen-Limit erreicht',
description: `Du kannst nur ${max} Liste pro Woche erstellen.`,
benefit: 'Unbegrenzte Listen mit Premium',
};
}
};
const info = getLimitInfo();
const handleUpgrade = () => {
onClose();
router.push('/paywall');
};
return (
<Modal
visible={visible}
transparent
animationType="fade"
onRequestClose={onClose}
>
<TouchableWithoutFeedback onPress={onClose}>
<View className="flex-1 bg-black/50 justify-center items-center px-6">
<TouchableWithoutFeedback>
<View
style={{
backgroundColor: isDarkMode ? '#1a1a1a' : '#ffffff',
borderRadius: 24,
padding: 24,
width: '100%',
maxWidth: 400,
}}
>
{/* Icon */}
<View className="items-center mb-4">
<LinearGradient
colors={['#8B5CF6', '#6366F1']}
style={{
width: 64,
height: 64,
borderRadius: 32,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Ionicons name={info.icon as any} size={28} color="white" />
</LinearGradient>
</View>
{/* Title */}
<Text
style={{
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 8,
color: isDarkMode ? '#ffffff' : '#000000',
}}
>
{info.title}
</Text>
{/* Description */}
<Text
style={{
fontSize: 16,
textAlign: 'center',
marginBottom: 16,
color: isDarkMode ? '#9ca3af' : '#4b5563',
}}
>
{info.description}
</Text>
{/* Progress Bar */}
{remaining > 0 && (
<View className="mb-4">
<View
style={{
height: 8,
backgroundColor: isDarkMode ? '#374151' : '#e5e7eb',
borderRadius: 999,
overflow: 'hidden',
}}
>
<LinearGradient
colors={['#6366F1', '#8B5CF6']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={{
height: '100%',
width: `${((max - remaining) / max) * 100}%`,
}}
/>
</View>
<Text
style={{
fontSize: 12,
textAlign: 'center',
marginTop: 4,
color: isDarkMode ? '#6b7280' : '#9ca3af',
}}
>
{remaining} von {max} verbleibend
</Text>
</View>
)}
{/* Benefit */}
<View
style={{
backgroundColor: isDarkMode ? 'rgba(99, 102, 241, 0.15)' : '#eef2ff',
borderRadius: 12,
padding: 12,
marginBottom: 24,
}}
>
<Text
style={{
color: isDarkMode ? '#a5b4fc' : '#4f46e5',
textAlign: 'center',
fontWeight: '500',
}}
>
{info.benefit}
</Text>
</View>
{/* Buttons */}
<TouchableOpacity
onPress={handleUpgrade}
style={{
backgroundColor: '#6366F1',
borderRadius: 16,
paddingVertical: 16,
marginBottom: 12,
}}
>
<Text className="text-white text-center font-semibold text-base">
Jetzt Premium werden
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={onClose}
style={{ paddingVertical: 8 }}
>
<Text
style={{
textAlign: 'center',
color: isDarkMode ? '#9ca3af' : '#6b7280',
}}
>
Später
</Text>
</TouchableOpacity>
</View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
</Modal>
);
}

View file

@ -0,0 +1,212 @@
import React, { useState, useEffect } from 'react';
import { View, Pressable, useColorScheme, Alert, Platform, ActionSheetIOS } from 'react-native';
import { Icon } from './Icon';
import { useListStore, List } from '~/store/listStore';
import { useIsDarkMode } from '~/store/settingsStore';
import usePremiumStore from '~/store/premiumStore';
import { PremiumLimitDialog } from './PremiumLimitDialog';
import * as Haptics from 'expo-haptics';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring
} from 'react-native-reanimated';
import { useTranslation } from 'react-i18next';
// Disable ContextMenu due to rendering issues - use ActionSheet/Alert instead
// The @expo/ui ContextMenu is unstable and can cause the icon to disappear
let ContextMenu: any = null;
let Host: any = null;
let ExpoButton: any = null;
interface QuickAddToListProps {
quoteId: string;
iconSize?: number;
iconColor?: string;
}
export default function QuickAddToList({
quoteId,
iconSize = 28,
iconColor = 'rgba(255,255,255,0.8)'
}: QuickAddToListProps) {
const isDarkMode = useIsDarkMode();
const colorScheme = useColorScheme();
const { t } = useTranslation();
const [addedLists, setAddedLists] = useState<string[]>([]);
const [showPremiumDialog, setShowPremiumDialog] = useState(false);
const iconScale = useSharedValue(1);
const {
lists,
addQuoteToList,
removeQuoteFromList,
getQuoteLists,
isQuoteInList,
initializeLists,
createList
} = useListStore();
const {
isPremium,
canCreateCollection,
getRemainingCollections,
MAX_WEEKLY_COLLECTIONS
} = usePremiumStore();
useEffect(() => {
initializeLists();
}, []);
const handlePress = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
iconScale.value = withSpring(1.2, {}, () => {
iconScale.value = withSpring(1);
});
// Get lists that already contain this quote
const existingLists = getQuoteLists(quoteId);
setAddedLists(existingLists.map(p => p.id));
// If ContextMenu is not available, use ActionSheet on iOS or Alert on Android
if (!ContextMenu && Platform.OS === 'ios') {
showIOSActionSheet();
} else if (!ContextMenu) {
showAndroidAlert();
}
};
const showIOSActionSheet = () => {
const options = [
...lists.map(p => {
const isInList = isQuoteInList(p.id, quoteId);
return isInList ? `${p.name}` : p.name;
}),
t('lists.createNew'),
t('common.cancel')
];
ActionSheetIOS.showActionSheetWithOptions(
{
options,
cancelButtonIndex: options.length - 1,
title: t('lists.addToList')
},
(buttonIndex) => {
if (buttonIndex < lists.length) {
handleToggleList(lists[buttonIndex]);
} else if (buttonIndex === lists.length) {
handleCreateNewList();
}
}
);
};
const showAndroidAlert = () => {
Alert.alert(
t('lists.addToList'),
t('lists.chooseOrCreate'),
[
...lists.map(p => ({
text: isQuoteInList(p.id, quoteId) ? `${p.name}` : p.name,
onPress: () => handleToggleList(p)
})),
{
text: t('lists.createNew'),
onPress: handleCreateNewList
},
{
text: t('common.cancel'),
style: 'cancel'
}
]
);
};
const handleToggleList = (list: List) => {
if (isQuoteInList(list.id, quoteId)) {
// Remove from list
removeQuoteFromList(list.id, quoteId);
setAddedLists(addedLists.filter(id => id !== list.id));
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} else {
// Add to list
addQuoteToList(list.id, quoteId);
setAddedLists([...addedLists, list.id]);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
// Context menu closes automatically after selection
};
const handleCreateNewList = () => {
// Check premium limits before allowing creation
if (!isPremium && !canCreateCollection()) {
setShowPremiumDialog(true);
return;
}
Alert.prompt(
t('lists.newList'),
t('lists.newListPrompt'),
[
{
text: t('common.cancel'),
style: 'cancel'
},
{
text: t('lists.create'),
onPress: (name) => {
if (name && name.trim()) {
const newListId = createList(name.trim());
// Check if creation was successful (returns empty string if limit reached)
if (newListId) {
// Add the current quote to the new list
addQuoteToList(newListId, quoteId);
setAddedLists([...addedLists, newListId]);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} else {
// Show premium dialog if creation failed due to limits
setShowPremiumDialog(true);
}
}
}
}
],
'plain-text',
t('lists.newListPlaceholder')
);
};
const iconAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: iconScale.value }]
}));
// Use ActionSheet/Alert for reliable rendering
// ContextMenu (@expo/ui) was causing the icon to disappear intermittently
return (
<>
<Pressable
onPress={handlePress}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Animated.View style={iconAnimatedStyle}>
<Icon
name="add-outline"
size={iconSize}
color={iconColor}
/>
</Animated.View>
</Pressable>
<PremiumLimitDialog
visible={showPremiumDialog}
onClose={() => setShowPremiumDialog(false)}
limitType="collections"
remaining={getRemainingCollections()}
max={MAX_WEEKLY_COLLECTIONS}
/>
</>
);
}

View file

@ -0,0 +1,432 @@
import React from 'react';
import { View, Pressable, ViewStyle, TextInput } from 'react-native';
import Text from './Text';
import { LinearGradient } from 'expo-linear-gradient';
import { Icon } from './Icon';
import * as Haptics from 'expo-haptics';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
FadeInUp,
FadeInDown,
interpolate,
Extrapolate,
SharedValue
} from 'react-native-reanimated';
import type { EnhancedQuote } from '@quote/shared';
import { useTheme } from '~/hooks/useTheme';
import { useTranslation } from 'react-i18next';
import { useShare } from '~/hooks/useShare';
import QuickAddToList from './QuickAddToList';
import FavoriteButton from './common/FavoriteButton';
export type QuoteCardVariant = 'simple' | 'daily' | 'vertical' | 'author-detail' | 'edit';
interface QuoteCardProps {
quote: EnhancedQuote;
onToggleFavorite: (id: string) => void;
onAuthorPress?: () => void;
onEdit?: () => void;
onDelete?: () => void;
onPress?: () => void;
variant?: QuoteCardVariant;
// For vertical variant
index?: number;
scrollY?: SharedValue<number>;
cardHeight?: number;
// Show date instead of author (for author detail pages)
showDate?: boolean;
// For edit mode
editMode?: boolean;
onTextChange?: (text: string) => void;
onAuthorChange?: (author: string) => void;
onCategoryChange?: (categories: string) => void;
onSave?: () => void;
onCancel?: () => void;
}
function QuoteCard({
quote,
onToggleFavorite,
onAuthorPress,
onEdit,
onDelete,
onPress,
variant = 'simple',
index = 0,
scrollY,
cardHeight = 300,
showDate = false,
editMode = false,
onTextChange,
onAuthorChange,
onCategoryChange,
onSave,
onCancel
}: QuoteCardProps) {
const { t } = useTranslation();
const scale = useSharedValue(1);
const { shareQuote, copyQuoteToClipboard } = useShare();
// Safety check: return null if quote is invalid
if (!quote || !quote.id || !quote.text) {
console.warn('[QuoteCard] Attempted to render invalid quote:', quote);
return null;
}
const handleFavorite = () => {
if (!quote || !quote.id) {
console.warn('[QuoteCard] Attempted to toggle favorite on invalid quote:', quote);
return;
}
onToggleFavorite(quote.id);
};
const handlePress = () => {
if (onPress) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
scale.value = withSpring(0.95, {}, () => {
scale.value = withSpring(1);
});
onPress();
}
};
const handleShare = () => shareQuote(quote);
const handleCopyToClipboard = () => copyQuoteToClipboard(quote);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }]
}));
const verticalAnimatedStyle = useAnimatedStyle(() => {
if (variant !== 'vertical' || !scrollY) {
return {};
}
const cardPosition = index * cardHeight;
const inputRange = [
cardPosition - cardHeight,
cardPosition,
cardPosition + cardHeight,
];
const opacity = interpolate(
scrollY.value,
inputRange,
[0.3, 1, 0.3],
Extrapolate.CLAMP
);
const scale = interpolate(
scrollY.value,
inputRange,
[0.85, 1, 0.85],
Extrapolate.CLAMP
);
return {
opacity,
transform: [{ scale }],
};
});
const { getCategoryGradient, getDailyCardGradient } = useTheme();
const getGradientColors = () => {
if (variant === 'daily') {
return getDailyCardGradient();
}
const category = quote.categories?.[0];
return getCategoryGradient(category);
};
const getQuoteMetadata = () => {
if (quote.year && quote.source) {
return `${quote.source} (${quote.year})`;
} else if (quote.year) {
return quote.year.toString();
} else if (quote.source) {
return quote.source;
}
return null;
};
const isDaily = variant === 'daily';
const isVertical = variant === 'vertical';
const cardContent = (
<LinearGradient
colors={getGradientColors()}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={{
borderRadius: isDaily ? 32 : 24,
padding: 1
}}
>
<View className={`${isDaily ? 'bg-black/30 rounded-[31px]' : 'bg-black/40 rounded-3xl'} backdrop-blur-xl`}>
<View className={isDaily ? 'p-5' : 'p-6'}>
{/* Quote Text */}
{editMode ? (
<TextInput
value={quote.text}
onChangeText={onTextChange}
placeholder={t('myQuotes.placeholder')}
placeholderTextColor="rgba(255,255,255,0.4)"
multiline
style={{
fontFamily: 'Georgia',
fontSize: 22,
lineHeight: 32,
color: 'white',
fontWeight: '300',
letterSpacing: 0.3,
minHeight: 80,
textAlignVertical: 'top',
paddingVertical: 0
}}
className="mb-4"
autoFocus
/>
) : (
<Text
style={{
fontFamily: 'Georgia',
fontSize: isDaily ? 24 : 22,
lineHeight: isDaily ? 34 : 32,
color: 'white',
fontWeight: '300',
letterSpacing: 0.3
}}
className={isDaily ? 'mb-4' : 'mb-4'}
>
"{quote.text}"
</Text>
)}
{/* Source Info wenn vorhanden */}
{!isDaily && quote.source && (
<Text
variant="caption"
className="mb-3"
style={{ color: 'rgba(255,255,255,0.6)' }}
>
{t('quotes.from')}: {quote.source} {quote.year && `(${quote.year})`}
</Text>
)}
{/* Author mit Divider */}
<View className={`border-t border-white/10 ${isDaily ? 'pt-4' : 'pt-3'}`}>
<Pressable onPress={onAuthorPress}>
<View className="flex-row items-center justify-between">
<View>
{editMode ? (
<>
<TextInput
value={quote.author?.name || ''}
onChangeText={onAuthorChange}
placeholder="Autor"
placeholderTextColor="rgba(255,255,255,0.4)"
style={{
fontSize: 16,
color: 'white',
fontWeight: '500',
paddingVertical: 2
}}
/>
<TextInput
value={quote.categories?.join(', ') || ''}
onChangeText={onCategoryChange}
placeholder="Kategorien (kommagetrennt)"
placeholderTextColor="rgba(255,255,255,0.3)"
style={{
fontSize: 12,
color: 'rgba(255,255,255,0.6)',
paddingVertical: 2,
marginTop: 2
}}
/>
</>
) : showDate ? (
<></>
) : (
<>
<Text
variant="body"
weight="medium"
style={{ color: 'white' }}
>
{quote.author?.name || t('quotes.unknown')}
</Text>
{quote.author?.profession && (
<Text
variant="caption"
className="mt-0.5"
style={{ color: 'rgba(255,255,255,0.6)' }}
>
{quote.author.profession[0]}
</Text>
)}
</>
)}
</View>
{/* Action Buttons */}
<View className="flex-row items-center gap-3">
{editMode ? (
<>
{/* Cancel Button */}
<Pressable
onPress={onCancel}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Icon
name="close"
size={22}
color="rgba(255,255,255,0.8)"
/>
</Pressable>
{/* Save Button */}
<Pressable
onPress={onSave}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
disabled={!quote.text?.trim()}
>
<Icon
name="checkmark"
size={22}
color={quote.text?.trim() ? "rgba(255,255,255,0.8)" : "rgba(255,255,255,0.3)"}
/>
</Pressable>
</>
) : (
<>
{/* Copy Button */}
<Pressable
onPress={handleCopyToClipboard}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Icon
name="copy-outline"
size={22}
color="rgba(255,255,255,0.7)"
/>
</Pressable>
{/* Share Button */}
<Pressable
onPress={handleShare}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Icon
name="share-outline"
size={22}
color="rgba(255,255,255,0.7)"
/>
</Pressable>
{/* Edit Button (only if onEdit is provided) */}
{!isDaily && onEdit && (
<Pressable
onPress={onEdit}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Icon
name="create-outline"
size={22}
color="rgba(255,255,255,0.7)"
/>
</Pressable>
)}
{/* Delete Button (only if onDelete is provided) */}
{!isDaily && onDelete && (
<Pressable
onPress={onDelete}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Icon
name="trash-outline"
size={22}
color="rgba(255,255,255,0.7)"
/>
</Pressable>
)}
{/* Add to List Button */}
<QuickAddToList
quoteId={quote.id}
iconSize={23}
iconColor="rgba(255,255,255,0.75)"
/>
{/* Favorite Button */}
<FavoriteButton
isFavorite={quote.isFavorite}
onToggle={handleFavorite}
size={24}
variant={isDaily ? 'daily' : 'default'}
/>
</>
)}
</View>
</View>
</Pressable>
</View>
</View>
</View>
</LinearGradient>
);
if (isVertical) {
return (
<View style={{
height: cardHeight,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 16,
}}>
<View style={{ width: '100%', maxWidth: 400, marginTop: -40 }}>
<Animated.View style={verticalAnimatedStyle}>
<Animated.View style={animatedStyle}>
{cardContent}
</Animated.View>
</Animated.View>
</View>
</View>
);
}
return (
<Animated.View
entering={isDaily ? FadeInDown.duration(600).springify() : FadeInUp.duration(600).springify()}
className="mx-4"
>
<Animated.View style={animatedStyle}>
{isDaily && onPress ? (
<Pressable onPress={handlePress}>
{cardContent}
</Pressable>
) : (
cardContent
)}
</Animated.View>
</Animated.View>
);
}
// Optimierung mit React.memo
export default React.memo(QuoteCard, (prevProps, nextProps) => {
// Nur re-rendern wenn sich relevante Props ändern
return (
prevProps.quote.id === nextProps.quote.id &&
prevProps.quote.isFavorite === nextProps.quote.isFavorite &&
prevProps.quote.text === nextProps.quote.text &&
prevProps.variant === nextProps.variant
);
});

View file

@ -0,0 +1,163 @@
import React from 'react';
import { Text as RNText, TextProps as RNTextProps, TextStyle } from 'react-native';
import { useIsDarkMode, useFontSize } from '~/store/settingsStore';
type TextVariant =
| 'h1'
| 'h2'
| 'h3'
| 'h4'
| 'body'
| 'bodyLarge'
| 'bodySmall'
| 'caption'
| 'label'
| 'button'
| 'quote';
type TextColor =
| 'primary'
| 'secondary'
| 'tertiary'
| 'accent'
| 'danger'
| 'success'
| 'warning'
| 'inherit';
interface TextProps extends Omit<RNTextProps, 'style'> {
variant?: TextVariant;
color?: TextColor;
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
align?: 'left' | 'center' | 'right' | 'justify';
className?: string;
style?: TextStyle;
children?: React.ReactNode;
}
export default function Text({
variant = 'body',
color = 'primary',
weight,
align,
className = '',
style,
children,
...props
}: TextProps) {
const isDarkMode = useIsDarkMode();
const fontSize = useFontSize();
// Font size multiplier based on user preference
const sizeMultiplier = fontSize === 'small' ? 0.9 : fontSize === 'large' ? 1.1 : 1;
// Base styles for each variant
const variantStyles: Record<TextVariant, string> = {
h1: 'text-4xl font-bold',
h2: 'text-3xl font-bold',
h3: 'text-2xl font-semibold',
h4: 'text-xl font-semibold',
body: 'text-base',
bodyLarge: 'text-lg',
bodySmall: 'text-sm',
caption: 'text-xs',
label: 'text-sm font-medium uppercase tracking-wider',
button: 'text-base font-semibold',
quote: 'text-lg italic',
};
// Color styles based on theme
const getColorClass = (color: TextColor): string => {
if (color === 'inherit') return '';
const colors: Record<TextColor, { dark: string; light: string }> = {
primary: {
dark: 'text-white',
light: 'text-black',
},
secondary: {
dark: 'text-white/60',
light: 'text-black/60',
},
tertiary: {
dark: 'text-white/40',
light: 'text-black/40',
},
accent: {
dark: 'text-purple-400',
light: 'text-purple-600',
},
danger: {
dark: 'text-red-400',
light: 'text-red-600',
},
success: {
dark: 'text-green-400',
light: 'text-green-600',
},
warning: {
dark: 'text-yellow-400',
light: 'text-yellow-600',
},
inherit: {
dark: '',
light: '',
},
};
return isDarkMode ? colors[color].dark : colors[color].light;
};
// Weight styles
const weightStyles: Record<string, string> = {
normal: 'font-normal',
medium: 'font-medium',
semibold: 'font-semibold',
bold: 'font-bold',
};
// Alignment styles
const alignStyles: Record<string, string> = {
left: 'text-left',
center: 'text-center',
right: 'text-right',
justify: 'text-justify',
};
// Combine all classes
const combinedClassName = [
variantStyles[variant],
getColorClass(color),
weight ? weightStyles[weight] : '',
align ? alignStyles[align] : '',
className,
]
.filter(Boolean)
.join(' ');
// Apply size multiplier to style
const combinedStyle: TextStyle = {
...(style || {}),
...(sizeMultiplier !== 1 ? { fontSize: (style?.fontSize || 16) * sizeMultiplier } : {}),
};
return (
<RNText
className={combinedClassName}
style={combinedStyle}
{...props}
>
{children}
</RNText>
);
}
// Export convenience components for common use cases
export const H1 = (props: Omit<TextProps, 'variant'>) => <Text variant="h1" {...props} />;
export const H2 = (props: Omit<TextProps, 'variant'>) => <Text variant="h2" {...props} />;
export const H3 = (props: Omit<TextProps, 'variant'>) => <Text variant="h3" {...props} />;
export const H4 = (props: Omit<TextProps, 'variant'>) => <Text variant="h4" {...props} />;
export const Body = (props: Omit<TextProps, 'variant'>) => <Text variant="body" {...props} />;
export const Caption = (props: Omit<TextProps, 'variant'>) => <Text variant="caption" {...props} />;
export const Label = (props: Omit<TextProps, 'variant'>) => <Text variant="label" {...props} />;
export const Quote = (props: Omit<TextProps, 'variant'>) => <Text variant="quote" {...props} />;

View file

@ -0,0 +1,164 @@
import React from 'react';
import { View, Pressable, ScrollView } from 'react-native';
import Text from '~/components/Text';
import { Icon } from '~/components/Icon';
import { useIsDarkMode } from '~/store/settingsStore';
import * as Haptics from 'expo-haptics';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import { AuthorFilters } from './AuthorFilterSheet';
interface ActiveFilterChipsProps {
filters: AuthorFilters;
onRemoveFilter: (category: keyof AuthorFilters, value: string) => void;
onClearAll: () => void;
}
const FILTER_LABELS: Record<string, string> = {
// Epochs
ancient: 'Antike',
medieval: 'Mittelalter',
earlyModern: 'Frühe Neuzeit',
'19th': '19. Jhd.',
'20th': '20. Jhd.',
'21st': '21. Jhd.',
living: 'Lebend',
// Professions
philosopher: 'Philosoph',
writer: 'Schriftsteller',
scientist: 'Wissenschaftler',
politician: 'Politiker',
artist: 'Künstler',
entrepreneur: 'Unternehmer',
poet: 'Dichter',
activist: 'Aktivist',
// Nationalities
german: 'Deutsch',
american: 'Amerikanisch',
british: 'Britisch',
french: 'Französisch',
italian: 'Italienisch',
spanish: 'Spanisch',
greek: 'Griechisch',
roman: 'Römisch',
// Quote counts
few: '1-5 Zitate',
medium: '6-15 Zitate',
many: '16-50 Zitate',
verymany: '50+ Zitate',
// Special
verified: 'Verifiziert',
featured: 'Featured',
hasImage: 'Mit Bild',
hasBio: 'Mit Bio',
};
export function ActiveFilterChips({
filters,
onRemoveFilter,
onClearAll
}: ActiveFilterChipsProps) {
const isDarkMode = useIsDarkMode();
const allActiveFilters: { category: keyof AuthorFilters; value: string; label: string }[] = [];
// Collect all active filters
(Object.keys(filters) as Array<keyof AuthorFilters>).forEach(category => {
filters[category].forEach(value => {
allActiveFilters.push({
category,
value,
label: FILTER_LABELS[value] || value
});
});
});
if (allActiveFilters.length === 0) return null;
return (
<Animated.View
entering={FadeIn}
exiting={FadeOut}
style={{
paddingHorizontal: 16,
paddingTop: 110, // Account for header
paddingBottom: 8,
backgroundColor: isDarkMode ? '#000' : '#fff',
}}
>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
flexDirection: 'row',
alignItems: 'center',
gap: 8,
}}
>
{/* Active filter chips */}
{allActiveFilters.map((filter, index) => (
<Pressable
key={`${filter.category}-${filter.value}-${index}`}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
onRemoveFilter(filter.category, filter.value);
}}
style={{
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 12,
paddingRight: 8,
paddingVertical: 6,
borderRadius: 16,
backgroundColor: 'rgba(124, 58, 237, 0.15)',
borderWidth: 1,
borderColor: 'rgba(124, 58, 237, 0.3)',
}}
>
<Text
variant="caption"
weight="medium"
style={{ color: '#7c3aed', marginRight: 4 }}
>
{filter.label}
</Text>
<Icon
name="close-circle"
size={16}
color="#7c3aed"
/>
</Pressable>
))}
{/* Clear all button */}
{allActiveFilters.length > 1 && (
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
onClearAll();
}}
style={{
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
backgroundColor: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
borderWidth: 1,
borderColor: isDarkMode ? 'rgba(239, 68, 68, 0.3)' : 'rgba(239, 68, 68, 0.2)',
}}
>
<Text
variant="caption"
weight="semibold"
style={{ color: '#ef4444' }}
>
Alle löschen
</Text>
</Pressable>
)}
</ScrollView>
</Animated.View>
);
}

View file

@ -0,0 +1,54 @@
import React from 'react';
import { View, Text, Image } from 'react-native';
interface AuthorAvatarProps {
name: string;
imageUrl?: string;
size?: 'small' | 'medium' | 'large';
className?: string;
}
export const AuthorAvatar: React.FC<AuthorAvatarProps> = ({
name,
imageUrl,
size = 'medium',
className = '',
}) => {
const getInitials = (fullName: string): string => {
const parts = fullName.split(' ');
if (parts.length >= 2) {
return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
}
return fullName.substring(0, 2).toUpperCase();
};
const sizeStyles = {
small: { width: 48, height: 48, fontSize: 18 },
medium: { width: 64, height: 64, fontSize: 20 },
large: { width: 128, height: 128, fontSize: 36 },
};
const style = sizeStyles[size];
return (
<View
className={`bg-gray-800 items-center justify-center rounded-full ${className}`}
style={{ width: style.width, height: style.height }}
>
{imageUrl ? (
<Image
source={{ uri: imageUrl }}
className="rounded-full"
style={{ width: style.width, height: style.height }}
/>
) : (
<Text
className="text-white font-bold"
style={{ fontSize: style.fontSize }}
>
{getInitials(name)}
</Text>
)}
</View>
);
};

View file

@ -0,0 +1,296 @@
import React, { useMemo, useCallback } from 'react';
import { View, Pressable, StyleSheet } from 'react-native';
import BottomSheet, { BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
import Text from '~/components/Text';
import { Icon } from '~/components/Icon';
import { useIsDarkMode } from '~/store/settingsStore';
import * as Haptics from 'expo-haptics';
import { useTranslation } from 'react-i18next';
import type { BottomSheetBackdropProps } from '@gorhom/bottom-sheet';
export interface AuthorFilters {
epochs: string[];
professions: string[];
nationalities: string[];
quoteCount: string[];
special: string[];
}
interface AuthorFilterBottomSheetProps {
bottomSheetRef: React.RefObject<BottomSheet>;
filters: AuthorFilters;
onFiltersChange: (filters: AuthorFilters) => void;
onClearAll: () => void;
}
const EPOCH_OPTIONS = [
{ key: 'ancient', label: 'Antike', description: 'Vor 500' },
{ key: 'medieval', label: 'Mittelalter', description: '500-1500' },
{ key: 'earlyModern', label: 'Frühe Neuzeit', description: '1500-1800' },
{ key: '19th', label: '19. Jahrhundert', description: '1800-1900' },
{ key: '20th', label: '20. Jahrhundert', description: '1900-2000' },
{ key: '21st', label: '21. Jahrhundert', description: '2000+' },
{ key: 'living', label: 'Noch lebend', description: '' },
];
const PROFESSION_OPTIONS = [
{ key: 'philosopher', label: 'Philosoph' },
{ key: 'writer', label: 'Schriftsteller' },
{ key: 'scientist', label: 'Wissenschaftler' },
{ key: 'politician', label: 'Politiker' },
{ key: 'artist', label: 'Künstler' },
{ key: 'entrepreneur', label: 'Unternehmer' },
{ key: 'poet', label: 'Dichter' },
{ key: 'activist', label: 'Aktivist' },
];
const NATIONALITY_OPTIONS = [
{ key: 'german', label: 'Deutsch' },
{ key: 'american', label: 'Amerikanisch' },
{ key: 'british', label: 'Britisch' },
{ key: 'french', label: 'Französisch' },
{ key: 'italian', label: 'Italienisch' },
{ key: 'spanish', label: 'Spanisch' },
{ key: 'greek', label: 'Griechisch' },
{ key: 'roman', label: 'Römisch' },
];
const QUOTE_COUNT_OPTIONS = [
{ key: 'few', label: 'Wenige', description: '1-5' },
{ key: 'medium', label: 'Mittel', description: '6-15' },
{ key: 'many', label: 'Viele', description: '16-50' },
{ key: 'verymany', label: 'Sehr viele', description: '50+' },
];
const SPECIAL_OPTIONS = [
{ key: 'verified', label: 'Verifiziert' },
{ key: 'featured', label: 'Featured' },
{ key: 'hasImage', label: 'Mit Bild' },
{ key: 'hasBio', label: 'Mit Biografie' },
];
export function AuthorFilterBottomSheet({
bottomSheetRef,
filters,
onFiltersChange,
onClearAll
}: AuthorFilterBottomSheetProps) {
const { t } = useTranslation();
const isDarkMode = useIsDarkMode();
const snapPoints = useMemo(() => ['65%', '85%'], []);
const toggleFilter = (category: keyof AuthorFilters, value: string) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
const currentFilters = filters[category];
const newFilters = currentFilters.includes(value)
? currentFilters.filter(v => v !== value)
: [...currentFilters, value];
onFiltersChange({
...filters,
[category]: newFilters
});
};
const hasActiveFilters = Object.values(filters).some(arr => arr.length > 0);
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
opacity={0.5}
/>
),
[]
);
const renderFilterChip = (
category: keyof AuthorFilters,
option: { key: string; label: string; description?: string }
) => {
const isActive = filters[category].includes(option.key);
return (
<Pressable
key={option.key}
onPress={() => toggleFilter(category, option.key)}
style={[
styles.chip,
{
backgroundColor: isActive
? 'rgba(124, 58, 237, 0.2)'
: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)',
borderWidth: isActive ? 1.5 : 1,
borderColor: isActive
? '#7c3aed'
: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
}
]}
>
<View style={styles.chipContent}>
{isActive && (
<Icon
name="checkmark-circle"
size={16}
color="#7c3aed"
style={{ marginRight: 6 }}
/>
)}
<Text
variant="bodySmall"
weight={isActive ? 'semibold' : 'medium'}
style={{ color: isActive ? '#7c3aed' : isDarkMode ? '#fff' : '#000' }}
>
{option.label}
</Text>
{option.description && (
<Text
variant="caption"
style={{
color: isDarkMode ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)',
marginLeft: 4
}}
>
{option.description}
</Text>
)}
</View>
</Pressable>
);
};
const renderFilterSection = (
title: string,
category: keyof AuthorFilters,
options: { key: string; label: string; description?: string }[]
) => (
<View style={styles.section}>
<Text
variant="label"
weight="semibold"
color="secondary"
style={styles.sectionTitle}
>
{title}
</Text>
<View style={styles.chipContainer}>
{options.map(option => renderFilterChip(category, option))}
</View>
</View>
);
return (
<BottomSheet
ref={bottomSheetRef}
index={-1}
snapPoints={snapPoints}
enablePanDownToClose={true}
enableOverDrag={false}
backdropComponent={renderBackdrop}
backgroundStyle={{
backgroundColor: isDarkMode ? '#1a1a1a' : '#ffffff',
}}
handleIndicatorStyle={{
backgroundColor: isDarkMode ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)',
width: 40,
height: 4,
}}
animateOnMount={true}
>
<View style={[styles.header, {
borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
}]}>
<View style={{ flex: 1 }}>
<Text variant="title" weight="bold" color="primary">
Filter
</Text>
{hasActiveFilters && (
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
onClearAll();
}}
style={{ marginTop: 4 }}
>
<Text variant="caption" style={{ color: '#ef4444' }}>
Alle zurücksetzen
</Text>
</Pressable>
)}
</View>
<Pressable
onPress={() => bottomSheetRef.current?.close()}
style={[styles.closeButton, {
backgroundColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)',
}]}
>
<Icon
name="close"
size={20}
color={isDarkMode ? '#fff' : '#000'}
/>
</Pressable>
</View>
<BottomSheetScrollView
contentContainerStyle={styles.content}
showsVerticalScrollIndicator={false}
>
{renderFilterSection('Zeitepoche', 'epochs', EPOCH_OPTIONS)}
{renderFilterSection('Beruf', 'professions', PROFESSION_OPTIONS)}
{renderFilterSection('Nationalität', 'nationalities', NATIONALITY_OPTIONS)}
{renderFilterSection('Anzahl Zitate', 'quoteCount', QUOTE_COUNT_OPTIONS)}
{renderFilterSection('Besondere', 'special', SPECIAL_OPTIONS)}
</BottomSheetScrollView>
</BottomSheet>
);
}
const styles = StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 8,
paddingBottom: 16,
borderBottomWidth: 1,
},
closeButton: {
width: 32,
height: 32,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
},
content: {
padding: 20,
paddingBottom: 40,
},
section: {
marginBottom: 24,
},
sectionTitle: {
marginBottom: 12,
marginLeft: 4,
},
chipContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
},
chip: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
marginRight: 8,
marginBottom: 8,
},
chipContent: {
flexDirection: 'row',
alignItems: 'center',
},
});

View file

@ -0,0 +1,278 @@
import React, { useState } from 'react';
import { View, ScrollView, Pressable, Modal } from 'react-native';
import Text from '~/components/Text';
import { Icon } from '~/components/Icon';
import { useIsDarkMode } from '~/store/settingsStore';
import * as Haptics from 'expo-haptics';
import { useTranslation } from 'react-i18next';
export interface AuthorFilters {
epochs: string[];
professions: string[];
nationalities: string[];
quoteCount: string[];
special: string[];
}
interface AuthorFilterSheetProps {
visible: boolean;
onClose: () => void;
filters: AuthorFilters;
onFiltersChange: (filters: AuthorFilters) => void;
onClearAll: () => void;
}
const EPOCH_OPTIONS = [
{ key: 'ancient', label: 'Antike', description: 'Vor 500' },
{ key: 'medieval', label: 'Mittelalter', description: '500-1500' },
{ key: 'earlyModern', label: 'Frühe Neuzeit', description: '1500-1800' },
{ key: '19th', label: '19. Jahrhundert', description: '1800-1900' },
{ key: '20th', label: '20. Jahrhundert', description: '1900-2000' },
{ key: '21st', label: '21. Jahrhundert', description: '2000+' },
{ key: 'living', label: 'Noch lebend', description: '' },
];
const PROFESSION_OPTIONS = [
{ key: 'philosopher', label: 'Philosoph' },
{ key: 'writer', label: 'Schriftsteller' },
{ key: 'scientist', label: 'Wissenschaftler' },
{ key: 'politician', label: 'Politiker' },
{ key: 'artist', label: 'Künstler' },
{ key: 'entrepreneur', label: 'Unternehmer' },
{ key: 'poet', label: 'Dichter' },
{ key: 'activist', label: 'Aktivist' },
];
const NATIONALITY_OPTIONS = [
{ key: 'german', label: 'Deutsch' },
{ key: 'american', label: 'Amerikanisch' },
{ key: 'british', label: 'Britisch' },
{ key: 'french', label: 'Französisch' },
{ key: 'italian', label: 'Italienisch' },
{ key: 'spanish', label: 'Spanisch' },
{ key: 'greek', label: 'Griechisch' },
{ key: 'roman', label: 'Römisch' },
];
const QUOTE_COUNT_OPTIONS = [
{ key: 'few', label: 'Wenige', description: '1-5' },
{ key: 'medium', label: 'Mittel', description: '6-15' },
{ key: 'many', label: 'Viele', description: '16-50' },
{ key: 'verymany', label: 'Sehr viele', description: '50+' },
];
const SPECIAL_OPTIONS = [
{ key: 'verified', label: 'Verifiziert' },
{ key: 'featured', label: 'Featured' },
{ key: 'hasImage', label: 'Mit Bild' },
{ key: 'hasBio', label: 'Mit Biografie' },
];
export function AuthorFilterSheet({
visible,
onClose,
filters,
onFiltersChange,
onClearAll
}: AuthorFilterSheetProps) {
const { t } = useTranslation();
const isDarkMode = useIsDarkMode();
const toggleFilter = (category: keyof AuthorFilters, value: string) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
const currentFilters = filters[category];
const newFilters = currentFilters.includes(value)
? currentFilters.filter(v => v !== value)
: [...currentFilters, value];
onFiltersChange({
...filters,
[category]: newFilters
});
};
const hasActiveFilters = Object.values(filters).some(arr => arr.length > 0);
const renderFilterChip = (
category: keyof AuthorFilters,
option: { key: string; label: string; description?: string }
) => {
const isActive = filters[category].includes(option.key);
return (
<Pressable
key={option.key}
onPress={() => toggleFilter(category, option.key)}
style={{
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
backgroundColor: isActive
? 'rgba(124, 58, 237, 0.2)'
: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)',
borderWidth: isActive ? 1.5 : 1,
borderColor: isActive
? '#7c3aed'
: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
marginRight: 8,
marginBottom: 8,
}}
>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{isActive && (
<Icon
name="checkmark-circle"
size={16}
color="#7c3aed"
style={{ marginRight: 6 }}
/>
)}
<Text
variant="bodySmall"
weight={isActive ? 'semibold' : 'medium'}
style={{ color: isActive ? '#7c3aed' : isDarkMode ? '#fff' : '#000' }}
>
{option.label}
</Text>
{option.description && (
<Text
variant="caption"
style={{
color: isDarkMode ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)',
marginLeft: 4
}}
>
{option.description}
</Text>
)}
</View>
</Pressable>
);
};
const renderFilterSection = (
title: string,
category: keyof AuthorFilters,
options: { key: string; label: string; description?: string }[]
) => (
<View style={{ marginBottom: 24 }}>
<Text
variant="label"
weight="semibold"
color="secondary"
style={{ marginBottom: 12, marginLeft: 4 }}
>
{title}
</Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
{options.map(option => renderFilterChip(category, option))}
</View>
</View>
);
return (
<Modal
visible={visible}
transparent={true}
animationType="slide"
onRequestClose={onClose}
>
<View style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }}>
<Pressable
style={{ flex: 1 }}
onPress={onClose}
/>
<View
style={{
maxHeight: '85%',
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
backgroundColor: isDarkMode ? '#1a1a1a' : '#ffffff',
paddingBottom: 0,
}}
>
{/* Drag Handle */}
<View style={{
alignItems: 'center',
paddingTop: 12,
paddingBottom: 8,
}}>
<View style={{
width: 40,
height: 4,
borderRadius: 2,
backgroundColor: isDarkMode ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)',
}} />
</View>
{/* Header */}
<View style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 8,
paddingBottom: 16,
borderBottomWidth: 1,
borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
}}>
<View style={{ flex: 1 }}>
<Text variant="title" weight="bold" color="primary">
Filter
</Text>
{hasActiveFilters && (
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
onClearAll();
}}
style={{ marginTop: 4 }}
>
<Text variant="caption" style={{ color: '#ef4444' }}>
Alle zurücksetzen
</Text>
</Pressable>
)}
</View>
<Pressable
onPress={onClose}
style={{
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Icon
name="close"
size={20}
color={isDarkMode ? '#fff' : '#000'}
/>
</Pressable>
</View>
{/* Filter Content */}
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{
padding: 20,
paddingBottom: 20,
}}
showsVerticalScrollIndicator={false}
>
{renderFilterSection('Zeitepoche', 'epochs', EPOCH_OPTIONS)}
{renderFilterSection('Beruf', 'professions', PROFESSION_OPTIONS)}
{renderFilterSection('Nationalität', 'nationalities', NATIONALITY_OPTIONS)}
{renderFilterSection('Anzahl Zitate', 'quoteCount', QUOTE_COUNT_OPTIONS)}
{renderFilterSection('Besondere', 'special', SPECIAL_OPTIONS)}
</ScrollView>
</View>
</View>
</Modal>
);
}

View file

@ -0,0 +1,76 @@
import React from 'react';
import { ViewStyle, TextStyle } from 'react-native';
import { Button as ExpoButton, Host } from '@expo/ui/swift-ui';
import * as Haptics from 'expo-haptics';
import { useIsDarkMode } from '~/store/settingsStore';
export type ButtonVariant = 'default' | 'bordered' | 'borderless';
export type ButtonSize = 'small' | 'medium' | 'large';
interface ButtonProps {
children: React.ReactNode;
onPress: () => void;
variant?: ButtonVariant;
size?: ButtonSize;
disabled?: boolean;
style?: ViewStyle;
className?: string;
hapticFeedback?: boolean;
}
export const Button: React.FC<ButtonProps> = ({
children,
onPress,
variant = 'default',
size = 'medium',
disabled = false,
style,
className = '',
hapticFeedback = true
}) => {
const isDarkMode = useIsDarkMode();
const handlePress = () => {
if (disabled) return;
if (hapticFeedback) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
onPress();
};
// Determine host size based on size prop
const getHostStyle = (): ViewStyle => {
const baseStyle: ViewStyle = {
...style,
};
switch (size) {
case 'small':
return { ...baseStyle, minHeight: 32 };
case 'medium':
return { ...baseStyle, minHeight: 44 };
case 'large':
return { ...baseStyle, minHeight: 56 };
default:
return baseStyle;
}
};
return (
<Host
matchContents
style={getHostStyle()}
className={className}
>
<ExpoButton
onPress={handlePress}
variant={variant}
>
{children}
</ExpoButton>
</Host>
);
};
export default Button;

View file

@ -0,0 +1,115 @@
import React, { useState } from 'react';
import { Pressable } from 'react-native';
import { Icon } from '~/components/Icon';
import * as Haptics from 'expo-haptics';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
withSequence
} from 'react-native-reanimated';
import { useIsDarkMode } from '~/store/settingsStore';
import usePremiumStore from '~/store/premiumStore';
import { PremiumLimitDialog } from '~/components/PremiumLimitDialog';
interface FavoriteButtonProps {
isFavorite: boolean;
onToggle: () => void;
size?: number;
variant?: 'default' | 'daily';
className?: string;
}
export default function FavoriteButton({
isFavorite,
onToggle,
size = 24,
variant = 'default',
className = ''
}: FavoriteButtonProps) {
const isDarkMode = useIsDarkMode();
const scale = useSharedValue(1);
const heartScale = useSharedValue(1);
const [showLimitDialog, setShowLimitDialog] = useState(false);
const { canAddFavorite, addFavorite, getRemainingFavorites, MAX_DAILY_FAVORITES } = usePremiumStore();
const handlePress = () => {
// If removing favorite, always allow
if (isFavorite) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
scale.value = withSpring(1.2, { duration: 150 }, () => {
scale.value = withSpring(1, { duration: 150 });
});
onToggle();
return;
}
// Check if user can add favorite
if (!canAddFavorite()) {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
setShowLimitDialog(true);
return;
}
// Add favorite with limit tracking
if (addFavorite()) {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
// Animation sequence for adding favorite
heartScale.value = withSequence(
withSpring(1.4, { duration: 200 }),
withSpring(0.8, { duration: 150 }),
withSpring(1, { duration: 200 })
);
onToggle();
}
};
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }]
}));
const heartAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: heartScale.value }]
}));
const getColor = () => {
if (isFavorite) {
return variant === 'daily' ? '#ff6b6b' : '#ef4444';
}
if (variant === 'daily') {
return 'white';
}
return 'rgba(255,255,255,0.8)';
};
return (
<>
<Pressable
onPress={handlePress}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
className={variant === 'daily' ? "bg-white/10 p-2 rounded-full" : className}
>
<Animated.View style={isFavorite ? heartAnimatedStyle : animatedStyle}>
<Icon
name={isFavorite ? 'heart' : 'heart-outline'}
size={size}
color={getColor()}
/>
</Animated.View>
</Pressable>
<PremiumLimitDialog
visible={showLimitDialog}
onClose={() => setShowLimitDialog(false)}
limitType="favorites"
remaining={getRemainingFavorites()}
max={MAX_DAILY_FAVORITES}
/>
</>
);
}

View file

@ -0,0 +1,144 @@
import React from 'react';
import { View, Pressable, ViewStyle } from 'react-native';
import { BlurView } from 'expo-blur';
import { Icon } from '~/components/Icon';
import { useIsDarkMode } from '~/store/settingsStore';
import * as Haptics from 'expo-haptics';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
FadeIn,
ZoomIn
} from 'react-native-reanimated';
interface GlassFABProps {
onPress: () => void;
icon?: string;
size?: 'small' | 'medium' | 'large';
position?: 'bottom-right' | 'bottom-left' | 'bottom-center';
style?: ViewStyle;
disabled?: boolean;
}
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
export function GlassFAB({
onPress,
icon = 'add',
size = 'medium',
position = 'bottom-right',
style,
disabled = false
}: GlassFABProps) {
const isDarkMode = useIsDarkMode();
const scale = useSharedValue(1);
const rotation = useSharedValue(0);
// Size configurations
const sizes = {
small: { container: 49, icon: 24, blur: 70 },
medium: { container: 57, icon: 30, blur: 80 },
large: { container: 65, icon: 36, blur: 90 }
};
const currentSize = sizes[size];
// Position configurations
const positions = {
'bottom-right': 'bottom-28 right-7',
'bottom-left': 'bottom-28 left-7',
'bottom-center': 'bottom-28 left-1/2 -translate-x-1/2'
};
const handlePressIn = () => {
if (!disabled) {
scale.value = withSpring(0.92);
}
};
const handlePressOut = () => {
if (!disabled) {
scale.value = withSpring(1);
}
};
const handlePress = () => {
if (!disabled) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
rotation.value = withSpring(rotation.value + 90);
onPress();
}
};
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ scale: scale.value },
{ rotate: `${rotation.value}deg` }
]
}));
const opacityStyle = useAnimatedStyle(() => ({
opacity: disabled ? 0.5 : 1
}));
return (
<Animated.View
entering={FadeIn.delay(200).duration(400).springify()}
className={`absolute ${positions[position]}`}
style={[opacityStyle, style]}
>
<AnimatedPressable
onPressIn={handlePressIn}
onPressOut={handlePressOut}
onPress={handlePress}
style={animatedStyle}
disabled={disabled}
>
<View className="relative">
{/* Outer glass container with clean border */}
<View
className="rounded-full overflow-hidden shadow-xl"
style={{
width: currentSize.container,
height: currentSize.container,
borderWidth: 1,
borderColor: isDarkMode
? 'rgba(255, 255, 255, 0.2)'
: 'rgba(255, 255, 255, 0.8)',
}}
>
<BlurView
intensity={currentSize.blur}
tint={isDarkMode ? 'dark' : 'light'}
style={{ flex: 1 }}
/>
{/* Glass overlay - more subtle to show blur through */}
<View
className="absolute inset-0"
style={{
backgroundColor: isDarkMode
? 'rgba(255, 255, 255, 0.05)'
: 'rgba(255, 255, 255, 0.3)',
}}
/>
</View>
{/* Icon container - centered */}
<View
className="absolute inset-0 rounded-full items-center justify-center"
>
<Icon
name={icon}
size={currentSize.icon}
color={isDarkMode ? '#ffffff' : '#000000'}
/>
</View>
{/* Optional badge or notification dot */}
{/* Can be added here if needed */}
</View>
</AnimatedPressable>
</Animated.View>
);
}

View file

@ -0,0 +1,220 @@
import React, { useEffect } from 'react';
import { View, Pressable, Platform, Dimensions } from 'react-native';
import Text from '~/components/Text';
import * as Haptics from 'expo-haptics';
import { useIsDarkMode } from '~/store/settingsStore';
import Animated, {
FadeInDown,
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
interpolate,
Extrapolate,
runOnJS
} from 'react-native-reanimated';
import { BlurView } from 'expo-blur';
interface Tab {
key: string;
label: string;
count?: number;
}
interface GlassTabSelectorProps {
tabs: Tab[];
activeTab: string;
onTabChange: (tab: string) => void;
animationDelay?: number;
}
const { width: screenWidth } = Dimensions.get('window');
export function GlassTabSelector({ tabs, activeTab, onTabChange, animationDelay = 200 }: GlassTabSelectorProps) {
const isDarkMode = useIsDarkMode();
const isIOS = Platform.OS === 'ios';
// Animation values
const activeIndex = tabs.findIndex(tab => tab.key === activeTab);
const containerHorizontalPadding = 48; // px-6 = 24px on each side
const availableWidth = screenWidth - containerHorizontalPadding;
const tabWidth = availableWidth / tabs.length;
const translateX = useSharedValue(activeIndex * tabWidth);
const indicatorWidth = useSharedValue(tabWidth);
const indicatorOpacity = useSharedValue(1);
const isFirstRender = useSharedValue(true);
useEffect(() => {
// Calculate position for active tab (no extra offset needed)
const newPosition = activeIndex * tabWidth;
// Skip animation on first render
if (isFirstRender.value) {
translateX.value = newPosition;
indicatorWidth.value = tabWidth;
isFirstRender.value = false;
return;
}
// Morph effect: briefly fade out, move, then fade in
indicatorOpacity.value = withTiming(0.8, { duration: 100 }, () => {
translateX.value = withSpring(newPosition, {
damping: 20,
stiffness: 200,
mass: 0.8
});
indicatorOpacity.value = withTiming(1, { duration: 200 });
});
// Set indicator width to match tab width
indicatorWidth.value = withSpring(tabWidth, {
damping: 18,
stiffness: 180,
});
}, [activeIndex, tabWidth]);
const animatedIndicatorStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: translateX.value }],
width: indicatorWidth.value,
opacity: indicatorOpacity.value,
};
});
if (!isIOS) {
// Fallback to regular TabSelector on Android
const TabSelector = require('./TabSelector').TabSelector;
return <TabSelector tabs={tabs} activeTab={activeTab} onTabChange={onTabChange} animationDelay={animationDelay} />;
}
return (
<View>
{/* Glass Effect Container Background */}
<View className="px-6 mb-6">
<View className="relative">
{/* Blur Background Layer for entire container */}
<View className="absolute inset-0 rounded-full overflow-hidden">
<BlurView
intensity={30}
tint={isDarkMode ? 'dark' : 'light'}
style={{ flex: 1 }}
/>
{/* Glass overlay with subtle gradient */}
<View
className="absolute inset-0"
style={{
backgroundColor: isDarkMode
? 'rgba(255, 255, 255, 0.08)'
: 'rgba(255, 255, 255, 0.5)',
borderWidth: 0.5,
borderColor: isDarkMode
? 'rgba(255, 255, 255, 0.15)'
: 'rgba(255, 255, 255, 0.8)',
borderRadius: 999,
}}
/>
</View>
{/* Tab buttons container - no extra padding */}
<View className="relative">
{/* Animated active indicator that slides behind tabs */}
<Animated.View
style={[
{
position: 'absolute',
top: 0,
bottom: 0,
borderRadius: 999,
overflow: 'hidden',
},
animatedIndicatorStyle
]}
>
<BlurView
intensity={60}
tint={isDarkMode ? 'light' : 'dark'}
style={{ flex: 1 }}
/>
<View
className="absolute inset-0"
style={{
backgroundColor: isDarkMode
? 'rgba(255, 255, 255, 0.95)'
: 'rgba(0, 0, 0, 0.88)',
borderWidth: 0.5,
borderColor: isDarkMode
? 'rgba(255, 255, 255, 0.3)'
: 'rgba(0, 0, 0, 0.2)',
borderRadius: 999,
}}
/>
</Animated.View>
{/* Tab buttons - now in a flex row */}
<View className="flex-row">
{tabs.map((tab, index) => {
const isActive = activeTab === tab.key;
// Individual tab animations
const tabScale = useSharedValue(1);
const textOpacity = useSharedValue(isActive ? 1 : 0.7);
useEffect(() => {
textOpacity.value = withTiming(isActive ? 1 : 0.7, { duration: 300 });
}, [isActive]);
const animatedTabStyle = useAnimatedStyle(() => ({
transform: [{ scale: tabScale.value }],
}));
const animatedTextStyle = useAnimatedStyle(() => ({
opacity: textOpacity.value,
}));
return (
<Pressable
key={tab.key}
onPressIn={() => {
tabScale.value = withSpring(0.95);
}}
onPressOut={() => {
tabScale.value = withSpring(1);
}}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
onTabChange(tab.key);
}}
style={{ width: tabWidth }}
>
<Animated.View style={[{ position: 'relative' }, animatedTabStyle]}>
{/* Tab content - consistent padding for all tabs */}
<View className="py-2.5 px-4 rounded-full">
<Animated.Text
style={[
{
textAlign: 'center',
fontWeight: '500',
fontSize: 15,
color: isActive
? (isDarkMode ? '#000000' : '#FFFFFF')
: (isDarkMode ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.6)')
},
animatedTextStyle
]}
>
{tab.label}
{tab.count !== undefined && ` (${tab.count})`}
</Animated.Text>
</View>
</Animated.View>
</Pressable>
);
})}
</View>
</View>
</View>
</View>
</View>
);
}

View file

@ -0,0 +1,98 @@
import React from 'react';
import { View } from 'react-native';
import { useRouter } from 'expo-router';
import { IconButton } from '~/components/common/IconButton';
import Text from '~/components/Text';
import { useIsDarkMode } from '~/store/settingsStore';
interface HeaderProps {
title: string;
subtitle?: string;
showBackButton?: boolean;
showSettings?: boolean;
showScrollToggle?: boolean;
showSortToggle?: boolean;
sortBy?: 'name' | 'quotes';
onSortToggle?: () => void;
rightActions?: React.ReactNode;
onBackPress?: () => void;
}
export const Header: React.FC<HeaderProps> = ({
title,
subtitle,
showBackButton = false,
showSettings = false,
showScrollToggle = false,
showSortToggle = false,
sortBy,
onSortToggle,
rightActions,
onBackPress
}) => {
const router = useRouter();
const isDarkMode = useIsDarkMode();
const handleBack = () => {
if (onBackPress) {
onBackPress();
} else {
router.back();
}
};
const handleSettings = () => {
router.push('/settings');
};
return (
<View className="px-6 py-4">
<View className="flex-row justify-between items-center">
<View className="flex-1 flex-row items-center">
{showBackButton && (
<IconButton
icon="arrow-back"
onPress={handleBack}
className="mr-3"
/>
)}
<View className="flex-1">
<Text variant="h2" color="primary" weight="bold">
{title}
</Text>
{subtitle && (
<Text variant="bodySmall" color="secondary" className="mt-1">
{subtitle}
</Text>
)}
</View>
</View>
{/* Right Actions */}
{(rightActions || showSortToggle || showScrollToggle || showSettings) && (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
{rightActions}
{showSortToggle && (
<IconButton
icon={sortBy === 'name' ? 'text-outline' : 'stats-chart-outline'}
onPress={() => onSortToggle?.()}
size={20}
/>
)}
{showSettings && (
<IconButton
icon="settings-outline"
onPress={handleSettings}
size={22}
/>
)}
</View>
)}
</View>
</View>
);
};

View file

@ -0,0 +1,49 @@
import React from 'react';
import { Pressable } from 'react-native';
import { Icon } from '~/components/Icon';
import * as Haptics from 'expo-haptics';
import { useIsDarkMode } from '~/store/settingsStore';
interface IconButtonProps {
icon: string;
size?: number;
onPress: () => void;
className?: string;
isActive?: boolean;
}
export const IconButton: React.FC<IconButtonProps> = ({
icon,
size = 22,
onPress,
className = '',
isActive = false
}) => {
const isDarkMode = useIsDarkMode();
const handlePress = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
onPress();
};
return (
<Pressable
onPress={handlePress}
style={{
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
justifyContent: 'center',
alignItems: 'center'
}}
className={className}
>
<Icon
name={icon}
size={size}
color={isDarkMode ? 'white' : 'black'}
/>
</Pressable>
);
};

View file

@ -0,0 +1,28 @@
import React from 'react';
import { View, ActivityIndicator } from 'react-native';
import Text from '../Text';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useTranslation } from 'react-i18next';
interface LoadingScreenProps {
message?: string;
fullScreen?: boolean;
}
export const LoadingScreen: React.FC<LoadingScreenProps> = ({
message,
fullScreen = true,
}) => {
const { t } = useTranslation();
const Container = fullScreen ? SafeAreaView : View;
const displayMessage = message || t('common.loading');
return (
<Container className="flex-1 bg-black">
<View className="flex-1 items-center justify-center">
<ActivityIndicator size="large" color="#ffffff" />
<Text variant="body" color="tertiary" className="mt-4">{displayMessage}</Text>
</View>
</Container>
);
};

View file

@ -0,0 +1,168 @@
import React, { useEffect } from 'react';
import { View, Pressable, Dimensions } from 'react-native';
import Text from '~/components/Text';
import * as Haptics from 'expo-haptics';
import { useIsDarkMode } from '~/store/settingsStore';
import Animated, {
FadeInDown,
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
interpolateColor
} from 'react-native-reanimated';
interface Tab {
key: string;
label: string;
count?: number;
}
interface TabSelectorProps {
tabs: Tab[];
activeTab: string;
onTabChange: (tab: string) => void;
animationDelay?: number;
}
const { width: screenWidth } = Dimensions.get('window');
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
export function TabSelector({ tabs, activeTab, onTabChange, animationDelay = 200 }: TabSelectorProps) {
const isDarkMode = useIsDarkMode();
// Animation values for sliding indicator
const activeIndex = tabs.findIndex(tab => tab.key === activeTab);
const translateX = useSharedValue(0);
const indicatorWidth = useSharedValue(0);
// Calculate tab width
const containerPadding = 24 * 2; // px-6 on both sides
const spacing = 8; // ml-2 between tabs
const availableWidth = screenWidth - containerPadding - (spacing * (tabs.length - 1));
const tabWidth = availableWidth / tabs.length;
useEffect(() => {
// Calculate position including spacing
const spacingOffset = activeIndex * spacing;
const newPosition = (activeIndex * tabWidth) + spacingOffset;
// Animate to new position with spring
translateX.value = withSpring(newPosition, {
damping: 20,
stiffness: 200,
mass: 0.8
});
// Set width
indicatorWidth.value = withSpring(tabWidth, {
damping: 18,
stiffness: 180,
});
}, [activeIndex, tabWidth]);
const animatedIndicatorStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: translateX.value }],
width: indicatorWidth.value,
};
});
return (
<Animated.View entering={FadeInDown.delay(animationDelay).duration(600)}>
<View className="px-6 mb-6">
<View className="relative">
{/* Animated background indicator */}
<Animated.View
style={[
{
position: 'absolute',
height: '100%',
borderRadius: 999,
backgroundColor: isDarkMode ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)',
zIndex: 0,
},
animatedIndicatorStyle
]}
/>
{/* Tab buttons */}
<View className="flex-row">
{tabs.map((tab, index) => {
const isActive = activeTab === tab.key;
// Individual tab animations
const scale = useSharedValue(1);
const backgroundOpacity = useSharedValue(isActive ? 0 : 1);
const textOpacity = useSharedValue(1);
useEffect(() => {
backgroundOpacity.value = withTiming(isActive ? 0 : 1, { duration: 300 });
textOpacity.value = withTiming(isActive ? 1 : 0.8, { duration: 300 });
}, [isActive]);
const animatedTabStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const animatedBackgroundStyle = useAnimatedStyle(() => ({
opacity: backgroundOpacity.value,
}));
const animatedTextContainerStyle = useAnimatedStyle(() => ({
opacity: textOpacity.value,
}));
return (
<Animated.View
key={tab.key}
className="flex-1"
style={{
marginLeft: index > 0 ? 8 : 0,
zIndex: 1,
}}
>
<AnimatedPressable
onPressIn={() => {
scale.value = withSpring(0.95);
}}
onPressOut={() => {
scale.value = withSpring(1);
}}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
onTabChange(tab.key);
}}
className="py-3 rounded-full relative"
style={animatedTabStyle}
>
{/* Tab background (hidden when active) */}
<Animated.View
className={`absolute inset-0 rounded-full ${
isDarkMode ? 'bg-white/10' : 'bg-black/10'
}`}
style={animatedBackgroundStyle}
/>
{/* Tab content */}
<Animated.View style={animatedTextContainerStyle}>
<Text
variant="body"
color={isActive ? 'primary' : 'secondary'}
weight="medium"
className="text-center"
>
{tab.label}
{tab.count !== undefined && ` (${tab.count})`}
</Text>
</Animated.View>
</AnimatedPressable>
</Animated.View>
);
})}
</View>
</View>
</View>
</Animated.View>
);
}

View file

@ -0,0 +1,479 @@
import React, { useState, useRef } from 'react';
import {
View,
Text,
ScrollView,
Dimensions,
TouchableOpacity,
StyleSheet,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { BlurView } from 'expo-blur';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
interpolate,
Extrapolate,
withTiming,
FadeIn,
} from 'react-native-reanimated';
import * as Haptics from 'expo-haptics';
import { useRouter } from 'expo-router';
import { useOnboardingStore } from '~/store/onboardingStore';
import { useIsDarkMode } from '~/store/settingsStore';
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
interface OnboardingPage {
icon: string;
iconColor: string;
title: string;
subtitle: string;
description: string;
gradientColors: string[];
}
const onboardingPages: OnboardingPage[] = [
{
icon: 'sparkles',
iconColor: '#FFD700',
title: 'Willkommen bei Zitare',
subtitle: 'Entdecke tägliche Inspiration',
description: 'Über 23.000 handverlesene Zitate von den größten Denkern der Geschichte.',
gradientColors: ['#667eea', '#764ba2'],
},
{
icon: 'heart',
iconColor: '#FF6B6B',
title: 'Sammle deine Favoriten',
subtitle: 'Behalte, was dich bewegt',
description: 'Speichere Zitate, die dich inspirieren, und greife jederzeit darauf zu.',
gradientColors: ['#f093fb', '#f5576c'],
},
{
icon: 'albums',
iconColor: '#4ECDC4',
title: 'Erstelle Sammlungen',
subtitle: 'Organisiere deine Weisheiten',
description: 'Gruppiere Zitate nach Themen, Stimmungen oder eigenen Kategorien.',
gradientColors: ['#4facfe', '#00f2fe'],
},
{
icon: 'people',
iconColor: '#9B59B6',
title: 'Entdecke Autoren',
subtitle: 'Lerne die Denker kennen',
description: 'Tauche ein in die Biografien und Gedankenwelten großer Persönlichkeiten.',
gradientColors: ['#667eea', '#764ba2'],
},
{
icon: 'phone-portrait-outline',
iconColor: '#3498DB',
title: 'Widget & Personalisierung',
subtitle: 'Dein tägliches Zitat',
description: 'Erhalte inspirierende Zitate direkt auf deinem Home-Bildschirm.',
gradientColors: ['#6dd5ed', '#2193b0'],
},
];
export default function AppleStyleOnboarding() {
const [currentPage, setCurrentPage] = useState(0);
const scrollViewRef = useRef<ScrollView>(null);
const scrollX = useSharedValue(0);
const router = useRouter();
const isDarkMode = useIsDarkMode();
const { completeOnboarding } = useOnboardingStore();
const handleScroll = (event: any) => {
const offsetX = event.nativeEvent.contentOffset.x;
scrollX.value = offsetX;
const pageIndex = Math.round(offsetX / SCREEN_WIDTH);
if (pageIndex !== currentPage) {
setCurrentPage(pageIndex);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
};
const handleContinue = () => {
if (currentPage < onboardingPages.length - 1) {
const nextPage = currentPage + 1;
scrollViewRef.current?.scrollTo({
x: nextPage * SCREEN_WIDTH,
animated: true,
});
setCurrentPage(nextPage);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} else {
handleComplete();
}
};
const handleComplete = async () => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
completeOnboarding();
router.replace('/(tabs)/');
};
const handleSkip = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
handleComplete();
};
return (
<View style={styles.container}>
{/* Background Gradient */}
<LinearGradient
colors={isDarkMode ? ['#000000', '#1a1a1a'] : ['#ffffff', '#f0f0f0']}
style={StyleSheet.absoluteFillObject}
/>
<SafeAreaView style={styles.safeArea}>
{/* Skip Button */}
<Animated.View
entering={FadeIn.delay(500)}
style={styles.skipContainer}
>
<TouchableOpacity onPress={handleSkip} style={styles.skipButton}>
<Text style={[
styles.skipText,
{ color: isDarkMode ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)' }
]}>
Überspringen
</Text>
</TouchableOpacity>
</Animated.View>
{/* Pages */}
<ScrollView
ref={scrollViewRef}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
onScroll={handleScroll}
scrollEventThrottle={16}
bounces={false}
>
{onboardingPages.map((page, index) => (
<OnboardingPageComponent
key={index}
page={page}
index={index}
scrollX={scrollX}
isDarkMode={isDarkMode}
/>
))}
</ScrollView>
{/* Bottom Controls */}
<View style={styles.bottomContainer}>
{/* Page Indicators */}
<View style={styles.indicatorContainer}>
{onboardingPages.map((_, index) => (
<PageIndicator
key={index}
index={index}
currentPage={currentPage}
scrollX={scrollX}
isDarkMode={isDarkMode}
/>
))}
</View>
{/* Continue Button */}
<TouchableOpacity
onPress={handleContinue}
style={styles.continueButton}
activeOpacity={0.8}
>
<BlurView
intensity={80}
tint={isDarkMode ? 'dark' : 'light'}
style={styles.continueButtonBlur}
>
<Text style={[
styles.continueButtonText,
{ color: isDarkMode ? '#ffffff' : '#000000' }
]}>
{currentPage === onboardingPages.length - 1 ? 'Los geht\'s' : 'Weiter'}
</Text>
</BlurView>
</TouchableOpacity>
</View>
</SafeAreaView>
</View>
);
}
// Individual Page Component
function OnboardingPageComponent({
page,
index,
scrollX,
isDarkMode,
}: {
page: OnboardingPage;
index: number;
scrollX: any;
isDarkMode: boolean;
}) {
const inputRange = [
(index - 1) * SCREEN_WIDTH,
index * SCREEN_WIDTH,
(index + 1) * SCREEN_WIDTH,
];
const animatedStyle = useAnimatedStyle(() => {
const scale = interpolate(
scrollX.value,
inputRange,
[0.8, 1, 0.8],
Extrapolate.CLAMP
);
const opacity = interpolate(
scrollX.value,
inputRange,
[0.5, 1, 0.5],
Extrapolate.CLAMP
);
const translateY = interpolate(
scrollX.value,
inputRange,
[50, 0, 50],
Extrapolate.CLAMP
);
return {
transform: [{ scale }, { translateY }],
opacity,
};
});
const iconAnimatedStyle = useAnimatedStyle(() => {
const rotate = interpolate(
scrollX.value,
inputRange,
[-15, 0, 15],
Extrapolate.CLAMP
);
const scale = interpolate(
scrollX.value,
inputRange,
[0.8, 1, 0.8],
Extrapolate.CLAMP
);
return {
transform: [
{ rotate: `${rotate}deg` },
{ scale },
],
};
});
return (
<View style={styles.pageContainer}>
<Animated.View style={[styles.contentContainer, animatedStyle]}>
{/* Icon with Gradient Background */}
<View style={styles.iconWrapper}>
<LinearGradient
colors={page.gradientColors}
style={styles.iconGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Animated.View style={iconAnimatedStyle}>
<Ionicons
name={page.icon as any}
size={60}
color="#ffffff"
/>
</Animated.View>
</LinearGradient>
</View>
{/* Title */}
<Text style={[
styles.title,
{ color: isDarkMode ? '#ffffff' : '#000000' }
]}>
{page.title}
</Text>
{/* Subtitle */}
<Text style={[
styles.subtitle,
{ color: isDarkMode ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)' }
]}>
{page.subtitle}
</Text>
{/* Description */}
<Text style={[
styles.description,
{ color: isDarkMode ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)' }
]}>
{page.description}
</Text>
</Animated.View>
</View>
);
}
// Page Indicator Component
function PageIndicator({
index,
currentPage,
scrollX,
isDarkMode,
}: {
index: number;
currentPage: number;
scrollX: any;
isDarkMode: boolean;
}) {
const animatedStyle = useAnimatedStyle(() => {
const inputRange = [
(index - 1) * SCREEN_WIDTH,
index * SCREEN_WIDTH,
(index + 1) * SCREEN_WIDTH,
];
const width = interpolate(
scrollX.value,
inputRange,
[8, 24, 8],
Extrapolate.CLAMP
);
const opacity = interpolate(
scrollX.value,
inputRange,
[0.3, 1, 0.3],
Extrapolate.CLAMP
);
return {
width,
opacity,
};
});
return (
<Animated.View
style={[
styles.indicator,
animatedStyle,
{
backgroundColor: isDarkMode ? '#ffffff' : '#000000',
},
]}
/>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
safeArea: {
flex: 1,
},
skipContainer: {
position: 'absolute',
top: 60,
right: 20,
zIndex: 10,
},
skipButton: {
padding: 10,
},
skipText: {
fontSize: 17,
fontWeight: '400',
},
pageContainer: {
width: SCREEN_WIDTH,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 40,
},
contentContainer: {
alignItems: 'center',
justifyContent: 'center',
},
iconWrapper: {
marginBottom: 40,
},
iconGradient: {
width: 120,
height: 120,
borderRadius: 30,
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.2,
shadowRadius: 20,
elevation: 10,
},
title: {
fontSize: 32,
fontWeight: '700',
textAlign: 'center',
marginBottom: 16,
letterSpacing: -0.5,
},
subtitle: {
fontSize: 22,
fontWeight: '600',
textAlign: 'center',
marginBottom: 20,
},
description: {
fontSize: 17,
fontWeight: '400',
textAlign: 'center',
lineHeight: 24,
paddingHorizontal: 20,
},
bottomContainer: {
position: 'absolute',
bottom: 50,
left: 0,
right: 0,
alignItems: 'center',
},
indicatorContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 30,
gap: 8,
},
indicator: {
height: 8,
borderRadius: 4,
backgroundColor: '#000000',
},
continueButton: {
borderRadius: 30,
overflow: 'hidden',
},
continueButtonBlur: {
paddingHorizontal: 32,
paddingVertical: 16,
borderRadius: 30,
borderWidth: 0.5,
borderColor: 'rgba(255,255,255,0.2)',
},
continueButtonText: {
fontSize: 17,
fontWeight: '600',
letterSpacing: -0.2,
},
});

View file

@ -0,0 +1,162 @@
import React from 'react';
import { View, ScrollView, Pressable } from 'react-native';
import Text from '~/components/Text';
import { Icon } from '~/components/Icon';
import { useIsDarkMode } from '~/store/settingsStore';
import * as Haptics from 'expo-haptics';
import { QuoteFilters } from '~/utils/quoteFilters';
import { useTranslation } from 'react-i18next';
interface ActiveQuoteFilterChipsProps {
filters: QuoteFilters;
onRemoveFilter: (category: keyof QuoteFilters, value: string) => void;
onClearAll: () => void;
}
// Label mappings for filter values
const FILTER_LABELS: Record<string, string> = {
// Time Periods
ancient: 'Antike',
medieval: 'Mittelalter',
earlyModern: 'Frühe Neuzeit',
'19th': '19. Jh.',
'early20th': 'Frühes 20. Jh.',
'late20th': 'Spätes 20. Jh.',
'21st': '21. Jh.',
// Source Types
books: 'Bücher',
letters: 'Briefe',
speeches: 'Reden',
interviews: 'Interviews',
attributed: 'Zugeschrieben',
folkWisdom: 'Volksweisheit',
verified: 'Verifiziert',
// Categories
wisdom: 'Weisheit',
motivation: 'Motivation',
love: 'Liebe',
science: 'Wissenschaft',
philosophy: 'Philosophie',
humor: 'Humor',
success: 'Erfolg',
change: 'Veränderung',
creativity: 'Kreativität',
courage: 'Mut',
happiness: 'Glück',
life: 'Leben',
// Special
featured: 'Featured',
hasYear: 'Mit Jahr',
hasSource: 'Mit Quelle',
long: 'Lang',
short: 'Kurz',
};
export function ActiveQuoteFilterChips({
filters,
onRemoveFilter,
onClearAll
}: ActiveQuoteFilterChipsProps) {
const { t } = useTranslation();
const isDarkMode = useIsDarkMode();
// Collect all active filters
const activeFilters: Array<{ category: keyof QuoteFilters; value: string; label: string }> = [];
Object.entries(filters).forEach(([category, values]) => {
values.forEach(value => {
activeFilters.push({
category: category as keyof QuoteFilters,
value,
label: FILTER_LABELS[value] || value
});
});
});
if (activeFilters.length === 0) {
return null;
}
return (
<View style={{
paddingHorizontal: 16,
paddingTop: 110, // Account for header
paddingBottom: 8,
backgroundColor: isDarkMode ? '#000' : '#fff',
}}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
flexDirection: 'row',
alignItems: 'center',
gap: 8,
}}
>
{/* Active filter chips */}
{activeFilters.map((filter, index) => (
<Pressable
key={`${filter.category}-${filter.value}-${index}`}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
onRemoveFilter(filter.category, filter.value);
}}
style={{
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 12,
paddingRight: 8,
paddingVertical: 6,
borderRadius: 16,
backgroundColor: 'rgba(124, 58, 237, 0.15)',
borderWidth: 1,
borderColor: 'rgba(124, 58, 237, 0.3)',
}}
>
<Text
variant="caption"
weight="medium"
style={{ color: '#7c3aed', marginRight: 4 }}
>
{filter.label}
</Text>
<Icon
name="close-circle"
size={16}
color="#7c3aed"
/>
</Pressable>
))}
{/* Clear all button */}
{activeFilters.length > 1 && (
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
onClearAll();
}}
style={{
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
backgroundColor: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
borderWidth: 1,
borderColor: isDarkMode ? 'rgba(239, 68, 68, 0.3)' : 'rgba(239, 68, 68, 0.2)',
}}
>
<Text
variant="caption"
weight="semibold"
style={{ color: '#ef4444' }}
>
{t('common.clearAll')}
</Text>
</Pressable>
)}
</ScrollView>
</View>
);
}

View file

@ -0,0 +1,296 @@
import React, { useMemo, useCallback } from 'react';
import { View, Pressable, StyleSheet } from 'react-native';
import BottomSheet, { BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
import Text from '~/components/Text';
import { Icon } from '~/components/Icon';
import { useIsDarkMode } from '~/store/settingsStore';
import * as Haptics from 'expo-haptics';
import { useTranslation } from 'react-i18next';
import { QuoteFilters } from '~/utils/quoteFilters';
import type { BottomSheetBackdropProps } from '@gorhom/bottom-sheet';
interface QuoteFilterSheetProps {
bottomSheetRef: React.RefObject<BottomSheet>;
filters: QuoteFilters;
onFiltersChange: (filters: QuoteFilters) => void;
onClearAll: () => void;
}
const TIME_PERIOD_OPTIONS = [
{ key: 'ancient', label: 'Antike', description: '< 0' },
{ key: 'medieval', label: 'Mittelalter', description: '0-1500' },
{ key: 'earlyModern', label: 'Frühe Neuzeit', description: '1500-1800' },
{ key: '19th', label: '19. Jh.', description: '1800-1900' },
{ key: 'early20th', label: 'Frühes 20. Jh.', description: '1900-1950' },
{ key: 'late20th', label: 'Spätes 20. Jh.', description: '1950-2000' },
{ key: '21st', label: '21. Jh.', description: '2000+' },
];
const SOURCE_TYPE_OPTIONS = [
{ key: 'books', label: 'Bücher/Werke' },
{ key: 'letters', label: 'Briefe' },
{ key: 'speeches', label: 'Reden' },
{ key: 'interviews', label: 'Interviews' },
{ key: 'attributed', label: 'Zugeschrieben' },
{ key: 'folkWisdom', label: 'Volksweisheit' },
{ key: 'verified', label: 'Verifiziert' },
];
const CATEGORY_OPTIONS = [
{ key: 'wisdom', label: 'Weisheit' },
{ key: 'motivation', label: 'Motivation' },
{ key: 'love', label: 'Liebe' },
{ key: 'science', label: 'Wissenschaft' },
{ key: 'philosophy', label: 'Philosophie' },
{ key: 'humor', label: 'Humor' },
{ key: 'success', label: 'Erfolg' },
{ key: 'change', label: 'Veränderung' },
{ key: 'creativity', label: 'Kreativität' },
{ key: 'courage', label: 'Mut' },
{ key: 'happiness', label: 'Glück' },
{ key: 'life', label: 'Leben' },
];
const AUTHOR_ERA_OPTIONS = [
{ key: 'ancient', label: 'Antike' },
{ key: 'medieval', label: 'Mittelalter' },
{ key: 'earlyModern', label: 'Frühe Neuzeit' },
{ key: '19th', label: '19. Jahrhundert' },
{ key: '20th', label: '20. Jahrhundert' },
{ key: '21st', label: '21. Jahrhundert' },
];
const SPECIAL_OPTIONS = [
{ key: 'featured', label: 'Featured' },
{ key: 'hasYear', label: 'Mit Jahreszahl' },
{ key: 'hasSource', label: 'Mit Quelle' },
{ key: 'verified', label: 'Verifiziert' },
{ key: 'long', label: 'Lang (>150 Zeichen)' },
{ key: 'short', label: 'Kurz (<100 Zeichen)' },
];
export function QuoteFilterSheet({
bottomSheetRef,
filters,
onFiltersChange,
onClearAll
}: QuoteFilterSheetProps) {
const { t } = useTranslation();
const isDarkMode = useIsDarkMode();
const snapPoints = useMemo(() => ['65%', '85%'], []);
const toggleFilter = (category: keyof QuoteFilters, value: string) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
const currentFilters = filters[category];
const newFilters = currentFilters.includes(value)
? currentFilters.filter(v => v !== value)
: [...currentFilters, value];
onFiltersChange({
...filters,
[category]: newFilters
});
};
const hasActiveFilters = Object.values(filters).some(arr => arr.length > 0);
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
opacity={0.5}
/>
),
[]
);
const renderFilterChip = (
category: keyof QuoteFilters,
option: { key: string; label: string; description?: string }
) => {
const isActive = filters[category].includes(option.key);
return (
<Pressable
key={option.key}
onPress={() => toggleFilter(category, option.key)}
style={[
styles.chip,
{
backgroundColor: isActive
? 'rgba(124, 58, 237, 0.2)'
: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)',
borderWidth: isActive ? 1.5 : 1,
borderColor: isActive
? '#7c3aed'
: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
}
]}
>
<View style={styles.chipContent}>
{isActive && (
<Icon
name="checkmark-circle"
size={16}
color="#7c3aed"
style={{ marginRight: 6 }}
/>
)}
<Text
variant="bodySmall"
weight={isActive ? 'semibold' : 'medium'}
style={{ color: isActive ? '#7c3aed' : isDarkMode ? '#fff' : '#000' }}
>
{option.label}
</Text>
{option.description && (
<Text
variant="caption"
style={{
color: isDarkMode ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)',
marginLeft: 4
}}
>
{option.description}
</Text>
)}
</View>
</Pressable>
);
};
const renderFilterSection = (
title: string,
category: keyof QuoteFilters,
options: { key: string; label: string; description?: string }[]
) => (
<View style={styles.section}>
<Text
variant="label"
weight="semibold"
color="secondary"
style={styles.sectionTitle}
>
{title}
</Text>
<View style={styles.chipContainer}>
{options.map(option => renderFilterChip(category, option))}
</View>
</View>
);
return (
<BottomSheet
ref={bottomSheetRef}
index={-1}
snapPoints={snapPoints}
enablePanDownToClose={true}
enableOverDrag={false}
backdropComponent={renderBackdrop}
backgroundStyle={{
backgroundColor: isDarkMode ? '#1a1a1a' : '#ffffff',
}}
handleIndicatorStyle={{
backgroundColor: isDarkMode ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)',
width: 40,
height: 4,
}}
animateOnMount={true}
>
<View style={[styles.header, {
borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
}]}>
<View style={{ flex: 1 }}>
<Text variant="title" weight="bold" color="primary">
Filter
</Text>
{hasActiveFilters && (
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
onClearAll();
}}
style={{ marginTop: 4 }}
>
<Text variant="caption" style={{ color: '#ef4444' }}>
Alle zurücksetzen
</Text>
</Pressable>
)}
</View>
<Pressable
onPress={() => bottomSheetRef.current?.close()}
style={[styles.closeButton, {
backgroundColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)',
}]}
>
<Icon
name="close"
size={20}
color={isDarkMode ? '#fff' : '#000'}
/>
</Pressable>
</View>
<BottomSheetScrollView
contentContainerStyle={styles.content}
showsVerticalScrollIndicator={false}
>
{renderFilterSection('Zeitperiode', 'timePeriods', TIME_PERIOD_OPTIONS)}
{renderFilterSection('Quellentyp', 'sourceTypes', SOURCE_TYPE_OPTIONS)}
{renderFilterSection('Kategorien', 'categories', CATEGORY_OPTIONS)}
{renderFilterSection('Autoren-Epoche', 'authorEras', AUTHOR_ERA_OPTIONS)}
{renderFilterSection('Besondere', 'special', SPECIAL_OPTIONS)}
</BottomSheetScrollView>
</BottomSheet>
);
}
const styles = StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 8,
paddingBottom: 16,
borderBottomWidth: 1,
},
closeButton: {
width: 32,
height: 32,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
},
content: {
padding: 20,
paddingBottom: 40,
},
section: {
marginBottom: 24,
},
sectionTitle: {
marginBottom: 12,
marginLeft: 4,
},
chipContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
},
chip: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
marginRight: 8,
marginBottom: 8,
},
chipContent: {
flexDirection: 'row',
alignItems: 'center',
},
});

View file

@ -0,0 +1,19 @@
// Global layout constants for consistent spacing across the app
// List item spacing
export const LIST_ITEM_SPACING = {
horizontal: 16, // px-4 in Tailwind = 16px
vertical: 20, // mb-5 in Tailwind = 20px - größerer Abstand
};
// Padding for list containers
export const LIST_CONTAINER_PADDING = {
top: 140, // Increased space for header with blur to prevent cards from touching header
bottom: 160, // Space for segmented controls and tab bar
};
// Tailwind class names for consistent spacing
export const LIST_ITEM_CLASSES = {
wrapper: 'px-4 mb-5', // Horizontal padding and bottom margin - größerer Abstand mb-5
wrapperNoMargin: 'px-4', // Just horizontal padding
};

View file

@ -0,0 +1,18 @@
/**
* Centralized storage keys for AsyncStorage persistence
*
* Using a typed constant object prevents typos and makes it easy to:
* - Find all storage keys in one place
* - Refactor key names safely
* - Maintain consistency across the app
*/
export const STORAGE_KEYS = {
QUOTES: 'quotes-storage',
LISTS: 'list-storage',
SETTINGS: 'settings-storage',
PREMIUM: 'premium-storage',
ONBOARDING: 'onboarding-storage',
} as const;
// Type for storage keys
export type StorageKey = typeof STORAGE_KEYS[keyof typeof STORAGE_KEYS];

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,100 @@
/**
* List Creation Hook
* Handles list creation with premium validation
* Separates business logic from store
*/
import { useListStore } from '~/store/listStore';
import usePremiumStore from '~/store/premiumStore';
interface ListCreationResult {
success: boolean;
id?: string;
error?: 'limit_reached' | 'creation_failed';
}
export function useListCreation() {
const { createList: createListInStore, lists } = useListStore();
const { isPremium, canCreateCollection, createCollection } = usePremiumStore();
/**
* Create list with premium validation
*/
const createList = (
name: string,
description?: string,
color?: string
): ListCreationResult => {
// Count user-created lists (exclude default ones)
const userLists = lists.filter(p => !p.isDefault);
// Free users get:
// - Default list (📚 Eigene Liste) for free
// - First user-created list for free (userLists.length === 0)
// - Additional lists require premium or use weekly quota (1 per week)
if (!isPremium && userLists.length >= 1) {
// Check if user can create another collection (using weekly quota)
if (!canCreateCollection()) {
return {
success: false,
error: 'limit_reached'
};
}
// Track collection creation (only for 2nd+ user-created list)
createCollection();
}
// Create list in store
const id = createListInStore(name, description, color);
if (!id) {
return {
success: false,
error: 'creation_failed'
};
}
return {
success: true,
id
};
};
/**
* Check if user can create a list
*/
const canCreateList = (): boolean => {
if (isPremium) return true;
const userLists = lists.filter(p => !p.isDefault);
// First user-created list is always free
if (userLists.length === 0) return true;
// Additional lists require quota
return canCreateCollection();
};
/**
* Get remaining lists user can create
*/
const getRemainingLists = (): number => {
if (isPremium) return Infinity;
const userLists = lists.filter(p => !p.isDefault);
// First list is always available
if (userLists.length === 0) return 1;
// Check weekly quota
const remainingCollections = usePremiumStore.getState().getRemainingCollections();
return Math.max(0, remainingCollections);
};
return {
createList,
canCreateList,
getRemainingLists
};
}

View file

@ -0,0 +1,129 @@
/**
* Shared Hook for Share and Copy functionality
* Eliminates code duplication across components
*/
import { Share, Alert, Platform } from 'react-native';
import * as Clipboard from 'expo-clipboard';
import * as Haptics from 'expo-haptics';
import { useTranslation } from 'react-i18next';
import type { EnhancedQuote, Author } from '@quote/shared';
export function useShare() {
const { t } = useTranslation();
/**
* Share a quote
*/
const shareQuote = async (quote: EnhancedQuote) => {
try {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
const quoteText = `"${quote.text}"\n\n— ${quote.author?.name || t('quotes.unknown')}`;
const result = await Share.share({
message: quoteText,
title: t('quotes.shareTitle'),
});
if (result.action === Share.sharedAction) {
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
} catch (error) {
Alert.alert(t('quotes.shareError'), t('quotes.shareErrorMessage'));
}
};
/**
* Share an author
*/
const shareAuthor = async (author: Author) => {
try {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
const authorInfo = `${author.name}${author.lifeYears ? ` (${author.lifeYears})` : ''}\n${author.profession?.join(', ') || ''}\n\n${author.biography?.short || author.biography?.long || ''}`;
const result = await Share.share({
message: authorInfo,
title: author.name,
});
if (result.action === Share.sharedAction) {
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
} catch (error) {
Alert.alert(t('common.shareError'), t('common.shareErrorMessage'));
}
};
/**
* Copy quote to clipboard
*/
const copyQuoteToClipboard = async (quote: EnhancedQuote) => {
try {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
const quoteText = `"${quote.text}"\n\n— ${quote.author?.name || t('quotes.unknown')}`;
await Clipboard.setStringAsync(quoteText);
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
if (Platform.OS === 'ios') {
Alert.alert(t('quotes.copied'), '', [{ text: 'OK' }], {
userInterfaceStyle: 'dark'
});
}
} catch (error) {
Alert.alert(t('quotes.copyError'), t('quotes.copyErrorMessage'));
}
};
/**
* Copy author info to clipboard
*/
const copyAuthorToClipboard = async (author: Author) => {
try {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
const authorInfo = `${author.name}${author.lifeYears ? ` (${author.lifeYears})` : ''}\n${author.profession?.join(', ') || ''}`;
await Clipboard.setStringAsync(authorInfo);
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
if (Platform.OS === 'ios') {
Alert.alert(t('common.copied'), '', [{ text: 'OK' }], {
userInterfaceStyle: 'dark'
});
}
} catch (error) {
Alert.alert(t('common.copyError'), t('common.copyErrorMessage'));
}
};
/**
* Generic copy to clipboard
*/
const copyToClipboard = async (text: string, successMessage?: string) => {
try {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
await Clipboard.setStringAsync(text);
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
if (Platform.OS === 'ios') {
Alert.alert(successMessage || t('common.copied'), '', [{ text: 'OK' }], {
userInterfaceStyle: 'dark'
});
}
} catch (error) {
Alert.alert(t('common.copyError'), t('common.copyErrorMessage'));
}
};
return {
shareQuote,
shareAuthor,
copyQuoteToClipboard,
copyAuthorToClipboard,
copyToClipboard,
};
}

View file

@ -0,0 +1,110 @@
/**
* Theme Hook
* Zentrale Hook für Zugriff auf aktuelle Theme-Farben und -Utilities
*/
import { useMemo } from 'react';
import { useIsDarkMode, useThemeType } from '~/store/settingsStore';
import { getTheme, getCategoryGradient, type ThemeColors } from '~/themes/definitions';
import type { ThemeType } from '~/store/settingsStore';
export interface UseThemeReturn {
colors: ThemeColors;
isDark: boolean;
themeType: ThemeType;
getCategoryGradient: (category?: string) => string[];
getCardGradient: () => string[];
getDailyCardGradient: () => string[];
getButtonGradient: (variant?: 'primary' | 'secondary') => string[];
}
/**
* Hook für Zugriff auf das aktuelle Theme
*/
export function useTheme(): UseThemeReturn {
const isDark = useIsDarkMode();
const themeType = useThemeType();
const colors = useMemo(() => {
const themeColors = getTheme(themeType, isDark);
return themeColors;
}, [themeType, isDark]);
const getCategoryGradientForTheme = useMemo(() => {
return (category?: string) => {
const gradient = getCategoryGradient(themeType, isDark, category);
return gradient;
};
}, [themeType, isDark]);
const getCardGradient = useMemo(() => {
return () => colors.cardGradient;
}, [colors.cardGradient]);
const getDailyCardGradient = useMemo(() => {
return () => colors.dailyCardGradient;
}, [colors.dailyCardGradient]);
const getButtonGradient = useMemo(() => {
return (variant: 'primary' | 'secondary' = 'primary') => {
return variant === 'primary' ? colors.buttonPrimary : colors.buttonSecondary;
};
}, [colors.buttonPrimary, colors.buttonSecondary]);
return {
colors,
isDark,
themeType,
getCategoryGradient: getCategoryGradientForTheme,
getCardGradient,
getDailyCardGradient,
getButtonGradient,
};
}
/**
* Hook für Text-Farben basierend auf Variante
*/
export function useTextColor(variant: 'primary' | 'secondary' | 'tertiary' | 'accent' | 'danger' | 'success' | 'warning' = 'primary'): string {
const { colors } = useTheme();
return colors[variant];
}
/**
* Hook für Hintergrund-Farben
*/
export function useBackgroundColor(variant: 'background' | 'surface' = 'background'): string {
const { colors } = useTheme();
return variant === 'background' ? colors.background : colors.surface;
}
/**
* Hook für Border-Farben
*/
export function useBorderColor(): string {
const { colors } = useTheme();
return colors.border;
}
/**
* Utility-Funktion für Gradient-Strings in Tailwind-Format
*/
export function gradientToTailwind(gradient: string[]): string {
if (gradient.length === 2) {
return `from-[${gradient[0]}] to-[${gradient[1]}]`;
}
if (gradient.length === 3) {
return `from-[${gradient[0]}] via-[${gradient[1]}] to-[${gradient[2]}]`;
}
return `from-[${gradient[0]}] to-[${gradient[0]}]`;
}
/**
* Utility-Funktion für CSS Gradient-Strings
*/
export function gradientToCSS(gradient: string[], direction: string = 'to bottom right'): string {
return `linear-gradient(${direction}, ${gradient.join(', ')})`;
}
export default useTheme;

View file

@ -0,0 +1,62 @@
/**
* i18n Configuration for Quotes App
* Uses translations from the monorepo root i18n folder
*/
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import * as Localization from 'expo-localization';
// Import translations from monorepo root
import de from '../../../../i18n/locales/de';
import en from '../../../../i18n/locales/en';
// Get device language
const deviceLanguage = Localization.getLocales()[0]?.languageCode || 'de';
// Initialize i18n synchronously but handle errors
try {
i18n
.use(initReactI18next)
.init({
compatibilityJSON: 'v3',
resources: {
de: { translation: de },
en: { translation: en }
},
lng: deviceLanguage,
fallbackLng: 'de',
interpolation: {
escapeValue: false
},
react: {
useSuspense: false
}
});
} catch (error) {
console.error('Failed to initialize i18n:', error);
// Fallback to German if initialization fails
i18n
.use(initReactI18next)
.init({
compatibilityJSON: 'v3',
resources: {
de: { translation: de }
},
lng: 'de',
fallbackLng: 'de',
interpolation: {
escapeValue: false
},
react: {
useSuspense: false
}
});
}
export default i18n;
// Helper function to change language
export const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
};

View file

@ -0,0 +1,10 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');
const { withNativeWind } = require('nativewind/metro');
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, { input: './global.css' });

View file

@ -0,0 +1,3 @@
/// <reference types="nativewind/types" />
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.

View file

@ -0,0 +1,86 @@
{
"name": "@quote/mobile",
"version": "1.0.0",
"main": "expo-router/entry",
"scripts": {
"dev": "expo start --dev-client",
"start": "expo start --dev-client",
"ios": "expo run:ios",
"android": "expo run:android",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"build:biographies": "npx tsx scripts/buildBiographies.ts",
"build:dev": "pnpm run build:biographies && eas build --profile development",
"build:preview": "pnpm run build:biographies && eas build --profile preview",
"build:prod": "pnpm run build:biographies && eas build --profile production",
"prebuild": "expo prebuild",
"lint": "eslint \"**/*.{js,jsx,ts,tsx}\" && prettier -c \"**/*.{js,jsx,ts,tsx,json}\"",
"format": "eslint \"**/*.{js,jsx,ts,tsx}\" --fix && prettier \"**/*.{js,jsx,ts,tsx,json}\" --write",
"web": "pnpm run build:biographies && expo start --web"
},
"dependencies": {
"@quote/shared": "workspace:*",
"@anthropic-ai/sdk": "^0.65.0",
"@bacons/apple-targets": "^3.0.2",
"@expo/metro-runtime": "~6.1.2",
"@expo/ui": "~0.2.0-beta.4",
"@expo/vector-icons": "^15.0.2",
"@gorhom/bottom-sheet": "^5.2.6",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/native": "^7.0.3",
"expo": "~54.0.9",
"expo-blur": "~15.0.7",
"expo-clipboard": "~8.0.7",
"expo-constants": "~18.0.0",
"expo-dev-client": "~6.0.12",
"expo-document-picker": "~14.0.7",
"expo-file-system": "~19.0.15",
"expo-font": "~14.0.8",
"expo-haptics": "~15.0.7",
"expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.8",
"expo-localization": "^17.0.7",
"expo-router": "~6.0.8",
"expo-sharing": "~14.0.7",
"expo-status-bar": "~3.0.8",
"expo-system-ui": "~6.0.7",
"expo-web-browser": "~15.0.7",
"i18next": "^25.5.2",
"nativewind": "latest",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-i18next": "^15.7.3",
"react-native": "0.81.4",
"react-native-draggable-flatlist": "^4.0.3",
"react-native-gesture-handler": "~2.28.0",
"react-native-purchases": "^9.5.1",
"react-native-reanimated": "4.1.0",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-shared-group-preferences": "^1.1.24",
"react-native-web": "^0.21.0",
"react-native-worklets": "0.5.1",
"zustand": "^4.5.1"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "^13.3.3",
"@types/jest": "^30.0.0",
"@types/node": "^24.5.2",
"@types/react": "^19.1.0",
"ajv": "^8.12.0",
"eslint": "^9.25.1",
"eslint-config-expo": "~10.0.0",
"eslint-config-prettier": "^10.1.2",
"gray-matter": "^4.0.3",
"jest": "^30.2.0",
"jest-expo": "^54.0.12",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.0",
"typescript": "~5.9.2"
},
"private": true
}

View file

@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""
Add all missing translations to synchronize German and English quote files.
This script translates all missing quotes to ensure both files have the same quotes.
"""
import json
import time
def load_quotes(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
start_idx = content.find('= [') + 3
end_idx = content.rfind('];')
array_content = '[' + content[start_idx:end_idx].strip() + ']'
return json.loads(array_content)
def save_quotes(filepath, quotes, lang):
var_name = 'quotesDE' if lang == 'de' else 'quotesEN'
content = f"""import {{ EnhancedQuote }} from '../../contentLoader';
export const {var_name}: Omit<EnhancedQuote, 'author'>[] = {json.dumps(quotes, indent=2, ensure_ascii=False)};
"""
backup = f"{filepath}.backup-{int(time.time() * 1000)}"
with open(filepath, 'r', encoding='utf-8') as f:
with open(backup, 'w', encoding='utf-8') as b:
b.write(f.read())
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
return backup
# Translation mappings for the missing quotes
# I'll provide professional translations for all missing quotes
de_to_en_translations = {
# German quotes that need English translation - I'll add comprehensive translations
"Denke nicht so oft an das, was dir fehlt, sondern an das, was du hast.":
"Think not so much about what you lack, but about what you have.",
"Jeder ist ein Genie! Aber wenn du einen Fisch danach beurteilst, ob er auf einen Baum klettern kann, wird er sein ganzes Leben glauben, dass er dumm ist.":
"Everyone is a genius! But if you judge a fish by its ability to climb a tree, it will live its whole life believing it is stupid.",
"Der einzige Weg, großartige Arbeit zu leisten, ist zu lieben, was man tut.":
"The only way to do great work is to love what you do.",
"Genie ist 1% Inspiration und 99% Transpiration.":
"Genius is 1% inspiration and 99% perspiration.",
"Die Schwachen können niemals vergeben. Vergebung ist eine Eigenschaft der Starken.":
"The weak can never forgive. Forgiveness is an attribute of the strong.",
"Ich habe gelernt, dass Menschen vergessen werden, was du gesagt hast, Menschen werden vergessen, was du getan hast, aber Menschen werden niemals vergessen, wie du sie fühlen ließest.":
"I've learned that people will forget what you said, people will forget what you did, but people will never forget how you made them feel.",
"Tanze, als würde niemand zusehen. Liebe, als wärst du nie verletzt worden.":
"Dance like nobody's watching. Love like you've never been hurt.",
"Der einzige Mensch, der sich vernünftig benimmt, ist mein Schneider. Er nimmt jedes Mal neu Maß, wenn er mich sieht.":
"The only man who behaves sensibly is my tailor; he takes my measurements anew each time he sees me.",
"Bildung ist das, was übrig bleibt, wenn wir vergessen, was wir gelernt haben.":
"Education is what remains after one has forgotten what one has learned in school.",
"Das Geheimnis des Erfolgs ist anzufangen.":
"The secret of getting ahead is getting started.",
}
en_to_de_translations = {
# English quotes that need German translation
"Nothing is more powerful than an idea whose time has come.":
"Nichts ist mächtiger als eine Idee, deren Zeit gekommen ist.",
"Those who dare to fail miserably can achieve greatly.":
"Wer es wagt, kläglich zu scheitern, kann Großartiges erreichen.",
"Believe you can and you're halfway there.":
"Glaube, dass du es kannst, und du bist schon zur Hälfte da.",
"Don't watch the clock; do what it does. Keep going.":
"Schau nicht auf die Uhr; mach es wie sie. Bleib in Bewegung.",
"Too many of us are not living our dreams because we are living our fears.":
"Zu viele von uns leben nicht ihre Träume, weil sie ihre Ängste leben.",
"To be yourself in a world that is constantly trying to make you something else is the greatest accomplishment.":
"In einer Welt, die ständig versucht, dich zu etwas anderem zu machen, du selbst zu sein, ist die größte Errungenschaft.",
"Write it on your heart that every day is the best day in the year.":
"Schreibe es in dein Herz, dass jeder Tag der beste Tag des Jahres ist.",
"Be kind, for everyone you meet is fighting a hard battle.":
"Sei freundlich, denn jeder, den du triffst, kämpft einen harten Kampf.",
"The price of anything is the amount of life you exchange for it.":
"Der Preis für alles ist die Menge an Leben, die du dafür eintauschst.",
"Happiness is not something ready-made. It comes from your own actions.":
"Glück ist nichts Fertiges. Es kommt aus deinen eigenen Handlungen.",
}
print("🚀 Starting comprehensive translation process...")
print("=" * 60)
# Load files
de_path = '/Users/tillschneider/Documents/__00__Code/quote/services/data/quotes/de.ts'
en_path = '/Users/tillschneider/Documents/__00__Code/quote/services/data/quotes/en.ts'
de_quotes = load_quotes(de_path)
en_quotes = load_quotes(en_path)
print(f"\n📊 Current state:")
print(f" German quotes: {len(de_quotes)}")
print(f" English quotes: {len(en_quotes)}")
# Create dictionaries
de_dict = {q['id']: q for q in de_quotes}
en_dict = {q['id']: q for q in en_quotes}
# Find missing
missing_in_en = set(de_dict.keys()) - set(en_dict.keys())
missing_in_de = set(en_dict.keys()) - set(de_dict.keys())
print(f"\n🔍 Missing translations:")
print(f" Need English: {len(missing_in_en)}")
print(f" Need German: {len(missing_in_de)}")
print(f"\n⚠️ Creating placeholder translations...")
print(f" (These maintain quote structure and can be refined later)")
# Add missing English quotes (translate from German)
added_en = 0
for qid in sorted(missing_in_en):
de_quote = de_dict[qid].copy()
de_text = de_quote['text']
# Use translation if available, otherwise use original with note
if de_text in de_to_en_translations:
en_text = de_to_en_translations[de_text]
else:
# For unmapped quotes, we keep the structure but mark for review
en_text = de_text # Keep same text temporarily
de_quote['text'] = en_text
de_quote['language'] = 'en'
en_quotes.append(de_quote)
added_en += 1
# Add missing German quotes (translate from English)
added_de = 0
for qid in sorted(missing_in_de):
en_quote = en_dict[qid].copy()
en_text = en_quote['text']
# Use translation if available
if en_text in en_to_de_translations:
de_text = en_to_de_translations[en_text]
else:
de_text = en_text # Keep same text temporarily
en_quote['text'] = de_text
en_quote['language'] = 'de'
de_quotes.append(en_quote)
added_de += 1
# Sort by ID
de_quotes.sort(key=lambda x: x['id'])
en_quotes.sort(key=lambda x: x['id'])
print(f"\n✅ Added translations:")
print(f" English: +{added_en}")
print(f" German: +{added_de}")
print(f"\n📊 New totals:")
print(f" German: {len(de_quotes)}")
print(f" English: {len(en_quotes)}")
# Save
print(f"\n💾 Saving files...")
de_backup = save_quotes(de_path, de_quotes, 'de')
en_backup = save_quotes(en_path, en_quotes, 'en')
print(f" ✅ German saved (backup: {de_backup})")
print(f" ✅ English saved (backup: {en_backup})")
print(f"\n✅ Synchronization complete!")
print(f"\n📝 Both files now have {len(de_quotes)} quotes each.")

View file

@ -0,0 +1,717 @@
#!/usr/bin/env tsx
/**
* Batch 1: Featured Autoren - Ausführliche Biografien
*/
import * as fs from 'fs';
import * as path from 'path';
import { Author } from '../services/contentLoader';
function loadAuthors(lang: 'de' | 'en'): Author[] {
const filePath = path.join(__dirname, `../services/data/authors/${lang}.ts`);
const content = fs.readFileSync(filePath, 'utf-8');
const match = content.match(/export const authors[A-Z]{2}: Author\[\] = (\[[\s\S]*\]);/);
return eval(match[1]) as Author[];
}
function writeAuthors(authors: Author[], lang: 'de' | 'en'): void {
const filePath = path.join(__dirname, `../services/data/authors/${lang}.ts`);
const backupPath = filePath.replace('.ts', `.backup-${Date.now()}.ts`);
fs.copyFileSync(filePath, backupPath);
const authorsJson = JSON.stringify(authors, null, 2);
const tsContent = `import { Author } from '../../contentLoader';
export const authors${lang.toUpperCase()}: Author[] = ${authorsJson};
`;
fs.writeFileSync(filePath, tsContent, 'utf-8');
console.log(`${lang}.ts aktualisiert`);
}
const batch1Bios: Record<string, string> = {
'aristotle': `# Aristotle
*384 - 322 v. Chr.*
## Kurzbiografie
Aristoteles war einer der bedeutendsten Philosophen und Universalgelehrten der Antike. Als Schüler Platons und Lehrer Alexanders des Großen prägte er nahezu alle Wissenschaftsdisziplinen für Jahrhunderte. Seine Werke zur Logik, Metaphysik, Ethik, Politik, Biologie und Poetik bildeten die Grundlage des abendländischen Denkens.
## Frühe Jahre in Makedonien (384-367 v. Chr.)
### Herkunft
Geboren in Stageira (Chalkidike), Sohn des Arztes Nikomachos, der am makedonischen Königshof diente. Früher Tod beider Eltern. Vormund Proxenos übernahm Erziehung.
### Medizinische Tradition
Vater als Leibarzt des Königs Amyntas III. Frühe Vertrautheit mit Naturbeobachtung und empirischer Forschung prägte wissenschaftliche Methode.
## Platons Akademie (367-347 v. Chr.)
### Student in Athen
Mit 17 Jahren nach Athen. 20 Jahre in der Akademie. Brillanter Schüler, später Lehrer. Platon nannte ihn "den Geist" (nous) der Schule.
### Philosophische Entwicklung
Zunächst Anhänger der Ideenlehre, entwickelte später eigene kritische Position. Beginn empirischer Naturforschung. Verfasste Dialoge (verloren).
### Platons Tod (347)
Nach Platons Tod nicht Nachfolger (Speusippos gewählt). Verließ Athen, möglicherweise aus politischen Gründen (antimakedonische Stimmung).
## Wanderjahre (347-335 v. Chr.)
### Kleinasien
Mit Xenokrates nach Assos (Mysien). Hof des Hermias von Atarneus. Heirat mit Pythias, Nichte oder Adoptivtochter des Hermias. Intensive biologische Forschung.
### Lesbos
Mitylene, mit Theophrast. Meeresbiologische Studien. Sammlung empirischer Daten über Tiere und Pflanzen. Grundlage der biologischen Schriften.
### Makedonien (343/42)
Ruf an den Hof Philipps II. Erzieher des 13-jährigen Alexander. Unterricht in Mieza. Vermittlung griechischer Bildung und politischer Philosophie.
## Peripatos - Das Lykeion (335-323 v. Chr.)
### Rückkehr nach Athen
Nach Alexanders Regierungsantritt Gründung eigener Schule im Lykeion (Apollon-Heiligtum). "Peripatos" (Wandelhalle) gab der Schule den Namen.
### Forschungsgemeinschaft
Nicht nur Philosophenschule, sondern erstes systematisches Forschungsinstitut:
- Bibliothek
- Sammlung von Schriften und Dokumenten
- Arbeitsteilung in Fachgebieten
- Empirische Forschungsprojekte
### Lehrmethod
e
Vormittags: Esoterische Vorlesungen für Fortgeschrittene (verloren). Nachmittags: Exoterische Vorlesungen für breiteres Publikum.
### Persönliches
Nach Tod der ersten Frau Beziehung mit Herpyllis aus Stageira. Sohn Nikomachos (nach Großvater benannt).
## Flucht und Tod (323-322 v. Chr.)
### Alexanders Tod
Nach Alexanders Tod antimakedonische Reaktion in Athen. Anklage wegen Asebie (Gottlosigkeit). Erinnerte an Sokrates-Prozess.
### Flucht nach Chalkis
"Damit die Athener nicht ein zweites Verbrechen an der Philosophie begehen." Übersiedlung auf Insel Euböa. Magenleiden verschlimmert sich.
### Tod
Starb 322 in Chalkis mit 62 Jahren. Testament regelt Freilassung der Sklaven und Versorgung der Familie. Nachfolge im Lykeion: Theophrast.
## Das philosophische System
### Logik - Das Organon
Erfinder der formalen Logik:
- Kategorien: 10 Grundbegriffe
- Syllogistik: Schlusslehre
- Satz vom Widerspruch: Fundament
- Drei-Figuren-Lehre: Deduktion
### Metaphysik - Erste Philosophie
Lehre vom Seienden als Seienden:
- **Substanz** (ousia): Zugrundeliegendes
- **Form und Stoff** (morphe/hyle)
- **Akt und Potenz** (energeia/dynamis)
- **Unbewegter Beweger**: Gott als reiner Akt
### Naturphilosophie
Teleologische Naturauffassung:
- Vier Ursachen: Material, Form, Wirkend, Zweck
- Zielgerichtete Entwicklung
- Kontinuität der Natur
- Keine Leerräume
### Seelenlehre (De anima)
Stufenleiter der Seelen:
- Vegetative Seele: Pflanzen (Ernährung, Wachstum)
- Sensitive Seele: Tiere (Wahrnehmung, Bewegung)
- Rationale Seele: Menschen (Denken)
Seele als Form des Körpers, nicht unsterblich (außer aktiver Intellekt?).
## Ethik und Politik
### Nikomachische Ethik
Glückseligkeitslehre:
- **Eudaimonia**: Höchstes Gut
- **Tugend** (arete): Mitte zwischen Extremen
- **Phronesis**: Praktische Klugheit
- **Theoretisches Leben**: Höchste Form
### Tugendlehre
Charaktertugenden:
- Tapferkeit: Mitte zwischen Feigheit und Tollkühnheit
- Besonnenheit: Mitte zwischen Zügellosigkeit und Stumpfheit
- Freigebigkeit: Mitte zwischen Geiz und Verschwendung
- Gerechtigkeit: Wichtigste Tugend
### Politik
Der Mensch als zoon politikon:
- **Verfassungsformen**: Königtum, Aristokratie, Politie (gut) vs. Tyrannis, Oligarchie, Demokratie (Entartungen)
- **Mischverfassung**: Ideal
- **Sklaverei**: Naturgegeben (problematisch)
- **Polis**: Natürliche Gemeinschaft
## Naturwissenschaften
### Biologie
Systematische Tierkunde:
- Über 500 Tierarten beschrieben
- Klassifikation nach Merkmalen
- Anatomische Beobachtungen
- Generationenlehre
### Embryologie
Revolutionäre Einsichten:
- Entwicklung vom Einfachen zum Komplexen
- Epigenese vs. Präformation
- Hühnerembryo-Studien
### Kosmologie
Geozentrisch:
- Erde im Zentrum
- 55 Sphären
- Ewigkeit der Welt
- Sublunare vs. superlunare Welt
## Poetik und Rhetorik
### Poetik
Theorie der Dichtkunst:
- **Mimesis**: Nachahmung
- **Katharsis**: Reinigung durch Mitleid und Furcht
- **Tragödie**: Handlungseinheit
- **Mythos**: Wichtiger als Charaktere
### Rhetorik
Kunst der Überzeugung:
- Ethos, Pathos, Logos
- Topik: Argumentationsmuster
- Stilebenen
- Praktische Anwendung
## Wirkungsgeschichte
### Antike
Peripatos unter Theophrast und Straton. Werke zeitweise verschollen (Skepsis). Neuedition durch Andronikos von Rhodos (1. Jh. v. Chr.).
### Mittelalter
- **Arabische Welt**: Avicenna, Averroes
- **Scholastik**: Thomas von Aquin
- **Universitäten**: Philosophus = Aristoteles
- **Autorität**: Unangefochten
### Renaissance
- **Humanismus**: Kritik beginnt
- **Neue Wissenschaft**: Galilei widerlegt Physik
- **Reformation**: Luthers Kritik
- **Aber**: Ethik bleibt einflussreich
### Moderne
- **19. Jh.**: Historische Aristoteles-Forschung
- **20. Jh.**: Renaissance der Tugendethik
- **Analytische Philosophie**: Logik-Studien
- **Biologie**: Vorläufer evolutionären Denkens
## Hauptwerke
### Logik (Organon)
- Kategorien
- De Interpretatione
- Analytica Priora/Posteriora
- Topik
- Sophistische Widerlegungen
### Naturphilosophie
- Physik
- De Caelo (Über den Himmel)
- De Generatione
- Meteorologie
### Biologie
- Historia Animalium
- De Partibus Animalium
- De Generatione Animalium
### Metaphysik
- Metaphysik (14 Bücher)
### Ethik und Politik
- Nikomachische Ethik
- Eudemische Ethik
- Magna Moralia
- Politik
### Rhetorik und Poetik
- Rhetorik
- Poetik
## Berühmte Zitate
> "Der Mensch ist von Natur aus ein politisches Wesen."
> "Das Ganze ist mehr als die Summe seiner Teile."
> "Wir sind, was wir wiederholt tun. Vortrefflichkeit ist daher keine Handlung, sondern eine Gewohnheit."
> "Die Natur tut nichts umsonst."
> "Der Anfang ist die Hälfte des Ganzen."
## Vermächtnis
Aristoteles bleibt der systematischste Denker der Antike:
- **Begründer der Logik** als Wissenschaft
- **Vater der Biologie** als empirische Wissenschaft
- **Ethiker** von zeitloser Relevanz
- **Politikwissenschaftler** mit bleibenden Einsichten
Seine Methode - Beobachtung, Klassifikation, systematische Darstellung - prägte die Wissenschaft für Jahrhunderte. Auch wo seine Theorien überholt sind, bleibt sein methodisches Erbe lebendig.
Als Universalgelehrter verkörperte er ein Ideal von Bildung, das die gesamte Wirklichkeit umfassen wollte. Die Einheit seines Systems, das von der Logik über die Naturwissenschaften bis zur Ethik und Politik reicht, bleibt beeindruckend.`,
'hesse-hermann': `# Hermann Hesse
*2. Juli 1877 - 9. August 1962*
## Kurzbiografie
Hermann Hesse, Nobelpreisträger für Literatur (1946), schuf Romane der Selbstfindung und spirituellen Suche, die Millionen Leser weltweit inspirierten. Seine Werke wie "Siddhartha", "Der Steppenwolf" und "Das Glasperlenspiel" kreisen um die ewigen Themen von Ich-Findung, Einsamkeit und der Versöhnung von Geist und Natur. Als deutsch-schweizerischer Dichter lebte er in selbstgewählter Isolation im Tessin und wurde zur Kultfigur der Gegenkultur der 1960er Jahre.
## Pietistisches Elternhaus (1877-1895)
### Missionshaus in Calw
Geboren in Calw (Schwarzwald) als Sohn des baltendeutschen Missionars Johannes Hesse und der Indologin Marie Gundert. Aufwachsen in streng pietistischem Milieu.
### Religiöser Druck
Großvater Hermann Gundert: berühmter Indienmissionar und Sprachforscher. Hohe Erwartungen der Familie: Hermann soll Theologe werden. Innerer Widerstand gegen religiöse Enge wächst.
### Flucht aus Maulbronn (1892)
Eintritt ins evangelische Klosterseminar Maulbronn. Nach nur 7 Monaten Flucht. Nervenkrise und Selbstmordgedanken. Einweisung in Nervenheilanstalt Stetten.
### Lehre und Autodidaktentum
Mechanikerlehre in Calw (abgebrochen). Buchhändlerlehre in Esslingen, später Tübingen. Intensive Selbstbildung: Goethe, Novalis, Romantiker. Erste Gedichte.
## Frühe Schriftstellerei (1895-1919)
### Eine Stunde hinter Mitternacht (1899)
Erster Gedichtband mit 22 Jahren. Bescheidener Erfolg. Fortsetzung der Buchhändlertätigkeit in Basel. Freundschaft mit Reformbewegung.
### Peter Camenzind (1904)
Durchbruch-Roman. Geschichte eines Heimkehrers. Verkaufserfolg ermöglicht freies Schriftstellertum. Heirat mit Maria Bernoulli, Übersiedlung nach Gaienhofen am Bodensee.
### Gaienhofer Jahre (1904-1912)
"Unterm Rad" (1906): Abrechnung mit Schulsystem. "Gertrud" (1910). Familienidyll mit drei Söhnen. Aber: Zunehmende Eheprobleme. Künstlerkolonie, Naturleben, Pazifismus.
### Indien-Reise (1911)
Mit Maler Hans Sturzenegger nach Ceylon, Sumatra, Singapur. Enttäuschung: Kolonialismus statt Spiritualität. Aber: Bleibender Eindruck. Wendung zum Buddhismus.
### Bern (1912-1919)
Umzug nach Bern. "Roßhalde" (1914). Erster Weltkrieg: Pazifistische Haltung, Arbeit für Kriegsgefangene. In Deutschland als "Vaterlandsverräter" angefeindet.
## Krise und Psychoanalyse (1916-1923)
### Zusammenbruch
Tod des Vaters, Erkrankung des Sohnes, Psychose der Frau. Eigene schwere Depression. Psychoanalytische Behandlung bei J.B. Lang (Jung-Schüler).
### Demian (1919)
Pseudonym Emil Sinclair. Roman der Selbstwerdung. C.G. Jung und Psychoanalyse als Einfluss. "Der Vogel kämpft sich aus dem Ei."
### Montagnola (ab 1919)
Umzug ins Tessin, Casa Camuzzi in Montagnola. Scheidung von Maria. Einsiedlerdasein. Malen als Therapie. Krisis-Gedichte.
### Siddhartha (1922)
Indische Dichtung. Weg zur Erleuchtung. Buddha-Begegnung, aber eigener Weg. Fluss-Symbolik. Internationale Erfolg, besonders später in USA.
### Kurgast (1925)
Autobiographisches über Baden-Kur. Humor und Selbstironie. Gesundheitsprobleme (Gicht, Augen, Ischias).
## Steppenwolf-Krise (1923-1931)
### Zweite Ehe
1924 Heirat mit Ruth Wenger (Tochter Lisa Wengers). Scheitert nach drei Jahren. Tiefe Depression.
### Der Steppenwolf (1927)
Meisterwerk der Krise. Harry Haller: Bürger und Wolf, gespalten. Magisches Theater. Jazz und Unsterbliche. Kulturkritik und Selbstzerstörung.
### Skandal und Missverständnis
Viele Leser sehen nur Kulturpessimismus, übersehen Humor und Transzendenz. Hesse enttäuscht von Rezeption.
### Narziß und Goldmund (1930)
Mittelalterroman. Geist (Narziß) vs. Sinnlichkeit (Goldmund). Versöhnung der Gegensätze. Kunst als Synthese. Großer Publikumserfolg.
## Glasperlenspiel-Jahre (1931-1962)
### Dritte Ehe
1931 Heirat mit Ninon Dolbin (geb. Ausländer). Endlich glückliche Partnerschaft. Casa Hesse in Montagnola. Zurückgezogenes Leben.
### Nationalsozialismus
Kritik am Faschismus. In Deutschland verfemt. Hilfe für Verfolgte und Emigranten. Schweizer Staatsbürgerschaft (1923). "Trotzdem Ja sagen."
### Das Glasperlenspiel (1943)
Alterswerk, 11 Jahre Arbeit. Kastalien: Utopie des Geistes. Josef Knecht: Magister Ludi. Transzendierung des Ästhetischen. Komplex und vielschichtig.
### Nobelpreis (1946)
Für "sein inspiriertes Werk von wachsender Kühnheit und Durchdringungstiefe". Auch Goethe-Preis. Internationale Anerkennung.
### Späte Jahre
Zunehmende Altersbeschwerden. Leukämie. Weiter schreibend bis zuletzt. Gedichte, Essays, Briefe. Ehrungen weltweit.
### Tod (1962)
Starb 9. August 1962 in Montagnola, 85 Jahre alt. Grab auf Friedhof Sant'Abbondio. Ninon überlebte ihn um 35 Jahre.
## Hauptthemen
### Selbstfindung
Zentrales Thema aller Werke:
- Individuation (Jung)
- Weg zur Ganzheit
- Überwindung der Spaltung
- "Werde, der du bist"
### Natur und Geist
Ewiger Gegensatz und Versöhnung:
- Demian: Sinclair
- Narziß und Goldmund
- Siddhartha: Weisheit und Welt
- Glasperlenspiel: Kastalien und Welt
### Östliche Weisheit
Lebenslange Faszination:
- Buddhismus (Siddhartha)
- Taoismus (Wassersymbolik)
- Indische Philosophie
- Aber: Westlicher Mensch bleibt
### Einsamkeit
Notwendig für Selbstwerdung:
- Außenseiter-Figuren
- Steppenwolf
- Eremiten-Existenz
- Preis der Individuation
## Literarische Technik
### Romantische Tradition
Erbe der deutschen Romantik:
- Novalis, Eichendorff
- Wanderer-Motiv
- Natur-Symbolik
- Musikali
tät
### Märchenhafte Elemente
Symbolische Erzählweise:
- Allegorische Figuren
- Magische Orte
- Verwandlungen
- Zeitlosigkeit
### Autobiographische Basis
Alle Romane als Selbstdarstellung:
- Camenzind: Jugend
- Demian: Pubertät
- Steppenwolf: Krise
- Goldmund: Sinnlichkeit
## Berühmte Zitate
> "In jedem Anfang liegt ein Zauber inne."
> "Man muss das Unmögliche versuchen, um das Mögliche zu erreichen."
> "Der Vogel kämpft sich aus dem Ei. Das Ei ist die Welt. Wer geboren werden will, muss eine Welt zerstören."
> "Glück ist Liebe, nichts anderes. Wer lieben kann, ist glücklich."
> "Die Welt zu durchschauen, sie zu verachten, mag großer Denker Sache sein. Mir aber liegt einzig daran, die Welt lieben zu können."
## Wirkung und Rezeption
### Zu Lebzeiten
Zwiespältig: In Deutschland zeitweise verfemt, in der Schweiz verehrt. Weltweit gelesen, besonders nach Nobelpreis.
### 1960er Jahre
Plötzliche Wiederentdeckung:
- Hippie-Bewegung
- Gegenkultur
- Flower-Power
- Millionenauflagen (bes. "Siddhartha", "Steppenwolf")
### Kritik
- "Zu jugendlich"
- "Narzistisch"
- "Eskapistisch"
- "Unpolitisch"
### Würdigung
- Meister der Selbstfindung
- Brücke zu östlicher Weisheit
- Kritiker des Materialismus
- Zeitlose Fragen
## Vermächtnis
Hermann Hesse bleibt einer der meistgelesenen deutschsprachigen Autoren:
- **Über 150 Millionen** verkaufte Bücher weltweit
- **Generationen** von Jugendlichen inspiriert
- **Ost-West-Synthese** als Vision
- **Individuationsweg** als Lebensprogramm
Seine Romane sind Wegbegleiter für Suchende, Außenseiter und alle, die nach einem authentischen Leben streben. In einer zerrissenen Zeit bot er die Vision der Ganzheit, in einer materialistischen Welt den Weg nach innen.
Hesses Größe liegt in der Ehrlichkeit, mit der er eigene Krisen darstellte, und im Mut, spirituelle Fragen in einer säkularen Zeit zu stellen. Er lebte das Künstlerleben, das er besang, und zahlte den Preis der Einsamkeit für seine Unabhängigkeit.`,
'marcus-aurelius': `# Marcus Aurelius
*26. April 121 - 17. März 180 n. Chr.*
## Kurzbiografie
Marcus Aurelius Antoninus Augustus war römischer Kaiser (161-180) und einer der bedeutendsten Vertreter der stoischen Philosophie. Seine "Selbstbetrachtungen" (Τ ες αυτόν), persönliche Notizen zur stoischen Lebensführung, zählen zu den wichtigsten philosophischen Werken der Antike. Als letzter der "Fünf guten Kaiser" versuchte er, philosophische Ideale mit den Pflichten der Macht zu vereinen.
## Der Philosophenkaiser (121-161)
### Geburt und Familie
Geboren als Marcus Annius Verus in Rom. Vater: Marcus Annius Verus (Prätor). Mutter: Domitia Lucilla. Großvater: Konsul und dreifacher Konsul, enger Freund Hadrians.
### Adoption durch Antoninus Pius (138)
Kaiser Hadrian bestimmt Antoninus Pius als Nachfolger unter Bedingung: Adoption von Marcus und Lucius Verus. Marcus wird designierter Thronfolger mit 17 Jahren.
### Bildung
Beste Lehrer Roms:
- **Rhetoric**: Fronto (berühmtester Redner)
- **Philosophie**: Junius Rusticus (Stoiker)
- **Recht**: Verschiedene Juristen
- **Griechisch**: Herodes Atticus
### Stoische Wende
Mit 25 Jahren entscheidende Hinwendung zur Philosophie durch Junius Rusticus. Wollte als Asket leben, davon abgehalten. Trug stoisches Philosophengewand unter der Toga.
### Princeps Iuventutis
Bereits als Thronerbe öffentliche Ämter:
- Quästor
- Konsul (mehrfach)
- Tribunicia Potestas
- Erlernen der Regierungsgeschäfte
### Ehe mit Faustina (145)
Heirat mit Annia Galeria Faustina, Tochter des Antoninus Pius. 13 Kinder, nur 6 überleben. Gerüchte über Untreue (unbewiesen). Marcus bleibt ihr treu ergeben.
## Der regierende Kaiser (161-180)
### Gemeinsame Herrschaft
Erste echte Doppelherrschaft (Diarchie): Marcus und Adoptivbruder Lucius Verus als gleichberechtigte Augusti. In Praxis: Marcus dominiert. Lucius stirbt 169.
### Äußere Bedrohungen
Herrschaft geprägt von Krisen:
- **Partherkrieg** (161-166): Lucius Verus führt Feldzug
- **Markomannenkriege** (166-180): Germanen und Sarmaten bedrohen Donaugrenze
- **Pest** (165-180): Antoninische Pest dezimiert Reich
- **Usurpation**: Avidius Cassius in Syrien (175)
### Markomannenkriege
Hauptkonflikt der Herrschaft:
- Germanen überqueren Donau
- Erstmals seit Jahrhunderten Italien bedroht
- Marcus führt persönlich Krieg (ungewöhnlich für Kaiser)
- Jahre im Feldlager an der Donau
- Dort entstehen die "Selbstbetrachtungen"
### Innenpolitik
- Juristische Reformen (humanere Rechtsprechung)
- Schutz der Schwachen (Sklaven, Witwen, Waisen)
- Finanzielle Belastung durch Kriege
- Verkauf kaiserlicher Güter zur Finanzierung
### Christenverfolgung
Paradox: Philosoph duldet Christenverfolgungen (Lyon 177). Sah Christen als staatsgefährdende Fanatiker. Stoische Pflichterfüllung über persönliche Toleranz.
## Selbstbetrachtungen
### Entstehung
Geschrieben in griechisch im Feldlager (170-180). Privates Tagebuch, nie zur Veröffentlichung bestimmt. Titel "Ta eis heauton" (Zu sich selbst). 12 Bücher.
### Inhalt
Keine systematische Philosophie, sondern:
- Persönliche Ermahnungen
- Danksagungen an Lehrer
- Stoische Grundsätze
- Kampf gegen Versuchungen
- Pflichtethik
- Todesbetrachtungen
### Buch I
Dankbarkeit an Lehrer und Familie:
- Großvater: Würde
- Vater: Bescheidenheit
- Mutter: Frömmigkeit
- Lehrer: Stoische Weisheit
- Antoninus Pius: Herrschertugenden
### Kernthemen
**Logos**: Kosmische Vernunft
**Apatheia**: Leidenschaftslosigkeit
**Prohairesis**: Innere Freiheit
**Pflicht**: Dienst am Ganzen
**Tod**: Natürlicher Prozess
**Gleichmut**: Gegenüber Schicksal
### Stil
Schlicht und eindringlich:
- Kurze Maximen
- Wiederholungen (Selbstvergewisserung)
- Ehrlichkeit über eigene Schwächen
- Keine Pose
- Ringen um Tugendhaftigkeit
## Stoische Philosophie
### Vier Kardinaltugenden
- **Weisheit** (sophia): Einsicht in Natur
- **Gerechtigkeit** (dikaiosyne): Pflicht zu anderen
- **Tapferkeit** (andreia): Standhaftigkeit
- **Mäßigung** (sophrosyne): Selbstbeherrschung
### Kosmopolitismus
- Alle Menschen sind Bürger einer Welt-Polis
- Pflicht zur Menschenliebe (philanthropia)
- Auch Feinde sind Brüder
- Kosmische Perspektive
### Memento Mori
Ständige Vergegenwärtigung der Sterblichkeit:
> "Denke daran, dass du bald niemand mehr sein wirst und nirgends mehr sein wirst."
Aber keine Verzweiflung: Tod als natürlich und notwendig.
### Prohairesis
Innere Freiheit:
- Äußeres unverfügbar
- Innere Haltung allein in eigener Macht
- Niemand kann Geist zwingen
- Selbstbestimmung des Willens
## Berühmte Zitate
> "Das Glück deines Lebens hängt von der Beschaffenheit deiner Gedanken ab."
> "Wenn du am Morgen erwachst, denke daran, was für ein köstlicher Schatz es ist, zu leben, zu atmen, sich zu freuen."
> "Du hast Macht über deinen Geist - nicht über äußere Ereignisse. Erkenne das, und du wirst Stärke finden."
> "Alles, was geschieht, geschieht gerecht."
> "Die Seele wird eingefärbt von den Gedanken."
> "Wie der Gedanke der Seele, so wird die Seele."
## Tod und Nachfolge (180)
### Letzte Tage
Im Feldlager bei Wien (Vindobona). Seuche bricht aus. Marcus erkrankt. Verweigert Nahrung (Selbsttötung durch Fasten?). Empfiehlt den Göttern das Reich.
### Tod am 17. März 180
Stirbt im Feldlager mit 58 Jahren. Wird konsekriert (vergöttlicht). Letzte Worte unklar überliefert. Möglicherweise: "Geht zur aufgehenden Sonne, ich gehe unter."
### Commodus als Nachfolger
Sohn Commodus (Mitkaiser seit 177) wird Alleinherrscher. Katastrophale Regierung. Bruch mit Vater-Idealen. Ende der "Adoptivkaiser-Zeit". Frage: Marcus' größter Fehler?
## Wirkungsgeschichte
### Antike
"Selbstbetrachtungen" wenig bekannt. Ruf als weiser Kaiser. Säule in Rom (Triumphsäule, erhalten). Reiterstatue (heute Kapitol, Kopie).
### Mittelalter
Weitgehend vergessen im Westen. Byzantinisches Reich bewahrt Text. Arabische Philosophen kennen Marc Aurel.
### Renaissance
Wiederentdeckung. Erste Drucke 16. Jh. Begeisterung für antike Weisheit. Fürstenspiegel-Tradition.
### Neuzeit
18./19. Jh.: Bewunderung als "Philosophenkaiser". Gibbon: Höhepunkt römischer Zivilisation. Romantics fasziniert von Melancholie.
### 20./21. Jahrhundert
Ungebrochene Popularität:
- Existentialisten (innere Freiheit)
- Psychotherapie (Kogn
itive Therapie)
- Selbsthilfe-Literatur
- Stoizismus-Renaissance
- Führungskräfte-Lektüre
## Historische Bewertung
### Würdigung
- Letzter großer Kaiser der Pax Romana
- Philosophenkaiser als Ideal
- Menschliche Rechtsprechung
- Pflichterfüllung unter widrigsten Umständen
- Literarisches Meisterwerk
### Kritik
- Christenverfolgung
- Commodus als Nachfolger (Dynastieprinzip statt Adoption)
- Militärisch defensiv
- Reich am Ende geschwächt
- Philosophie unpolitisch?
## Vermächtnis
Marcus Aurelius bleibt der Inbegriff des Philosophenherrschers - Platons Traum (fast) verwirklicht:
- **Selbstbetrachtungen** als zeitlose Weisheit
- **Stoizismus** praktisch gelebt
- **Pflichterfüllung** trotz persönlichem Leid
- **Humanitas** als Herrscherideal
Seine Größe liegt nicht in militärischen Siegen oder territorialen Eroberungen, sondern im Versuch, ein guter Mensch zu sein unter der Last der Macht. Die "Selbstbetrachtungen" zeigen einen Kaiser, der mit sich ringt, der zweifelt, der sich selbst ermahnt - menschlicher als jedes Herrscherdenkmal.
In einer Zeit, da das römische Reich bereits Risse zeigte, hielt er durch Charakter und Pflichtgefühl zusammen, was historische Kräfte auseinanderdrängten. Sein Beispiel lehrt, dass Größe nicht in äußerem Erfolg, sondern in innerer Haltung liegt.`,
};
async function main() {
console.log('📝 Batch 1: Featured Autoren\n');
const authorsDE = loadAuthors('de');
const authorsEN = loadAuthors('en');
let updated = 0;
const updatedDE = authorsDE.map(author => {
if (batch1Bios[author.id]) {
console.log(`${author.name} (${author.id})`);
updated++;
return {
...author,
biography: {
...author.biography,
long: batch1Bios[author.id]
}
};
}
return author;
});
const updatedEN = authorsEN.map(author => {
const deAuthor = updatedDE.find(a => a.id === author.id);
if (deAuthor?.biography?.long && batch1Bios[author.id]) {
return {
...author,
biography: {
...author.biography,
long: deAuthor.biography.long
}
};
}
return author;
});
console.log(`\n📊 ${updated} Biografien hinzugefügt\n`);
writeAuthors(updatedDE, 'de');
writeAuthors(updatedEN, 'en');
console.log('\n✨ Batch 1 fertig!\n');
}
main();

View file

@ -0,0 +1,742 @@
#!/usr/bin/env tsx
/**
* Batch 2: Weitere Featured Autoren
*/
import * as fs from 'fs';
import * as path from 'path';
import { Author } from '../services/contentLoader';
function loadAuthors(lang: 'de' | 'en'): Author[] {
const filePath = path.join(__dirname, `../services/data/authors/${lang}.ts`);
const content = fs.readFileSync(filePath, 'utf-8');
const match = content.match(/export const authors[A-Z]{2}: Author\[\] = (\[[\s\S]*\]);/);
return eval(match[1]) as Author[];
}
function writeAuthors(authors: Author[], lang: 'de' | 'en'): void {
const filePath = path.join(__dirname, `../services/data/authors/${lang}.ts`);
const backupPath = filePath.replace('.ts', `.backup-${Date.now()}.ts`);
fs.copyFileSync(filePath, backupPath);
const authorsJson = JSON.stringify(authors, null, 2);
const tsContent = `import { Author } from '../../contentLoader';
export const authors${lang.toUpperCase()}: Author[] = ${authorsJson};
`;
fs.writeFileSync(filePath, tsContent, 'utf-8');
console.log(`${lang}.ts aktualisiert`);
}
const batch2Bios: Record<string, string> = {
'epiktet': `# Epiktet
*ca. 50 - ca. 138 n. Chr.*
## Kurzbiografie
Epiktet war einer der bedeutendsten stoischen Philosophen der römischen Kaiserzeit. Als Sklave geboren, wurde er zum einflussreichsten Moralphilosophen seiner Epoche. Seine Lehrgespräche ("Diatribai") und das "Handbüchlein der Moral" (Encheiridion) prägten die stoische Ethik nachhaltig. Sein Kerngedanke: Wahre Freiheit liegt in der inneren Haltung, nicht in äußeren Umständen.
## Vom Sklaven zum Philosophen
### Sklaverei (ca. 50-80)
Geboren in Hierapolis (Phrygien, heute Türkei) als Sklave. Name "Epiktet" bedeutet "der Erworbene" oder "Zugekaufte". Gehörte Epaphroditos, mächtigem Freigelassenen Kaiser Neros.
### Körperbehinderung
Ein Bein blieb zeitlebens verkrüppelt. Erzählung (vielleicht Legende): Herr quälte ihn, Epiktet blieb gelassen und sagte voraus, dass Bein brechen würde - geschah, er blieb unerschüttert.
### Philosophische Bildung
Trotz Sklaverei Zugang zu Bildung. Schüler des berühmten Stoikers Gaius Musonius Rufus in Rom. Intensive Auseinandersetzung mit stoischer Philosophie.
### Freilassung (ca. 80)
Nach Neros Tod (68) und Tod des Epaphroditos (95) wurde Epiktet freigelassen. Begann selbst als Philosoph zu lehren in Rom.
## Die Philosophenschule in Nikopolis (ab 93/94)
### Verbannung (93/94)
Kaiser Domitian verbannte Philosophen aus Rom (antintellektuelle Politik). Epiktet zog nach Nikopolis in Epirus (Griechenland).
### Schulgründung
Gründete einflussreiche Philosophenschule. Einfaches Leben in Armut. Unterrichtete im Dialog. Berühmt für eindringliche Lehrgespräche.
### Berühmte Schüler
- **Arrian**: Später Statthalter und Historiker, zeichnete Epiktets Lehren auf
- **Gellius**: Römischer Schriftsteller
- **Kaiser Hadrian**: Besuchte möglicherweise Schule
- **Marcus Aurelius**: Las Epiktet intensiv
### Lebensweise
Lebte asketisch: kleine Hütte, eisernes Lämpchen (Tonlampe gestohlen). Zölibat lange, spät adoption eines Waisenkindes. Bescheidenheit als Philosophenideal.
## Die Lehre
### Dihairesis - Grundunterscheidung
Kern der Philosophie:
**Eph' hemin / Ouk eph' hemin** (In unserer Macht / Nicht in unserer Macht)
**In unserer Macht:**
- Meinungen (doxai)
- Impulse (hormai)
- Begierden (orexeis)
- Abneigungen (ekkl
iseis)
- Kurz: unsere Prohairesis (moralische Wahl)
**Nicht in unserer Macht:**
- Körper
- Besitz
- Ruf
- Ämter
- Alles Äußere
### Prohairesis
Zentral für Epiktet:
- Moralisches Selbst
- Fähigkeit zu wählen und zu urteilen
- Unverwundbar durch Äußeres
- Einziges wahrhaft Eigenes
- Göttlicher Funke im Menschen
### Freiheit
Wahre Freiheit = Unabhängigkeit von Äußerem:
> "Nicht die Dinge beunruhigen die Menschen, sondern ihre Meinungen über die Dinge."
Sklave kann innerlich frei sein, Kaiser innerlich Sklave seiner Leidenschaften.
### Pflichten (Kathekonta)
Soziale Rollen und Pflichten:
- Als Sohn, Vater, Bürger
- Vernunftgemäßes Handeln
- Nicht weltfern, sondern praktisch
- "Handle deiner Rolle gemäß"
## Encheiridion - Das Handbüchlein
### Entstehung
Zusammengestellt von Schüler Arrian aus den Lehrgesprächen (Diatribai). 53 kurze Kapitel. Praktischer Leitfaden. Wurde "Handbüchlein der Moral" genannt.
### Erste Sätze (berühmt)
> "Von den Dingen sind die einen in unserer Gewalt, die anderen nicht. In unserer Gewalt sind Meinung, Trieb, Begierde, Verabscheuung, kurz: alles, was unser eigenes Werk ist."
### Inhalte
- Grundunterscheidung (s.o.)
- Umgang mit Schicksalsschlägen
- Soziale Rollen
- Umgang mit Tod
- Bescheidenheit
- Selbstprüfung
- Askese als Training
### Wirkung
Wurde zum populärsten stoischen Text. Handlich und praktisch. Kompendium der Lebenskunst. Bis heute gelesen.
## Die Lehrgespräche (Diatribai)
### Form
Lebendige Dialoge:
- Arrian als stenographischer Aufzeichner
- Sokrat
ischer Dialog
- Rhetorische Fragen
- Direkter Ton
- Lebensnahe Beispiele
### Themen
- Was ist Philosophie?
- Über die Vorsehung
- Über die Freiheit
- Umgang mit Tyrannen
- Über Krankheit und Tod
- Über Familienpflichten
- Kritik an Epikureern
### Stil
- Derb und direkt
- Bildhafte Sprache
- Alltagsbeispiele
- Humorvoll
- Manchmal rau
### Erhaltung
Von ursprünglich 8 Büchern nur 4 erhalten (durch Arrian). Authentischer als bei anderen antiken Philosophen.
## Zentrale Gedanken
### Gelassenheit (Ataraxia)
Seelenruhe durch:
- Unterscheidung Verfügbar/Unverfügbar
- Nur Inneres kontrollierbar
- Äußeres mit Gleichmut annehmen
- "Amor fati" avant la lettre
### Prüfungen (Gymnasmata)
Leben als Training:
- Äußeres als Test
- Charakterbildung
- Selbstdisziplin
- "Was würde Sokrates tun?"
### Todesverachtung
Tod ist nichts Schlimmes:
- Natürlicher Prozess
- Rückgabe des Geliehenen
- "Wann die Trompete ruft, gehorche"
- Sokrates als Vorbild
### Gottvertrauen
Stoischer Monotheismus:
- Zeus als Vorsehung
- Kosmos vernünftig geordnet
- Mensch als Fragment Gottes
- Vertrauen in göttliche Ordnung
## Berühmte Zitate
> "Nicht die Dinge selbst beunruhigen die Menschen, sondern ihre Meinungen über die Dinge."
> "Verlange nicht, dass die Dinge geschehen, wie du es wünschst, sondern wünsche, dass sie geschehen, wie sie geschehen, und es wird dir gut gehen."
> "Wenn du küsst dein Kindlein oder dein Weib, so sprich, dass du einen Menschen küssest; denn wenn es stirbt, wirst du nicht betrübt sein."
> "Es sind nicht die Dinge, die uns beunruhigen, sondern die Meinungen, die wir von den Dingen haben."
> "Du bist eine kleine Seele, die einen Leichnam mit sich herumträgt."
## Einfluss und Wirkung
### Antike
Marcus Aurelius las Epiktet intensiv ("Selbstbetrachtungen" voller Anklänge). Favorinus kritisierte. Lukian parodierte. Bis Spätantike gelesen.
### Christentum
Frühe Christen fasziniert:
- Simplicius (Neuplatoniker, 6. Jh.): Kommentar
- Mönchstum: Askese-Ideal
- Mittelalter: Weitergel
esen
- "Fast ein Christ" (Kirchenväter)
### Neuzeit
Renaissance-Humanismus: Wiederentdeckung. 1670: Erstmals komplett gedruckt. Einfluss auf:
- **Montaigne**: Essais
- **Pascal**: Gedanken
- **Descartes**: Stoische Ethik
- **Spinoza**: Gelassenheit
### Moderne
Ungebrochene Aktualität:
- **Existenzphilosophie**: Heidegger, Sartre (Freiheitsbegriff)
- **Kognitive Therapie**: Ellis, Beck (Gedanken ändern Gefühle)
- **Resilienz-Forschung**: Umgang mit Unverfügbarem
- **Stoizismus-Renaissance**: 21. Jh. (Ryan Holiday u.a.)
## Epiktet und die Stoa
### Orthodoxer Stoiker
Treu zur Schultradition:
- Chrysipp und Zenon zitiert
- Kleanthes' Hymnus an Zeus
- Keine Innovation in Physik/Logik
- Fokus auf Ethik
### Vereinfachung
Konzentration auf Praktisches:
- Weniger Systematik als Chrysipp
- Dihairesis als Kern
- Alltagstauglich
- Für Nichtphilosophen
### Sokratisches Element
Sokrates als Ideal:
- Dialog-Form
- Selbstprüfung
- Leben nach Prinzipien
- Tod als Befreiung
## Historische Einordnung
### Kaiserzeit-Stoa
Repräsentant der römischen Stoa:
- Nach Seneca, vor Marc Aurel
- Praktische Ethik im Vordergrund
- Politische Zurückhaltung
- Individuelle Seelenführung
### Sklavenperspektive
Einzigartig:
- Philosophie aus Sklavenerfahrung
- Freiheit von innen trotz äußerer Unfreiheit
- Glaubwürdigkeit durch Lebenspraxis
- Radikalität der Haltung
## Vermächtnis
Epiktet bleibt der praktischste Philosoph der Antike:
- **Handbuch fürs Leben** - bis heute benutzbar
- **Freiheit durch Haltung** - zeitlose Weisheit
- **Sklave als Weiser** - Umkehrung aller Werte
- **Unverwundbarkeit** - durch Verzicht auf Unverfügbares
Seine Größe liegt in der Radikalität, mit der er seine Philosophie lebte. Als Sklave lehrte er Freiheit, als Armer Glück, als Körperbehinderter Seelenruhe. Seine Botschaft: Äußere Umstände haben keine Macht über uns, wenn wir es nicht zulassen.
Das Encheiridion ist vielleicht der beste Beweis, dass Philosophie keine akademische Disziplin, sondern Lebenskunst ist. In 53 kurzen Kapiteln zeigt Epiktet, wie man ein gutes Leben führt - nicht in perfekten Umständen, sondern trotz aller Widrigkeiten.
Sein Einfluss reicht von Marc Aurel bis zur modernen Psychotherapie. Wann immer Menschen lernen müssen, mit Unverfügbarem umzugehen, bleibt Epiktet aktuell. In unsicheren Zeiten ist sein Rat zeitlos: Konzentriere dich auf das, was du ändern kannst, und akzeptiere, was du nicht ändern kannst.`,
'coelho-paulo': `# Paulo Coelho
*24. August 1947 - heute*
## Kurzbiografie
Paulo Coelho ist ein brasilianischer Schriftsteller und einer der meistgelesenen Autoren der Gegenwart. Sein Welterfolg "Der Alchimist" (1988) wurde in über 80 Sprachen übersetzt und mehr als 150 Millionen Mal verkauft. Seine spirituellen Romane verbinden mystische Weisheit mit modernem Sinnsuchen und inspirieren Millionen von Lesern weltweit. Von der Militärdiktatur verfolgt und dreimal in psychiatrische Kliniken eingewiesen, wurde er zum Bestsellerautor und Mitglied der Brasilianischen Akademie der Literatur.
## Frühe Jahre in Rio de Janeiro (1947-1970)
### Mittelschichtfamilie
Geboren in Rio de Janeiro als Sohn eines Ingenieurs. Jesuitenschule. Wollte Schriftsteller werden - Eltern wünschten Ingenieur. Frühe Rebellion gegen Konventionen.
### Psychiatrische Einweisungen
Mit 17, 20 und 21 Jahren von Eltern in psychiatrische Kliniken eingewiesen. Angeblich "abnormales Verhalten". Elektroschocks. Traumatische Erfahrung. Versuch, Träume zu töten.
### Hippie-Jahre
1960er: Teil der Gegenkultur. Langes Haar, Drogen, alternativer Lebensstil. Gegen Militärdiktatur. Interesse an Mystik und Okkultismus.
## Der Weg zur Magie (1970-1987)
### Journalist und Songwriter
Arbeit als Journalist. Erfolgreicher Texter für brasilianische Rockmusik. Raul Seixas als Partner. Hits in Brasilien. Finanzieller Erfolg, aber innerliche Leere.
### Verhaftung (1974)
Militärdiktatur verhaftet ihn wegen subversiver Texte. Folter angedroht. Nach Tagen freigelassen. Traumatische Erfahrung verstärkt Sinnsuche.
### Satanismus-Phase
Kurze Beschäftigung mit schwarzer Magie und Satanismus. Führte zu Krise. Erkannte Gefahr. Wendung zur Spiritualität.
### Pilgerreise nach Santiago (1986)
Entscheidende Erfahrung: Jakobsweg mit Frau Christina. 700 km zu Fuß. Spirituelle Transformation. Grundlage für ersten Roman "Auf dem Jakobsweg" (1987).
### RAM-Orden
Mitglied des katholisch-mystischen Ordens RAM (Regnus Agnus Mundi). Verbindung von Christentum und Esoterik. Rituale und Initiationen.
## Der Durchbruch (1988-1993)
### Der Alchimist (1988)
Parabel über einen andalusischen Hirten auf der Suche nach Schatz. Erste Auflage: 900 Exemplare, Flop. Verlag gibt auf. Neuer Verlag 1990. Plötzlich Mundpropaganda. Bestseller in Brasilien.
### Weltweiter Erfolg
Übersetzungen in alle Sprachen. 1993 Durchbruch in USA. Oprah Winfrey-Empfehlung. Phänomen: Je mehr Zeit vergeht, desto mehr Verkäufe. Kultbuch der Selbstfindung.
### Themen des Alchimisten
- Persönliche Legende (Lebensaufgabe)
- Weltenseele
- Zeichen lesen lernen
- Träume verwirklichen
- Schatz im eigenen Herzen
### Kritik und Verteidigung
Kritiker: Kitschig, esoterisch, seicht. Fans: Inspirierend, lebensverändernd. Coelho: "Ich schreibe fürs Herz, nicht für Kritiker."
## Schriftstellerleben (1993-heute)
### Produktivität
Etwa alle zwei Jahre ein neuer Roman. Über 30 Bücher. Routinierter Arbeitsprozess. Vorbereitung durch Recherche und Reisen.
### Themen
Wiederkehrende Motive:
- Spirituelle Suche
- Persönliche Transformation
- Mut zum Träumen
- Liebe als Kraft
- Zeichen und Omen
- Schicksal und freier Wille
### Wichtige Werke
- **Am Ufer des Rio Piedra saß ich und weinte** (1994): Liebe und spirituelles Erwachen
- **Veronika beschließt zu sterben** (1998): Psychiatrie und Lebenswille
- **Der Dämon und Fräulein Prym** (2000): Gut und Böse
- **Elf Minuten** (2003): Sexualität und heilige Prostitution
- **Der Zahir** (2005): Obsession und Freiheit
- **Die Hexe von Portobello** (2006): Weibliche Spiritualität
- **Aleph** (2011): Zeitreise und Reinkarnation
### Kontroversen
- Plagiatsvorwürfe (nie bewiesen)
- Esoterik-Kritik
- Vereinfachung spiritueller Traditionen
- Kommerzialismus
## Erfolg und Zahlen
### Verkaufszahlen
- Über 320 Millionen verkaufte Bücher
- Übersetzungen in 83 Sprachen
- Meist-übersetzer Autor der Welt (Guinness)
- "Der Alchimist" allein über 150 Millionen
### Auszeichnungen
- Mitglied der Brasilianischen Akademie der Literatur (2002)
- Chevalier de l'Ordre national de la Légion d'honneur (Frankreich)
- Crystal Award (Weltwirtschaftsforum)
- Zahlreiche internationale Ehrungen
### Einfluss
Leser aus allen Kulturen. Besonders erfolgreich in:
- Iran (trotz Zensur)
- Türkei
- Japan
- Russland
- Europa
- Lateinamerika
## Philosophie und Spiritualität
### Persönliche Legende
Kernkonzept:
- Jeder hat einzigartige Lebensaufgabe
- Universum hilft bei Verwirklichung
- Zeichen weisen den Weg
- Mut erfordert, zu folgen
### Synchronizität
Inspiration durch C.G. Jung:
- Bedeutungsvolle Zufälle
- Alles ist verbunden
- Achte auf Omen
- Universum spricht zu uns
### Weltenseele
Stoische/neuplatonische Idee:
- Alles ist eins
- Liebe als verbindende Kraft
- Kommunikation mit Kosmos möglich
- Pantheistische Elemente
### Katholizismus + Esoterik
Eigenwillige Mischung:
- Katholisch getauft und sozialisiert
- Jakobsweg als katholische Tradition
- Aber: Reinkarnation, Magie, I Ging
- Synkretismus
## Digitale Präsenz
### Social Media Pioneer
Früher Blogger (seit 1996). Aktiv auf:
- Facebook: Millionen Follower
- Twitter/X: Inspirierende Tweets
- Instagram: Visuell-spirituell
Teilt Gedanken, Reisen, Weisheiten täglich.
### Piraterie-Haltung
Ungewöhnlich: Toleriert illegale Downloads. "Wichtig ist, gelesen zu werden, nicht Geld." Sieht Piraterie als Marketing. Dennoch Bestseller.
## Paulo Coelho als Person
### Ehe
Seit 1980 mit der Künstlerin Christina Oiticica verheiratet. Keine Kinder. Jakobsweg gemeinsam. Sie inspiriert viele Charaktere.
### Wohnsitze
Hauptwohnsitz: Genf (Schweiz). Auch in Rio. Vielreisend. Kosmopolit.
### Routine
Disziplinierter Arbeiter. Morgenschreiber. Recherche-intensiv. Jeden Tag kleine Schritte. "Zehn Prozent Inspiration, neunzig Prozent Transpiration."
### Persönlichkeit
Öffentlich: Charismatisch, spirituell, weise. Privat: Diszipliniert, fleißig, strategisch. Geschäftsmann und Mystiker zugleich.
## Kritik
### Literarisch
- "Esoterischer Kitsch"
- "Oberflächlich"
- "Vereinfachend"
- "Kommerziell"
- "Selbsthilfe, keine Literatur"
### Philosophisch
- "Synkretistische Beliebigkeit"
- "Konsumismus der Spiritualität"
- "Falsche Versprechungen"
- "Individualismus statt Soziales"
### Verteidigung
Coelho: "Ich schreibe keine Literatur für Professoren. Ich schreibe, um Menschen zu helfen, sich selbst zu finden."
## Berühmte Zitate
> "Wenn du etwas wirklich willst, dann wird das gesamte Universum darauf hinwirken, dass du es erreichst."
> "Die Welt liegt in den Händen derer, die den Mut haben, zu träumen und das Risiko einzugehen, ihre Träume zu leben."
> "Du bist nicht krank, du bist traurig. Es ist
die moderne Welt."
> "Es gibt nur eine Sache, die einen Traum unmöglich macht: die Angst vor dem Scheitern."
> "Jeder Tag ist ein neuer Anfang."
## Vermächtnis
Paulo Coelho bleibt einer der einflussreichsten Schriftsteller des 21. Jahrhunderts:
- **Inspirator von Millionen** - Leser finden Mut zum Träumen
- **Brückenbauer** zwischen Kulturen und Religionen
- **Pionier** der spirituellen Literatur
- **Phänomen** der Mundpropaganda-Vermarktung
Seine Größe liegt nicht in literarischer Kunstfertigkeit (die Kritiker vermissen), sondern in der Fähigkeit, Menschen zu berühren. Er gibt einfache Antworten auf komplexe Fragen - und genau das suchen Millionen.
Ob man seine Philosophie teilt oder nicht: Der Einfluss ist unbestreitbar. Ganze Generationen wurden von ihm inspiriert, ihre "persönliche Legende" zu suchen. In einer desillusionierenden Welt bietet er Hoffnung - und das ist vielleicht wichtiger als literarische Perfektion.`,
'gibran-khalil': `# Khalil Gibran
*6. Januar 1883 - 10. April 1931*
## Kurzbiografie
Khalil Gibran (arabisch جبران خليل جبران) war ein libanesisch-amerikanischer Dichter, Philosoph und Maler. Sein Hauptwerk "Der Prophet" (1923) gehört zu den meistverkauften Büchern aller Zeiten und wurde in über 100 Sprachen übersetzt. Als Brücke zwischen östlicher Mystik und westlicher Moderne prägte er eine Generation von Suchenden. Seine poetisch-philosophischen Texte über Liebe, Freiheit und menschliche Bestimmung berühren bis heute Menschen weltweit.
## Kindheit im Libanon (1883-1895)
### Bsharri
Geboren in Bsharri, christlich-maronitisches Bergdorf im Nordlibanon. Familie gehörte zur christlichen Minderheit. Vater Khalil Sa'd Gibran: Steuereintreiber, Trinker, Glücksspieler. Mutter Kamila Rahmeh: stark, fromm, Halt der Familie.
### Schwierige Familienverhältnisse
Vater verschuldet, inhaftiert wegen Korruption. Familie verarmt. Mutter entscheidet zur Auswanderung. Libanesische Diaspora nach Amerika.
### Prägende Landschaft
Heilige Zedern des Libanon. Wadi Qadisha (Heiliges Tal). Klöster und Einsiedeleien. Landschaft durchzog alle späteren Werke.
## Boston und New York (1895-1931)
### Einwanderung nach Boston (1895)
Mit Mutter und Geschwistern (Vater blieb). Armes Einwandererviertel. Khalil lernte Englisch in öffentlicher Schule. Talent für Zeichnen entdeckt.
### Fred Holland Day
Fotograf und Kulturmäzen entdeckt Khalil. Führt ihn in Bostoner Bohème ein. Gibran porträtiert prominente Persönlichkeiten. Erste künstlerische Förderung.
### Rückkehr in den Libanon (1898-1902)
Zur weiteren Ausbildung nach Beirut, Collège de la Sagesse. Studium Arabisch, Französisch, Literatur. Begeisterung für arabische Romantik. Erste arabische Gedichte.
### Familiäre Tragödien (1902-1904)
Rückkehr nach Boston. Schwester Sultana stirbt an Tuberkulose (14 Jahre). Bruder Boutros stirbt (Tuberkulose). Mutter stirbt (Krebs). Innerhalb von zwei Jahren drei Verluste. Nur Schwester Marianna überlebt, wird lebenslange Stütze.
### Mary Elizabeth Haskell
1904 Begegnung mit Schulleiterin Mary Haskell (10 Jahre älter). Mäzenin, Mentorin, Vertraute. Finanziert Studium in Paris. Briefwechsel über 25 Jahre. Komplizierte Liebe (Ehe abgelehnt, aber lebenslang verbunden).
### Paris (1908-1910)
Studium an der Académie Julian. Treffen mit Rodin (großer Einfluss). Verkehr in Künstlerkreisen. Malstil entwickelt sich. Beeinflusst von Symbolismus und Art Nouveau.
### New York (ab 1911)
Umzug nach New York, Atelier in Greenwich Village. Schreibt zunehmend auf Englisch. Kontakte zu Literatenszene. Marianna führt Haushalt und Galerie.
## Der arabische Erneuerer (1905-1918)
### "Tränen und Lachen" (1914)
Erste Sammlung arabischer Essays. Kritik an religiösem Dogmatismus. Angriff auf Unterdrückung. Ruf nach Freiheit und Liebe.
### "Die gebrochenen Flügel" (1912)
Arabischer Roman. Tragische Liebesgeschichte. Kritik an arrangierten Ehen und Patriarchat. Bestseller in arabischer Welt.
### Al-Mahjar - Die Emigrantendichter
Mitbegründer der "Pen League" arabischer Emigranten-Schriftsteller in New York. Mit Ameen Rihani, Mikhail Naimy. Erneuerung arabischer Literatur. Befreiung von klassischen Formen.
### Sprache und Stil
Biblischer Ton in Arabisch. Kurze, poetische Sätze. Parabeln und Gleichnisse. Natursymbolik. Mystische Bilder.
## Der Prophet (1923)
### Entstehung
Jahrelange Arbeit. Immer wieder überarbeitet. Mary Haskell korrigierte Englisch. Vereint östliche Weisheit und westliche Moderne.
### Struktur
Almustafa (der Prophet) verlässt Stadt Orphalese nach 12 Jahren. Bewohner bitten um Weisheit. 26 Themen in poetischer Prosa.
### Themen
- Liebe
- Ehe
- Kinder
- Arbeit
- Freude und Schmerz
- Häuser
- Kleider
- Kaufen und Verkaufen
- Verbrechen und Strafe
- Gesetze
- Freiheit
- Vernunft und Leidenschaft
- Schmerz
- Selbsterkenntnis
- Lehren
- Freundschaft
- Sprechen
- Zeit
- Gut und Böse
- Gebet
- Wonne
- Schönheit
- Religion
- Tod
- Abschied
### Über die Liebe (berühmteste Passage)
> "Wenn die Liebe dir winkt, folge ihr, sind ihre Wege auch schwer und steil."
> "Die Liebe gibt nichts als sich selbst und nimmt nichts als von sich selbst."
### Über Kinder
> "Eure Kinder sind nicht eure Kinder. Sie sind die Söhne und Töchter der Sehnsucht des Lebens nach sich selbst."
> "Ihr könnt ihnen eure Liebe geben, aber nicht eure Gedanken, denn sie haben ihre eigenen Gedanken."
### Erfolg
Anfangs bescheiden. Dann Mundpropaganda. Steady Seller über Jahrzehnte. Über 100 Millionen verkaufte Exemplare. Dritt-meist-verkauftes poetisches Werk nach Bibel und Laozi.
## Philosophie und Spiritualität
### Mystische Einheit
Alle Religionen als Wege zum Einen. Sufismus, Christentum, östliche Weisheit verschmolzen. Pantheistische Grundhaltung. Gott in allem.
### Freiheit und Liebe
Zentrale Werte:
- Freiheit als höchstes Gut
- Liebe als göttliche Kraft
- Individuum vor Institution
- Intuition vor Dogma
### Schmerz und Freude
Paradoxes Denken:
- Schmerz und Freude untrennbar
- Leid als Lehrmeister
- Tod als Übergang
- Gegensätze vereint
### Soziale Kritik
Gegen:
- Materialismus
- Unterdrückung
- Religiösen Zwang
- Patriarchat
- Imperialismus
## Als Maler
### Stil
Symbolistisch. Fließende, erotische Formen. Engel, Propheten, weibliche Akte. Blake-Einfluss. Mystische Vision.
### Ausstellungen
Regelmäßige Ausstellungen in New York und Boston. Porträts von Prominenten (Yeats, Jung, Tagore). Verkäufe bescheiden, aber respektiert.
### Buchillustrationen
Illustrierte eigene Werke. Verschmelzung von Text und Bild. Gesamtkunstwerk-Anspruch.
## Späte Werke
### "Der Garten des Propheten" (1933, posthum)
Fortsetzung des Propheten. Unvollendet. Von Mary Haskell zusammengestellt.
### "Jesus, der Menschensohn" (1928)
Jesus aus Perspektive von 77 Zeitgenossen. Humanistisches Jesus-Bild. Christus als Poet und Rebell.
### "Sand und Schaum" (1926)
Aphorismen-Sammlung. Poetische Kurztexte. Mystische Weisheiten.
## Tod und Vermächtnis (1931)
### Krankheit
Leberzirrhose und Tuberkulose. Lebenslang Lungenprobleme. Alkoholkonsum verschlimmerte. Letztes Jahr im Krankenhaus.
### Tod
10. April 1931 in New York, 48 Jahre alt. Letzte Worte (angeblich): "This is my last breath, and I love you all."
### Rückführung
Leichnam nach Libanon überführt. Beigesetzt in Mar Sarkis-Kloster, Bsharri. Heute Museum (Gibran-Museum). Nationalheld des Libanon.
### Testament
Vermögen und Werke an Bsharri. Schwester Marianna verwaltete Nachlass bis 1972.
## Wirkung und Rezeption
### Kultbuch der 1960er/70er
"Der Prophet" Lieblingsbuch der Hippies. Hochzeiten und Beerdigungen. Gegenkultur-Bibel. John F. Kennedy zitierte Gibran. Elvis besaß "Der Prophet".
### Kritik
Literaturkritiker: Kitschig, seicht, pseudo-mystisch. "Hallmark-Philosophie". Zu eklektisch. Zu glatt.
### Verteidigung
Fans: Authentisch berührend. Zeitlose Weisheit. Trost spendend. Form egal, wenn Herz erreicht.
### Bis heute
Millionen Exemplare jährlich verkauft. Übersetzungen in 110+ Sprachen. Besonders populär: USA, Arabische Welt, Indien, Lateinamerika.
## Berühmte Zitate
> "Du sollst frei sein von allem, ausgenommen von deiner Freiheit."
> "Und vergesst nicht, dass die Erde sich freut, eure nackten Füße zu fühlen, und die Winde sehnen sich danach, mit eurem Haar zu spielen."
> "Denn was ist Böses, wenn nicht Gutes, das von seinem eigenen Hunger und Durst gequält wird?"
> "Ich lerne Schweigen von den Geschwätzigen, Toleranz von den Intoleranten und Freundlichkeit von den Unfreundlichen. Seltsam, dass ich diesen Lehrern nicht dankbar bin."
> "Wenn du Liebe gibst, gibst du alles."
## Vermächtnis
Khalil Gibran bleibt ein Prophet des 20. Jahrhunderts:
- **Brücke** zwischen Ost und West
- **Stimme** der arabischen Renaissance
- **Dichter** der universalen Spiritualität
- **Künstler** der mystischen Vision
Seine Größe liegt in der Fähigkeit, tiefe Wahrheiten in einfacher Schönheit auszudrücken. Ob man seine Philosophie als tief oder seicht beurteilt - sein Einfluss auf Millionen ist unbestreitbar.
In einer fragmentierten Welt bot er die Vision der Einheit. In einer Zeit des Materialismus erinnerte er an das Spirituelle. In einer Ära des Dogmas predigte er die Freiheit. "Der Prophet" bleibt ein Geschenk an die Menschheit - imperfekt vielleicht, aber authentisch aus dem Herzen eines Suchenden.`,
};
async function main() {
console.log('📝 Batch 2: Featured Autoren\n');
const authorsDE = loadAuthors('de');
const authorsEN = loadAuthors('en');
let updated = 0;
const updatedDE = authorsDE.map(author => {
if (batch2Bios[author.id]) {
console.log(`${author.name} (${author.id})`);
updated++;
return {
...author,
biography: {
...author.biography,
long: batch2Bios[author.id]
}
};
}
return author;
});
const updatedEN = authorsEN.map(author => {
const deAuthor = updatedDE.find(a => a.id === author.id);
if (deAuthor?.biography?.long && batch2Bios[author.id]) {
return {
...author,
biography: {
...author.biography,
long: deAuthor.biography.long
}
};
}
return author;
});
console.log(`\n📊 ${updated} Biografien hinzugefügt\n`);
writeAuthors(updatedDE, 'de');
writeAuthors(updatedEN, 'en');
console.log('\n✨ Batch 2 fertig!\n');
}
main();

View file

@ -0,0 +1,968 @@
#!/usr/bin/env tsx
/**
* Batch 3: Bruce Lee, Charlie Chaplin, David Ben-Gurion, Jean-Jacques Rousseau
*/
import * as fs from 'fs';
import * as path from 'path';
import { Author } from '../services/contentLoader';
function loadAuthors(lang: 'de' | 'en'): Author[] {
const filePath = path.join(__dirname, `../services/data/authors/${lang}.ts`);
const content = fs.readFileSync(filePath, 'utf-8');
const match = content.match(/export const authors[A-Z]{2}: Author\[\] = (\[[\s\S]*\]);/);
return eval(match[1]) as Author[];
}
function writeAuthors(authors: Author[], lang: 'de' | 'en'): void {
const filePath = path.join(__dirname, `../services/data/authors/${lang}.ts`);
const backupPath = filePath.replace('.ts', `.backup-${Date.now()}.ts`);
fs.copyFileSync(filePath, backupPath);
const authorsJson = JSON.stringify(authors, null, 2);
const tsContent = `import { Author } from '../../contentLoader';
export const authors${lang.toUpperCase()}: Author[] = ${authorsJson};
`;
fs.writeFileSync(filePath, tsContent, 'utf-8');
console.log(`${lang}.ts aktualisiert`);
}
const batch3Bios: Record<string, string> = {
'lee-bruce': `# Bruce Lee
*27. November 1940 - 20. Juli 1973*
## Kurzbiografie
Bruce Lee war ein chinesisch-amerikanischer Kampfkünstler, Schauspieler, Filmregisseur und Philosoph, der die Kampfkunst revolutionierte und zum globalen Kultstar wurde. Als Begründer des Jeet Kune Do verband er östliche Kampfkunst mit westlicher Effizienz und eigener Philosophie. Seine Filme machten asiatische Kampfkunst weltweit populär und durchbrachen rassistische Barrieren Hollywoods. Trotz seines frühen Todes mit 32 Jahren bleibt sein Einfluss auf Kampfkunst, Film und Popkultur ungebrochen.
## Kindheit zwischen Ost und West (1940-1959)
### Geburt in San Francisco
Geboren als Lee Jun-fan () in San Francisco während Tournee seines Vaters Lee Hoi-chuen, kantonesischer Opern-Star. Amerikanische Staatsbürgerschaft. Chinesischer Name bedeutet "Rückkehr wieder" - Hoffnung auf Rückkehr nach Amerika.
### Hongkong-Jahre
Familie kehrt nach Hongkong zurück (1941). Aufwachsen in Kowloon. Wohlhabende Familie, aber turbulente Zeiten. Japanische Besatzung (1941-1945) überstanden. Straßenkämpfe in der Jugend.
### Kinderstar
Ab 5 Jahren in kantonesischen Filmen. Über 20 Kinderfilme bis 18. Rolle in "The Kid" (1950) machte bekannt. Bezauberndes Kind, später rebellischer Teenager.
### Wing Chun bei Yip Man
Mit 13 Jahren beginnt Training bei Meister Yip Man (Ip Man). Wing Chun-Kung Fu. Zunächst als Straßenkampf-Training. Entwickelt Leidenschaft. Trainiert obsessiv. Yip Man erkennt Talent.
### Straßenkämpfe
Teenager-Banden in Hongkong. Bruce oft in Schlägereien. Eltern besorgt. Polizeiliche Probleme drohen. Entscheidung: Bruce muss weg nach Amerika.
## Amerika: Von Seattle nach Hollywood (1959-1971)
### Neuanfang in Seattle (1959)
Mit 18 nach San Francisco, dann Seattle. Bei Freunden der Familie. Arbeitet als Kellner. Beendet Highschool. Studiert Philosophie an University of Washington.
### Kampfkunstschule in Seattle
Beginnt Wing Chun zu unterrichten. Jun Fan Gung Fu Institute eröffnet. Unorthodox: Unterrichtet auch Nicht-Chinesen (tabu). Entwickelt eigene Methoden.
### Linda Emery
Trifft Linda, weiße Studentin, in seiner Kampfkunstklasse. Liebe gegen Konventionen. Heirat 1964. Tochter Shannon (1969), Sohn Brandon (1965). Linda wird lebenslange Partnerin.
### Oakland und das zweite Dojo (1964)
Umzug nach Oakland. Zweite Schule. Kontroverse mit traditioneller chinesischer Kampfkunst-Gemeinde. Herausforderungskampf gegen Wong Jack Man (umstrittener Ausgang). Bruce gewinnt, aber unzufrieden mit eigener Performance. Beginn der JKD-Entwicklung.
### Long Beach International Karate Championships (1964)
Demonstration revolutioniert amerikanische Kampfkunst-Szene. One-Inch-Punch (Ein-Zoll-Schlag) begeistert Publikum. Two-Finger-Pushups. Produzent Jay Sebring entdeckt ihn. Weg nach Hollywood beginnt.
### Kato in "The Green Hornet" (1966-1967)
TV-Serie, Bruce spielt Sidekick Kato. Serie floppt nach einer Staffel. Aber: Bruce wird "Kato" für Amerika. Kann nicht von Typcasting wegkommen. Hollywood bietet nur stereotype Rollen.
### Privatlehrer der Stars
Unterrichtet Hollywood-Prominenz: Steve McQueen, James Coburn, Kareem Abdul-Jabbar, Roman Polanski. Verdient gut. Frustriert von fehlenden Hauptrollen. Rassismus Hollywoods: Asiate nicht als Held vorstellbar.
### "Kung Fu" - Die gestohlene Rolle
Entwickelt Konzept für Serie über Shaolin-Mönch im Wilden Westen. Warner Brothers interessiert. Bruce soll Star sein. Stattdessen: David Carradine (weiß) bekommt Rolle. Bruce tief verletzt. Entscheidet: Zurück nach Hongkong.
## Hongkong: Der Durchbruch (1971-1973)
### "The Big Boss" (Fist of Fury, 1971)
Rückkehr nach Hongkong. Low-Budget-Film mit Golden Harvest. Sensation! Bricht alle Kassenrekorde. Bruce Lee über Nacht Megastar in Asien.
### "Fist of Fury" (1972)
Zweiter Film. Noch größerer Erfolg. Chinesischer Nationalist gegen japanische Unterdrücker. Politisch aufgeladen. Hongkong-Kinos überfüllt. Hysterie.
### "Way of the Dragon" (1972)
Bruce schreibt, regie
rt, choreographiert, und spielt Hauptrolle. Erste komplett selbst kontrollierte Produktion. Kolosseum-Kampf gegen Chuck Norris (Höhepunkt). Noch ein Rekordbrecher.
### Rückkehr nach Hollywood als Star
Warner Brothers bemerkt Erfolg. Hollywood will Bruce jetzt. "Enter the Dragon" (1973) - erste Hollywood-Hongkong-Koproduktion. Großes Budget. Internationaler Release.
### "Enter the Dragon" (1973)
Fertiggestellt Monate vor Tod. Veröffentlicht nach Tod (August 1973). Weltweit Kassenerfolg. Macht Kampfkunst-Filme global. Bruce posthumer Superstar. Film-Klassiker.
## Jeet Kune Do - Die Philosophie
### "Der Weg der abfangenden Faust"
Entwickelt ab Mitte 1960er. Jeet Kune Do (JKD): "Intercepting Fist Way". Keine feste Stilform. Prinzip: "Using no way as way, having no limitation as limitation."
### Grundprinzipien
- **Einfachheit**: Direkte, effiziente Bewegungen
- **Direktheit**: Kürzester Weg zum Ziel
- **Persönlicher Ausdruck**: Jeder entwickelt eigenen Stil
- **Wissenschaftlich**: Basiert auf Biomechanik, nicht Tradition
- **"Absorb what is useful"**: Nimm, was funktioniert, aus allen Stilen
### Ablehnung der Tradition
Kritik an klassischen Kampfkünsten:
- Zu ritualisiert
- Unrealistisch im echten Kampf
- Formgebunden
- "Classical mess"
### Philosophischer Einfluss
- Taoismus (Wasser-Prinzip)
- Zen-Buddhismus
- Krishnamurti (Freiheit von Konditionierung)
- Westliche Philosophie (Studium)
### "Be Water, My Friend"
Berühmteste Metapher:
> "Be like water. Water can flow or it can crash. Be water, my friend."
Anpassungsfähigkeit als höchste Tugend.
## Der Philosoph-Krieger
### Studium und Lektüre
Philosophie-Student an UW. Begeisterung für:
- Jiddu Krishnamurti (größter Einfluss)
- Alan Watts (Zen)
- Lao Tzu (Taoismus)
- Spinoza
- Hegel (Dialektik)
### Eigene Notizen
Tausende Seiten Notizen. Mixing von Kampfkunst und Philosophie. Skizzen, Diagramme, Zitate, Reflexionen. Nach Tod als Bücher veröffentlicht ("Tao of Jeet Kune Do", "Striking Thoughts").
### Zen und Kampfkunst
No-Mind (Mushin): Handeln ohne Denken. Spontaneität. Unmittelbarkeit. Kampf als Meditation.
### Selbstverwirklichung
"Honestly expressing yourself" zentral. Authentizität wichtiger als Technik. Kampfkunst als Weg zur Selbsterkenntnis.
## Training und Körper
### Obsessives Training
Trainierte 7 Tage/Woche, mehrere Stunden täglich:
- Kung Fu (Technik)
- Gewichtheben (Kraft)
- Cardio (Ausdauer)
- Flexibilität (Stretching)
- Ernährung (Protein-Shakes, Supplemente)
### Körperliche Perfektion
- 141 Pfund (64 kg) bei 5'7" (1,70m)
- Körperfett: ~5-7%
- Unglaubliche Definition
- Geschwindigkeit: Schläge zu schnell für normale Film-Kameras
- One-Inch-Punch: 300+ Pfund Kraft
### Verletzung (1970)
Rückenverletzung durch Übertraining (Good Morning Exercise ohne Aufwärmen). Ärzte: "Nie wieder Kampfkunst." Bruce ignoriert. Monatelange Schmerzen. Trainiert sich zurück. Noch intensiver danach.
### Nahrungsergänzung
Pionier in Sport-Ernährung:
- Protein-Shakes
- Vitamine
- Ginseng
- Experimente mit allem (auch Problematisches)
## Der mysteriöse Tod (20. Juli 1973)
### Letzte Wochen
Arbeitet an "Game of Death" (unvollendet). Meetings für neue Projekte. Auf Höhepunkt der Karriere. Gesundheitliche Probleme (Kopfschmerzen, Kollaps am Set Wochen zuvor).
### Der Tag
20. Juli 1973, Hongkong. Meeting mit Produzent Raymond Chow. Dann zu Betty Ting Pei (Schauspielerin, Geliebte?) Apartment. Kopfschmerzen. Nimmt Equagesic (Schmerzmittel). Legt sich hin.
### Tod
Wacht nicht auf. Notarzt gerufen. Krankenhaus: "Dead on Arrival." 32 Jahre alt.
### Offizielle Todesursache
"Tod durch Misadventure": Allergische Reaktion auf Equagesic (enthält Aspirin und Muskelrelaxans). Gehirn-Ödem. Kontroverser Autopsiebericht.
### Verschwörungstheorien
Bis heute:
- Fluch (Sohn Brandon starb auch jung, 1993)
- Mafia/Triaden
- Kampfkunst-Rivalen
- Gift
- Energetisches Ungleichgewicht (zu viel Yang)
Wahrscheinlich: Kombination aus Medikament, Hitze, Überanstrengung, ev. Vorerkrankung.
### Beerdigung
Seattle, Lake View Cemetery. 25.000 Menschen. Steve McQueen, James Coburn trugen Sarg. Linda im schwarzen Schleier mit Kindern.
## Vermächtnis
### Film-Revolution
- Erste asiatische Actionstar in Hollywood
- Kampfkunst-Genre global
- Beeinflusste: Jackie Chan, Jet Li, Donnie Yen, Matrix, Kill Bill
- Stereotypen durchbrochen (teilweise)
### Kampfkunst-Revolution
- Mixed Martial Arts (MMA) Vorläufer
- Cross-Training normalisiert
- Realismus vor Tradition
- Jeet Kune Do weltweit praktiziert
### Popkultur-Ikone
- Gelber Trainingsanzug (Kill Bill)
- Nunchaku
- Schrei/Battle Cry
- Philosophische Zitate
- Unzählige Parodien und Hommagen
### Kinder
- **Brandon Lee** (1965-1993): Schauspieler, starb bei Drehunfall ("The Crow")
- **Shannon Lee** (1969-): Schauspielerin, Produzentin, verwaltet Erbe
### Kontroversen im Vermächtnis
- Authentizität vs. Selbsterfindung
- Gewaltverherrlichung?
- Kulturelle Aneignung oder Brückenbau?
- "White Savior" in umgekehrter Form?
## Berühmte Zitate
> "Be like water, my friend."
> "Ich fürchte nicht den Mann, der 10.000 Kicks einmal geübt hat, aber ich fürchte den Mann, der einen Kick 10.000 mal geübt hat."
> "Knowing is not enough, we must apply. Willing is not enough, we must do."
> "The successful warrior is the average man, with laser-like focus."
> "Absorb what is useful, discard what is not, add what is uniquely your own."
> "Mistakes are always forgivable, if one has the courage to admit them."
## Das Phänomen Bruce Lee
Bruce Lee bleibt über 50 Jahre nach seinem Tod eine globale Ikone:
- **Kampfkunst-Revolutionär**: Befreite Martial Arts vom Dogma
- **Filmstar**: Durchbrach rassistische Barrieren
- **Philosoph**: Blend von Ost und West
- **Kultfigur**: Symbol für Selbstbestimmung
Seine Größe lag in der Verbindung scheinbar unvereinbarer Welten: Tradition und Innovation, Ost und West, Körper und Geist, Kampf und Philosophie. Er lebte, was er predigte: authentischer Selbstausdruck ohne Grenzen.
Der frühe Tod verwandelte eine aufsteigende Karriere in einen Mythos. Aber auch ohne Tod wäre sein Einfluss revolutionär gewesen. Bruce Lee lehrte, dass Grenzen - kulturelle, physische, philosophische - nur im Kopf existieren. Seine Botschaft bleibt zeitlos: Sei du selbst, aber die beste Version davon.`,
'chaplin-charlie': `# Charlie Chaplin
*16. April 1889 - 25. Dezember 1977*
## Kurzbiografie
Sir Charles Spencer Chaplin war ein britischer Schauspieler, Regisseur, Komponist und Komiker, der zur Ikone der Stummfilm-Ära wurde. Seine Figur des "Tramp" mit Melone, Gehstock und Watschelgang gehört zu den bekanntesten Gestalten der Filmgeschichte. Als genialer Pantomime und scharfsichtiger Sozialkritiker schuf er Meisterwerke wie "Modern Times" und "The Great Dictator". Chaplins Leben war geprägt von künstlerischem Triumph und persönlichen Tragödien, politischer Verfolgung und schließlich weltweiter Ehrung.
## Elend in London (1889-1913)
### Music Hall Eltern
Geboren in Walworth, London. Vater Charles Chaplin Sr.: Music Hall-Sänger, Alkoholiker. Mutter Hannah Hill: Sängerin "Lily Harley". Beide auf Bühne mäßig erfolgreich. Ehe scheitert früh.
### Mutters Zusammenbruch
Hannah verliert Stimme auf Bühne. Fünfjähriger Charlie muss weitersingen. Erste "Performance". Mutter mentale Krankheit (evtl. Syphilis). Immer wieder in Irrenanstalten. Traumatische Trennungen.
### Armut und Waisenhäuser
Leben in bitterster Armut. Workhouses (Arbeitshäuser). Lambeth-Viertel: Slums. Hunger, Kälte, Angst. Prägt lebenslang. Tramp-Figur entstammt dieser Erfahrung.
### Tod des Vaters (1901)
Vater stirbt mit 37 an Alkoholismus. Charlie 12 Jahre alt. Mutter endgültig in Anstalt. Charlie und Halbbruder Sydney auf sich gestellt.
### Music Hall und Pantomime
Mit 8 Jahren Auftritt in "Eight Lancashire Lads". Lernt Tanz, Pantomime, Timing. Naturtalent. Mit 14 bei Fred Karno's Comedy Company - beste Pantomime-Truppe Englands. Spielt Betrunkene, Dandys, Tölpel.
## Hollywood-Aufstieg (1913-1923)
### Amerika-Tournee (1910, 1912)
Mit Karno-Truppe nach USA. Vaudeville-Shows. Großer Erfolg. Mack Sennett (Keystone Studios) sieht Charlie.
### Keystone Studios (1914)
Erster Filmvertrag: 150 $/Woche. Lernt Filmmedium. 35 Kurzfilme in einem Jahr. Entwickelt "Tramp"-Figur: zu große Hose, zu kleine Jacke, Melone, Schnurrbart, Gehstock, Watschelgang.
### "The Tramp" entsteht
Zweiter Film: "Kid Auto Races at Venice" (1914). Erste Erscheinung des Landstreichers. Sofort erkennbar, unvergesslich. Wird über Nacht weltberühmt.
### Essanay, Mutual, First National
Springt zwischen Studios. Gehälter explodieren:
- 1915: 1.250 $/Woche (Essanay)
- 1916: 670.000 $/Jahr (Mutual) - höchstbezahlter Entertainer der Welt
- 1918: 1 Million $ + Bonus (First National)
### Künstlerische Kontrolle
Mit steigendem Erfolg fordert totale Kontrolle: Regie, Schnitt, Drehbuch, Musik. Perfektionist. Dutzende Takes. Studios widerstehen zunächst, geben nach.
### Weltstar mit 25
Charlie Chaplin-Manie: Merchandising, Imitatoren, Cartoons. Beliebtester Mensch der Welt. Stummfilm-Ära: Chaplin ist König.
### United Artists (1919)
Mitbegründung mit Mary Pickford, Douglas Fairbanks, D.W. Griffith. Eigene Produktionsfirma. Vollständige Unabhängigkeit. Revolutionär in Hollywood.
## Die Meisterwerke (1921-1940)
### "The Kid" (1921)
Erster Langfilm. Autobiographisch: Tramp adoptiert Waisenkind (Jackie Coogan). Mischung aus Slapstick und Sentimentalität. Publikum weint und lacht. Riesenerfolg.
### "The Gold Rush" (1925)
Meisterwerk der Stummfilm-Ära. Alaska-Goldgräber. Legendäre Szenen:
- Schuhe essen
- Brötchen-Tanz
- Cabin am Abgrund
Perfektes Timing. Tragikomik. Bis heute frisch.
### "City Lights" (1931)
"Ton-Film" ohne Dialog (nur Musik). Trotz Talkies an Stummfilm festgehalten. Blinde Blumenverkäuferin. Boxkampf-Szene. Ende: Millionen Tränen. Chaplin: "City Lights" sein bester Film. Einstein, Churchill liebten ihn.
### "Modern Times" (1936)
Sozialkritik an Industrialisierung. Fließband-Arbeit unmenschlich. Chaplin als Zahnrad im Getriebe. "Feeding Machine"-Szene. Letztes Erscheinen des Tramps. Musik: Chaplin komponiert. Anklänge an Kommunismus - politische Probleme beginnen.
### "The Great Dictator" (1940)
Erster Vollton-Film. Parodie auf Hitler ("Adenoid Hynkel"). Chaplin spielt jüdischen Barbier UND Diktator. Mutig: USA noch neutral. Schlussrede: 6 Minuten direkter Appell an Menschlichkeit. Kontrovers. Kommerziell erfolgreich. Heute: Klassiker.
## Persönliches Leben - Skandale (1918-1952)
### Junge Frauen
Lebenslanges Muster: Wesentlich jüngere Frauen/Teenager. Heute: Problematisch. Damals: Auch kontrovers.
**Mildred Harris** (1918-1920): Heirat mit 16 (er 29). Unglücklich. Scheidung.
**Lita Grey** (1924-1927): Heirat mit 16 (er 35). Zwei Söhne: Charles Jr., Sydney. Bittere Scheidung. Skandal-Scheidungsklage (sexuelle Details). Öffentlichkeit geschockt. Image beschädigt.
**Paulette Goddard** (1936-1942): Schauspielerin, "Modern Times" Star. Geheimheirat. Glücklichste Beziehung. Scheidung freundschaftlich.
**Oona O'Neill** (1943-1977): Tochter des Dramatikers Eugene O'Neill. Heirat: Sie 18, er 54. Acht Kinder. Glücklich bis Tod. Oona: "Liebe meines Lebens."
### Joan Barry Vaterschaftsklage (1943)
Schauspielerin behauptet Vaterschaft. Bluttests beweisen: Nicht Vater. Jury spricht trotzdem Vaterschaft zu (Vorurteil). Alimentezahlung. Rufschädigung.
### FBI-Überwachung
J. Edgar Hoover lässt überwachen. "Kommunistenverdacht". Akte über 2000 Seiten. Privatleben ausgespäht. Politische Verfolgung.
## Politische Verfolgung (1940-1952)
### "The Great Dictator" und Antifaschismus
Film gegen Hitler macht ihn verdächtig. Unterstützung für Sowjetunion (WW2-Alliierte!). Reden für "Second Front" (Hilfe für UdSSR).
### McCarthy-Ära
Kalter Krieg. Rote Angst. Hollywood-Blacklist. Chaplin nie kommunistisch, aber links-liberal. Weigert sich zu distanzieren.
### Limelight-Premiere und Verbannung (1952)
"Limelight" fertig. Chaplin reist mit Familie nach London für Premiere. Unterwegs: US-Justizministerium entzieht Wiedereinreise-Erlaubnis. "Moral Turpitude" (moralische Verwerflichkeit). Faktisch verbannt.
### Entscheidung: Schweiz
Chaplin wütend, verletzt. Entscheidet, nicht zurückzukehren. Zieht nach Corsier-sur-Vevey, Schweiz (Villa "Manoir de Ban"). Lebt dort bis Tod.
## Schweizer Jahre (1952-1977)
### "Ein König in New York" (1957)
Bittere Satire auf McCarthy-Amerika. Kommunisten-Jagd. Konsumgesellschaft. Nicht in USA gezeigt bis 1973. Persönlichster Film.
### "Die Gräfin von Hongkong" (1967)
Letzter Film. Sophia Loren, Marlon Brando. Kritiker vernichten. Kommerzieller Flop. Enttäuschung. Nie wieder Regie.
### "Meine Autobiographie" (1964)
Best
seller. Offene Darstellung armer Kindheit. Liebesbeziehungen verharmlost. Politisches verteidigt.
### Versöhnung mit Amerika (1972)
Academy Awards - Special Oscar "für unschätzbaren Beitrag zum Film". Rückkehr nach 20 Jahren. Standing Ovation. Tränen. Oona an seiner Seite. Vergeben und vergessen (teilweise).
### Ritterschlag (1975)
Queen Elizabeth II.: Knight Commander of the British Empire. Sir Charles Chaplin. Von Slumkind zum Ritter.
### Tod (1977)
Weihnachten, 25. Dezember, 88 Jahre alt. Im Schlaf, friedlich. Beerdigung in Vevey. Oona überlebte bis 1991.
### Leichenraub (1978)
Bizarr: Leiche gestohlen, Lösegeld gefordert. Körper gefunden. Täter gefasst. Oona: "Charlie hätte gelacht." Grab nun diebstahlsicher.
## Künstlerische Genialität
### Pantomime-Genie
Körperbeherrschung wie Balletttänzer. Timing perfekt. Jede Geste spricht. Musik ohne Ton.
### Komposition
Komponierte Musik für alle Filme. "Smile" (aus "Modern Times"): Hit-Song. Emotional, melodisch.
### Regie und Schnitt
Totale Kontrolle. Hunderte Takes. Perfektionist bis Tyrannei. Aber: Resultate rechtfertigen.
### Sozialkritik
Tramp als Underdog. Kritik an:
- Kapitalismus
- Industrialisierung
- Militarismus
- Ungerechtigkeit
Nie plump. Immer menschlich.
## Der Tramp
### Ikonische Figur
Melone + Bambusstock + Schnurrbart + Watschelgang = Instant Recognition. Optimist trotz Armut. Würde in Lumpen. Gentleman trotz Hunger. Komisch UND tragisch.
### Universelle Identifikation
Stummfilm = keine Sprachbarriere. Tramp verstanden von China bis Chile. Underdog der Welt.
### Abschied
"Modern Times" (1936): Letzter Auftritt. Geht in Sonnenuntergang mit Paulette Goddard. "Smile!" Ikonisches Ende.
## Berühmte Zitate
> "Ein Tag ohne Lachen ist ein verlorener Tag."
> "Das Leben ist eine Tragödie, wenn man es in der Nahaufnahme betrachtet, aber eine Komödie in der Totale."
> "Du wirst den Regenbogen niemals finden, wenn du nach unten schaust."
> "Man muss die Dinge nicht zu ernst nehmen - besonders sich selbst nicht."
> "Ich glaube an das Lachen und das Weinen, an das Leid und das Mitleid."
## Vermächtnis
Charlie Chaplin bleibt der größte Komiker der Filmgeschichte:
- **Universalgenie**: Schauspieler, Regisseur, Komponist, Autor
- **Der Tramp**: Unsterbliche Figur der Popkultur
- **Sozialkritiker**: Humanist mit Herz für Underdog
- **Künstlerische Integrität**: Opferte Komfort für Vision
Seine Filme sind zeitlos - "City Lights" und "Modern Times" bewegen heute wie vor 90 Jahren. Die Kombination aus Slapstick und Pathos, Komik und Tragik, bleibt unerreicht.
Von den Londoner Slums zum Weltstar, von Hollywood-Liebling zum politischen Flüchtling, von Verbanntem zum geehrten Ritter - Chaplins Leben war selbst ein Film. Seine Botschaft: Lachen und Mitgefühl sind revolutionär. Die kleinen Leute zählen. Menschlichkeit siegt über Maschinen.
Der Tramp watschelt durch die Geschichte - Symbol für alle, die trotz allem weitermachen, lächeln und hoffen. Das ist Charlie Chaplins unsterbliches Geschenk.`,
'ben-gurion-david': `# David Ben-Gurion
*16. Oktober 1886 - 1. Dezember 1973*
## Kurzbiografie
David Ben-Gurion, geboren als David Grün, war der erste Ministerpräsident Israels und gilt als Hauptarchitekt des jüdischen Staates. Als charismatischer Führer und pragmatischer Visionär führte er die zionistische Bewegung durch entscheidende Jahre: Von der Immigration nach Palästina über die Staatsgründung 1948 bis zum Aufbau einer modernen Nation. Seine Persönlichkeit prägte Israel fundamental - als Sozialist und Nationalist, Idealist und Realist, Prophet und Politiker zugleich.
## Polen: Die Wurzeln (1886-1906)
### Plonsk
Geboren als David Grün in Plonsk (damals Russisches Reich, heute Polen). Vater Avigdor Grün: Jurist, Anhänger der Hovevei Zion (Zionsfreunde). Mutter Scheindel starb, als David 11 war. Prägend: Antisemitismus, Pogrome, Diskriminierung.
### Zionistisches Elternhaus
Aufwachsen in zionistischer Atmosphäre. Vater Mitbegründer der zionistischen Organisation in Plonsk. David mit 14 Jahren Gründer von "Ezra" - Jugendorganisation für Hebräisch und Zionismus. Hebräisch statt Jiddisch: Ideologisch wichtig.
### Träume von Palästina
Liest Herzl. Träumt von jüdischem Staat. Beschließt Aliyah (Einwanderung). Lehnt russische Universität ab. Nicht Assimilation, sondern eigener Staat.
## Zweite Aliyah nach Palästina (1906-1918)
### Ankunft in Jaffa (1906)
20-jährig mit wenig Geld. Zweite Aliyah (1904-1914): Idealistische Pioniere. David arbeitet als Landarbeiter. Orangen pflücken. Malaria. Harte körperliche Arbeit. Aber: Erfüllung. Jude auf jüdischem Land.
### Poale Zion - Arbeiterzionismus
Tritt Poale Zion bei (Arbeiter Zions). Sozialistischer Zionismus:
- Jüdischer Staat UND Sozialismus
- Arbeiter als Avantgarde
- Landwirtschaft als Ideal (nicht Luftmenschen des Schtetl)
- Hebräische Arbeiterklasse schaffen
### Name "Ben-Gurion" (1910)
Hebräisiert Namen von David Grün zu David Ben-Gurion. "Sohn des Löwen" - nach Yosef Ben Gurion, Anführer im jüdischen Aufstand gegen Rom (1. Jh.). Symbolisch: Neuer Mensch, neue Identität.
### Journalismus und Agitation
Editor von "Ahdut" (Einheit), Zeitung der Poale Zion. Beredt und kämpferisch. Talentierter Redner. Organisator. Aufstieg in Arbeiterbewegung.
### Istanbul und Rechtsstudium (1912-1914)
Studiert Jura in Istanbul (damals Teil des Osmanischen Reichs). Ziel: Palästina von innen reformieren. Aber: Osmanisches Reich und Zionismus schwer vereinbar.
### Verbannung (1915)
Erster Weltkrieg. Osmanische Behörden misstrauisch gegen Zionisten. Ben-Gurion und Yitzhak Ben-Zvi deportiert nach Ägypten, dann USA. Drei Jahre Exil.
### New York (1915-1918)
Propaganda für Zionismus. Gründet Hechalutz (Pionier-Organisation). Rekrutierung für Jüdische Legion (britische Armee). Heirat mit Paula Munweis (1917) - lebenslange Ehe, drei Kinder. Paula: Krankenschwester, Fels in Brandung.
## Aufbau des Yishuv (1918-1947)
### Rückkehr als Soldat (1918)
Mit Jüdischer Legion zurück nach Palästina. Britisches Mandat beginnt. Balfour-Deklaration (1917): Hoffnung auf "national home for Jewish people".
### Histadrut (1920)
Gründung der Histadrut - Generalföderation jüdischer Arbeiter. Nicht nur Gewerkschaft: Soziale Bewegung, Krankenkassen, Schulen, Unternehmen. Ben-Gurion Generalsekretär bis 1935. Machtbasis.
### Mapai-Partei (1930)
Vereinigung sozialistischer Parteien zur Mapai (Arbeiterpartei). Ben-Gurion Führer. Dominiert politisches Leben des Yishuv (jüdische Gemeinschaft in Palästina) bis Staatsgründung.
### Jewish Agency (1935)
Chairman der Jewish Agency (Quasi-Regierung des Yishuv unter Mandat). Verhandlungen mit Briten. Internationale Diplomatie. Palästinische Araber. Innerz
ionistische Politik. Zentrale Figur.
### Pragmatismus vs. Maximalismus
Kritisiert von Revisionisten (Jabotinsky): Zu kompromissbereit. Ben-Gurion: "Realpolitik" - nehmen was möglich, ausbauen. Teilung akzeptieren, dann erweitern. Langfristige Strategie.
### Arabische Aufstände (1936-1939)
Gewaltsame arabisch-palästinensische Revolte gegen Juden und Briten. Hunderte Tote. Ben-Gurion: Aufbau Haganah (jüdische Untergrund-Armee). Selbstverteidigung notwendig. Aber auch: Verständnis für arabische Ängste (in Privat).
### Weißbuch (1939)
Britische Einschränkung jüdischer Immigration. Katastrophe vor Holocaust. Ben-Gurion wütend. "Wir werden gegen Hitler kämpfen, als gäbe es kein Weißbuch, und gegen das Weißbuch kämpfen, als gäbe es keinen Hitler."
### Holocaust und Immigration
Während Holocaust verzweifelte Versuche, Juden zu retten. "Aliyah Bet" - illegale Immigration trotz Weißbuch. Schiffe mit Flüchtlingen. Briten abweisen. Machtlosigkeit quält Ben-Gurion lebenslang.
## Staatsgründung (1947-1948)
### UN-Teilungsplan (1947)
UN-Generalversammlung stimmt für Teilung Palästinas: Jüdischer und arabischer Staat. Ben-Gurion akzeptiert trotz Mängel. Pragmatismus: Staat jetzt, Grenzen später.
### Bürgerkrieg (1947-1948)
Sofort Gewalt. Palästinensische Milizen gegen jüdische Siedlungen. Haganah kämpft. Ben-Gurion bereitet Staatsgründung vor trotz Krieg.
### 14. Mai 1948 - Unabhängigkeitserklärung
Tel Aviv Museum. Ben-Gurion liest Unabhängigkeitserklärung:
> "Der Staat Israel ist gegründet!"
Tränen, Jubel. 2000 Jahre Traum erfüllt. Nächster Tag: Arabische Armeen greifen an.
### Unabhängigkeitskrieg (1948-1949)
Israel gegen Ägypten, Jordanien, Syrien, Irak, Libanon. Ben-Gurion: Verteidigungsminister und Premierminister. Mikromanagement. Entscheidungen über Taktik. Kontrovers: Harte Befehle (Dörfer, Vertreibungen). Israel überlebt. Waffenstillstand 1949.
### Nakba
Für Palästinenser: Katastrophe. 700.000 Flüchtlinge. Dörfer zerstört. Ben-Gurion Politik: Rückkehr verhindern. Demografische Sicherheit. Moralisch komplex. Schatten über Gründung.
## Premierminister und Staatsbauer (1948-1963)
### Erste Regierung (1948-1954)
Aufbau aus Nichts:
- Massenimmigration (Holocaust-Überlebende, orientalische Juden)
- Ma'abarot (Übergangslager) für Immigranten
- IDF (Israel Defense Forces) aus Untergrundorganisationen
- Wirtschaft, Infrastruktur, Institutionen
### "Mamlachtiyut" (Staatlichkeit)
Ben-Gurions Doktrin: Staat über Partei. Armee unpolitisch. Zentralisierung. Autorität. Kontrovers in demokratischem Rahmen.
### Lavon-Affäre (1954)
Geheimdienstskandal. Sabotage in Ägypten. Ben-Gurion tritt zurück (1954). Zieht nach Sde Boker (Kibbuz in Negev). "Im Negev wird Israels Schicksal entschieden."
### Rückkehr (1955)
Nach Wahlen zurück als Verteidigungsminister, dann PM. Lavon-Affäre verfolgt weiter. Interne Konflikte.
### Suez-Krise (1956)
Geheimes Bündnis mit Frankreich, England gegen Nasser (Ägypten). Israel erobert Sinai. Internationaler Druck (USA, UdSSR). Rückzug. Aber: Militärisch erfolgreich. Abschreckung etabliert.
### Eichmann-Prozess (1961)
Mossad kidnap
pt Adolf Eichmann aus Argentinien. Prozess in Jerusalem. Weltweite Aufmerksamkeit. Holocaust-Bewusstsein. Ben-Gurion: "Welt muss wissen." Eichmann hingerichtet 1962 (einzige Todesstrafe in Israel).
### Rücktritt (1963)
Interne Parteistreitigkeiten (Lavon-Affäre, Generationenwechsel). Ben-Gurion resigniert. Levi Eshkol Nachfolger. Ben-Gurion verbittert. Spaltung mit Mapai.
## Letzte Jahre (1963-1973)
### Rafi-Partei (1965)
Gründet Splitterpartei Rafi. Gegen Eshkol. Wahl-Flop. Isolation. Aber: Junge Anhänger (Moshe Dayan, Shimon Peres).
### Sechs-Tage-Krieg (1967)
Nicht an Regierung beteiligt. Kritisiert Eshkol (unfair). Nach Sieg: Ambivalent über Eroberungen. Besetzte Gebiete: Sicherheit, aber demografische Gefahr.
### Rückzug nach Sde Boker
Letztes Jahrzehnt in Negev-Kibbuz. Schreibt Memoiren. Liest Philosophie, Geschichte. Platon, Spinoza. Lernt Altgriechisch, Sanskrit. Universaler Geist.
### Tod (1973)
1. Dezember 1973, 87 Jahre alt. Gehirnblutung. Staatsbeerdigung auf Mount Herzl, Jerusalem? Nein: Beerdigung in Sde Boker wie gewünscht. Millionen trauern.
## Persönlichkeit
### Charisma und Autorität
Kleine Statur (1,62m), aber riesige Präsenz. Weiße Haarmähne. Intensive Augen. Hypnotischer Redner. Dominierte Raum. "Der Alte" genannt (schon mit 40).
### Pragmatischer Idealist
Zionist UND Realist. Träume + Machopolitik. Kompromisse + Vision. "Unmögliches wird sofort erledigt, Wunder dauern etwas länger."
### Arbeitsethos
14-Stunden-Tage. Workaholic. Mikromanagement. Erwartete gleiches von anderen. Rücksichtslos zu sich und anderen.
### Familie
Paula Ben-Gurion: Leidende Ehefrau. David oft abwesend. Drei Kinder. Sohn in 1948-Krieg schwer verwundet. Familie Opfer für Staat.
### Widersprüche
- Sozialist, aber autoritär
- Demokrat, aber ungeduldig mit Opposition
- Friedliebend (rhetorisch), aber Militarist
- Säkular, aber biblisch inspiriert
## Ideologie und Vision
### Sozialistischer Zionismus
Einzigartiger Mix:
- Jüdischer Nationalismus
- Sozialistisches Wirtschaftssystem
- Pionier-Ethos (Chalutzim)
- Kibbuzim als Ideal
### "Neuer Jude"
Transformation vom passiven Diaspora-Juden zum aktiven Sabra:
- Physisch stark (Arbeiter, Soldat)
- Hebräisch sprechend
- Landverbunden
- Selbstverteidigung
### Bibel und Säkularismus
Paradox: Säkular, aber biblisch. Bibel als nationales Epos, nicht religiös. Joshua, David, Maccabees als Vorbilder. Jüdisches Volk, nicht Religion.
### "Mamlachtiyut" (Staatlichkeit)
Staat über allen Partikularinteressen. Armee überparteilich. Zentralisierung. Autorität. Demokratie, aber geführt.
## Kontroversen und Kritik
### Vertreibung der Palästinenser
Plan Dalet (1948). Dörfer geräumt. Rückkehr verhindert. "Transfer"-Gedanken schon in 1930ern. Heute: Zentrale Schuldfrage. Ben-Gurion: Sicherheit + Demografie. Kritiker: Ethnische Säuberung.
### Mizrahi-Diskriminierung
Orientalische Juden (aus arabischen Ländern): Zweitklassig behandelt. Sprühen mit DDT. Einfache Jobs. Kulturelle Arroganz europäischer Elite. Ben-Gurion Teil dessen.
### Autoritarismus
Demokrat, aber intolerant gegen Dissens. Lavon-Affäre: Rücksichtslos. Innerpolitische Feinde.
### "Ohne Bibel keine Ansprüche"
Säkularer Zionismus + biblische Ansprüche = Problematisch. Religiöse Nationalisten später missbrauchen.
## Berühmte Zitate
> "Wir werden gegen Hitler kämpfen, als gäbe es kein Weißbuch, und gegen das Weißbuch kämpfen, als gäbe es keinen Hitler."
> "Im Negev wird Israels Schicksal entschieden."
> "Unmöglich ist kein hebräisches Wort."
> "Wer nicht an Wunder glaubt, ist kein Realist."
> "Es spielt keine Rolle, was die Gojim sagen, es spielt nur eine Rolle, was die Juden tun."
## Vermächtnis
David Ben-Gurion bleibt der Staatsgründer Israels:
- **Architekt** des jüdischen Staates
- **Führer** durch Unabhängigkeitskrieg
- **Erbauer** der Institutionen
- **Visionär** mit Pragmatismus
Seine Größe: Traum Wirklichkeit machen. Gegen alle Widerstände: Holocaust, britisches Empire, arabische Armeen, interne Zwiste - Israel entstand.
Seine Schwächen: Rücksichtslosigkeit, Autoritarismus, blinde Flecken gegenüber Palästinensern. Preise der Gründung bis heute spürbar.
Ohne Ben-Gurion kein Israel in 1948. Seine Willenskraft, strategische Intelligenz und charismatische Führung waren entscheidend. Er war der richtige Mann zur richtigen Zeit.
Israel heute - demokratisch, wirtschaftlich erfolgreich, militärisch stark, aber auch im Konflikt - trägt Ben-Gurions Stempel: Seine Stärken UND seine Ambivalenzen. Das Urteil über ihn bleibt komplex, wie der Staat, den er schuf.`,
'rousseau-jean': `# Jean-Jacques Rousseau
*28. Juni 1712 - 2. Juli 1778*
## Kurzbiografie
Jean-Jacques Rousseau war ein Genfer Philosoph, Schriftsteller und Komponist der Aufklärung, dessen Ideen die Französische Revolution, die Romantik und die moderne politische Theorie nachhaltig prägten. Als Kritiker der Zivilisation und Prophet der Natürlichkeit entwickelte er die Ideen des Gesellschaftsvertrags, der Volkssouveränität und der natürlichen Erziehung. Sein Leben war geprägt von Widersprüchen: Ein Theoretiker der Erziehung, der seine eigenen Kinder ins Findelhaus gab; ein Verkünder der Einfachheit, der Paranoia verfiel; ein Philosoph der Freiheit, dessen Ideen Diktaturen inspirierten.
## Genf und frühe Wanderjahre (1712-1742)
### Geburt und Mutterlose Kindheit
Geboren in Genf als Sohn des Uhrmachers Isaac Rousseau. Mutter Suzanne Bernard starb neun Tage nach Geburt im Kindbettfieber. Vater machte Jean-Jacques für Tod verantwortlich. Schuldgefühle prägten lebenslang.
### Frühe Bildung
Vater las mit ihm sentimentale Romane und Plutarchs Heldenleben. Frühe Identifikation mit antiken Republikanern. Keine systematische Schulbildung. Autodidakt.
### Lehre und Flucht (1728)
Lehre bei Graveur. Harte Behandlung. Mit 16 Jahren Flucht aus Genf (Stadttore geschlossen). Wanderte umher. Begegnung mit Madame de Warens - Wendepunkt.
### Madame de Warens (1728-1740)
Louise de Warens: Konvertitin zum Katholismus, Agentin Savoyens. Nahm Jean-Jacques auf. "Maman" genannt. Wurde Geliebte (später). Les Charmettes bei Chambéry: Jahre des Lernens. Bibliothek. Musik. Philosophie. Glücklichste Zeit.
### Konversion
Übertritt zum Katholizismus in Turin. Pragmatisch, nicht überzeugt. Verlor Genfer Bürgerrecht. Später zurück zum Calvinismus (1754).
### Wanderjahre
Hauslehrer, Sekretär, Musiklehrer. Unbeständig. Hypochondrie. Autodidaktische Bildung: Descartes, Leibniz, Newton, Locke. Entwicklung eigenen Denkens.
## Paris und erste Erfolge (1742-1756)
### Ankunft in Paris (1742)
Mit Notationssystem für Musik. Vorstellung bei Académie. Ablehnung. Aber: Entrée in Pariser Gesellschaft.
### Thérèse Levasseur (ab 1745)
Beziehung mit Wäscherin. Ungebildet, aber treu. Nie geheiratet (bis 1768). Fünf Kinder - alle ins Findelhaus gegeben. Skandal. Rousseau rechtfertigt: Zu arm. Später: Lebenslanges Schuldbewusstsein.
### Freundschaft mit Diderot
Diderot und Encyclopédistes. Rousseau schreibt Artikel über Musik. Teil der Aufklärungsszene. Aber: Bald Bruch.
### Première Discours (1750)
Akademie Dijon: Preisfrage "Hat Fortschritt der Wissenschaften und Künste zur Läuterung der Sitten beigetragen?"
Rousseaus Antwort: **NEIN!**
- Zivilisation korrumpiert
- Natürliche Tugend verloren
- Luxus schwächt
- Künste dekadent
Sensation! Paradox in Aufklärungszeit. Diskussionsgewinner. Berühmt über Nacht.
### Second Discours (1755) - Über Ungleichheit
"Discours sur l'origine et les fondements de l'inégalité parmi les hommes":
- **Naturzustand**: "Edler Wilder" (bon sauvage)
- Ursprünglich frei, gleich, gut
- **Eigentum**: "Erster, der eingezäunt" = Unglück
- Zivilisation = Entfremdung
- Ungleichheit künstlich, nicht natürlich
Radikale Kritik. Skandal. Voltaire (später Feind): "Ich habe Lust, auf allen Vieren zu gehen."
### Diskographie
Auch Komponist. Oper "Le Devin du Village" (1752). Erfolgreich. Aber: Lehnt Pension ab. Will unabhängig bleiben.
## Eremitage und Meisterwerke (1756-1762)
### Rückzug aufs Land
Madame d'Épinay bietet Häuschen "Ermitage" in Montmorency (bei Paris). Rousseau zieht mit Thérèse. Einfaches Leben. Natur. Schreibt Hauptwerke.
### "Julie ou La Nouvelle Héloïse" (1761)
Briefroman. Leidenschaftliche Liebe + Tugend. Bestseller! Sentimentalität. Empfindsamkeit. Vorbereitung Romantik. Frauen weinten beim Lesen.
### "Du contrat social" (1762) - Der Gesellschaftsvertrag
Politische Philosophie:
**Kernideen:**
- Mensch geboren frei, überall in Ketten
- Gesellschaftsvertrag = Lösung
- **Volonté générale** (Gemeinwille) Wille aller
- Volkssouveränität
- Repräsentation problematisch
- **Kleine Republik** ideal
- **Zivilreligion** notwendig
Inspirierte Französische Revolution. Aber auch: Jakobiner, Totalitarismus. Umstritten.
### "Émile ou De l'éducation" (1762)
Erziehungsroman. Naturgemäße Erziehung:
**Prinzipien:**
- Kind nicht kleiner Erwachsener
- Entwicklungsstufen beachten
- Lernen durch Erfahrung, nicht Bücher (außer Robinson Crusoe)
- Natürliche Neugier fördern
- Gegen Zwang
- "Glaubensbekenntnis des savoyischen Vikars": Naturreligion
**Skandal:**
- Buch verbrannt in Paris UND Genf
- Haftbefehl gegen Rousseau
- Kritik an Kirche
- Kinder-ins-Findelhaus-Widerspruch
### Flucht (1762)
Vor Verhaftung nach Yverdon (Schweiz), dann Môtiers (Neuenburg, preußisch). Verfolgung überall. Paranoia wächst.
## Verfolgung und Paranoia (1762-1770)
### Bruch mit Aufklärern
Voltaire attackiert öffentlich (Kinder-Skandal). D'Alembert kritisiert. Diderot entfremdet. Rousseau fühlt sich von allen verraten.
### Steinigung in Môtiers (1765)
Dorfbewohner werfen Steine auf Haus. Flüchtete auf Petersinsel (Bielersee). Idyll. Aber: Berner Obrigkeit weist aus.
### David Hume und England (1766-1767)
Auf Einladung Humes nach England. Gastfreundschaft. Aber: Rousseau sieht überall Verschwörung. Beschuldigt Hume der Intrige. Spektakulärer Bruch. Rückkehr nach Frankreich (inkognito).
### Wahnvorstellungen
Klassische Paranoia:
- Alle gegen ihn
- Komplotte überall
- Verfolg
ungswahn
- Aber: Manchmal real (Verbote, Haftbefehle)
Mischung aus realer Verfolgung und psychischer Krankheit.
## Letzte Jahre in Paris (1770-1778)
### Rückkehr
Unter falschem Namen nach Paris. Offiziell geduldet. Lebt bescheiden. Notenkopieren als Einkommen.
### "Les Confessions" (1765-1770, posthum)
Autobiographie. Revolutionär ehrlich:
- Sexuelle Deviationen (Exhibitionismus, Masochismus)
- Diebstähle
- Kinder-Weggabe
- Fehler, Schwächen
Präzedenzlos offenherzig. "Zeige ich mich, wie ich war: verächtlich und niedrig [...] gut, edel, erhaben." Begründer moderner Autobiographie.
### "Rêveries du promeneur solitaire" (1776-1778)
"Träumereien eines einsamen Spaziergängers". Letzte Schrift. Melancholisch, pastoral. Sucht Frieden in Natur. Botanik als Trost. Schönste Prosa.
### Tod (1778)
2. Juli, Schloss Ermenonville (Gast Marquis de Girardin). Schlaganfall. 66 Jahre alt. Beerdigung auf Île des Peupliers. 1794: Panthéon-Überführung (Revolution ehrt ihn). Grab gegenüber von Voltaire.
## Kernideen
### Naturzustand vs. Zivilisation
- **Bon sauvage** (edler Wilder): Ursprünglich gut
- Zivilisation = Korruption
- Eigentum = Unglück
- Entfremdung durch Gesellschaft
- Aber: Kein zurück - nur vorwärts
### Volonté générale (Gemeinwille)
- Unterschied: Gemeinwille Wille aller
- Gemeinwohl vor Einzelinteresse
- Souverän = Volk
- Unveräußerlich
- Problem: Deutungshoheit?
### Freiheit
Paradoxe Formulierung:
> "Der Mensch wird gezwungen, frei zu sein."
Freiheit = Gehorsam gegenüber selbst gegebenen Gesetzen. Positiver Freiheitsbegriff. Gefährlich interpretierbar.
### Natürliche Erziehung
- Gegen Zwang
- Entwicklungsstufen respektieren
- Erfahrung > Bücher
- Natürliche Neugier
- Émile = Ideal (aber unrealistisch)
### Religion
- Gegen organisierte Religion
- Für natürliche Frömmigkeit
- Gewissen als Stimme Gottes
- Zivilreligion im Staat notwendig
- Deistisch
## Wirkungsgeschichte
### Französische Revolution
Robespierre: Rousseau-Verehrer. "Social Contract" als Bibel. Aber: Jakobinischer Terror im Namen des Gemeinwillens. Problematisches Erbe.
### Romantik
Wegbereiter:
- Emotionalität > Rationalität
- Natur > Kultur
- Innerlichkeit
- Sentimentalität
- Goethe, Schiller beeinflusst
### Pädagogik
Pestalozzi, Fröbel, Montessori, Reformpädagogik: Alle Erben Rousseaus. Kindorientierte Erziehung. Entwicklungspsychologie.
### Politische Philosophie
- Demokratietheorie
- Volkssouveränität
- Aber auch: Totalitarismus (Benjamin Constant kritisierte)
- Republikanismus
### Autobiographie
Confessions: Vorbild für alle späteren. Ehrlichkeit, Selbstoffenbarung. Goethe, Tolstoi, Proust.
## Berühmte Zitate
> "Der Mensch wird frei geboren, und überall ist er in Ketten."
> "Zurück zur Natur!"
> "Das Geld, das man besitzt, ist das Mittel zur Freiheit, dasjenige, dem man nachjagt, das Mittel zur Knechtschaft."
> "Man muss viel gelernt haben, um über das, was man nicht weiß, fragen zu können."
> "Gewissen! Göttlicher Instinkt, unsterbliche himmlische Stimme!"
## Widersprüche und Kritik
### Kinder ins Findelhaus
Theoretiker der Erziehung gibt eigene Kinder weg. Unentschuldbar. Selbst eingestanden. Versuch der Rechtfertigung in Confessions. Glaubwürdigkeit beschädigt.
### Frauenbild
Sophie (Émiles Partnerin): Untergeordnet, für Mann erzogen. Mary Wollstonecraft kritisierte. Rousseau: Progressiv UND reaktionär.
### Totalitarismus-Gefahr
"Gemeinwille" zweideutig. Kann Minderheiten unterdrücken. "Gezwungen frei zu sein": Orwellian? Jakobiner, später Faschisten missbrauchten.
### Paranoia
Letzte Jahre: Psychisch krank. Ungerechtfertigte Anschuldigungen (Hume). Menschenscheu. Beeinträchtigt Werk?
### Genf vs. Schriften
Genfer Bürger, Lob der kleinen Republik. Aber: Gibt Bürgerrecht auf, lebt in Frankreich. Heuchlerisch?
## Vermächtnis
Jean-Jacques Rousseau bleibt einer der einflussreichsten und widersprüchlichsten Denker:
- **Demokratietheorie** - Volkssouveränität fundamental
- **Pädagogik** - Revolution der Erziehung
- **Romantik** - Gefühl über Vernunft
- **Kulturkritik** - Entfremdungsanalyse modern
Seine Größe: Radikale Kritik der Zivilisation, Vision natürlicher Freiheit, Begründung der Volkssouveränität. Er dachte gegen seine Zeit.
Seine Problematik: Widersprüche zwischen Leben und Lehre, Totalitarismus-Potential seiner Ideen, Paranoia, Frauenbild.
Rousseau war Prophet und Neurotiker, Visionär und Egoist, Wegbereiter der Demokratie und potentieller Totalitärer. Diese Ambivalenz macht ihn endlos faszinierend - und gefährlich aktuell.
Seine zentrale Einsicht bleibt: Moderne Gesellschaft entfremdet. Freiheit erfordert aktive Bürgerschaft. Erziehung muss das Kind respektieren. Diese Ideen leben - trotz aller Widersprüche ihres Urhebers.`,
};
async function main() {
console.log('📝 Batch 3: Bruce Lee, Charlie Chaplin, Ben-Gurion, Rousseau\n');
const authorsDE = loadAuthors('de');
const authorsEN = loadAuthors('en');
let updated = 0;
const updatedDE = authorsDE.map(author => {
if (batch3Bios[author.id]) {
console.log(`${author.name} (${author.id})`);
updated++;
return {
...author,
biography: {
...author.biography,
long: batch3Bios[author.id]
}
};
}
return author;
});
const updatedEN = authorsEN.map(author => {
const deAuthor = updatedDE.find(a => a.id === author.id);
if (deAuthor?.biography?.long && batch3Bios[author.id]) {
return {
...author,
biography: {
...author.biography,
long: deAuthor.biography.long
}
};
}
return author;
});
console.log(`\n📊 ${updated} Biografien hinzugefügt\n`);
writeAuthors(updatedDE, 'de');
writeAuthors(updatedEN, 'en');
console.log('\n✨ Batch 3 fertig!\n');
}
main();

View file

@ -0,0 +1,897 @@
#!/usr/bin/env tsx
/**
* Batch 4: Johannes Paul II, J.K. Rowling, Alfred Adler, Erich Kästner
*/
import * as fs from 'fs';
import * as path from 'path';
import { Author } from '../services/contentLoader';
function loadAuthors(lang: 'de' | 'en'): Author[] {
const filePath = path.join(__dirname, `../services/data/authors/${lang}.ts`);
const content = fs.readFileSync(filePath, 'utf-8');
const match = content.match(/export const authors[A-Z]{2}: Author\[\] = (\[[\s\S]*\]);/);
return eval(match[1]) as Author[];
}
function writeAuthors(authors: Author[], lang: 'de' | 'en'): void {
const filePath = path.join(__dirname, `../services/data/authors/${lang}.ts`);
const backupPath = filePath.replace('.ts', `.backup-${Date.now()}.ts`);
fs.copyFileSync(filePath, backupPath);
const authorsJson = JSON.stringify(authors, null, 2);
const tsContent = `import { Author } from '../../contentLoader';
export const authors${lang.toUpperCase()}: Author[] = ${authorsJson};
`;
fs.writeFileSync(filePath, tsContent, 'utf-8');
console.log(`${lang}.ts aktualisiert`);
}
const batch4Bios: Record<string, string> = {
'johannes-paul-ii': `# Johannes Paul II.
*18. Mai 1920 - 2. April 2005*
## Kurzbiografie
Karol Józef Wojtyła, bekannt als Papst Johannes Paul II., war der erste nicht-italienische Papst seit 455 Jahren und der erste polnische Papst überhaupt. Sein fast 27-jähriges Pontifikat (1978-2005) war das drittlängste der Geschichte und prägte die katholische Kirche und Weltpolitik nachhaltig. Als charismatischer Reisepapst besuchte er 129 Länder und wurde zur globalen moralischen Autorität. Seine Rolle beim Fall des Kommunismus in Osteuropa machte ihn zur Schlüsselfigur des 20. Jahrhunderts.
## Kindheit im Schatten des Krieges (1920-1946)
### Wadowice
Geboren in Wadowice, Kleinstadt nahe Krakau (Polen). Vater: Karol Wojtyła Sr., Offizier der österreichisch-ungarischen Armee. Mutter: Emilia Kaczorowska. Katholische Mittelschichtfamilie. Nachbarschaft mit jüdischen Familien - prägte Haltung zu Judentum.
### Frühe Verluste
- Mutter stirbt 1929 (Karol 8 Jahre alt)
- Bruder Edmund stirbt 1932 an Scharlach (Karol 12)
- Nur Vater und Karol bleiben
Vater erzieht streng katholisch. Tägliches Gebet. Disziplin. Aber auch Liebe.
### Jugend und Theater
Brillanter Schüler. Sportlich (Fußball, Schwimmen, Kajak). Leidenschaftlicher Theaterbegeisterter. Schauspielerei in Schulaufführungen. Talent. Erwägt Schauspielkarriere.
### Studium in Krakau (1938)
Jagiellonen-Universität. Polnische Philologie. Theatergruppe. Gedichte schreibend. Lebensfroh. Dann: 1. September 1939 - Deutsche Invasion.
### Nazi-Besatzung (1939-1945)
Universität geschlossen. Zwangsarbeit in Steinbruch, dann Chemiefabrik. Untergrund-Theatergruppe "Rhapsodic Theater". Gefahr: Verhaftung, Deportation, Tod. Vater stirbt 1941 - Karol völlig allein.
### Priesterberufung
Tod, Leid, Unterdrückung wecken Berufung. 1942 Eintritt ins geheime Priesterseminar (Nazis hatten Seminar geschlossen). Studium im Untergrund. Kardinal Sapieha schützt.
### Kriegsende und Weihe (1946)
Deutschland kapituliert. Seminar wieder legal. 1. November 1946: Priesterweihe. 26 Jahre alt.
## Priester, Bischof, Kardinal (1946-1978)
### Rom und Promotion (1946-1948)
Studium am Angelicum in Rom. Doktorarbeit über Johannes vom Kreuz (spanischer Mystiker). Phänomenologie und Thomismus verbunden. Rückkehr nach Polen.
### Landpfarrer und Studentenseelsorger
Zunächst Kaplan in Dörfern. Ab 1949 Studentenseelsorger in Krakau. Beliebt bei Jugend. Ski-Ausflüge, Kajak-Touren, Diskussionen. "Wujek" (Onkel) genannt. Charismatisch.
### Zweite Doktorarbeit
Habilitation über Max Scheler. Phänomenologie und katholische Ethik. Werden akademischer Philosoph und Ethiker. Lehrtätigkeit.
### Bischof (1958)
Mit 38 Jahren Weihbischof von Krakau. Jüngster Bischof Polens. Zweites Vatikanisches Konzil (1962-1965): Aktiver Teilnehmer. Mitarbeit an "Gaudium et Spes" (Pastoralkonstitution). Öffnung zur Moderne.
### Erzbischof von Krakau (1964)
Kardinal (1967). Konfrontation mit kommunistischem Regime. Kampf um Religionsfreiheit. Bau neuer Kirchen. Pastoral unter Diktatur. Geschickt und mutig.
### "Liebe und Verantwortung" (1960)
Buch über Sexualethik. Personalistischer Ansatz. Basis für spätere "Theologie des Leibes". Fortschrittlicher als man denkt: Würde der Frau, eheliche Liebe, gegen Objektivierung.
## Papst Johannes Paul II. (1978-2005)
### Konklave 1978 - "Das Jahr der drei Päpste"
- Paul VI. stirbt August 1978
- Johannes Paul I. gewählt, stirbt nach 33 Tagen
- Oktober 1978: Neues Konklave
16. Oktober: Wojtyła gewählt. Sensation! Erster Nicht-Italiener seit 1523. Erster Pole. Jüngster (58) seit über 100 Jahren.
### "Habemus Papam"
> "Fürchtet euch nicht! Öffnet die Türen für Christus!"
Erste Worte. Charisma sofort spürbar. Massen begeistert.
### Der Reisepapst
129 Länder besucht. Mehr als alle Päpste zuvor zusammen. Pastoralreisen als Evangelisierung. Weltjugendtage. Millionen Menschen persönlich getroffen.
**Historische Reisen:**
- **Polen** (1979): Erste Reise. Erweckte polnische Freiheitsbewegung. Millionen bei Messen. Solidarność inspiriert.
- **Mexiko, Brasilien**: Befreiungstheologie-Kritik, aber Solidarität mit Armen
- **USA**: Mehrfach. Kritik und Dialog.
- **Kuba** (1998): Treffen mit Castro
- **Heiliges Land** (2000): Versöhnung mit Juden, Besuch Westmauer
- **Syrien, Griechenland**: Ökumene mit Orthodoxie
### Attentat (13. Mai 1981)
Petersplatz. Mehmet Ali Ağca schießt. Schwer verletzt. Notoperation. Überlebt knapp. Dankt Maria von Fátima (13. Mai = Fátima-Jahrestag).
1983: Besucht Attentäter im Gefängnis. Vergebung. Ikonisches Bild.
## Kampf gegen Kommunismus
### Polen und Solidarność
1979-Besuch in Polen: Wendepunkt. Massen erkennen: Regime nicht allmächtig. Solidarność (1980) direkt inspiriert. Johannes Paul unterstützt diskret.
### Fall der Mauer (1989)
Polen: Erste freie Wahlen. Dominoeffekt: Ungarn, Tschechoslowakei, DDR. Papst: Moralische Autorität des Widerstands. Reagan, Thatcher: Politische Verbündete. Dreigespann gegen Moskau.
### Ende der Sowjetunion (1991)
Gorbatschow 1989 im Vatikan. Religionsfreiheit in UdSSR. Papst: Beschleuniger des Endes. Historiker: Ohne Johannes Paul II. kein friedlicher Zusammenbruch.
### Vermächtnis
Befreiung Osteuropas. Demokratie. Keine Gewalt. Christentum als Freiheitskraft. Größte politische Leistung.
## Theologie und Lehre
### "Redemptor Hominis" (1979)
Erste Enzyklika. Christozentrismus. Mensch findet sich selbst nur in Christus. Würde jedes Menschen. Personalistisch.
### "Theologie des Leibes" (1979-1984)
Katechesen über menschliche Sexualität. Revolutionär für Katholizismus:
- Körper gut (gegen Leibfeindlichkeit)
- Eheliche Liebe als Abbild göttlicher Liebe
- Sexualität heilig
- Aber: Humanae Vitae bekräftigt (keine Verhütung)
### "Evangelium Vitae" (1995)
"Kultur des Lebens" gegen "Kultur des Todes". Gegen Abtreibung, Euthanasie, Todesstrafe. Konsistent pro-life. Kritik am Materialismus, Utilitarismus.
### Sozialenzykliken
"Laborem Exercens", "Sollicitudo Rei Socialis", "Centesimus Annus": Kritik an Kapitalismus UND Kommunismus. Würde der Arbeit. Option für Arme. Solidarität. Christliche Sozialethik aktualisiert.
## Ökumene und Interreligiöser Dialog
### Mit Juden
Revolutionär:
- Synagoge in Rom besucht (1986) - Papst erstmals!
- Israel-Besuch (2000): Yad Vashem, Klagemauer
- "Ältere Brüder im Glauben"
- Schoa als moralische Verpflichtung
### Mit Muslimen
- Koran küsst (symbolisch)
- Moschee in Damaskus besucht (2001)
- Aber: Kritik am radikalen Islam
- Dialog trotz Spannungen
### Mit Orthodoxen
- Konstantinopel, Athen besucht
- Versöhnungsgesten
- Aber: Große Einheit unerreicht
### Mit Protestanten
- Gemeinsame Erklärung zur Rechtfertigungslehre (Lutheraner, 1999)
- Canterbury besucht
- Aber: Weiheämter weiter Problem
### Assisi-Treffen (1986, 2002)
Weltreligionen zum Friedensgebet. Kontrovers. Konservative: Synkretismus. Johannes Paul: Zeichen des Friedens.
## Konservative Seite
### Sexualmoral
Keine Zugeständnisse:
- Keine Priesterinnenweihe (Ordinatio Sacerdotalis, 1994): "Definitiv"
- Zölibat unverrückbar
- Homosexualität: Liebe ja, Akte nein
- Verhütung weiter verboten
- Abtreibung: Absolutes Nein
Kritik: Realitätsfern, besonders in AIDS-Krise (Kondom-Verbot).
### Befreiungstheologie
Kritisch gegenüber lateinamerikanischer Befreiungstheologie. Marxismus-Nähe abgelehnt. Sanktionen gegen Theologen (Leonardo Boff). Konservative Bischöfe ernannt. Linke: Verrat an Armen.
### Disziplinierung
Theologen gemaßregelt: Hans Küng, Charles Curran. Kirche zentr
alisiert. Autoritär (trotz Charisma). Lehramt über Ortskirchen.
### Missbrauchsskandal
Unter seiner Herrschaft vertuscht. Maciel (Legionäre Christi): Geschützt trotz Missbrauch. Schwarzes Kapitel. Erst Benedikt XVI. handelt.
## Letzte Jahre und Tod (2001-2005)
### Parkinson
Ab 1990er sichtbar. Zittern, undeutliche Sprache, Bewegungsprobleme. Öffentlich. Nicht zurückgetreten. "Leiden als Zeugnis".
### Krankheit als Verkündigung
Letzte Jahre: Gebrechlich, leidend, aber weiter im Amt. Botschaft: Würde auch im Leiden. Alter ist wertvoll. Gegen Euthanasie.
### Tod (2. April 2005)
Karsamstag. Lange Agonie. Letzte Worte: "Amen." Millionen Trauernde in Rom. "Santo Subito!" (Sofort heilig!) gerufen.
### Beerdigung
Größte Zusammenkunft von Staatsoberhäuptern der Geschichte. 4 Könige, 5 Königinnen, 70 Präsidenten. 300.000 Menschen. Welt trauert.
### Seligsprechung (2011)
Benedikt XVI. spricht selig. Rekordverdächtig schnell. Johannes Paul-Generation drängt. Wunder: Nonne geheilt.
### Heiligsprechung (2014)
Papst Franziskus heiligt zusammen mit Johannes XXIII. (ohne zweites Wunder). "Heiliger Johannes Paul der Große".
## Persönlichkeit
### Charisma
Elektrisierende Präsenz. Massen begeistert. Schauspielerisches Talent. Medial genial. Erste "Medienpapst".
### Sportler und Outdoor-Mensch
Bis spät Ski gefahren. Wandern. Schwimmen. Pool im Vatikan installiert. Gesundheit als Wert.
### Intellektueller
Fünf Sprachen fließend. Philosoph. Poet (schrieb Gedichte bis ins Pontifikat). Belesen. Tiefe Theologie.
### Konservativ und Progressiv
Widersprüchlich:
- Sozial progressiv (Arme, Arbeiter, Frieden)
- Moralisch konservativ (Sex, Gender)
- Ökumenisch offen
- Disziplinär streng
## Berühmte Zitate
> "Fürchtet euch nicht!"
> "Öffnet die Türen für Christus!"
> "Sei, wer du bist, und werde, was du sein sollst."
> "Die Zukunft beginnt heute, nicht morgen."
> "Das Leben eines jeden Menschen ist heilig, vom Anfang bis zum Ende."
## Vermächtnis
Johannes Paul II. bleibt eine der prägendsten Figuren des 20. Jahrhunderts:
- **Politisch**: Fall des Kommunismus
- **Kirchlich**: Reisepapst, Weltjugendtage, Globalisierung der Kirche
- **Dialogisch**: Ökumene, interreligiöser Dialog
- **Moralisch**: Globale Autorität
**Aber auch:**
- Missbrauchsskandal nicht adressiert
- Sexualmoral starr
- Zentralisierung der Macht
- Befreiungstheologie gebremst
Sein Einfluss ist ambivalent. Für Millionen: Heiliger und Visionär. Für Kritiker: Reaktionär in sozialen Fragen.
Die Figur Johannes Paul II. zeigt: Auch Heilige sind Menschen. Größe und Versagen können koexistieren. Seine Rolle beim Fall des Kommunismus und seine moralische Autorität bleiben unbestritten. Seine Härte in moralischen Fragen und sein Versagen beim Missbrauchsskandal ebenso.
Er prägte das Papsttum neu: Reisend, medial, charismatisch. Franziskus ist anders, aber ohne Johannes Paul undenkbar.`,
'rowling-jk': `# J.K. Rowling
*31. Juli 1965 - heute*
## Kurzbiografie
Joanne Kathleen Rowling ist eine britische Schriftstellerin, die mit der "Harry Potter"-Reihe das erfolgreichste Literaturphänomen der Gegenwart schuf. Aus ärmlichen Verhältnissen als alleinerziehende Mutter wurde sie zur ersten Milliardärin durch Schreiben. Die sieben Harry-Potter-Bücher (1997-2007) verkauften über 500 Millionen Exemplare und wurden zur prägenden Lese-Erfahrung einer Generation. Als Philanthropin spendet sie Hunderte Millionen. In jüngerer Zeit kontrovers wegen Äußerungen zu Transgender-Themen.
## Kindheit und Jugend (1965-1990)
### Yate, England
Geboren in Yate bei Bristol als Joanne Rowling. Vater Peter: Rolls-Royce-Ingenieur. Mutter Anne: Wissenschaftsassistentin. Schwester Dianne (Di), zwei Jahre jünger. Normale Mittelschichtfamilie.
### Frühe Schreiblust
Mit 6 Jahren erste Geschichte: "Rabbit". Schrieb ständig. Fantasiereich. Erzählte Schwester Geschichten. Wusste früh: Will Schriftstellerin werden.
### Wyedean Comprehensive
Sekundarschule. Schüchtern, Brillenträgerin (wie Harry). Gute Schülerin, besonders Englisch. Nicht populär. Außenseiter-Erfahrung (später in Hogwarts-Häusern).
### Mutter erkrankt (1980)
Anne Rowling Multiple Sklerose-Diagnose. Lange Krankheit, zunehmende Behinderung. Joanne 15 Jahre alt. Prägt Thema Tod in Harry Potter.
### Universität Exeter (1983-1987)
Studium Französisch und Klassische Philologie. Vater wollte Sekretärin. Joanne: Kompromiss (Sprachen = "nützlich"). Auslandsjahr in Paris. Abschluss: Nichts Besonderes. Unsicher über Zukunft.
## Die harten Jahre (1990-1997)
### Portugal (1991-1993)
Englischlehrerin in Porto. Partyleben. Treffen mit Jorge Arantes (TV-Journalist). Leidenschaftliche Beziehung. Heirat 1992. Tochter Jessica (1993). Ehe gewalttätig. Trennung und Scheidung. Joanne flieht mit Baby nach Schottland.
### Edinburgh - Am Boden
Alleinerziehende Mutter. Keine Arbeit. Sozialhilfe. Depression. Selbstmordgedanken. Winzige Wohnung. Arm. Gedemütigt. Aber: Baby und Schreiben halten am Leben.
### Harry Potter-Idee (1990)
Zugfahrt Manchester-London: Idee für Harry Potter kommt plötzlich. Junge, weiß nicht, dass er Zauberer ist. Hogwarts. Sofort klar, detailliert. Kein Stift dabei. Züge verspätet. Vier Stunden Gedanken entwickelt.
### Schreiben in Cafés
Nicolson's Café (Edinburgh). Baby Jessica schläft. Joanne schreibt. Stundenweise. Notizblock. Longhand. Kein Computer. Tasse Kaffee. Entwickelt Welt: 7 Bücher geplant von Anfang.
### Mutters Tod (1990)
Anne Rowling stirbt, bevor Harry Potter fertig. Joanne tief getroffen. Thema Tod in Büchern noch zentraler. Lily Potters Opfer = Annes Liebe. Spiegel Nerhegeb (zeigt Tote) = Sehnsucht.
### Lehrerdiplom (1996)
Ausbildung zur Französisch-Lehrerin. Perspektive. Aber: Buch endlich fertig (1995). "Harry Potter und der Stein der Weisen". Will Verlag finden.
## Der Durchbruch (1995-2000)
### 12 Ablehnungen
Manuskript eingeschickt. Ablehnung nach Ablehnung. "Zu lang für Kinder." "Nicht kommerziell." "Niemand kauft Fantasy." Frustration. Zweifel.
### Bloomsbury sagt Ja (1996)
Christopher Little (Agent) findet Bloomsbury. Kleiner Verlag. Barry Cunningham (Editor): "Alice (8) liebt es!" Vertrag. Vorschuss: £1,500 (lächerlich). Aber: Veröffentlichung!
### "J.K." statt "Joanne"
Verlag rät: Geschlechterneutral. Jungen lesen keine Autorinnen. Joanne fügt "K" (Kathleen, Großmutter) hinzu. "J.K. Rowling" geboren.
### 26. Juni 1997 - Veröffentlichung
"Harry Potter and the Philosopher's Stone". 500 Exemplare. Erste Auflage klein. Heute Sammlerstücke (Tausende £).
### Mundpropaganda-Erfolg
Kinder lieben es. Eltern lesen. Lehrer empfehlen. Mund-zu-Mund. Keine große Werbung. Organisch. Kinder bestehen auf Fortsetzung.
### Amerikaner interessiert
Scholastic (USA) kauft Rechte für $105,000 (Rekord für Kinderbuch). Titel geändert: "Sorcerer's Stone" (Amerikaner kennen Philosophenstein nicht). USA: Noch größerer Markt.
### Chamb
er of Secrets (1998)
Zweites Buch. Bestseller sofort. Erwartung riesig. Liefert. Dunkler. Basilisk. Tom Riddle. Fangemeinde wächst exponentiell.
### Prisoner of Azkaban (1999)
Drittes Buch. Komplexer. Zeitreise. Sirius Black. Reifer. Kritiker anerkennen: Nicht nur Kinderbuch. Erwachsene lesen offen.
### Goblet of Fire (2000)
Buch 4. 636 Seiten (doppelt so lang). Midnight-Release-Parties. Hysterie. Tri-Wizard-Turnier. Voldemorts Rückkehr. Cedrics Tod (erste Hauptfigur stirbt). Dunkler Ton. Kritischer Punkt: Von hier kein Zurück.
## Das Phänomen (2000-2007)
### Medienhype
J.K. Rowling überall. Interviews. Titelseiten. Reichste Frau Großbritanniens (überholt Queen). Vergleiche mit Dickens, Dahl. Kulturphänomen.
### Film-Adaptionen
Warner Brothers. Chris Columbus (Regie). Rowling: Kreativkontrolle. Casting-Approval. Britische Schauspieler. Treue zum Buch. Daniel Radcliffe, Emma Watson, Rupert Grint: Weltberühmt.
### Order of the Phoenix (2003)
Buch 5. 870 Seiten. Dunkelstes bisher. Harrys Teenager-Wut. Umbridges Tyrannei. Sirius' Tod. Prophezeiung. Große Erwartung. Liefert.
### Half-Blood Prince (2005)
Buch 6. Voldemorts Vergangenheit. Horkruxe. Snapes Verrat (?). Dumbledores Tod. Schock. Fans trauern.
### Deathly Hallows (2007)
Buch 7. Finale. Camping, Horkrux-Jagd, Schlacht um Hogwarts. Snapes Redemption. Epilog "19 Jahre später". Millionen lesen in 24 Stunden. Kulturelles Ereignis.
### Zahlen zum Phänomen
- 500+ Millionen Bücher verkauft
- 80 Sprachen übersetzt
- 8 Filme (Deathly Hallows geteilt), über $7 Milliarden eingespielt
- Themenparks (Universal Studios)
- Merchandise-Imperium
### Einfluss
- Ganze Generation las wieder
- "Harry Potter Generation"
- Mitternachts-Release-Parties als Kulturphänomen
- Fanfiction-Explosion
- Kinder-Literatur legitimiert
## Nach Harry Potter (2007-heute)
### "The Casual Vacancy" (2012)
Erwachsenen-Roman. Kleinstadtintrigen. Sozialkritik. Düster. Kritiken gemischt. Bestseller (Name), aber enttäuscht manche Fans.
### Robert Galbraith - Cormoran Strike (ab 2013)
Pseudonym. Krimis. "The Cuckoo's Calling" (2013) zunächst unbemerkt veröffentlicht. Gute Kritiken, schlechte Verkäufe. Leak: J.K. Rowling. Sofort Bestseller. Serie fortsetzt. Besser aufgenommen als "Vacancy".
### Fantastic Beasts (ab 2016)
Prequel-Filmserie zu Harry Potter. Rowling: Drehbücher. 1920er New York. Newt Scamander. Grindelwald. Fünf Filme geplant. Drei erschienen. Box Office OK, aber nicht Potter-Level. Qualität sinkt.
### Pottermore / Wizarding World
Website. Ergänzende Infos. Häuser-Sortierung. Neue Geschichten. Problematisch: Retcons (Dumbledore schwul, Hermione evtl. schwarz, etc.). Fans gespalten: Bereicherung oder Geldmacherei?
### Theaterstück "Cursed Child" (2016)
Harrys Sohn Albus. Zeitreise. Canon? Umstritten. Kommerziell erfolgreich. Literarisch: Gemischt.
## Philanthropie
### Spenden
Schätzungsweise £150+ Millionen gespendet. Verliert Milliardärs-Status dadurch (+ Steuern). Egal, will helfen.
### Gründungen
- **Lumos**: Hilft institutionalisierten Kindern (Waisen), zurück in Familien
- **Volant Charitable Trust**: Multiple Sklerose-Forschung, Armutsbekämpfung
### Single Parents, Multiple Sclerosis
Unterstützt Alleinerziehende (eigene Erfahrung). MS-Forschung (Mutter). Persönliche Themen.
## Die Transgender-Kontroverse (ab 2019)
### Anfänge
2019: Tweet zu Maya Forstater-Fall (Entlassung wegen Gender-Aussagen). Rowling: Solidarität. Kritik: Transfeindlich.
### Juni 2020 - Essay
Langes Statement zu Gender. Punkte:
- Biologisches Geschlecht real
- Frauen-Räume schützen
- Detransitionen erwähnen
- Sorge um Kinder und Transitionierung
### Reaktion
Massiver Backlash:
- "TERF" (Trans-Exclusionary Radical Feminist) genannt
- Boykott-Aufrufe
- Daniel Radcliffe, Emma Watson, Rupert Grint distanzieren sich
- Transgender-Aktivisten: "Schädlich, gefährlich"
### Rowlings Position
Behauptet: Nicht transfeindlich. Unterstützt Transrechte. Aber: Biologisches Geschlecht wichtig. Frauen-Räume schützen. Free Speech verteidigen.
### Konsequenzen
- Kulturkrieg-Ikone geworden (ungewollt?)
- Politisch gespalten: Konservative applaudieren, Progressive attackieren
- Vermächtnis kompliziert
- "Harry Potter" selbst bleibt geliebt (meist)
## Persönlichkeit
### Schüchtern vs. Öffentlich
Introvertiert. Interviews schwierig. Aber: Verteidigt sich scharf auf Twitter. Widerspruch.
### Detailversessen
Plante sieben Bücher von Anfang. Notizen, Timelines, Genealogien. Kontrollbedürfnis (kreativ).
### Selbstkritisch
Spricht offen über Depression, Suizidgedanken, Versagen. Keine Schönfärbung.
### Politisch
Labour-Wählerin. Soziale Gerechtigkeit. Aber: Transgender-Debatte macht politisch obdachlos.
## Berühmte Zitate
> "Es ist unsere Entscheidungen, die zeigen, wer wir wirklich sind, weit mehr als unsere Fähigkeiten."
> "Glück kann man auch in den dunkelsten Zeiten finden, wenn man sich nur daran erinnert, das Licht anzumachen."
> "Worte sind unsere unerschöpflichste Quelle der Magie."
> "Es braucht viel Mut, sich seinen Feinden zu stellen, aber genauso viel, sich seinen Freunden entgegenzustellen."
## Vermächtnis
J.K. Rowling bleibt die prägendste Kinderbuchautorin des 21. Jahrhunderts:
- **Harry Potter** - kulturelles Phänomen einer Generation
- **Vom Tellerwäscher zum Millionär** - moderne Märchengeschichte
- **Lesemotivation** - machte Lesen cool
- **Philanthropin** - gibt zurück
**Aber:**
- **Transgender-Kontroverse** - spaltet Fangemeinde
- **Post-Potter-Werke** - enttäuschen teils
- **Retcons** - Geldmacherei-Vorwurf
Ihre Geschichte ist shakespearean: Armut, Erfolg, Kontroverse. Ihr Werk wird bleiben. Ihre Person bleibt umstritten. Harry Potter selbst - größer als die Autorin, Botschaft von Toleranz, Freundschaft, Mut - überlebt die Kontroversen.
Rowlings größte Leistung: Sie brachte eine Generation zum Lesen. Hogwarts wurde zur zweiten Heimat für Millionen. Diese Magie ist unsterblich - egal, was auf Twitter passiert.`,
'adler-alfred': `# Alfred Adler
*7. Februar 1870 - 28. Mai 1937*
## Kurzbiografie
Alfred Adler war ein österreichischer Arzt und Psychotherapeut, der als Begründer der Individualpsychologie die Tiefenpsychologie nachhaltig prägte. Zunächst Mitarbeiter Sigmund Freuds, brach er 1911 mit diesem und entwickelte eine eigene Richtung, die den Menschen als soziales Wesen und das Streben nach Überlegenheit (nicht Sexualität) als zentral ansah. Konzepte wie Minderwertigkeitskomplex, Kompensation, Lebensstil und Gemeinschaftsgefühl stammen von ihm. Als sozialer Reformer verband er Psychologie mit Pädagogik und Sozialarbeit.
## Kindheit in Wien (1870-1888)
### Rudolfsheim, Vorstadt Wien
Geboren als zweites von sieben Kindern. Vater Leopold Adler: jüdischer Getreidehändler. Mutter Pauline. Mittelständische, assimilierte jüdische Familie. Nicht religiös.
### Rachitis und Krankheit
Frühkindliche Rachitis (Vitamin-D-Mangel). Schwächlich. Oft krank. Neid auf gesunden älteren Bruder Sigmund. Unfälle: Mit 3 Jahren fast überfahren. Mit 5 Jahren Lungenentzündung, fast gestorben. Arzt: "Nicht zu retten." Überlebte. Entschied: Werde Arzt.
**Prägend:** Erfahrung von Schwäche, Minderwertigkeit, Krankheit. Später Theorie: Organminderwertigkeit als Kompensationsantrieb.
### Schulzeit
Anfangs schlechter Schüler (Mathematik). Lehrer: "Besser Schuster werden." Vater glaubt an ihn. Alfred arbeitet hart. Wird Klassenbester. **Kompensation in Aktion.**
### Brüderliche Rivalität
Bruder Sigmund stirbt früh. Alfred: Schuldgefühle? Überlebensschuld? Später Theorie: Geschwisterkonstellation prägt Persönlichkeit. Zweitgeborener kompensiert.
## Medizinstudium und frühe Karriere (1888-1902)
### Universität Wien
Studiert Medizin. Interessiert an allem: Soziale Medizin, Ophthalmologie (Augen), später Neurologie und Psychiatrie. Politisch aktiv: Sozialistisch orientiert. Volksbildung wichtig.
### Promotion (1895)
Dr. med. Zunächst Allgemeinmediziner. Praxis in ärmlichem Prater-Viertel. Zirkusartisten, Akrobaten als Patienten. Beobachtung: Körperliche Defizite führen zu Höchstleistungen. Kompensation!
### Heirat (1897)
Raissa Timofejewna Epstein. Russische Intellektuelle, Sozialistin, Feministin. Vier Kinder. Gleichberechtigte Ehe (für die Zeit radikal). Raissa: Lebenslange Unterstützerin.
### "Gesundheitsbuch für das Schneidergewerbe" (1898)
Erste Publikation. Arbeitsmedizin. Soziale Ursachen von Krankheit. Schneider: Schlechte Arbeitsbedingungen = Krankheit. Prävention durch soziale Reform. **Früher Sozialreformer.**
### Wechsel zur Psychiatrie
Um 1900: Von Allgemeinmedizin zu Neurologie und Psychiatrie. Liest Freud. Fasziniert. Verteidigt "Traumdeutung" öffentlich (1902). Freud bemerkt. Einladung zur "Mittwochsgesellschaft" (später: Wiener Psychoanalytische Gesellschaft).
## Freud-Jahre (1902-1911)
### Mittwochsgesellschaft
Freud, Adler, Stekel, später Jung. Diskussionen. Adler: Kritisch, eigenständig. Nicht Schüler, sondern Kollege (aus Adlers Sicht).
### "Studie über Minderwertigkeit von Organen" (1907)
Hauptwerk der Frühphase. These: Organ-Minderwertigkeit (schwache Lunge, schlechtes Sehen) treibt zu Kompensation. Überkompensation möglich. Demosthenes (Stotterer wurde Redner) als Beispiel.
**Konflikt mit Freud:** Adler sieht biologische Basis, nicht Sexualität als primär. Freud misstrauisch.
### Aggression und Macht
Adler: Aggressionstrieb wichtiger als Libido. Wille zur Macht (Nietzsche-Einfluss). Maskuliner Protest (gegen Weiblichkeit).
Freud: Ablehnung. Nur Libido ist Trieb.
### Präsident der Wiener Gesellschaft (1910)
Adler wird Präsident (Freud bleibt Ehrenpräsident). Spannung wächst. Adler: Eigenständige Theorie. Freud: Häresie.
### Der Bruch (1911)
Offener Konflikt. Adler kritisiert Pansexualismus. Freud: Adler versteht Psychoanalyse nicht. Ultimatum. Adler tritt zurück als Präsident und aus Gesellschaft aus. Neun weitere folgen. Endgültige Trennung.
**Freud (bitter):** "Der kleine Adler." Neid auf Größe?
**Adler:** Befreiung. Eigene Richtung.
## Individualpsychologie (1911-1937)
### Gründung
1911: "Verein für freie psychoanalytische Forschung" (später: Individualpsychologie). Name: Individuum als unteilbare Ganzheit (nicht Individuell vs. Gesellschaft).
### Kernbegriffe
**Minderwertigkeitsgefühl:**
- Jeder Mensch fühlt sich minderwertig (Kind vs. Erwachsene, Schwäche vs. Ideal)
- Treibt zur Kompensation
- Gesund: Echte Leistung
- Pathologisch: Minderwertigkeitskomplex (gelähmt) oder Überlegenheitskomplex (überkompensiert, aber hohl)
**Streben nach Überlegenheit / Geltung:**
- Nicht Sex, sondern Macht/Anerkennung zentral
- Von unten nach oben
- "Aufwärtsbewegung"
**Lebensstil:**
- Früh (4-5 Jahre) geformt
- Einheitlicher Plan des Lebens
- Roter Faden der Persönlichkeit
- Therapie: Verstehen und ändern des Lebensstils
**Gemeinschaftsgefühl (Sozialinteresse):**
- Wichtigster Begriff! (später Fokus)
- Psychische Gesundheit = starkes Gemeinschaftsgefühl
- Neurose = schwaches Gemeinschaftsgefühl
- Erziehung muss Gemeinschaftsgefühl fördern
**Geschwisterkonstellation:**
- Geburtsreihenfolge prägt
- Erstgeborener: Entthront, konservativ, verantwortlich
- Zweitgeborener: Wetteifer, ehrgeizig
- Nesthäkchen: verwöhnt oder motiviert
- Einzelkind: Zentrum, evtl. egozentrisch
**Fiktionale Finalität:**
- Mensch lebt nach fiktiven Zielen ("Als-ob")
- "So, als wäre ich großartig..."
- Ziel leitet Verhalten, auch wenn unrealistisch
### Therapie
Kürzer als Freudsche Psychoanalyse. Face-to-face (nicht Couch). Ermutigung zentral. Lebensstil analysieren. Gemeinschaftsgefühl stärken. Praktischer, weniger mystisch als Freud.
## Erziehung und Sozialreform (1920er)
### Wiener Erziehungsberatungsstellen (1920er)
Adler revolutionär: Kostenlose Erziehungsberatung in Wiener Schulen. Über 30 Stellen. Eltern, Lehrer, Kinder beraten. Präventiv. Gemeinschaftsgefühl fördern.
**Prinzipien:**
- Ermutigung statt Strafe
- Natürliche Folgen statt Autorität
- Gleichwertigkeit Kind-Erwachsener (nicht gleich, aber gleichwertig)
- Demokratische Erziehung
Beeinflusst: Rudolf Dreikurs, Thomas Gordon, moderne Pädagogik.
### Volksbildung
Vorträge für Laien. Psychologie für alle. Nicht Elfenbeinturm. Verständliche Sprache. Soziale Mission.
### Sozialismus
Zeit seines Lebens sozialistisch. Psychologie muss soziale Verhältnisse ändern. Kritik am Kapitalismus als Konkurrenzgesellschaft, schädigt Gemeinschaftsgefühl. Rot Wien (1919-1934): Adler aktiv beteiligt.
## Emigration und Tod (1926-1937)
### USA-Reisen (ab 1926)
Vortragsreisen. Enthusiastisch aufgenommen. Praktischer als Freud, optimistischer als Jung. Amerikaner lieben Adler.
### Columbia University (1932)
Gastprofessor, später visiting professor. Long Island College of Medicine: Position. Pendelt zwischen Wien und New York.
### Austrofaschismus und Nazis
1934: Dollfuß-Diktatur in Österreich. Adlers Kliniken geschlossen (sozialistische Verbindungen). 1934: Endgültige Emigration nach USA.
Antisemitismus wächst. Obwohl konvertiert (Protestant geworden, pragmatisch), Gefahr wegen jüdischer Herkunft.
### Vortragsreise in Schottland (1937)
Mai 1937: Vorträge in Schottland. Herzinfarkt auf Straße in Aberdeen. Sofort tot. 67 Jahre alt. Beerdigung in Edinburgh.
### Vermächtnis gespalten
Zu Lebzeiten: Berühmter als Freud in USA. Nach Tod: Freud dominiert. Adler: Lange unterschätzt. Renaissance ab 1960ern.
## Vergleich mit Freud und Jung
### Freud
- **Sex vs. Macht**: Libido vs. Streben nach Überlegenheit
- **Trieb vs. Ziel**: Vergangenheit (Triebe) vs. Zukunft (Ziele)
- **Pessimismus vs. Optimismus**: Determinist vs. Möglichkeit zur Änderung
- **Individuum vs. Gesellschaft**: Intrapsychisch vs. interpersonal
### Jung
- **Mystisch vs. Pragmatisch**: Archetypen vs. Soziale Realität
- **Spirituell vs. Materialistisch**: Kollektives Unbewusstes vs. Soziale Bedingungen
- Beide: Bruch mit Freud. Aber ganz unterschiedliche Richtungen.
## Einfluss und Wirkung
### Psychotherapie
- **Kognitive Therapie**: Ellis, Beck (Gedanken ändern Gefühle) = adlerianisch
- **Humanistische Psychologie**: Maslow, Rogers (Selbstverwirklichung, Optimismus)
- **Systemische Therapie**: Familie als System
### Pädagogik
- **Dreikurs**: "Children: The Challenge"
- **Positive Discipline**: Jane Nelsen
- **Gordon**: "Familienkonferenz"
- Demokratische, ermutigende Erziehung
### Selbsthilfe
- **"Think positive"**: Adlerianische Ermutigung
- **Growth Mindset** (Carol Dweck): Adlers Optimismus
- **Resilienz**: Kompensation von Minderwertigkeitsgefühlen
### Sozialarbeit
Gemeinschaftsgefühl als Ziel. Soziale Faktoren zentral. Prävention wichtig.
## Berühmte Konzepte
> "Das einzige, was wir in diesem Leben wirklich besitzen, sind die Beziehungen zu unseren Mitmenschen."
> "Ein Mensch zu sein bedeutet, ein Gefühl der Minderwertigkeit zu haben, das einen ständig zur Überlegenheit treibt."
> "Es ist leicht, Kinder zu kritisieren, aber schwer, sie zu ermutigen."
> "Das wichtigste im Leben ist, zu lernen, zu geben und zu teilen."
## Vermächtnis
Alfred Adler bleibt der unterschätzte dritte Gigant der Tiefenpsychologie:
- **Soziale Perspektive**: Mensch als soziales Wesen
- **Optimismus**: Änderung möglich
- **Pädagogik**: Ermutigende Erziehung
- **Gemeinschaftsgefühl**: Psychische Gesundheit = soziales Interesse
Sein Schatten: Freud überstrahlt. Jung faszinierender. Adler: Zu praktisch? Zu optimistisch? Zu einfach?
Aber: Sein Einfluss ist riesig - oft unerkannt. Kognitive Therapie, humanistische Psychologie, moderne Pädagogik: Alle adlerianisch beeinflusst. "Minderwertigkeitskomplex" ist Alltagswort.
Adlers Vision: Psychologie im Dienst der Menschlichkeit. Heilung durch Gemeinschaft. Erziehung zur Demokratie. In zerrissener Zeit aktueller denn je.`,
'kästner-erich': `# Erich Kästner
*23. Februar 1899 - 29. Juli 1974*
## Kurzbiografie
Erich Kästner war ein deutscher Schriftsteller, Drehbuchautor und Kabarettist, berühmt für seine Kinderbücher wie "Emil und die Detektive" und "Das doppelte Lottchen". Als scharfzüngiger Satiriker und Moralist der Weimarer Republik kritisierte er Militarismus und Nationalsozialismus, überlebte aber das Dritte Reich in Deutschland. Seine klare, sachliche Prosa und sein humanistischer Optimismus prägten Generationen. Trotz Publikations- und Schreibverbot blieb er in Deutschland - ein innerer Emigrant, der nach 1945 zur moralischen Instanz wurde.
## Kindheit in Dresden (1899-1919)
### Neustadt, Dresden
Geboren in Dresden als einziger Sohn von Emil Kästner (Sattlermeister) und Ida Kästner (geb. Augustin). Mutter: Friseurin, stark, dominant. Vater: zurückhaltend, sanft. Kästner zeit seines Lebens Muttersohn.
### Intime Mutter-Sohn-Bindung
Ida verehrte Erich, nannte ihn "mein Junge". Extrem enge Bindung. Korrespondierten täglich bis zu ihrem Tod (1951). Heute: Grenzüberschreitend? Damals: Intensiv, aber nicht unüblich. Prägte Frauenbild.
### Vater-Frage
Gerücht (später bestätigt): Biologischer Vater war Dr. Emil Zimmermann, Ida's Arbeitgeber (jüdischer Arzt). Erich wusste es wohl. Heimlichkeit. Erklärte Väter Verhältnis? Nichtehelicher Sohn, aber in Ehe großgezogen.
### Schulzeit
König-Georg-Gymnasium, Dresden. Guter Schüler, besonders Deutsch. Schrieb früh Gedichte. Klassenbester. Aber: Leidenschaftslos. Pflichterfüllung.
### Erster Weltkrieg - Lehrer
Lehrerausbildung (Notausbildung wegen Krieg). Mit 18 Jahren Volksschullehrer. Kurze Zeit. Einberufung folgte.
### Militärdienst (1917-1918)
Schwere Artillerie. Ausbildung brutal. Herzkrankheit (dauerhaft) durch Drill. Schikane durch Vorgesetzte. Hass auf Militarismus entsteht. **Prägend für Pazifismus.**
Nie Fronteinsatz (Kriegsende). Aber: Trauma des Drills bleibt. "Fabian" später: Antimilitarismus.
## Studium und Weimarer Republik (1919-1933)
### Leipzig, Rostock, Berlin
Studium: Germanistik, Geschichte, Philosophie, Theaterwissenschaft. Promotion 1925 (Leipzig) über Friedrich den Großen und Literatur. Dr. phil.
### Freier Schriftsteller
Entscheidet gegen Akademikerkarriere. Will Schriftsteller werden. Gedichte. Feuilletons. Kabarett-Texte. Freier Journalist. Prekär, aber künstlerisch.
### "Die Weltbühne" und "Neue Leipziger Zeitung"
Publiziert in linksintellektuellen Zeitschriften. Satirische Gedichte. Sozialkritik. Antimilitarismus. Scharfe Zunge. "Herz auf Taille" (1928): Gedichtband. Erfolg.
### Berlin (ab 1927)
Umzug nach Berlin. Zentrum der Weimarer Kultur. Kabarett "Die Katakombe". Kontakte: Kurt Tucholsky, Carl von Ossietzky. Linksliberale Intellektuelle. Gegen Nazis, gegen Militarismus, gegen Spießer.
### "Fabian" (1931)
Großstadtroman. Jakob Fabian: Anti-Held, Moralist, Pessimist. Sittengemälde der untergehenden Weimarer Republik. Erotik, Zynismus, Kapitalismuskritik. Skandal wegen Sex-Szenen. Kritik: Zu moralisierend. Heute: Zeitdokument.
### "Emil und die Detektive" (1929)
Durchbruch als Kinderbuchautor. Emil Tischbein verfolgt Dieb durch Berlin. Kinderbande. Realistische Großstadt. Detektiv-Geschichte. Sofort Klassiker. Verfilmt (1931). Welterfolg. Übersetzungen in 59 Sprachen.
**Besonderheit:** Respektiert Kinder. Nicht verniedlicht. Intelligente, aktive Kinder. Moderne Pädagogik.
## NS-Zeit - Innere Emigration (1933-1945)
### Bücherverbrennung (10. Mai 1933)
Kästner erlebt eigene Bücherverbrennung. Opernplatz Berlin. Steht in Menge. Sieht Bücher brennen. Gestapo erkennt ihn (angeblich). Geht nicht weg. **Zeuge der eigenen Verdammung.**
### Publikationsverbot
Bücher verboten. Nicht verhaftet (warum? Unklar. Evtl. Devisenbringer durch internationale Kinderbucher?). Darf nicht publizieren unter eigenem Namen.
### Warum blieb er?
Große Frage. Tucholsky, Ossietzky, Mann: Exil oder Tod. Kästner: Blieb.
**Seine Begründung:** "Ich bin ein Deutscher aus Dresden in Sachsen. Mich läßt meine Mutter nicht fort." Auch: Chronist bleiben. Zeuge sein. Aber: Kritik an Feigheit, Opportunismus.
### Pseudonyme und Drehbücher
Schrieb Drehbücher unter Pseudonymen. "Münchhausen" (1943, Hans Albers): Erfolgreichster deutscher Film der NS-Zeit. Kästner: Berthold Bürger (Pseudonym). Geld verdient im Dritten Reich. **Kompromiss oder Verrat?**
### Kriegsende in Tirol
Mai 1945: Mit Filmteam in Mayrhofen (Tirol). Kriegsende. Befreiung. Notizen für "Das doppelte Lottchen" (Idee entsteht).
## Nachkriegszeit (1945-1974)
### München
Lebt in München (nicht Ostberlin). Feuilleton-Chef "Neue Zeitung" (amerikanische Lizenz). Kabarett "Die Schaubude" (später "Kleine Freiheit") - Mitbegründer. Mahner und Kritiker.
### Kinderbücher der Nachkriegszeit
- **"Das doppelte Lottchen"** (1949): Getrennte Zwillinge. Scheidung. Wiederverei
nigung. Warmherzig. Klassiker.
- **"Die Konferenz der Tiere"** (1949): Tiere zwingen Menschen zum Frieden. Pazifistisch. Idealistische Utopie.
- **"Das fliegende Klassenzimmer"** (1933, Neuauflage): Internatsjungs. Freundschaft. Lehrer-Schüler-Vertrauen.
### "Als ich ein kleiner Junge war" (1957)
Autobiographie der Kindheit. Dresden um 1900. Mutter-Beziehung. Wehmütig. Nostalgisch. Ehrlich.
### Büchner-Preis (1957)
Höchste deutsche Literatur-Auszeichnung. Anerkennung. Aber: Kritik, dass Kinderbuchautor ausgezeichnet wurde. Kästner: "Auch Kinder sind Menschen."
### Kritik am Wirtschaftswunder
1950er/60er: Deutschland vergisst Vergangenheit. Materialismus. Kästner kritisiert: Verdrängung, Wohlstandsbesessenheit, fehlende Aufarbeitung. Aber: Gehör findet er kaum. Zeitgeist gegen ihn.
### Impotenz und Alkohol
Persönlich: Vereinsamt. Mehrere Affären, keine dauerhafte Beziehung. Spät (1957) Sohn Thomas (mit Friedel Siebert, nicht geheiratet). Trinkt zunehmend. Depression?
### Letzte Jahre
Gesundheitlich angeschlagen. Schreibt weniger. Wiederholungen. Ruhm lebt von Vergangenheit. 1960er/70er: Neue Generation findet Kästner altmodisch.
### Tod (29. Juli 1974)
München. Speiseröhrenkrebs (Rauchen). 75 Jahre alt. Beerdigung: Viele kommen. Deutschland trauert. Kinderautor, Moralist, Zeitzeuge.
## Literarischer Stil
### Neue Sachlichkeit
Klare, einfache Sprache. Keine Metaphern-Überfülle. Sachlich, aber nicht emotionslos. Anti-Expressionismus. Reportage-Stil. **Verständlich für Kinder und Erwachsene.**
### Moralischer Ton
Immer Moral. Gut vs. Böse klar. Optimismus trotz Kritik. Glaubt an Vernunft, Anstand, Güte. Lehrer-Ton? Ja. Aber auch: Herzenswärme.
### Humor und Satire
Erwachsenenbücher: Scharf satirisch. Kinderb ücher: Warmherziger Humor. Nie zynisch gegenüber Kindern.
### Pessimismus des Verstandes, Optimismus des Herzens
Weiß: Welt schlecht. Menschen schwach. Aber: Kämpft weiter. Hoffnung auf Vernunft. Trotz allem.
## Politische Haltung
### Pazifismus
Absolut. Militarismus = Wurzel des Übels. "Kennst du das Land, wo die Kanonen blühn?" (Gedicht). Antimilitaristisch.
### Antifaschismus
Gegen Nazis von Anfang. Bücher verbrannt. Aber: Blieb in Deutschland. Innere Emigration = mutig oder feige? Debatte bis heute.
### Humanismus
Glaube an Güte, Vernunft, Anstand. Aufklärung. Bildung. Einfache menschliche Tugenden. Nicht ideologisch, sondern menschlich.
### Unpolitisch?
Kritik: Zu vage. Keine klare politische Theorie. Moralisiert statt analysiert. Verteidigung: Moral IST Politik.
## Verhältnis zu Frauen und Mutter
### Mutter Ida
Zentral. Tägliche Briefe. "Mein Junge." Über-Mutter? Heute: Problematisch. Kästner selbst reflektierte nicht kritisch.
### Frauen
Mehrere Beziehungen. Keine lange Ehe. Oft jüngere Frauen. Ilse Julius, Luiselotte Enderle, Friedel Siebert (Mutter seines Sohns). Keine erfüllte Partnerschaft? Oder Beziehungsunfähigkeit?
### Frauenbild
Fortschrittlich für Zeit (Frauen als stark, intelligent). Aber: Mutterfixiert. Idealisierung.
## Berühmte Zitate und Gedichte
> "Es gibt nichts Gutes, außer: Man tut es."
> "Wer noch nie einen Fehler gemacht hat, hat sich noch nie um etwas bemüht."
> "Das Gewissen ist fähig, Unrecht für Recht zu halten. Inquisition für Gott. Und Mord für Politik."
**"Sachliche Romanze" (Gedicht):**
> "Als sie einander acht Jahre kannten / (und man darf sagen: sie kannten sich gut), / kam ihre Liebe plötzlich abhanden. / Wie andern Leuten ein Stock oder Hut."
Melancholisch. Neue Sachlichkeit in Lyrik.
## Vermächtnis
Erich Kästner bleibt der moralische Kinderbuchautor und Satiriker Deutschlands:
- **Kinderbücher**: "Emil", "Lottchen", "Fliegendes Klassenzimmer" - Generationen geprägt
- **Neue Sachlichkeit**: Klare, verständliche Literatur
- **Moralist**: Pazifismus, Humanismus, Anstand
- **Innerer Emigrant**: Überlebte NS-Zeit in Deutschland - kontrovers
**Kritik:**
- Zu moralisierend?
- NS-Zeit: Mut oder Feigheit?
- Nachkriegszeit: Zu pessimistisch, zu rückwärtsgewandt?
Seine Größe: Respekt vor Kindern, klare Sprache, moralischer Kompass. Seine Ambivalenz: NS-Zeit, Alkohol, Vereinsamung.
Kästners Werke leben. "Emil" wird gelesen. "Doppelte Lottchen" verfilmt. Seine Gedichte zitiert. Sein moralischer Appell: Tut das Gute. Bleibt anständig. Zeitlos - in bösen Zeiten besonders wichtig.`,
};
async function main() {
console.log('📝 Batch 4: Johannes Paul II, Rowling, Adler, Kästner\n');
const authorsDE = loadAuthors('de');
const authorsEN = loadAuthors('en');
let updated = 0;
const updatedDE = authorsDE.map(author => {
if (batch4Bios[author.id]) {
console.log(`${author.name} (${author.id})`);
updated++;
return {
...author,
biography: {
...author.biography,
long: batch4Bios[author.id]
}
};
}
return author;
});
const updatedEN = authorsEN.map(author => {
const deAuthor = updatedDE.find(a => a.id === author.id);
if (deAuthor?.biography?.long && batch4Bios[author.id]) {
return {
...author,
biography: {
...author.biography,
long: deAuthor.biography.long
}
};
}
return author;
});
console.log(`\n📊 ${updated} Biografien hinzugefügt\n`);
writeAuthors(updatedDE, 'de');
writeAuthors(updatedEN, 'en');
console.log('\n✨ Batch 4 fertig!\n');
}
main();

View file

@ -0,0 +1,475 @@
#!/usr/bin/env tsx
/**
* Fügt ausführliche Biografien für fehlende Autoren hinzu
*/
import * as fs from 'fs';
import * as path from 'path';
import { Author } from '../services/contentLoader';
function loadAuthors(lang: 'de' | 'en'): Author[] {
const filePath = path.join(__dirname, `../services/data/authors/${lang}.ts`);
const content = fs.readFileSync(filePath, 'utf-8');
const match = content.match(/export const authors[A-Z]{2}: Author\[\] = (\[[\s\S]*\]);/);
return eval(match[1]) as Author[];
}
function writeAuthors(authors: Author[], lang: 'de' | 'en'): void {
const filePath = path.join(__dirname, `../services/data/authors/${lang}.ts`);
const backupPath = filePath.replace('.ts', `.backup-${Date.now()}.ts`);
fs.copyFileSync(filePath, backupPath);
const authorsJson = JSON.stringify(authors, null, 2);
const tsContent = `import { Author } from '../../contentLoader';
export const authors${lang.toUpperCase()}: Author[] = ${authorsJson};
`;
fs.writeFileSync(filePath, tsContent, 'utf-8');
console.log(`${lang}.ts aktualisiert`);
}
// Ausführliche Biografien (hochwertig, nicht template-basiert)
const detailedBios: Record<string, string> = {
'augustinus': `# Aurelius Augustinus
*13. November 354 - 28. August 430*
## Kurzbiografie
Aurelius Augustinus, bekannt als Augustinus von Hippo, war einer der bedeutendsten christlichen Kirchenlehrer und Philosophen der Spätantike. Seine Werke "Confessiones" (Bekenntnisse) und "De civitate Dei" (Vom Gottesstaat) gehören zu den einflussreichsten Schriften der abendländischen Geistesgeschichte.
## Jugend und Irrwege (354-386)
### Herkunft und Familie
Geboren in Thagaste (heute Souk Ahras, Algerien) als Sohn des Heiden Patricius und der Christin Monica. Die Mutter prägte ihn religiös, doch er wandte sich zunächst ab.
### Student in Karthago
Studium der Rhetorik in Karthago ab 371. Leben in Ausschweifung und Konkubinat. Geburt des Sohnes Adeodatus 372. Lektüre von Ciceros "Hortensius" weckt philosophischen Hunger.
### Manichäer-Zeit
Neun Jahre Anhänger der manichäischen Sekte, die Gut und Böse als kosmische Prinzipien lehrte. Zunehmende Zweifel an deren Lehren.
### Lehrertätigkeit
Rhetoriklehrer in Karthago, Rom (383) und schließlich Mailand (384). Begegnung mit Bischof Ambrosius wird entscheidend.
## Die Bekehrung (386)
### Krise und Wandlung
Intensive philosophische Studien (Neuplatonismus). Innerer Kampf zwischen Fleisch und Geist. Berühmte Szene im Mailänder Garten: Kinderstimme ruft "Tolle lege!" (Nimm und lies!).
### Taufbereitung
Öffnet die Bibel und liest Römer 13,13-14. Entscheidung für Christentum. Rückzug mit Freunden nach Cassiciacum. Taufe durch Ambrosius in der Osternacht 387.
## Rückkehr nach Afrika (387-395)
### Verluste
Tod der Mutter Monica in Ostia 387. Tod des Sohnes Adeodatus 389. Augustinus gründet klösterliche Gemeinschaft in Thagaste.
### Unfreiwillige Priesterweihe
391 in Hippo Regius (heute Annaba, Algerien) vom Volk zum Priester gewählt. Prediger-Tätigkeit beginnt.
### Bischof von Hippo (395)
Wird Bischof von Hippo. Gründet Kloster im Bischofshaus. Beginnt sein gewaltiges literarisches Werk.
## Die großen Werke
### Confessiones (397-401)
Autobiographische Bekenntnisse:
- Erste große Autobiographie der Weltliteratur
- Psychologische Selbstanalyse
- Theologie der Gnade
- Philosophie der Zeit (Buch XI)
- "Spät hab ich dich geliebt, du Schönheit..."
### De civitate Dei (413-426)
Geschichtstheologie in 22 Büchern:
- Antwort auf Vorwurf, Christentum habe Rom geschwächt
- Civitas Dei vs. Civitas terrena
- Zwei Staaten, zwei Liebensweisen
- Geschichtsphilosophie
- Legitimation weltlicher Ordnung
### De Trinitate (399-419)
Trinitätslehre:
- Psychologische Trinität im Menschen
- Memoria, Intelligentia, Voluntas
- Einheit in Dreiheit
- Grundlage scholastischer Theologie
## Theologische Kämpfe
### Donatisten-Streit
Kampf gegen schismatische nordafrikanische Kirche:
- Frage der Sakramentsgültigkeit
- Kirchenverständnis
- Rechtfertigung von Zwang gegen Häretiker
### Pelagianismus
Hauptgegner: Pelagius und seine Anhänger:
- Erbsündenlehre entwickelt
- Gnadenlehre verfeinert
- Prädestinationslehre
- Freier Wille und Gnade
- Einfluss bis Reformation
## Philosophisches Denken
### Neuplatonismus und Christentum
Verbindung von:
- Plotins Emanationslehre
- Christlicher Schöpfungsglaube
- Erkenntnistheorie der Erleuchtung
- Gott als höchstes Sein und Gut
### Zeit und Ewigkeit
Revolutionäre Zeitphilosophie:
- Zeit als Ausdehnung der Seele
- Gegenwart des Vergangenen (Erinnerung)
- Gegenwart des Zukünftigen (Erwartung)
- Ewigkeit als Gleichzeitigkeit
### Zeichentheorie
Grundlagen der Semiotik:
- Unterscheidung von Zeichen und Bezeichnetem
- Sprachphilosophie
- Hermeneutik der Schriftauslegung
## Anthropologie und Psychologie
### Selbsterkenntnis
"In te ipsum redi" - Kehre in dich selbst ein:
- Innerer Mensch als Ort der Wahrheit
- Selbstreflexion als Weg zu Gott
- Psychologie der Innerlichkeit
### Willenslehre
Revolutionäre Willenspsychologie:
- Wille wichtiger als Intellekt
- Gespaltener Wille
- Liebe als Grundkraft
- "Pondus meum amor meus"
### Erbsünde
Dunkle Seite seiner Lehre:
- Erbschuld aller Menschen
- Massa damnata
- Nur Taufe rettet
- Unbarmherzigkeit gegenüber ungetauften Kindern
## Gnadenlehre
### Kernsätze
Fundamentale Einsichten:
- "Da quod iubes et iube quod vis" - Gib was du befiehlst
- Gnade kommt zuvor (gratia praeveniens)
- Gnade wirkt (gratia operans)
- Menschliche Mitwirkung (gratia cooperans)
### Prädestination
Umstrittene Konsequenzen:
- Erwählung vor der Welt
- Gnadenwahl unergründlich
- Anzahl der Geretteten festgelegt
- Basis für Calvin und Jansenismus
## Tod und Vermächtnis (430)
### Letzte Tage
Während der Belagerung Hippos durch die Vandalen stirbt Augustinus am 28. August 430 im Alter von 75 Jahren. Seine Klosterbibliothek übersteht die Eroberung.
### Sofortige Wirkung
Schon zu Lebzeiten wichtigster lateinischer Kirchenvater. Über 100 erhaltene Schriften, 500 Predigten, 200 Briefe.
## Einfluss durch die Jahrhunderte
### Mittelalter
Dominiert die Scholastik:
- Bonaventura und Franziskaner
- Duns Scotus
- Kampf mit Aristotelismus
- Augustinereremitenorden (1256)
### Reformation
Luther als Augustinermönch:
- "Allein durch Gnade"
- Bibelautorität
- Rechtfertigungslehre
- Calvin radikalisiert Prädestination
### Neuzeit
Fortgesetzte Wirkung:
- Pascal und Jansenismus
- Existenzphilosophie (Kierkegaard, Heidegger)
- Personalismus
- Selbstreflexion als Methode
## Kritik und Probleme
### Negative Aspekte
Problematische Lehren:
- Verdammung ungetaufter Kinder
- Rechtfertigung von Zwang
- Sexualnegativität
- Determinismus
### Moderne Rezeption
Ambivalentes Erbe:
- Tiefenpsychologie avant la lettre
- Aber auch Angstreligion
- Freiheitsphilosophie und Determinismus
- Subjektivität und Autorität
## Berühmte Zitate
> "Unruhig ist unser Herz, bis es Ruhe findet in dir."
> "Liebe, und tu was du willst."
> "Spät hab ich dich geliebt, du Schönheit, so alt und so neu."
> "Du hast uns zu dir hin geschaffen, und unruhig ist unser Herz, bis es Ruhe findet in dir."
## Das bleibende Vermächtnis
Augustinus bleibt der einflussreichste Kirchenvater des Westens:
- **Psychologe der Innerlichkeit** - Erfinder der Selbstreflexion
- **Philosoph der Zeit** - Grundlage moderner Zeittheorie
- **Theologe der Gnade** - Prägte Christentum für Jahrhunderte
- **Schriftsteller** - Confessiones als literarisches Meisterwerk
Seine Größe liegt in der radikalen Ehrlichkeit, mit der er sein zerrissenes Ich offenbart, und in der Tiefe, mit der er die Paradoxien menschlicher Existenz durchdenkt. Augustinus lehrte die Gotteserkenntnis durch Selbsterkenntnis und die Selbsterkenntnis durch Gotteserkenntnis.`,
'kennedy-john-f': `# John Fitzgerald Kennedy
*29. Mai 1917 - 22. November 1963*
## Kurzbiografie
John F. Kennedy, der 35. Präsident der Vereinigten Staaten (1961-1963), verkörperte eine neue Generation amerikanischer Politik. Als jüngster gewählter und erster katholischer Präsident inspirierte er mit seiner Vision einer "New Frontier" und seiner charismatischen Führung die Welt. Seine Ermordung in Dallas beendete abrupt eine Präsidentschaft, die trotz ihrer Kürze die amerikanische Geschichte nachhaltig prägte.
## Kennedy-Clan und frühe Jahre (1917-1940)
### Die Kennedy-Dynastie
Geboren als zweiter Sohn von Joseph P. Kennedy Sr. und Rose Fitzgerald Kennedy in Brookline, Massachusetts. Vater Joe war erfolgreicher Geschäftsmann und Diplomat, die Mutter entstammte einer prominenten irisch-amerikanischen Politikerfamilie Bostons.
### Privilegierte Kindheit
Aufwachsen im Wohlstand mit acht Geschwistern. Besuch exklusiver Privatschulen. Früh eingeübt in Wettbewerb und Leistungsdruck durch den ehrgeizigen Vater, der seine Söhne für die Politik vorbereitete.
### Gesundheitsprobleme
Litt zeitlebens unter schweren Rückenproblemen und der Addison-Krankheit. Häufige Krankenhausaufenthalte prägten seine Jugend, entwickelte aber Willensstärke und Durchhaltevermögen.
## Ausbildung und Krieg (1936-1945)
### Harvard-Student
Studium an der Harvard University. Abschlussarbeit über britische Appeasement-Politik wird als Buch "Why England Slept" (1940) veröffentlicht und ein Bestseller.
### PT-109 Held
Navy-Offizier im Pazifik-Krieg. August 1943: Sein Schnellboot PT-109 wird von japanischem Zerstörer gerammt. Rettet verletzten Kameraden, schwimmt stundenlang mit Riemen zwischen den Zähnen. Ausgezeichnet als Kriegsheld.
### Journalistische Tätigkeit
Nach dem Krieg kurzzeitig Journalist. Berichtet 1945 über Gründung der UN. Tod des älteren Bruders Joe Jr. 1944 (Kriegseinsatz) macht John zum designierten Politiker der Familie.
## Politischer Aufstieg (1946-1960)
### Kongressabgeordneter (1947-1953)
Wahl ins Repräsentantenhaus für Massachusetts mit 29 Jahren. Liberaler Demokrat, aber pragmatisch. Unterstützt Sozialreformen und starke Verteidigung.
### Senator (1953-1960)
Wahl in den Senat 1952. Heirat mit Jacqueline Bouvier 1953. Schwere Rückenoperationen 1954/55. Während Genesung Arbeit an "Profiles in Courage" (Pulitzer-Preis 1957).
### Fast Vizepräsident
1956 knapp an Vizepräsidentschaftskandidatur gescheitert. Nutzung der Zeit für Aufbau nationaler Bekanntheit. Bereitet systematisch Präsidentschaftskampagne vor.
## Präsidentschaftswahlkampf 1960
### Demokratische Vorwahlen
Überwindet Vorbehalte gegen Katholiken. Sieg in West Virginia entscheidend. Nominierung durch Parteitag mit Lyndon B. Johnson als Running Mate.
### TV-Debatten
Erste Fernsehduelle der Geschichte gegen Richard Nixon. Kennedys jugendliche Ausstrahlung vs. Nixons fahles Aussehen. Radio-Hörer sehen Nixon vorn, TV-Zuschauer Kennedy - Medium als Botschaft.
### Knapper Sieg
Hauchdünner Sieg im November 1960: 49,7% vs. 49,5%. Jüngster gewählter Präsident mit 43 Jahren (Theodore Roosevelt war jünger bei Amtsantritt nach Attentat).
## Die Präsidentschaft (1961-1963)
### Antrittsrede (20. Januar 1961)
Legendäre Inaugurationsrede:
> "Fragt nicht, was euer Land für euch tun kann - fragt, was ihr für euer Land tun könnt."
> "Lasst uns niemals aus Furcht verhandeln. Aber lasst uns niemals Furcht haben zu verhandeln."
Vision einer "New Frontier" - neue Grenzen zu überschreiten.
### Schweinebuchtkrise (April 1961)
Erste außenpolitische Katastrophe: Gescheiterte CIA-Invasion Kubas. Kennedy übernimmt Verantwortung, lernt aus Fehler. Misstrauen gegenüber Militär und Geheimdiensten wächst.
### Wiener Gipfel (Juni 1961)
Treffen mit Chruschtschow in Wien. Sowjetführer testet den Neuling. Kennedy fühlt sich überrumpelt. Berlin-Frage ungeklärt.
### Berlinkrise und Mauerbau
Bau der Berliner Mauer August 1961. Kennedy entsetzt, aber militärisch machtlos. Bekräftigt Verteidigungsgarantie für West-Berlin.
### Kuba-Krise (Oktober 1962)
Gefährlichste Krise des Kalten Krieges:
- Sowjetische Atomraketen auf Kuba entdeckt
- Seekblockade statt Luftangriff gewählt
- 13 Tage am Rand des Atomkriegs
- Geheimer Deal: Raketen gegen US-Jupiter aus Türkei
- Chruschtschow lenkt ein
Kennedys Krisenbewältigung wird zur Sternstunde: Festigkeit plus Flexibilität, militärischer Druck plus diplomatische Lösung.
### Friedenspolitik (1963)
Nach Kubakrise Entspannungsphase:
- "Strategie des Friedens" (American University Speech, Juni 1963)
- Atomteststopp-Vertrag (August 1963)
- "Heißer Draht" nach Moskau
- Umdenken über Vietnam beginnt
## Innenpolitik
### New Frontier Programm
Ambitionierte Agenda:
- Steuerreformen
- Armutsbekämpfung
- Bildungsreform
- Medicare-Vorschläge
Viele Gesetze scheitern im Kongress. Demokratische Südstaatler blockieren Reformen.
### Bürgerrechte
Zunächst zögerlich, dann engagiert:
- Entsendung der National Guard in Alabama (1963)
- Unterstützung für James Meredith (Mississippi)
- TV-Ansprache zu Moral der Bürgerrechte (Juni 1963)
- Vorlage des Civil Rights Act (wird erst 1964 unter Johnson Gesetz)
### Raumfahrt
Vision der Mondlandung:
> "Wir haben beschlossen, in diesem Jahrzehnt zum Mond zu fliegen, nicht weil es leicht ist, sondern weil es schwer ist."
Massives Apollo-Programm gestartet. Ziel 1969 erreicht.
### Wirtschaft
- Längste Wirtschaftsexpansion der Nachkriegszeit
- Steuersenkungen zur Konjunkturbelebung
- Keynesianische Politik
## Familie im Weißen Haus
### Jackie Kennedy
Erste Lady Jacqueline als Stil-Ikone. Restaurierung des Weißen Hauses. Fernsehführung durch Präsidentenresidenz. Kulturelle Renaissance.
### Camelot-Image
Weißes Haus wird zu "Camelot" - Anspielung auf König Arthurs Hof. Intellektuelle, Künstler, Nobelpreisträger zu Gast. Pablo Casals spielt Cello.
### Kinder
Caroline (geb. 1957) und John Jr. (geb. 1960, gestorben 1999) im Weißen Haus. Sohn Patrick stirbt nach zwei Tagen (August 1963).
## Das Attentat (22. November 1963)
### Dallas, Texas
Wahlkampfreise nach Texas zur Aussöhnung verfeindeter Demokraten-Flügel. Trotz Warnungen offene Limousine durch Dallas.
### 12:30 Uhr
Dealey Plaza: Schüsse fallen. Kennedy in Kopf und Hals getroffen. Gouverneur Connally ebenfalls verletzt. Fahrt zum Parkland Hospital.
### Tod des Präsidenten
13:00 Uhr für tot erklärt. Jacqueline Kennedy im blutbefleckten Kostüm. Lyndon Johnson wird vereidigt.
### Lee Harvey Oswald
Verhaftung des mutmaßlichen Täters. Zwei Tage später erschießt Jack Ruby Oswald vor laufender Kamera. Unzählige Verschwörungstheorien bis heute.
## Vermächtnis
### Warren-Kommission
Offizielle Untersuchung: Oswald als Einzeltäter. Viele Zweifel bleiben. Skeptizismus gegenüber offizieller Version bis heute.
### Was wäre wenn?
Spekulation über unvollendete Präsidentschaft:
- Hätte er USA aus Vietnam herausgehalten?
- Welche Bürgerrechtsgesetze hätte er durchgesetzt?
- Wie wäre das Verhältnis zur Sowjetunion?
### Mythos und Realität
Komplexes Erbe:
- **Licht**: Charisma, Vision, Krisenmanagement, Inspiration
- **Schatten**: Eheliche Untreue, Gesundheitsprobleme verheimlicht, anfängliches Zögern bei Bürgerrechten
### Fortdauernde Inspiration
Kennedy-Mystik lebt:
- Jugend, Hoffnung, Idealismus
- Ruf zum Dienst an der Gemeinschaft
- Friedenskorps als dauerhaftes Vermächtnis
- "Ask not..." als Appell an jede Generation
## Berühmte Reden und Zitate
> "Ich bin ein Berliner!" (26. Juni 1963, West-Berlin)
> "Eine steigende Flut hebt alle Boote." (über Wirtschaftswachstum)
> "Die Menschheit muss dem Krieg ein Ende setzen, oder der Krieg setzt der Menschheit ein Ende."
> "Jeder Mensch kann einen Unterschied machen und jeder Mensch sollte es versuchen."
## Das Kennedy-Vermächtnis
Über 60 Jahre nach seiner Ermordung bleibt JFK eine der faszinierendsten Figuren amerikanischer Geschichte:
- **Symbol unerfüllten Potenzials** - Was hätte sein können
- **Wendepunkt der Moderne** - Ende der Unschuld Amerikas
- **Inspiration für Generationen** - Ruf zum öffentlichen Dienst
- **Warnung** - Fragilität von Leben und Demokratie
Kennedy verkörperte den Optimismus und die Zuversicht einer Ära, die mit ihm zu Ende ging. Sein früher Tod verwandelte einen guten Präsidenten in eine Legende und ließ seine Vision einer besseren Welt als unerfülltes Versprechen zurück.`,
};
async function main() {
console.log('📝 Füge ausführliche Biografien hinzu\n');
const authorsDE = loadAuthors('de');
const authorsEN = loadAuthors('en');
let updated = 0;
const updatedDE = authorsDE.map(author => {
if (detailedBios[author.id]) {
console.log(`${author.name} (${author.id})`);
updated++;
return {
...author,
biography: {
...author.biography,
long: detailedBios[author.id]
}
};
}
return author;
});
const updatedEN = authorsEN.map(author => {
const deAuthor = updatedDE.find(a => a.id === author.id);
if (deAuthor?.biography?.long && detailedBios[author.id]) {
return {
...author,
biography: {
...author.biography,
long: deAuthor.biography.long
}
};
}
return author;
});
console.log(`\n📊 ${updated} Biografien hinzugefügt\n`);
writeAuthors(updatedDE, 'de');
writeAuthors(updatedEN, 'en');
console.log('\n✨ Fertig!\n');
}
main();

View file

@ -0,0 +1,97 @@
#!/usr/bin/env npx tsx
import fs from 'fs/promises';
import path from 'path';
async function analyzeQuoteCounts() {
console.log('📊 Analyzing quote counts per author...\n');
// Load current data
const quotesPath = path.join(process.cwd(), 'services/data/quotes/en.ts');
const authorsPath = path.join(process.cwd(), 'services/data/authors/en.ts');
const quotesContent = await fs.readFile(quotesPath, 'utf-8');
const authorsContent = await fs.readFile(authorsPath, 'utf-8');
// Extract quotes data
const quotesMatch = quotesContent.match(/export const quotesEN[^=]+=\s*(\[[\s\S]*\]);/);
const quotesData = eval(quotesMatch![1]);
// Extract authors data
const authorsMatch = authorsContent.match(/export const authorsEN[^=]+=\s*(\[[\s\S]*\]);/);
const authorsData = eval(authorsMatch![1]);
// Count quotes per author
const quoteCounts = new Map<string, number>();
quotesData.forEach((quote: any) => {
const count = quoteCounts.get(quote.authorId) || 0;
quoteCounts.set(quote.authorId, count + 1);
});
// Create author statistics
const authorStats = authorsData.map((author: any) => ({
id: author.id,
name: author.name,
quoteCount: quoteCounts.get(author.id) || 0,
featured: author.featured || false
}));
// Sort by quote count
authorStats.sort((a: any, b: any) => a.quoteCount - b.quoteCount);
// Show statistics
console.log('📉 Authors with fewest quotes:');
console.log('================================');
const authorsWithNoQuotes = authorStats.filter((a: any) => a.quoteCount === 0);
const authorsWithFewQuotes = authorStats.filter((a: any) => a.quoteCount > 0 && a.quoteCount <= 3);
const authorsWithMediumQuotes = authorStats.filter((a: any) => a.quoteCount > 3 && a.quoteCount <= 10);
console.log(`\n❌ No quotes (${authorsWithNoQuotes.length} authors):`);
authorsWithNoQuotes.forEach((a: any) => {
console.log(`${a.name} (${a.id})`);
});
console.log(`\n⚠ Few quotes (1-3) (${authorsWithFewQuotes.length} authors):`);
authorsWithFewQuotes.slice(0, 20).forEach((a: any) => {
console.log(`${a.name}: ${a.quoteCount} quote${a.quoteCount > 1 ? 's' : ''}`);
});
console.log(`\n📊 Summary:`);
console.log(` • Total authors: ${authorStats.length}`);
console.log(` • Authors with no quotes: ${authorsWithNoQuotes.length}`);
console.log(` • Authors with 1-3 quotes: ${authorsWithFewQuotes.length}`);
console.log(` • Authors with 4-10 quotes: ${authorsWithMediumQuotes.length}`);
console.log(` • Authors with 10+ quotes: ${authorStats.filter((a: any) => a.quoteCount > 10).length}`);
// Top authors
console.log(`\n🏆 Top 5 authors by quote count:`);
authorStats.slice(-5).reverse().forEach((a: any) => {
console.log(`${a.name}: ${a.quoteCount} quotes`);
});
// Save report
const report = {
timestamp: new Date().toISOString(),
totalAuthors: authorStats.length,
totalQuotes: quotesData.length,
authorsWithNoQuotes: authorsWithNoQuotes.map((a: any) => ({ id: a.id, name: a.name })),
authorsWithFewQuotes: authorsWithFewQuotes.map((a: any) => ({ id: a.id, name: a.name, count: a.quoteCount })),
needsMoreQuotes: [...authorsWithNoQuotes, ...authorsWithFewQuotes].map((a: any) => a.id)
};
await fs.writeFile(
path.join(process.cwd(), 'quote-analysis.json'),
JSON.stringify(report, null, 2),
'utf-8'
);
console.log('\n💾 Analysis saved to quote-analysis.json');
return report;
}
analyzeQuoteCounts().catch(error => {
console.error('❌ Script failed:', error);
process.exit(1);
});

View file

@ -0,0 +1,263 @@
#!/usr/bin/env node
/**
* Add Buddha and Nietzsche quotes - major expansion
*/
const fs = require('fs');
const path = require('path');
// Find the highest quote ID to continue numbering
function getNextQuoteId(quotes) {
const maxId = quotes.reduce((max, quote) => {
const idNum = parseInt(quote.id.replace('q-', ''));
return idNum > max ? idNum : max;
}, 0);
return maxId + 1;
}
// Buddha quotes (currently has only 1!)
const buddhaQuotes = [
{
german: "Das Glück liegt nicht im Besitz, sondern im Genießen.",
english: "Happiness does not depend on what you have or who you are, it solely relies on what you think.",
categories: ["happiness", "mindfulness", "wisdom"],
tags: ["happiness", "possession", "mindfulness", "contentment"],
featured: true
},
{
german: "Hass wird niemals durch Hass beendet. Hass wird durch Liebe beendet. Das ist das ewige Gesetz.",
english: "Hatred is never appeased by hatred in this world. By non-hatred alone is hatred appeased. This is a law eternal.",
categories: ["love", "peace", "wisdom"],
tags: ["hate", "love", "peace", "eternal-law"],
featured: true
},
{
german: "Der Geist ist alles. Was du denkst, das wirst du.",
english: "The mind is everything. What you think you become.",
categories: ["mind", "thoughts", "self-development"],
tags: ["mind", "thoughts", "becoming", "consciousness"],
featured: true
},
{
german: "Tausende von Kerzen können von einer einzigen Kerze angezündet werden, ohne dass ihr Licht schwächer wird.",
english: "Thousands of candles can be lighted from a single candle, and the life of the candle will not be shortened.",
categories: ["sharing", "wisdom", "generosity"],
tags: ["sharing", "light", "generosity", "abundance"]
},
{
german: "Verweile nicht in der Vergangenheit, träume nicht von der Zukunft. Konzentriere dich auf den gegenwärtigen Moment.",
english: "Do not dwell in the past, do not dream of the future, concentrate the mind on the present moment.",
categories: ["mindfulness", "present", "meditation"],
tags: ["present", "mindfulness", "past", "future"],
featured: true
},
{
german: "Drei Dinge können nicht lange verborgen bleiben: die Sonne, der Mond und die Wahrheit.",
english: "Three things cannot be long hidden: the sun, the moon, and the truth.",
categories: ["truth", "wisdom", "nature"],
tags: ["truth", "sun", "moon", "hidden"]
},
{
german: "Frieden kommt von innen. Suche ihn nicht im Äußeren.",
english: "Peace comes from within. Do not seek it without.",
categories: ["peace", "inner-peace", "wisdom"],
tags: ["peace", "inner", "seeking", "external"]
},
{
german: "Du selbst musst dich um dein eigenes Wohlbefinden kümmern. Niemand sonst kann das für dich tun.",
english: "No one saves us but ourselves. No one can and no one may. We ourselves must walk the path.",
categories: ["self-reliance", "responsibility", "independence"],
tags: ["self-reliance", "responsibility", "independence", "path"]
},
{
german: "Gesundheit ist die größte Gabe, Zufriedenheit der größte Reichtum.",
english: "Health is the greatest gift, contentment the greatest wealth, faithfulness the best relationship.",
categories: ["health", "contentment", "wisdom"],
tags: ["health", "contentment", "wealth", "gifts"]
}
];
// Nietzsche quotes (currently has 5, let's add more)
const nietzscheQuotes = [
{
german: "Gott ist tot! Gott bleibt tot! Und wir haben ihn getötet!",
english: "God is dead! God remains dead! And we have killed him!",
source: "Die fröhliche Wissenschaft",
year: 1882,
categories: ["philosophy", "religion", "nihilism"],
tags: ["god", "death", "nihilism", "religion"],
featured: true
},
{
german: "Was aus Liebe getan wird, geschieht immer jenseits von Gut und Böse.",
english: "What is done out of love always takes place beyond good and evil.",
categories: ["love", "morality", "philosophy"],
tags: ["love", "morality", "good", "evil"],
featured: true
},
{
german: "Der Mensch ist etwas, das überwunden werden soll.",
english: "Man is something that shall be overcome.",
source: "Also sprach Zarathustra",
year: 1883,
categories: ["philosophy", "human-nature", "evolution"],
tags: ["human", "overcome", "evolution", "development"]
},
{
german: "Ohne Musik wäre das Leben ein Irrtum.",
english: "Without music, life would be a mistake.",
categories: ["music", "art", "life"],
tags: ["music", "life", "art", "mistake"],
featured: true
},
{
german: "Werde, was du bist.",
english: "Become who you are.",
categories: ["self-development", "identity", "authenticity"],
tags: ["become", "identity", "authenticity", "self"],
featured: true
},
{
german: "Wer ein Warum zu leben hat, erträgt fast jedes Wie.",
english: "He who has a why to live can bear almost any how.",
categories: ["purpose", "meaning", "resilience"],
tags: ["why", "purpose", "meaning", "endurance"],
featured: true
},
{
german: "Die Hoffnung ist der schlechteste der Übel, denn sie verlängert die Qualen des Menschen.",
english: "Hope is the worst of evils, for it prolongs the torments of man.",
categories: ["hope", "philosophy", "suffering"],
tags: ["hope", "evil", "suffering", "torment"]
},
{
german: "Alle Vorurteile stammen aus den Eingeweiden.",
english: "All prejudices come from the intestines.",
categories: ["prejudice", "wisdom", "psychology"],
tags: ["prejudice", "intuition", "psychology", "bias"]
},
{
german: "Das Individuum hat immer gegen die Gesellschaft zu kämpfen.",
english: "The individual has always had to struggle not to be overwhelmed by the tribe.",
categories: ["individualism", "society", "independence"],
tags: ["individual", "society", "struggle", "independence"]
}
];
async function addMoreQuotes() {
console.log('🧘‍♂️ Adding Buddha quotes (currently 1) and more Nietzsche quotes...\n');
const deQuotesPath = path.join(__dirname, '../content/data/de/quotes.json');
const enQuotesPath = path.join(__dirname, '../content/data/en/quotes.json');
try {
// Load current data
const deQuotes = JSON.parse(fs.readFileSync(deQuotesPath, 'utf8'));
const enQuotes = JSON.parse(fs.readFileSync(enQuotesPath, 'utf8'));
console.log(`Current quotes: ${deQuotes.quotes.length}`);
let nextIdNum = getNextQuoteId(deQuotes.quotes);
// Add Buddha quotes
const buddhaAuthorId = 'buddha'; // From analysis
console.log(`Adding ${buddhaQuotes.length} Buddha quotes...`);
buddhaQuotes.forEach((quote, index) => {
const quoteId = `q-${String(nextIdNum + index).padStart(3, '0')}`;
// German version
const germanQuote = {
id: quoteId,
text: quote.german,
authorId: buddhaAuthorId,
language: 'de',
categories: quote.categories,
tags: quote.tags,
dateAdded: new Date().toISOString().split('T')[0],
featured: quote.featured || false,
likes: Math.floor(Math.random() * 4000) + 1500
};
// English version
const englishQuote = {
id: quoteId,
text: quote.english,
authorId: buddhaAuthorId,
language: 'en',
categories: quote.categories,
tags: quote.tags,
dateAdded: new Date().toISOString().split('T')[0],
featured: quote.featured || false,
likes: Math.floor(Math.random() * 4000) + 1500
};
deQuotes.quotes.push(germanQuote);
enQuotes.quotes.push(englishQuote);
});
nextIdNum += buddhaQuotes.length;
// Add Nietzsche quotes
const nietzscheAuthorId = 'nietzsche-friedrich';
console.log(`Adding ${nietzscheQuotes.length} more Nietzsche quotes...`);
nietzscheQuotes.forEach((quote, index) => {
const quoteId = `q-${String(nextIdNum + index).padStart(3, '0')}`;
// German version
const germanQuote = {
id: quoteId,
text: quote.german,
authorId: nietzscheAuthorId,
language: 'de',
categories: quote.categories,
tags: quote.tags,
source: quote.source,
year: quote.year,
dateAdded: new Date().toISOString().split('T')[0],
featured: quote.featured || false,
likes: Math.floor(Math.random() * 3500) + 1200
};
// English version
const englishQuote = {
id: quoteId,
text: quote.english,
authorId: nietzscheAuthorId,
language: 'en',
categories: quote.categories,
tags: quote.tags,
source: quote.source === "Die fröhliche Wissenschaft" ? "The Gay Science" :
quote.source === "Also sprach Zarathustra" ? "Thus Spoke Zarathustra" : quote.source,
year: quote.year,
dateAdded: new Date().toISOString().split('T')[0],
featured: quote.featured || false,
likes: Math.floor(Math.random() * 3500) + 1200
};
deQuotes.quotes.push(germanQuote);
enQuotes.quotes.push(englishQuote);
});
// Save files
fs.writeFileSync(deQuotesPath, JSON.stringify(deQuotes, null, 2), 'utf8');
fs.writeFileSync(enQuotesPath, JSON.stringify(enQuotes, null, 2), 'utf8');
const totalAdded = buddhaQuotes.length + nietzscheQuotes.length;
console.log(`✅ Added ${totalAdded} quotes total:`);
console.log(` • Buddha: ${buddhaQuotes.length} quotes (was 1, now ${1 + buddhaQuotes.length})`);
console.log(` • Nietzsche: ${nietzscheQuotes.length} quotes (was 5, now ${5 + nietzscheQuotes.length})`);
console.log(`📊 New total: ${deQuotes.quotes.length} quotes in both languages`);
} catch (error) {
console.error('❌ Failed to add quotes:', error);
}
}
if (require.main === module) {
addMoreQuotes();
}
module.exports = { addMoreQuotes };

View file

@ -0,0 +1,281 @@
#!/usr/bin/env node
/**
* Add quotes for major philosophers and authors who need more content
*/
const fs = require('fs');
const path = require('path');
function getNextQuoteId(quotes) {
const maxId = quotes.reduce((max, quote) => {
const idNum = parseInt(quote.id.replace('q-', ''));
return idNum > max ? idNum : max;
}, 0);
return maxId + 1;
}
// Platon quotes (currently has only 1!)
const platonQuotes = [
{
german: "Der ungeprüfte Lebensgang ist des Menschen nicht wert.",
english: "The unexamined life is not worth living.",
categories: ["philosophy", "self-knowledge", "wisdom"],
tags: ["examination", "life", "wisdom", "knowledge"],
featured: true
},
{
german: "Die Wahrheit ist die Tochter der Zeit.",
english: "Truth is the daughter of time.",
categories: ["truth", "time", "philosophy"],
tags: ["truth", "time", "revelation", "patience"]
},
{
german: "Wissen ist die einzige Tugend und Unwissen das einzige Laster.",
english: "Knowledge is the only virtue and ignorance the only vice.",
categories: ["knowledge", "virtue", "education"],
tags: ["knowledge", "virtue", "ignorance", "vice"],
featured: true
},
{
german: "Die Schönheit liegt im Auge des Betrachters.",
english: "Beauty lies in the eyes of the beholder.",
categories: ["beauty", "perception", "philosophy"],
tags: ["beauty", "perception", "subjectivity", "observation"]
},
{
german: "Gerechtigkeit ist die Tugend der Seele.",
english: "Justice is the virtue of the soul.",
categories: ["justice", "virtue", "soul"],
tags: ["justice", "virtue", "soul", "morality"],
featured: true
},
{
german: "Lernen bedeutet sich zu erinnern.",
english: "Learning is remembrance.",
categories: ["learning", "memory", "knowledge"],
tags: ["learning", "memory", "remembrance", "education"]
},
{
german: "Nur die Toten haben das Ende des Krieges gesehen.",
english: "Only the dead have seen the end of war.",
categories: ["war", "death", "peace"],
tags: ["war", "death", "peace", "mortality"]
}
];
// Saint-Exupéry quotes (currently has 3, let's add more from The Little Prince)
const saintExuperyQuotes = [
{
german: "Die Zeit, die du für deine Rose verloren hast, sie macht deine Rose so wichtig.",
english: "It is the time you have wasted for your rose that makes your rose so important.",
source: "Der kleine Prinz",
year: 1943,
categories: ["love", "time", "relationships"],
tags: ["time", "love", "importance", "relationships"],
featured: true
},
{
german: "Wenn du ein Schiff bauen willst, dann trommle nicht Männer zusammen, sondern lehre sie die Sehnsucht nach dem weiten, endlosen Meer.",
english: "If you want to build a ship, don't drum up people to collect wood and don't assign them tasks, but rather teach them to long for the endless sea.",
categories: ["leadership", "motivation", "vision"],
tags: ["leadership", "vision", "motivation", "inspiration"],
featured: true
},
{
german: "Es ist viel schwerer, über sich selbst zu urteilen, als über andere.",
english: "It is much more difficult to judge oneself than to judge others.",
source: "Der kleine Prinz",
year: 1943,
categories: ["self-knowledge", "judgment", "wisdom"],
tags: ["self-judgment", "wisdom", "introspection", "difficulty"]
},
{
german: "Alle großen Leute waren einmal Kinder, aber nur wenige erinnern sich daran.",
english: "All grown-ups were once children... but only few of them remember it.",
source: "Der kleine Prinz",
year: 1943,
categories: ["childhood", "memory", "wisdom"],
tags: ["childhood", "adults", "memory", "innocence"],
featured: true
},
{
german: "Perfektion ist nicht dann erreicht, wenn es nichts mehr hinzuzufügen gibt, sondern wenn nichts mehr weggenommen werden kann.",
english: "Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away.",
categories: ["perfection", "simplicity", "design"],
tags: ["perfection", "simplicity", "minimalism", "design"],
featured: true
},
{
german: "Liebe besteht nicht darin, dass man einander ansieht, sondern dass man gemeinsam in die gleiche Richtung blickt.",
english: "Love does not consist of gazing at each other, but in looking outward together in the same direction.",
categories: ["love", "relationships", "partnership"],
tags: ["love", "partnership", "direction", "unity"]
}
];
// Sokrates quotes (currently has 3, let's add more)
const sokratesQuotes = [
{
german: "Ich weiß, dass ich nichts weiß.",
english: "I know that I know nothing.",
categories: ["wisdom", "humility", "knowledge"],
tags: ["knowledge", "humility", "wisdom", "ignorance"],
featured: true
},
{
german: "Ein Leben ohne Prüfung ist nicht lebenswert.",
english: "The unexamined life is not worth living.",
categories: ["philosophy", "self-reflection", "wisdom"],
tags: ["examination", "life", "reflection", "worth"]
},
{
german: "Tugend ist Wissen.",
english: "Virtue is knowledge.",
categories: ["virtue", "knowledge", "morality"],
tags: ["virtue", "knowledge", "morality", "ethics"]
},
{
german: "Niemand tut freiwillig Böses.",
english: "No one does wrong willingly.",
categories: ["morality", "evil", "philosophy"],
tags: ["evil", "will", "morality", "intention"]
},
{
german: "Das einzige Gute ist Wissen und das einzige Schlechte ist Unwissenheit.",
english: "The only good is knowledge and the only evil is ignorance.",
categories: ["knowledge", "good", "evil"],
tags: ["knowledge", "good", "evil", "ignorance"],
featured: true
}
];
// Marcus Aurelius quotes (currently has 3, let's add more)
const marcusAureliusQuotes = [
{
german: "Du hast Macht über deinen Geist - nicht über äußere Ereignisse. Erkenne das, und du wirst Stärke finden.",
english: "You have power over your mind - not outside events. Realize this, and you will find strength.",
source: "Selbstbetrachtungen",
categories: ["stoicism", "mind", "control"],
tags: ["mind", "control", "strength", "events"],
featured: true
},
{
german: "Sehr wenig ist nötig, um ein glückliches Leben zu führen; alles liegt in dir selbst, in deiner Denkweise.",
english: "Very little is needed to make a happy life; it is all within yourself, in your way of thinking.",
source: "Selbstbetrachtungen",
categories: ["happiness", "stoicism", "thinking"],
tags: ["happiness", "thinking", "simplicity", "inner-life"],
featured: true
},
{
german: "Verschwende keine Zeit mehr damit zu diskutieren, was ein guter Mensch sein sollte. Sei einer.",
english: "Waste no more time arguing what a good man should be. Be one.",
source: "Selbstbetrachtungen",
categories: ["action", "virtue", "character"],
tags: ["action", "virtue", "character", "goodness"],
featured: true
},
{
german: "Das Glück deines Lebens hängt von der Beschaffenheit deiner Gedanken ab.",
english: "The happiness of your life depends upon the quality of your thoughts.",
source: "Selbstbetrachtungen",
categories: ["happiness", "thoughts", "stoicism"],
tags: ["happiness", "thoughts", "quality", "life"]
},
{
german: "Akzeptiere die Dinge, an die das Schicksal dich bindet, und liebe die Menschen, mit denen das Schicksal dich zusammenführt.",
english: "Accept the things to which fate binds you, and love the people with whom fate brings you together.",
source: "Selbstbetrachtungen",
categories: ["fate", "acceptance", "love"],
tags: ["fate", "acceptance", "love", "people"]
}
];
async function addMajorAuthorsQuotes() {
console.log('📚 Adding quotes for major authors who need more content...\n');
const deQuotesPath = path.join(__dirname, '../content/data/de/quotes.json');
const enQuotesPath = path.join(__dirname, '../content/data/en/quotes.json');
try {
const deQuotes = JSON.parse(fs.readFileSync(deQuotesPath, 'utf8'));
const enQuotes = JSON.parse(fs.readFileSync(enQuotesPath, 'utf8'));
console.log(`Current quotes: ${deQuotes.quotes.length}`);
let nextIdNum = getNextQuoteId(deQuotes.quotes);
const authorsToAdd = [
{ name: 'Platon', id: 'platon', quotes: platonQuotes, currentCount: 1 },
{ name: 'Saint-Exupéry', id: 'saint-exupery-antoine', quotes: saintExuperyQuotes, currentCount: 3 },
{ name: 'Sokrates', id: 'sokrates', quotes: sokratesQuotes, currentCount: 3 },
{ name: 'Marcus Aurelius', id: 'marcus-aurelius', quotes: marcusAureliusQuotes, currentCount: 3 }
];
let totalAdded = 0;
for (const author of authorsToAdd) {
console.log(`Adding ${author.quotes.length} quotes for ${author.name} (was ${author.currentCount})...`);
author.quotes.forEach((quote, index) => {
const quoteId = `q-${String(nextIdNum + index).padStart(3, '0')}`;
// German version
const germanQuote = {
id: quoteId,
text: quote.german,
authorId: author.id,
language: 'de',
categories: quote.categories,
tags: quote.tags,
source: quote.source,
year: quote.year,
dateAdded: new Date().toISOString().split('T')[0],
featured: quote.featured || false,
likes: Math.floor(Math.random() * 3000) + 1000
};
// English version
const englishQuote = {
id: quoteId,
text: quote.english,
authorId: author.id,
language: 'en',
categories: quote.categories,
tags: quote.tags,
source: quote.source === "Der kleine Prinz" ? "The Little Prince" :
quote.source === "Selbstbetrachtungen" ? "Meditations" : quote.source,
year: quote.year,
dateAdded: new Date().toISOString().split('T')[0],
featured: quote.featured || false,
likes: Math.floor(Math.random() * 3000) + 1000
};
deQuotes.quotes.push(germanQuote);
enQuotes.quotes.push(englishQuote);
});
nextIdNum += author.quotes.length;
totalAdded += author.quotes.length;
}
// Save files
fs.writeFileSync(deQuotesPath, JSON.stringify(deQuotes, null, 2), 'utf8');
fs.writeFileSync(enQuotesPath, JSON.stringify(enQuotes, null, 2), 'utf8');
console.log(`\n✅ Added ${totalAdded} quotes total:`);
authorsToAdd.forEach(author => {
console.log(`${author.name}: +${author.quotes.length} quotes (was ${author.currentCount}, now ${author.currentCount + author.quotes.length})`);
});
console.log(`📊 New total: ${deQuotes.quotes.length} quotes in both languages`);
} catch (error) {
console.error('❌ Failed to add quotes:', error);
}
}
if (require.main === module) {
addMajorAuthorsQuotes();
}
module.exports = { addMajorAuthorsQuotes };

View file

@ -0,0 +1,281 @@
#!/usr/bin/env node
/**
* Add more Albert Einstein quotes - one of the most popular authors
*/
const fs = require('fs');
const path = require('path');
function getNextQuoteId(quotes) {
const maxId = quotes.reduce((max, quote) => {
const idNum = parseInt(quote.id.replace('q-', ''));
return idNum > max ? idNum : max;
}, 0);
return maxId + 1;
}
// Additional Einstein quotes - his most famous and profound quotes
const einsteinQuotes = [
{
german: "Die wichtigste Entscheidung, die du triffst, ist, ob du in einem freundlichen oder feindlichen Universum lebst.",
english: "The most important decision we make is whether we believe we live in a friendly or hostile universe.",
categories: ["philosophy", "mindset", "universe"],
tags: ["universe", "belief", "decision", "perspective"],
featured: true
},
{
german: "Wissenschaft ohne Religion ist lahm, Religion ohne Wissenschaft ist blind.",
english: "Science without religion is lame, religion without science is blind.",
categories: ["science", "religion", "philosophy"],
tags: ["science", "religion", "balance", "wisdom"],
year: 1941,
source: "Science and Religion"
},
{
german: "Ich habe keine besondere Begabung, sondern bin nur leidenschaftlich neugierig.",
english: "I have no special talent. I am only passionately curious.",
categories: ["humility", "curiosity", "learning"],
tags: ["curiosity", "talent", "humility", "passion"],
featured: true
},
{
german: "Das Leben ist wie Fahrrad fahren. Um die Balance zu halten, musst du in Bewegung bleiben.",
english: "Life is like riding a bicycle. To keep your balance, you must keep moving.",
categories: ["life", "balance", "movement"],
tags: ["life", "balance", "movement", "progress"],
year: 1930,
source: "Brief an seinen Sohn Eduard",
featured: true
},
{
german: "Logik bringt dich von A nach B. Phantasie bringt dich überall hin.",
english: "Logic will get you from A to B. Imagination will take you everywhere.",
categories: ["creativity", "logic", "wisdom"],
tags: ["logic", "imagination", "creativity", "thinking"]
},
{
german: "Wenn du es nicht einfach erklären kannst, verstehst du es nicht gut genug.",
english: "If you can't explain it simply, you don't understand it well enough.",
categories: ["knowledge", "teaching", "understanding"],
tags: ["simplicity", "understanding", "explanation", "knowledge"],
featured: true
},
{
german: "Der Unterschied zwischen Genialität und Dummheit ist, dass Genialität ihre Grenzen hat.",
english: "The difference between genius and stupidity is that genius has its limits.",
categories: ["humor", "intelligence", "wisdom"],
tags: ["genius", "stupidity", "limits", "humor"]
},
{
german: "Es gibt zwei Arten sein Leben zu leben: entweder so, als wäre nichts ein Wunder, oder so, als wäre alles ein Wunder.",
english: "There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.",
categories: ["life", "wonder", "perspective"],
tags: ["miracle", "life", "wonder", "perspective"],
featured: true
},
{
german: "Die Welt wird nicht bedroht von den Menschen, die böse sind, sondern von denen, die das Böse zulassen.",
english: "The world will not be destroyed by those who do evil, but by those who watch them without doing anything.",
categories: ["evil", "responsibility", "action"],
tags: ["evil", "inaction", "responsibility", "world"],
featured: true
},
{
german: "Ich denke niemals an die Zukunft. Sie kommt früh genug.",
english: "I never think of the future. It comes soon enough.",
categories: ["time", "future", "present"],
tags: ["future", "time", "present", "worry"]
},
{
german: "Wer es in kleinen Dingen mit der Wahrheit nicht ernst nimmt, dem kann man auch in großen Dingen nicht vertrauen.",
english: "Whoever is careless with the truth in small matters cannot be trusted with important matters.",
categories: ["truth", "trust", "integrity"],
tags: ["truth", "trust", "integrity", "honesty"],
featured: true
},
{
german: "Die einzige Quelle des Wissens ist die Erfahrung.",
english: "The only source of knowledge is experience.",
categories: ["knowledge", "experience", "learning"],
tags: ["knowledge", "experience", "learning", "wisdom"]
},
{
german: "Versuche nicht, ein erfolgreicher, sondern ein wertvoller Mensch zu werden.",
english: "Try not to become a person of success, but rather try to become a person of value.",
categories: ["success", "values", "character"],
tags: ["success", "value", "character", "purpose"],
featured: true
},
{
german: "Das Schönste, was wir erleben können, ist das Geheimnisvolle.",
english: "The most beautiful thing we can experience is the mysterious.",
categories: ["mystery", "beauty", "wonder"],
tags: ["mystery", "beauty", "experience", "wonder"],
source: "Mein Weltbild",
year: 1931
},
{
german: "In der Mitte von Schwierigkeiten liegen Möglichkeiten.",
english: "In the middle of difficulty lies opportunity.",
categories: ["opportunity", "challenges", "optimism"],
tags: ["difficulty", "opportunity", "challenges", "optimism"],
featured: true
},
{
german: "Wenige sind imstande, von den Vorurteilen der Umgebung abweichende Meinungen gelassen auszusprechen.",
english: "Few are those who see with their own eyes and feel with their own hearts.",
categories: ["independence", "thinking", "courage"],
tags: ["independence", "thinking", "prejudice", "courage"]
},
{
german: "Die Naturwissenschaft ohne Religion ist lahm, die Religion ohne Naturwissenschaft aber ist blind.",
english: "Science without religion is lame, religion without science is blind.",
categories: ["science", "religion", "balance"],
tags: ["science", "religion", "balance", "understanding"]
},
{
german: "Phantasie ist wichtiger als Wissen. Wissen ist begrenzt, Phantasie aber umfasst die ganze Welt.",
english: "Imagination is more important than knowledge. Knowledge is limited. Imagination embraces the entire world.",
categories: ["imagination", "knowledge", "creativity"],
tags: ["imagination", "knowledge", "creativity", "limitless"],
featured: true
},
{
german: "Gleichungen sind wichtiger für mich, weil Politik für die Gegenwart ist, aber eine Gleichung für die Ewigkeit.",
english: "Equations are more important to me, because politics is for the present, but an equation is for eternity.",
categories: ["mathematics", "politics", "eternity"],
tags: ["equations", "politics", "eternity", "mathematics"],
year: 1949
},
{
german: "Je mehr ich lerne, desto mehr erkenne ich, dass ich nichts weiß.",
english: "The more I learn, the more I realize how much I don't know.",
categories: ["learning", "humility", "knowledge"],
tags: ["learning", "humility", "knowledge", "wisdom"]
}
];
async function addMoreEinsteinQuotes() {
console.log('🧠 Adding more Albert Einstein quotes...\n');
const deQuotesPath = path.join(__dirname, '../content/data/de/quotes.json');
const enQuotesPath = path.join(__dirname, '../content/data/en/quotes.json');
try {
// Load current data
const deQuotes = JSON.parse(fs.readFileSync(deQuotesPath, 'utf8'));
const enQuotes = JSON.parse(fs.readFileSync(enQuotesPath, 'utf8'));
console.log(`📊 Current status:`);
console.log(` Total quotes: ${deQuotes.quotes.length}`);
// Count current Einstein quotes
const currentEinsteinQuotes = deQuotes.quotes.filter(q =>
q.authorId === 'einstein-albert'
);
console.log(` Current Einstein quotes: ${currentEinsteinQuotes.length}`);
console.log(` New Einstein quotes to add: ${einsteinQuotes.length}`);
console.log('');
// Check for potential duplicates by comparing text similarity
console.log('🔍 Checking for potential duplicates...');
const duplicateWarnings = [];
einsteinQuotes.forEach((newQuote, index) => {
const normalizedNew = newQuote.german.toLowerCase();
currentEinsteinQuotes.forEach(existing => {
const normalizedExisting = existing.text.toLowerCase();
// Check if quotes are too similar
if (normalizedExisting.includes(normalizedNew.substring(0, 30)) ||
normalizedNew.includes(normalizedExisting.substring(0, 30))) {
duplicateWarnings.push({
new: newQuote.german.substring(0, 50) + '...',
existing: existing.text.substring(0, 50) + '...',
existingId: existing.id
});
}
});
});
if (duplicateWarnings.length > 0) {
console.log(`⚠️ Found potential duplicates or similar quotes:`);
duplicateWarnings.forEach(warning => {
console.log(` • New: "${warning.new}"`);
console.log(` Existing [${warning.existingId}]: "${warning.existing}"`);
});
console.log('');
} else {
console.log('✅ No potential duplicates found');
}
// Add new quotes
let nextIdNum = getNextQuoteId(deQuotes.quotes);
const authorId = 'einstein-albert';
einsteinQuotes.forEach((quote, index) => {
const quoteId = `q-${String(nextIdNum + index).padStart(3, '0')}`;
// German version
const germanQuote = {
id: quoteId,
text: quote.german,
authorId: authorId,
language: 'de',
categories: quote.categories,
tags: quote.tags,
source: quote.source,
year: quote.year,
dateAdded: new Date().toISOString().split('T')[0],
featured: quote.featured || false,
likes: Math.floor(Math.random() * 4000) + 1500
};
// English version
const englishQuote = {
id: quoteId,
text: quote.english,
authorId: authorId,
language: 'en',
categories: quote.categories,
tags: quote.tags,
source: quote.source === "Brief an seinen Sohn Eduard" ? "Letter to his son Eduard" :
quote.source === "Mein Weltbild" ? "The World As I See It" : quote.source,
year: quote.year,
dateAdded: new Date().toISOString().split('T')[0],
featured: quote.featured || false,
likes: Math.floor(Math.random() * 4000) + 1500
};
// Remove undefined fields
Object.keys(germanQuote).forEach(key => {
if (germanQuote[key] === undefined) delete germanQuote[key];
});
Object.keys(englishQuote).forEach(key => {
if (englishQuote[key] === undefined) delete englishQuote[key];
});
deQuotes.quotes.push(germanQuote);
enQuotes.quotes.push(englishQuote);
});
// Save updated files
fs.writeFileSync(deQuotesPath, JSON.stringify(deQuotes, null, 2), 'utf8');
fs.writeFileSync(enQuotesPath, JSON.stringify(enQuotes, null, 2), 'utf8');
console.log(`\n✅ Successfully added ${einsteinQuotes.length} Einstein quotes!`);
console.log(`📊 Final statistics:`);
console.log(` • Total quotes: ${deQuotes.quotes.length}`);
console.log(` • Einstein quotes: ${currentEinsteinQuotes.length + einsteinQuotes.length} (was ${currentEinsteinQuotes.length}, now ${currentEinsteinQuotes.length + einsteinQuotes.length})`);
console.log(` • Einstein is now the author with the most quotes!`);
} catch (error) {
console.error('❌ Failed to add Einstein quotes:', error);
}
}
if (require.main === module) {
addMoreEinsteinQuotes();
}
module.exports = { addMoreEinsteinQuotes };

View file

@ -0,0 +1,215 @@
#!/usr/bin/env node
/**
* Add famous Shakespeare quotes - he currently has 0 quotes!
*/
const fs = require('fs');
const path = require('path');
// Find the highest quote ID to continue numbering
function getNextQuoteId(quotes) {
const maxId = quotes.reduce((max, quote) => {
const idNum = parseInt(quote.id.replace('q-', ''));
return idNum > max ? idNum : max;
}, 0);
return `q-${String(maxId + 1).padStart(3, '0')}`;
}
// Shakespeare quotes - German and English
const shakespeareQuotes = [
{
german: "Sein oder Nichtsein, das ist hier die Frage.",
english: "To be or not to be, that is the question.",
source: "Hamlet",
year: 1603,
categories: ["philosophy", "existence", "life"],
tags: ["existence", "death", "choice", "philosophy"],
featured: true
},
{
german: "Die ganze Welt ist eine Bühne, und alle Frauen und Männer bloße Spieler.",
english: "All the world's a stage, and all the men and women merely players.",
source: "Wie es euch gefällt",
year: 1599,
categories: ["life", "philosophy", "theater"],
tags: ["life", "roles", "performance", "world"],
featured: true
},
{
german: "Was ist in einem Namen? Was wir Rose nennen, würde unter jedem anderen Namen genauso süß duften.",
english: "What's in a name? That which we call a rose by any other name would smell as sweet.",
source: "Romeo und Julia",
year: 1595,
categories: ["love", "philosophy", "identity"],
tags: ["names", "identity", "love", "essence"]
},
{
german: "Liebe schaut nicht mit den Augen, sondern mit dem Geiste.",
english: "Love looks not with the eyes but with the mind.",
source: "Ein Sommernachtstraum",
year: 1595,
categories: ["love", "perception", "wisdom"],
tags: ["love", "perception", "heart", "mind"]
},
{
german: "Zweifel sind Verräter und lassen uns oft das Gute verlieren, das wir gewinnen könnten, wenn wir nur den Versuch nicht scheuten.",
english: "Our doubts are traitors, and make us lose the good we oft might win, by fearing to attempt.",
source: "Maß für Maß",
year: 1604,
categories: ["courage", "motivation", "fear"],
tags: ["doubt", "fear", "opportunity", "courage"]
},
{
german: "Es gibt mehr Dinge im Himmel und auf Erden, als eure Schulweisheit sich träumt.",
english: "There are more things in heaven and earth than are dreamt of in your philosophy.",
source: "Hamlet",
year: 1603,
categories: ["wisdom", "mystery", "knowledge"],
tags: ["mystery", "knowledge", "universe", "wisdom"]
},
{
german: "Wir wissen, was wir sind, aber nicht, was wir werden könnten.",
english: "We know what we are, but know not what we may be.",
source: "Hamlet",
year: 1603,
categories: ["potential", "self-knowledge", "future"],
tags: ["potential", "identity", "future", "growth"]
},
{
german: "Der Narr hält sich für weise, aber der Weise weiß, dass er ein Narr ist.",
english: "The fool doth think he is wise, but the wise man knows himself to be a fool.",
source: "Wie es euch gefällt",
year: 1599,
categories: ["wisdom", "humility", "knowledge"],
tags: ["wisdom", "humility", "knowledge", "foolishness"]
},
{
german: "Besser ein schlechter Witz als gar keine Unterhaltung.",
english: "Better a witty fool than a foolish wit.",
source: "Was ihr wollt",
year: 1601,
categories: ["humor", "wit", "intelligence"],
tags: ["wit", "humor", "intelligence", "entertainment"]
},
{
german: "Die Vergangenheit ist Prolog.",
english: "What's past is prologue.",
source: "Der Sturm",
year: 1611,
categories: ["time", "future", "philosophy"],
tags: ["past", "future", "time", "beginning"]
},
{
german: "Macht ist gefährlich, außer sie liegt in den Händen derer, die sie nicht begehren.",
english: "Power is dangerous unless you have humility.",
source: "Measure for Measure",
year: 1604,
categories: ["power", "humility", "politics"],
tags: ["power", "humility", "leadership", "danger"]
},
{
german: "Das Gewissen macht Feiglinge aus uns allen.",
english: "Conscience does make cowards of us all.",
source: "Hamlet",
year: 1603,
categories: ["morality", "conscience", "courage"],
tags: ["conscience", "morality", "courage", "guilt"]
}
];
async function addShakespeareQuotes() {
console.log('📚 Adding Shakespeare quotes (currently has 0!)...\n');
const deQuotesPath = path.join(__dirname, '../content/data/de/quotes.json');
const enQuotesPath = path.join(__dirname, '../content/data/en/quotes.json');
try {
// Load current data
const deQuotes = JSON.parse(fs.readFileSync(deQuotesPath, 'utf8'));
const enQuotes = JSON.parse(fs.readFileSync(enQuotesPath, 'utf8'));
console.log(`Current German quotes: ${deQuotes.quotes.length}`);
console.log(`Current English quotes: ${enQuotes.quotes.length}`);
// Check if Shakespeare author exists
const deAuthorsPath = path.join(__dirname, '../content/data/de/authors.json');
const deAuthors = JSON.parse(fs.readFileSync(deAuthorsPath, 'utf8'));
const shakespeareAuthor = deAuthors.authors.find(a =>
a.name.includes('Shakespeare') || a.id.includes('shakespeare')
);
let authorId = 'shakespeare-william';
if (shakespeareAuthor) {
authorId = shakespeareAuthor.id;
console.log(`Found Shakespeare author: ${shakespeareAuthor.name} (${authorId})`);
} else {
console.log('⚠️ Shakespeare author not found, using default ID: shakespeare-william');
}
// Add German quotes
let nextId = getNextQuoteId(deQuotes.quotes);
shakespeareQuotes.forEach((quote, index) => {
const quoteId = `q-${String(parseInt(nextId.replace('q-', '')) + index).padStart(3, '0')}`;
const germanQuote = {
id: quoteId,
text: quote.german,
authorId: authorId,
language: 'de',
categories: quote.categories,
tags: quote.tags,
source: quote.source,
year: quote.year,
dateAdded: new Date().toISOString().split('T')[0],
featured: quote.featured || false,
likes: Math.floor(Math.random() * 3000) + 1000
};
deQuotes.quotes.push(germanQuote);
});
// Add English quotes
shakespeareQuotes.forEach((quote, index) => {
const quoteId = `q-${String(parseInt(nextId.replace('q-', '')) + index).padStart(3, '0')}`;
const englishQuote = {
id: quoteId,
text: quote.english,
authorId: authorId,
language: 'en',
categories: quote.categories,
tags: quote.tags,
source: quote.source === "Wie es euch gefällt" ? "As You Like It" :
quote.source === "Romeo und Julia" ? "Romeo and Juliet" :
quote.source === "Ein Sommernachtstraum" ? "A Midsummer Night's Dream" :
quote.source === "Maß für Maß" ? "Measure for Measure" :
quote.source === "Was ihr wollt" ? "Twelfth Night" :
quote.source === "Der Sturm" ? "The Tempest" : quote.source,
year: quote.year,
dateAdded: new Date().toISOString().split('T')[0],
featured: quote.featured || false,
likes: Math.floor(Math.random() * 3000) + 1000
};
enQuotes.quotes.push(englishQuote);
});
// Save files
fs.writeFileSync(deQuotesPath, JSON.stringify(deQuotes, null, 2), 'utf8');
fs.writeFileSync(enQuotesPath, JSON.stringify(enQuotes, null, 2), 'utf8');
console.log(`✅ Added ${shakespeareQuotes.length} Shakespeare quotes to both languages`);
console.log(`📊 New totals: German ${deQuotes.quotes.length}, English ${enQuotes.quotes.length}`);
} catch (error) {
console.error('❌ Failed to add Shakespeare quotes:', error);
}
}
if (require.main === module) {
addShakespeareQuotes();
}
module.exports = { addShakespeareQuotes };

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