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:
2026-04-11 00:40:55 +03:00
commit e176c00e52
25 changed files with 3786 additions and 0 deletions

71
src/pages/Login.tsx Normal file
View 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
View 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>
);
};