Primakov Alexandr Alexandrovich 71ee0c1c0e Enhance UI feedback and responsiveness across authentication and tracking pages
- Updated button text to reflect loading states in SignIn, SignUp, and Tracker pages.
- Adjusted padding and heading sizes for better responsiveness in Main, Stats, and Tracker pages.
- Improved layout consistency by modifying padding properties in Card components.
2025-11-17 14:10:18 +03:00

265 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Link } from 'react-router-dom'
import {
Box,
Button,
Input,
VStack,
HStack,
Text,
Heading,
Card,
Textarea,
Stack,
} from '@chakra-ui/react'
import { Field } from '../../components/ui/field'
import { cigarettesApi } from '../../api/client'
import { URLs } from '../../__data__/urls'
import type { Cigarette } from '../../types/api'
const logCigaretteSchema = z.object({
smokedAt: z.string().optional(),
note: z.string().optional(),
})
type LogCigaretteFormData = z.infer<typeof logCigaretteSchema>
export const TrackerPage: React.FC = () => {
const [cigarettes, setCigarettes] = useState<Cigarette[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<LogCigaretteFormData>({
resolver: zodResolver(logCigaretteSchema),
})
const fetchCigarettes = async () => {
try {
const response = await cigarettesApi.getAll()
if (response.success) {
// Show most recent first
setCigarettes(response.body.reverse())
}
} catch (err: any) {
console.error('Failed to fetch cigarettes:', err)
}
}
useEffect(() => {
fetchCigarettes()
}, [])
const logQuick = async () => {
setIsLoading(true)
setError(null)
setSuccess(null)
try {
const response = await cigarettesApi.log({})
if (response.success) {
setSuccess('Сигарета записана!')
await fetchCigarettes()
setTimeout(() => setSuccess(null), 3000)
}
} catch (err: any) {
const errorMessage = err?.response?.data?.errors || err?.response?.data?.message || 'Ошибка при записи'
setError(errorMessage)
} finally {
setIsLoading(false)
}
}
const onSubmit = async (data: LogCigaretteFormData) => {
setIsLoading(true)
setError(null)
setSuccess(null)
try {
const response = await cigarettesApi.log({
smokedAt: data.smokedAt || undefined,
note: data.note || undefined,
})
if (response.success) {
setSuccess('Сигарета записана с заметкой!')
reset()
await fetchCigarettes()
setTimeout(() => setSuccess(null), 3000)
}
} catch (err: any) {
const errorMessage = err?.response?.data?.errors || err?.response?.data?.message || 'Ошибка при записи'
setError(errorMessage)
} finally {
setIsLoading(false)
}
}
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
return (
<Box minH="100vh" bg="gray.50" p={{ base: 2, md: 8 }}>
<VStack gap={6} maxW="4xl" mx="auto">
<Heading size={{ base: 'xl', md: '2xl' }}>Трекер курения</Heading>
<HStack w="full" gap={4}>
<Link to={URLs.baseUrl}>
<Button colorScheme="gray" variant="outline">
На главную
</Button>
</Link>
<Link to={URLs.baseUrl + '/stats'}>
<Button colorScheme="teal" variant="outline">
Статистика
</Button>
</Link>
</HStack>
{/* Quick log button */}
<Card.Root w="full" bg="blue.50">
<Card.Body p={{ base: 4, md: 6 }}>
<VStack gap={4}>
<Text fontSize="lg" fontWeight="bold">
Быстрая запись
</Text>
<Button
colorScheme="blue"
size="lg"
w="full"
onClick={logQuick}
disabled={isLoading}
>
{isLoading ? 'Запись...' : 'Записать сигарету (текущее время)'}
</Button>
</VStack>
</Card.Body>
</Card.Root>
{/* Form with custom time and note */}
<Card.Root w="full">
<Card.Body p={{ base: 4, md: 6 }}>
<form onSubmit={handleSubmit(onSubmit)}>
<VStack gap={4} align="stretch">
<Heading size="md">Запись с дополнительными данными</Heading>
<Field
label="Время (необязательно)"
helperText="Оставьте пустым для текущего времени"
invalid={!!errors.smokedAt}
errorText={errors.smokedAt?.message}
>
<Input
{...register('smokedAt')}
type="datetime-local"
placeholder="Выберите время"
/>
</Field>
<Field
label="Заметка (необязательно)"
invalid={!!errors.note}
errorText={errors.note?.message}
>
<Textarea
{...register('note')}
placeholder="Добавьте комментарий..."
rows={3}
/>
</Field>
<Button
type="submit"
colorScheme="green"
w="full"
disabled={isLoading}
>
{isLoading ? 'Запись...' : 'Записать с заметкой'}
</Button>
</VStack>
</form>
</Card.Body>
</Card.Root>
{/* Success/Error messages */}
{success && (
<Card.Root w="full" bg="green.50" borderColor="green.500" borderWidth={2}>
<Card.Body>
<Text color="green.700" fontWeight="bold">
{success}
</Text>
</Card.Body>
</Card.Root>
)}
{error && (
<Card.Root w="full" bg="red.50" borderColor="red.500" borderWidth={2}>
<Card.Body>
<Text color="red.700" fontWeight="bold">
{error}
</Text>
</Card.Body>
</Card.Root>
)}
{/* Recent cigarettes list */}
<Card.Root w="full">
<Card.Body p={{ base: 4, md: 6 }}>
<VStack gap={4} align="stretch">
<Heading size="md">Последние записи</Heading>
{cigarettes.length === 0 ? (
<Text color="gray.500" textAlign="center" py={4}>
Записей пока нет
</Text>
) : (
<Stack gap={2}>
{cigarettes.slice(0, 10).map((cigarette) => (
<Box
key={cigarette.id}
p={3}
bg="gray.100"
borderRadius="md"
borderWidth={1}
borderColor="gray.300"
>
<HStack justify="space-between" align="start">
<VStack align="start" gap={1}>
<Text fontWeight="bold">
{formatDate(cigarette.smokedAt)}
</Text>
{cigarette.note && (
<Text fontSize="sm" color="gray.600">
{cigarette.note}
</Text>
)}
</VStack>
</HStack>
</Box>
))}
</Stack>
)}
</VStack>
</Card.Body>
</Card.Root>
</VStack>
</Box>
)
}