mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 14:26:42 +02:00
style: auto-format codebase with Prettier
Applied formatting to 1487+ files using pnpm format:write - TypeScript/JavaScript files - Svelte components - Astro pages - JSON configs - Markdown docs 13 files still need manual review (Astro JSX comments)
This commit is contained in:
parent
0241f5554c
commit
d36b321d9d
3952 changed files with 661498 additions and 739751 deletions
|
|
@ -1,21 +1,21 @@
|
|||
{
|
||||
"name": "@quote/content",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./data": "./src/data/index.ts",
|
||||
"./config": "./src/config/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quote/shared": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.9.2"
|
||||
}
|
||||
"name": "@quote/content",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./data": "./src/data/index.ts",
|
||||
"./config": "./src/config/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quote/shared": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.9.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
{
|
||||
"name": "@quote/shared",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./types": "./src/types/index.ts",
|
||||
"./data": "./src/data/index.ts",
|
||||
"./utils": "./src/utils/index.ts",
|
||||
"./services": "./src/services/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.9.2"
|
||||
},
|
||||
"dependencies": {}
|
||||
"name": "@quote/shared",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./types": "./src/types/index.ts",
|
||||
"./data": "./src/data/index.ts",
|
||||
"./utils": "./src/utils/index.ts",
|
||||
"./services": "./src/services/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.9.2"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,96 +5,96 @@
|
|||
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'],
|
||||
},
|
||||
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',
|
||||
},
|
||||
contentType: 'quote',
|
||||
contentLabel: {
|
||||
singular: 'Zitat',
|
||||
plural: 'Zitate',
|
||||
},
|
||||
authorLabel: {
|
||||
singular: 'Autor',
|
||||
plural: 'Autoren',
|
||||
},
|
||||
|
||||
colors: {
|
||||
primary: '#667eea',
|
||||
secondary: '#764ba2',
|
||||
accent: '#f093fb',
|
||||
},
|
||||
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,
|
||||
},
|
||||
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',
|
||||
},
|
||||
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',
|
||||
},
|
||||
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
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -15,110 +15,111 @@ export type ContentType = 'quote' | 'proverb' | 'poem' | 'speech' | 'fable' | 't
|
|||
* Color configuration for the app
|
||||
*/
|
||||
export interface AppColors {
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
accent?: string;
|
||||
background?: string;
|
||||
surface?: string;
|
||||
text?: string;
|
||||
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;
|
||||
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';
|
||||
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[];
|
||||
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;
|
||||
// Basic Info
|
||||
metadata: AppMetadata;
|
||||
|
||||
// Content Type
|
||||
contentType: ContentType;
|
||||
contentLabel: {
|
||||
singular: string;
|
||||
plural: string;
|
||||
};
|
||||
authorLabel?: {
|
||||
singular: string;
|
||||
plural: string;
|
||||
};
|
||||
// Content Type
|
||||
contentType: ContentType;
|
||||
contentLabel: {
|
||||
singular: string;
|
||||
plural: string;
|
||||
};
|
||||
authorLabel?: {
|
||||
singular: string;
|
||||
plural: string;
|
||||
};
|
||||
|
||||
// Branding
|
||||
colors: AppColors;
|
||||
// Branding
|
||||
colors: AppColors;
|
||||
|
||||
// Features
|
||||
features: AppFeatures;
|
||||
// Features
|
||||
features: AppFeatures;
|
||||
|
||||
// Display
|
||||
display: ContentDisplayConfig;
|
||||
// Display
|
||||
display: ContentDisplayConfig;
|
||||
|
||||
// Custom fields (app-specific)
|
||||
custom?: Record<string, any>;
|
||||
// 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';
|
||||
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;
|
||||
export interface FullAppConfig<TContent extends ContentItem = ContentItem>
|
||||
extends AppConfig<TContent> {
|
||||
navigation: NavigationConfig;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,29 +14,29 @@
|
|||
* 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;
|
||||
};
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -44,35 +44,35 @@ export interface ContentAuthor {
|
|||
* 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;
|
||||
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;
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic Tag type
|
||||
*/
|
||||
export interface ContentTag {
|
||||
id: string;
|
||||
name: string;
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -83,98 +83,98 @@ export interface ContentTag {
|
|||
* Quote-specific metadata
|
||||
*/
|
||||
export interface QuoteMetadata {
|
||||
source?: string;
|
||||
year?: number;
|
||||
context?: string;
|
||||
verified?: boolean;
|
||||
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;
|
||||
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
|
||||
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[];
|
||||
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
|
||||
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;
|
||||
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
|
||||
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;
|
||||
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
|
||||
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;
|
||||
moral?: string;
|
||||
characters?: string[];
|
||||
setting?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -14,16 +14,16 @@ import type { ContentItem, ContentAuthor } from '../types';
|
|||
* Format author/creator name
|
||||
*/
|
||||
export function formatAuthorName(author: ContentAuthor): string {
|
||||
return author.name;
|
||||
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`;
|
||||
if (!author.lifespan) return '';
|
||||
const { birth, death } = author.lifespan;
|
||||
return death ? `${birth} - ${death}` : `${birth} - Present`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -33,124 +33,98 @@ export function getAuthorLifespan(author: ContentAuthor): string {
|
|||
/**
|
||||
* 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)
|
||||
);
|
||||
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));
|
||||
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);
|
||||
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)];
|
||||
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))
|
||||
);
|
||||
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();
|
||||
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();
|
||||
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[]> = {};
|
||||
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);
|
||||
});
|
||||
});
|
||||
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;
|
||||
return grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get featured content items
|
||||
*/
|
||||
export function getFeaturedContent<T extends ContentItem>(
|
||||
items: T[]
|
||||
): T[] {
|
||||
return items.filter(item => item.featured === true);
|
||||
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);
|
||||
export function getFavoriteContent<T extends ContentItem>(items: T[]): T[] {
|
||||
return items.filter((item) => item.isFavorite === true);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022"],
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022"],
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
{
|
||||
"name": "@quote/web-ui",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./styles": "./src/styles/index.ts",
|
||||
"./components/*": "./src/components/*.svelte",
|
||||
"./stores/*": "./src/stores/*.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quote/shared": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
"name": "@quote/web-ui",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./styles": "./src/styles/index.ts",
|
||||
"./components/*": "./src/components/*.svelte",
|
||||
"./stores/*": "./src/stores/*.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quote/shared": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,503 +1,518 @@
|
|||
<script lang="ts" generics="T extends ContentItem">
|
||||
import type { ContentItem, AppConfig } from '@quote/shared';
|
||||
import ContentCard from './ContentCard.svelte';
|
||||
import { toast } from '../stores/toast';
|
||||
import type { ContentItem, AppConfig } from '@quote/shared';
|
||||
import ContentCard from './ContentCard.svelte';
|
||||
import { toast } from '../stores/toast';
|
||||
|
||||
interface Props {
|
||||
config: AppConfig;
|
||||
content: T[];
|
||||
allAuthors?: any[];
|
||||
favoriteStorageKey: string;
|
||||
showAuthor?: boolean;
|
||||
pageTitle: string;
|
||||
}
|
||||
interface Props {
|
||||
config: AppConfig;
|
||||
content: T[];
|
||||
allAuthors?: any[];
|
||||
favoriteStorageKey: string;
|
||||
showAuthor?: boolean;
|
||||
pageTitle: string;
|
||||
}
|
||||
|
||||
let {
|
||||
config,
|
||||
content,
|
||||
allAuthors = [],
|
||||
favoriteStorageKey,
|
||||
showAuthor = true,
|
||||
pageTitle
|
||||
}: Props = $props();
|
||||
let {
|
||||
config,
|
||||
content,
|
||||
allAuthors = [],
|
||||
favoriteStorageKey,
|
||||
showAuthor = true,
|
||||
pageTitle,
|
||||
}: Props = $props();
|
||||
|
||||
let searchTerm = $state('');
|
||||
let selectedCategory = $state('all');
|
||||
let favorites = $state<Set<string>>(new Set());
|
||||
let isSearchOpen = $state(false);
|
||||
let searchTerm = $state('');
|
||||
let selectedCategory = $state('all');
|
||||
let favorites = $state<Set<string>>(new Set());
|
||||
let isSearchOpen = $state(false);
|
||||
|
||||
// Pagination state
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
let currentPage = $state(1);
|
||||
let isLoadingMore = $state(false);
|
||||
// Pagination state
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
let currentPage = $state(1);
|
||||
let isLoadingMore = $state(false);
|
||||
|
||||
// Load favorites from localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedFavorites = localStorage.getItem(favoriteStorageKey);
|
||||
if (savedFavorites) {
|
||||
favorites = new Set(JSON.parse(savedFavorites));
|
||||
}
|
||||
}
|
||||
// Load favorites from localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedFavorites = localStorage.getItem(favoriteStorageKey);
|
||||
if (savedFavorites) {
|
||||
favorites = new Set(JSON.parse(savedFavorites));
|
||||
}
|
||||
}
|
||||
|
||||
// Get content with author info
|
||||
const contentWithAuthors = content.map(item => ({
|
||||
...item,
|
||||
author: allAuthors.find(a => a.id === item.authorId)
|
||||
}));
|
||||
// Get content with author info
|
||||
const contentWithAuthors = content.map((item) => ({
|
||||
...item,
|
||||
author: allAuthors.find((a) => a.id === item.authorId),
|
||||
}));
|
||||
|
||||
// Get unique categories
|
||||
const categories = ['all', ...new Set(content.flatMap(item => item.categories || []).filter(Boolean))];
|
||||
// Get unique categories
|
||||
const categories = [
|
||||
'all',
|
||||
...new Set(content.flatMap((item) => item.categories || []).filter(Boolean)),
|
||||
];
|
||||
|
||||
// Filter content (all matching items)
|
||||
let allFilteredContent = $derived(
|
||||
contentWithAuthors
|
||||
.map(item => ({
|
||||
...item,
|
||||
isFavorite: favorites.has(item.id)
|
||||
}))
|
||||
.filter(item => {
|
||||
const matchesSearch = item.text.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.author?.name?.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesCategory = selectedCategory === 'all' ||
|
||||
item.categories?.includes(selectedCategory);
|
||||
return matchesSearch && matchesCategory;
|
||||
})
|
||||
);
|
||||
// Filter content (all matching items)
|
||||
let allFilteredContent = $derived(
|
||||
contentWithAuthors
|
||||
.map((item) => ({
|
||||
...item,
|
||||
isFavorite: favorites.has(item.id),
|
||||
}))
|
||||
.filter((item) => {
|
||||
const matchesSearch =
|
||||
item.text.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.author?.name?.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesCategory =
|
||||
selectedCategory === 'all' || item.categories?.includes(selectedCategory);
|
||||
return matchesSearch && matchesCategory;
|
||||
})
|
||||
);
|
||||
|
||||
// Paginated content (only show what should be visible)
|
||||
let filteredContent = $derived(
|
||||
allFilteredContent.slice(0, currentPage * ITEMS_PER_PAGE)
|
||||
);
|
||||
// Paginated content (only show what should be visible)
|
||||
let filteredContent = $derived(allFilteredContent.slice(0, currentPage * ITEMS_PER_PAGE));
|
||||
|
||||
// Check if there are more items to load
|
||||
let hasMore = $derived(filteredContent.length < allFilteredContent.length);
|
||||
// Check if there are more items to load
|
||||
let hasMore = $derived(filteredContent.length < allFilteredContent.length);
|
||||
|
||||
function toggleSearch() {
|
||||
isSearchOpen = !isSearchOpen;
|
||||
if (!isSearchOpen) {
|
||||
searchTerm = '';
|
||||
selectedCategory = 'all';
|
||||
currentPage = 1;
|
||||
}
|
||||
}
|
||||
function toggleSearch() {
|
||||
isSearchOpen = !isSearchOpen;
|
||||
if (!isSearchOpen) {
|
||||
searchTerm = '';
|
||||
selectedCategory = 'all';
|
||||
currentPage = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
isLoadingMore = true;
|
||||
setTimeout(() => {
|
||||
currentPage++;
|
||||
isLoadingMore = false;
|
||||
}, 300);
|
||||
}
|
||||
function loadMore() {
|
||||
isLoadingMore = true;
|
||||
setTimeout(() => {
|
||||
currentPage++;
|
||||
isLoadingMore = false;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Reset page when search/filter changes
|
||||
$effect(() => {
|
||||
searchTerm;
|
||||
selectedCategory;
|
||||
currentPage = 1;
|
||||
});
|
||||
// Reset page when search/filter changes
|
||||
$effect(() => {
|
||||
searchTerm;
|
||||
selectedCategory;
|
||||
currentPage = 1;
|
||||
});
|
||||
|
||||
function handleToggleFavorite(event: CustomEvent) {
|
||||
const { contentId } = event.detail;
|
||||
const wasAdded = !favorites.has(contentId);
|
||||
function handleToggleFavorite(event: CustomEvent) {
|
||||
const { contentId } = event.detail;
|
||||
const wasAdded = !favorites.has(contentId);
|
||||
|
||||
if (favorites.has(contentId)) {
|
||||
favorites.delete(contentId);
|
||||
} else {
|
||||
favorites.add(contentId);
|
||||
}
|
||||
favorites = new Set(favorites);
|
||||
if (favorites.has(contentId)) {
|
||||
favorites.delete(contentId);
|
||||
} else {
|
||||
favorites.add(contentId);
|
||||
}
|
||||
favorites = new Set(favorites);
|
||||
|
||||
// Save to localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(favoriteStorageKey, JSON.stringify([...favorites]));
|
||||
}
|
||||
// Save to localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(favoriteStorageKey, JSON.stringify([...favorites]));
|
||||
}
|
||||
|
||||
// Show toast
|
||||
if (wasAdded) {
|
||||
toast.success('Zu Favoriten hinzugefügt');
|
||||
} else {
|
||||
toast.info('Von Favoriten entfernt');
|
||||
}
|
||||
}
|
||||
// Show toast
|
||||
if (wasAdded) {
|
||||
toast.success('Zu Favoriten hinzugefügt');
|
||||
} else {
|
||||
toast.info('Von Favoriten entfernt');
|
||||
}
|
||||
}
|
||||
|
||||
function handleAuthorClick(event: CustomEvent) {
|
||||
const { authorId } = event.detail;
|
||||
if (authorId) {
|
||||
window.location.href = `/authors/${authorId}`;
|
||||
}
|
||||
}
|
||||
function handleAuthorClick(event: CustomEvent) {
|
||||
const { authorId } = event.detail;
|
||||
if (authorId) {
|
||||
window.location.href = `/authors/${authorId}`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{pageTitle} - {config.metadata.displayName}</title>
|
||||
<title>{pageTitle} - {config.metadata.displayName}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="browse-page">
|
||||
<div class="header-container">
|
||||
<div class="header-row">
|
||||
<h2>{pageTitle}</h2>
|
||||
<div class="header-container">
|
||||
<div class="header-row">
|
||||
<h2>{pageTitle}</h2>
|
||||
|
||||
<button
|
||||
class="search-fab"
|
||||
onclick={toggleSearch}
|
||||
aria-label="Toggle search"
|
||||
>
|
||||
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
{#if isSearchOpen}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
{:else}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button class="search-fab" onclick={toggleSearch} aria-label="Toggle search">
|
||||
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
{#if isSearchOpen}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
{:else}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if isSearchOpen}
|
||||
<div class="search-bar">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Durchsuchen..."
|
||||
bind:value={searchTerm}
|
||||
class="search"
|
||||
/>
|
||||
{#if isSearchOpen}
|
||||
<div class="search-bar">
|
||||
<input type="text" placeholder="Durchsuchen..." bind:value={searchTerm} class="search" />
|
||||
|
||||
<select bind:value={selectedCategory} class="category-filter">
|
||||
{#each categories as category}
|
||||
<option value={category}>
|
||||
{category === 'all' ? 'Alle Kategorien' : category}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<select bind:value={selectedCategory} class="category-filter">
|
||||
{#each categories as category}
|
||||
<option value={category}>
|
||||
{category === 'all' ? 'Alle Kategorien' : category}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if allFilteredContent.length === 0 && (searchTerm || selectedCategory !== 'all')}
|
||||
<!-- Empty Search Results -->
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.35-4.35"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Keine Ergebnisse gefunden</h3>
|
||||
<p>Versuche es mit anderen Suchbegriffen oder Kategorien</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="content-grid">
|
||||
{#each filteredContent as item (item.id)}
|
||||
<ContentCard
|
||||
content={item}
|
||||
on:toggleFavorite={handleToggleFavorite}
|
||||
on:authorClick={handleAuthorClick}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{#if allFilteredContent.length === 0 && (searchTerm || selectedCategory !== 'all')}
|
||||
<!-- Empty Search Results -->
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.35-4.35"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Keine Ergebnisse gefunden</h3>
|
||||
<p>Versuche es mit anderen Suchbegriffen oder Kategorien</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="content-grid">
|
||||
{#each filteredContent as item (item.id)}
|
||||
<ContentCard
|
||||
content={item}
|
||||
on:toggleFavorite={handleToggleFavorite}
|
||||
on:authorClick={handleAuthorClick}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Load More Button -->
|
||||
{#if hasMore}
|
||||
<div class="load-more-container">
|
||||
<button
|
||||
class="load-more-btn"
|
||||
onclick={loadMore}
|
||||
disabled={isLoadingMore}
|
||||
>
|
||||
{#if isLoadingMore}
|
||||
<svg class="spinner" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<circle cx="12" cy="12" r="10" stroke-width="3" stroke-opacity="0.25"></circle>
|
||||
<path d="M12 2a10 10 0 0 1 10 10" stroke-width="3" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
Laden...
|
||||
{:else}
|
||||
Mehr laden ({allFilteredContent.length - filteredContent.length} weitere)
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
<!-- Load More Button -->
|
||||
{#if hasMore}
|
||||
<div class="load-more-container">
|
||||
<button class="load-more-btn" onclick={loadMore} disabled={isLoadingMore}>
|
||||
{#if isLoadingMore}
|
||||
<svg
|
||||
class="spinner"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" stroke-width="3" stroke-opacity="0.25"></circle>
|
||||
<path d="M12 2a10 10 0 0 1 10 10" stroke-width="3" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
Laden...
|
||||
{:else}
|
||||
Mehr laden ({allFilteredContent.length - filteredContent.length} weitere)
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if isSearchOpen}
|
||||
<div class="floating-results">
|
||||
{allFilteredContent.length} von {content.length} {config.contentLabel.plural}
|
||||
{#if filteredContent.length < allFilteredContent.length}
|
||||
• {filteredContent.length} angezeigt
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if isSearchOpen}
|
||||
<div class="floating-results">
|
||||
{allFilteredContent.length} von {content.length}
|
||||
{config.contentLabel.plural}
|
||||
{#if filteredContent.length < allFilteredContent.length}
|
||||
• {filteredContent.length} angezeigt
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.browse-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
padding-bottom: var(--spacing-2xl);
|
||||
}
|
||||
.browse-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
padding-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.header-container {
|
||||
max-width: 700px;
|
||||
margin: 0 auto var(--spacing-xl);
|
||||
}
|
||||
.header-container {
|
||||
max-width: 700px;
|
||||
margin: 0 auto var(--spacing-xl);
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
.header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
margin: 0;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
margin: 0;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.search-fab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 9999px;
|
||||
background: rgb(var(--color-primary));
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.search-fab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 9999px;
|
||||
background: rgb(var(--color-primary));
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.search-fab:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.search-fab:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.search-fab:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.search-fab:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
margin-top: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
background: rgb(var(--color-surface));
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
margin-top: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
background: rgb(var(--color-surface));
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
flex: 1;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: 2px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 1rem;
|
||||
background: rgb(var(--color-background));
|
||||
color: rgb(var(--color-text-primary));
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
.search {
|
||||
flex: 1;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: 2px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 1rem;
|
||||
background: rgb(var(--color-background));
|
||||
color: rgb(var(--color-text-primary));
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.search:focus {
|
||||
outline: none;
|
||||
border-color: rgb(var(--color-primary));
|
||||
}
|
||||
.search:focus {
|
||||
outline: none;
|
||||
border-color: rgb(var(--color-primary));
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: 2px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 1rem;
|
||||
background: rgb(var(--color-background));
|
||||
color: rgb(var(--color-text-primary));
|
||||
transition: border-color var(--transition-fast);
|
||||
min-width: 180px;
|
||||
}
|
||||
.category-filter {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: 2px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 1rem;
|
||||
background: rgb(var(--color-background));
|
||||
color: rgb(var(--color-text-primary));
|
||||
transition: border-color var(--transition-fast);
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.category-filter:focus {
|
||||
outline: none;
|
||||
border-color: rgb(var(--color-primary));
|
||||
}
|
||||
.category-filter:focus {
|
||||
outline: none;
|
||||
border-color: rgb(var(--color-primary));
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xl);
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.content-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xl);
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.floating-results {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
background: rgba(var(--color-surface), 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: var(--radius-full);
|
||||
border: 1px solid rgba(var(--color-border), 0.5);
|
||||
box-shadow: var(--shadow-lg);
|
||||
color: rgb(var(--color-text-secondary));
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
z-index: 20;
|
||||
animation: fadeInUp 0.3s ease;
|
||||
}
|
||||
.floating-results {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
background: rgba(var(--color-surface), 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: var(--radius-full);
|
||||
border: 1px solid rgba(var(--color-border), 0.5);
|
||||
box-shadow: var(--shadow-lg);
|
||||
color: rgb(var(--color-text-secondary));
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
z-index: 20;
|
||||
animation: fadeInUp 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, 10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
}
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, 10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
max-width: 500px;
|
||||
margin: var(--spacing-2xl) auto;
|
||||
text-align: center;
|
||||
padding: var(--spacing-2xl);
|
||||
}
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
max-width: 500px;
|
||||
margin: var(--spacing-2xl) auto;
|
||||
text-align: center;
|
||||
padding: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
margin: 0 auto var(--spacing-lg);
|
||||
color: rgb(var(--color-text-tertiary));
|
||||
opacity: 0.5;
|
||||
}
|
||||
.empty-icon {
|
||||
margin: 0 auto var(--spacing-lg);
|
||||
color: rgb(var(--color-text-tertiary));
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
font-size: 1.5rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
margin: 0 0 var(--spacing-sm) 0;
|
||||
}
|
||||
.empty-state h3 {
|
||||
font-size: 1.5rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
margin: 0 0 var(--spacing-sm) 0;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 1rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
margin: 0;
|
||||
}
|
||||
.empty-state p {
|
||||
font-size: 1rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Load More Button */
|
||||
.load-more-container {
|
||||
max-width: 700px;
|
||||
margin: var(--spacing-xl) auto 0;
|
||||
text-align: center;
|
||||
}
|
||||
/* Load More Button */
|
||||
.load-more-container {
|
||||
max-width: 700px;
|
||||
margin: var(--spacing-xl) auto 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.load-more-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-md) var(--spacing-2xl);
|
||||
background: rgb(var(--color-surface));
|
||||
color: rgb(var(--color-text-primary));
|
||||
border: 2px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
.load-more-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-md) var(--spacing-2xl);
|
||||
background: rgb(var(--color-surface));
|
||||
color: rgb(var(--color-text-primary));
|
||||
border: 2px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.load-more-btn:hover:not(:disabled) {
|
||||
background: rgb(var(--color-primary));
|
||||
color: white;
|
||||
border-color: rgb(var(--color-primary));
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.load-more-btn:hover:not(:disabled) {
|
||||
background: rgb(var(--color-primary));
|
||||
color: white;
|
||||
border-color: rgb(var(--color-primary));
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.load-more-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.load-more-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
.spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.browse-page {
|
||||
padding-bottom: var(--spacing-xl);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.browse-page {
|
||||
padding-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.header-container {
|
||||
max-width: 100%;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
.header-container {
|
||||
max-width: 100%;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.header-row {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
.header-row {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.search-fab {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
.search-fab {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm);
|
||||
}
|
||||
.search-bar {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
min-width: auto;
|
||||
width: 100%;
|
||||
}
|
||||
.category-filter {
|
||||
min-width: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
gap: var(--spacing-lg);
|
||||
max-width: 100%;
|
||||
}
|
||||
.content-grid {
|
||||
gap: var(--spacing-lg);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.floating-results {
|
||||
bottom: 5rem;
|
||||
font-size: 0.8125rem;
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
}
|
||||
.floating-results {
|
||||
bottom: 5rem;
|
||||
font-size: 0.8125rem;
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
.empty-state {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.empty-state h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
}
|
||||
.empty-state p {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,73 +1,77 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
categories: string[];
|
||||
selectedCategory: string | null;
|
||||
onSelect: (category: string | null) => void;
|
||||
primaryColor?: string;
|
||||
}
|
||||
interface Props {
|
||||
categories: string[];
|
||||
selectedCategory: string | null;
|
||||
onSelect: (category: string | null) => void;
|
||||
primaryColor?: string;
|
||||
}
|
||||
|
||||
let { categories, selectedCategory = $bindable(null), onSelect, primaryColor }: Props = $props();
|
||||
let { categories, selectedCategory = $bindable(null), onSelect, primaryColor }: Props = $props();
|
||||
|
||||
function handleSelect(category: string | null) {
|
||||
selectedCategory = category;
|
||||
onSelect(category);
|
||||
}
|
||||
function handleSelect(category: string | null) {
|
||||
selectedCategory = category;
|
||||
onSelect(category);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="category-filters">
|
||||
<button
|
||||
class="category-btn"
|
||||
class:active={!selectedCategory}
|
||||
onclick={() => handleSelect(null)}
|
||||
style={!selectedCategory && primaryColor ? `background: rgb(${primaryColor}); border-color: rgb(${primaryColor}); color: white;` : ''}
|
||||
>
|
||||
Alle
|
||||
</button>
|
||||
{#each categories as category}
|
||||
<button
|
||||
class="category-btn"
|
||||
class:active={selectedCategory === category}
|
||||
onclick={() => handleSelect(category)}
|
||||
style={selectedCategory === category && primaryColor ? `background: rgb(${primaryColor}); border-color: rgb(${primaryColor}); color: white;` : ''}
|
||||
>
|
||||
{category}
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
class="category-btn"
|
||||
class:active={!selectedCategory}
|
||||
onclick={() => handleSelect(null)}
|
||||
style={!selectedCategory && primaryColor
|
||||
? `background: rgb(${primaryColor}); border-color: rgb(${primaryColor}); color: white;`
|
||||
: ''}
|
||||
>
|
||||
Alle
|
||||
</button>
|
||||
{#each categories as category}
|
||||
<button
|
||||
class="category-btn"
|
||||
class:active={selectedCategory === category}
|
||||
onclick={() => handleSelect(category)}
|
||||
style={selectedCategory === category && primaryColor
|
||||
? `background: rgb(${primaryColor}); border-color: rgb(${primaryColor}); color: white;`
|
||||
: ''}
|
||||
>
|
||||
{category}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.category-filters {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
.category-filters {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
border: 2px solid rgb(var(--color-border));
|
||||
background: rgb(var(--color-background));
|
||||
color: rgb(var(--color-text-secondary));
|
||||
border-radius: var(--radius-full);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
.category-btn {
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
border: 2px solid rgb(var(--color-border));
|
||||
background: rgb(var(--color-background));
|
||||
color: rgb(var(--color-text-secondary));
|
||||
border-radius: var(--radius-full);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.category-btn:hover {
|
||||
border-color: rgb(var(--color-primary));
|
||||
color: rgb(var(--color-primary));
|
||||
}
|
||||
.category-btn:hover {
|
||||
border-color: rgb(var(--color-primary));
|
||||
color: rgb(var(--color-primary));
|
||||
}
|
||||
|
||||
.category-btn.active {
|
||||
background: rgb(var(--color-primary));
|
||||
border-color: rgb(var(--color-primary));
|
||||
color: white;
|
||||
}
|
||||
.category-btn.active {
|
||||
background: rgb(var(--color-primary));
|
||||
border-color: rgb(var(--color-primary));
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.category-filters {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.category-filters {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,398 +1,431 @@
|
|||
<script lang="ts" generics="T extends ContentItem">
|
||||
import type { ContentItem } from '@quote/shared';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { toast } from '../stores/toast';
|
||||
import type { ContentItem } from '@quote/shared';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { toast } from '../stores/toast';
|
||||
|
||||
interface Props {
|
||||
content: T & { author?: any; isFavorite?: boolean };
|
||||
variant?: 'simple' | 'daily';
|
||||
category?: string;
|
||||
showAuthor?: boolean;
|
||||
showSource?: boolean;
|
||||
gradientStyle?: string;
|
||||
}
|
||||
interface Props {
|
||||
content: T & { author?: any; isFavorite?: boolean };
|
||||
variant?: 'simple' | 'daily';
|
||||
category?: string;
|
||||
showAuthor?: boolean;
|
||||
showSource?: boolean;
|
||||
gradientStyle?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
content,
|
||||
variant = 'simple',
|
||||
category,
|
||||
showAuthor = true,
|
||||
showSource = true,
|
||||
gradientStyle
|
||||
}: Props = $props();
|
||||
let {
|
||||
content,
|
||||
variant = 'simple',
|
||||
category,
|
||||
showAuthor = true,
|
||||
showSource = true,
|
||||
gradientStyle,
|
||||
}: Props = $props();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Get gradient colors based on category
|
||||
function getCategoryGradient(cat?: string): string {
|
||||
const gradients: Record<string, string> = {
|
||||
'life': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
'wisdom': 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
||||
'success': 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
||||
'motivation': 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
|
||||
'love': 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
|
||||
'happiness': 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)',
|
||||
'philosophy': 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)',
|
||||
'courage': 'linear-gradient(135deg, #ff9a56 0%, #ff6a88 100%)',
|
||||
'creativity': 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)',
|
||||
'peace': 'linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%)',
|
||||
'knowledge': 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)',
|
||||
};
|
||||
// Get gradient colors based on category
|
||||
function getCategoryGradient(cat?: string): string {
|
||||
const gradients: Record<string, string> = {
|
||||
life: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
wisdom: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
||||
success: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
||||
motivation: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
|
||||
love: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
|
||||
happiness: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)',
|
||||
philosophy: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)',
|
||||
courage: 'linear-gradient(135deg, #ff9a56 0%, #ff6a88 100%)',
|
||||
creativity: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)',
|
||||
peace: 'linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%)',
|
||||
knowledge: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)',
|
||||
};
|
||||
|
||||
if (cat && gradients[cat.toLowerCase()]) {
|
||||
return gradients[cat.toLowerCase()];
|
||||
}
|
||||
if (cat && gradients[cat.toLowerCase()]) {
|
||||
return gradients[cat.toLowerCase()];
|
||||
}
|
||||
|
||||
// Default gradient
|
||||
return 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
|
||||
}
|
||||
// Default gradient
|
||||
return 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
|
||||
}
|
||||
|
||||
function handleCopy() {
|
||||
const authorName = content.author?.name || content.authorId || 'Unknown';
|
||||
const text = `"${content.text}" — ${authorName}`;
|
||||
navigator.clipboard.writeText(text);
|
||||
dispatch('copy', { content });
|
||||
showCopyFeedback();
|
||||
toast.success('Kopiert!');
|
||||
}
|
||||
function handleCopy() {
|
||||
const authorName = content.author?.name || content.authorId || 'Unknown';
|
||||
const text = `"${content.text}" — ${authorName}`;
|
||||
navigator.clipboard.writeText(text);
|
||||
dispatch('copy', { content });
|
||||
showCopyFeedback();
|
||||
toast.success('Kopiert!');
|
||||
}
|
||||
|
||||
function handleShare() {
|
||||
const authorName = content.author?.name || content.authorId || 'Unknown';
|
||||
const text = `"${content.text}" — ${authorName}`;
|
||||
function handleShare() {
|
||||
const authorName = content.author?.name || content.authorId || 'Unknown';
|
||||
const text = `"${content.text}" — ${authorName}`;
|
||||
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
title: 'Content',
|
||||
text: text,
|
||||
}).catch((error) => {
|
||||
if (error.name !== 'AbortError') {
|
||||
handleCopy();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
handleCopy();
|
||||
}
|
||||
if (navigator.share) {
|
||||
navigator
|
||||
.share({
|
||||
title: 'Content',
|
||||
text: text,
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.name !== 'AbortError') {
|
||||
handleCopy();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
handleCopy();
|
||||
}
|
||||
|
||||
dispatch('share', { content });
|
||||
}
|
||||
dispatch('share', { content });
|
||||
}
|
||||
|
||||
function handleFavorite() {
|
||||
dispatch('toggleFavorite', { contentId: content.id });
|
||||
}
|
||||
function handleFavorite() {
|
||||
dispatch('toggleFavorite', { contentId: content.id });
|
||||
}
|
||||
|
||||
function handleAuthorClick() {
|
||||
dispatch('authorClick', { authorId: content.author?.id || content.authorId });
|
||||
}
|
||||
function handleAuthorClick() {
|
||||
dispatch('authorClick', { authorId: content.author?.id || content.authorId });
|
||||
}
|
||||
|
||||
let showCopySuccess = $state(false);
|
||||
let showCopySuccess = $state(false);
|
||||
|
||||
function showCopyFeedback() {
|
||||
showCopySuccess = true;
|
||||
setTimeout(() => {
|
||||
showCopySuccess = false;
|
||||
}, 2000);
|
||||
}
|
||||
function showCopyFeedback() {
|
||||
showCopySuccess = true;
|
||||
setTimeout(() => {
|
||||
showCopySuccess = false;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
const finalGradient = gradientStyle || getCategoryGradient(category || content.categories?.[0]);
|
||||
const isDaily = variant === 'daily';
|
||||
const finalGradient = gradientStyle || getCategoryGradient(category || content.categories?.[0]);
|
||||
const isDaily = variant === 'daily';
|
||||
</script>
|
||||
|
||||
<article
|
||||
class="content-card"
|
||||
class:daily={isDaily}
|
||||
style="background: {finalGradient}"
|
||||
>
|
||||
<div class="card-inner">
|
||||
<!-- Content Text -->
|
||||
<blockquote class="content-text">
|
||||
<p>"{content.text}"</p>
|
||||
</blockquote>
|
||||
<article class="content-card" class:daily={isDaily} style="background: {finalGradient}">
|
||||
<div class="card-inner">
|
||||
<!-- Content Text -->
|
||||
<blockquote class="content-text">
|
||||
<p>"{content.text}"</p>
|
||||
</blockquote>
|
||||
|
||||
<!-- Source Info (for quotes) -->
|
||||
{#if !isDaily && showSource && 'source' in content && content.source}
|
||||
<p class="source-info">
|
||||
From: {content.source}
|
||||
{#if 'year' in content && content.year}
|
||||
({content.year})
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
<!-- Source Info (for quotes) -->
|
||||
{#if !isDaily && showSource && 'source' in content && content.source}
|
||||
<p class="source-info">
|
||||
From: {content.source}
|
||||
{#if 'year' in content && content.year}
|
||||
({content.year})
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<!-- Origin Info (for proverbs) -->
|
||||
{#if !isDaily && 'origin' in content && content.origin}
|
||||
<p class="source-info">
|
||||
{content.origin}
|
||||
</p>
|
||||
{/if}
|
||||
<!-- Origin Info (for proverbs) -->
|
||||
{#if !isDaily && 'origin' in content && content.origin}
|
||||
<p class="source-info">
|
||||
{content.origin}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<!-- Meaning (for proverbs) -->
|
||||
{#if 'meaning' in content && content.meaning}
|
||||
<div class="meaning-box">
|
||||
<strong>Bedeutung:</strong>
|
||||
<p>{content.meaning}</p>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Meaning (for proverbs) -->
|
||||
{#if 'meaning' in content && content.meaning}
|
||||
<div class="meaning-box">
|
||||
<strong>Bedeutung:</strong>
|
||||
<p>{content.meaning}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Author Section -->
|
||||
{#if showAuthor}
|
||||
<div class="author-section">
|
||||
<button
|
||||
class="author-info"
|
||||
onclick={handleAuthorClick}
|
||||
type="button"
|
||||
>
|
||||
<div>
|
||||
<p class="author-name">
|
||||
{content.author?.name || content.authorId || 'Unknown'}
|
||||
</p>
|
||||
{#if content.author?.profession && content.author.profession.length > 0}
|
||||
<p class="author-profession">
|
||||
{content.author.profession[0]}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
<!-- Author Section -->
|
||||
{#if showAuthor}
|
||||
<div class="author-section">
|
||||
<button class="author-info" onclick={handleAuthorClick} type="button">
|
||||
<div>
|
||||
<p class="author-name">
|
||||
{content.author?.name || content.authorId || 'Unknown'}
|
||||
</p>
|
||||
{#if content.author?.profession && content.author.profession.length > 0}
|
||||
<p class="author-profession">
|
||||
{content.author.profession[0]}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<!-- Copy Button -->
|
||||
<button
|
||||
class="action-btn"
|
||||
onclick={handleCopy}
|
||||
title="Copy"
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
{#if showCopySuccess}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<!-- Copy Button -->
|
||||
<button
|
||||
class="action-btn"
|
||||
onclick={handleCopy}
|
||||
title="Copy"
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
{#if showCopySuccess}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Share Button -->
|
||||
<button
|
||||
class="action-btn"
|
||||
onclick={handleShare}
|
||||
title="Share"
|
||||
aria-label="Share"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="18" cy="5" r="3"></circle>
|
||||
<circle cx="6" cy="12" r="3"></circle>
|
||||
<circle cx="18" cy="19" r="3"></circle>
|
||||
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
|
||||
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Share Button -->
|
||||
<button class="action-btn" onclick={handleShare} title="Share" aria-label="Share">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="18" cy="5" r="3"></circle>
|
||||
<circle cx="6" cy="12" r="3"></circle>
|
||||
<circle cx="18" cy="19" r="3"></circle>
|
||||
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
|
||||
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Favorite Button -->
|
||||
<button
|
||||
class="action-btn favorite-btn"
|
||||
class:is-favorite={content.isFavorite}
|
||||
onclick={handleFavorite}
|
||||
title={content.isFavorite ? 'Remove from favorites' : 'Add to favorites'}
|
||||
aria-label={content.isFavorite ? 'Remove from favorites' : 'Add to favorites'}
|
||||
>
|
||||
{#if content.isFavorite}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Favorite Button -->
|
||||
<button
|
||||
class="action-btn favorite-btn"
|
||||
class:is-favorite={content.isFavorite}
|
||||
onclick={handleFavorite}
|
||||
title={content.isFavorite ? 'Remove from favorites' : 'Add to favorites'}
|
||||
aria-label={content.isFavorite ? 'Remove from favorites' : 'Add to favorites'}
|
||||
>
|
||||
{#if content.isFavorite}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
|
||||
></path>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.content-card {
|
||||
position: relative;
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 1px;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform var(--transition-base), box-shadow var(--transition-base);
|
||||
}
|
||||
.content-card {
|
||||
position: relative;
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 1px;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition:
|
||||
transform var(--transition-base),
|
||||
box-shadow var(--transition-base);
|
||||
}
|
||||
|
||||
.content-card.daily {
|
||||
border-radius: 32px;
|
||||
}
|
||||
.content-card.daily {
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
.content-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.content-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card-inner {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: calc(var(--radius-xl) - 1px);
|
||||
padding: var(--spacing-xl);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.card-inner {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: calc(var(--radius-xl) - 1px);
|
||||
padding: var(--spacing-xl);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.daily .card-inner {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 31px;
|
||||
padding: var(--spacing-2xl);
|
||||
}
|
||||
.daily .card-inner {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 31px;
|
||||
padding: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.content-text {
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.content-text {
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content-text p {
|
||||
font-family: Georgia, serif;
|
||||
font-size: 1.375rem;
|
||||
line-height: 2rem;
|
||||
color: white;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.3px;
|
||||
margin: 0;
|
||||
}
|
||||
.content-text p {
|
||||
font-family: Georgia, serif;
|
||||
font-size: 1.375rem;
|
||||
line-height: 2rem;
|
||||
color: white;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.3px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.daily .content-text p {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2.125rem;
|
||||
}
|
||||
.daily .content-text p {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2.125rem;
|
||||
}
|
||||
|
||||
.source-info {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
text-align: center;
|
||||
}
|
||||
.source-info {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.meaning-box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius-md);
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.meaning-box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius-md);
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.meaning-box strong {
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.meaning-box strong {
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.meaning-box p {
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.meaning-box p {
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.author-section {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding-top: var(--spacing-md);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
.author-section {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding-top: var(--spacing-md);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.daily .author-section {
|
||||
padding-top: var(--spacing-lg);
|
||||
}
|
||||
.daily .author-section {
|
||||
padding-top: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.author-info {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
.author-info {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.author-info:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.author-info:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
margin: 0 0 2px 0;
|
||||
}
|
||||
.author-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
margin: 0 0 2px 0;
|
||||
}
|
||||
|
||||
.author-profession {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin: 0;
|
||||
}
|
||||
.author-profession {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
align-items: center;
|
||||
}
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: var(--spacing-xs);
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
transition: all var(--transition-fast);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.action-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: var(--spacing-xs);
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
transition: all var(--transition-fast);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
color: white;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.action-btn:hover {
|
||||
color: white;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.action-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.favorite-btn.is-favorite {
|
||||
color: #ff6b9d;
|
||||
}
|
||||
.favorite-btn.is-favorite {
|
||||
color: #ff6b9d;
|
||||
}
|
||||
|
||||
.favorite-btn.is-favorite:hover {
|
||||
color: #ff4081;
|
||||
}
|
||||
.favorite-btn.is-favorite:hover {
|
||||
color: #ff4081;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.content-text p {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.content-text p {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
.action-buttons {
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.action-btn svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
.action-btn svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,317 +1,347 @@
|
|||
<script lang="ts">
|
||||
interface AppInfo {
|
||||
name: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
url: string;
|
||||
features: string[];
|
||||
}
|
||||
interface AppInfo {
|
||||
name: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
url: string;
|
||||
features: string[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
currentAppName: string;
|
||||
pageTitle?: string;
|
||||
}
|
||||
interface Props {
|
||||
currentAppName: string;
|
||||
pageTitle?: string;
|
||||
}
|
||||
|
||||
let { currentAppName, pageTitle = 'Apps entdecken' }: Props = $props();
|
||||
let { currentAppName, pageTitle = 'Apps entdecken' }: Props = $props();
|
||||
|
||||
const allApps: AppInfo[] = [
|
||||
{
|
||||
name: 'quotes',
|
||||
displayName: 'Zitate',
|
||||
description: 'Inspirierende Zitate von großen Denkern und Philosophen',
|
||||
icon: '💭',
|
||||
color: '#667eea',
|
||||
url: 'http://localhost:5173',
|
||||
features: ['1000+ Zitate', 'Berühmte Autoren', 'Kategorien & Tags']
|
||||
},
|
||||
{
|
||||
name: 'proverbs',
|
||||
displayName: 'Sprichwörter',
|
||||
description: 'Zeitlose Weisheiten und Redewendungen aus aller Welt',
|
||||
icon: '📜',
|
||||
color: '#f59e0b',
|
||||
url: 'http://localhost:5171',
|
||||
features: ['Deutsche Sprichwörter', 'Volksweisheiten', 'Redensarten']
|
||||
},
|
||||
{
|
||||
name: 'poems',
|
||||
displayName: 'Gedichte',
|
||||
description: 'Klassische und moderne Gedichte der deutschen Literatur',
|
||||
icon: '✍️',
|
||||
color: '#ec4899',
|
||||
url: 'http://localhost:5172',
|
||||
features: ['Klassische Gedichte', 'Verschiedene Epochen', 'Berühmte Dichter']
|
||||
},
|
||||
{
|
||||
name: 'fables',
|
||||
displayName: 'Fabeln',
|
||||
description: 'Klassische Fabeln von Äsop, La Fontaine und Lessing',
|
||||
icon: '🦊',
|
||||
color: '#8b5cf6',
|
||||
url: 'http://localhost:5174',
|
||||
features: ['Äsop Fabeln', 'Moralische Lehren', 'Tiergeschichten']
|
||||
}
|
||||
];
|
||||
const allApps: AppInfo[] = [
|
||||
{
|
||||
name: 'quotes',
|
||||
displayName: 'Zitate',
|
||||
description: 'Inspirierende Zitate von großen Denkern und Philosophen',
|
||||
icon: '💭',
|
||||
color: '#667eea',
|
||||
url: 'http://localhost:5173',
|
||||
features: ['1000+ Zitate', 'Berühmte Autoren', 'Kategorien & Tags'],
|
||||
},
|
||||
{
|
||||
name: 'proverbs',
|
||||
displayName: 'Sprichwörter',
|
||||
description: 'Zeitlose Weisheiten und Redewendungen aus aller Welt',
|
||||
icon: '📜',
|
||||
color: '#f59e0b',
|
||||
url: 'http://localhost:5171',
|
||||
features: ['Deutsche Sprichwörter', 'Volksweisheiten', 'Redensarten'],
|
||||
},
|
||||
{
|
||||
name: 'poems',
|
||||
displayName: 'Gedichte',
|
||||
description: 'Klassische und moderne Gedichte der deutschen Literatur',
|
||||
icon: '✍️',
|
||||
color: '#ec4899',
|
||||
url: 'http://localhost:5172',
|
||||
features: ['Klassische Gedichte', 'Verschiedene Epochen', 'Berühmte Dichter'],
|
||||
},
|
||||
{
|
||||
name: 'fables',
|
||||
displayName: 'Fabeln',
|
||||
description: 'Klassische Fabeln von Äsop, La Fontaine und Lessing',
|
||||
icon: '🦊',
|
||||
color: '#8b5cf6',
|
||||
url: 'http://localhost:5174',
|
||||
features: ['Äsop Fabeln', 'Moralische Lehren', 'Tiergeschichten'],
|
||||
},
|
||||
];
|
||||
|
||||
const otherApps = $derived(allApps.filter(app => app.name !== currentAppName));
|
||||
const otherApps = $derived(allApps.filter((app) => app.name !== currentAppName));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{pageTitle}</title>
|
||||
<title>{pageTitle}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="discover-page">
|
||||
<div class="header">
|
||||
<h1>{pageTitle}</h1>
|
||||
<p class="subtitle">Entdecke weitere Apps aus unserer Sammlung</p>
|
||||
</div>
|
||||
<div class="header">
|
||||
<h1>{pageTitle}</h1>
|
||||
<p class="subtitle">Entdecke weitere Apps aus unserer Sammlung</p>
|
||||
</div>
|
||||
|
||||
<div class="apps-grid">
|
||||
{#each otherApps as app (app.name)}
|
||||
<a href={app.url} target="_blank" rel="noopener noreferrer" class="app-card">
|
||||
<div class="app-icon" style="background: linear-gradient(135deg, {app.color} 0%, {app.color}dd 100%)">
|
||||
<span class="icon-emoji">{app.icon}</span>
|
||||
</div>
|
||||
<div class="apps-grid">
|
||||
{#each otherApps as app (app.name)}
|
||||
<a href={app.url} target="_blank" rel="noopener noreferrer" class="app-card">
|
||||
<div
|
||||
class="app-icon"
|
||||
style="background: linear-gradient(135deg, {app.color} 0%, {app.color}dd 100%)"
|
||||
>
|
||||
<span class="icon-emoji">{app.icon}</span>
|
||||
</div>
|
||||
|
||||
<div class="app-content">
|
||||
<h2 class="app-title">{app.displayName}</h2>
|
||||
<p class="app-description">{app.description}</p>
|
||||
<div class="app-content">
|
||||
<h2 class="app-title">{app.displayName}</h2>
|
||||
<p class="app-description">{app.description}</p>
|
||||
|
||||
<div class="app-features">
|
||||
{#each app.features as feature}
|
||||
<span class="feature-badge">{feature}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-features">
|
||||
{#each app.features as feature}
|
||||
<span class="feature-badge">{feature}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-cta">
|
||||
<span class="cta-text">App öffnen</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14"></path>
|
||||
<path d="m12 5 7 7-7 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="app-cta">
|
||||
<span class="cta-text">App öffnen</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14"></path>
|
||||
<path d="m12 5 7 7-7 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M12 16v-4"></path>
|
||||
<path d="M12 8h.01"></path>
|
||||
</svg>
|
||||
<p>Alle Apps teilen sich das gleiche moderne Design und nutzen dieselbe Technologie für ein einheitliches Erlebnis.</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M12 16v-4"></path>
|
||||
<path d="M12 8h.01"></path>
|
||||
</svg>
|
||||
<p>
|
||||
Alle Apps teilen sich das gleiche moderne Design und nutzen dieselbe Technologie für ein
|
||||
einheitliches Erlebnis.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.discover-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: var(--spacing-2xl);
|
||||
}
|
||||
.discover-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: rgb(var(--color-text-primary));
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: rgb(var(--color-text-primary));
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.125rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
margin: 0;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 1.125rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.apps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: var(--spacing-xl);
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
.apps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: var(--spacing-xl);
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.app-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgb(var(--color-surface));
|
||||
border: 2px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--spacing-xl);
|
||||
text-decoration: none;
|
||||
transition: all var(--transition-base);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.app-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgb(var(--color-surface));
|
||||
border: 2px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--spacing-xl);
|
||||
text-decoration: none;
|
||||
transition: all var(--transition-base);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, rgb(var(--color-primary)) 0%, rgb(var(--color-primary-dark)) 100%);
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-base);
|
||||
}
|
||||
.app-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgb(var(--color-primary)) 0%,
|
||||
rgb(var(--color-primary-dark)) 100%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-base);
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-xl);
|
||||
border-color: rgb(var(--color-primary) / 0.3);
|
||||
}
|
||||
.app-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-xl);
|
||||
border-color: rgb(var(--color-primary) / 0.3);
|
||||
}
|
||||
|
||||
.app-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
.app-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: var(--radius-xl);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.app-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: var(--radius-xl);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.icon-emoji {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
.icon-emoji {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
flex: 1;
|
||||
}
|
||||
.app-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: rgb(var(--color-text-primary));
|
||||
margin: 0 0 var(--spacing-sm) 0;
|
||||
}
|
||||
.app-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: rgb(var(--color-text-primary));
|
||||
margin: 0 0 var(--spacing-sm) 0;
|
||||
}
|
||||
|
||||
.app-description {
|
||||
font-size: 1rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
line-height: 1.6;
|
||||
margin: 0 0 var(--spacing-lg) 0;
|
||||
}
|
||||
.app-description {
|
||||
font-size: 1rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
line-height: 1.6;
|
||||
margin: 0 0 var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
.app-features {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
.app-features {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.feature-badge {
|
||||
display: inline-block;
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
background: rgb(var(--color-primary) / 0.1);
|
||||
color: rgb(var(--color-primary));
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.feature-badge {
|
||||
display: inline-block;
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
background: rgb(var(--color-primary) / 0.1);
|
||||
color: rgb(var(--color-primary));
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.app-cta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
color: rgb(var(--color-primary));
|
||||
font-weight: 600;
|
||||
margin-top: auto;
|
||||
}
|
||||
.app-cta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
color: rgb(var(--color-primary));
|
||||
font-weight: 600;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.app-card:hover .app-cta {
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
.app-card:hover .app-cta {
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.app-card:hover .app-cta svg {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
.app-card:hover .app-cta svg {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.app-cta svg {
|
||||
transition: transform var(--transition-base);
|
||||
}
|
||||
.app-cta svg {
|
||||
transition: transform var(--transition-base);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-lg);
|
||||
background: rgb(var(--color-primary) / 0.05);
|
||||
border: 1px solid rgb(var(--color-primary) / 0.2);
|
||||
border-radius: var(--radius-lg);
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
.info-box {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-lg);
|
||||
background: rgb(var(--color-primary) / 0.05);
|
||||
border: 1px solid rgb(var(--color-primary) / 0.2);
|
||||
border-radius: var(--radius-lg);
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.info-box svg {
|
||||
flex-shrink: 0;
|
||||
color: rgb(var(--color-primary));
|
||||
margin-top: 2px;
|
||||
}
|
||||
.info-box svg {
|
||||
flex-shrink: 0;
|
||||
color: rgb(var(--color-primary));
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.info-box p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.apps-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
.apps-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.app-card {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
.app-card {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
.app-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.icon-emoji {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.icon-emoji {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.app-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.app-description {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
.app-description {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
.info-box {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.info-box svg {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
.info-box svg {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,159 +1,163 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
children?: any;
|
||||
}
|
||||
interface Props {
|
||||
children?: any;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
let { children }: Props = $props();
|
||||
|
||||
let hasError = $state(false);
|
||||
let errorMessage = $state('');
|
||||
let hasError = $state(false);
|
||||
let errorMessage = $state('');
|
||||
|
||||
onMount(() => {
|
||||
// Global error handler
|
||||
const handleError = (event: ErrorEvent) => {
|
||||
console.error('Error caught:', event.error);
|
||||
hasError = true;
|
||||
errorMessage = event.error?.message || 'Ein unerwarteter Fehler ist aufgetreten';
|
||||
};
|
||||
onMount(() => {
|
||||
// Global error handler
|
||||
const handleError = (event: ErrorEvent) => {
|
||||
console.error('Error caught:', event.error);
|
||||
hasError = true;
|
||||
errorMessage = event.error?.message || 'Ein unerwarteter Fehler ist aufgetreten';
|
||||
};
|
||||
|
||||
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
|
||||
console.error('Unhandled promise rejection:', event.reason);
|
||||
hasError = true;
|
||||
errorMessage = event.reason?.message || 'Ein unerwarteter Fehler ist aufgetreten';
|
||||
};
|
||||
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
|
||||
console.error('Unhandled promise rejection:', event.reason);
|
||||
hasError = true;
|
||||
errorMessage = event.reason?.message || 'Ein unerwarteter Fehler ist aufgetreten';
|
||||
};
|
||||
|
||||
window.addEventListener('error', handleError);
|
||||
window.addEventListener('unhandledrejection', handleUnhandledRejection);
|
||||
window.addEventListener('error', handleError);
|
||||
window.addEventListener('unhandledrejection', handleUnhandledRejection);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('error', handleError);
|
||||
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
|
||||
};
|
||||
});
|
||||
return () => {
|
||||
window.removeEventListener('error', handleError);
|
||||
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
|
||||
};
|
||||
});
|
||||
|
||||
function handleReset() {
|
||||
hasError = false;
|
||||
errorMessage = '';
|
||||
window.location.reload();
|
||||
}
|
||||
function handleReset() {
|
||||
hasError = false;
|
||||
errorMessage = '';
|
||||
window.location.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if hasError}
|
||||
<div class="error-boundary">
|
||||
<div class="error-container">
|
||||
<div class="error-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<h2>Etwas ist schiefgelaufen</h2>
|
||||
<p class="error-message">{errorMessage}</p>
|
||||
<div class="error-actions">
|
||||
<button class="btn btn-primary" onclick={handleReset}>
|
||||
Seite neu laden
|
||||
</button>
|
||||
<a href="/" class="btn btn-secondary">
|
||||
Zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-boundary">
|
||||
<div class="error-container">
|
||||
<div class="error-icon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<h2>Etwas ist schiefgelaufen</h2>
|
||||
<p class="error-message">{errorMessage}</p>
|
||||
<div class="error-actions">
|
||||
<button class="btn btn-primary" onclick={handleReset}> Seite neu laden </button>
|
||||
<a href="/" class="btn btn-secondary"> Zur Startseite </a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.error-boundary {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-xl);
|
||||
background: rgb(var(--color-background));
|
||||
}
|
||||
.error-boundary {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-xl);
|
||||
background: rgb(var(--color-background));
|
||||
}
|
||||
|
||||
.error-container {
|
||||
max-width: 500px;
|
||||
text-align: center;
|
||||
}
|
||||
.error-container {
|
||||
max-width: 500px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
margin: 0 auto var(--spacing-lg);
|
||||
color: rgb(var(--color-error));
|
||||
}
|
||||
.error-icon {
|
||||
margin: 0 auto var(--spacing-lg);
|
||||
color: rgb(var(--color-error));
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.75rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 1rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
margin: 0 0 var(--spacing-xl) 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.error-message {
|
||||
font-size: 1rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
margin: 0 0 var(--spacing-xl) 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: var(--spacing-sm) var(--spacing-xl);
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
.btn {
|
||||
padding: var(--spacing-sm) var(--spacing-xl);
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: rgb(var(--color-primary));
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
.btn-primary {
|
||||
background: rgb(var(--color-primary));
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgb(var(--color-surface));
|
||||
color: rgb(var(--color-text-primary));
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
}
|
||||
.btn-secondary {
|
||||
background: rgb(var(--color-surface));
|
||||
color: rgb(var(--color-text-primary));
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgb(var(--color-background));
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: rgb(var(--color-background));
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.error-boundary {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.error-boundary {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
.error-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,53 +1,53 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
primaryColor?: string;
|
||||
primaryDarkColor?: string;
|
||||
}
|
||||
interface Props {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
primaryColor?: string;
|
||||
primaryDarkColor?: string;
|
||||
}
|
||||
|
||||
let { title, subtitle, primaryColor, primaryDarkColor }: Props = $props();
|
||||
let { title, subtitle, primaryColor, primaryDarkColor }: Props = $props();
|
||||
|
||||
const gradientStyle = $derived(() => {
|
||||
if (primaryColor && primaryDarkColor) {
|
||||
return `background: linear-gradient(135deg, rgb(${primaryColor}), rgb(${primaryDarkColor})); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;`;
|
||||
}
|
||||
return `background: linear-gradient(135deg, rgb(var(--color-primary)), rgb(var(--color-primary-dark))); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;`;
|
||||
});
|
||||
const gradientStyle = $derived(() => {
|
||||
if (primaryColor && primaryDarkColor) {
|
||||
return `background: linear-gradient(135deg, rgb(${primaryColor}), rgb(${primaryDarkColor})); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;`;
|
||||
}
|
||||
return `background: linear-gradient(135deg, rgb(var(--color-primary)), rgb(var(--color-primary-dark))); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<h1 class="title" style={gradientStyle()}>{title}</h1>
|
||||
{#if subtitle}
|
||||
<p class="subtitle">{subtitle}</p>
|
||||
{/if}
|
||||
<h1 class="title" style={gradientStyle()}>{title}</h1>
|
||||
{#if subtitle}
|
||||
<p class="subtitle">{subtitle}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
.title {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
margin: 0;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,66 +1,69 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
onInput?: (value: string) => void;
|
||||
}
|
||||
interface Props {
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
onInput?: (value: string) => void;
|
||||
}
|
||||
|
||||
let { value = $bindable(''), placeholder = 'Suchen...', onInput }: Props = $props();
|
||||
let { value = $bindable(''), placeholder = 'Suchen...', onInput }: Props = $props();
|
||||
|
||||
function handleInput(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
value = target.value;
|
||||
if (onInput) {
|
||||
onInput(target.value);
|
||||
}
|
||||
}
|
||||
function handleInput(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
value = target.value;
|
||||
if (onInput) {
|
||||
onInput(target.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="search-box">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<path d="m21 21-4.35-4.35"/>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
oninput={handleInput}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.35-4.35" />
|
||||
</svg>
|
||||
<input type="text" {value} oninput={handleInput} {placeholder} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.search-box {
|
||||
position: relative;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
.search-box {
|
||||
position: relative;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.search-box svg {
|
||||
position: absolute;
|
||||
left: var(--spacing-md);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: rgb(var(--color-text-tertiary));
|
||||
pointer-events: none;
|
||||
}
|
||||
.search-box svg {
|
||||
position: absolute;
|
||||
left: var(--spacing-md);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: rgb(var(--color-text-tertiary));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
padding: var(--spacing-md) var(--spacing-md) var(--spacing-md) calc(var(--spacing-md) * 3);
|
||||
border: 2px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: 1rem;
|
||||
background-color: rgb(var(--color-background));
|
||||
color: rgb(var(--color-text-primary));
|
||||
transition: border-color var(--transition-base);
|
||||
}
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
padding: var(--spacing-md) var(--spacing-md) var(--spacing-md) calc(var(--spacing-md) * 3);
|
||||
border: 2px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: 1rem;
|
||||
background-color: rgb(var(--color-background));
|
||||
color: rgb(var(--color-text-primary));
|
||||
transition: border-color var(--transition-base);
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
outline: none;
|
||||
border-color: rgb(var(--color-primary));
|
||||
}
|
||||
.search-box input:focus {
|
||||
outline: none;
|
||||
border-color: rgb(var(--color-primary));
|
||||
}
|
||||
|
||||
.search-box input::placeholder {
|
||||
color: rgb(var(--color-text-tertiary));
|
||||
}
|
||||
.search-box input::placeholder {
|
||||
color: rgb(var(--color-text-tertiary));
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,191 +1,198 @@
|
|||
<script lang="ts">
|
||||
import { toast, type Toast } from '../stores/toast';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import { toast, type Toast } from '../stores/toast';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
|
||||
let toasts = $state<Toast[]>([]);
|
||||
let toasts = $state<Toast[]>([]);
|
||||
|
||||
toast.subscribe(value => {
|
||||
toasts = value;
|
||||
});
|
||||
toast.subscribe((value) => {
|
||||
toasts = value;
|
||||
});
|
||||
|
||||
function handleClose(id: string) {
|
||||
toast.remove(id);
|
||||
}
|
||||
function handleClose(id: string) {
|
||||
toast.remove(id);
|
||||
}
|
||||
|
||||
function getIcon(type: Toast['type']) {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
function getIcon(type: Toast['type']) {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||
</svg>`;
|
||||
case 'error':
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
case 'error':
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||
</svg>`;
|
||||
case 'warning':
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
case 'warning':
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
||||
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||
</svg>`;
|
||||
case 'info':
|
||||
default:
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
case 'info':
|
||||
default:
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||
</svg>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="toast-container">
|
||||
{#each toasts as toastItem (toastItem.id)}
|
||||
<div
|
||||
class="toast toast-{toastItem.type}"
|
||||
transition:fly={{ y: 20, duration: 300 }}
|
||||
role="alert"
|
||||
>
|
||||
<div class="toast-icon">
|
||||
{@html getIcon(toastItem.type)}
|
||||
</div>
|
||||
<p class="toast-message">{toastItem.message}</p>
|
||||
<button
|
||||
class="toast-close"
|
||||
onclick={() => handleClose(toastItem.id)}
|
||||
aria-label="Close notification"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{#each toasts as toastItem (toastItem.id)}
|
||||
<div
|
||||
class="toast toast-{toastItem.type}"
|
||||
transition:fly={{ y: 20, duration: 300 }}
|
||||
role="alert"
|
||||
>
|
||||
<div class="toast-icon">
|
||||
{@html getIcon(toastItem.type)}
|
||||
</div>
|
||||
<p class="toast-message">{toastItem.message}</p>
|
||||
<button
|
||||
class="toast-close"
|
||||
onclick={() => handleClose(toastItem.id)}
|
||||
aria-label="Close notification"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
pointer-events: none;
|
||||
}
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xl);
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
pointer-events: auto;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xl);
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
pointer-events: auto;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.toast-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
border-left: 4px solid rgb(var(--color-success));
|
||||
}
|
||||
.toast-success {
|
||||
border-left: 4px solid rgb(var(--color-success));
|
||||
}
|
||||
|
||||
.toast-success .toast-icon {
|
||||
color: rgb(var(--color-success));
|
||||
}
|
||||
.toast-success .toast-icon {
|
||||
color: rgb(var(--color-success));
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
border-left: 4px solid rgb(var(--color-error));
|
||||
}
|
||||
.toast-error {
|
||||
border-left: 4px solid rgb(var(--color-error));
|
||||
}
|
||||
|
||||
.toast-error .toast-icon {
|
||||
color: rgb(var(--color-error));
|
||||
}
|
||||
.toast-error .toast-icon {
|
||||
color: rgb(var(--color-error));
|
||||
}
|
||||
|
||||
.toast-warning {
|
||||
border-left: 4px solid rgb(var(--color-warning));
|
||||
}
|
||||
.toast-warning {
|
||||
border-left: 4px solid rgb(var(--color-warning));
|
||||
}
|
||||
|
||||
.toast-warning .toast-icon {
|
||||
color: rgb(var(--color-warning));
|
||||
}
|
||||
.toast-warning .toast-icon {
|
||||
color: rgb(var(--color-warning));
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
border-left: 4px solid rgb(var(--color-info));
|
||||
}
|
||||
.toast-info {
|
||||
border-left: 4px solid rgb(var(--color-info));
|
||||
}
|
||||
|
||||
.toast-info .toast-icon {
|
||||
color: rgb(var(--color-info));
|
||||
}
|
||||
.toast-info .toast-icon {
|
||||
color: rgb(var(--color-info));
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
line-height: 1.5;
|
||||
}
|
||||
.toast-message {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: var(--spacing-xs);
|
||||
cursor: pointer;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
transition: all var(--transition-fast);
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.toast-close {
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: var(--spacing-xs);
|
||||
cursor: pointer;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
transition: all var(--transition-fast);
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
background: rgba(var(--color-border), 0.5);
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
.toast-close:hover {
|
||||
background: rgba(var(--color-border), 0.5);
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 768px) {
|
||||
.toast-container {
|
||||
bottom: 6rem; /* Above mobile bottom nav */
|
||||
right: 1rem;
|
||||
left: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
/* Mobile */
|
||||
@media (max-width: 768px) {
|
||||
.toast-container {
|
||||
bottom: 6rem; /* Above mobile bottom nav */
|
||||
right: 1rem;
|
||||
left: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.toast {
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
.toast {
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
.toast-message {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for progress bar (optional enhancement) */
|
||||
@keyframes shrink {
|
||||
from {
|
||||
width: 100%;
|
||||
}
|
||||
to {
|
||||
width: 0%;
|
||||
}
|
||||
}
|
||||
/* Animation for progress bar (optional enhancement) */
|
||||
@keyframes shrink {
|
||||
from {
|
||||
width: 100%;
|
||||
}
|
||||
to {
|
||||
width: 0%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,121 +2,119 @@ import { writable } from 'svelte/store';
|
|||
import { browser } from '$app/environment';
|
||||
|
||||
export interface QuoteList {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
quoteIds: string[];
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
quoteIds: string[];
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
const LISTS_KEY = 'quote-lists';
|
||||
|
||||
function createListsStore() {
|
||||
// Load initial data from localStorage
|
||||
const initialLists: QuoteList[] = browser
|
||||
? JSON.parse(localStorage.getItem(LISTS_KEY) || '[]')
|
||||
: [];
|
||||
// Load initial data from localStorage
|
||||
const initialLists: QuoteList[] = browser
|
||||
? JSON.parse(localStorage.getItem(LISTS_KEY) || '[]')
|
||||
: [];
|
||||
|
||||
const { subscribe, set, update } = writable<QuoteList[]>(initialLists);
|
||||
const { subscribe, set, update } = writable<QuoteList[]>(initialLists);
|
||||
|
||||
// Helper to save to localStorage
|
||||
function saveToStorage(lists: QuoteList[]) {
|
||||
if (browser) {
|
||||
localStorage.setItem(LISTS_KEY, JSON.stringify(lists));
|
||||
}
|
||||
}
|
||||
// Helper to save to localStorage
|
||||
function saveToStorage(lists: QuoteList[]) {
|
||||
if (browser) {
|
||||
localStorage.setItem(LISTS_KEY, JSON.stringify(lists));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
// Create a new list
|
||||
createList: (name: string, description?: string) => {
|
||||
const newList: QuoteList = {
|
||||
id: `list-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
name,
|
||||
description,
|
||||
quoteIds: [],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
// Create a new list
|
||||
createList: (name: string, description?: string) => {
|
||||
const newList: QuoteList = {
|
||||
id: `list-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
name,
|
||||
description,
|
||||
quoteIds: [],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
update(lists => {
|
||||
const updated = [...lists, newList];
|
||||
saveToStorage(updated);
|
||||
return updated;
|
||||
});
|
||||
update((lists) => {
|
||||
const updated = [...lists, newList];
|
||||
saveToStorage(updated);
|
||||
return updated;
|
||||
});
|
||||
|
||||
return newList.id;
|
||||
},
|
||||
return newList.id;
|
||||
},
|
||||
|
||||
// Update a list
|
||||
updateList: (id: string, updates: Partial<Omit<QuoteList, 'id' | 'createdAt'>>) => {
|
||||
update(lists => {
|
||||
const updated = lists.map(list =>
|
||||
list.id === id
|
||||
? { ...list, ...updates, updatedAt: Date.now() }
|
||||
: list
|
||||
);
|
||||
saveToStorage(updated);
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
// Update a list
|
||||
updateList: (id: string, updates: Partial<Omit<QuoteList, 'id' | 'createdAt'>>) => {
|
||||
update((lists) => {
|
||||
const updated = lists.map((list) =>
|
||||
list.id === id ? { ...list, ...updates, updatedAt: Date.now() } : list
|
||||
);
|
||||
saveToStorage(updated);
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
|
||||
// Delete a list
|
||||
deleteList: (id: string) => {
|
||||
update(lists => {
|
||||
const updated = lists.filter(list => list.id !== id);
|
||||
saveToStorage(updated);
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
// Delete a list
|
||||
deleteList: (id: string) => {
|
||||
update((lists) => {
|
||||
const updated = lists.filter((list) => list.id !== id);
|
||||
saveToStorage(updated);
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
|
||||
// Add quote to list
|
||||
addQuoteToList: (listId: string, quoteId: string) => {
|
||||
update(lists => {
|
||||
const updated = lists.map(list => {
|
||||
if (list.id === listId && !list.quoteIds.includes(quoteId)) {
|
||||
return {
|
||||
...list,
|
||||
quoteIds: [...list.quoteIds, quoteId],
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
return list;
|
||||
});
|
||||
saveToStorage(updated);
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
// Add quote to list
|
||||
addQuoteToList: (listId: string, quoteId: string) => {
|
||||
update((lists) => {
|
||||
const updated = lists.map((list) => {
|
||||
if (list.id === listId && !list.quoteIds.includes(quoteId)) {
|
||||
return {
|
||||
...list,
|
||||
quoteIds: [...list.quoteIds, quoteId],
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
return list;
|
||||
});
|
||||
saveToStorage(updated);
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
|
||||
// Remove quote from list
|
||||
removeQuoteFromList: (listId: string, quoteId: string) => {
|
||||
update(lists => {
|
||||
const updated = lists.map(list => {
|
||||
if (list.id === listId) {
|
||||
return {
|
||||
...list,
|
||||
quoteIds: list.quoteIds.filter(id => id !== quoteId),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
return list;
|
||||
});
|
||||
saveToStorage(updated);
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
// Remove quote from list
|
||||
removeQuoteFromList: (listId: string, quoteId: string) => {
|
||||
update((lists) => {
|
||||
const updated = lists.map((list) => {
|
||||
if (list.id === listId) {
|
||||
return {
|
||||
...list,
|
||||
quoteIds: list.quoteIds.filter((id) => id !== quoteId),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
return list;
|
||||
});
|
||||
saveToStorage(updated);
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
|
||||
// Get a specific list
|
||||
getList: (id: string): QuoteList | undefined => {
|
||||
let foundList: QuoteList | undefined;
|
||||
subscribe(lists => {
|
||||
foundList = lists.find(list => list.id === id);
|
||||
})();
|
||||
return foundList;
|
||||
},
|
||||
};
|
||||
// Get a specific list
|
||||
getList: (id: string): QuoteList | undefined => {
|
||||
let foundList: QuoteList | undefined;
|
||||
subscribe((lists) => {
|
||||
foundList = lists.find((list) => list.id === id);
|
||||
})();
|
||||
return foundList;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const listsStore = createListsStore();
|
||||
|
|
|
|||
|
|
@ -4,21 +4,22 @@ import { browser } from '$app/environment';
|
|||
const SIDEBAR_KEY = 'sidebar-collapsed';
|
||||
|
||||
function createSidebarStore() {
|
||||
const stored = browser ? localStorage.getItem(SIDEBAR_KEY) === 'true' : false;
|
||||
const { subscribe, set, update } = writable<boolean>(stored);
|
||||
const stored = browser ? localStorage.getItem(SIDEBAR_KEY) === 'true' : false;
|
||||
const { subscribe, set, update } = writable<boolean>(stored);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
toggle: () => update(v => {
|
||||
const newValue = !v;
|
||||
if (browser) localStorage.setItem(SIDEBAR_KEY, String(newValue));
|
||||
return newValue;
|
||||
}),
|
||||
set: (value: boolean) => {
|
||||
if (browser) localStorage.setItem(SIDEBAR_KEY, String(value));
|
||||
set(value);
|
||||
}
|
||||
};
|
||||
return {
|
||||
subscribe,
|
||||
toggle: () =>
|
||||
update((v) => {
|
||||
const newValue = !v;
|
||||
if (browser) localStorage.setItem(SIDEBAR_KEY, String(newValue));
|
||||
return newValue;
|
||||
}),
|
||||
set: (value: boolean) => {
|
||||
if (browser) localStorage.setItem(SIDEBAR_KEY, String(value));
|
||||
set(value);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const isSidebarCollapsed = createSidebarStore();
|
||||
|
|
|
|||
|
|
@ -4,32 +4,32 @@ import { browser } from '$app/environment';
|
|||
export type Theme = 'light' | 'dark';
|
||||
|
||||
function createThemeStore() {
|
||||
const { subscribe, set, update } = writable<Theme>('light');
|
||||
const { subscribe, set, update } = writable<Theme>('light');
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
toggle: () => {
|
||||
update(current => {
|
||||
const newTheme = current === 'light' ? 'dark' : 'light';
|
||||
if (browser) {
|
||||
localStorage.setItem('theme', newTheme);
|
||||
document.documentElement.setAttribute('data-theme', newTheme);
|
||||
}
|
||||
return newTheme;
|
||||
});
|
||||
},
|
||||
init: () => {
|
||||
if (browser) {
|
||||
const savedTheme = localStorage.getItem('theme') as Theme | null;
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
toggle: () => {
|
||||
update((current) => {
|
||||
const newTheme = current === 'light' ? 'dark' : 'light';
|
||||
if (browser) {
|
||||
localStorage.setItem('theme', newTheme);
|
||||
document.documentElement.setAttribute('data-theme', newTheme);
|
||||
}
|
||||
return newTheme;
|
||||
});
|
||||
},
|
||||
init: () => {
|
||||
if (browser) {
|
||||
const savedTheme = localStorage.getItem('theme') as Theme | null;
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
|
||||
|
||||
document.documentElement.setAttribute('data-theme', initialTheme);
|
||||
set(initialTheme);
|
||||
}
|
||||
}
|
||||
};
|
||||
document.documentElement.setAttribute('data-theme', initialTheme);
|
||||
set(initialTheme);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const theme = createThemeStore();
|
||||
|
|
|
|||
|
|
@ -3,49 +3,49 @@ import { writable } from 'svelte/store';
|
|||
export type ToastType = 'success' | 'error' | 'info' | 'warning';
|
||||
|
||||
export interface Toast {
|
||||
id: string;
|
||||
message: string;
|
||||
type: ToastType;
|
||||
duration: number;
|
||||
id: string;
|
||||
message: string;
|
||||
type: ToastType;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
function createToastStore() {
|
||||
const { subscribe, update } = writable<Toast[]>([]);
|
||||
const { subscribe, update } = writable<Toast[]>([]);
|
||||
|
||||
function addToast(message: string, type: ToastType = 'info', duration: number = 3000) {
|
||||
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
function addToast(message: string, type: ToastType = 'info', duration: number = 3000) {
|
||||
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const toast: Toast = {
|
||||
id,
|
||||
message,
|
||||
type,
|
||||
duration,
|
||||
};
|
||||
const toast: Toast = {
|
||||
id,
|
||||
message,
|
||||
type,
|
||||
duration,
|
||||
};
|
||||
|
||||
update(toasts => [...toasts, toast]);
|
||||
update((toasts) => [...toasts, toast]);
|
||||
|
||||
// Auto-remove after duration
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
removeToast(id);
|
||||
}, duration);
|
||||
}
|
||||
// Auto-remove after duration
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
removeToast(id);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function removeToast(id: string) {
|
||||
update(toasts => toasts.filter(t => t.id !== id));
|
||||
}
|
||||
function removeToast(id: string) {
|
||||
update((toasts) => toasts.filter((t) => t.id !== id));
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
success: (message: string, duration?: number) => addToast(message, 'success', duration),
|
||||
error: (message: string, duration?: number) => addToast(message, 'error', duration),
|
||||
info: (message: string, duration?: number) => addToast(message, 'info', duration),
|
||||
warning: (message: string, duration?: number) => addToast(message, 'warning', duration),
|
||||
remove: removeToast,
|
||||
};
|
||||
return {
|
||||
subscribe,
|
||||
success: (message: string, duration?: number) => addToast(message, 'success', duration),
|
||||
error: (message: string, duration?: number) => addToast(message, 'error', duration),
|
||||
info: (message: string, duration?: number) => addToast(message, 'info', duration),
|
||||
warning: (message: string, duration?: number) => addToast(message, 'warning', duration),
|
||||
remove: removeToast,
|
||||
};
|
||||
}
|
||||
|
||||
export const toast = createToastStore();
|
||||
|
|
|
|||
|
|
@ -1,62 +1,62 @@
|
|||
import type { Config } from 'tailwindcss';
|
||||
|
||||
export default {
|
||||
content: [], // Will be extended by consuming apps
|
||||
darkMode: ['class', '[data-theme="dark"]'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: 'rgb(var(--color-primary) / <alpha-value>)',
|
||||
dark: 'rgb(var(--color-primary-dark) / <alpha-value>)',
|
||||
},
|
||||
secondary: 'rgb(var(--color-secondary) / <alpha-value>)',
|
||||
background: 'rgb(var(--color-background) / <alpha-value>)',
|
||||
surface: {
|
||||
DEFAULT: 'rgb(var(--color-surface) / <alpha-value>)',
|
||||
elevated: 'rgb(var(--color-surface-elevated) / <alpha-value>)',
|
||||
},
|
||||
text: {
|
||||
primary: 'rgb(var(--color-text-primary) / <alpha-value>)',
|
||||
secondary: 'rgb(var(--color-text-secondary) / <alpha-value>)',
|
||||
tertiary: 'rgb(var(--color-text-tertiary) / <alpha-value>)',
|
||||
},
|
||||
border: {
|
||||
DEFAULT: 'rgb(var(--color-border) / <alpha-value>)',
|
||||
hover: 'rgb(var(--color-border-hover) / <alpha-value>)',
|
||||
},
|
||||
success: 'rgb(var(--color-success) / <alpha-value>)',
|
||||
warning: 'rgb(var(--color-warning) / <alpha-value>)',
|
||||
error: 'rgb(var(--color-error) / <alpha-value>)',
|
||||
info: 'rgb(var(--color-info) / <alpha-value>)',
|
||||
},
|
||||
spacing: {
|
||||
xs: 'var(--spacing-xs)',
|
||||
sm: 'var(--spacing-sm)',
|
||||
md: 'var(--spacing-md)',
|
||||
lg: 'var(--spacing-lg)',
|
||||
xl: 'var(--spacing-xl)',
|
||||
'2xl': 'var(--spacing-2xl)',
|
||||
},
|
||||
borderRadius: {
|
||||
sm: 'var(--radius-sm)',
|
||||
md: 'var(--radius-md)',
|
||||
lg: 'var(--radius-lg)',
|
||||
xl: 'var(--radius-xl)',
|
||||
full: 'var(--radius-full)',
|
||||
},
|
||||
boxShadow: {
|
||||
sm: 'var(--shadow-sm)',
|
||||
md: 'var(--shadow-md)',
|
||||
lg: 'var(--shadow-lg)',
|
||||
xl: 'var(--shadow-xl)',
|
||||
},
|
||||
transitionDuration: {
|
||||
fast: '150ms',
|
||||
base: '200ms',
|
||||
slow: '300ms',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
content: [], // Will be extended by consuming apps
|
||||
darkMode: ['class', '[data-theme="dark"]'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: 'rgb(var(--color-primary) / <alpha-value>)',
|
||||
dark: 'rgb(var(--color-primary-dark) / <alpha-value>)',
|
||||
},
|
||||
secondary: 'rgb(var(--color-secondary) / <alpha-value>)',
|
||||
background: 'rgb(var(--color-background) / <alpha-value>)',
|
||||
surface: {
|
||||
DEFAULT: 'rgb(var(--color-surface) / <alpha-value>)',
|
||||
elevated: 'rgb(var(--color-surface-elevated) / <alpha-value>)',
|
||||
},
|
||||
text: {
|
||||
primary: 'rgb(var(--color-text-primary) / <alpha-value>)',
|
||||
secondary: 'rgb(var(--color-text-secondary) / <alpha-value>)',
|
||||
tertiary: 'rgb(var(--color-text-tertiary) / <alpha-value>)',
|
||||
},
|
||||
border: {
|
||||
DEFAULT: 'rgb(var(--color-border) / <alpha-value>)',
|
||||
hover: 'rgb(var(--color-border-hover) / <alpha-value>)',
|
||||
},
|
||||
success: 'rgb(var(--color-success) / <alpha-value>)',
|
||||
warning: 'rgb(var(--color-warning) / <alpha-value>)',
|
||||
error: 'rgb(var(--color-error) / <alpha-value>)',
|
||||
info: 'rgb(var(--color-info) / <alpha-value>)',
|
||||
},
|
||||
spacing: {
|
||||
xs: 'var(--spacing-xs)',
|
||||
sm: 'var(--spacing-sm)',
|
||||
md: 'var(--spacing-md)',
|
||||
lg: 'var(--spacing-lg)',
|
||||
xl: 'var(--spacing-xl)',
|
||||
'2xl': 'var(--spacing-2xl)',
|
||||
},
|
||||
borderRadius: {
|
||||
sm: 'var(--radius-sm)',
|
||||
md: 'var(--radius-md)',
|
||||
lg: 'var(--radius-lg)',
|
||||
xl: 'var(--radius-xl)',
|
||||
full: 'var(--radius-full)',
|
||||
},
|
||||
boxShadow: {
|
||||
sm: 'var(--shadow-sm)',
|
||||
md: 'var(--shadow-md)',
|
||||
lg: 'var(--shadow-lg)',
|
||||
xl: 'var(--shadow-xl)',
|
||||
},
|
||||
transitionDuration: {
|
||||
fast: '150ms',
|
||||
base: '200ms',
|
||||
slow: '300ms',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
} satisfies Config;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"paths": {
|
||||
"@quote/shared": ["../shared/src/index.ts"],
|
||||
"@quote/web-ui": ["./src/index.ts"],
|
||||
"@quote/web-ui/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.svelte"],
|
||||
"exclude": ["node_modules"]
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"paths": {
|
||||
"@quote/shared": ["../shared/src/index.ts"],
|
||||
"@quote/web-ui": ["./src/index.ts"],
|
||||
"@quote/web-ui/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.svelte"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue