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

@ -0,0 +1,5 @@
/**
* App Configurations
*/
export { quotesAppConfig } from './quotes.config';

View file

@ -0,0 +1,100 @@
/**
* Default configuration for Quotes app
*/
import type { FullAppConfig, Quote } from '../types';
export const quotesAppConfig: FullAppConfig<Quote> = {
metadata: {
name: 'quotes',
displayName: 'Zitate',
description: 'Inspirierende Zitate von großen Denkern',
version: '1.0.0',
primaryLanguage: 'de',
supportedLanguages: ['de', 'en'],
},
contentType: 'quote',
contentLabel: {
singular: 'Zitat',
plural: 'Zitate',
},
authorLabel: {
singular: 'Autor',
plural: 'Autoren',
},
colors: {
primary: '#667eea',
secondary: '#764ba2',
accent: '#f093fb',
},
features: {
favorites: true,
lists: true,
sharing: true,
search: true,
filters: true,
authors: true,
categories: true,
tags: true,
dailyContent: true,
notifications: true,
widgets: true,
cloudSync: true,
premium: true,
},
display: {
showAuthor: true,
showDate: false,
showSource: true,
showCategory: true,
showTags: true,
cardStyle: 'detailed',
swipeDirection: 'horizontal',
},
navigation: {
tabs: [
{
id: 'quotes',
label: 'Zitate',
icon: 'quote',
route: '/(tabs)',
enabled: true,
},
{
id: 'authors',
label: 'Autoren',
icon: 'person',
route: '/(tabs)/authors',
enabled: true,
},
{
id: 'lists',
label: 'Listen',
icon: 'list',
route: '/(tabs)/liste',
enabled: true,
},
{
id: 'favorites',
label: 'Favoriten',
icon: 'heart',
route: '/(tabs)/myquotes',
enabled: true,
},
{
id: 'search',
label: 'Suche',
icon: 'search',
route: '/(tabs)/search',
enabled: true,
},
],
showTabBar: true,
tabBarStyle: 'ios',
},
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,14 @@
/**
* Data exports - For backward compatibility
*
* Note: In the new architecture, content-specific data should be imported
* from dedicated packages like @quote/content-quotes, @quote/content-proverbs, etc.
*
* These exports are kept for backward compatibility with existing apps.
*/
// Re-export all data modules for backward compatibility
export { quotesDE } from './quotes/de';
export { quotesEN } from './quotes/en';
export { authorsDE } from './authors/de';
export { authorsEN } from './authors/en';

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
/**
* @quote/shared - Shared package for content apps
*
* Exports types, data, utilities, and configs used across all content apps
* (quotes, proverbs, poems, etc.)
*/
// Export types
export * from './types';
// Export data
export * from './data';
// Export utils
export * from './utils';
// Export configs
export * from './configs';

View file

@ -0,0 +1,124 @@
/**
* App Configuration Types
*
* Defines the configuration structure for each content app
*/
import type { ContentItem } from './index';
/**
* Content type identifier
*/
export type ContentType = 'quote' | 'proverb' | 'poem' | 'speech' | 'fable' | 'thought';
/**
* Color configuration for the app
*/
export interface AppColors {
primary: string;
secondary?: string;
accent?: string;
background?: string;
surface?: string;
text?: string;
}
/**
* Feature flags for the app
*/
export interface AppFeatures {
favorites?: boolean;
lists?: boolean;
sharing?: boolean;
search?: boolean;
filters?: boolean;
authors?: boolean;
categories?: boolean;
tags?: boolean;
dailyContent?: boolean;
notifications?: boolean;
widgets?: boolean;
cloudSync?: boolean;
premium?: boolean;
}
/**
* Content display configuration
*/
export interface ContentDisplayConfig {
showAuthor?: boolean;
showDate?: boolean;
showSource?: boolean;
showCategory?: boolean;
showTags?: boolean;
cardStyle?: 'minimal' | 'detailed' | 'magazine';
swipeDirection?: 'horizontal' | 'vertical';
}
/**
* App metadata
*/
export interface AppMetadata {
name: string;
displayName: string;
description: string;
version: string;
author?: string;
website?: string;
icon?: string;
primaryLanguage: string;
supportedLanguages: string[];
}
/**
* Main App Configuration
*/
export interface AppConfig<TContent extends ContentItem = ContentItem> {
// Basic Info
metadata: AppMetadata;
// Content Type
contentType: ContentType;
contentLabel: {
singular: string;
plural: string;
};
authorLabel?: {
singular: string;
plural: string;
};
// Branding
colors: AppColors;
// Features
features: AppFeatures;
// Display
display: ContentDisplayConfig;
// Custom fields (app-specific)
custom?: Record<string, any>;
}
/**
* Navigation configuration
*/
export interface NavigationConfig {
tabs: {
id: string;
label: string;
icon: string;
route: string;
enabled: boolean;
}[];
showTabBar?: boolean;
tabBarStyle?: 'ios' | 'android' | 'custom';
}
/**
* Complete app configuration including navigation
*/
export interface FullAppConfig<TContent extends ContentItem = ContentItem> extends AppConfig<TContent> {
navigation: NavigationConfig;
}

View file

@ -0,0 +1,207 @@
/**
* Shared Type Definitions for Content Apps
*
* This file contains generalized types that can be used across
* multiple content apps (quotes, proverbs, poems, etc.)
*/
// ============================================================================
// BASE TYPES - Generic for all content types
// ============================================================================
/**
* Generic Author/Creator type
* Can represent quote authors, poets, speakers, etc.
*/
export interface ContentAuthor {
id: string;
name: string;
profession?: string[];
biography?: {
short: string;
long?: string;
sections?: { [key: string]: any };
keyAchievements?: string[];
famousQuote?: string;
};
lifespan?: {
birth: string;
death?: string;
};
verified?: boolean;
featured?: boolean;
imageUrl?: string;
image?: {
thumbnail?: string;
full?: string;
credit?: string;
source?: string;
};
}
/**
* Generic Content Item type with optional metadata
* Base type for all content (quotes, proverbs, poems, etc.)
*/
export interface ContentItem<TMetadata = Record<string, any>> {
id: string;
text: string;
authorId: string;
author?: ContentAuthor;
categories?: string[];
tags?: string[];
featured?: boolean;
likes?: number;
isFavorite?: boolean;
language?: string;
category?: string;
metadata?: TMetadata;
}
/**
* Generic Category type
*/
export interface ContentCategory {
id: string;
name: string;
description?: string;
}
/**
* Generic Tag type
*/
export interface ContentTag {
id: string;
name: string;
}
// ============================================================================
// SPECIFIC TYPES - Content-type specific implementations
// ============================================================================
/**
* Quote-specific metadata
*/
export interface QuoteMetadata {
source?: string;
year?: number;
context?: string;
verified?: boolean;
}
/**
* Quote type - extends ContentItem with quote-specific metadata
*/
export interface Quote extends ContentItem<QuoteMetadata> {
source?: string;
year?: number;
}
/**
* Proverb-specific metadata
*/
export interface ProverbMetadata {
origin?: string; // e.g., "Deutsches Sprichwort", "Chinese Proverb"
meaning?: string; // Explanation of the proverb
variants?: string[]; // Different versions of the proverb
relatedProverbs?: string[]; // IDs of related proverbs
}
/**
* Proverb type - extends ContentItem with proverb-specific metadata
*/
export interface Proverb extends ContentItem<ProverbMetadata> {
origin?: string;
meaning?: string;
variants?: string[];
}
/**
* Poem-specific metadata
*/
export interface PoemMetadata {
verses?: string[]; // Individual stanzas/verses
rhymeScheme?: string; // e.g., "ABAB", "AABB"
form?: string; // e.g., "Sonnet", "Haiku"
firstPublished?: number;
collection?: string; // Name of the poetry collection
}
/**
* Poem type - extends ContentItem with poem-specific metadata
*/
export interface Poem extends ContentItem<PoemMetadata> {
verses?: string[];
rhymeScheme?: string;
form?: string;
}
/**
* Speech-specific metadata
*/
export interface SpeechMetadata {
date?: string; // Date of the speech
location?: string; // Where it was given
occasion?: string; // Context/occasion
audience?: string; // Who it was for
duration?: number; // Length in minutes
videoUrl?: string; // Link to video if available
}
/**
* Historical Speech type
*/
export interface Speech extends ContentItem<SpeechMetadata> {
date?: string;
location?: string;
occasion?: string;
}
/**
* Fable-specific metadata
*/
export interface FableMetadata {
moral?: string; // The moral/lesson of the fable
characters?: string[]; // Main characters (e.g., "Rabe", "Fuchs")
setting?: string; // Where the fable takes place
length?: 'short' | 'medium' | 'long'; // Length classification
collection?: string; // Collection name if part of one
}
/**
* Fable type - extends ContentItem with fable-specific metadata
*/
export interface Fable extends ContentItem<FableMetadata> {
moral?: string;
characters?: string[];
setting?: string;
}
// ============================================================================
// BACKWARD COMPATIBILITY - Aliases for existing code
// ============================================================================
/**
* @deprecated Use ContentAuthor instead
* Kept for backward compatibility with existing quotes app
*/
export type Author = ContentAuthor;
/**
* @deprecated Use Quote instead
* Kept for backward compatibility with existing quotes app
*/
export type EnhancedQuote = Quote;
/**
* @deprecated Use ContentCategory instead
*/
export type QuoteCategory = ContentCategory;
/**
* @deprecated Use ContentTag instead
*/
export type QuoteTag = ContentTag;
// Export config types
export * from './config';

View file

@ -0,0 +1,180 @@
/**
* Shared Utility Functions
*
* Generic utilities that work with any content type (quotes, proverbs, poems, etc.)
*/
import type { ContentItem, ContentAuthor } from '../types';
// ============================================================================
// AUTHOR/CREATOR UTILITIES
// ============================================================================
/**
* Format author/creator name
*/
export function formatAuthorName(author: ContentAuthor): string {
return author.name;
}
/**
* Get author/creator lifespan string
*/
export function getAuthorLifespan(author: ContentAuthor): string {
if (!author.lifespan) return '';
const { birth, death } = author.lifespan;
return death ? `${birth} - ${death}` : `${birth} - Present`;
}
// ============================================================================
// CONTENT FILTERING - Generic functions that work with any ContentItem
// ============================================================================
/**
* Filter content items by category
*/
export function filterContentByCategory<T extends ContentItem>(
items: T[],
category: string
): T[] {
return items.filter(item =>
item.category === category ||
item.categories?.includes(category)
);
}
/**
* Filter content items by tag
*/
export function filterContentByTag<T extends ContentItem>(
items: T[],
tag: string
): T[] {
return items.filter(item => item.tags?.includes(tag));
}
/**
* Filter content items by author
*/
export function filterContentByAuthor<T extends ContentItem>(
items: T[],
authorId: string
): T[] {
return items.filter(item => item.authorId === authorId);
}
/**
* Get random content item from array
*/
export function getRandomContent<T extends ContentItem>(
items: T[]
): T | undefined {
if (items.length === 0) return undefined;
return items[Math.floor(Math.random() * items.length)];
}
/**
* Search content items by text
*/
export function searchContent<T extends ContentItem>(
items: T[],
searchTerm: string
): T[] {
const term = searchTerm.toLowerCase();
return items.filter(item =>
item.text.toLowerCase().includes(term) ||
item.tags?.some(tag => tag.toLowerCase().includes(term))
);
}
/**
* Get unique categories from content items
*/
export function getUniqueCategories<T extends ContentItem>(
items: T[]
): string[] {
const categories = new Set<string>();
items.forEach(item => {
if (item.category) categories.add(item.category);
item.categories?.forEach(cat => categories.add(cat));
});
return Array.from(categories).sort();
}
/**
* Get unique tags from content items
*/
export function getUniqueTags<T extends ContentItem>(
items: T[]
): string[] {
const tags = new Set<string>();
items.forEach(item => {
item.tags?.forEach(tag => tags.add(tag));
});
return Array.from(tags).sort();
}
/**
* Group content items by category
*/
export function groupContentByCategory<T extends ContentItem>(
items: T[]
): Record<string, T[]> {
const grouped: Record<string, T[]> = {};
items.forEach(item => {
const categories = item.categories || (item.category ? [item.category] : ['uncategorized']);
categories.forEach(category => {
if (!grouped[category]) {
grouped[category] = [];
}
grouped[category].push(item);
});
});
return grouped;
}
/**
* Get featured content items
*/
export function getFeaturedContent<T extends ContentItem>(
items: T[]
): T[] {
return items.filter(item => item.featured === true);
}
/**
* Get favorite content items
*/
export function getFavoriteContent<T extends ContentItem>(
items: T[]
): T[] {
return items.filter(item => item.isFavorite === true);
}
// ============================================================================
// BACKWARD COMPATIBILITY - Quote-specific aliases
// ============================================================================
import type { Quote } from '../types';
/**
* @deprecated Use filterContentByCategory instead
*/
export const filterQuotesByCategory = filterContentByCategory<Quote>;
/**
* @deprecated Use filterContentByTag instead
*/
export const filterQuotesByTag = filterContentByTag<Quote>;
/**
* @deprecated Use getRandomContent instead
*/
export const getRandomQuote = getRandomContent<Quote>;
/**
* @deprecated Use searchContent instead
*/
export const searchQuotes = searchContent<Quote>;