← back to Ken

react-dash/src/Login.jsx

169 lines

import React, { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { login, checkSession, googleLogin, getGoogleConfig } from './api';

export default function Login() {
  const [user, setUser] = useState('');
  const [pass, setPass] = useState('');
  const [showPass, setShowPass] = useState(false);
  const [err, setErr] = useState('');
  const [loading, setLoading] = useState(false);
  const [checking, setChecking] = useState(true);
  const [googleReady, setGoogleReady] = useState(false);
  const [googleClientId, setGoogleClientId] = useState(null);
  const googleBtnRef = useRef(null);
  const nav = useNavigate();

  useEffect(() => {
    checkSession().then(ok => {
      if (ok) nav('/');
      else setChecking(false);
    });
    // Load Google config
    getGoogleConfig().then(cfg => {
      if (cfg.configured && cfg.client_id) {
        setGoogleClientId(cfg.client_id);
        loadGoogleScript(cfg.client_id);
      }
    });
  }, []);

  function loadGoogleScript(clientId) {
    if (document.getElementById('google-gsi')) return;
    const script = document.createElement('script');
    script.id = 'google-gsi';
    script.src = 'https://accounts.google.com/gsi/client';
    script.async = true;
    script.defer = true;
    script.onload = () => initGoogle(clientId);
    document.head.appendChild(script);
  }

  function initGoogle(clientId) {
    if (!window.google?.accounts?.id) return;
    window.google.accounts.id.initialize({
      client_id: clientId,
      callback: handleGoogleResponse,
      auto_select: false,
    });
    setGoogleReady(true);
    if (googleBtnRef.current) {
      window.google.accounts.id.renderButton(googleBtnRef.current, {
        type: 'standard',
        theme: 'filled_black',
        size: 'large',
        text: 'signin_with',
        shape: 'pill',
        width: '100%',
      });
    }
  }

  useEffect(() => {
    if (googleReady && googleBtnRef.current && window.google?.accounts?.id) {
      window.google.accounts.id.renderButton(googleBtnRef.current, {
        type: 'standard',
        theme: 'filled_black',
        size: 'large',
        text: 'signin_with',
        shape: 'pill',
        width: 360,
      });
    }
  }, [googleReady]);

  async function handleGoogleResponse(response) {
    setErr('');
    setLoading(true);
    try {
      const result = await googleLogin(response.credential);
      if (result.success) {
        nav('/');
      } else {
        setErr(result.error || 'Google login failed');
      }
    } catch {
      setErr('Connection error');
    }
    setLoading(false);
  }

  async function submit(e) {
    e.preventDefault();
    setErr('');
    setLoading(true);
    try {
      const ok = await login(user, pass);
      if (ok) nav('/');
      else setErr('Invalid credentials');
    } catch {
      setErr('Connection error');
    }
    setLoading(false);
  }

  if (checking) {
    return <div className="min-h-screen flex items-center justify-center"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
  }

  return (
    <div className="min-h-screen flex items-center justify-center p-4">
      <div className="w-full max-w-md">
        <div className="text-center mb-8">
          <div className="inline-flex items-center justify-center w-20 h-20 rounded-2xl mb-4" style={{ background: 'linear-gradient(135deg,#3b82f6,#8b5cf6)' }}>
            <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" /><circle cx="12" cy="12" r="3" /></svg>
          </div>
          <h1 className="text-3xl font-extrabold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">POLY</h1>
          <p className="text-gray-400 mt-2 text-sm">Weather Forecast Micro-Betting System</p>
        </div>

        {err && <div className="text-red-400 text-sm bg-red-500/10 rounded-lg p-3 flex items-center gap-2 mb-4">
          <svg className="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
          {err}
        </div>}

        {/* Google Sign-In */}
        {googleClientId && (
          <div className="mb-4">
            <div ref={googleBtnRef} className="flex justify-center" />
            <div className="relative my-5">
              <div className="absolute inset-0 flex items-center"><div className="w-full border-t border-gray-800" /></div>
              <div className="relative flex justify-center text-xs"><span className="px-3 text-gray-500" style={{ background: 'var(--bg)' }}>or sign in with password</span></div>
            </div>
          </div>
        )}

        <form onSubmit={submit} className="card space-y-4">
          <div>
            <label className="block text-xs font-medium text-gray-400 mb-1.5">Username</label>
            <input type="text" value={user} onChange={e => setUser(e.target.value)} required autoComplete="username" className="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:border-blue-500 transition" placeholder="admin" />
          </div>
          <div>
            <label className="block text-xs font-medium text-gray-400 mb-1.5">Password</label>
            <div className="relative">
              <input type={showPass ? 'text' : 'password'} value={pass} onChange={e => setPass(e.target.value)} required autoComplete="current-password" className="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-3 py-2.5 pr-10 text-sm focus:outline-none focus:border-blue-500 transition" placeholder="Enter password" />
              <button type="button" onClick={() => setShowPass(!showPass)} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300">
                <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d={showPass ? 'M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.879L21 21' : 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z'} /></svg>
              </button>
            </div>
          </div>
          <button type="submit" disabled={loading} className="btn btn-p w-full py-2.5 flex items-center justify-center gap-2 disabled:opacity-50">
            {loading && <span className="animate-spin w-4 h-4 border-2 border-white border-t-transparent rounded-full" />}
            {loading ? 'Signing in...' : 'Sign In'}
          </button>
        </form>

        <div className="mt-6 grid grid-cols-3 gap-2 text-center">
          {[{ l: 'Bankroll', v: '$50', c: 'text-green-400' }, { l: 'Mode', v: 'Paper', c: 'text-yellow-400' }, { l: 'Status', v: 'Active', c: 'text-blue-400' }].map(s =>
            <div key={s.l} className="card !p-3"><div className={`text-lg font-bold ${s.c}`}>{s.v}</div><div className="text-[10px] text-gray-500 uppercase">{s.l}</div></div>
          )}
        </div>
        <div className="text-center mt-4 space-y-1">
          <p className="text-gray-600 text-[10px]">Session: httpOnly cookie / 30-day expiry / IP whitelist</p>
          {googleClientId && <p className="text-gray-600 text-[10px]">Google OAuth 2.0 enabled</p>}
        </div>
      </div>
    </div>
  );
}