Separated weight tracking
This commit is contained in:
6
App.tsx
6
App.tsx
@@ -12,6 +12,7 @@ import { TabView, WorkoutSession, WorkoutSet, WorkoutPlan, User, Language } from
|
|||||||
import { getSessions, saveSession, deleteSession, getPlans, getActiveSession, updateActiveSession, deleteActiveSession, updateSetInActiveSession, deleteSetFromActiveSession } from './services/storage';
|
import { getSessions, saveSession, deleteSession, getPlans, getActiveSession, updateActiveSession, deleteActiveSession, updateSetInActiveSession, deleteSetFromActiveSession } from './services/storage';
|
||||||
import { getCurrentUserProfile, getMe } from './services/auth';
|
import { getCurrentUserProfile, getMe } from './services/auth';
|
||||||
import { getSystemLanguage } from './services/i18n';
|
import { getSystemLanguage } from './services/i18n';
|
||||||
|
import { logWeight } from './services/weight';
|
||||||
import { generateId } from './utils/uuid';
|
import { generateId } from './utils/uuid';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -114,6 +115,11 @@ function App() {
|
|||||||
|
|
||||||
// Save to database immediately
|
// Save to database immediately
|
||||||
await saveSession(currentUser.id, newSession);
|
await saveSession(currentUser.id, newSession);
|
||||||
|
|
||||||
|
// If startWeight was provided (meaning user explicitly entered it), log it to weight history
|
||||||
|
if (startWeight) {
|
||||||
|
await logWeight(startWeight);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEndSession = async () => {
|
const handleEndSession = async () => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { User, Language, ExerciseDef, ExerciseType } from '../types';
|
import { User, Language, ExerciseDef, ExerciseType, BodyWeightRecord } from '../types';
|
||||||
import { createUser, changePassword, updateUserProfile, getCurrentUserProfile, getUsers, deleteUser, toggleBlockUser, adminResetPassword, getMe } from '../services/auth';
|
import { createUser, changePassword, updateUserProfile, getCurrentUserProfile, getUsers, deleteUser, toggleBlockUser, adminResetPassword, getMe } from '../services/auth';
|
||||||
import { getExercises, saveExercise } from '../services/storage';
|
import { getExercises, saveExercise } from '../services/storage';
|
||||||
|
import { getWeightHistory, logWeight } from '../services/weight';
|
||||||
import { User as UserIcon, LogOut, Save, Shield, UserPlus, Lock, Calendar, Ruler, Scale, PersonStanding, Globe, ChevronDown, ChevronUp, Trash2, Ban, KeyRound, Dumbbell, Archive, ArchiveRestore, Pencil, Plus } from 'lucide-react';
|
import { User as UserIcon, LogOut, Save, Shield, UserPlus, Lock, Calendar, Ruler, Scale, PersonStanding, Globe, ChevronDown, ChevronUp, Trash2, Ban, KeyRound, Dumbbell, Archive, ArchiveRestore, Pencil, Plus } from 'lucide-react';
|
||||||
import ExerciseModal from './ExerciseModal';
|
import ExerciseModal from './ExerciseModal';
|
||||||
import { t } from '../services/i18n';
|
import { t } from '../services/i18n';
|
||||||
@@ -23,6 +24,11 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
|||||||
const [birthDate, setBirthDate] = useState<string>('');
|
const [birthDate, setBirthDate] = useState<string>('');
|
||||||
const [gender, setGender] = useState<string>('MALE');
|
const [gender, setGender] = useState<string>('MALE');
|
||||||
|
|
||||||
|
// Weight Tracker
|
||||||
|
const [weightHistory, setWeightHistory] = useState<BodyWeightRecord[]>([]);
|
||||||
|
const [todayWeight, setTodayWeight] = useState<string>('');
|
||||||
|
const [showWeightTracker, setShowWeightTracker] = useState(false);
|
||||||
|
|
||||||
// Admin: Create User
|
// Admin: Create User
|
||||||
const [newUserEmail, setNewUserEmail] = useState('');
|
const [newUserEmail, setNewUserEmail] = useState('');
|
||||||
const [newUserPass, setNewUserPass] = useState('');
|
const [newUserPass, setNewUserPass] = useState('');
|
||||||
@@ -72,9 +78,22 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
|||||||
refreshUserList();
|
refreshUserList();
|
||||||
}
|
}
|
||||||
refreshExercises();
|
refreshExercises();
|
||||||
|
refreshWeightHistory();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [user.id, user.role, JSON.stringify(user.profile)]);
|
}, [user.id, user.role, JSON.stringify(user.profile)]);
|
||||||
|
|
||||||
|
const refreshWeightHistory = async () => {
|
||||||
|
const history = await getWeightHistory();
|
||||||
|
setWeightHistory(history);
|
||||||
|
|
||||||
|
// Check if we have a weight for today
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const todayRecord = history.find(r => r.dateStr === today);
|
||||||
|
if (todayRecord) {
|
||||||
|
setTodayWeight(todayRecord.weight.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const refreshUserList = async () => {
|
const refreshUserList = async () => {
|
||||||
const res = await getUsers();
|
const res = await getUsers();
|
||||||
if (res.success && res.users) {
|
if (res.success && res.users) {
|
||||||
@@ -99,6 +118,27 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
|||||||
setSnackbar({ isOpen: true, message, type });
|
setSnackbar({ isOpen: true, message, type });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLogWeight = async () => {
|
||||||
|
if (!todayWeight) return;
|
||||||
|
const weightVal = parseFloat(todayWeight);
|
||||||
|
if (isNaN(weightVal)) return;
|
||||||
|
|
||||||
|
const res = await logWeight(weightVal);
|
||||||
|
if (res) {
|
||||||
|
showSnackbar('Weight logged successfully', 'success');
|
||||||
|
refreshWeightHistory();
|
||||||
|
// Also update the profile weight display if it's today
|
||||||
|
setWeight(todayWeight);
|
||||||
|
// And trigger user update to sync across app
|
||||||
|
const userRes = await getMe();
|
||||||
|
if (userRes.success && userRes.user && onUserUpdate) {
|
||||||
|
onUserUpdate(userRes.user);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showSnackbar('Failed to log weight', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSaveProfile = async () => {
|
const handleSaveProfile = async () => {
|
||||||
const res = await updateUserProfile(user.id, {
|
const res = await updateUserProfile(user.id, {
|
||||||
weight: parseFloat(weight) || undefined,
|
weight: parseFloat(weight) || undefined,
|
||||||
@@ -267,6 +307,55 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* WEIGHT TRACKER */}
|
||||||
|
<div className="bg-surface-container rounded-xl p-4 border border-outline-variant/20">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowWeightTracker(!showWeightTracker)}
|
||||||
|
className="w-full flex justify-between items-center text-sm font-bold text-primary"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2"><Scale size={14} /> Weight Tracker</span>
|
||||||
|
{showWeightTracker ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showWeightTracker && (
|
||||||
|
<div className="mt-4 space-y-4">
|
||||||
|
<div className="flex gap-2 items-end">
|
||||||
|
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2 flex-1">
|
||||||
|
<label className="text-[10px] text-on-surface-variant font-medium">Today's Weight (kg)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
value={todayWeight}
|
||||||
|
onChange={(e) => setTodayWeight(e.target.value)}
|
||||||
|
className="w-full bg-transparent text-on-surface focus:outline-none"
|
||||||
|
placeholder="Enter weight..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleLogWeight}
|
||||||
|
className="bg-primary text-on-primary px-4 py-3 rounded-lg font-medium text-sm mb-[1px]"
|
||||||
|
>
|
||||||
|
Log
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 max-h-60 overflow-y-auto">
|
||||||
|
<h4 className="text-xs font-medium text-on-surface-variant">History</h4>
|
||||||
|
{weightHistory.length === 0 ? (
|
||||||
|
<p className="text-xs text-on-surface-variant italic">No weight records yet.</p>
|
||||||
|
) : (
|
||||||
|
weightHistory.map(record => (
|
||||||
|
<div key={record.id} className="flex justify-between items-center p-3 bg-surface-container-high rounded-lg">
|
||||||
|
<span className="text-sm text-on-surface">{new Date(record.date).toLocaleDateString()}</span>
|
||||||
|
<span className="text-sm font-bold text-primary">{record.weight} kg</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* EXERCISE MANAGER */}
|
{/* EXERCISE MANAGER */}
|
||||||
<div className="bg-surface-container rounded-xl p-4 border border-outline-variant/20">
|
<div className="bg-surface-container rounded-xl p-4 border border-outline-variant/20">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo, useState, useEffect } from 'react';
|
||||||
import { WorkoutSession, ExerciseType, Language } from '../types';
|
import { WorkoutSession, ExerciseType, Language, BodyWeightRecord } from '../types';
|
||||||
|
import { getWeightHistory } from '../services/weight';
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
|
||||||
import { t } from '../services/i18n';
|
import { t } from '../services/i18n';
|
||||||
|
|
||||||
@@ -10,6 +11,15 @@ interface StatsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Stats: React.FC<StatsProps> = ({ sessions, lang }) => {
|
const Stats: React.FC<StatsProps> = ({ sessions, lang }) => {
|
||||||
|
const [weightRecords, setWeightRecords] = useState<BodyWeightRecord[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchWeights = async () => {
|
||||||
|
const records = await getWeightHistory();
|
||||||
|
setWeightRecords(records);
|
||||||
|
};
|
||||||
|
fetchWeights();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const volumeData = useMemo(() => {
|
const volumeData = useMemo(() => {
|
||||||
const data = [...sessions].reverse().map(session => {
|
const data = [...sessions].reverse().map(session => {
|
||||||
@@ -46,13 +56,11 @@ const Stats: React.FC<StatsProps> = ({ sessions, lang }) => {
|
|||||||
}, [sessions, lang]);
|
}, [sessions, lang]);
|
||||||
|
|
||||||
const weightData = useMemo(() => {
|
const weightData = useMemo(() => {
|
||||||
return [...sessions].reverse()
|
return [...weightRecords].reverse().map(record => ({
|
||||||
.filter(s => s.userBodyWeight)
|
date: new Date(record.date).toLocaleDateString(lang === 'ru' ? 'ru-RU' : 'en-US', { day: 'numeric', month: 'short' }),
|
||||||
.map(session => ({
|
weight: record.weight
|
||||||
date: new Date(session.startTime).toLocaleDateString(lang === 'ru' ? 'ru-RU' : 'en-US', { day: 'numeric', month: 'short' }),
|
|
||||||
weight: session.userBodyWeight
|
|
||||||
}));
|
}));
|
||||||
}, [sessions, lang]);
|
}, [weightRecords, lang]);
|
||||||
|
|
||||||
if (sessions.length < 2) {
|
if (sessions.length < 2) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
Binary file not shown.
@@ -24,6 +24,18 @@ model User {
|
|||||||
sessions WorkoutSession[]
|
sessions WorkoutSession[]
|
||||||
exercises Exercise[]
|
exercises Exercise[]
|
||||||
plans WorkoutPlan[]
|
plans WorkoutPlan[]
|
||||||
|
weightRecords BodyWeightRecord[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model BodyWeightRecord {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
weight Float
|
||||||
|
date DateTime @default(now())
|
||||||
|
dateStr String // YYYY-MM-DD for unique constraint
|
||||||
|
|
||||||
|
@@unique([userId, dateStr])
|
||||||
}
|
}
|
||||||
|
|
||||||
model UserProfile {
|
model UserProfile {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import exerciseRoutes from './routes/exercises';
|
|||||||
import sessionRoutes from './routes/sessions';
|
import sessionRoutes from './routes/sessions';
|
||||||
import planRoutes from './routes/plans';
|
import planRoutes from './routes/plans';
|
||||||
import aiRoutes from './routes/ai';
|
import aiRoutes from './routes/ai';
|
||||||
|
import weightRoutes from './routes/weight';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ app.use('/api/exercises', exerciseRoutes);
|
|||||||
app.use('/api/sessions', sessionRoutes);
|
app.use('/api/sessions', sessionRoutes);
|
||||||
app.use('/api/plans', planRoutes);
|
app.use('/api/plans', planRoutes);
|
||||||
app.use('/api/ai', aiRoutes);
|
app.use('/api/ai', aiRoutes);
|
||||||
|
app.use('/api/weight', weightRoutes);
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
res.send('GymFlow AI API is running');
|
res.send('GymFlow AI API is running');
|
||||||
|
|||||||
21
server/src/middleware/auth.ts
Normal file
21
server/src/middleware/auth.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
||||||
|
|
||||||
|
export const authenticateToken = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const authHeader = req.headers['authorization'];
|
||||||
|
const token = authHeader && authHeader.split(' ')[1];
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.sendStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt.verify(token, JWT_SECRET, (err: any, user: any) => {
|
||||||
|
if (err) {
|
||||||
|
return res.sendStatus(403);
|
||||||
|
}
|
||||||
|
(req as any).user = user;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
};
|
||||||
78
server/src/routes/weight.ts
Normal file
78
server/src/routes/weight.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { authenticateToken } from '../middleware/auth';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
// Get weight history
|
||||||
|
router.get('/', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = (req as any).user.userId;
|
||||||
|
const weights = await prisma.bodyWeightRecord.findMany({
|
||||||
|
where: { userId },
|
||||||
|
orderBy: { date: 'desc' },
|
||||||
|
take: 365 // Limit to last year for now
|
||||||
|
});
|
||||||
|
res.json(weights);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching weight history:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch weight history' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log weight
|
||||||
|
router.post('/', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = (req as any).user.userId;
|
||||||
|
const { weight, dateStr } = req.body;
|
||||||
|
|
||||||
|
if (!weight || !dateStr) {
|
||||||
|
return res.status(400).json({ error: 'Weight and dateStr are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert: Update if exists for this day, otherwise create
|
||||||
|
const record = await prisma.bodyWeightRecord.upsert({
|
||||||
|
where: {
|
||||||
|
userId_dateStr: {
|
||||||
|
userId,
|
||||||
|
dateStr
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
weight: parseFloat(weight),
|
||||||
|
date: new Date(dateStr) // Update date object just in case
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
userId,
|
||||||
|
weight: parseFloat(weight),
|
||||||
|
dateStr,
|
||||||
|
date: new Date(dateStr)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also update the user profile weight to the latest logged weight
|
||||||
|
// But only if the logged date is today or in the future (or very recent)
|
||||||
|
// For simplicity, let's just update the profile weight if it's the most recent record
|
||||||
|
// Or we can just update it always if the user considers this their "current" weight.
|
||||||
|
// Let's check if this is the latest record by date.
|
||||||
|
const latestRecord = await prisma.bodyWeightRecord.findFirst({
|
||||||
|
where: { userId },
|
||||||
|
orderBy: { date: 'desc' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (latestRecord && latestRecord.id === record.id) {
|
||||||
|
await prisma.userProfile.update({
|
||||||
|
where: { userId },
|
||||||
|
data: { weight: parseFloat(weight) }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(record);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error logging weight:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to log weight' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -75,6 +75,7 @@ const translations = {
|
|||||||
create_btn: 'Create',
|
create_btn: 'Create',
|
||||||
completed_session_sets: 'Completed in this session',
|
completed_session_sets: 'Completed in this session',
|
||||||
add_weight: 'Add. Weight',
|
add_weight: 'Add. Weight',
|
||||||
|
no_exercises_found: 'No exercises found',
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
type_strength: 'Free Weights & Machines',
|
type_strength: 'Free Weights & Machines',
|
||||||
@@ -225,6 +226,7 @@ const translations = {
|
|||||||
create_btn: 'Создать',
|
create_btn: 'Создать',
|
||||||
completed_session_sets: 'Выполнено в этой тренировке',
|
completed_session_sets: 'Выполнено в этой тренировке',
|
||||||
add_weight: 'Доп. вес',
|
add_weight: 'Доп. вес',
|
||||||
|
no_exercises_found: 'Упражнения не найдены',
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
type_strength: 'Свободные веса и тренажеры',
|
type_strength: 'Свободные веса и тренажеры',
|
||||||
|
|||||||
53
services/weight.ts
Normal file
53
services/weight.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { BodyWeightRecord } from '../types';
|
||||||
|
|
||||||
|
const API_URL = '/api';
|
||||||
|
|
||||||
|
export const getWeightHistory = async (): Promise<BodyWeightRecord[]> => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/weight`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch weight history');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching weight history:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logWeight = async (weight: number, dateStr?: string): Promise<BodyWeightRecord | null> => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Default to today if no date provided
|
||||||
|
const date = dateStr || new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
const response = await fetch(`${API_URL}/weight`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ weight, dateStr: date })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to log weight');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error logging weight:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
7
types.ts
7
types.ts
@@ -72,6 +72,13 @@ export interface UserProfile {
|
|||||||
language?: Language;
|
language?: Language;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BodyWeightRecord {
|
||||||
|
id: string;
|
||||||
|
weight: number;
|
||||||
|
date: string; // ISO string
|
||||||
|
dateStr: string; // YYYY-MM-DD
|
||||||
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user