Backend is here. Default admin is created if needed.
This commit is contained in:
@@ -29,14 +29,14 @@ const AICoach: React.FC<AICoachProps> = ({ history, lang }) => {
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const chat = createFitnessChat(history);
|
||||
if (chat) {
|
||||
chatSessionRef.current = chat;
|
||||
} else {
|
||||
setError(t('ai_error', lang));
|
||||
}
|
||||
const chat = createFitnessChat(history);
|
||||
if (chat) {
|
||||
chatSessionRef.current = chat;
|
||||
} else {
|
||||
setError(t('ai_error', lang));
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Failed to initialize AI");
|
||||
setError("Failed to initialize AI");
|
||||
}
|
||||
}, [history, lang]);
|
||||
|
||||
@@ -59,28 +59,38 @@ const AICoach: React.FC<AICoachProps> = ({ history, lang }) => {
|
||||
try {
|
||||
const result: GenerateContentResponse = await chatSessionRef.current.sendMessage({ message: userMsg.text });
|
||||
const text = result.text;
|
||||
|
||||
const aiMsg: Message = {
|
||||
id: crypto.randomUUID(),
|
||||
role: 'model',
|
||||
text: text || "Error generating response."
|
||||
|
||||
const aiMsg: Message = {
|
||||
id: crypto.randomUUID(),
|
||||
role: 'model',
|
||||
text: text || "Error generating response."
|
||||
};
|
||||
setMessages(prev => [...prev, aiMsg]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setMessages(prev => [...prev, { id: crypto.randomUUID(), role: 'model', text: 'Connection error.' }]);
|
||||
let errorText = 'Connection error.';
|
||||
if (err instanceof Error) {
|
||||
try {
|
||||
const json = JSON.parse(err.message);
|
||||
if (json.error) errorText = json.error;
|
||||
else errorText = err.message;
|
||||
} catch {
|
||||
errorText = err.message;
|
||||
}
|
||||
}
|
||||
setMessages(prev => [...prev, { id: crypto.randomUUID(), role: 'model', text: errorText }]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full p-6 text-center text-on-surface-variant">
|
||||
<AlertTriangle size={48} className="text-error mb-4" />
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full p-6 text-center text-on-surface-variant">
|
||||
<AlertTriangle size={48} className="text-error mb-4" />
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -88,7 +98,7 @@ const AICoach: React.FC<AICoachProps> = ({ history, lang }) => {
|
||||
{/* Header */}
|
||||
<div className="p-4 bg-surface-container shadow-elevation-1 flex items-center gap-3 z-10">
|
||||
<div className="w-10 h-10 rounded-full bg-secondary-container flex items-center justify-center">
|
||||
<Bot size={20} className="text-on-secondary-container" />
|
||||
<Bot size={20} className="text-on-secondary-container" />
|
||||
</div>
|
||||
<h2 className="text-xl font-normal text-on-surface">{t('ai_expert', lang)}</h2>
|
||||
</div>
|
||||
@@ -97,22 +107,21 @@ const AICoach: React.FC<AICoachProps> = ({ history, lang }) => {
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4 pb-4">
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[85%] p-4 rounded-[20px] text-sm leading-relaxed shadow-sm ${
|
||||
msg.role === 'user'
|
||||
? 'bg-primary text-on-primary rounded-br-none'
|
||||
<div className={`max-w-[85%] p-4 rounded-[20px] text-sm leading-relaxed shadow-sm ${msg.role === 'user'
|
||||
? 'bg-primary text-on-primary rounded-br-none'
|
||||
: 'bg-surface-container-high text-on-surface border border-outline-variant/20 rounded-bl-none'
|
||||
}`}>
|
||||
}`}>
|
||||
{msg.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{loading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-surface-container-high px-4 py-3 rounded-[20px] rounded-bl-none flex gap-2 items-center text-on-surface-variant text-sm">
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
{t('ai_typing', lang)}
|
||||
</div>
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-surface-container-high px-4 py-3 rounded-[20px] rounded-bl-none flex gap-2 items-center text-on-surface-variant text-sm">
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
{t('ai_typing', lang)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
@@ -120,21 +129,21 @@ const AICoach: React.FC<AICoachProps> = ({ history, lang }) => {
|
||||
{/* Input */}
|
||||
<div className="p-4 bg-surface-container mt-auto">
|
||||
<div className="flex gap-2 items-center bg-surface-container-high rounded-full border border-outline-variant px-2 py-1">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 bg-transparent border-none px-4 py-3 text-on-surface focus:outline-none placeholder-on-surface-variant"
|
||||
placeholder={t('ai_placeholder', lang)}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={loading || !input.trim()}
|
||||
className="w-10 h-10 bg-primary rounded-full flex items-center justify-center text-on-primary hover:opacity-90 disabled:opacity-50 transition-opacity shrink-0"
|
||||
>
|
||||
<Send size={18} />
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 bg-transparent border-none px-4 py-3 text-on-surface focus:outline-none placeholder-on-surface-variant"
|
||||
placeholder={t('ai_placeholder', lang)}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={loading || !input.trim()}
|
||||
className="w-10 h-10 bg-primary rounded-full flex items-center justify-center text-on-primary hover:opacity-90 disabled:opacity-50 transition-opacity shrink-0"
|
||||
>
|
||||
<Send size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { login, changePassword } from '../services/auth';
|
||||
import { User, Language } from '../types';
|
||||
@@ -15,15 +14,16 @@ const Login: React.FC<LoginProps> = ({ onLogin, language, onLanguageChange }) =>
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
|
||||
// Force Password Change State
|
||||
const [needsChange, setNeedsChange] = useState(false);
|
||||
const [tempUser, setTempUser] = useState<User | null>(null);
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
|
||||
const handleLogin = (e: React.FormEvent) => {
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const res = login(email, password);
|
||||
|
||||
const res = await login(email, password);
|
||||
if (res.success && res.user) {
|
||||
if (res.user.isFirstLogin) {
|
||||
setTempUser(res.user);
|
||||
@@ -42,104 +42,104 @@ const Login: React.FC<LoginProps> = ({ onLogin, language, onLanguageChange }) =>
|
||||
const updatedUser = { ...tempUser, isFirstLogin: false };
|
||||
onLogin(updatedUser);
|
||||
} else {
|
||||
setError(t('login_password_short', language));
|
||||
setError(t('login_password_short', language));
|
||||
}
|
||||
};
|
||||
|
||||
if (needsChange) {
|
||||
return (
|
||||
<div className="h-screen bg-surface flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-sm bg-surface-container p-8 rounded-[28px] shadow-elevation-2">
|
||||
<h2 className="text-2xl text-on-surface mb-2">{t('change_pass_title', language)}</h2>
|
||||
<p className="text-sm text-on-surface-variant mb-6">{t('change_pass_desc', language)}</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-4 py-2">
|
||||
<label className="text-[10px] text-on-surface-variant font-medium">{t('change_pass_new', language)}</label>
|
||||
<input
|
||||
type="password"
|
||||
className="w-full bg-transparent text-lg text-on-surface focus:outline-none pt-1"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleChangePassword}
|
||||
className="w-full py-3 bg-primary text-on-primary rounded-full font-medium"
|
||||
>
|
||||
{t('change_pass_save', language)}
|
||||
</button>
|
||||
</div>
|
||||
return (
|
||||
<div className="h-screen bg-surface flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-sm bg-surface-container p-8 rounded-[28px] shadow-elevation-2">
|
||||
<h2 className="text-2xl text-on-surface mb-2">{t('change_pass_title', language)}</h2>
|
||||
<p className="text-sm text-on-surface-variant mb-6">{t('change_pass_desc', language)}</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-4 py-2">
|
||||
<label className="text-[10px] text-on-surface-variant font-medium">{t('change_pass_new', language)}</label>
|
||||
<input
|
||||
type="password"
|
||||
className="w-full bg-transparent text-lg text-on-surface focus:outline-none pt-1"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleChangePassword}
|
||||
className="w-full py-3 bg-primary text-on-primary rounded-full font-medium"
|
||||
>
|
||||
{t('change_pass_save', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-surface flex flex-col items-center justify-center p-6 relative">
|
||||
|
||||
|
||||
{/* Language Toggle */}
|
||||
<div className="absolute top-6 right-6 flex items-center bg-surface-container rounded-full px-3 py-1 gap-2 border border-outline-variant/20">
|
||||
<Globe size={16} className="text-on-surface-variant" />
|
||||
<select
|
||||
value={language}
|
||||
onChange={(e) => onLanguageChange(e.target.value as Language)}
|
||||
className="bg-transparent text-sm text-on-surface focus:outline-none appearance-none"
|
||||
>
|
||||
<option value="en">English</option>
|
||||
<option value="ru">Русский</option>
|
||||
</select>
|
||||
<Globe size={16} className="text-on-surface-variant" />
|
||||
<select
|
||||
value={language}
|
||||
onChange={(e) => onLanguageChange(e.target.value as Language)}
|
||||
className="bg-transparent text-sm text-on-surface focus:outline-none appearance-none"
|
||||
>
|
||||
<option value="en">English</option>
|
||||
<option value="ru">Русский</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center mb-12">
|
||||
<div className="w-20 h-20 bg-primary-container rounded-2xl flex items-center justify-center text-on-primary-container mb-4 shadow-elevation-2 transform rotate-3">
|
||||
<Dumbbell size={40} />
|
||||
<Dumbbell size={40} />
|
||||
</div>
|
||||
<h1 className="text-3xl font-normal text-on-surface tracking-tight">{t('login_title', language)}</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="w-full max-w-sm space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="group bg-surface-container-high rounded-t-lg border-b border-outline-variant focus-within:border-primary transition-colors">
|
||||
<div className="flex items-center px-4 pt-4">
|
||||
<Mail size={16} className="text-on-surface-variant mr-2" />
|
||||
<label className="text-xs text-on-surface-variant font-medium">{t('login_email', language)}</label>
|
||||
</div>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full bg-transparent px-4 pb-3 pt-1 text-lg text-on-surface focus:outline-none"
|
||||
placeholder="user@gymflow.ai"
|
||||
/>
|
||||
<div className="group bg-surface-container-high rounded-t-lg border-b border-outline-variant focus-within:border-primary transition-colors">
|
||||
<div className="flex items-center px-4 pt-4">
|
||||
<Mail size={16} className="text-on-surface-variant mr-2" />
|
||||
<label className="text-xs text-on-surface-variant font-medium">{t('login_email', language)}</label>
|
||||
</div>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full bg-transparent px-4 pb-3 pt-1 text-lg text-on-surface focus:outline-none"
|
||||
placeholder="user@gymflow.ai"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="group bg-surface-container-high rounded-t-lg border-b border-outline-variant focus-within:border-primary transition-colors">
|
||||
<div className="flex items-center px-4 pt-4">
|
||||
<Lock size={16} className="text-on-surface-variant mr-2" />
|
||||
<label className="text-xs text-on-surface-variant font-medium">{t('login_password', language)}</label>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full bg-transparent px-4 pb-3 pt-1 text-lg text-on-surface focus:outline-none"
|
||||
/>
|
||||
<div className="group bg-surface-container-high rounded-t-lg border-b border-outline-variant focus-within:border-primary transition-colors">
|
||||
<div className="flex items-center px-4 pt-4">
|
||||
<Lock size={16} className="text-on-surface-variant mr-2" />
|
||||
<label className="text-xs text-on-surface-variant font-medium">{t('login_password', language)}</label>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full bg-transparent px-4 pb-3 pt-1 text-lg text-on-surface focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="text-error text-sm text-center bg-error-container/10 p-2 rounded-lg">{error}</div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-4 bg-primary text-on-primary rounded-full font-medium text-lg shadow-elevation-1 flex items-center justify-center gap-2 hover:shadow-elevation-2 transition-all"
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-4 bg-primary text-on-primary rounded-full font-medium text-lg shadow-elevation-1 flex items-center justify-center gap-2 hover:shadow-elevation-2 transition-all"
|
||||
>
|
||||
{t('login_btn', language)} <ArrowRight size={20} />
|
||||
{t('login_btn', language)} <ArrowRight size={20} />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-8 text-xs text-on-surface-variant text-center max-w-xs">
|
||||
{t('login_contact_admin', language)}
|
||||
|
||||
<p className="mt-8 text-xs text-on-surface-variant text-center max-w-xs mx-auto">
|
||||
{t('login_contact_admin', language)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user