diff --git a/api/src/routes/auth.js b/api/src/routes/auth.js
index 07cc3aa..aee64f2 100644
--- a/api/src/routes/auth.js
+++ b/api/src/routes/auth.js
@@ -6,6 +6,8 @@ async function authRoutes(app) {
app.post('/auth/register', async (req) => {
const { email, name, phone, password, language } = req.body;
if (!email || !name || !password) throw { statusCode: 400, message: 'Email, name and password required' };
+ if (typeof email !== 'string' || !email.includes('@') || email.length < 5) throw { statusCode: 400, message: 'Invalid email address' };
+ if (typeof name !== 'string' || name.trim().length < 2) throw { statusCode: 400, message: 'Name must be at least 2 characters' };
if (password.length < 6) throw { statusCode: 400, message: 'Password must be at least 6 characters' };
// Check if email exists
@@ -92,6 +94,26 @@ async function authRoutes(app) {
// TODO: Implement OAuth flows
return { status: 'not_implemented', provider, message: 'OAuth coming soon. Use email/password.' };
});
+
+ // TOTP 2FA setup
+ app.post('/auth/2fa/setup', { preHandler: [async (req) => { await req.jwtVerify(); }] }, async (req) => {
+ // Generate TOTP secret (placeholder - needs speakeasy package)
+ return { status: 'not_implemented', message: '2FA coming in next release. Use password auth.' };
+ });
+
+ // WebAuthn registration (placeholder)
+ app.post('/auth/webauthn/register', async (req) => {
+ return { status: 'not_implemented', message: 'WebAuthn biometric auth coming soon.' };
+ });
+
+ // OAuth initiate (placeholder)
+ app.get('/auth/oauth/:provider/init', async (req) => {
+ const { provider } = req.params;
+ const providers = ['google', 'facebook', 'apple'];
+ if (!providers.includes(provider)) throw { statusCode: 400, message: 'Unknown provider' };
+ return { status: 'not_implemented', provider, message: `${provider} OAuth coming soon. Configure at Settings > Connections.` };
+ });
+
}
module.exports = authRoutes;
diff --git a/apps/tasks/app/forgot-password/page.tsx b/apps/tasks/app/forgot-password/page.tsx
index c470536..4960790 100644
--- a/apps/tasks/app/forgot-password/page.tsx
+++ b/apps/tasks/app/forgot-password/page.tsx
@@ -12,7 +12,7 @@ export default function ForgotPasswordPage() {
{t("auth.forgotPassword")}
- {t("common.loading")}...
+ {t("forgotPassword.description")}
{t("common.back")}
diff --git a/apps/tasks/app/globals.css b/apps/tasks/app/globals.css
index f28811b..f7b7539 100644
--- a/apps/tasks/app/globals.css
+++ b/apps/tasks/app/globals.css
@@ -230,6 +230,24 @@ main {
}
}
+
+/* Mobile modal fix */
+.modal-content {
+ max-height: 90vh;
+ max-height: 90dvh; /* dynamic viewport height */
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+ overscroll-behavior: contain;
+}
+
+/* Ensure modal buttons visible above keyboard */
+@media (max-width: 640px) {
+ .modal-content {
+ max-height: 85vh;
+ max-height: 85dvh;
+ }
+}
+
/* Selection color */
::selection {
background: rgba(59, 130, 246, 0.3);
diff --git a/apps/tasks/app/login/page.tsx b/apps/tasks/app/login/page.tsx
index e380d79..be1f775 100644
--- a/apps/tasks/app/login/page.tsx
+++ b/apps/tasks/app/login/page.tsx
@@ -56,7 +56,7 @@ export default function LoginPage() {
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
- placeholder="vas@email.cz"
+ placeholder={t("auth.emailPlaceholder")}
autoFocus
autoComplete="email"
/>
diff --git a/apps/tasks/app/projects/page.tsx b/apps/tasks/app/projects/page.tsx
index fa89d05..251af6f 100644
--- a/apps/tasks/app/projects/page.tsx
+++ b/apps/tasks/app/projects/page.tsx
@@ -13,7 +13,7 @@ import {
export default function ProjectsPage() {
const { token, user } = useAuth();
- const { t } = useTranslation();
+ const { t, locale } = useTranslation();
const router = useRouter();
const [projects, setProjects] = useState
([]);
const [loading, setLoading] = useState(true);
@@ -86,8 +86,10 @@ export default function ProjectsPage() {
}
}
+ const dateLocale = locale === "ua" ? "uk-UA" : locale === "cs" ? "cs-CZ" : locale === "he" ? "he-IL" : "ru-RU";
+
function formatDate(d: string) {
- return new Date(d).toLocaleDateString("cs-CZ", { day: "numeric", month: "short" });
+ return new Date(d).toLocaleDateString(dateLocale, { day: "numeric", month: "short" });
}
if (!token) return null;
diff --git a/apps/tasks/app/register/page.tsx b/apps/tasks/app/register/page.tsx
index e4d1483..574ea22 100644
--- a/apps/tasks/app/register/page.tsx
+++ b/apps/tasks/app/register/page.tsx
@@ -113,7 +113,7 @@ export default function RegisterPage() {
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
- placeholder="+420 123 456 789"
+ placeholder={t("auth.phonePlaceholder")}
/>
diff --git a/apps/tasks/app/settings/page.tsx b/apps/tasks/app/settings/page.tsx
index ec94cb6..b2e0629 100644
--- a/apps/tasks/app/settings/page.tsx
+++ b/apps/tasks/app/settings/page.tsx
@@ -184,7 +184,7 @@ export default function SettingsPage() {
{/* App info */}
-
Task Team v0.1.0
+
{t("common.appName")} {t("common.appVersion")}
);
diff --git a/apps/tasks/components/Header.tsx b/apps/tasks/components/Header.tsx
index a0041aa..66884ec 100644
--- a/apps/tasks/components/Header.tsx
+++ b/apps/tasks/components/Header.tsx
@@ -48,7 +48,7 @@ export default function Header() {
- Task Team
+ {t("common.appName")}
{/* Right: Desktop controls */}
diff --git a/apps/tasks/components/TaskForm.tsx b/apps/tasks/components/TaskForm.tsx
index ead76db..369ccdf 100644
--- a/apps/tasks/components/TaskForm.tsx
+++ b/apps/tasks/components/TaskForm.tsx
@@ -170,7 +170,7 @@ export default function TaskForm({
-
+
);
diff --git a/apps/tasks/lib/i18n.tsx b/apps/tasks/lib/i18n.tsx
index 85b09fc..ac63386 100644
--- a/apps/tasks/lib/i18n.tsx
+++ b/apps/tasks/lib/i18n.tsx
@@ -57,13 +57,11 @@ export function I18nProvider({ children }: { children: ReactNode }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
- setMounted(true);
- if (typeof window !== "undefined") {
- const stored = localStorage.getItem(STORAGE_KEY) as Locale | null;
- if (stored && MESSAGES[stored]) {
- setLocaleState(stored);
- }
+ const stored = localStorage.getItem(STORAGE_KEY) as Locale | null;
+ if (stored && MESSAGES[stored]) {
+ setLocaleState(stored);
}
+ setMounted(true);
}, []);
const setLocale = useCallback((newLocale: Locale) => {
@@ -73,29 +71,32 @@ export function I18nProvider({ children }: { children: ReactNode }) {
}
}, []);
+ // Always use "cs" for translations until mounted to match SSR output
+ const activeLocale = mounted ? locale : "cs";
+
const t = useCallback(
(key: string): string => {
- const value = getNestedValue(MESSAGES[locale], key);
+ const value = getNestedValue(MESSAGES[activeLocale], key);
if (value !== undefined) return value;
// Fallback: return last segment of the key (the raw value) instead of "undefined"
return key.split(".").pop() || key;
},
- [locale]
+ [activeLocale]
);
- const localeInfo = LOCALES.find((l) => l.code === locale) || LOCALES[0];
+ const localeInfo = LOCALES.find((l) => l.code === activeLocale) || LOCALES[0];
const dir = localeInfo.dir;
const isRTL = dir === "rtl";
- // Update html attributes when locale changes
+ // Update html attributes when locale changes (only after mount)
useEffect(() => {
if (!mounted) return;
document.documentElement.lang = locale;
- document.documentElement.dir = dir;
- }, [locale, dir, mounted]);
+ document.documentElement.dir = LOCALES.find((l) => l.code === locale)?.dir || "ltr";
+ }, [locale, mounted]);
return (
-
+
{children}
);
diff --git a/apps/tasks/messages/cs.json b/apps/tasks/messages/cs.json
index 15a9506..e645467 100644
--- a/apps/tasks/messages/cs.json
+++ b/apps/tasks/messages/cs.json
@@ -31,7 +31,9 @@
"good": "Dobré",
"strong": "Silné",
"excellent": "Výborné"
- }
+ },
+ "emailPlaceholder": "vas@email.cz",
+ "phonePlaceholder": "+420 123 456 789"
},
"tasks": {
"title": "Úkoly",
@@ -128,7 +130,10 @@
"confirm": "Potvrdit",
"menu": "Menu",
"closeMenu": "Zavřít menu",
- "toggleTheme": "Přepnout téma"
+ "toggleTheme": "Přepnout téma",
+ "appName": "Task Team",
+ "appVersion": "v0.1.0",
+ "appDescription": "Sprava ukolu pro tym"
},
"calendar": {
"title": "Kalendář"
@@ -142,6 +147,10 @@
"color": "Barva",
"icon": "Ikona",
"tasks": "Ukoly",
- "members": "Clenove"
+ "members": "Clenove",
+ "title": "Projekty"
+ },
+ "forgotPassword": {
+ "description": "Obnova hesla bude brzy k dispozici."
}
-}
\ No newline at end of file
+}
diff --git a/apps/tasks/messages/he.json b/apps/tasks/messages/he.json
index dcb35be..d368c2c 100644
--- a/apps/tasks/messages/he.json
+++ b/apps/tasks/messages/he.json
@@ -31,7 +31,9 @@
"good": "טובה",
"strong": "חזקה",
"excellent": "מצוינת"
- }
+ },
+ "emailPlaceholder": "your@email.com",
+ "phonePlaceholder": "+972 50 123 4567"
},
"tasks": {
"title": "משימות",
@@ -128,7 +130,10 @@
"confirm": "אישור",
"menu": "תפריט",
"closeMenu": "סגור תפריט",
- "toggleTheme": "החלף ערכת נושא"
+ "toggleTheme": "החלף ערכת נושא",
+ "appName": "Task Team",
+ "appVersion": "v0.1.0",
+ "appDescription": "ניהול משימות לצוות"
},
"calendar": {
"title": "לוח שנה"
@@ -142,6 +147,10 @@
"color": "צבע",
"icon": "איקון",
"tasks": "משימות",
- "members": "חברים"
+ "members": "חברים",
+ "title": "פרויקטים"
+ },
+ "forgotPassword": {
+ "description": "שחזור סיסמה יהיה זמין בקרוב."
}
-}
\ No newline at end of file
+}
diff --git a/apps/tasks/messages/ru.json b/apps/tasks/messages/ru.json
index 0a10493..8156cd8 100644
--- a/apps/tasks/messages/ru.json
+++ b/apps/tasks/messages/ru.json
@@ -31,7 +31,9 @@
"good": "Хороший",
"strong": "Сильный",
"excellent": "Отличный"
- }
+ },
+ "emailPlaceholder": "vas@email.ru",
+ "phonePlaceholder": "+7 999 123 4567"
},
"tasks": {
"title": "Задачи",
@@ -128,7 +130,10 @@
"confirm": "Подтвердить",
"menu": "Меню",
"closeMenu": "Закрыть меню",
- "toggleTheme": "Переключить тему"
+ "toggleTheme": "Переключить тему",
+ "appName": "Task Team",
+ "appVersion": "v0.1.0",
+ "appDescription": "Управление задачами для команды"
},
"calendar": {
"title": "Календарь"
@@ -142,6 +147,10 @@
"color": "Цвет",
"icon": "Иконка",
"tasks": "Задачи",
- "members": "Участники"
+ "members": "Участники",
+ "title": "Проекты"
+ },
+ "forgotPassword": {
+ "description": "Восстановление пароля будет доступно в ближайшее время."
}
-}
\ No newline at end of file
+}
diff --git a/apps/tasks/messages/ua.json b/apps/tasks/messages/ua.json
index 8fcb248..e2e3e1c 100644
--- a/apps/tasks/messages/ua.json
+++ b/apps/tasks/messages/ua.json
@@ -31,7 +31,9 @@
"good": "Добрий",
"strong": "Сильний",
"excellent": "Відмінний"
- }
+ },
+ "emailPlaceholder": "vas@email.ua",
+ "phonePlaceholder": "+380 99 123 4567"
},
"tasks": {
"title": "Завдання",
@@ -128,7 +130,10 @@
"confirm": "Підтвердити",
"menu": "Меню",
"closeMenu": "Закрити меню",
- "toggleTheme": "Перемкнути тему"
+ "toggleTheme": "Перемкнути тему",
+ "appName": "Task Team",
+ "appVersion": "v0.1.0",
+ "appDescription": "Управлiння завданнями для команди"
},
"calendar": {
"title": "Календар"
@@ -142,6 +147,10 @@
"color": "Колір",
"icon": "Іконка",
"tasks": "Завдання",
- "members": "Учасники"
+ "members": "Учасники",
+ "title": "Проекти"
+ },
+ "forgotPassword": {
+ "description": "Вiдновлення паролю буде доступне найближчим часом."
}
-}
\ No newline at end of file
+}