diff --git a/backend/bot.js b/backend/bot.js index 29cddd3..7dc0f44 100644 --- a/backend/bot.js +++ b/backend/bot.js @@ -87,12 +87,12 @@ app.post( let text = `🧼 Расчёт мыла: ${soapName}\n\n`; text += `🔹 Вес мыла: ${weight} г\n`; - text += `🔹 Цена за 1 кг основы: ${basePrice} ₽/кг\n\n`; + // text += `🔹 Цена за 1 кг основы: ${basePrice} ₽/кг\n\n`; - text += `🔹 Отдушка: ${aromaWeight} г по ${aromaPrice} ₽/фасовка\n`; - text += `🔹 Пигмент: ${pigmentWeight} г по ${pigmentPrice} ₽/фасовка\n\n`; + // text += `🔹 Отдушка: ${aromaWeight} г по ${aromaPrice} ₽/фасовка\n`; + // text += `🔹 Пигмент: ${pigmentWeight} г по ${pigmentPrice} ₽/фасовка\n\n`; - text += `🔹 Цена формы: ${moldPrice} ₽\n\n`; + // text += `🔹 Цена формы: ${moldPrice} ₽\n\n`; text += `🔹 Упаковка:\n`; text += ` • Пакет/коробка: ${box} ₽\n`; diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 38c89fb..e72d494 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1,3 +1,5 @@ +@import url('https://fonts.googleapis.com/css2?family=Sofia+Sans+Condensed:ital,wght@0,100..900;1,100..900&display=swap'); + @import "tailwindcss"; :root { @@ -8,5 +10,43 @@ body { background: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + font-family: "Sofia Sans Condensed", sans-serif; } + +/* Скрываем стрелочки у input[type=number] */ +input[type='number']::-webkit-outer-spin-button, +input[type='number']::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} +input[type='number'] { + appearance: textfield; + -moz-appearance: textfield; +} + +/* Скрываем надпись «файл не выбран» и название файла */ +input[type="file"] { + color: transparent; /* сам путь/имя файла и надписи станут прозрачными */ + position: relative; + z-index: 1; +} + +/* Стилизуем кнопку «Выбрать файл» отдельно (псевдо-элемент, у разных браузеров он может называться по-разному) */ +input[type="file"]::file-selector-button { + color: #e5e7eb; /* текст кнопки (gray-200) */ + background-color: #374151; /* фон кнопки (gray-700) */ + border: none; + padding: 0.5rem 1rem; + border-radius: 0.375rem; /* rounded-md */ + cursor: pointer; +} + +/* Для старых версий IE/Edge: */ +input[type="file"]::-ms-browse { + color: #e5e7eb; + background-color: #374151; + border: none; + padding: 0.5rem 1rem; + border-radius: 0.375rem; + cursor: pointer; +} \ No newline at end of file diff --git a/frontend/components/SoapCalculator.tsx b/frontend/components/SoapCalculator.tsx index 1fd34c4..0a8bbb2 100644 --- a/frontend/components/SoapCalculator.tsx +++ b/frontend/components/SoapCalculator.tsx @@ -1,66 +1,81 @@ // components/SoapCalculator.tsx 'use client'; -import { useState, useEffect, ChangeEvent } from 'react'; +import { useState, useEffect, ChangeEvent, FormEvent } from 'react'; import { calculateTotal } from '@/lib/calc'; +import Image from 'next/image'; type InputNumberProps = { label: string; value: string; onChange: (v: string) => void; - placeholder?: string; }; -const InputNumber = ({ label, value, onChange, placeholder }: InputNumberProps) => ( - -); +const InputNumber = ({ label, value, onChange }: InputNumberProps) => { + // Генерируем id на основе текста лейбла (без пробелов) + const id = label.toLowerCase().replace(/\s+/g, '-'); -const CostBlock = ({ - title, - value, - highlight = false, -}: { - title: string; - value: number; - highlight?: boolean; -}) => ( -
- {title}: {value.toFixed(1)} руб -
-); + return ( +
+ onChange(e.target.value)} + placeholder=" " + className={` + peer + h-10 w-full + bg-gray-700 + border-2 border-gray-600 + rounded-md + text-gray-200 + placeholder-transparent + pl-3 + focus:outline-none focus:border-sky-500 + appearance-none + `} + /> + +
+ ); +}; export default function SoapCalculator() { - // 1) Поля ввода как строки (по умолчанию пустые) const [soapName, setSoapName] = useState(''); - const [weight, setWeight] = useState(''); // г - const [basePrice, setBasePrice] = useState(''); // руб/кг - const [aromaPrice, setAromaPrice] = useState(''); // руб/фасовка - const [aromaWeight, setAromaWeight] = useState(''); // г фасовка - const [pigmentPrice, setPigmentPrice] = useState(''); // руб/фасовка - const [pigmentWeight, setPigmentWeight] = useState(''); // г фасовка - const [moldPrice, setMoldPrice] = useState(''); // руб за форму - const [box, setBox] = useState(''); // руб - const [filler, setFiller] = useState(''); // руб - const [ribbon, setRibbon] = useState(''); // руб - const [labelValue, setLabelValue] = useState(''); // руб наклейка - const [markup, setMarkup] = useState(''); // % + const [weight, setWeight] = useState(''); + const [basePrice, setBasePrice] = useState(''); + const [aromaPrice, setAromaPrice] = useState(''); + const [aromaWeight, setAromaWeight] = useState(''); + const [pigmentPrice, setPigmentPrice] = useState(''); + const [pigmentWeight, setPigmentWeight] = useState(''); + const [moldPrice, setMoldPrice] = useState(''); + const [box, setBox] = useState(''); + const [filler, setFiller] = useState(''); + const [ribbon, setRibbon] = useState(''); + const [labelValue, setLabelValue] = useState(''); + const [markup, setMarkup] = useState(''); - // Файл фотографии и chat_id const [photoFile, setPhotoFile] = useState(null); const [chatId, setChatId] = useState(null); - // 2) Извлекаем chat_id из URL при монтировании + // При монтировании достаём chat_id из URL useEffect(() => { const params = new URLSearchParams(window.location.search); const id = params.get('chat_id'); @@ -69,25 +84,25 @@ export default function SoapCalculator() { } }, []); - // 3) Преобразуем строковые поля в числа (или 0, если не число) + // Конвертация строковых значений в числа const toNum = (str: string) => { const n = parseFloat(str.replace(',', '.')); return isNaN(n) ? 0 : n; }; - const weightNum = toNum(weight); - const basePriceNum = toNum(basePrice); - const aromaPriceNum = toNum(aromaPrice); - const aromaWeightNum = toNum(aromaWeight); - const pigmentPriceNum = toNum(pigmentPrice); + const weightNum = toNum(weight); + const basePriceNum = toNum(basePrice); + const aromaPriceNum = toNum(aromaPrice); + const aromaWeightNum = toNum(aromaWeight); + const pigmentPriceNum = toNum(pigmentPrice); const pigmentWeightNum = toNum(pigmentWeight); - const moldPriceNum = toNum(moldPrice); - const boxNum = toNum(box); - const fillerNum = toNum(filler); - const ribbonNum = toNum(ribbon); - const labelNum = toNum(labelValue); - const markupNum = toNum(markup); + const moldPriceNum = toNum(moldPrice); + const boxNum = toNum(box); + const fillerNum = toNum(filler); + const ribbonNum = toNum(ribbon); + const labelNum = toNum(labelValue); + const markupNum = toNum(markup); - // 4) Считаем все базовые значения через calculateTotal + // Считаем все базовые значения через calculateTotal const result = calculateTotal({ weight: weightNum, basePrice: basePriceNum, @@ -104,11 +119,9 @@ export default function SoapCalculator() { }, }); - // 5) Итоговые значения const finalPrice = result.total * (1 + markupNum / 100); const pricePer100g = weightNum > 0 ? (finalPrice / weightNum) * 100 : 0; - // 6) Обработчик изменения фото const handlePhotoChange = (e: ChangeEvent) => { if (e.target.files && e.target.files[0]) { setPhotoFile(e.target.files[0]); @@ -117,14 +130,13 @@ export default function SoapCalculator() { } }; - // 7) Обработчик отправки формы на backend - const handleSubmit = async () => { + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); if (!chatId) { alert('❗ Не найден chat_id. Откройте калькулятор через Telegram-бота.'); return; } - // Собираем FormData const formData = new FormData(); formData.append('chat_id', chatId); formData.append('soapName', soapName || ''); @@ -149,8 +161,6 @@ export default function SoapCalculator() { } try { - // Замените URL на публичный адрес вашего backend (Express/ngrok/другой хостинг) - // Например: 'https://abcd1234.ngrok.io/api/submit' const res = await fetch('https://api-dosoap.duckdns.org/api/submit', { method: 'POST', body: formData, @@ -158,8 +168,6 @@ export default function SoapCalculator() { if (res.ok) { alert('✅ Расчёт успешно отправлен в Telegram!'); - - // Сбрасываем все поля на пустые строки / null setSoapName(''); setWeight(''); setBasePrice(''); @@ -174,8 +182,6 @@ export default function SoapCalculator() { setLabelValue(''); setMarkup(''); setPhotoFile(null); - - // Прокручиваем страницу вверх window.scrollTo({ top: 0, behavior: 'smooth' }); } else { const text = await res.text(); @@ -188,51 +194,102 @@ export default function SoapCalculator() { }; return ( -
+
{chatId === null && ( -
+
❗ Не найден chat_id. Откройте калькулятор через Telegram-бота.
)} +
+ Logo +
+ {/* Название мыла */} -
@@ -260,13 +315,11 @@ export default function SoapCalculator() { label="Цена пигмента, руб" value={pigmentPrice} onChange={setPigmentPrice} - placeholder="Введите, например 20" />
@@ -276,61 +329,64 @@ export default function SoapCalculator() { label="Цена формы, руб" value={moldPrice} onChange={setMoldPrice} - placeholder="Введите, например 500" /> {/* Блок «Упаковка» */}
- - - + + +
- {/* Блок «Наценка и цена 100 г» */} -
- +
+
{/* Кнопка «Отправить расчёт» */} -
+ ); } + +type CostBlockProps = { + title: string; + value: number; + highlight?: boolean; +}; + +const CostBlock = ({ title, value, highlight = false }: CostBlockProps) => ( +
+ {title}: {value.toFixed(1)} руб +
+); diff --git a/frontend/public/logo.svg b/frontend/public/logo.svg new file mode 100644 index 0000000..bfe0d51 --- /dev/null +++ b/frontend/public/logo.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file