diff --git a/frontend/src/components/blog/SearchIcon.astro b/frontend/src/components/blog/SearchIcon.astro deleted file mode 100644 index bbd7de6..0000000 --- a/frontend/src/components/blog/SearchIcon.astro +++ /dev/null @@ -1,55 +0,0 @@ ---- -// Компонент иконки поиска (лупа) ---- - - - - - - diff --git a/frontend/src/components/reviews/ReviewCard.astro b/frontend/src/components/reviews/ReviewCard.astro index f111de6..ec8d6aa 100644 --- a/frontend/src/components/reviews/ReviewCard.astro +++ b/frontend/src/components/reviews/ReviewCard.astro @@ -72,41 +72,28 @@ const formatDate = (dateStr: string) => {
-

Полезен ли этот отзыв?

-
- - -
-
- - - - - {votesCount} - -
+ +
@@ -211,70 +198,43 @@ const formatDate = (dateStr: string) => { .voting-section { margin-top: auto; - padding-top: 1.5rem; + padding-top: 1.25rem; border-top: 1px solid #e2e8f0; display: flex; - flex-direction: column; - gap: 0.75rem; - } - - .voting-question { - color: #475569; - font-size: 0.875rem; - font-weight: 600; - margin: 0; - } - - .voting-auth-note { - color: #dc2626 !important; - font-size: 0.7rem !important; - font-style: italic; - margin: 0; - } - - .voting-buttons { - display: flex; - gap: 0.75rem; - align-items: center; + gap: 1rem; + justify-content: center; } .vote-btn { display: inline-flex; align-items: center; - gap: 0.375rem; - padding: 0.5rem 0.875rem; - border: 2px solid #e2e8f0; + gap: 0.5rem; + padding: 0.625rem 1rem; + border: 1px solid #e2e8f0; border-radius: 0.5rem; background: #ffffff; cursor: pointer; transition: all 0.2s ease; font-size: 0.875rem; - font-weight: 600; + font-weight: 500; color: #64748b; } .vote-btn svg { - width: 1.25rem; - height: 1.25rem; + width: 1.125rem; + height: 1.125rem; } - .reaction-icon { - width: 1.25rem; - height: 1.25rem; + .vote-btn:hover { + border-color: #1e3050; + color: #1e3050; + background: #f8fafc; } - .vote-btn-up:hover, - .vote-btn-up.active { - border-color: #22c55e; - color: #22c55e; - background: rgba(34, 197, 94, 0.1); - } - - .vote-btn-down:hover, - .vote-btn-down.active { - border-color: #ef4444; - color: #ef4444; - background: rgba(239, 68, 68, 0.1); + .vote-btn.active { + background: #1e3050; + border-color: #1e3050; + color: #ffffff; } .vote-btn:disabled { @@ -302,27 +262,6 @@ const formatDate = (dateStr: string) => { } } - .voting-stars { - display: flex; - align-items: center; - } - - .voting-stats { - display: flex; - align-items: center; - gap: 1rem; - margin-top: 0.5rem; - } - - .votes-count { - display: inline-flex; - align-items: center; - gap: 0.375rem; - color: #64748b; - font-size: 0.875rem; - font-weight: 500; - } - .icon { width: 1.125rem; height: 1.125rem; @@ -492,13 +431,12 @@ const formatDate = (dateStr: string) => { return; } - const data = await response.json(); - buttons.forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - - const votesNumber = section.querySelector('.votes-number'); - if (votesNumber && typeof data.likes === 'number') { - votesNumber.textContent = data.likes.toString(); + const resData = await response.json(); + const btnCountEl = btn.querySelector('.votes-number'); + if (btnCountEl) { + const isLike = voteType === 'likes'; + const newCount = isLike ? (resData.likes || 0) : (resData.dislikes || 0); + btnCountEl.textContent = String(newCount); } } catch (err) { console.error('[ReviewCard] Vote failed:', err); @@ -541,17 +479,21 @@ const formatDate = (dateStr: string) => { return; } - if (!response.ok) { +if (!response.ok) { + const errorText = await response.text(); + console.error('[Vote JS] Error:', errorText); return; } - const data = await response.json(); - buttons.forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - - const votesNumber = section.querySelector('.votes-number'); - if (votesNumber && typeof data.likes === 'number') { - votesNumber.textContent = data.likes.toString(); + const resData = await response.json(); + console.log('[Vote JS] Response:', resData); + + const btnCountEl = btn.querySelector('.votes-number'); + if (btnCountEl) { + const isLike = voteType === 'likes'; + const newCount = isLike ? (resData.likes || 0) : (resData.dislikes || 0); + btnCountEl.textContent = String(newCount); + console.log('[Vote JS] Set count to:', newCount); } } catch (err) { console.error('[ReviewCard] Vote failed:', err); diff --git a/frontend/src/components/reviews/ReviewForm.tsx b/frontend/src/components/reviews/ReviewForm.tsx index 6594e28..6feb59f 100644 --- a/frontend/src/components/reviews/ReviewForm.tsx +++ b/frontend/src/components/reviews/ReviewForm.tsx @@ -16,12 +16,6 @@ interface ReviewFormProps { }; } -const EMOJIS = [ - "👍", "👎", "❤️", "😊", "😂", "🎉", "🔥", "👏", - "😢", "😮", "😡", "🙏", "⭐", "💯", "❤️‍🔥", "🤔", - "👀", "💪", "🚀", "✨" -]; - const DANGEROUS_PATTERNS = [ /)<[^<]*)*<\/script>/gi, /javascript:/gi, @@ -34,7 +28,7 @@ const DANGEROUS_PATTERNS = [ /url\s*\(\s*['"]*\s*javascript:/gi, ]; -const MAX_TEXT_LENGTH = 2000; +const MAX_TEXT_LENGTH = 500; const MIN_TEXT_LENGTH = 50; const MAX_NAME_LENGTH = 50; const MAX_PROFESSION_LENGTH = 100; @@ -47,15 +41,23 @@ interface ValidationErrors { text?: string; } +const RATING_LABELS: Record = { + 1: "Плохо", + 2: "Не очень", + 3: "Нормально", + 4: "Хорошо", + 5: "Отлично", +}; + export default function ReviewForm(props: ReviewFormProps) { const [name, setName] = createSignal(""); const [surname, setSurname] = createSignal(""); const [profession, setProfession] = createSignal(""); const [rating, setRating] = createSignal(0); + const [hoverRating, setHoverRating] = createSignal(0); const [text, setText] = createSignal(""); const [errors, setErrors] = createSignal({}); const [touched, setTouched] = createSignal<{ [key: string]: boolean }>({}); - const [showEmojiPicker, setShowEmojiPicker] = createSignal(false); createEffect(() => { if (props.user?.name) { @@ -83,38 +85,38 @@ export default function ReviewForm(props: ReviewFormProps) { const validateName = (value: string): string | undefined => { const trimmed = value.trim(); - if (!trimmed) return "Имя обязательно"; - if (trimmed.length > MAX_NAME_LENGTH) return `Максимум ${MAX_NAME_LENGTH} символов`; + if (!trimmed) return "Введите имя"; + if (trimmed.length > MAX_NAME_LENGTH) return `Макс. ${MAX_NAME_LENGTH} символов`; return undefined; }; const validateSurname = (value: string): string | undefined => { const trimmed = value.trim(); - if (!trimmed) return "Фамилия обязательна"; - if (trimmed.length > MAX_NAME_LENGTH) return `Максимум ${MAX_NAME_LENGTH} символов`; + if (!trimmed) return "Введите фамилию"; + if (trimmed.length > MAX_NAME_LENGTH) return `Макс. ${MAX_NAME_LENGTH} символов`; return undefined; }; const validateProfession = (value: string): string | undefined => { const trimmed = value.trim(); - if (!trimmed) return "Профессия обязательна"; - if (trimmed.length > MAX_PROFESSION_LENGTH) return `Максимум ${MAX_PROFESSION_LENGTH} символов`; + if (!trimmed) return "Укажите профессию"; + if (trimmed.length > MAX_PROFESSION_LENGTH) return `Макс. ${MAX_PROFESSION_LENGTH} символов`; return undefined; }; const validateRating = (value: number): string | undefined => { - if (!value || value < 1 || value > 5) return "Выберите оценку"; + if (!value || value < 1 || value > 5) return "Поставьте оценку"; return undefined; }; const validateText = (value: string): string | undefined => { const trimmed = value.trim(); - if (!trimmed) return "Текст отзыва обязателен"; + if (!trimmed) return "Напишите отзыв"; if (trimmed.length < MIN_TEXT_LENGTH) return `Минимум ${MIN_TEXT_LENGTH} символов`; if (trimmed.length > MAX_TEXT_LENGTH) - return `Максимум ${MAX_TEXT_LENGTH} символов`; - if (containsDangerousContent(trimmed)) return "Обнаружен опасный контент"; + return `Макс. ${MAX_TEXT_LENGTH} символов`; + if (containsDangerousContent(trimmed)) return "Недопустимый контент"; return undefined; }; @@ -138,11 +140,6 @@ export default function ReviewForm(props: ReviewFormProps) { } }; - const addEmoji = (emoji: string) => { - setText((prev) => prev + emoji); - setShowEmojiPicker(false); - }; - const validateForm = (): boolean => { const newErrors: ValidationErrors = { name: validateName(name()), @@ -187,7 +184,7 @@ export default function ReviewForm(props: ReviewFormProps) { const handleBlur = (field: string) => { setTouched((prev) => ({ ...prev, [field]: true })); - + const fieldValidators: Record string | undefined> = { name: () => validateName(name()), surname: () => validateSurname(surname()), @@ -203,21 +200,13 @@ export default function ReviewForm(props: ReviewFormProps) { }; const isValid = () => { - return !errors().name && !errors().surname && !errors().profession && - !errors().rating && !errors().text && - name().trim() && surname().trim() && profession().trim() && + return !errors().name && !errors().surname && !errors().profession && + !errors().rating && !errors().text && + name().trim() && surname().trim() && profession().trim() && rating() > 0 && text().trim(); }; - const ratingOptions = [ - { value: 5, label: "5 — Отлично" }, - { value: 4, label: "4 — Хорошо" }, - { value: 3, label: "3 — Удовлетворительно" }, - { value: 2, label: "2 — Плохо" }, - { value: 1, label: "1 — Очень плохо" }, - ]; - - const getInputClass = (field: keyof ValidationErrors) => { + const getFieldClass = (field: keyof ValidationErrors) => { const hasError = errors()[field] && touched()[field]; return `w-full px-4 py-3 rounded-xl border transition-all resize-none bg-white text-gray-900 placeholder-gray-400 outline-none ${ hasError @@ -226,172 +215,430 @@ export default function ReviewForm(props: ReviewFormProps) { }`; }; + const displayRating = () => hoverRating() || rating(); + return ( -
-
-
- - setName(e.currentTarget.value)} - onBlur={() => handleBlur("name")} - placeholder="Иван" - class={getInputClass("name")} - /> - -

- - - - {errors().name} -

-
+
+
+
+

Оставить отзыв

+

Ваш опыт поможет другим клиентам

-
- - setSurname(e.currentTarget.value)} - onBlur={() => handleBlur("surname")} - placeholder="Иванов" - class={getInputClass("surname")} - /> - -

- - - - {errors().surname} -

-
-
-
+ +
+
+
+ + setName(e.currentTarget.value)} + onBlur={() => handleBlur("name")} + placeholder="Иван" + class={getFieldClass("name")} + /> + +

{errors().name}

+
+
+
+ + setSurname(e.currentTarget.value)} + onBlur={() => handleBlur("surname")} + placeholder="Иванов" + class={getFieldClass("surname")} + /> + +

{errors().surname}

+
+
+
+
-
- - setProfession(e.currentTarget.value)} - onBlur={() => handleBlur("profession")} - placeholder="Например: Предприниматель, Врач, Инженер..." - class={getInputClass("profession")} - /> - -

- - - - {errors().profession} -

-
-
+
+
+ + setProfession(e.currentTarget.value)} + onBlur={() => handleBlur("profession")} + placeholder="Например: Водитель, Предприниматель..." + class={getFieldClass("profession")} + /> + +

{errors().profession}

+
+
+
-
- - - -

- - - - {errors().rating} -

-
-
- -
-
- -
- - -
-
- - {(emoji) => ( +
+
+ +
+
+ + {(star) => ( )}
+ 0}> + {RATING_LABELS[rating()]} +
- + +

{errors().rating}

+
+
-
-