я.метрика
This commit is contained in:
@@ -9,7 +9,8 @@ import { CartItem } from './CartItem';
|
||||
import { CartFooter } from './CartFooter';
|
||||
import { ProductModal } from './ProductModal';
|
||||
import {useGlobalZustandState} from "../../hooks/useGlobalZustandState";
|
||||
import ym from "react-yandex-metrika";
|
||||
import {NotEnoughPopup} from "./NotEnoughPopup";
|
||||
import {reachGoal} from "../../hooks/useYM";
|
||||
|
||||
interface CartProps {
|
||||
products: Product[];
|
||||
@@ -18,6 +19,7 @@ interface CartProps {
|
||||
isLoading: boolean;
|
||||
onProductsChange: Dispatch<SetStateAction<Product[]>>;
|
||||
onComplete: () => void;
|
||||
onMistake: () => void;
|
||||
onLoadingChange: (isLoading: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -29,7 +31,9 @@ export function Cart({
|
||||
onProductsChange,
|
||||
onComplete,
|
||||
onLoadingChange,
|
||||
onMistake,
|
||||
}: CartProps) {
|
||||
const [showNotEnoughPopup, setShowNotEnoughPopup] = useState(false);
|
||||
const [visibleCount, setVisibleCount] = useState(5);
|
||||
const [displayText, setDisplayText] = useState('');
|
||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
||||
@@ -54,10 +58,11 @@ export function Cart({
|
||||
onComplete,
|
||||
onLoadingChange,
|
||||
setDisplayText,
|
||||
onMistake,
|
||||
});
|
||||
|
||||
async function handleReplace(product: Product) {
|
||||
ym('reachGoal', 'ac_product_replace_' + product.name);
|
||||
reachGoal('ac_product_replace_' + product.name);
|
||||
setProcessingProductName(product.name);
|
||||
|
||||
try {
|
||||
@@ -69,7 +74,7 @@ export function Cart({
|
||||
}
|
||||
|
||||
async function handleDelete(product: Product) {
|
||||
ym('reachGoal', 'ac_product_delete_' + product.name);
|
||||
reachGoal('ac_product_delete_' + product.name);
|
||||
setProcessingProductName(product.name);
|
||||
|
||||
try {
|
||||
@@ -85,7 +90,7 @@ export function Cart({
|
||||
<Summary>
|
||||
<div>
|
||||
<SummaryTitle>
|
||||
Ваша корзина на <Green>{days} {declension(days, ['день', 'дня', 'дней'])}</Green>
|
||||
Ваша закупка на <Green>{days} {declension(days, ['день', 'дня', 'дней'])}</Green>
|
||||
{showFullRefreshButton && <AbsoluteRefreshButton>Начать заново</AbsoluteRefreshButton>}
|
||||
</SummaryTitle>
|
||||
|
||||
@@ -122,6 +127,11 @@ export function Cart({
|
||||
onDelete={() => handleDelete(product)}
|
||||
/>
|
||||
))}
|
||||
{products.length === visibleProducts.length && isComplete && !isLoading && <NotEnoughWrapper>
|
||||
<ShowNotEnough type="button" onClick={() => setShowNotEnoughPopup(true)}>
|
||||
Не хватило
|
||||
</ShowNotEnough>
|
||||
</NotEnoughWrapper>}
|
||||
</CartList>
|
||||
|
||||
{products.length === 0 && (
|
||||
@@ -155,6 +165,8 @@ export function Cart({
|
||||
setSelectedProduct(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{showNotEnoughPopup && <NotEnoughPopup onClose={() => {setShowNotEnoughPopup(false)}} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -339,4 +351,13 @@ const ShowMore = styled.button`
|
||||
stroke-linejoin: round;
|
||||
margin-left: 6px;
|
||||
}
|
||||
`;
|
||||
|
||||
const NotEnoughWrapper = styled.div`
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
justify-content: center;
|
||||
`;
|
||||
const ShowNotEnough = styled(ShowMore)`
|
||||
text-align: center;
|
||||
`;
|
||||
@@ -3,7 +3,8 @@ import {BagIcon} from "../Icons/BagIcon";
|
||||
import {useState} from "react";
|
||||
import {OrderPopup} from "./OrderPopup";
|
||||
import {useAppSelector} from "../../store";
|
||||
import ym from "react-yandex-metrika";
|
||||
|
||||
import {reachGoal} from "../../hooks/useYM";
|
||||
|
||||
|
||||
interface CartFooterProps {
|
||||
@@ -31,10 +32,10 @@ export function CartFooter({ isVisible, totalPrice }: CartFooterProps) {
|
||||
</TotalWrap>
|
||||
|
||||
<CheckoutButton type="button" onClick={() => {
|
||||
ym('reachGoal', 'ac_hvatilo')
|
||||
reachGoal('ac_hvatilo')
|
||||
setShowCompletePopup(true)
|
||||
}}>
|
||||
{users.length > 1 ? 'Нам' : 'Мне'} хватило
|
||||
Хватило
|
||||
</CheckoutButton>
|
||||
{showCompletePopup && <>
|
||||
<OrderPopup setShowCompletePopup={setShowCompletePopup} />
|
||||
|
||||
@@ -2,7 +2,7 @@ import {useEffect, useState} from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {getProductAmount} from '../../utils/product';
|
||||
import type {Product} from '../../types/cart';
|
||||
import ym from "react-yandex-metrika";
|
||||
import {reachGoal} from "../../hooks/useYM";
|
||||
|
||||
interface CartItemProps {
|
||||
product: Product;
|
||||
@@ -21,8 +21,10 @@ export function CartItem({
|
||||
}: CartItemProps) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
ym('reachGoal', 'ac_product_replacement_menu_' + product.name);
|
||||
}, [product.name])
|
||||
if (isMenuOpen) {
|
||||
reachGoal('ac_product_replacement_menu_' + product.name);
|
||||
}
|
||||
}, [product.name, isMenuOpen])
|
||||
|
||||
return (
|
||||
<Item $processing={isProcessing}>
|
||||
@@ -148,6 +150,7 @@ const MenuButton = styled.button`
|
||||
`;
|
||||
|
||||
const Overlay = styled.div`
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
inset: 0;
|
||||
|
||||
@@ -32,6 +32,7 @@ const Overlay = styled.div`
|
||||
align-items: center;
|
||||
z-index: 4;
|
||||
overflow: auto;
|
||||
height: 100vh;
|
||||
`;
|
||||
const Window = styled.div`
|
||||
background-color: #ffffff;
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import styled, {css} from "styled-components";
|
||||
import {TextInput} from "../components/TextInput";
|
||||
import {useState} from "react";
|
||||
import FeedbackTextarea from "../components/Textarea";
|
||||
import {subscribeOnFail} from "../../api/promoApi";
|
||||
|
||||
export const NotEnoughPopup = ({onClose}: any) => {
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [value, setValue] = useState('');
|
||||
const [sent, setSent] = useState(false);
|
||||
|
||||
return <Overlay>
|
||||
<Window>
|
||||
<CloseButton type="button" aria-label="Закрыть" onClick={() => onClose()}>
|
||||
×
|
||||
</CloseButton>
|
||||
<ModalContent>
|
||||
{sent ? <>
|
||||
<h2>Сообщение отправлено</h2>
|
||||
<V8/>
|
||||
<p>
|
||||
Спасибо, ваша помощь крайне важна для нас, всё записали!
|
||||
</p>
|
||||
<img src="/images/broc-fail.png" alt=""/>
|
||||
</> : <>
|
||||
<h2>Чего-то не хватило?</h2>
|
||||
<V8/>
|
||||
<p>
|
||||
Расскажите, что не так с текущей корзиной — мы постараемся учесть ваши пожелания. За отзыв
|
||||
подарим <strong>3 бесплатные корзины</strong>
|
||||
</p>
|
||||
<V24/>
|
||||
<TextInput placeholder={'Почта'} value={email} onChange={setEmail}/>
|
||||
<V16/>
|
||||
<FeedbackTextarea maxLength={400} value={value} onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setValue(e.target.value);
|
||||
}}></FeedbackTextarea>
|
||||
<V16/>
|
||||
<V24/>
|
||||
<ActiveButton onClick={() => subscribeOnFail(email, value).then(() => {setSent(true)})}>Отправить</ActiveButton>
|
||||
</>}
|
||||
</ModalContent>
|
||||
</Window>
|
||||
</Overlay>
|
||||
};
|
||||
|
||||
const V8 = styled.div`height: 8px;`;
|
||||
const V16 = styled.div`height: 16px;`;
|
||||
const V24 = styled.div`height: 24px;`;
|
||||
|
||||
const Overlay = styled.div`
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 4;
|
||||
`;
|
||||
const Window = styled.div`
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
width: 515px;
|
||||
max-width: 100%;
|
||||
padding: 20px 24px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
z-index: 9;
|
||||
max-height: 90vh;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-weight: 300;
|
||||
line-height: 12px;
|
||||
color: #CEDFD0;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
transition: 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #ACBAAE;
|
||||
}
|
||||
|
||||
${({theme}) => theme.media.mobile(css`
|
||||
top: 40px;
|
||||
`)};
|
||||
`;
|
||||
|
||||
const ModalContent = styled.div`
|
||||
padding: 20px;
|
||||
color: #0C3B2E;
|
||||
box-sizing: content-box;
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
`;
|
||||
const ButtonBlock = styled.div`
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-direction: column;
|
||||
`;
|
||||
const Button = styled.div`
|
||||
height: 54px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
color: #5EA12D;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #F7FBF6;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: #EEF8EB;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
`;
|
||||
const ActiveButton = styled(Button)`
|
||||
background-color: #5EA12D;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: ${({theme}) => theme.colors.greenHover};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #4A8D19;
|
||||
}
|
||||
`;
|
||||
@@ -1,7 +1,8 @@
|
||||
import styled, {css} from "styled-components";
|
||||
import {useState} from "react";
|
||||
import {FinishPopupContent} from "./FinishPopupContent";
|
||||
import ym from "react-yandex-metrika";
|
||||
import {reachGoal} from "../../hooks/useYM";
|
||||
|
||||
|
||||
export const OrderPopup = ({setShowCompletePopup}: any) => {
|
||||
const [showFinal, setShowFinal] = useState(false);
|
||||
@@ -18,11 +19,11 @@ export const OrderPopup = ({setShowCompletePopup}: any) => {
|
||||
<ServiceFeeDescription>Включена в итоговую стоимость заказа</ServiceFeeDescription>
|
||||
<ButtonBlock>
|
||||
<ActiveButton onClick={() => {
|
||||
ym('reachGoal', 'ac_order_button')
|
||||
reachGoal('ac_order_button')
|
||||
setShowFinal(true)
|
||||
}}>Продолжить</ActiveButton>
|
||||
<Button onClick={() => {
|
||||
ym('reachGoal', 'ac_no_order_button')
|
||||
reachGoal('ac_no_order_button')
|
||||
setShowCompletePopup(false)
|
||||
}}>Вернуться к корзине</Button>
|
||||
</ButtonBlock>
|
||||
@@ -32,6 +33,7 @@ export const OrderPopup = ({setShowCompletePopup}: any) => {
|
||||
}
|
||||
|
||||
const Overlay = styled.div`
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
inset: 0;
|
||||
|
||||
@@ -3,7 +3,8 @@ import { formatCurrency } from '../../utils/formatCurrency';
|
||||
import { getLargeProductImage } from '../../utils/product';
|
||||
import type { Product } from '../../types/cart';
|
||||
import {useEffect} from "react";
|
||||
import ym from "react-yandex-metrika";
|
||||
|
||||
import {reachGoal} from "../../hooks/useYM";
|
||||
|
||||
interface ProductModalProps {
|
||||
product: Product | null;
|
||||
@@ -20,7 +21,7 @@ export function ProductModal({
|
||||
}: ProductModalProps) {
|
||||
useEffect(() => {
|
||||
if (product !== null) {
|
||||
ym('reachGoal', 'ac_open_popup')
|
||||
reachGoal('ac_open_popup')
|
||||
}
|
||||
}, [product]);
|
||||
|
||||
@@ -62,6 +63,7 @@ export function ProductModal({
|
||||
}
|
||||
|
||||
const Overlay = styled.div`
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
inset: 0;
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { setOption } from '../../api/promoApi';
|
||||
import { useDebounceBatch } from '../../hooks/useDebounceBatch';
|
||||
import ym from "react-yandex-metrika";
|
||||
import {reachGoal} from "../../hooks/useYM";
|
||||
|
||||
|
||||
interface CategoriesSectionProps {
|
||||
categories: string[];
|
||||
@@ -32,7 +33,7 @@ export function CategoriesSection({
|
||||
const sendUpdatedCategories = useDebounceBatch<string>(async categoryNames => {
|
||||
let nextCategories = [...selectedCategories];
|
||||
|
||||
ym('reachGoal', 'ac_set_categories');
|
||||
reachGoal('ac_set_categories');
|
||||
categoryNames.forEach(categoryName => {
|
||||
if (nextCategories.includes(categoryName)) {
|
||||
nextCategories = nextCategories.filter(item => item !== categoryName);
|
||||
|
||||
@@ -2,7 +2,8 @@ import {useEffect, useState} from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type {Gender, User} from '../../types/user';
|
||||
import {Tag, TagsInput, TagsWrap} from './TagsInput';
|
||||
import ym from "react-yandex-metrika";
|
||||
|
||||
import {reachGoal} from "../../hooks/useYM";
|
||||
|
||||
interface PersonModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -46,7 +47,7 @@ export function PersonModal({
|
||||
if (!isOpen) return;
|
||||
|
||||
if (user) {
|
||||
ym('reachGoal', 'ac_family_edit');
|
||||
reachGoal('ac_family_edit');
|
||||
setForm({
|
||||
name: user.name,
|
||||
gender: user.gender,
|
||||
@@ -116,22 +117,32 @@ export function PersonModal({
|
||||
{showExpanded ? (
|
||||
<Content onClick={event => event.stopPropagation()}>
|
||||
<Header>
|
||||
<h2>{showExpanded == 'favorite' ? 'Любимые' : 'Не добавлять'}</h2>
|
||||
<h2>{showExpanded == 'favorite' ? 'Любимые' : 'Исключить'}</h2>
|
||||
<CloseButton type="button" onClick={() => setShowExpanded('')}>
|
||||
×
|
||||
</CloseButton>
|
||||
</Header>
|
||||
<FormGroup>
|
||||
<TagsWrap>
|
||||
{((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).map(tag => (
|
||||
<Tag key={tag} $type={showExpanded}>
|
||||
{tag}
|
||||
<button type="button"
|
||||
onClick={() => (showExpanded == 'favorite' ? handleFavoriteChange : handleAvoidChange)(
|
||||
{/*{((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).map(tag => (*/}
|
||||
{availableTags.map(tag => (
|
||||
<Tag
|
||||
key={tag}
|
||||
$type={showExpanded}
|
||||
$active={(showExpanded == 'favorite' ? form.favorite : form.avoided).indexOf(tag) > -1}
|
||||
onClick={() => {
|
||||
if ((showExpanded == 'favorite' ? form.favorite : form.avoided).indexOf(tag) > -1) {
|
||||
(showExpanded == 'favorite' ? handleFavoriteChange : handleAvoidChange)(
|
||||
((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).filter(item => item != tag)
|
||||
)}>
|
||||
×
|
||||
</button>
|
||||
)
|
||||
} else {
|
||||
(showExpanded == 'favorite' ? handleFavoriteChange : handleAvoidChange)(
|
||||
((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).concat(tag)
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</TagsWrap>
|
||||
@@ -208,7 +219,7 @@ export function PersonModal({
|
||||
|
||||
<PrefsGrid>
|
||||
<TagsInput
|
||||
title="Не добавлять"
|
||||
title="Исключить"
|
||||
type="avoid"
|
||||
value={form.avoided}
|
||||
availableTags={availableTags}
|
||||
@@ -238,6 +249,7 @@ export function PersonModal({
|
||||
}
|
||||
|
||||
const Overlay = styled.div`
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
@@ -354,6 +366,7 @@ const GenderButton = styled.button<{ $active: boolean }>`
|
||||
cursor: pointer;
|
||||
transition: 0.1s;
|
||||
font-weight: 370;
|
||||
height: 44px;
|
||||
|
||||
&:hover {
|
||||
outline: 1px solid #5EA12D;
|
||||
@@ -363,6 +376,7 @@ const GenderButton = styled.button<{ $active: boolean }>`
|
||||
|
||||
const PrefsGrid = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
+105
-151
@@ -1,5 +1,5 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {declension} from "../../utils/declension";
|
||||
|
||||
interface TagsInputProps {
|
||||
title: string;
|
||||
@@ -13,163 +13,140 @@ interface TagsInputProps {
|
||||
|
||||
export function TagsInput({
|
||||
title,
|
||||
type,
|
||||
value,
|
||||
availableTags,
|
||||
excludedTags,
|
||||
onChange,
|
||||
onExpand,
|
||||
}: TagsInputProps) {
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const matches = useMemo(() => {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
|
||||
if (!normalizedQuery) return [];
|
||||
|
||||
const selected = new Set([...value, ...excludedTags]);
|
||||
|
||||
return availableTags.filter(tag =>
|
||||
tag.toLowerCase().includes(normalizedQuery) && !selected.has(tag),
|
||||
);
|
||||
}, [availableTags, excludedTags, query, value]);
|
||||
|
||||
function addTag(tag: string) {
|
||||
if (value.includes(tag)) return;
|
||||
|
||||
onChange([...value, tag]);
|
||||
setQuery('');
|
||||
}
|
||||
|
||||
function removeTag(tag: string) {
|
||||
onChange(value.filter(item => item !== tag));
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<Title>{title}</Title>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={type === 'avoid' ? 'Например: лук' : 'Например: яблоко'}
|
||||
value={query}
|
||||
onChange={event => setQuery(event.target.value)}
|
||||
/>
|
||||
|
||||
{matches.length > 0 && (
|
||||
<Autocomplete>
|
||||
{matches.map(match => (
|
||||
<button
|
||||
key={match}
|
||||
type="button"
|
||||
onClick={() => addTag(match)}
|
||||
>
|
||||
{match}
|
||||
</button>
|
||||
))}
|
||||
</Autocomplete>
|
||||
)}
|
||||
|
||||
<TagsWrap>
|
||||
{value.slice(0, 4).map(tag => (
|
||||
<Tag key={tag} $type={type}>
|
||||
{tag}
|
||||
<button type="button" onClick={() => removeTag(tag)}>
|
||||
×
|
||||
</button>
|
||||
</Tag>
|
||||
))}
|
||||
{value.length > 4 && <TagCollapse $type={'favorite'} $collapsed={true} onClick={() => onExpand()}>
|
||||
+{value.length - 4}
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<polyline points="18 15 12 9 6 15" />
|
||||
</svg>
|
||||
</TagCollapse>}
|
||||
</TagsWrap>
|
||||
</Column>
|
||||
<Row>
|
||||
<Column>
|
||||
<Title>{title}</Title>
|
||||
<Info>{value.length > 0 ? declension(value.length, ['Выбрана', 'Выбраны', 'Выбраны']) + ': ' + value.length : 'Не выбраны'}</Info>
|
||||
</Column>
|
||||
<AddWrapper>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onExpand}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
</AddWrapper>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
const Column = styled.div`
|
||||
flex: 1;
|
||||
position: relative;
|
||||
const AddWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
const Button = styled.button`
|
||||
padding: 18px 23px;
|
||||
border-radius: 12px;
|
||||
min-width: 160px;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${({theme}) => theme.colors.green};
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
|
||||
span {
|
||||
color: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #F7FBF6;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: #EEF8EB;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 20px;
|
||||
margin-left: 8px;
|
||||
font-weight: 300;
|
||||
}
|
||||
`;
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
flex: 1 1 100%;
|
||||
justify-content: center;
|
||||
`
|
||||
const Column = styled.div`
|
||||
flex: 1 1 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const Info = styled.span`
|
||||
color: ${({theme}) => theme.colors.gray};
|
||||
`;
|
||||
const Title = styled.span`
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: 370;
|
||||
color: ${({ theme }) => theme.colors.darkGreen};
|
||||
color: ${({theme}) => theme.colors.darkGreen};
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const Input = styled.input`
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #e1e1e1;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
margin-bottom: 6px;
|
||||
height: 44px;
|
||||
|
||||
&::placeholder {
|
||||
color:#A3ADA2;
|
||||
}
|
||||
&:hover {
|
||||
border-color: ${({theme}) => theme.colors.gray};
|
||||
}
|
||||
&:focus {
|
||||
border-color: ${({ theme }) => theme.colors.green};
|
||||
}
|
||||
`;
|
||||
|
||||
const Autocomplete = styled.div`
|
||||
position: absolute;
|
||||
top: 65px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border: 1px solid #e1e1e1;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
z-index: 20;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: 0.2s;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const TagsWrap = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
`;
|
||||
|
||||
export const Tag = styled.div<{ $type: 'avoid' | 'favorite' }>`
|
||||
const palette = {
|
||||
inactive: {
|
||||
avoid: {
|
||||
bg: '#FDE3E3',
|
||||
text: '#0c3b2e',
|
||||
},
|
||||
favorite: {
|
||||
bg: '#F1F9D7',
|
||||
text: '#0c3b2e',
|
||||
}
|
||||
},
|
||||
active: {
|
||||
avoid: {
|
||||
bg: '#9F2518',
|
||||
text: '#FDE3E3',
|
||||
},
|
||||
favorite: {
|
||||
bg: '#5EA12D',
|
||||
text: '#F1F9D7',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const Tag = styled.div<{ $type: 'avoid' | 'favorite', $active: boolean }>`
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
background-color: ${({ $type }) => ($type === 'avoid' ? '#fce4ec' : '#f0f4c3')};
|
||||
color: ${({ $type }) => ($type === 'avoid' ? '#c62828' : '#33691e')};
|
||||
background-color: ${({$type,$active}) => palette[$active ? 'active' : 'inactive'][$type].bg};
|
||||
color: ${({$type,$active}) => palette[$active ? 'active' : 'inactive'][$type].text};
|
||||
|
||||
button {
|
||||
margin-left: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
opacity: 0.6;
|
||||
@@ -182,26 +159,3 @@ export const Tag = styled.div<{ $type: 'avoid' | 'favorite' }>`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const TagCollapse = styled(Tag)<{$collapsed: boolean}>`
|
||||
border: 1px solid #E7EDE7;
|
||||
background: transparent;
|
||||
color: ${({ theme }) => theme.colors.darkGreen};
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f4c3;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
stroke: ${({ theme }) => theme.colors.darkGreen};
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
transition: transform 0.3s ease;
|
||||
transform: ${({ $collapsed }) => ($collapsed ? 'rotate(180deg)' : 'none')};
|
||||
}
|
||||
`
|
||||
@@ -91,7 +91,7 @@ const LabelsContainer = styled.div`
|
||||
const Label = styled.span`
|
||||
position: absolute;
|
||||
font-size: 16px;
|
||||
color: #333333;
|
||||
color: #0C3B2E;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 14px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import styled, {css} from 'styled-components';
|
||||
|
||||
export type InputState = 'default' | 'success' | 'error' | 'disabled';
|
||||
|
||||
@@ -23,91 +23,91 @@ const Container = styled.div`
|
||||
`;
|
||||
|
||||
const Label = styled.label<{ $state: InputState }>`
|
||||
font-size: 16px;
|
||||
color: #000;
|
||||
margin-bottom: 4px;
|
||||
|
||||
${({ $state }) => $state === 'disabled' && css`
|
||||
color: #9e9e9e;
|
||||
`}
|
||||
font-size: 16px;
|
||||
color: #000;
|
||||
margin-bottom: 4px;
|
||||
|
||||
${({$state}) => $state === 'disabled' && css`
|
||||
color: #9e9e9e;
|
||||
`}
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div<{ $state: InputState; $allowClear: boolean }>`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
transition: border-color 0.2s;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
/* Default State */
|
||||
border: 1px solid #e0e0e0;
|
||||
&:focus-within {
|
||||
border-color: #b3b3b3;
|
||||
}
|
||||
|
||||
/* Success State */
|
||||
${({ $state }) => $state === 'success' && css`
|
||||
border: 2px solid #4CAF50;
|
||||
`}
|
||||
|
||||
/* Error State */
|
||||
${({ $state }) => $state === 'error' && css`
|
||||
border: 2px solid #F44336;
|
||||
`}
|
||||
|
||||
/* Disabled State */
|
||||
${({ $state }) => $state === 'disabled' && css`
|
||||
background-color: #f2f2f2;
|
||||
/* Default State */
|
||||
border: 1px solid #e0e0e0;
|
||||
`}
|
||||
|
||||
/* Padding adjustments for clear button */
|
||||
${({ $allowClear }) => $allowClear && css`
|
||||
input {
|
||||
padding-right: 40px;
|
||||
&:focus-within {
|
||||
border-color: #b3b3b3;
|
||||
}
|
||||
`}
|
||||
|
||||
/* Success State */
|
||||
${({$state}) => $state === 'success' && css`
|
||||
border: 2px solid #4CAF50;
|
||||
`} /* Error State */ ${({$state}) => $state === 'error' && css`
|
||||
border: 2px solid #F44336;
|
||||
`} /* Disabled State */ ${({$state}) => $state === 'disabled' && css`
|
||||
background-color: #f2f2f2;
|
||||
border: 1px solid #e0e0e0;
|
||||
`} /* Padding adjustments for clear button */ ${({$allowClear}) => $allowClear && css`
|
||||
input {
|
||||
padding-right: 40px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const Input = styled.input<{ $state: InputState }>`
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
|
||||
&::placeholder {
|
||||
color: #A3ADA2;
|
||||
}
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font-size: 16px;
|
||||
color: #0C3B2E;
|
||||
|
||||
${({ $state }) => $state === 'disabled' && css`
|
||||
color: #9e9e9e;
|
||||
cursor: not-allowed;
|
||||
`}
|
||||
&::placeholder {
|
||||
color: #A3ADA2;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #72b541;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: #72b541;
|
||||
}
|
||||
|
||||
${({$state}) => $state === 'disabled' && css`
|
||||
color: #9e9e9e;
|
||||
cursor: not-allowed;
|
||||
`}
|
||||
`;
|
||||
|
||||
const ClearButton = styled.button<{ $state: InputState }>`
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
color: #9e9e9e;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
color: #9e9e9e;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
&:hover {
|
||||
color: #0C3B2E;
|
||||
}
|
||||
|
||||
${({ $state }) => $state === 'disabled' && css`
|
||||
cursor: not-allowed;
|
||||
color: #c0c0c0;
|
||||
`}
|
||||
${({$state}) => $state === 'disabled' && css`
|
||||
cursor: not-allowed;
|
||||
color: #c0c0c0;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const TextInput: React.FC<TextInputProps> = ({
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
// Стилизованные компоненты
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
`;
|
||||
|
||||
const StyledTextarea = styled.textarea`
|
||||
width: 100%;
|
||||
min-height: 224px;
|
||||
/* Добавляем нижний отступ, чтобы текст не заезжал под счетчик */
|
||||
padding: 16px 16px 40px 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
color: #0C3B2E;
|
||||
background: white;
|
||||
resize: vertical; /* Разрешаем растягивать по высоте */
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease-in-out;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #a0a0a0;
|
||||
}
|
||||
&:focus {
|
||||
border-color: #72b541;
|
||||
}
|
||||
`;
|
||||
|
||||
const Counter = styled.div`
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
right: 0;
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
// Интерфейс пропсов
|
||||
interface FeedbackTextareaProps {
|
||||
placeholder?: string;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
interface FeedbackTextareaProps {
|
||||
value: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
placeholder?: string;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
const FeedbackTextarea: React.FC<FeedbackTextareaProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Напишите, что бы вы хотели поменять',
|
||||
maxLength = 250,
|
||||
}) => {
|
||||
return (
|
||||
<Container>
|
||||
<StyledTextarea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
<Counter>
|
||||
{value.length}/{maxLength}
|
||||
</Counter>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedbackTextarea;
|
||||
Reference in New Issue
Block a user