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) => (
-
- {label}
- onChange(e.target.value)}
- />
-
-);
+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
+ `}
+ />
+
+ {label}
+
+
+ );
+};
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 (
-
@@ -276,61 +329,64 @@ export default function SoapCalculator() {
label="Цена формы, руб"
value={moldPrice}
onChange={setMoldPrice}
- placeholder="Введите, например 500"
/>
{/* Блок «Упаковка» */}
-
-
-
+
+
+
-
{/* Блок «Наценка и цена 100 г» */}
-
-
+
+
{/* Кнопка «Отправить расчёт» */}
Отправить расчёт в Telegram
-
+
);
}
+
+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