LoginPage.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import { useState } from 'react';
  2. import { useNavigate } from 'react-router-dom';
  3. import { useMutation } from '@tanstack/react-query';
  4. import { useTranslation } from 'react-i18next';
  5. import { useAuth } from '../contexts/AuthContext';
  6. import { useToast } from '../contexts/ToastContext';
  7. import { useTheme } from '../contexts/ThemeContext';
  8. import { HelpCircle, X } from 'lucide-react';
  9. export function LoginPage() {
  10. const navigate = useNavigate();
  11. const { t } = useTranslation();
  12. const { login } = useAuth();
  13. const { showToast } = useToast();
  14. const { mode } = useTheme();
  15. const [username, setUsername] = useState('');
  16. const [password, setPassword] = useState('');
  17. const [showForgotPassword, setShowForgotPassword] = useState(false);
  18. const loginMutation = useMutation({
  19. mutationFn: () => login(username, password),
  20. onSuccess: () => {
  21. showToast(t('login.loginSuccess'));
  22. navigate('/');
  23. },
  24. onError: (error: Error) => {
  25. showToast(error.message || t('login.loginFailed'), 'error');
  26. },
  27. });
  28. const handleSubmit = (e: React.FormEvent) => {
  29. e.preventDefault();
  30. if (!username || !password) {
  31. showToast(t('login.enterCredentials'), 'error');
  32. return;
  33. }
  34. loginMutation.mutate();
  35. };
  36. return (
  37. <div className="min-h-screen flex items-center justify-center bg-bambu-dark p-4">
  38. <div className="max-w-md w-full space-y-8 p-8 bg-gradient-to-br from-bambu-card to-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary shadow-lg">
  39. <div className="text-center">
  40. <div className="flex items-center justify-center mb-6">
  41. <img
  42. src={mode === 'dark' ? '/img/bambuddy_logo_dark_transparent.png' : '/img/bambuddy_logo_light.png'}
  43. alt="Bambuddy"
  44. className="h-16"
  45. />
  46. </div>
  47. <h2 className="text-3xl font-bold text-white">
  48. {t('login.title')}
  49. </h2>
  50. <p className="mt-2 text-sm text-bambu-gray">
  51. {t('login.subtitle')}
  52. </p>
  53. </div>
  54. <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
  55. <div className="space-y-4">
  56. <div>
  57. <label htmlFor="username" className="block text-sm font-medium text-white mb-2">
  58. {t('login.username')}
  59. </label>
  60. <input
  61. id="username"
  62. type="text"
  63. required
  64. value={username}
  65. onChange={(e) => setUsername(e.target.value)}
  66. className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
  67. placeholder={t('login.usernamePlaceholder')}
  68. autoComplete="username"
  69. />
  70. </div>
  71. <div>
  72. <label htmlFor="password" className="block text-sm font-medium text-white mb-2">
  73. {t('login.password')}
  74. </label>
  75. <input
  76. id="password"
  77. type="password"
  78. required
  79. value={password}
  80. onChange={(e) => setPassword(e.target.value)}
  81. className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
  82. placeholder={t('login.passwordPlaceholder')}
  83. autoComplete="current-password"
  84. />
  85. </div>
  86. </div>
  87. <div>
  88. <button
  89. type="submit"
  90. disabled={loginMutation.isPending}
  91. className="w-full flex justify-center py-3 px-4 bg-bambu-green hover:bg-bambu-green-light text-white font-medium rounded-lg shadow-lg shadow-bambu-green/20 hover:shadow-bambu-green/30 focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:ring-offset-2 focus:ring-offset-bambu-dark-secondary transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-bambu-green"
  92. >
  93. {loginMutation.isPending ? t('login.signingIn') : t('login.signIn')}
  94. </button>
  95. </div>
  96. <div className="text-center">
  97. <button
  98. type="button"
  99. onClick={() => setShowForgotPassword(true)}
  100. className="text-sm text-bambu-gray hover:text-bambu-green transition-colors"
  101. >
  102. {t('login.forgotPassword')}
  103. </button>
  104. </div>
  105. </form>
  106. </div>
  107. {/* Forgot Password Modal */}
  108. {showForgotPassword && (
  109. <div
  110. className="fixed inset-0 bg-black flex items-center justify-center z-50 p-4"
  111. onClick={() => setShowForgotPassword(false)}
  112. >
  113. <div
  114. className="w-full max-w-md bg-bambu-card rounded-xl border border-bambu-dark-tertiary shadow-lg p-6"
  115. onClick={(e) => e.stopPropagation()}
  116. >
  117. <div className="flex items-center justify-between mb-4">
  118. <div className="flex items-center gap-2">
  119. <HelpCircle className="w-5 h-5 text-bambu-green" />
  120. <h2 className="text-lg font-semibold text-white">{t('login.forgotPasswordTitle')}</h2>
  121. </div>
  122. <button
  123. onClick={() => setShowForgotPassword(false)}
  124. className="p-1 rounded-lg hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white transition-colors"
  125. >
  126. <X className="w-5 h-5" />
  127. </button>
  128. </div>
  129. <div className="space-y-4">
  130. <p className="text-bambu-gray">
  131. {t('login.forgotPasswordMessage')}
  132. </p>
  133. <div className="bg-bambu-dark rounded-lg p-4 space-y-2">
  134. <p className="text-sm text-white font-medium">{t('login.howToReset')}</p>
  135. <ol className="text-sm text-bambu-gray space-y-1 list-decimal list-inside">
  136. <li>{t('login.resetStep1')}</li>
  137. <li>{t('login.resetStep2')}</li>
  138. <li>{t('login.resetStep3')}</li>
  139. <li>{t('login.resetStep4')}</li>
  140. </ol>
  141. </div>
  142. <button
  143. onClick={() => setShowForgotPassword(false)}
  144. className="w-full py-2 px-4 bg-bambu-dark-tertiary hover:bg-bambu-dark text-white rounded-lg transition-colors"
  145. >
  146. {t('login.gotIt')}
  147. </button>
  148. </div>
  149. </div>
  150. </div>
  151. )}
  152. </div>
  153. );
  154. }