mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 01:21:24 +02:00
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:
parent
3a8d6bcf94
commit
ea3285dcbb
285 changed files with 645599 additions and 8 deletions
5
apps/quote/packages/shared/src/configs/index.ts
Normal file
5
apps/quote/packages/shared/src/configs/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/**
|
||||
* App Configurations
|
||||
*/
|
||||
|
||||
export { quotesAppConfig } from './quotes.config';
|
||||
100
apps/quote/packages/shared/src/configs/quotes.config.ts
Normal file
100
apps/quote/packages/shared/src/configs/quotes.config.ts
Normal 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',
|
||||
},
|
||||
};
|
||||
4792
apps/quote/packages/shared/src/data/authors/de.ts
Normal file
4792
apps/quote/packages/shared/src/data/authors/de.ts
Normal file
File diff suppressed because one or more lines are too long
4819
apps/quote/packages/shared/src/data/authors/en.ts
Normal file
4819
apps/quote/packages/shared/src/data/authors/en.ts
Normal file
File diff suppressed because one or more lines are too long
14
apps/quote/packages/shared/src/data/index.ts
Normal file
14
apps/quote/packages/shared/src/data/index.ts
Normal 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';
|
||||
35020
apps/quote/packages/shared/src/data/quotes/de.ts
Normal file
35020
apps/quote/packages/shared/src/data/quotes/de.ts
Normal file
File diff suppressed because it is too large
Load diff
35083
apps/quote/packages/shared/src/data/quotes/en.ts
Normal file
35083
apps/quote/packages/shared/src/data/quotes/en.ts
Normal file
File diff suppressed because it is too large
Load diff
18
apps/quote/packages/shared/src/index.ts
Normal file
18
apps/quote/packages/shared/src/index.ts
Normal 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';
|
||||
124
apps/quote/packages/shared/src/types/config.ts
Normal file
124
apps/quote/packages/shared/src/types/config.ts
Normal 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;
|
||||
}
|
||||
207
apps/quote/packages/shared/src/types/index.ts
Normal file
207
apps/quote/packages/shared/src/types/index.ts
Normal 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';
|
||||
180
apps/quote/packages/shared/src/utils/index.ts
Normal file
180
apps/quote/packages/shared/src/utils/index.ts
Normal 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>;
|
||||
Loading…
Add table
Add a link
Reference in a new issue