feat: multi-language quote selection with per-user language picker

Users can now choose which languages their motivational quotes appear in
via toggle buttons in Settings. Added 45 curated quotes in French, Spanish,
and Italian. Quote language preference is stored per-user in the database.

v1.22.0
This commit is contained in:
mARTin 2026-03-09 08:11:23 +01:00
parent 9ac7198b24
commit 0b68d275d0
6 changed files with 376 additions and 13 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.21.0", "version": "1.22.0",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -98,6 +98,7 @@ model User {
customWeekdayNames String? @default("") customWeekdayNames String? @default("")
startDayOffset Int @default(-1) startDayOffset Int @default(-1)
quoteSourceUrls String[] @default([]) quoteSourceUrls String[] @default([])
quoteLanguages String[] @default(["en", "de"])
accounts Account[] accounts Account[]
cachedCalendarEvents CachedCalendarEvent[] cachedCalendarEvents CachedCalendarEvent[]
calendarConnections CalendarConnection[] calendarConnections CalendarConnection[]

View File

@ -36,6 +36,7 @@ export async function GET(req: Request) {
goalFallbackType: true, goalFallbackType: true,
goalDefaultSentence: true, goalDefaultSentence: true,
language: true, language: true,
quoteLanguages: true,
} }
}); });
@ -92,9 +93,11 @@ export async function GET(req: Request) {
return NextResponse.json({ goal: holidayHint, isHoliday: true }); return NextResponse.json({ goal: holidayHint, isHoliday: true });
} }
// 4. Fallback to ZenQuotes motivational quote (English only) // 4. Fallback to ZenQuotes motivational quote (if English is in quote languages)
const userLang = user?.language || 'en'; const quoteLangs: string[] = (user as any)?.quoteLanguages?.length > 0
if (userLang === 'en') { ? (user as any).quoteLanguages
: [user?.language || 'en'];
if (quoteLangs.includes('en')) {
try { try {
const res = await fetch('https://zenquotes.io/api/random', { signal: AbortSignal.timeout(3000) }); const res = await fetch('https://zenquotes.io/api/random', { signal: AbortSignal.timeout(3000) });
if (res.ok) { if (res.ok) {
@ -108,8 +111,9 @@ export async function GET(req: Request) {
} }
} }
// 5. Fallback to local curated quotes (supports DE and EN) // 5. Fallback to local curated quotes (pick random from selected languages)
const localQuote = getRandomLocalQuote(userLang); const randomLang = quoteLangs[Math.floor(Math.random() * quoteLangs.length)];
const localQuote = getRandomLocalQuote(randomLang);
if (localQuote) { if (localQuote) {
return NextResponse.json({ goal: `${localQuote.text}${localQuote.author}`, isQuote: true }); return NextResponse.json({ goal: `${localQuote.text}${localQuote.author}`, isQuote: true });
} }

View File

@ -88,6 +88,7 @@ export async function GET(request: NextRequest) {
weekdayCase: true, weekdayCase: true,
customWeekdayNames: true, customWeekdayNames: true,
quoteSourceUrls: true, quoteSourceUrls: true,
quoteLanguages: true,
accountNumber: true, accountNumber: true,
createdAt: true createdAt: true
} }
@ -130,7 +131,7 @@ export async function PATCH(request: NextRequest) {
cwFontFamily, cwFontSize, cwFontWeight, cwColor, cwFontFamily, cwFontSize, cwFontWeight, cwColor,
yearFontFamily, yearFontSize, yearFontWeight, yearColor, yearFontFamily, yearFontSize, yearFontWeight, yearColor,
showTaskCheckboxes, dayHeaderGap, showTaskCheckboxes, dayHeaderGap,
showCompletedTasks, showLines, startDayOffset, weekdayFormat, weekdayCase, customWeekdayNames, quoteSourceUrls showCompletedTasks, showLines, startDayOffset, weekdayFormat, weekdayCase, customWeekdayNames, quoteSourceUrls, quoteLanguages
} = body; } = body;
const updateData: any = { const updateData: any = {
@ -208,6 +209,7 @@ export async function PATCH(request: NextRequest) {
...(weekdayCase !== undefined && { weekdayCase }), ...(weekdayCase !== undefined && { weekdayCase }),
...(customWeekdayNames !== undefined && { customWeekdayNames }), ...(customWeekdayNames !== undefined && { customWeekdayNames }),
...(quoteSourceUrls !== undefined && { quoteSourceUrls }), ...(quoteSourceUrls !== undefined && { quoteSourceUrls }),
...(quoteLanguages !== undefined && { quoteLanguages }),
}; };
if (password && password.trim() !== "") { if (password && password.trim() !== "") {
updateData.passwordHash = await bcrypt.hash(password, 10); updateData.passwordHash = await bcrypt.hash(password, 10);
@ -293,6 +295,7 @@ export async function PATCH(request: NextRequest) {
weekdayCase: true, weekdayCase: true,
customWeekdayNames: true, customWeekdayNames: true,
quoteSourceUrls: true, quoteSourceUrls: true,
quoteLanguages: true,
accountNumber: true, accountNumber: true,
} }
}); });

View File

@ -1126,6 +1126,7 @@ export default function WeeklyView() {
dayHeaderGap?: string; dayHeaderGap?: string;
showTaskCheckboxes?: boolean; showTaskCheckboxes?: boolean;
quoteSourceUrls?: string[]; quoteSourceUrls?: string[];
quoteLanguages?: string[];
startDayOffset?: number; startDayOffset?: number;
id?: string; id?: string;
accountNumber?: number; accountNumber?: number;
@ -1195,6 +1196,7 @@ export default function WeeklyView() {
yearFontWeight: "700", yearFontWeight: "700",
quoteSourceUrl: "", quoteSourceUrl: "",
quoteSourceUrls: [], quoteSourceUrls: [],
quoteLanguages: ["en", "de"],
}); });
const [motivationalQuote, setMotivationalQuote] = useState(""); const [motivationalQuote, setMotivationalQuote] = useState("");
const [showSummary, setShowSummary] = useState(false); const [showSummary, setShowSummary] = useState(false);
@ -1645,15 +1647,18 @@ export default function WeeklyView() {
} }
} }
// Final fallback: use local curated quotes // Final fallback: use local curated quotes in user-selected languages
const lang = profile.language === "de" ? "de" : "en"; const quoteLangs = profile.quoteLanguages && profile.quoteLanguages.length > 0
const localQuote = getRandomLocalQuote(lang); ? profile.quoteLanguages
: [profile.language || "en"];
const randomLang = quoteLangs[Math.floor(Math.random() * quoteLangs.length)];
const localQuote = getRandomLocalQuote(randomLang);
if (localQuote) { if (localQuote) {
setMotivationalQuote(`${localQuote.text}${localQuote.author}`); setMotivationalQuote(`${localQuote.text}${localQuote.author}`);
} else { } else {
setMotivationalQuote(lang === "de" ? "Bleib fokussiert und produktiv." : "Stay focused and productive."); setMotivationalQuote(randomLang === "de" ? "Bleib fokussiert und produktiv." : "Stay focused and productive.");
} }
}, [profile.goalFallbackType, profile.quoteSourceUrl, profile.quoteSourceUrls, profile.language]); }, [profile.goalFallbackType, profile.quoteSourceUrl, profile.quoteSourceUrls, profile.language, profile.quoteLanguages]);
// Fetch tasks on mount // Fetch tasks on mount
useEffect(() => { useEffect(() => {
@ -8003,9 +8008,11 @@ interface SettingsSidebarProps {
showTaskCheckboxes?: boolean; showTaskCheckboxes?: boolean;
startDayOffset?: number; startDayOffset?: number;
quoteSourceUrls: string[]; quoteSourceUrls: string[];
quoteLanguages: string[];
}) => void; }) => void;
setCurrentWeekStart: (d: Date) => void; setCurrentWeekStart: (d: Date) => void;
quoteSourceUrls?: string[]; quoteSourceUrls?: string[];
quoteLanguages?: string[];
goal: string; goal: string;
setGoal: (goal: string) => void; setGoal: (goal: string) => void;
saveGoal: (goal: string) => void; saveGoal: (goal: string) => void;
@ -8434,6 +8441,7 @@ function SettingsSidebar({
dayHeaderGap?: string; dayHeaderGap?: string;
showTaskCheckboxes?: boolean; showTaskCheckboxes?: boolean;
quoteSourceUrls?: string[]; quoteSourceUrls?: string[];
quoteLanguages?: string[];
startDayOffset?: number; startDayOffset?: number;
id?: string; id?: string;
accountNumber?: number; accountNumber?: number;
@ -11775,8 +11783,82 @@ function SettingsSidebar({
<p style={{ fontSize: "0.75rem", color: "var(--weekly-settings-label)", marginTop: "8px", fontStyle: "italic" }}> <p style={{ fontSize: "0.75rem", color: "var(--weekly-settings-label)", marginTop: "8px", fontStyle: "italic" }}>
{profile.language === "de" {profile.language === "de"
? "Wenn keine externe Quelle antwortet, werden lokale kuratierte Zitate in Ihrer Sprache verwendet." ? "Wenn keine externe Quelle antwortet, werden lokale kuratierte Zitate in Ihrer Sprache verwendet."
: profile.language === "fr"
? "Si aucune source externe ne répond, des citations locales dans votre langue sont utilisées."
: profile.language === "es"
? "Si ninguna fuente externa responde, se usan citas locales en tu idioma."
: profile.language === "it"
? "Se nessuna fonte esterna risponde, vengono usate citazioni locali nella tua lingua."
: "If no external source responds, curated local quotes in your language are used as fallback."} : "If no external source responds, curated local quotes in your language are used as fallback."}
</p> </p>
<div style={{ marginTop: "12px" }}>
<label
style={{
display: "block",
fontSize: "0.9rem",
marginBottom: "6px",
color: "var(--weekly-settings-label)",
}}
>
{profile.language === "de" ? "Zitatsprachen" : profile.language === "fr" ? "Langues des citations" : profile.language === "es" ? "Idiomas de citas" : profile.language === "it" ? "Lingue delle citazioni" : "Quote Languages"}
</label>
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}>
{[
{ code: "en", label: "English" },
{ code: "de", label: "Deutsch" },
{ code: "fr", label: "Français" },
{ code: "es", label: "Español" },
{ code: "it", label: "Italiano" },
].map((lang) => {
const selected = (profile.quoteLanguages || ["en", "de"]).includes(lang.code);
return (
<label
key={lang.code}
style={{
display: "flex",
alignItems: "center",
gap: "4px",
fontSize: "0.85rem",
cursor: "pointer",
padding: "4px 10px",
borderRadius: "6px",
border: selected ? "1px solid var(--weekly-teal)" : "1px solid var(--weekly-border)",
background: selected ? "var(--weekly-teal)" : "transparent",
color: selected ? "white" : "var(--weekly-text)",
transition: "all 0.15s ease",
}}
>
<input
type="checkbox"
checked={selected}
onChange={() => {
const current = profile.quoteLanguages || ["en", "de"];
const updated = selected
? current.filter((c: string) => c !== lang.code)
: [...current, lang.code];
if (updated.length > 0) {
setProfile((p) => ({ ...p, quoteLanguages: updated }));
}
}}
style={{ display: "none" }}
/>
{lang.label}
</label>
);
})}
</div>
<p style={{ fontSize: "0.75rem", color: "var(--weekly-text-light)", marginTop: "4px" }}>
{profile.language === "de"
? "Wählen Sie die Sprachen für Ihre Zitate. Mindestens eine muss ausgewählt sein."
: profile.language === "fr"
? "Choisissez les langues de vos citations. Au moins une doit être sélectionnée."
: profile.language === "es"
? "Elige los idiomas de tus citas. Al menos uno debe estar seleccionado."
: profile.language === "it"
? "Scegli le lingue delle citazioni. Almeno una deve essere selezionata."
: "Choose which languages your quotes appear in. At least one must be selected."}
</p>
</div>
</div> </div>
)} )}
{profile.goalFallbackType === "default" && ( {profile.goalFallbackType === "default" && (

View File

@ -3,7 +3,7 @@
export interface LocalQuote { export interface LocalQuote {
text: string; text: string;
author: string; author: string;
language: "de" | "en"; language: "de" | "en" | "fr" | "es" | "it";
tags: string[]; tags: string[];
} }
@ -525,6 +525,279 @@ export const LOCAL_QUOTES: LocalQuote[] = [
language: "en", language: "en",
tags: ["work", "perseverance", "success"], tags: ["work", "perseverance", "success"],
}, },
// --- French Quotes ---
{
text: "Il n'y a qu'une façon d'échouer, c'est d'abandonner avant d'avoir réussi.",
author: "Olivier Lockert",
language: "fr",
tags: ["perseverance", "motivation"],
},
{
text: "Le succès n'est pas final, l'échec n'est pas fatal : c'est le courage de continuer qui compte.",
author: "Winston Churchill",
language: "fr",
tags: ["perseverance", "success", "motivation"],
},
{
text: "La vie, ce n'est pas d'attendre que les orages passent, c'est d'apprendre à danser sous la pluie.",
author: "Sénèque",
language: "fr",
tags: ["life", "wisdom", "perseverance"],
},
{
text: "Ce n'est pas parce que les choses sont difficiles que nous n'osons pas, c'est parce que nous n'osons pas qu'elles sont difficiles.",
author: "Sénèque",
language: "fr",
tags: ["motivation", "wisdom"],
},
{
text: "Le meilleur moment pour planter un arbre était il y a vingt ans. Le deuxième meilleur moment est maintenant.",
author: "Proverbe chinois",
language: "fr",
tags: ["motivation", "wisdom", "work"],
},
{
text: "Celui qui déplace une montagne commence par déplacer de petites pierres.",
author: "Confucius",
language: "fr",
tags: ["perseverance", "motivation", "work"],
},
{
text: "La simplicité est la sophistication suprême.",
author: "Léonard de Vinci",
language: "fr",
tags: ["wisdom", "creativity"],
},
{
text: "Rien n'est permanent, sauf le changement.",
author: "Héraclite",
language: "fr",
tags: ["wisdom", "life"],
},
{
text: "Fais de ta vie un rêve, et d'un rêve, une réalité.",
author: "Antoine de Saint-Exupéry",
language: "fr",
tags: ["motivation", "life", "creativity"],
},
{
text: "L'imagination est plus importante que le savoir.",
author: "Albert Einstein",
language: "fr",
tags: ["creativity", "wisdom"],
},
{
text: "On ne voit bien qu'avec le cœur. L'essentiel est invisible pour les yeux.",
author: "Antoine de Saint-Exupéry",
language: "fr",
tags: ["wisdom", "life"],
},
{
text: "Le bonheur n'est pas quelque chose de prêt à l'emploi. Il vient de vos propres actions.",
author: "Dalaï Lama",
language: "fr",
tags: ["wisdom", "life", "motivation"],
},
{
text: "Chaque jour est une nouvelle chance de changer ta vie.",
author: "Inconnu",
language: "fr",
tags: ["motivation", "life"],
},
{
text: "Il faut toujours viser la lune, car même en cas d'échec, on atterrit dans les étoiles.",
author: "Oscar Wilde",
language: "fr",
tags: ["motivation", "success"],
},
{
text: "La créativité, c'est l'intelligence qui s'amuse.",
author: "Albert Einstein",
language: "fr",
tags: ["creativity", "humor"],
},
// --- Spanish Quotes ---
{
text: "No es la especie más fuerte la que sobrevive, sino la que mejor se adapta al cambio.",
author: "Charles Darwin",
language: "es",
tags: ["wisdom", "perseverance", "life"],
},
{
text: "El único modo de hacer un gran trabajo es amar lo que haces.",
author: "Steve Jobs",
language: "es",
tags: ["work", "motivation", "success"],
},
{
text: "La vida es lo que pasa mientras estás ocupado haciendo otros planes.",
author: "John Lennon",
language: "es",
tags: ["life", "wisdom"],
},
{
text: "Sé tú mismo; todos los demás ya están ocupados.",
author: "Oscar Wilde",
language: "es",
tags: ["life", "wisdom", "humor"],
},
{
text: "El mejor momento para plantar un árbol fue hace veinte años. El segundo mejor momento es ahora.",
author: "Proverbio chino",
language: "es",
tags: ["motivation", "wisdom", "work"],
},
{
text: "La imaginación es más importante que el conocimiento.",
author: "Albert Einstein",
language: "es",
tags: ["creativity", "wisdom"],
},
{
text: "No importa lo lento que vayas, siempre y cuando no te detengas.",
author: "Confucio",
language: "es",
tags: ["perseverance", "motivation"],
},
{
text: "El secreto de ir adelante es empezar.",
author: "Mark Twain",
language: "es",
tags: ["motivation", "work", "success"],
},
{
text: "La felicidad de tu vida depende de la calidad de tus pensamientos.",
author: "Marco Aurelio",
language: "es",
tags: ["wisdom", "life"],
},
{
text: "Sé el cambio que deseas ver en el mundo.",
author: "Mahatma Gandhi",
language: "es",
tags: ["wisdom", "life", "motivation"],
},
{
text: "La creatividad es la inteligencia divirtiéndose.",
author: "Albert Einstein",
language: "es",
tags: ["creativity", "humor"],
},
{
text: "Cada día es una nueva oportunidad para cambiar tu vida.",
author: "Desconocido",
language: "es",
tags: ["motivation", "life"],
},
{
text: "Lo que no te mata, te hace más fuerte.",
author: "Friedrich Nietzsche",
language: "es",
tags: ["perseverance", "motivation"],
},
{
text: "Haz lo que puedas, con lo que tengas, donde estés.",
author: "Theodore Roosevelt",
language: "es",
tags: ["motivation", "work", "perseverance"],
},
{
text: "Grandes cosas nunca vienen de la zona de confort.",
author: "Desconocido",
language: "es",
tags: ["motivation", "perseverance", "success"],
},
// --- Italian Quotes ---
{
text: "La semplicità è la raffinatezza suprema.",
author: "Leonardo da Vinci",
language: "it",
tags: ["wisdom", "creativity"],
},
{
text: "Nel mezzo delle difficoltà nascono le opportunità.",
author: "Albert Einstein",
language: "it",
tags: ["perseverance", "motivation", "success"],
},
{
text: "L'unico modo di fare un ottimo lavoro è amare quello che fai.",
author: "Steve Jobs",
language: "it",
tags: ["work", "motivation", "success"],
},
{
text: "Chi non osa nulla, non speri nulla.",
author: "Friedrich Schiller",
language: "it",
tags: ["motivation", "perseverance"],
},
{
text: "La vita è quello che ti accade mentre sei impegnato a fare altri progetti.",
author: "John Lennon",
language: "it",
tags: ["life", "wisdom"],
},
{
text: "Sii il cambiamento che vuoi vedere nel mondo.",
author: "Mahatma Gandhi",
language: "it",
tags: ["wisdom", "life", "motivation"],
},
{
text: "Il miglior momento per piantare un albero era vent'anni fa. Il secondo miglior momento è adesso.",
author: "Proverbio cinese",
language: "it",
tags: ["motivation", "wisdom", "work"],
},
{
text: "Non importa quanto vai piano, l'importante è non fermarsi.",
author: "Confucio",
language: "it",
tags: ["perseverance", "motivation"],
},
{
text: "Il segreto per andare avanti è iniziare.",
author: "Mark Twain",
language: "it",
tags: ["motivation", "work", "success"],
},
{
text: "La fantasia è più importante del sapere.",
author: "Albert Einstein",
language: "it",
tags: ["creativity", "wisdom"],
},
{
text: "Quello che non ti uccide ti rende più forte.",
author: "Friedrich Nietzsche",
language: "it",
tags: ["perseverance", "motivation"],
},
{
text: "La creatività è l'intelligenza che si diverte.",
author: "Albert Einstein",
language: "it",
tags: ["creativity", "humor"],
},
{
text: "Ogni giorno è un nuovo inizio.",
author: "T.S. Eliot",
language: "it",
tags: ["motivation", "life"],
},
{
text: "Fai quel che puoi, con quel che hai, dove sei.",
author: "Theodore Roosevelt",
language: "it",
tags: ["motivation", "work", "perseverance"],
},
{
text: "Le grandi cose non vengono mai dalla zona di comfort.",
author: "Sconosciuto",
language: "it",
tags: ["motivation", "perseverance", "success"],
},
]; ];
/** /**