feat: Initial Material You client implementation
Implemented a Material You (Material 3) client for the bonch-open-api. - Added API client with axios - Added Zustand store for API config and user data - Added Login and Profile pages - Set up routing with react-router-dom - Added global styles with Material 3 tokens Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
71
src/pages/Login.tsx
Normal file
71
src/pages/Login.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useStore } from '../store/useStore';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { api } from '../api/client';
|
||||
|
||||
export const Login: React.FC = () => {
|
||||
const { apiDomain, apiKey, setApiDomain, setApiKey } = useStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
// Basic validation
|
||||
if (!apiDomain || !apiKey) {
|
||||
throw new Error('Please fill all fields');
|
||||
}
|
||||
|
||||
// Check health
|
||||
await api.checkHealth();
|
||||
|
||||
// If health check passes, try to get profile
|
||||
await api.getProfile();
|
||||
|
||||
navigate('/profile');
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || err.message || 'Connection failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '400px', margin: '100px auto', padding: '20px' }}>
|
||||
<h1 style={{ marginBottom: '24px', textAlign: 'center' }}>Bonch Client</h1>
|
||||
<form onSubmit={handleLogin}>
|
||||
<input
|
||||
type="text"
|
||||
className="m3-text-field"
|
||||
placeholder="API Domain (e.g. https://api.example.com)"
|
||||
value={apiDomain}
|
||||
onChange={(e) => setApiDomain(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
className="m3-text-field"
|
||||
placeholder="API Key"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
/>
|
||||
{error && (
|
||||
<p style={{ color: 'var(--md-sys-color-error)', marginBottom: '16px' }}>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
className="m3-button m3-button-filled"
|
||||
style={{ width: '100%' }}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Connecting...' : 'Login'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
112
src/pages/Profile.tsx
Normal file
112
src/pages/Profile.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import { Profile as ProfileType } from '../types/api';
|
||||
import { useStore } from '../store/useStore';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { LogOut, User, Mail, GraduationCap, Users } from 'lucide-react';
|
||||
|
||||
export const Profile: React.FC = () => {
|
||||
const [profile, setProfile] = useState<ProfileType | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const { reset } = useStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProfile = async () => {
|
||||
try {
|
||||
const data = await api.getProfile();
|
||||
setProfile(data);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || err.message || 'Failed to load profile');
|
||||
if (err.response?.status === 401) {
|
||||
navigate('/login');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProfile();
|
||||
}, [navigate]);
|
||||
|
||||
const handleLogout = () => {
|
||||
reset();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
if (loading) return <div style={{ padding: '20px', textAlign: 'center' }}>Loading profile...</div>;
|
||||
if (error) return (
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: 'var(--md-sys-color-error)' }}>
|
||||
{error}
|
||||
<br />
|
||||
<button onClick={() => navigate('/login')} className="m3-button m3-button-tonal" style={{ marginTop: '16px' }}>
|
||||
Back to Login
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
|
||||
<h1>Profile</h1>
|
||||
<button onClick={handleLogout} className="m3-button m3-button-tonal">
|
||||
<LogOut size={18} style={{ marginRight: '8px' }} />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{profile && (
|
||||
<div className="m3-card">
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<User size={24} style={{ marginRight: '16px' }} />
|
||||
<div>
|
||||
<div style={{ fontSize: '12px', opacity: 0.7 }}>Full Name</div>
|
||||
<div style={{ fontSize: '18px', fontWeight: 500 }}>{profile.fullName}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{profile.email && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<Mail size={24} style={{ marginRight: '16px' }} />
|
||||
<div>
|
||||
<div style={{ fontSize: '12px', opacity: 0.7 }}>Email</div>
|
||||
<div>{profile.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{profile.group && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<Users size={24} style={{ marginRight: '16px' }} />
|
||||
<div>
|
||||
<div style={{ fontSize: '12px', opacity: 0.7 }}>Group</div>
|
||||
<div>{profile.group}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{profile.faculty && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<GraduationCap size={24} style={{ marginRight: '16px' }} />
|
||||
<div>
|
||||
<div style={{ fontSize: '12px', opacity: 0.7 }}>Faculty</div>
|
||||
<div>{profile.faculty}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '24px' }}>
|
||||
<h2 style={{ fontSize: '18px', marginBottom: '16px' }}>Other Services</h2>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
|
||||
<button className="m3-button m3-button-tonal" disabled>Schedule (Soon)</button>
|
||||
<button className="m3-button m3-button-tonal" disabled>Grades (Soon)</button>
|
||||
<button className="m3-button m3-button-tonal" disabled>Debts (Soon)</button>
|
||||
<button className="m3-button m3-button-tonal" disabled>Messages (Soon)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user