fix(front): buttons

This commit is contained in:
ulitsaRaskolnikova 2026-02-09 17:35:38 +03:00
parent 7183f6e497
commit 4e9713910c
3 changed files with 138 additions and 89 deletions

View File

@ -1,14 +1,35 @@
import React from 'react';
import React, { useState } from 'react';
import type { DomainResponse } from '../../api/models';
import { Button, Grid, GridItem, HStack, Stack, Text } from '@chakra-ui/react';
import { Button, Grid, GridItem, HStack, Text } from '@chakra-ui/react';
import DateText from '../DateText';
import { ArrowRight } from 'lucide-react';
import { ORDER_AXIOS_INSTANCE } from '~/api/apiClientOrders';
type Props = {
domains: DomainResponse[];
onRenewed?: () => void;
};
const DomainList = (props: Props) => {
const [renewingFqdn, setRenewingFqdn] = useState<string | null>(null);
const [renewedFqdns, setRenewedFqdns] = useState<Set<string>>(new Set());
const handleRenew = async (fqdn: string, period: 'MONTH' | 'YEAR') => {
setRenewingFqdn(fqdn);
try {
await ORDER_AXIOS_INSTANCE.post('/domains/renew', {
l3Domains: [fqdn],
period,
});
setRenewedFqdns((prev) => new Set(prev).add(fqdn));
props.onRenewed?.();
} catch {
// ignore
} finally {
setRenewingFqdn(null);
}
};
return (
<Grid
templateColumns={'50% 20% auto'}
@ -19,32 +40,61 @@ const DomainList = (props: Props) => {
py={2.5}
borderRadius={'sm'}
>
{props.domains.map((domain) => (
<React.Fragment key={domain.id}>
<GridItem key={domain.id}>
<Text>{domain.fqdn}</Text>
</GridItem>
<GridItem>
{domain.expiresAt ? (
<Text>
до <DateText as={'span'}>{domain.expiresAt}</DateText>
</Text>
) : (
<Text color="fg.muted"></Text>
)}
</GridItem>
<GridItem>
<HStack justifyContent={'flex-end'}>
<Button size={'sm'} colorPalette={'secondary'}>
продлить
</Button>
<Button size={'sm'} colorPalette={'secondary'}>
DNS <ArrowRight />
</Button>
</HStack>
</GridItem>
</React.Fragment>
))}
{props.domains.map((domain) => {
const fqdn = domain.fqdn ?? '';
const isRenewing = renewingFqdn === fqdn;
const wasRenewed = renewedFqdns.has(fqdn);
return (
<React.Fragment key={domain.id ?? fqdn}>
<GridItem>
<Text>{fqdn}</Text>
</GridItem>
<GridItem>
{domain.expiresAt ? (
<Text>
до <DateText as={'span'}>{domain.expiresAt}</DateText>
</Text>
) : (
<Text color="fg.muted"></Text>
)}
</GridItem>
<GridItem>
<HStack justifyContent={'flex-end'}>
{wasRenewed ? (
<Button size={'sm'} colorPalette={'green'} variant={'solid'} disabled>
Продлён
</Button>
) : (
<>
<Button
size={'sm'}
colorPalette={'secondary'}
onClick={() => handleRenew(fqdn, 'MONTH')}
loading={isRenewing}
disabled={renewingFqdn !== null && !isRenewing}
>
+1 мес
</Button>
<Button
size={'sm'}
colorPalette={'secondary'}
onClick={() => handleRenew(fqdn, 'YEAR')}
loading={isRenewing}
disabled={renewingFqdn !== null && !isRenewing}
>
+1 год
</Button>
</>
)}
<Button size={'sm'} colorPalette={'secondary'}>
DNS <ArrowRight />
</Button>
</HStack>
</GridItem>
</React.Fragment>
);
})}
</Grid>
);
};

View File

@ -1,5 +1,4 @@
import {
Box,
Button,
Field,
Heading,
@ -37,15 +36,15 @@ const CartPage = () => {
const [totalMonthly, setTotalMonthly] = useState(0);
const [totalYearly, setTotalYearly] = useState(0);
const [searchResults, setSearchResults] = useState<DomainSearchResult[]>([]);
const [isCartLoading, setIsCartLoading] = useState(false);
const [isSearchLoading, setIsSearchLoading] = useState(false);
const [addingFqdn, setAddingFqdn] = useState<string | null>(null);
const [checkoutLoading, setCheckoutLoading] = useState(false);
const [error, setError] = useState('');
const cartCount = useMemo(() => cartDomains.length, [cartDomains]);
const cartSet = useMemo(() => new Set(cartDomains), [cartDomains]);
const loadCart = useCallback(async () => {
setIsCartLoading(true);
setError('');
try {
const { data } = await ORDER_AXIOS_INSTANCE.get<CartResponse>('/cart/me');
setCartDomains(data.l3Domains ?? []);
@ -53,8 +52,6 @@ const CartPage = () => {
setTotalYearly(data.totalYearlyPrice ?? 0);
} catch {
setError('Не удалось загрузить корзину.');
} finally {
setIsCartLoading(false);
}
}, []);
@ -102,7 +99,7 @@ const CartPage = () => {
const handleAddToCart = useCallback(
async (fqdn: string) => {
setIsCartLoading(true);
setAddingFqdn(fqdn);
setError('');
try {
await ORDER_AXIOS_INSTANCE.post(`/cart/${encodeURIComponent(fqdn)}`);
@ -110,7 +107,7 @@ const CartPage = () => {
} catch {
setError('Не удалось добавить домен в корзину.');
} finally {
setIsCartLoading(false);
setAddingFqdn(null);
}
},
[loadCart]
@ -119,7 +116,7 @@ const CartPage = () => {
const handleCheckout = useCallback(
async (period: 'MONTH' | 'YEAR') => {
if (cartDomains.length === 0) return;
setIsCartLoading(true);
setCheckoutLoading(true);
setError('');
try {
await ORDER_AXIOS_INSTANCE.post('/cart/checkout', { period });
@ -129,7 +126,7 @@ const CartPage = () => {
} catch {
setError('Не удалось оформить покупку.');
} finally {
setIsCartLoading(false);
setCheckoutLoading(false);
}
},
[cartDomains]
@ -161,7 +158,7 @@ const CartPage = () => {
size={'sm'}
colorPalette={'secondary'}
onClick={() => handleCheckout('MONTH')}
loading={isCartLoading}
loading={checkoutLoading}
>
Купить на месяц
</Button>
@ -169,7 +166,7 @@ const CartPage = () => {
size={'sm'}
colorPalette={'secondary'}
onClick={() => handleCheckout('YEAR')}
loading={isCartLoading}
loading={checkoutLoading}
>
Купить на год
</Button>
@ -205,26 +202,38 @@ const CartPage = () => {
{searchResults.length > 0 && (
<Stack bg={'accent.muted'} p={5} borderRadius={'md'} gap={2}>
<Text fontWeight={'bold'}>Результаты поиска</Text>
{searchResults.map((result) => (
<HStack key={result.fqdn} justifyContent={'space-between'}>
<Text>{result.fqdn}</Text>
<HStack>
<Text>{result.monthlyPrice} / месяц</Text>
{result.free ? (
<Button
size={'sm'}
colorPalette={'secondary'}
onClick={() => handleAddToCart(result.fqdn)}
loading={isCartLoading}
>
В корзину
</Button>
) : (
<Text color={'fg.muted'}>Занят</Text>
)}
{searchResults.map((result) => {
const inCart = cartSet.has(result.fqdn);
const isAdding = addingFqdn === result.fqdn;
return (
<HStack key={result.fqdn} justifyContent={'space-between'}>
<Text>{result.fqdn}</Text>
<HStack>
<Text>{result.monthlyPrice} / месяц</Text>
{result.free ? (
inCart ? (
<Button size={'sm'} colorPalette={'green'} variant={'solid'} disabled>
В корзине
</Button>
) : (
<Button
size={'sm'}
colorPalette={'secondary'}
onClick={() => handleAddToCart(result.fqdn)}
loading={isAdding}
disabled={addingFqdn !== null && !isAdding}
>
В корзину
</Button>
)
) : (
<Text color={'fg.muted'}>Занят</Text>
)}
</HStack>
</HStack>
</HStack>
))}
);
})}
</Stack>
)}
</Stack>

View File

@ -1,5 +1,5 @@
import { Button, Heading, HStack, Stack, Text } from '@chakra-ui/react';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import type { DomainResponse } from '~/api/models/domain-order';
import { AXIOS_INSTANCE } from '~/api/apiClientDomains';
import DomainList from '../../components/dashboard/DomainList';
@ -15,36 +15,26 @@ interface UserDomainDetailed {
const DomainsPage = () => {
const [domains, setDomains] = useState<DomainResponse[]>([]);
useEffect(() => {
let isMounted = true;
const loadDomains = async () => {
try {
const { data } = await AXIOS_INSTANCE.get<UserDomainDetailed[]>('/userDomains/detailed');
if (isMounted) {
const mapped: DomainResponse[] = (data ?? []).map((d) => ({
id: d.id?.toString(),
fqdn: d.fqdn,
zoneName: d.zoneName,
activatedAt: d.activatedAt,
expiresAt: d.expiresAt,
}));
setDomains(mapped);
}
} catch {
if (isMounted) {
setDomains([]);
}
}
};
loadDomains();
return () => {
isMounted = false;
};
const loadDomains = useCallback(async () => {
try {
const { data } = await AXIOS_INSTANCE.get<UserDomainDetailed[]>('/userDomains/detailed');
const mapped: DomainResponse[] = (data ?? []).map((d) => ({
id: d.id?.toString(),
fqdn: d.fqdn,
zoneName: d.zoneName,
activatedAt: d.activatedAt,
expiresAt: d.expiresAt,
}));
setDomains(mapped);
} catch {
setDomains([]);
}
}, []);
useEffect(() => {
loadDomains();
}, [loadDomains]);
return (
<Stack gap={5}>
<HStack justifyContent={'space-between'}>
@ -56,7 +46,7 @@ const DomainsPage = () => {
</Button>
</HStack>
</HStack>
<DomainList domains={domains} />
<DomainList domains={domains} onRenewed={loadDomains} />
</Stack>
);
};