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 +}