feat: Add mock mode for UI-only development

- Created mock API implementation in src/api/mock.ts
- Added isMockMode toggle to useStore
- Implemented switching between real and mock API in client.ts
- Added Mock Mode toggle to Login page
- Added 'npm run dev:mock' command

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-04-11 00:51:43 +03:00
parent 5ec94df727
commit 7cbe5ab6e2
6 changed files with 61 additions and 2 deletions

View File

@@ -1 +1,2 @@
VITE_API_DOMAIN=http://localhost:3000 VITE_API_DOMAIN=http://localhost:3000
VITE_MOCK_MODE=false

View File

@@ -5,6 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"dev:mock": "VITE_MOCK_MODE=true vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview"

View File

@@ -1,6 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import { useStore } from '../store/useStore'; import { useStore } from '../store/useStore';
import type { Profile, ApiResponse } from '../types/api'; import type { Profile, ApiResponse } from '../types/api';
import { mockApi } from './mock';
const getClient = () => { const getClient = () => {
const { apiDomain, apiKey } = useStore.getState(); const { apiDomain, apiKey } = useStore.getState();
@@ -14,11 +15,17 @@ const getClient = () => {
export const api = { export const api = {
getProfile: async () => { getProfile: async () => {
if (useStore.getState().isMockMode) {
return mockApi.getProfile();
}
const client = getClient(); const client = getClient();
const response = await client.get<ApiResponse<Profile>>('/v1/profile'); const response = await client.get<ApiResponse<Profile>>('/v1/profile');
return response.data.data; return response.data.data;
}, },
checkHealth: async () => { checkHealth: async () => {
if (useStore.getState().isMockMode) {
return mockApi.checkHealth();
}
const client = getClient(); const client = getClient();
const response = await client.get('/v1/health'); const response = await client.get('/v1/health');
return response.data; return response.data;

26
src/api/mock.ts Normal file
View File

@@ -0,0 +1,26 @@
import type { Profile } from '../types/api';
export const mockProfile: Profile = {
fullName: 'Иванов Иван Иванович',
group: 'ИКПИ-11',
faculty: 'Инфокоммуникационных сетей и систем',
studentId: '2210000',
email: 'ivanov.ii@example.com',
raw: {
'Дата рождения': '01.01.2004',
'Статус': 'Студент',
},
};
export const mockApi = {
getProfile: async (): Promise<Profile> => {
return new Promise((resolve) => {
setTimeout(() => resolve(mockProfile), 500);
});
},
checkHealth: async (): Promise<any> => {
return new Promise((resolve) => {
setTimeout(() => resolve({ status: 'ok', mock: true }), 300);
});
},
};

View File

@@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom';
import { api } from '../api/client'; import { api } from '../api/client';
export const Login: React.FC = () => { export const Login: React.FC = () => {
const { apiDomain, apiKey, setApiDomain, setApiKey } = useStore(); const { apiDomain, apiKey, isMockMode, setApiDomain, setApiKey, setMockMode } = useStore();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const navigate = useNavigate(); const navigate = useNavigate();
@@ -16,10 +16,14 @@ export const Login: React.FC = () => {
try { try {
// Basic validation // Basic validation
if (!apiDomain || !apiKey) { if (!isMockMode && (!apiDomain || !apiKey)) {
throw new Error('Please fill all fields'); throw new Error('Please fill all fields');
} }
if (isMockMode && !apiKey) {
setApiKey('mock-session');
}
// Check health // Check health
await api.checkHealth(); await api.checkHealth();
@@ -44,6 +48,7 @@ export const Login: React.FC = () => {
placeholder="API Domain (e.g. https://api.example.com)" placeholder="API Domain (e.g. https://api.example.com)"
value={apiDomain} value={apiDomain}
onChange={(e) => setApiDomain(e.target.value)} onChange={(e) => setApiDomain(e.target.value)}
disabled={isMockMode}
/> />
<input <input
type="password" type="password"
@@ -51,7 +56,22 @@ export const Login: React.FC = () => {
placeholder="API Key" placeholder="API Key"
value={apiKey} value={apiKey}
onChange={(e) => setApiKey(e.target.value)} onChange={(e) => setApiKey(e.target.value)}
disabled={isMockMode}
/> />
<div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center' }}>
<input
type="checkbox"
id="mockMode"
checked={isMockMode}
onChange={(e) => setMockMode(e.target.checked)}
style={{ marginRight: '8px' }}
/>
<label htmlFor="mockMode" style={{ fontSize: '14px', cursor: 'pointer' }}>
Enable Mock Mode (UI Only)
</label>
</div>
{error && ( {error && (
<p style={{ color: 'var(--md-sys-color-error)', marginBottom: '16px' }}> <p style={{ color: 'var(--md-sys-color-error)', marginBottom: '16px' }}>
{error} {error}

View File

@@ -4,8 +4,10 @@ import { persist } from 'zustand/middleware';
interface AppState { interface AppState {
apiDomain: string; apiDomain: string;
apiKey: string; apiKey: string;
isMockMode: boolean;
setApiDomain: (domain: string) => void; setApiDomain: (domain: string) => void;
setApiKey: (key: string) => void; setApiKey: (key: string) => void;
setMockMode: (isMock: boolean) => void;
reset: () => void; reset: () => void;
} }
@@ -14,8 +16,10 @@ export const useStore = create<AppState>()(
(set) => ({ (set) => ({
apiDomain: import.meta.env.VITE_API_DOMAIN || '', apiDomain: import.meta.env.VITE_API_DOMAIN || '',
apiKey: '', apiKey: '',
isMockMode: import.meta.env.VITE_MOCK_MODE === 'true',
setApiDomain: (apiDomain) => set({ apiDomain }), setApiDomain: (apiDomain) => set({ apiDomain }),
setApiKey: (apiKey) => set({ apiKey }), setApiKey: (apiKey) => set({ apiKey }),
setMockMode: (isMockMode) => set({ isMockMode }),
reset: () => set({ apiKey: '' }), reset: () => set({ apiKey: '' }),
}), }),
{ {