mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
refactor: rename zitare -> quotes (Zitate)
Zitare was opaque Latin/Italian-flavored branding. Renamed to clear English "quotes" (DE: Zitate) matching short-concrete-noun cluster. - Module, routes, API, i18n, standalone landing app, plans dirs - Dexie tables: quotesFavorites, quotesLists, quotesListTags, customQuotes (dropped redundant "quotes" prefix on the last) - Logo QuotesLogo, theme quotes.css, search provider, dashboard widget QuoteWidget - German user-facing label "Zitate" (English brand stays Quotes) Pre-launch, no data migration needed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7a1f11c971
commit
851a281e5a
196 changed files with 891 additions and 776 deletions
24
apps/quotes/packages/content/package.json
Normal file
24
apps/quotes/packages/content/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "@quotes/content",
|
||||
"version": "0.2.0",
|
||||
"description": "Static quote content for Quotes",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
103
apps/quotes/packages/content/src/categories.ts
Normal file
103
apps/quotes/packages/content/src/categories.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* Quote categories
|
||||
*/
|
||||
export const CATEGORIES = [
|
||||
'motivation',
|
||||
'weisheit',
|
||||
'liebe',
|
||||
'leben',
|
||||
'erfolg',
|
||||
'glueck',
|
||||
'freundschaft',
|
||||
'mut',
|
||||
'hoffnung',
|
||||
'natur',
|
||||
'humor',
|
||||
'wissenschaft',
|
||||
'kunst',
|
||||
] as const;
|
||||
|
||||
export type Category = (typeof CATEGORIES)[number];
|
||||
|
||||
/**
|
||||
* German labels for categories
|
||||
*/
|
||||
export const CATEGORY_LABELS: Record<Category, string> = {
|
||||
motivation: 'Motivation',
|
||||
weisheit: 'Weisheit',
|
||||
liebe: 'Liebe',
|
||||
leben: 'Leben',
|
||||
erfolg: 'Erfolg',
|
||||
glueck: 'Glück',
|
||||
freundschaft: 'Freundschaft',
|
||||
mut: 'Mut',
|
||||
hoffnung: 'Hoffnung',
|
||||
natur: 'Natur',
|
||||
humor: 'Humor',
|
||||
wissenschaft: 'Wissenschaft',
|
||||
kunst: 'Kunst',
|
||||
};
|
||||
|
||||
/** Curated theme decks — cross-category collections around a topic. */
|
||||
export const THEME_DECKS = [
|
||||
{
|
||||
id: 'stoizismus',
|
||||
label: 'Stoizismus',
|
||||
description: 'Gelassenheit und innere Stärke',
|
||||
authors: ['Marcus Aurelius', 'Seneca', 'Epiktet'],
|
||||
},
|
||||
{
|
||||
id: 'feminismus',
|
||||
label: 'Feminismus',
|
||||
description: 'Gleichberechtigung und Selbstbestimmung',
|
||||
authors: ['Simone de Beauvoir', 'Virginia Woolf', 'Maya Angelou', 'Marie Curie', 'Frida Kahlo'],
|
||||
},
|
||||
{
|
||||
id: 'unternehmertum',
|
||||
label: 'Unternehmertum',
|
||||
description: 'Innovation und Durchhaltevermögen',
|
||||
authors: ['Steve Jobs', 'Henry Ford', 'Thomas Edison', 'Walt Disney'],
|
||||
},
|
||||
{
|
||||
id: 'philosophie',
|
||||
label: 'Philosophie',
|
||||
description: 'Die großen Fragen des Lebens',
|
||||
authors: [
|
||||
'Sokrates',
|
||||
'Platon',
|
||||
'Aristoteles',
|
||||
'Immanuel Kant',
|
||||
'Friedrich Nietzsche',
|
||||
'Konfuzius',
|
||||
'Laozi',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'literatur',
|
||||
label: 'Literatur',
|
||||
description: 'Worte der großen Dichter und Schriftsteller',
|
||||
authors: [
|
||||
'Johann Wolfgang von Goethe',
|
||||
'Oscar Wilde',
|
||||
'Mark Twain',
|
||||
'William Shakespeare',
|
||||
'Rainer Maria Rilke',
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type ThemeDeckId = (typeof THEME_DECKS)[number]['id'];
|
||||
|
||||
/**
|
||||
* Get label for a category
|
||||
*/
|
||||
export function getCategoryLabel(category: Category): string {
|
||||
return CATEGORY_LABELS[category];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is a valid category
|
||||
*/
|
||||
export function isValidCategory(value: string): value is Category {
|
||||
return CATEGORIES.includes(value as Category);
|
||||
}
|
||||
44
apps/quotes/packages/content/src/index.ts
Normal file
44
apps/quotes/packages/content/src/index.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Types
|
||||
export type {
|
||||
Quote,
|
||||
TranslatedText,
|
||||
AuthorBio,
|
||||
SupportedLanguage,
|
||||
OriginalLanguage,
|
||||
} from './types';
|
||||
export { SUPPORTED_LANGUAGES, ORIGINAL_LANGUAGES } from './types';
|
||||
export type { Category } from './categories';
|
||||
|
||||
// Data
|
||||
export { QUOTES, QUOTE_COUNT } from './quotes';
|
||||
export { CATEGORIES, CATEGORY_LABELS, THEME_DECKS } from './categories';
|
||||
export type { ThemeDeckId } from './categories';
|
||||
|
||||
// Utilities
|
||||
export {
|
||||
getRandomQuote,
|
||||
getDailyQuote,
|
||||
getQuotesByCategory,
|
||||
getRandomQuoteByCategory,
|
||||
searchQuotes,
|
||||
getQuoteById,
|
||||
getQuoteByIndex,
|
||||
getAllCategories,
|
||||
getCategoryByName,
|
||||
getQuoteText,
|
||||
formatQuote,
|
||||
formatQuoteWithNumber,
|
||||
getTotalCount,
|
||||
getQuotesByTag,
|
||||
getAllTags,
|
||||
getQuotesByAuthor,
|
||||
getAllAuthors,
|
||||
getQuotesByThemeDeck,
|
||||
fuzzySearchQuotes,
|
||||
getVerifiedQuotes,
|
||||
getQuotesByYearRange,
|
||||
getQuotesByOriginalLanguage,
|
||||
} from './utils';
|
||||
export type { AuthorInfo } from './utils';
|
||||
|
||||
export { getCategoryLabel, isValidCategory } from './categories';
|
||||
1898
apps/quotes/packages/content/src/quotes.ts
Normal file
1898
apps/quotes/packages/content/src/quotes.ts
Normal file
File diff suppressed because it is too large
Load diff
107
apps/quotes/packages/content/src/types.ts
Normal file
107
apps/quotes/packages/content/src/types.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import type { Category } from './categories';
|
||||
|
||||
/**
|
||||
* Supported languages for quote translations
|
||||
*/
|
||||
export const SUPPORTED_LANGUAGES = ['original', 'de', 'en', 'it', 'fr', 'es'] as const;
|
||||
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
|
||||
|
||||
/**
|
||||
* Original language of a quote
|
||||
*/
|
||||
export const ORIGINAL_LANGUAGES = [
|
||||
'de', // German
|
||||
'en', // English
|
||||
'fr', // French
|
||||
'es', // Spanish
|
||||
'it', // Italian
|
||||
'la', // Latin
|
||||
'el', // Greek (ancient & modern)
|
||||
'zh', // Chinese
|
||||
'sa', // Sanskrit
|
||||
'ar', // Arabic
|
||||
'fa', // Persian
|
||||
'ja', // Japanese
|
||||
'ru', // Russian
|
||||
'pt', // Portuguese
|
||||
'nl', // Dutch
|
||||
'da', // Danish
|
||||
'hi', // Hindi
|
||||
'bn', // Bengali
|
||||
] as const;
|
||||
export type OriginalLanguage = (typeof ORIGINAL_LANGUAGES)[number];
|
||||
|
||||
/**
|
||||
* Translated text object
|
||||
*/
|
||||
export interface TranslatedText {
|
||||
/** Original language text */
|
||||
original: string;
|
||||
/** German translation */
|
||||
de: string;
|
||||
/** English translation */
|
||||
en: string;
|
||||
/** Italian translation */
|
||||
it: string;
|
||||
/** French translation */
|
||||
fr: string;
|
||||
/** Spanish translation */
|
||||
es: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Author biography in multiple languages
|
||||
*/
|
||||
export interface AuthorBio {
|
||||
de?: string;
|
||||
en?: string;
|
||||
it?: string;
|
||||
fr?: string;
|
||||
es?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A quote with author, translations, and metadata
|
||||
*/
|
||||
export interface Quote {
|
||||
/** Unique identifier (e.g., 'mot-1', 'weis-2') */
|
||||
id: string;
|
||||
|
||||
/** Quote text in all supported languages */
|
||||
text: TranslatedText;
|
||||
|
||||
/** Author name */
|
||||
author: string;
|
||||
|
||||
/** Category for filtering */
|
||||
category: Category;
|
||||
|
||||
/** Original language of the quote */
|
||||
originalLanguage: OriginalLanguage;
|
||||
|
||||
/** Source: book, speech, interview, letter, etc. */
|
||||
source?: string;
|
||||
|
||||
/** Year the quote was made/published */
|
||||
year?: number;
|
||||
|
||||
/** Additional tags for search/filtering */
|
||||
tags?: string[];
|
||||
|
||||
/** URL to author image */
|
||||
imageUrl?: string;
|
||||
|
||||
/** Short author biography */
|
||||
authorBio?: AuthorBio;
|
||||
|
||||
/** Whether the quote source has been verified */
|
||||
verified?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper type for creating quotes with partial translations
|
||||
* (translations can be added incrementally)
|
||||
*/
|
||||
export type PartialQuote = Omit<Quote, 'text'> & {
|
||||
text: Partial<TranslatedText> & { original: string; de: string };
|
||||
};
|
||||
339
apps/quotes/packages/content/src/utils.ts
Normal file
339
apps/quotes/packages/content/src/utils.ts
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
import { QUOTES } from './quotes';
|
||||
import {
|
||||
CATEGORIES,
|
||||
CATEGORY_LABELS,
|
||||
THEME_DECKS,
|
||||
type Category,
|
||||
type ThemeDeckId,
|
||||
} from './categories';
|
||||
import type { Quote, SupportedLanguage } from './types';
|
||||
|
||||
/**
|
||||
* Get a random quote
|
||||
*/
|
||||
export function getRandomQuote(): Quote {
|
||||
const index = Math.floor(Math.random() * QUOTES.length);
|
||||
return QUOTES[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deterministic daily quote based on date
|
||||
*/
|
||||
export function getDailyQuote(date: Date = new Date()): Quote {
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
const hash = hashString(dateStr);
|
||||
const index = Math.abs(hash) % QUOTES.length;
|
||||
return QUOTES[index];
|
||||
}
|
||||
|
||||
// ─── Pre-built category index (built once, O(1) per lookup) ──
|
||||
|
||||
let _categoryIndex: Map<Category, Quote[]> | null = null;
|
||||
|
||||
function getCategoryIndex(): Map<Category, Quote[]> {
|
||||
if (!_categoryIndex) {
|
||||
_categoryIndex = new Map();
|
||||
for (const cat of CATEGORIES) _categoryIndex.set(cat, []);
|
||||
for (const q of QUOTES) _categoryIndex.get(q.category)?.push(q);
|
||||
}
|
||||
return _categoryIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get quotes by category (uses pre-built index for O(1) lookups).
|
||||
*/
|
||||
export function getQuotesByCategory(category: Category): Quote[] {
|
||||
return getCategoryIndex().get(category) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a random quote from a specific category
|
||||
*/
|
||||
export function getRandomQuoteByCategory(category: Category): Quote | null {
|
||||
const quotes = getQuotesByCategory(category);
|
||||
if (quotes.length === 0) return null;
|
||||
const index = Math.floor(Math.random() * quotes.length);
|
||||
return quotes[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search quotes by text or author (searches in specified language, defaults to German)
|
||||
*/
|
||||
export function searchQuotes(searchText: string, language: SupportedLanguage = 'de'): Quote[] {
|
||||
const lowerSearch = searchText.toLowerCase();
|
||||
return QUOTES.filter((q) => {
|
||||
const text = language === 'original' ? q.text.original : q.text[language];
|
||||
return text.toLowerCase().includes(lowerSearch) || q.author.toLowerCase().includes(lowerSearch);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a quote by ID
|
||||
*/
|
||||
export function getQuoteById(id: string): Quote | undefined {
|
||||
return QUOTES.find((q) => q.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get quote by index (1-based)
|
||||
*/
|
||||
export function getQuoteByIndex(index: number): Quote | null {
|
||||
if (index < 1 || index > QUOTES.length) return null;
|
||||
return QUOTES[index - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories with counts
|
||||
*/
|
||||
export function getAllCategories(): { category: Category; label: string; count: number }[] {
|
||||
return CATEGORIES.map((category) => ({
|
||||
category,
|
||||
label: CATEGORY_LABELS[category],
|
||||
count: QUOTES.filter((q) => q.category === category).length,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find category by name (partial match)
|
||||
*/
|
||||
export function getCategoryByName(name: string): Category | null {
|
||||
const lowerName = name.toLowerCase();
|
||||
|
||||
// Exact match first
|
||||
if (CATEGORIES.includes(lowerName as Category)) {
|
||||
return lowerName as Category;
|
||||
}
|
||||
|
||||
// Partial match
|
||||
for (const category of CATEGORIES) {
|
||||
if (
|
||||
category.startsWith(lowerName) ||
|
||||
CATEGORY_LABELS[category].toLowerCase().startsWith(lowerName)
|
||||
) {
|
||||
return category;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get quote text in a specific language
|
||||
*/
|
||||
export function getQuoteText(quote: Quote, language: SupportedLanguage = 'de'): string {
|
||||
if (language === 'original') {
|
||||
return quote.text.original;
|
||||
}
|
||||
return quote.text[language];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a quote for display
|
||||
*/
|
||||
export function formatQuote(quote: Quote, language: SupportedLanguage = 'de'): string {
|
||||
const text = getQuoteText(quote, language);
|
||||
const categoryLabel = CATEGORY_LABELS[quote.category];
|
||||
return `"${text}"\n\n— *${quote.author}*\n\n[${categoryLabel}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a quote with number
|
||||
*/
|
||||
export function formatQuoteWithNumber(
|
||||
quote: Quote,
|
||||
number: number,
|
||||
language: SupportedLanguage = 'de'
|
||||
): string {
|
||||
const text = getQuoteText(quote, language);
|
||||
const categoryLabel = CATEGORY_LABELS[quote.category];
|
||||
return `**#${number}**\n"${text}"\n\n— *${quote.author}* [${categoryLabel}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total quote count
|
||||
*/
|
||||
export function getTotalCount(): number {
|
||||
return QUOTES.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get quotes by tag
|
||||
*/
|
||||
export function getQuotesByTag(tag: string): Quote[] {
|
||||
const lowerTag = tag.toLowerCase();
|
||||
return QUOTES.filter((q) => q.tags?.some((t) => t.toLowerCase() === lowerTag));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unique tags
|
||||
*/
|
||||
export function getAllTags(): string[] {
|
||||
const tags = new Set<string>();
|
||||
QUOTES.forEach((q) => q.tags?.forEach((t) => tags.add(t)));
|
||||
return Array.from(tags).sort();
|
||||
}
|
||||
|
||||
// ─── Pre-built author index ──────────────────────────────────
|
||||
|
||||
let _authorIndex: Map<string, Quote[]> | null = null;
|
||||
|
||||
function getAuthorIndex(): Map<string, Quote[]> {
|
||||
if (!_authorIndex) {
|
||||
_authorIndex = new Map();
|
||||
for (const q of QUOTES) {
|
||||
const key = q.author.toLowerCase();
|
||||
let arr = _authorIndex.get(key);
|
||||
if (!arr) {
|
||||
arr = [];
|
||||
_authorIndex.set(key, arr);
|
||||
}
|
||||
arr.push(q);
|
||||
}
|
||||
}
|
||||
return _authorIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get quotes by author (substring match on name).
|
||||
*/
|
||||
export function getQuotesByAuthor(author: string): Quote[] {
|
||||
const lowerAuthor = author.toLowerCase();
|
||||
// Exact match via index first
|
||||
const exact = getAuthorIndex().get(lowerAuthor);
|
||||
if (exact) return exact;
|
||||
// Fall back to substring match across all authors
|
||||
const results: Quote[] = [];
|
||||
for (const [key, quotes] of getAuthorIndex()) {
|
||||
if (key.includes(lowerAuthor)) results.push(...quotes);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Author summary for browse pages. */
|
||||
export interface AuthorInfo {
|
||||
name: string;
|
||||
quoteCount: number;
|
||||
categories: string[];
|
||||
bio?: { de?: string; en?: string; it?: string; fr?: string; es?: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unique authors with their quote counts, categories, and bios.
|
||||
* Sorted by quote count descending, then name ascending.
|
||||
*/
|
||||
export function getAllAuthors(): AuthorInfo[] {
|
||||
const map = new Map<string, AuthorInfo>();
|
||||
for (const q of QUOTES) {
|
||||
let info = map.get(q.author);
|
||||
if (!info) {
|
||||
info = { name: q.author, quoteCount: 0, categories: [], bio: q.authorBio };
|
||||
map.set(q.author, info);
|
||||
}
|
||||
info.quoteCount++;
|
||||
if (!info.categories.includes(q.category)) {
|
||||
info.categories.push(q.category);
|
||||
}
|
||||
// Prefer the bio entry that has content
|
||||
if (!info.bio && q.authorBio) info.bio = q.authorBio;
|
||||
}
|
||||
return Array.from(map.values()).sort(
|
||||
(a, b) => b.quoteCount - a.quoteCount || a.name.localeCompare(b.name)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get verified quotes only
|
||||
*/
|
||||
export function getVerifiedQuotes(): Quote[] {
|
||||
return QUOTES.filter((q) => q.verified === true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get quotes by year range
|
||||
*/
|
||||
export function getQuotesByYearRange(startYear: number, endYear: number): Quote[] {
|
||||
return QUOTES.filter((q) => q.year !== undefined && q.year >= startYear && q.year <= endYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get quotes by original language
|
||||
*/
|
||||
export function getQuotesByOriginalLanguage(language: string): Quote[] {
|
||||
return QUOTES.filter((q) => q.originalLanguage === language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get quotes for a curated theme deck.
|
||||
*/
|
||||
export function getQuotesByThemeDeck(deckId: ThemeDeckId): Quote[] {
|
||||
const deck = THEME_DECKS.find((d) => d.id === deckId);
|
||||
if (!deck) return [];
|
||||
const authorSet = new Set(deck.authors.map((a) => a.toLowerCase()));
|
||||
return QUOTES.filter((q) => authorSet.has(q.author.toLowerCase()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuzzy search — matches even with typos using bigram similarity.
|
||||
* Falls back to simple substring match for short queries.
|
||||
*/
|
||||
export function fuzzySearchQuotes(
|
||||
query: string,
|
||||
language: SupportedLanguage = 'de',
|
||||
threshold = 0.3
|
||||
): Quote[] {
|
||||
const normalizedQuery = query.toLowerCase().trim();
|
||||
if (!normalizedQuery) return [];
|
||||
|
||||
// For very short queries (1-2 chars), use exact substring
|
||||
if (normalizedQuery.length <= 2) return searchQuotes(query, language);
|
||||
|
||||
const queryBigrams = toBigrams(normalizedQuery);
|
||||
|
||||
return QUOTES.filter((q) => {
|
||||
const text = language === 'original' ? q.text.original : q.text[language];
|
||||
const haystack = `${text} ${q.author}`.toLowerCase();
|
||||
|
||||
// Fast path: exact substring match
|
||||
if (haystack.includes(normalizedQuery)) return true;
|
||||
|
||||
// Check individual words for fuzzy match
|
||||
const queryWords = normalizedQuery.split(/\s+/);
|
||||
return queryWords.every((word) => {
|
||||
if (haystack.includes(word)) return true;
|
||||
if (word.length <= 2) return false;
|
||||
const wordBigrams = toBigrams(word);
|
||||
// Check if any word in the haystack has high bigram similarity
|
||||
return haystack.split(/\s+/).some((hw) => {
|
||||
if (hw.length <= 2) return false;
|
||||
return bigramSimilarity(wordBigrams, toBigrams(hw)) >= threshold;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toBigrams(s: string): Set<string> {
|
||||
const bigrams = new Set<string>();
|
||||
for (let i = 0; i < s.length - 1; i++) {
|
||||
bigrams.add(s.slice(i, i + 2));
|
||||
}
|
||||
return bigrams;
|
||||
}
|
||||
|
||||
function bigramSimilarity(a: Set<string>, b: Set<string>): number {
|
||||
let intersection = 0;
|
||||
for (const bigram of a) {
|
||||
if (b.has(bigram)) intersection++;
|
||||
}
|
||||
return (2 * intersection) / (a.size + b.size);
|
||||
}
|
||||
|
||||
// Helper function
|
||||
function hashString(str: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
16
apps/quotes/packages/content/tsconfig.json
Normal file
16
apps/quotes/packages/content/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
9
apps/quotes/packages/content/tsup.config.ts
Normal file
9
apps/quotes/packages/content/tsup.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm', 'cjs'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue