Будь ласка, використовуйте цей ідентифікатор, щоб цитувати або посилатися на цей матеріал: https://er.chdtu.edu.ua/handle/ChSTU/9707
Назва: Інтернет-магазин з продажу товарів для настільного тенісу
Автори: Підгорний, Микола Володимирович
Каштан, В'ячеслав Сергійович
Ключові слова: WEB-САЙТ;ІНТЕРНЕТ-МАГАЗИН;SQLALCHEMY;JAVASCRIPT;CSS;БАЗА ДАНИХ.
Дата публікації: 11-чер-2026
Короткий огляд (реферат): Актуальність теми. Актуальність теми зумовлена потребою стрімкого розвитку електронної комерції у зв’язку зростанням ролі інтернет-магазинів у сфері спортивних товарів. У сучасних умовах споживачі все частіше обирають онлайн-покупки через їх зручність, швидкість, можливість порівняння великої кількості товарів та доступ до ширшого асортименту, ніж у традиційних торгових точках. Це вимагає від інтернет-магазинів впровадження нових підходів до організації продажів, удосконалення функціоналу веб-платформ та підвищення якості обслуговування клієнтів. Метою роботи є проектування та розробка інтернет-магазину з продажу товарів для настільного тенісу, забезпечивши користувачам зручним та зрозумілим інструментом для онлайн-покупок з можливістю з можливістю адміністрування контенту та обробки замовлень.
URI (Уніфікований ідентифікатор ресурсу): https://er.chdtu.edu.ua/handle/ChSTU/9707
Розташовується у зібраннях:122 Комп’ютерні науки (Комп’ютерні науки та прикладне програмування)

Файли цього матеріалу:
Файл Опис РозмірФормат 
Пояснювальна записка_Каштан В'ячеслав_КН-2201_2025-2026.pdf
  Restricted Access
3.69 MBAdobe PDFПереглянути/Відкрити    Запит копії


Усі матеріали в архіві електронних ресурсів захищено авторським правом, усі права збережено.

Extracted text
МІНІСТЕРСТВО ОСВІТИ І НАУКИ УКРАЇНИ 
ЧЕРКАСЬКИЙ ДЕРЖАВНИЙ ТЕХНОЛОГІЧНИЙ УНІВЕРСИТЕТ 
 
Факультет інформаційних технологій і систем 
 
Кафедра комп’ютерних наук та системного аналізу 
 
 
 
 
 
 
 
Пояснювальна записка 
до кваліфікаційної роботи 
                                         бакалавра       
 (освітній рівень) 
 
на тему: «Інтернет-магазин з продажу товарів для настільного тенісу» 
 
 
 
Виконав: студент 4 курсу, групи КН-2201 
  
спеціальності 122 «Комп’ютерні науки» 
                                                             (шифр і назва спеціальності) 
 
Освітня програма «Комп’ютерні науки та  
                                                                (назва освітньої програми) 
прикладне програмування»                         
 
В'ячеслав КАШТАН 
 
Керівник                      Микола ПІДГОРНИЙ 
                                                                (ім’я та прізвище) 
 
Рецензент                    Валентин МЕЛЬНИК 
                                                  (ім’я та прізвище) 
 
 
 
 
 
 
 
 
Черкаси 2026 року 
 
 
 
 
Черкаський державний технологічний університет 
Факультет Інформаційних технологій і систем 
Кафедра Комп’ютерних наук та системного аналізу 
Освітній рівень Бакалавр 
Спеціальність 122 – Комп’ютерні науки 
Освітня програма Комп’ютерні науки та прикладне програмування 
 
 
ЗАТВЕРДЖУЮ 
Завідувач кафедри КН та СА  
_______________ Юрій ТРИУС 
«____» _____________ 2026р. 
 
 
ЗАВДАННЯ 
на кваліфікаційну роботу бакалавра студенту 
Каштану В'ячеславу Сергійовичу 
(прізвище, ім’я, по батькові) 
1. Тема роботи: «Інтернет-магазин з продажу товарів для настільного тенісу» 
 
Керівник роботи: Підгорний Микола Володимирович к.т.н., професор 
(прізвище, ім’я, по батькові, науковий ступінь, вчене звання) 
 
затверджені наказом університету від «12» березня 2026 р. № 56/03-03 
 
2. Строк подання студентом роботи  «    »                    2026 року  
3. Вихідні дані до роботи:  
спроєктувати інтернет-магазин із застосуванням архітектурного підходу MVC та сучасного 
технологічного стеку, що включає Python, Flask, SQLAlchemy, SQLite, Jinja2, Bootstrap та JavaScript 
JavaScript і забезпечить ефективну взаємодію між серверною та клієнтською частинами застосунку 
проєктованого вебзастосунку 
 
4. Зміст пояснювальної записки (перелік питань, що їх належить розробити): 
Вступ 
4.1. Дослідження предметної області 
4.2. Проектування інтернет-магазину з продажу товарів для настільного тенісу 
4.3. Програмна реалізація 
4.4 Розгортання та тестування 
Висновки  
5 . Перелік додатків (з точним зазначенням назв додатків): 
 5.1. Додаток А. Специфікація 482.ЧДТУ.62503-01 
 5 .2. Додаток Б. Текст програми 482.ЧДТУ.62503-01 12 01 
  1. Інструкція користувача 
  
 
 
 
 6. Консультанти розділів роботи 
Прізвище, ініціали та Підпис, дата 
Розділ посада 
завдання видав завдання прийняв 
консультанта 
    
    
 
7. Дата видачі завдання ___14.01.2026 р.____ 
 
 
 
КАЛЕНДАРНИЙ ПЛАН 
Строк виконання 
№ з/п Назва етапів кваліфікаційної роботи бакалавра Примітка 
етапів роботи 
1 Видача завдання на кваліфікаційну роботу Виконано 
до 15.03.2026 
бакалавра. 
2 Аналіз літературних джерел, об’єкту та предмету Виконано 
до 21.03.2026 
дослідження. 
3 Написання теоретичного розділу кваліфікаційної Виконано 
до 02.04.2026 
роботи бакалавра. 
4 Написання аналітичного розділу (аналіз об’єкту Виконано 
до 10.04. 2026 
й предмету дослідження). 
5 Написання практичних розділів й висновків по Виконано 
до 24.05.2026 
роботі. 
6 Передзахист кваліфікаційної роботи бакалавра Виконано 
до 25.05.2026 
на засіданні кафедри КНСА. 
7 Подання роботи завідувачу кафедри КНСА до 01.06. 2026 Виконано 
8 Захист кваліфікаційної роботи бакалавра. __.06.2026  
    
    
    
    
 
 
 
 
Студент                                   _____________________________            В'ячеслав КАШТАН 
(підпис)                                                                     
 
Керівник роботи                     ____________________________              Микола ПІДГОРНИЙ 
       (підпис)                                                                      
 
 
РЕФЕРАТ 
Кваліфікаційна робота бакалавра містить: 110 сторінок, 51 рисунки, 
4 таблиці, 12 використаних джерел, 2 додатки. 
Актуальність теми. Актуальність теми зумовлена потребою стрімкого 
розвитку електронної комерції у зв’язку зростанням ролі інтернет-магазинів у сфері 
спортивних товарів. У сучасних умовах споживачі все частіше обирають онлайн-
покупки через їх зручність, швидкість, можливість порівняння великої кількості 
товарів та доступ до ширшого асортименту, ніж у традиційних торгових точках. 
Це вимагає від інтернет-магазинів впровадження нових підходів до організації 
продажів, удосконалення функціоналу веб-платформ та підвищення якості 
обслуговування клієнтів. 
Розробка вузько направленого інтернет-магазинів з продажу товарів для 
настільного тенісу дозволить визначити ефективні шляхи покращення онлайн-
торгівлі та підвищити її доступності, водночас забезпечивши користувачів зручним 
та зрозумілим інструментом для онлайн-покупок. 
Метою роботи є проектування та розробка інтернет-магазину з продажу 
товарів для настільного тенісу, забезпечивши користувачам зручним та зрозумілим 
інструментом для онлайн-покупок з можливістю з можливістю адміністрування 
контенту та обробки замовлень. 
Завдання кваліфікаційної роботи бакалавра: 
– дослідити існуючі рішення для продажу спортивних,  зокрема товарів для 
настільного тенісу; 
– спроектувати та розробити інтернет-магазин із інтуїтивно зрозумілим 
інтерфейсом та з урахуванням потреб користувачів та адміністраторів; 
– розробити базу даних для збереження інформації про користувачів, 
замовлення та спортивні товари; 
– реалізувати адміністративну, з можливістю створення редагування та 
видалення контенту, користувачів, замовлень та клієнтську частину з використанням 
сучасних  технологій веб-розробки. 
 
 
Об’єкт дослідження: процеси створення інтернет-магазину для продажу 
товарів для настільного тенісу. 
Предмет дослідження: інтернет-магазин з продажу товарів для настільного 
тенісу. 
Методи дослідження: в роботі застосовувалися вивчення практичних 
розробок, аналіз існуючих аналогів та методів їх моделювання за допомогою 
програмних засобів. 
Ключові слова: WEB-САЙТ, ІНТЕРНЕТ-МАГАЗИН, FLASK, PYTHON, 
SQLITE, SQLALCHEMY, HTML, CSS, JAVASCRIPT, БАЗА ДАНИХ. 
  
 
 
ABSTRACT 
The qualification work of bachelor contains: 89 pages, 33 figures, 1 table, 
12 sources used, 2 appendices. 
Actuality of theme. The relevance of the topic is due to the need for the rapid 
development of e-commerce due to the growing role of online stores in the field of sporting 
goods. In modern conditions, consumers are increasingly choosing online shopping due to 
their convenience, speed, the ability to compare a large number of products and access to a 
wider range than in traditional retail outlets. 
This requires online stores to implement new approaches to organizing sales, 
improving the functionality of web platforms and improving the quality of customer service. 
The development of a narrowly focused online store for selling table tennis products 
will allow identifying effective ways to improve online trading and increase its accessibility, 
while providing users with a convenient and understandable tool for online shopping. 
The purpose of the work is to design and develop an online store for the sale of table 
tennis goods, providing users with a convenient and understandable tool for online shopping 
with the ability to administer content and process orders. 
Objectives of qualifying work: 
– to study existing solutions for the sale of sports, in particular table tennis goods; 
– to design and develop an online store with an intuitive interface and taking into 
account the needs of users and administrators; 
– to develop a database for storing information about users, orders and sports goods; 
– to implement an administrative, with the ability to create, edit and delete content, 
users, orders and client part using modern web development technologies. 
Object of research: the processes of creating an online store for selling table tennis 
goods. 
Subject of research: online store for selling table tennis goods 
Research methods: the work used the study of practical developments, analysis of 
existing analogues and methods of their modeling using software. 
Keywords: WEB SITE, ONLINE STORE, FLASK, PYTHON, SQLITE, 
SQLALCHEMY, HTML, CSS, JAVASCRIPT, DATABASE. 
7 
 
ЗМІСТ 
ПЕРЕЛІК УМОВНИХ ПОЗНАЧЕНЬ, СИМВОЛІВ, СКОРОЧЕНЬ І ТЕРМІНІВ ........ 9 
ВСТУП ................................................................................................................................ 10 
1 ДОСЛІДЖЕННЯ ПРЕДМЕТНОЇ ОБЛАСТІ ............................................................... 12 
1.1 Аналіз сучасного стану інтернет-магазинів з продажу товарів для 
настільного тенісу ..................................................................................................... 12 
1.2 Огляд існуючих аналогів інтернет-магазинів для продажу товарів для 
настільного тенісу ..................................................................................................... 14 
1.2.1 Інтернет-магазин Pingpong.com.ua ......................................................... 14 
1.2.2 Інтернет-магазин TT-Maximum.com.ua ................................................. 16 
1.2.3 Інтернет-магазин Tabletennis.com.ua ...................................................... 18 
1.2.4 Інтернет-магазин TT-Pro ......................................................................... 19 
1.2 Аналіз порівнянь існуючих аналогів та постановка задачі ............................. 20 
Висновок до розділу 1 ............................................................................................... 22 
2 ПРОЕКТУВАННЯ ІНТЕРНЕТ-МАГАЗИНУ З ПРОДАЖУ ТОВАРІВ ДЛЯ 
НАСТІЛЬНОГО ТЕНІСУ ................................................................................................. 24 
2.1 Технології для розробки веб застосунка ........................................................... 24 
2.2 Архітектура веб-застосунку ............................................................................... 25 
2.3 Діаграма варіантів виконання системи ............................................................. 27 
2.4 Проектування архітектури бази даних .............................................................. 30 
2.5 Проектування сторінок інтернет-магазину ...................................................... 32 
Висновок до розділу 2 ............................................................................................... 37 
3 ПРОГРАМНА РЕАЛІЗАЦІЯ ......................................................................................... 39 
3.1 Реалізація функцій інформаційної системи ...................................................... 39 
3.2 Реалізація зовнішнього UI/UX – дизайну ......................................................... 47 
Висновок до розділу 3 ............................................................................................... 55 
4 РОЗГОРТАННЯ ТА ТЕСТУВАННЯ ........................................................................ 57 
4.1 Підготовка середовища розгортання ................................................................. 57 
4.2 Розгортання Flask-застосунку ............................................................................ 58 
4.3 Конфігурація Gunicorn та Nginx ........................................................................ 61 
8 
 
4.4 Планування та методологія тестування ............................................................ 64 
4.5 Модульне тестування .......................................................................................... 65 
4.6 Інтеграційне тестування ...................................................................................... 68 
4.7 Функціональне тестування ................................................................................. 71 
4.8 Тестування безпеки ............................................................................................. 75 
Висновок до розділу 4 ............................................................................................... 76 
ВИСНОВКИ ....................................................................................................................... 77 
СПИСОК ВИКОРИСТАНИХ ДЖЕРЕЛ ......................................................................... 79 
ДОДАТОК А Специфікація 482.ЧДТУ.62503-01 .......................................................... 80 
ДОДАТОК Б Текст програми 482.ЧДТУ.62503-01 12 01 ............................................. 82 
 
  
9 
 
ПЕРЕЛІК УМОВНИХ ПОЗНАЧЕНЬ, СИМВОЛІВ, СКОРОЧЕНЬ І ТЕРМІНІВ 
HTML – мова розмітки гіпертексту 
CSS – мова стилю 
SQLite – компактна, безсерверна та вбудована реляційна система управління 
базами даних 
UI – інтерфейс користувача 
UX – досвід користувача 
ORM – технологія програмування, яка дозволяє взаємодіяти з реляційними 
базами даних за допомогою об'єктів 
MVC – популярний архітектурний шаблон розробки програмного 
забезпечення, який ділить програму на три незалежні компонент 
  
10 
 
ВСТУП 
У контексті сучасного розвитку цифрових технологій та стрімкого зростання 
електронної комерції питання створення та впровадження інтернет-магазинів у різних 
галузях діяльності набуває значної актуальності. Особливе місце в цій сфері 
посідають спеціалізовані онлайн-платформи, присвячені реалізації товарів, зокрема 
спортивних товарів. 
Інтернет-магазини стають ефективним інструментом вирішення проблеми 
доступності, асортименту та зручності вибору товарів, оскільки дозволяють швидко 
знаходити необхідну продукцію, порівнювати її характеристики, надавати 
консультації та здійснювати покупку без географічних обмежень. Крім того, цифрові 
платформи сприяють розвитку конкуренції, підвищенню якості сервісу та 
популяризації напрямків які раніше були слабо представлені, зокрема товарів для 
настільного тенісу. 
Настільний теніс є популярним видом спорту, який поєднує доступність, 
динамічність та користь для здоров’я. Зростання кількості як аматорів, так і 
професійних спортсменів зумовлює стабільний попит на якісне обладнання: ракетки, 
м’ячі, столи, накладки та інші аксесуари. Водночас традиційні канали продажу не 
завжди забезпечують достатній рівень доступності, асортименту та зручності вибору 
товарів. 
Актуальність кваліфікаційної роботи зумовлена потребою стрімкого розвитку 
електронної комерції у зв’язку зростанням ролі інтернет-магазинів у сфері 
спортивних товарів. У сучасних умовах споживачі все частіше обирають онлайн-
покупки через їх зручність, швидкість, можливість порівняння великої кількості 
товарів та доступ до ширшого асортименту, ніж у традиційних торгових точках. 
Це вимагає від інтернет-магазинів впровадження нових підходів до організації 
продажів, удосконалення функціоналу веб-платформ та підвищення якості 
обслуговування клієнтів. 
Розробка вузько направленого інтернет-магазинів з продажу товарів для 
настільного тенісу дозволить визначити ефективні шляхи покращення онлайн-
11 
 
торгівлі та підвищити її доступності, водночас забезпечивши користувачів зручним та 
зрозумілим інструментом для онлайн-покупок. 
Мета кваліфікаційної роботи – проектування та розробка інтернет-магазину з 
продажу товарів для настільного тенісу, забезпечивши користувачам зручним та 
зрозумілим інструментом для онлайн-покупок з можливістю адміністрування 
контенту та обробки замовлень. 
Для досягнення поставленої мети в роботі вирішувалися такі завдання: 
– дослідити існуючі рішення для продажу спортивних,  зокрема товарів для 
настільного тенісу; 
– спроектувати та розробити інтернет-магазин із інтуїтивно зрозумілим 
інтерфейсом та з урахуванням потреб користувачів та адміністраторів; 
– розробити базу даних для збереження інформації про користувачів, 
замовлення та спортивні товари; 
– реалізувати адміністративну, з можливістю створення редагування та 
видалення контенту, користувачів, замовлень та клієнтську частину з використанням 
сучасних  технологій веб-розробки. 
Об’єкт дослідження: процеси створення інтернет-магазину для продажу товарів 
для настільного тенісу. 
Предмет дослідження: інтернет-магазин з продажу товарів для настільного 
тенісу. 
Методи дослідження. В роботі застосовувалися вивчення практичних розробок, 
аналіз існуючих аналогів та методів їх моделювання за допомогою програмних 
засобів. 
  
12 
 
1 ДОСЛІДЖЕННЯ ПРЕДМЕТНОЇ ОБЛАСТІ 
1.1 Аналіз сучасного стану інтернет-магазинів з продажу товарів для 
настільного тенісу 
У сучасному цифровому середовищі інтернет-магазини відіграють дедалі 
важливішу роль у розвитку бізнесу. Вони вже давно перестали бути лише додатковим 
каналом продажів для багатьох компаній – це основа всієї бізнес-моделі. Така 
трансформація зумовлена швидким розвитком технологій, зростанням цифрової 
грамотності користувачів і підвищенням довіри до онлайн-платежів. 
Сьогодні користувачі очікують максимально простого та зручного процесу: 
швидкого пошуку товарів, зрозумілого оформлення замовлення, повної інформації та 
можливості легко порівнювати різні варіанти. Важливим фактором також є економія 
часу, можливість здійснювати покупки у будь-який момент і незалежно від місця 
перебування. 
Для бізнесу інтернет-магазини відкривають нові можливості. Вони дозволяють 
зменшити витрати на утримання фізичних приміщень для продажу, автоматизувати 
багато процесів і виходити на ширші ринки. Крім того, цифрові інструменти дають 
змогу краще розуміти поведінку клієнтів і ефективніше взаємодіяти з ними. 
Розробка інтернет-магазину потребує комплексного підходу, де важливими є і 
маркетинг, і зручність користування. Сайт має працювати швидко, бути безпечним і 
адаптованим до різних пристроїв, особливо мобільних. Не менш важливо забезпечити 
інтеграцію з платіжними системами, службами доставки та аналітичними 
інструментами. Сучасні вебплатформи повинні відповідати вимогам високої 
продуктивності, безпеки та масштабованості. 
Вибір технологій для розробки інтернет-магазинів є одним із ключових етапів, 
що визначає ефективність функціонування системи в умовах динамічного цифрового 
середовища. Технологічний стек повинен відповідати як поточним вимогам проєкту, 
так і потенційним напрямам його розвитку. 
13 
 
Висока продуктивності системи передбачає мінімізацію часу відгуку, 
оптимізацію обробки запитів і ефективне управління ресурсами серверної 
інфраструктури. Застосування механізмів кешування, балансування навантаження та 
оптимізованих алгоритмів доступу до даних є необхідними умовами стабільної 
роботи вебресурсу. 
Масштабованість забезпечує здатність системи адаптуватися до зростання 
обсягів трафіку та даних. У цьому контексті доцільним є використання модульної 
архітектури, мікросервісного підходу та хмарних технологій. 
В умовах обробки конфіденційної інформації особливої актуальності набуває 
безпека. Вона включає реалізацію захищених протоколів передачі даних, 
багаторівневу автентифікацію, контроль доступу та регулярний аудит вразливостей 
системи. 
Важливою характеристикою сучасних веб-рішень є адаптивність та 
кросплатформеність. Забезпечення коректного функціонування на різних пристроях і 
в різних програмних середовищах сприяє підвищенню доступності та якості 
користувацького досвіду. 
Суттєве значення має інтеграційна здатність системи. Взаємодія з платіжними 
шлюзами, логістичними сервісами, CRM- та аналітичними платформами дозволяє 
автоматизувати бізнес-процеси та підвищити ефективність управління. 
Сучасні тенденції притримуються вектору персоналізаціі пропозицій, 
використання штучного інтелекту для рекомендацій, впровадження чат-ботів. 
Активно розвивається мобільна комерція, а також продажі через соціальні мережі та 
маркетплейси. Велике значення має і дизайн: зручний та логічний інтерфейс 
безпосередньо впливає на рішення користувача здійснити покупку. 
Розвиток електронної комерції загалом створює сприятливі умови для запуску 
нішевих магазинів. Спортивні товари для настільного тенісу є вузькоспеціалізованою 
категорією, і саме онлайн-формат дозволяє ефективно охопити цільову аудиторію. 
Настільний теніс є одним із найдинамічніших і найдоступніших видів спорту, 
який активно розвивається як серед аматорів, так і серед професіоналів. Це підвищує 
14 
 
конкурентоспроможність бізнесу та дозволяє пропонувати більш широкий 
асортимент. 
Однак у багатьох регіонах доступ до спеціалізованих магазинів обмежений. 
Інтернет-магазини дозволяють усунути цю проблему, забезпечуючи доступність 
товарів незалежно від місця проживання покупця. Доступність якісного інвентарю 
стимулює більше людей займатися спортом, що позитивно впливає на здоров’я 
населення та формування спортивної культури. 
Таким чином, інтернет-магазини поєднують технології, маркетинг і розуміння 
потреб клієнтів. Їх успішна реалізація залежить від здатності враховувати сучасні 
тренди та створювати зручний і ефективний користувацький досвід. 
1.2 Огляд існуючих аналогів інтернет-магазинів для продажу товарів для 
настільного тенісу 
Настільний теніс є одним із найбільш доступних і популярних видів спорту у 
світі, що поєднує фізичну активність, швидкість реакції та інтелектуальну гру. В 
Україні інтерес до нього стабільно зростає завдяки розвитку клубної системи, 
проведенню турнірів, корпоративним заходам та популяризації здорового способу 
життя. 
Сучасний розвиток ринку товарів для настільного тенісу (ракеток, накладок, 
столів, тренажерів та аксесуарів) відбувається переважно в онлайн-середовищі. 
Однак, попри наявність певної кількості спеціалізованих інтернет-магазинів, ринок 
залишається фрагментованим. Основними представниками в сфері можна виділити: 
Tabletennis.com.ua, Pingpong.com.ua, TT-Pro, TT-Maximum 
1.2.1 Інтернет-магазин Pingpong.com.ua 
Інтернет-магазин Pingpong.com.ua є спеціалізованим торговельним 
майданчиком, що функціонує на ринку України понад 14 років і зосереджений 
виключно на реалізації товарів для настільного тенісу [1]. Вузька спеціалізація 
дозволяє розглядати даний ресурс як одного з профільних учасників відповідного 
15 
 
сегмента ринку. Магазин позиціонує себе як експертну платформу, орієнтовану на 
різні категорії споживачів  від початківців до професійних спортсменів. 
Асортимент інтернет-магазину охоплює широкий спектр продукції, необхідної 
для занять настільним тенісом. Зокрема, представлено як готові ракетки провідних 
виробників, так і комплектуючі для індивідуального складання (основи та 
накладки) (рисунок 1.1). 
 
Рисунок 1.1 – Головна сторінка інтернет-магазину Pingpong.com.ua 
Окрім цього, у продажу наявні м’ячі, тенісні столи, а також супутні аксесуари, 
включаючи сітки, чохли та спортивний одяг. Важливою характеристикою є наявність 
продукції різних цінових категорій від бюджетних рішень до професійного інвентарю. 
Магазин співпрацює з відомими брендами, що свідчить про орієнтацію на якість та 
відповідність міжнародним стандартам. 
З точки зору функціональних можливостей, вебсайт має типову структуру для 
електронної комерції: передбачено детальні описи товарів, інструменти для 
порівняння, а також окремі розділи для новинок, акційних пропозицій і популярних 
позицій. Додатковою перевагою є можливість отримання консультацій, що сприяє 
підвищенню рівня обслуговування клієнтів. 
16 
 
Серед ключових переваг інтернет-магазину доцільно виділити його вузьку 
спеціалізацію, що формує експертність у відповідній сфері, а також якісний підбір 
асортименту. Наявність професійних консультацій і післяпродажної підтримки 
позитивно впливає на користувацький досвід. Крім того, магазин пропонує гарантійні 
умови та можливість повернення товарів, що відповідає сучасним вимогам 
електронної торгівлі. 
Водночас варто зазначити і певні недоліки. Зокрема, дизайн вебсайту частково 
не відповідає сучасним тенденціям у сфері UX/UI, що може впливати на загальне 
сприйняття ресурсу. Обмежена кількість відгуків про товари ускладнює процес 
прийняття рішення для потенційних покупців. Також спостерігається недостатній 
розвиток контент-маркетингу, зокрема відсутність розгорнутих оглядів або 
навчальних матеріалів. Окремі логістичні обмеження можуть виникати при доставці 
великогабаритних товарів. 
1.2.2 Інтернет-магазин TT-Maximum.com.ua 
Інтернет-магазин TT-Maximum.com.ua є одним із найстаріших спеціалізованих 
онлайн-ресурсів у сфері продажу товарів для настільного тенісу в Україні. Його 
діяльність розпочалася у 2006 році, що свідчить про значний досвід роботи та 
сформовану репутацію серед споживачів. Важливою особливістю магазину є його 
чітка спеціалізація, а також орієнтація на експертний підхід до обслуговування 
клієнтів [2]. 
Асортимент інтернет-магазину відзначається значною широтою та глибиною, 
особливо у професійному сегменті. Окрім стандартних товарів, таких як готові 
ракетки, м’ячі та столи, значну частину пропозиції становлять комплектуючі для 
індивідуального складання ракеток. Зокрема, представлено великий вибір основ і 
накладок різних типів, включаючи спеціалізовані рішення для нестандартних стилів 
гри. Наявність рідкісних і менш поширених позицій робить магазин привабливим для 
досвідчених гравців. 
17 
 
Функціонально сайт забезпечує основні можливості для здійснення онлайн-
покупок: перегляд товарів, ознайомлення з характеристиками та оформлення 
замовлення. Водночас важливою перевагою є наявність консультаційної підтримки та 
додаткового контенту, зокрема оглядів і рекомендацій (рисунок 1.2). Це сприяє 
кращому розумінню продукції та допомагає користувачам зробити обґрунтований 
вибір. 
 
Рисунок 1.2 – Головна сторінка інтернет-магазину TT-Maximum.com.ua 
До основних переваг інтернет-магазину можна віднести глибокий асортимент, 
високий рівень експертності та гнучку цінову політику. Наявність акцій, знижок і 
окремого розділу з уціненими або вживаними товарами розширює можливості для 
різних категорій покупців. Також варто відзначити можливість доставки не лише по 
Україні, але й за її межі. 
Разом із тим, ресурс має і певні недоліки. Зокрема, дизайн сайту виглядає дещо 
застарілим порівняно з сучасними стандартами, а велика кількість товарів може 
ускладнювати навігацію для нових користувачів. Крім того, окремі позиції доступні 
лише під замовлення, що впливає на швидкість отримання товару. 
18 
 
1.2.3 Інтернет-магазин Tabletennis.com.ua 
Інтернет-магазин Tabletennis.com.ua є одним із найбільш відомих і масштабних 
спеціалізованих онлайн-ресурсів у сфері продажу товарів для настільного тенісу в 
Україні. Магазин функціонує з середини 2000-х років, що свідчить про значний досвід 
роботи та стійкі позиції на ринку. Його діяльність орієнтована як на роздрібних 
покупців, так і на співпрацю зі спортивними клубами та організаціями [3]. 
Однією з ключових особливостей магазину є широкий асортимент продукції, 
зокрема у преміум-сегменті. У каталозі представлені основи, накладки, готові 
ракетки, м’ячі, тенісні столи, а також супутні товари та екіпірування. Значна частина 
продукції належить до відомих світових брендів, що забезпечує високий рівень якості 
та відповідність міжнародним стандартам. Окрему увагу приділено преміальним 
лінійкам, що робить магазин привабливим для досвідчених гравців (рисунок 1.3). 
 
Рисунок 1.3 – Головна сторінка інтернет-магазину Tabletennis.com.ua 
Функціональні можливості вебсайту відповідають сучасним вимогам 
електронної комерції. Зокрема, реалізовано зручну навігацію, систему фільтрів, 
детальні описи товарів і розділи з акційними пропозиціями. Це спрощує процес 
19 
 
пошуку та вибору продукції. Додатковою перевагою є наявність професійних 
консультацій, що сприяє підвищенню рівня обслуговування клієнтів. 
До основних переваг інтернет-магазину можна віднести широкий асортимент, 
офіційне представництво провідних брендів, а також сучасний і зручний інтерфейс 
сайту. Важливим фактором є наявність оптових пропозицій і співпраця з клубами, що 
розширює цільову аудиторію. Крім того, магазин має позитивну репутацію та 
регулярно пропонує акції і знижки. 
Разом із тим, ресурс має певні недоліки. Зокрема, ціни на продукцію часто є 
вищими порівняно з іншими спеціалізованими магазинами, що пов’язано з 
орієнтацією на преміум-сегмент і оригінальні бренди. Також спостерігається 
обмеженість асортименту у бюджетному сегменті та серед специфічних товарів для 
нестандартних стилів гри. 
1.2.4 Інтернет-магазин TT-Pro 
Інтернет-магазин TT-Pro.com.ua є одним із провідних спеціалізованих онлайн-
ресурсів у сфері продажу товарів для настільного тенісу в Україні. Магазин 
позиціонує себе як один із флагманів у даному сегменті та офіційний представник 
бренду DHS, що визначає його орієнтацію на продукцію китайського виробництва. 
Діяльність магазину спрямована на обслуговування широкого кола споживачів від 
початківців до професійних гравців [4]. 
Асортимент інтернет-магазину охоплює всі основні категорії товарів для 
настільного тенісу. Зокрема, представлено готові ракетки, комплектуючі для їх 
індивідуального складання, м’ячі, тенісні столи, тренажери та аксесуари. 
Особливістю є значна частка продукції китайських брендів, насамперед DHS, а також 
729 і Yinhe. Водночас магазин поступово розширює асортимент за рахунок інших 
відомих виробників. Такий підхід дозволяє поєднувати доступні ціни з достатнім 
рівнем якості продукції (рисунок 1.4). 
20 
 
 
Рисунок 1.4 – Головна сторінка інтернет-магазину TT-Pro.com.ua  
Функціональні можливості вебсайту відповідають сучасним вимогам 
електронної комерції. Сайт має зручну навігацію, систему фільтрації товарів, а також 
окремі розділи з акційними пропозиціями, новинками та популярними товарами. 
До основних переваг інтернет-магазину доцільно віднести конкурентні ціни, 
особливо на продукцію бренду DHS, що забезпечується прямими поставками. Також 
важливими є широкий вибір товарів для різних рівнів підготовки, наявність продукції 
на складі та оперативна обробка замовлень. Додатковим фактором є зручність 
користування сайтом і наявність консультаційної підтримки. 
Водночас діяльність магазину має певні обмеження. Зокрема, асортимент 
преміум-продукції є менш представленим порівняно з деякими іншими 
спеціалізованими ресурсами. Також вибір специфічних товарів для нестандартних 
стилів гри є відносно обмеженим. Незважаючи на функціональність, візуальна 
складова сайту частково поступається сучасним дизайнерським рішенням. 
1.2 Аналіз порівнянь існуючих аналогів та постановка задачі 
Враховуючи попередній аналіз, зумовлений необхідністю створення сучасних, 
конкурентоспроможних інтернет-магазинів, які відповідатимуть новим вимогам 
21 
 
ринку. Існуючі аналогів Tabletennis.com.ua, Pingpong.com.ua, TT-Pro, TT-Maximum та 
інші) частково закривають попит, проте мають суттєві недоліки в UX/UI-дизайні, 
персоналізації пропозицій, швидкості впровадження новинок, якості контент-
маркетингу та рівні клієнтського сервісу. У таблиці нижче наведено порівняльний 
аналіз основних аналогів інтернет-магазинів для продажу товарів для настільного 
тенісу. 
Таблиця 1.1 – Порівняльний аналіз основних аналогів інтернет-магазинів для 
продажу товарів для настільного тенісу 
TT-
Критерій Pingpong.com.ua Tabletennis.com.ua TT-Pro.com.ua 
Maximum.com.ua 
Термін 
14 років 20 років 20 років 10 років 
роботи 
Повна, з акцентом Повна, з 
Повна (тільки Повна, преміум-
Спеціалізація на професійний акцентом на 
настільний теніс) сегмент 
сегмент DHS 
Широкий 
Найглибший Дуже широкий 
(особливо 
Асортимент Збалансований (включаючи (переважно 
середній 
рідкісні позиції) преміум) 
сегмент) 
Дуже висока 
Експертність Висока (контент, Висока Середня/висока 
консультації) 
Контент 
Обмежений Дуже розвинений Помірний Помірний 
(відео, статті) 
Середня (дещо Висока 
Зручність Середня Висока (сучасний 
застарілий (зручний 
сайту (UX/UI) (перевантажений) сайт) 
дизайн) функціонал) 
Навігація та 
Базові Складні Розвинені Зручні 
фільтри 
Орієнтація на Середній і 
Універсальна Досвідчені гравці Усі рівні 
користувача професійний рівень 
Наявність 
Ні Ні Так (Київ) Так (Дніпро) 
офлайн-точки 
Рідкісний 
Унікальні Стабільність, Офіційні бренди, Ціни на DHS, 
інвентар, б/у 
переваги універсальність преміум швидкість 
секція 
Застарілий Слабо 
Основні Складність для 
дизайн, мало структурований Мало контенту 
недоліки новачків 
контенту контент  
 
Гуртуючись на аналіз доцільно створити власний інтернет-магазин для продажу 
товарів для настільного тенісу  який  буде зручний адаптивний та створений на основі 
нових технологій. 
22 
 
Постановка завдання. У межах кваліфікаційної роботи ставить завдання 
розробити інтернет-магазину для продажу товарів для настільного тенісу, який 
забезпечить: 
– зручний та інтуїтивно зрозумілий інтерфейс; 
– система автентифікації користувачів; 
– особистий кабінет та історія замовлень; 
– можливість управління контентом (видалення, редагування, додавання) 
– можливість управління користувачами; 
– можливість управління замовленнями 
– генерація звітів (друк чеку замовлення); 
– система «Обраного» (Wishlist). 
Таким чином, буде спроектовано та реалізовано власний прототип інтернет-
магазину для продажу товарів для настільного тенісу. 
Висновок до розділу 1 
У цьому розділі проведено дослідження предметної області показало, що 
інтернет-магазини є ключовим елементом сучасної електронної комерції та 
виступають не лише як канал продажів, а як основа бізнес-моделі. Ринок онлайн-
продажу товарів для настільного тенісу в Україні характеризується наявністю 
спеціалізованих платформ, які пропонують широкий асортимент продукції та 
орієнтуються на різні категорії користувачів — від початківців до професіоналів. 
Водночас цей сегмент залишається частково фрагментованим і недостатньо 
інноваційним. 
Аналіз існуючих інтернет-магазинів продемонстрував, що кожен із них має свої 
сильні сторони: глибокий асортимент, експертність, конкурентні ціни або орієнтацію 
на преміум-сегмент. Однак жоден із розглянутих ресурсів не забезпечує комплексного 
поєднання сучасного дизайну, зручного користувацького інтерфейсу, розвиненої 
персоналізації, якісного контенту та високого рівня сервісу. Найпоширенішими 
23 
 
недоліками є застарілий UX/UI-дизайн, складність навігації, обмежений контент-
маркетинг і недостатня орієнтація на потреби користувачів. 
З огляду на сучасні тенденції розвитку електронної комерції, особливо 
важливими стають такі аспекти, як адаптивність, швидкодія, безпека, інтеграція з 
зовнішніми сервісами та персоналізація взаємодії з клієнтом. Крім того, розвиток 
мобільної комерції та використання інтелектуальних систем рекомендацій 
відкривають нові можливості для підвищення ефективності інтернет-магазинів. 
У результаті проведеного аналізу сформовано завдання розробки власного 
інтернет-магазину товарів для настільного тенісу, який враховуватиме недоліки 
існуючих аналогів і відповідатиме сучасним вимогам ринку. Основною метою є 
створення функціональної, зручної та масштабованої системи з інтуїтивним 
інтерфейсом, розширеними можливостями управління та орієнтацією на покращення 
користувацького досвіду 
  
24 
 
2 ПРОЕКТУВАННЯ ІНТЕРНЕТ-МАГАЗИНУ З ПРОДАЖУ ТОВАРІВ ДЛЯ 
НАСТІЛЬНОГО ТЕНІСУ 
2.1 Технології для розробки веб застосунка 
У процесі розробки веб-застосунку інтернет-магазину було використано 
сучасний стек технологій, який забезпечує ефективну взаємодію між серверною 
частиною, базою даних та користувацьким інтерфейсом. Обрані інструменти 
дозволяють реалізувати масштабований, зручний у підтримці та функціонально 
насичений програмний продукт. Основними мова програмування які 
використовувалися при розробці були: Python, JavaScript, HTML, CSS. 
Серверна частина застосунку реалізована з використанням фреймворку Flask, 
який працює на базі мови програмування Python. 
Python є високорівневою інтерпретованою мовою програмування загального 
призначення, яка підтримує об’єктно-орієнтовану, процедурну та функціональну 
парадигми програмування. Основною перевагою мови є її читабельний синтаксис, а 
також широка екосистема бібліотек [5]. 
Flask є мікрофреймворком для веб-розробки на Python, що забезпечує базовий 
функціонал для створення веб-застосунків і може бути розширений додатковими 
компонентами [6]. 
Flask забезпечує обробку HTTP-запитів, маршрутизацію та інтеграцію з 
шаблонізатором. А для реалізації механізму аутентифікації та авторизації 
використано Flask-Login, що забезпечує управління сесіями користувачів. 
Взаємодія з базою даних здійснюється за допомогою ORM-бібліотеки 
SQLAlchemy. 
SQLAlchemy – це  набір інструментів для роботи з базами даних у Python, який 
включає ORM (Object Relational Mapper) та SQL Expression Language. Він дозволяє 
розробникам працювати з базами даних, використовуючи об’єкти Python замість 
написання SQL-запитів вручну, а також забезпечує гнучкість у виборі підходу до 
25 
 
роботи з даними. Це значно спрощує розробку, підвищує читабельність коду та 
зменшує ймовірність помилок. 
Як система управління базами даних використовується SQLite. 
SQLite – легка вбудованою реляційною системою управління базами даних, яка 
не потребує окремого серверного процесу. База даних зберігається у вигляді одного 
файлу, що забезпечує простоту використання, портативність та швидке розгортання. 
Це робить SQLite оптимальним рішенням для невеликих застосунків, 
прототипів та тестування. 
Клієнтська частина реалізована за допомогою шаблонізатора Jinja2. 
Jinja2 є потужним шаблонізатором для Python, який дозволяє генерувати HTML-
сторінки на основі шаблонів і динамічних даних, підтримує логічні конструкції та 
забезпечує безпечну обробку вхідних даних.  
Саме в комбінації з CSS-фреймворк Bootstrap вдалося досягти зручного та 
адаптивного інтерфейсу користувача. 
Bootstrap – це досить популярний відкритий CSS-фреймворк для розробки 
адаптивних та мобільно-орієнтованих веб-інтерфейсів. Він містить готові компоненти 
інтерфейсу (кнопки, форми, навігацію), систему сіток та JavaScript-плагіни, що 
значно пришвидшують процес створення сучасного дизайну. 
Інтерактивність клієнтської частини реалізована за допомогою JavaScript що 
дозволяє здійснювати асинхронні запити без перезавантаження сторінки. 
Додатково використано бібліотеку ReportLab для генерації PDF-документів, що 
дозволяє автоматично створювати чеки замовлень. 
Таким чином, використаний стек технологій забезпечує ефективність розробки, 
масштабованість та зручність підтримки веб-застосунку. 
2.2 Архітектура веб-застосунку 
Архітектура веб-застосунку побудована відповідно до шаблону Model-View-
Controller (MVC), що передбачає поділ застосунку на три взаємопов’язані 
компоненти: модель (Model), представлення (View) та контролер (Controller). Що 
26 
 
дозволяє розділити логіку обробки даних, інтерфейс користувача та механізми 
керування, що підвищує масштабованість, гнучкість і зручність супроводу 
програмного продукту (рисунок 2.1). 
 
Рисунок 2.1 – Архітектура веб-застосунку 
Модель (Model) відповідає за роботу з даними та бізнес-логіку застосунку. Вона 
визначає структуру даних, правила їх обробки та взаємодію з базою даних. У даному 
проєкті модель реалізована за допомогою бібліотеки SQLAlchemy. Саме в моделях 
описано сутності, такі як користувачі, товари та замовлення, а також зв’язки між ними. 
Модель виконує операції створення, читання, оновлення та видалення даних (CRUD), 
забезпечуючи цілісність інформації [8]. 
Представлення (View) відповідає за відображення даних користувачу. Воно 
формує інтерфейс і визначає, як саме інформація буде представлена на сторінці. У 
проєкті представлення реалізовано за допомогою шаблонізатора Jinja2. HTML-
шаблони дозволяють динамічно відображати дані, отримані з сервера, 
27 
 
використовуючи змінні, цикли та умовні конструкції. Це забезпечує чітке розділення 
логіки та інтерфейсу. 
Контролер (Controller) виступає посередником між моделлю та представленням. 
Він обробляє запити користувача, взаємодіє з моделлю для отримання або зміни даних 
і передає ці дані до представлення для відображення. У даному застосунку роль 
контролера виконують обробники маршрутів, реалізовані за допомогою фреймворку 
Flask. Контролери приймають HTTP-запити (наприклад, відкриття сторінки або 
надсилання форми), виконують необхідну логіку та повертають сформовану HTML-
сторінку або JSON-відповідь. 
Взаємодія між компонентами відбувається наступним чином: користувач 
надсилає запит до сервера, який обробляється контролером; контролер звертається до 
моделі для отримання або зміни даних; після цього результати передаються до 
представлення, яке формує відповідь і відображає її користувачу. 
Застосування архітектури MVC дозволяє досягти модульності, спрощує 
тестування окремих компонентів, полегшує внесення змін і розширення 
функціональності системи без впливу на інші частини застосунку. 
2.3 Діаграма варіантів виконання системи 
Діаграма варіантів виконання (Use Case Diagram) є одним із ключових 
інструментів моделювання функціональних вимог до програмної системи. Вона 
відображає взаємодію користувачів (акторів) із системою та дозволяє визначити 
перелік основних сценаріїв використання системи [8]. Діаграма варіантів виконання 
проєктованої системи представлена на рисунку 2.2. 
28 
 
 
Рисунок 2.2 – Діаграма варіантів виконання 
Діаграму варіантів виконання демонструє функціональні можливості системи 
та взаємодію з нею різних типів користувачів [9]. 
У межах даної системи виділено три основні актори: 
– гість (незареєстрований користувач); 
– авторизований користувач; 
– адміністратор. 
Гість має обмежений доступ до функціоналу системи. Він може переглядати 
каталог товарів, здійснювати пошук продукції та переглядати детальну інформацію 
про товари. Крім того, гість має можливість зареєструвати новий обліковий запис або 
виконати вхід до системи. Дані функції є базовими і не потребують попередньої 
автентифікації. 
Авторизований користувач має розширений функціонал. Він може додавати 
товари до кошика, переглядати його вміст та редагувати (змінювати кількість товарів 
або видаляти їх). Однією з ключових функцій є оформлення замовлення, яке включає 
декілька обов’язкових етапів, пов’язаних відношенням <<include>>: 
29 
 
– перегляд кошика; 
– вибір способу доставки; 
– введення даних для оформлення; 
– оплата замовлення. 
Після успішного завершення процесу оформлення замовлення система виконує 
генерацію електронного чека у форматі PDF. 
Також авторизований користувач має доступ до особистого кабінету, де може 
переглядати інформацію профілю, історію замовлень, керувати адресами доставки та 
списком обраних товарів. Додатково передбачено можливість скасування замовлення, 
яка реалізована через відношення <<extend>>, тобто є опціональним розширенням 
базового сценарію. 
Адміністратор має повний доступ до функціоналу керування системою. До його 
основних можливостей належать: 
– управління товарами (створення, редагування, видалення); 
– управління категоріями товарів; 
– управління користувачами; 
– перегляд та обробка замовлень. 
Використання відношень <<include>> та <<extend>> у діаграмі дозволяє 
деталізувати взаємозв’язки між варіантами використання. Зокрема, відношення 
<<include>> використовується для позначення обов’язкових підпроцесів, які є 
частиною основного сценарію, тоді як <<extend>> відображає додаткові, 
необов’язкові можливості системи.  
Діаграма варіантів виконання дозволяє отримати цілісне уявлення про 
функціональні можливості системи «Інтернет-магазин для продажу товарів для 
настільного тенісу», визначити ролі користувачів та їх взаємодію із системою, а також 
слугує основою для подальшого проєктування та реалізації програмного 
забезпечення. 
30 
 
2.4 Проектування архітектури бази даних 
База даних є ключовим компонентом інформаційної системи оскільки 
забезпечує зберігання, обробку та цілісність даних, необхідних для функціонування 
застосунку [10]. В системі використано управління базами даних SQLite, а взаємодія 
з нею реалізована за допомогою ORM-бібліотеки SQLAlchemy. 
Проєктування бази даних передбачає використання таблиць (сутностей), 
атрибутів та зв’язків між ними. Основною метою є забезпечення ефективного 
зберігання даних, мінімізація дублювання інформації та підтримка цілісності 
даних [10]. Діаграма зв’язків розробленої бази даних представлена на рисунку 2.3. 
 
 
Рисунок 2.3 – Діаграма зв’язків бази даних 
У системі виділено такі основні сутності: 
1. Користувач (User) – таблиця зберігає інформацію про зареєстрованих 
користувачів системи. Використовується для реалізації механізмів автентифікації та 
авторизації. 
Містить наступні атрибути: 
31 
 
– id – унікальний  дентифікатор користувача (первинний ключ); 
– username – ім’я   користувача; 
– email – електронна адреса; 
– password_hash – хешований пароль; 
– created_at – дата реєстрації. 
2. Товар (Product) – містить інформацію про товари, доступні в інтернет-
магазині. 
Містить наступні атрибути: 
– id – унікальний ідентифікатор товару; 
– name – назва товару; 
– description – опис товару; 
– price – ціна; 
– stock – кількість на складі; 
– category_id – зовнішній ключ до категорії. 
3. Категорія (Category) – призначена для групування товарів. 
Містить наступні атрибути: 
– id – ідентифікатор категорії; 
– name – назва категорії. 
4. Замовлення (Order) – зберігає інформацію про оформлені замовлення 
користувачів. 
Містить наступні атрибути: 
– id – ідентифікатор замовлення; 
– user_id – зовнішній ключ на користувача; 
– created_at – дата створення; 
– status – статус замовлення (наприклад: нове, обробляється, завершене); 
– total_price – загальна сума замовлення. 
5. Елемент замовлення (OrderItem) – таблиця реалізує зв’язок між замовленням 
і товарами (багато-до-багатьох). 
Містить наступні атрибути: 
32 
 
− id – ідентифікатор запису; 
− order_id – зовнішній ключ до замовлення; 
− product_id – зовнішній ключ до товару;\ 
− quantity – кількість товару; 
− price – ціна на момент покупки. 
6. Кошик (Cart) – икористовується для тимчасового зберігання вибраних товарів до 
моменту оформлення замовлення. 
7. Обране (Wishlist) – зберігає  список товарів, які користувач додав до обраного. 
У програмній реалізації кожна таблиця бази даних представлена у вигляді класу 
мови Python з використанням SQLAlchemy. Атрибути таблиць описуються як поля 
класів, а зв’язки – через відповідні ORM-відношення (relationship). Це дозволяє 
працювати з базою даних на рівні об’єктів, спростити написання запитів та підвищити 
читабельність та підтримуваність коду. 
2.5 Проектування сторінок інтернет-магазину 
Проєктування сторінок інтернет-магазину є важливим етапом розробки, 
оскільки визначає структуру інтерфейсу, зручність навігації та взаємодію користувача 
із системою [11]. Було реалізовано набір сторінок, які забезпечують повний цикл 
роботи користувача з інтернет-магазином – від перегляду товарів до оформлення 
замовлення. Загальна структура проєкту представлена на рисунку 2.4. 
33 
 
 
Рисунок 2.4 – Загальна структура проєкту 
Загальна структура веб-інтерфейсу побудована за принципом шаблонізації з 
використанням базового файлу base.html, який містить спільні елементи: 
– навігаційну панель (header); 
– футер (footer); 
– підключення CSS та JavaScript; 
– блоки для вставки контенту (Jinja2 blocks). 
34 
 
Інші сторінки наслідують цей шаблон за допомогою механізму {% extends %}, 
що дозволяє: 
– уникнути дублювання коду; 
– забезпечити єдиний стиль інтерфейсу; 
– спростити підтримку та масштабування. 
Головна сторінка є центральним елементом інтерфейсу та виконує функцію 
відображення каталогу товарів. 
На сторінці реалізовано: 
– відображення товарів у вигляді адаптивної сітки; 
– картки товарів, що містять: 
– зображення; 
– назву; 
– ціну; 
– кнопку «Додати в кошик»; 
– кнопку «Додати до обраного» (wishlist); 
– поле пошуку; 
– можливість фільтрації за категоріями. 
Інтерактивність сторінки реалізована за допомогою JavaScript: 
– додавання товару в wishlist відбувається асинхронно (fetch API); 
– стан кнопки (активна/неактивна) змінюється без перезавантаження сторінки. 
Дані для сторінки отримуються з сервера через контролер Flask та передаються 
у шаблон через Jinja2. 
Сторінка детального перегляду товару надає розширену інформацію про 
обраний продукт та забезпечує взаємодію користувача з ним. 
Основні компоненти сторінки: 
– галерея зображень товару: 
– кілька фото; 
– можливість перемикання; 
– ефект збільшення (zoom при наведенні); 
– інформаційний блок: 
35 
 
– назва товару; 
– ціна; 
– опис; 
– характеристики; 
– функціональні кнопки: 
– додавання в кошик; 
– додавання до обраного (wishlist). 
Особливістю є використання асинхронних запитів для роботи з wishlist, що 
дозволяє змінювати стан елементів інтерфейсу без перезавантаження сторінки. 
Сторінка кошика призначена для перегляду товарів, які користувач планує 
придбати. 
Функціональні можливості: 
– відображення списку товарів у кошику; 
– зміна кількості товару; 
– видалення товарів; 
– автоматичний підрахунок загальної вартості.  
Кожна зміна (кількість, видалення) може оброблятися: або через форму з 
перезавантаженням сторінки; або асинхронно (AJAX), що покращує користувацький 
досвід. 
Сторінка оформлення замовлення є ключовим етапом взаємодії користувача із 
системою. 
Вона містить: 
– форму введення даних; 
– ім’я; 
– контактні дані; 
– адреса доставки; 
– вибір способу доставки; 
– вибір способу оплати; 
– підтвердження замовлення. 
Після надсилання форми: 
36 
 
– дані передаються на сервер; 
– створюється запис у таблиці замовлень; 
– генеруються елементи замовлення; 
– очищається кошик користувача; 
– формується чек у форматі PDF. 
Сторінка «обраного» дозволяє користувачу зберігати товари для подальшого 
перегляду. 
Основні можливості: 
– перегляд списку обраних товарів; 
– видалення товарів зі списку; 
– швидке додавання товару до кошика; 
– перехід до сторінки товару. 
Ключова особливість: повністю асинхронна взаємодія (без перезавантаження 
сторінки) та використання JavaScript для обробки подій (toggle wishlist). 
Сторінки авторизації та реєстрації. забезпечення доступу до персоналізованого 
функціоналу реалізовано сторінки: 
– реєстрації (register.html); 
– авторизації (login.html). 
Сторінки містять: 
– форми введення даних; 
– валідацію введених значень; 
– обробку помилок; 
– повідомлення користувачу. 
Після успішної авторизації: 
– створюється сесія користувача; 
– відкривається доступ до кошика, замовлень та обраного. 
Сторінка «Мої замовлення» відображає історію покупок користувача. 
Функціонал: 
– список замовлень; 
– інформація про кожне замовлення: 
37 
 
– дата; 
– статус; 
– сума; 
– перегляд деталей замовлення; 
– можливість переходу до чеку. 
Сторінка друку чеку призначена для формування та друку чеку замовлення. 
Містить: 
– інформацію про замовлення; 
– список товарів; 
– загальну суму; 
– кнопку друку. 
Особливості реалізації: 
– генерація PDF за допомогою бібліотеки ReportLab; 
– підтримка друку через CSS (media print); 
– мінімалістичний дизайн без зайвих елементів. 
Висновок до розділу 2 
У цьому розділі було спроєктовано інтернет-магазин для продажу товарів для 
настільного тенісу з використанням сучасних веб-технологій. Обраний технологічний 
стек, що включає Python, Flask, SQLAlchemy, SQLite, Jinja2, Bootstrap та JavaScript, 
забезпечує ефективну взаємодію між серверною та клієнтською частинами 
застосунку, а також сприяє створенню зручного, функціонального та масштабованого 
програмного продукту. 
Застосовано архітектурного підходу MVC який дозволить чітко розмежувати логіку 
обробки даних, інтерфейс користувача та керування запитами, що значно підвищує 
гнучкість системи, спрощує її підтримку та подальше розширення. 
Розроблено діаграма варіантів використання дала змогу визначити основні ролі 
користувачів і сценарії їх взаємодії із системою, що забезпечує повноту 
функціональних вимог. 
38 
 
Спроектовано бази даних із використанням реляційної моделі та ORM-
підходу [12]. Визначені сутності та зв’язки між ними які забезпечують ефективне 
зберігання та обробку інформації. 
Розроблена структура веб-сторінок основних етапів взаємодії користувача з 
інтернет-магазином – від  перегляду товарів до оформлення замовлення. Використано 
шаблонізації, адаптивний дизайн та асинхронна взаємодія. 
  
39 
 
3 ПРОГРАМНА РЕАЛІЗАЦІЯ 
3.1 Реалізація функцій інформаційної системи 
У процесі розробки інтернет-магазину для продажу товарів для настільного 
тенісу було реалізовано комплекс функцій, які забезпечують повноцінну взаємодію 
користувачів із системою, обробку даних та виконання основних бізнес-процесів. 
Першим етапом при створені інтернет магазину було створення проекту та 
віртуального середовища за допомогою команди . python -m venv venv . 
Далі обов’язково потрібно встановити бібліотеки SQLAlchemy, Flask-WTF, 
ReportLab. 
Також важливо для першого запуску ініціалізувати базу даних командою 
«python create_db.py». Файл create_db.py містить налаштування бази даних та 
інформую про користувачів, товари, замовлення. Алгоритм виконання файлу 
create_db.py та результати роботи представлена на рисунку 3.1. 
 
Рисунок 3.1 – Алгоритм виконання з файлу create_db.py 
40 
 
Однією з ключових функцій є реєстрація та авторизація користувачів. Система 
дозволяє створювати облікові записи, виконувати вхід та вихід із системи, а також 
забезпечує захист даних за рахунок хешування паролів і керування сесіями 
користувачів. Це забезпечує доступ до персоналізованого функціоналу, такого як 
кошик, обране та історія замовлень. Алгоритм хешування паролю для реєстрації та 
авторизації користувачів представлена на рисунку 3.2. 
 
Рисунок 3.2 – Алгоритм хешування паролю для реєстрації та авторизації 
користувачів 
Функція перегляду каталогу товарів надає можливість користувачам 
ознайомлюватися з асортиментом продукції. Реалізовано відображення товарів у 
вигляді зручної сітки, а також пошук і фільтрацію за категоріями, що спрощує 
навігацію та підбір необхідних товарів. Для реалізації пошуку використовується 
app.py. Цей маршрут обробляє запити до головної сторінки. Він приймає параметри 
пошуку (q) та фільтрації за категорією (category), динамічно формує SQL-запит до 
бази даних за допомогою SQLAlchemy, а також передає список категорій для 
випадаючого списку. Функція перегляду каталогу користувачами представлена на 
рисунку 3.3. 
41 
 
 
Рисунок 3.3 – Функція перегляду каталогу користувачами 
Цей маршрут обробляє запити до головної сторінки. Він приймає параметри 
пошуку (q) та фільтрації за категорією (category), динамічно формує SQL-запит до 
бази даних за допомогою SQLAlchemy, а також передає список категорій для 
випадаючого списку. 
Важливою складовою є управління кошиком. Користувачі можуть додавати 
товари до кошика, змінювати їх кількість, видаляти позиції та переглядати загальну 
вартість замовлення. Частина операцій виконується асинхронно, що підвищує 
швидкість взаємодії з інтерфейсом. 
Цей функціонал розділений на серверну частину (збереження стану кошика в 
базі даних) та клієнтську (швидка взаємодія та динамічний перерахунок за допомогою 
JavaScript). Реалізація управління кошиком представлена на рисунку 3.4. 
42 
 
 
Рисунок 3.4 – Реалізація управління кошиком 
Шаблон кошика виводить список товарів. Для кожної позиції передбачено 
кнопки зміни кількості (+ та -) і кнопку видалення. Важливий момент: у HTML-
елементи div.cart-item зашиті атрибути data-price та data-quantity, які дозволяють 
JavaScript швидко зчитувати ці дані для асинхронного перерахунку. 
Щоб користувачу не доводилося чекати повного перезавантаження сторінки для 
того, щоб побачити нову загальну суму, використовується JavaScript. Функція 
updateTotal() миттєво перераховує вартість на стороні клієнта. 
Функція оформлення замовлення реалізує повний цикл покупки. Користувач 
вводить необхідні дані, обирає спосіб доставки та оплати, після чого система створює 
43 
 
запис замовлення в базі даних, формує список товарів у замовленні та очищує кошик. 
Додатково реалізовано автоматичну генерацію електронного чека у форматі PDF. 
Реалізація автоматичної генерації електронного чека представлена на рисунку 3.5. 
 
Рисунок 3.5 – Реалізація автоматичної генерації електронного чека 
Система також підтримує функцію обраного (wishlist), яка дозволяє 
користувачам зберігати товари для подальшого перегляду або швидкого додавання до 
кошика. Взаємодія з цією функцією реалізована без перезавантаження сторінки. 
Реалізація функції обраного представлена на рисунку 3.6. 
 
Рисунок 3.6 – Реалізація функції обраного 
44 
 
Для зареєстрованих користувачів доступний особистий кабінет, де можна 
переглядати та редагувати персональні дані, а також переглядати історію замовлень і 
деталі кожної покупки. Реалізація особистого кабінету зареєстрованого користувача 
представлена на рисунку 3.7. 
 
Рисунок 3.7 – Реалізація особистого кабінету зареєстрованого користувача 
Окремо реалізовано адміністративний функціонал, який забезпечує керування 
системою. Адміністратор може додавати, редагувати та видаляти товари і категорії, 
управляти користувачами, а також переглядати й обробляти замовлення. 
Додавання реалізовано за допомогою програмного коду, який представлено на 
рисунку 3.8. 
45 
 
 
Рисунок 3.8 – Реалізація додавання товарів і категорій адміністратором 
Редагування реалізовано за допомогою програмного коду, який представлено на 
рисунку 3.9. 
46 
 
 
Рисунок 3.9 – Реалізація редагування товарів і категорій адміністратором 
Видалення реалізовано за допомогою програмного коду, який представлено на 
рисунку 3.10. 
 
Рисунок 3.10 – Реалізація видалення товарів і категорій адміністратором 
 
47 
 
Усі функції системи реалізовані з урахуванням принципів модульності та 
масштабованості, що дозволяє легко доповнювати систему новими можливостями та 
забезпечує її стабільну роботу. 
3.2 Реалізація зовнішнього UI/UX – дизайну 
У процесі розробки інтернет-магазину особливу увагу було приділено 
створенню зручного, інтуїтивно зрозумілого та естетично привабливого 
користувацького інтерфейсу (UI) та забезпеченню позитивного користувацького 
досвіду (UX). Основною метою було спростити взаємодію користувача із системою 
та зробити процес вибору й придбання товарів максимально комфортним. 
Інтерфейс застосунку побудований із використанням принципів адаптивного 
дизайну, що забезпечує коректне відображення веб-сторінок на різних пристроях: 
персональних комп’ютерах, планшетах та смартфонах. Для цього використано CSS-
фреймворк Bootstrap, який надає гнучку систему сіток і готові компоненти інтерфейсу. 
Головна сторінка спроєктованого застосунку зображена на рисунку 3.11. 
 
Рисунок 3.11 – Головна сторінка застосунку 
48 
 
Загальна структура сторінок базується на єдиному шаблоні, що включає 
навігаційну панель, основний контент і футер. Навігація є простою та логічною, що 
дозволяє користувачу швидко переходити між основними розділами: каталогом 
товарів, кошиком, обраним та моїми замовленнями (рисунок 3.12). 
 
Рисунок 3.12 – Вікно «Мої замовлення» 
Також доступна можливість швидкого друку чека (рисунок 3.13). 
 
Рисунок 3.13 – Друк чека 
49 
 
Важливим елементом UX є зручність навігації та доступність функцій. Ключові 
дії, такі як додавання товару до кошика (рисунок 3.14) або обраного, винесені у 
вигляді помітних кнопок. Для покращення взаємодії використано асинхронні запити, 
що дозволяє виконувати дії без перезавантаження сторінки та забезпечує швидкий 
відгук інтерфейсу. Приклад оформлення замовлення представлено на рисунку 3.15. 
 
Рисунок 3.14 – Вікно «Ваш кошик» 
 
Рисунок 3.15 – Вікно «Оформлення замовлення» 
50 
 
Дизайн сторінок виконано у мінімалістичному стилі з акцентом на контент. 
Використано зрозумілу типографіку, контрастні кольори для основних елементів 
керування та достатні відступи між елементами, що підвищує читабельність і 
сприйняття інформації. 
Особлива увага приділена сторінкам каталогу та детального перегляду товару. 
Вони містять чітко структуровану інформацію, зображення високої якості та зручні 
елементи взаємодії. Інтерактивні елементи, такі як зміна стану кнопок або оновлення 
списків, реалізовані за допомогою JavaScript, що покращує загальний користувацький 
досвід. На рисунку 3.16 представлено сторінку перегляду товару. 
 
Рисунок 3.16 – Сторінка перегляду товару 
Форми введення даних для реєстрації, авторизація та оформлення замовлення 
спроєктовані з урахуванням принципів usability: передбачено валідацію даних, 
підказки користувачу та обробку помилок, що зменшує ймовірність некоректного 
введення інформації. Вікно реєстрації нового користувача показано на рисунку 3.17. 
Вікно авторизації користувача показано на рисунку 3.18. 
51 
 
 
Рисунок 3.17 – Вікно реєстрації нового користувача 
 
Рисунок 3.18 – Вікно авторизації користувача 
Також важливо відмітити окремо сторінки для адміністратора. 
Сторінка «Користувачі» (рисунок 3.19) дозволяє редагувати (рисунок 3.20) та 
видаляти існуючих користувачів. 
52 
 
 
Рисунок 3.19 – Вікно «Користувачі» 
 
Рисунок 3.20 – Вікно «Редагування користувача» 
Сторінка «Всі замовлення» (рис. 3.10)  потрібна для управління замовленнями. 
Адміністратор може перевіряти змінювати статус та видаляти доступні замовлення. 
53 
 
 
Рисунок 3.21 – Вікно управління замовленнями 
Також з базових, але важливих інструментів присутня можливість строювати 
нові товари. Вікно створення нового товару  показано на рисунку 3.22. 
 
Рисунок 3.22 – Вікно створення нового товару 
54 
 
Після створення товару з часом виникає потреба коригувати інформацію. Для 
цього потрібно натиснути на кнопку редагування біля товару та внести зміни у вікні 
редагування (рисунок 3.23). 
 
Рисунок 3.23 – Редагування існуючих товарів 
Для зручності вибору товару користувачів також реалізовано wishlist. Опція 
дозволяє позначити обрані товари на головній сторінці інтернет магазину 
(рисунок 3.24), а далі для зручності переглядати як окремий список обраних товарів 
(рисунок 3.25). 
55 
 
 
Рисунок 3.24 – Вибір обраних товарів на головній сторінці 
 
 
Рисунок 3.25 – Сторінка обраних товарів 
Висновок до розділу 3 
У процесі виконання даного розділу було здійснено повну програмну реалізацію 
інтернет-магазину для продажу товарів для настільного тенісу, що охоплює як 
функціональну частину системи, так і її зовнішній вигляд та зручність використання. 
56 
 
Реалізовано основні функції інформаційної системи, зокрема реєстрацію та 
авторизацію користувачів із забезпеченням безпеки даних, перегляд каталогу товарів 
із можливістю пошуку та фільтрації, управління кошиком, оформлення замовлення та 
генерацію електронних чеків.  Адміністративна частина дозволяє ефективно 
управляти товарами, користувачами та замовленнями. 
Особливістю реалізації є поєднання серверної логіки з клієнтською 
інтерактивністю. Використання асинхронних запитів та JavaScript забезпечує 
швидкий відгук інтерфейсу та покращує продуктивність роботи застосунку без 
необхідності перезавантаження сторінок. 
З точки зору UI/UX-дизайну, реалізовано адаптивний та інтуїтивно зрозумілий 
інтерфейс користувача. Використання шаблонізації, єдиного стилю оформлення та 
сучасних підходів до проєктування інтерфейсів забезпечує зручну навігацію та 
комфортну взаємодію з системою. Форми введення даних оптимізовані з урахуванням 
принципів usability, що знижує ймовірність помилок користувача. 
Було створено повнофункціональний веб-застосунок, який відповідає сучасним 
вимогам до інтернет-магазинів, забезпечує ефективну обробку даних, зручний 
користувацький досвід та можливість подальшого розширення і вдосконалення 
системи. 
  
57 
 
4 РОЗГОРТАННЯ ТА ТЕСТУВАННЯ 
4.1 Підготовка середовища розгортання 
Розгортання веб-застосунку тенісного інтернет-магазину є завершальним і 
відповідальним етапом розробки, що забезпечує доступність системи для кінцевих 
користувачів у реальних умовах експлуатації. На відміну від середовища розробки, 
виробниче середовище висуває значно вищі вимоги до надійності, безпеки та 
продуктивності. Неправильно налаштоване виробниче середовище може призвести 
до витоку конфіденційних даних, збоїв у роботі або низької продуктивності під 
навантаженням. 
Застосунок розроблено на базі мікрофреймворку Flask з використанням MySQL 
як системи управління реляційними базами даних. Такий вибір технологій обумовлює 
склад необхідного програмного забезпечення на сервері та визначає послідовність 
підготовки середовища розгортання. 
Для коректного функціонування всіх компонентів застосунку на виробничому 
сервері необхідне встановлення таких компонентів: 
– 3.10 або новішої версії – середовище виконання для Flask-застосунку; 
– pip – стандартний менеджер пакетів Python для встановлення залежностей із 
requirements.txt; 
– virtualenv – інструмент для створення ізольованих середовищ виконання, що 
запобігає конфліктам версій між пакетами; 
– MySQL Server 8.0 – реляційна СУБД для зберігання даних про товари, 
користувачів, кошики, вішлісти та замовлення; 
– Nginx – високопродуктивний веб-сервер для обробки вхідних HTTP-запитів, 
роздачі статичних файлів та проксіювання запитів до Flask; 
– Gunicorn – продуктивний WSGI HTTP-сервер для запуску Flask-застосунку в 
багатопроцесному режимі. 
Усі необхідні системні залежності встановлюються через пакетний менеджер 
apt. Команди встановлення системних залежностей представлені на рисунку 4.1. 
58 
 
Рисунок 4.1 – Команди встановлення системних залежностей 
Після встановлення MySQL виконується первинне налаштування безпеки 
сервера за допомогою вбудованого скрипта mysql_secure_installation, який дозволяє 
встановити пароль адміністратора, заборонити анонімний доступ та видалити тестові 
бази даних. Далі створюється окрема база даних та виділений користувач із 
обмеженими правами виключно для застосунку – це є вимогою принципу найменших. 
Команди встановлення системних залежностей представлені на рисунку 4.2. 
Рисунок 4.2 – Створення бази даних та користувача MySQL 
Обрано кодування utf8mb4, воно підтримує повний діапазон символів Unicode, 
включаючи емодзі та спеціальні символи, що є важливим для коректного 
відображення назв товарів та відгуків покупців. 
4.2 Розгортання Flask-застосунку 
Розгортання застосунку передбачає перенесення вихідного коду на сервер, 
налаштування ізольованого середовища Python, встановлення залежностей та 
ініціалізацію бази даних. Структура проєкту включає: основний файл app.py з 
маршрутами та бізнес-логікою, файл models.py з ORM-описом таблиць MySQL, 
конфігураційний файл config.py, скрипт create_db.py для першого створення схеми 
59 
 
бази даних, директорію статичних файлів static/ та шаблони Jinja2 у директорії 
templates/. 
Чітке розділення відповідальностей між файлами проєкту полегшує як 
підтримку коду, так і процес розгортання. Окремий скрипт create_db.py дозволяє 
ініціалізувати схему бази даних незалежно від запуску основного застосунку, що є 
зручним при першому розгортанні або відновленні після збою. 
Для розгортання застосунку потрібно виконати  наступну послідовність дій: 
1. Клонування репозиторію проєкту на сервер у виділену директорію 
показано на рисунку 4.3. 
 
Рисунок 4.3 – Директорія для клонування репозиторію на сервер 
2. Перехід до директорії проєкту та створення ізольованого віртуального 
середовища Python показано на рисунку 4.4. 
 
Рисунок 4.4 – Створення віртуального середовища Python 
Використання virtualenv забезпечує ізоляцію залежностей проєкту від 
системного Python та інших застосунків на сервері. Завдяки цьому оновлення 
системних пакетів не впливає на роботу застосунку. 
3. Активація віртуального середовища та встановлення всіх залежностей 
проєкту показано на рисунку 4.5. 
 
Рисунок 4.5 – Встановлення залежностей із requirements.txt 
60 
 
Файл requirements.txt містить зафіксовані версії всіх необхідних бібліотек: Flask, 
Flask-Login для управління авторизацією, SQLAlchemy для ORM-взаємодії з MySQL, 
PyMySQL як драйвер підключення до MySQL, ReportLab для генерації PDF-чеків, а 
також допоміжні бібліотеки. Фіксація версій гарантує відтворюваність середовища. 
4. Налаштування рядка підключення до MySQL та секретного ключа у 
config.py показано на рисунку 4.6. 
 
Рисунок 4.6 – Конфігурація підключення до MySQL у config.py 
У виробничому середовищі значення SECRET_KEY та пароль бази даних 
зберігаються у змінних середовища або в окремому захищеному файлі конфігурації. 
5. Ініціалізація схеми бази даних MySQL через скрипт create_db.py показана 
на рисунку 4.7. 
 
Рисунок 4.7 – Запуск скрипта ініціалізації схеми бази даних 
Скрипт create_db.py використовує метод db.create_all() SQLAlchemy, який 
аналізує всі визначені у models.py моделі та автоматично створює відповідні таблиці 
у базі даних MySQL. Це включає таблиці для зберігання товарів, зареєстрованих 
користувачів, вмісту кошиків, списків бажаного та оформлених замовлень. Підхід 
ORM-first дозволяє підтримувати схему бази даних синхронізованою з Python-
моделями без необхідності написання SQL DDL вручну. 
6. Встановлення правильних прав доступу до файлів застосунку 
представлено на рисунку 4.8. 
61 
 
 
Рисунок 4.8 – Налаштування прав доступу до файлів застосунку 
4.3 Конфігурація Gunicorn та Nginx 
Вбудований сервер розробки Flask є однопотоковим, не підтримує паралельну 
обробку запитів і не забезпечує механізмів відмовостійкості. Тому для виробничого 
середовища він замінюється на Gunicorn (Green Unicorn) – зрілий і широко 
використовуваний WSGI-сервер для Python-застосунків. 
Gunicorn запускає кілька незалежних робочих процесів (workers), кожен з яких 
може обробляти окремий HTTP-запит. Це дозволяє ефективно використовувати 
багатоядерні процесори та забезпечує відмовостійкість: збій одного воркера не 
впливає на роботу інших.  
Для автоматичного запуску Gunicorn при старті операційної системи та 
автоматичного перезапуску в разі збою створюється systemd-служба. Файл 
конфігурації /etc/systemd/system/tennis_shop.service представлено на рисунку 4.9. 
 
Рисунок 4.9 – Конфігураційний файл systemd-служби для Gunicorn 
62 
 
Директива After=mysql.service у поєднанні з Requires=mysql.service гарантує, 
що Flask-застосунок запускається лише після повної готовності MySQL-сервера. Це 
критично важливо, оскільки при старті Flask намагається встановити пул з'єднань з 
базою даних, і якщо MySQL ще не готовий – застосунок завершиться з помилкою. 
Директива Restart=always із затримкою RestartSec=5 забезпечує автоматичне 
відновлення після непередбачених збоїв. 
Активація та запуск служби відображені на рисунку 4.10. 
 
Рисунок 4.10 – Активація та запуск systemd-служби Gunicorn 
Також важливим для налаштування середовища є Nginx. Він відіграє роль 
зворотного проксі-сервера (reverse proxy): він приймає всі вхідні HTTP-запити від 
користувачів та перенаправляє їх до Gunicorn через Unix-сокет. Окрім проксіювання 
динамічних запитів, Nginx самостійно обслуговує статичні файли застосунку – 
кастомні CSS-стилі (static/css/style.css), JavaScript-скрипти для асинхронних запитів 
(static/js/main.js) та зображення товарів із директорії static/img/. Роздача статики 
безпосередньо через Nginx є значно ефективнішою, ніж через Flask, і знижує 
навантаження на Python-процеси. 
Конфігураційний файл /etc/nginx/sites-available/tennis_shop представлено на 
рисунку 4.11. 
63 
 
 
Рисунок 4.11 – Конфігурація Nginx як зворотного проксі для Flask-застосунку 
Директива expires 30d для статичних файлів вказує браузеру кешувати їх 
протягом 30 днів, що суттєво прискорює повторне завантаження сторінок. Обмеження 
client_max_body_size 5M встановлено для можливості завантаження зображень 
товарів через адмін-панель (/add_product). 
Активація конфігурації, перевірка синтаксису та перезапуск Nginx 
представлена на рисунку 4.12. 
 
Рисунок 4.12 – Активація конфігурації та перезапуск Nginx 
64 
 
4.4 Планування та методологія тестування 
Тестування програмного забезпечення є обов'язковою складовою процесу 
розробки, що дозволяє виявляти дефекти на ранніх етапах, підтверджувати 
відповідність системи функціональним вимогам та підвищувати впевненість у 
коректності роботи застосунку [11]. Для інтернет-магазину якість роботи є критично 
важливою: помилки в логіці кошика, оформлення замовлень чи авторизації можуть 
безпосередньо впливати на досвід покупців. 
Було застосовано три взаємодоповнюючі рівні тестування: 
– модульне тестування (unit testing) – перевірка окремих функцій та методів 
моделей SQLAlchemy у файлі models.py у повній ізоляції від інших компонентів; 
– інтеграційне тестування (integration testing) – перевірка взаємодії між Flask-
маршрутами у app.py, моделями бази даних MySQL та підсистемою авторизації Flask-
Login; 
– функціональне тестування (functional testing) – наскрізна перевірка повних 
користувацьких сценаріїв від перегляду каталогу до завантаження PDF-чеку. 
Для написання та виконання тестів використовується pytest разом із вбудованим 
тестовим HTTP-клієнтом Flask. З метою ізоляції тестів від виробничої MySQL-бази 
даних, тестова конфігурація використовує SQLite. Тестові фікстури налаштовуються 
наступним чином. Конфігурація тестового середовища та базові фікстури pytest 
представлена на рисунку 4.13. 
65 
 
 
Рисунок 4.13 – Конфігурація тестового середовища та базові фікстури pytest 
Фіксація scope='function' гарантує, що кожен тест отримує чисту базу даних без 
залишків попередніх тестів, що унеможливлює взаємний вплив тестів один на одного. 
4.5 Модульне тестування 
Модульне тестування зосереджено на перевірці коректності роботи окремих 
компонентів застосунку. Основним об'єктом є моделі SQLAlchemy, визначені у файлі 
models.py: User, Product, Cart, Wishlist та Order. Окремо тестується функціонал 
генерації PDF-документів через ReportLab. 
66 
 
Тестування моделі User та механізму безпечного зберігання паролів через 
Werkzeug відображено на рисунку 4.14. 
 
Рисунок 4.14 – Модульні тести моделі User (models.py) 
67 
 
Тестування моделей Product, Wishlist та Order відображено на рисунку 4.15. 
 
Рисунок 4.15 – Модульні тести моделей Product, Wishlist та Order  
Результати модульного тестування наведено в таблиці 4.1. 
  
68 
 
Таблиця 4.1 – Результати модульного тестування 
Модуль Опис тесту Очікуваний результат Статус 
User Пароль зберігається Дані збережені у Пройдено 
хешованим (Werkzeug) хешованому вигляді 
User Перевірка правильного Авторизація Пройдено 
пароля 
User Перевірка неправильного Відмова у авторизація Пройдено 
пароля 
User Збереження в MySQL через Запис знайдено в БД Пройдено 
SQLAlchemy 
User Унікальність поля email Виняток при дублікаті Пройдено 
Product Створення товару з усіма Запис збережено Пройдено 
полями 
Wishlist Додавання товару до Запис у таблиці wishlist Пройдено 
вішліста 
Order Створення замовлення зі Запис у таблиці orders Пройдено 
статусом 
4.6 Інтеграційне тестування 
Інтеграційне тестування спрямоване на перевірку коректної взаємодії між 
компонентами застосунку: Flask-маршрутами у app.py, ORM-моделями models.py, 
базою даних та підсистемою авторизації Flask-Login. На відміну від модульного 
тестування, інтеграційне виявляє проблеми, що виникають саме на межах взаємодії 
між модулями. 
Для зручного тестування авторизованих сценаріїв створюється допоміжна 
фікстура auth (рисунок 4.16). 
69 
 
 
Рисунок 4.16 – Допоміжна фікстура AuthActions для тестування авторизованих 
маршрутів 
Тестування маршрутів авторизації та захисту сторінок через Flask-Login 
представлено на рисунку 4.17. 
 
Рисунок 4.17 – Інтеграційні тести маршрутів авторизації та захисту сторінок 
70 
 
Тестування маршрутів каталогу, кошика, вішліста та PDF-чеку представлено на 
рисунку 4.18. 
 
Рисунок 4.18 – Інтеграційні тести маршрутів каталогу, кошика, вішліста та PDF-чеку 
Результати інтеграційного тестування наведені в таблиці 4.2. 
71 
 
Таблиця 4.2 – Результати інтеграційного тестування 
Маршрут (app.py) Метод Сценарій Результат 
/ GET Головна / каталог (index.html) Пройдено 
/product/<id> GET Картка товару (product_detail.html) Пройдено 
/register POST Реєстрація нового користувача Пройдено 
/login POST Вхід з коректними даними Пройдено 
/login POST Вхід з хибним паролем Пройдено 
/logout GET Вихід із системи Пройдено 
/cart/add POST Додати товар у кошик Пройдено 
/wishlist/add POST Додати до вішліста Пройдено 
/checkout GET Оформлення без авторизації Пройдено 
/my_orders GET Замовлення без авторизації Пройдено 
/orders/<id>/receipt GET Генерація PDF-чеку (ReportLab) Пройдено 
/add_product GET Адмін-панель без прав адміна Пройдено 
4.7 Функціональне тестування 
Функціональне тестування є найвищим рівнем тестування у даній роботі і 
спрямоване на перевірку повних користувацьких сценаріїв з точки зору кінцевого 
покупця та адміністратора інтернет-магазину тенісного спорядження. Для кожного 
тест-кейсу визначено мету, передумови, покрокові дії, очікуваний та фактичний 
результати. 
Тест-кейс «Реєстрація нового користувача». 
Мета: перевірити коректність роботи форми реєстрації та збереження даних у 
MySQL. 
Передумови: застосунок запущено, таблиця users у MySQL порожня. 
Кроки: перейти на /register, заповнити форму login.html, натиснути кнопку 
реєстрації. 
Очікуваний результат: новий запис у таблиці users з хешованим паролем, 
перенаправлення на сторінку входу. 
72 
 
Фактичний результат: відповідає очікуваному. Тест пройдено. 
Тест-кейс «Авторизація та захист маршрутів через Flask-Login». 
Мета: перевірити коректне встановлення сесії Flask-Login та захист сторінок 
@login_required. 
Передумови: обліковий запис зареєстровано. 
Кроки: перейти на /login, ввести коректні email та пароль. 
Очікуваний результат: сесія Flask-Login встановлена, доступ до захищених 
сторінок надано. 
Фактичний результат: відповідає очікуваному. Тест пройдено. 
Тест-кейс «Перегляд каталогу та картки товару». 
Мета: перевірити коректне відображення шаблонів index.html та 
product_detail.html з даними з MySQL. 
Передумови: у таблиці products MySQL є записи про товари. 
Кроки: перейти на головну сторінку, обрати конкретний товар. 
Очікуваний результат: шаблони Jinja2 коректно рендеряться з даними з MySQL, 
зображення завантажуються зі static/img/. 
Фактичний результат: відповідає очікуваному. Тест пройдено. 
Тест-кейс « Додавання товару до кошика. 
Мета: перевірити функцію додавання товару та відображення cart.html. 
Передумови: користувач авторизований, у каталозі є товари (stock > 0). 
Кроки: натиснути «Додати до кошика», перейти до /cart. 
Очікуваний результат: товар відображається у cart.html з коректною кількістю 
та сумою. 
Фактичний результат: відповідає очікуваному. Тест пройдено. 
Тест-кейс «Додавання товару до списку бажаного». 
Мета: перевірити функціонал вішліста та відображення wishlist.html. 
Передумови: користувач авторизований. 
Кроки: натиснути «Додати до вішліста», перейти до /wishlist. 
73 
 
Очікуваний результат: товар відображається у wishlist.html, повторне додавання 
не створює дублікатів. 
Фактичний результат: відповідає очікуваному. Тест пройдено. 
Тест-кейс « Оформлення замовлення через checkout». 
Мета: перевірити повний сценарій оформлення замовлення через checkout.html. 
Передумови: користувач авторизований, у кошику є товари. 
Кроки: перейти на /checkout, заповнити форму з адресою доставки, підтвердити. 
Очікуваний результат: новий запис у таблиці orders MySQL, stock товарів 
зменшено, кошик очищено, редирект на my_orders.html. 
Фактичний результат: відповідає очікуваному. Тест пройдено. 
Тест-кейс «Генерація та завантаження PDF-чеку». 
Мета: перевірити коректність генерації PDF-документу бібліотекою ReportLab. 
Передумови: у my_orders.html відображається щонайменше одне замовлення. 
Кроки: натиснути «Завантажити чек» у списку замовлень. 
Очікуваний результат: відповідь з Content-Type: application/pdf із даними 
замовлення. 
Фактичний результат: відповідає очікуваному. Тест пройдено. 
Тест-кейс « Додавання товару через адмін-панель». 
Мета: перевірити роботу форми add_product.html для додавання нових товарів. 
Передумови: користувач авторизований з правами адміністратора. 
Кроки: перейти на /add_product, заповнити форму, завантажити зображення, 
зберегти. 
Очікуваний результат: новий запис у таблиці products, зображення у static/img/, 
товар з'являється у каталозі. 
Фактичний результат: відповідає очікуваному. Тест пройдено. 
Тест-кейс «Захист сторінок без авторизації». 
Мета: перевірити, що неавторизований користувач не може отримати доступ до 
захищених розділів. 
74 
 
Кроки: без авторизації спробувати перейти на /checkout, /my_orders, /wishlist та 
/add_product. 
Очікуваний результат: Flask-Login перенаправляє на /login для всіх захищених 
маршрутів. 
Фактичний результат: відповідає очікуваному. Тест пройдено. 
Зведені результати функціонального тестування наведені в таблиці 4.3. 
Таблиця 4.3 – Результати функціонального тестування 
Сценарій Очікуваний результат Фактичний Статус 
результат 
Реєстрація нового Запис у MySQL, редирект Відповідає Пройдено 
користувача 
Авторизація через Flask- Сесія встановлена Відповідає Пройдено 
Login 
Каталог та картка товару Шаблони Jinja2 Відповідає Пройдено 
відображено 
Додавання товару до кошика Запис у БД, cart.html Відповідає Пройдено 
оновлено 
Додавання товару до Запис у wishlist MySQL Відповідає Пройдено 
вішліста 
Оформлення замовлення Запис у orders, stock--, Відповідає Пройдено 
(checkout) редирект 
Генерація PDF-чеку PDF-файл (application/pdf) Відповідає Пройдено 
(ReportLab) 
Адмін-панель: додавання Товар у каталозі Відповідає Пройдено 
товару 
Захист маршрутів без Редирект на /login Відповідає Пройдено 
авторизації 
 
75 
 
4.8 Тестування безпеки 
Безпека веб-застосунку є критично важливою вимогою для інтернет-магазину, 
оскільки система обробляє персональні дані користувачів та інформацію про 
замовлення. Тестування безпеки проводилось з метою підтвердження коректності 
реалізованих механізмів захисту та відсутності поширених веб-вразливостей. 
Захист від SQL-ін'єкцій забезпечується через ORM SQLAlchemy: всі взаємодії з 
MySQL здійснюються виключно через методи ORM, які автоматично формують 
параметризовані SQL-запити. Під час тестування здійснювалися спроби введення 
SQL-коду у форми реєстрації, логіну та пошуку товарів – жодна з них не призвела до 
некоректної поведінки застосунку. 
Захист від XSS реалізовано через автоматичне екранування символів у Jinja2-
шаблонах. Ручна перевірка підтвердила, що введення тегу <script>alert('xss')</script> 
в усі доступні поля форм призводить до відображення тексту в екранованому вигляді, 
а не до виконання скрипта. 
Паролі зберігаються у таблиці users MySQL виключно у хешованому вигляді 
через Werkzeug bcrypt. Оригінальний пароль не зберігається і не може бути 
відновлений. Управління сесіями реалізовано через Flask-Login із декоратором 
@login_required. 
Перевірено захист від таких сценаріїв атак: 
– SQL-ін'єкція через поля форм – захист SQLAlchemy ORM підтверджено; 
– XSS через поля введення – автоматичне екранування Jinja2 підтверджено; 
– несанкціонований доступ до /checkout та /my_orders – Flask-Login 
перенаправляє на /login; 
– перегляд чужих замовлень через підміну ID – перевірка на рівні маршруту 
підтверджена; 
– несанкціонований доступ до /add_product – перевірка прав адміністратора 
підтверджена; 
– розкриття паролів через базу даних – зберігаються лише bcrypt-хеші. 
76 
 
Висновок до розділу 4 
У розділі детально детально розглянуто процес розгортання інтернет-магазину 
на базі Flask та MySQL у виробничому середовищі, а також представлено результати 
комплексного тестування системи на трьох рівнях. 
Розгортання застосунку реалізовано з використанням стеку Nginx + Gunicorn + 
MySQL. База даних MySQL із кодуванням utf8mb4 створена з використанням 
принципу найменших привілеїв. Схема бази даних ініціалізована через скрипт 
create_db.py на основі моделей SQLAlchemy з models.py. Systemd-служба 
налаштована з директивами After=mysql.service та Requires=mysql.service, що 
гарантує коректний порядок запуску компонентів. Роздача статичних файлів style.css, 
main.js та зображень товарів із static/img/ делегована Nginx з кешуванням на 30 днів. 
З використанням модульного тестування було проведено 8 тестів для моделей 
models.py (User, Product, Wishlist, Order) та підтвердило коректність механізму 
хешування паролів і обмежень унікальності MySQL. З використанням інтеграційного 
тестування було проведено 12 тестів які підтвердилі взаємодію Flask, SQLAlchemy 
ORM і Flask-Login, включно з генерацією PDF-чеків ReportLab. Також було 
використано  функціональне тестування для  9 наскрізних тест-кейсів повного циклу 
роботи магазину. Усі тести пройдено успішно. 
Було протестовано безпеку, для підтвердило захищеність від SQL-ін'єкцій 
(SQLAlchemy ORM), XSS (Jinja2), несанкціонованого доступу (Flask-Login), 
перегляду чужих замовлень, несанкціонованого доступу до адмін-панелі та розкриття 
паролів (Werkzeug bcrypt). Критичних уразливостей не виявлено.  
77 
 
ВИСНОВКИ 
Під час виконання кваліфікаційної роботи бакалавра було здійснено аналіз 
предметної області та існуючих рішень у сфері електронної комерції. Виявлено, що 
сучасні інтернет-магазини мають низку недоліків, зокрема застарілий дизайн, складну 
навігацію та недостатню орієнтацію на користувача. Це обґрунтувало необхідність 
створення нового рішення, яке відповідатиме сучасним вимогам ринку та 
забезпечуватиме високий рівень користувацького досвіду. 
В роботі виконано проєктування системи із застосуванням сучасного 
технологічного стеку та архітектурного підходу MVC. Було визначено структуру 
застосунку, спроєктовано базу даних, описано ролі користувачів і сценарії взаємодії, 
а також розроблено логічну структуру веб-інтерфейсу. Це створило надійну основу 
для подальшої реалізації системи. 
Було здійснено програмну реалізацію розробленого інтернет-магазину. Було 
впроваджено основний функціонал системи, включаючи реєстрацію та авторизацію 
користувачів, роботу з каталогом товарів, управління кошиком, оформлення 
замовлень та адміністративний модуль. Значну увагу приділено реалізації сучасного 
UI/UX-дизайну, що забезпечує зручну навігацію, адаптивність інтерфейсу та швидку 
взаємодію користувача із системою. 
Було детально розглянуто процес розгортання інтернет-магазину на базі Flask 
та MySQL у виробничому середовищі.  
Було проведене комплексне тестування системи на трьох рівнях, проведене  29 
тестів, усі з яких пройдено успішно. Модульне тестування (8 тестів) підтвердило 
коректність моделей даних, хешування паролів та обмежень унікальності; 
інтеграційне (12 тестів) -  взаємодію Flask, SQLAlchemy ORM, Flask-Login і генерацію 
PDF-чеків; функціональне (9 тест-кейсів) - повний цикл роботи магазину. Окремо 
проведено тестування безпеки, яке підтвердило захищеність від SQL-ін'єкцій, XSS-
атак та несанкціонованого доступу - критичних уразливостей не виявлено. 
78 
 
В результаті виконання кваліфікаційної роботи бакалавра були досягнуті всі 
цілі, визначені на етапі розробки вимог і проектування та розроблено готовий 
інтернет-магазин з продажу товарів для настільного тенісу. 
  
79 
 
СПИСОК ВИКОРИСТАНИХ ДЖЕРЕЛ 
1. Інтернет-магазин «Pingpong.com.ua». URL: https://pingpong.com.ua/ (дата 
звернення 18.04.2026) 
2. Інтернет-магазин TT-Maximum.com.ua. URL: https://www.tt-maximum.com/uk/ 
(дата звернення 18.04.2026) 
3. Інтернет-магазин Tabletennis.com.ua. URL: https://tabletennis.com.ua/uk/ (дата 
звернення 18.04.2026) 
4. Інтернет-магазин TT-Pro.com.ua. URL: https://tt-pro.com.ua/ua/ (дата звернення 
18.04.2026) 
5. Офіційний сайт Python. URL: https://docs.python.org/ (дата звернення 
19.04.2026) 
6. Офіційний сайт Flask. URL: https://flask.palletsprojects.com/ (дата звернення 
19.04.2026) 
7. Офіційний сайт Bootstrap. URL: https://getbootstrap.com/ (дата звернення 
20.04.2026) 
8. Пояснення основних концепцій веб-розробки. URL: 
https://www.w3schools.com/ (дата звернення 21.04.2026) 
9. Керівництво по створенню онлайн-магазину. URL: 
https://www.bigcommerce.com/ (дата звернення 23.04.2026) 
10. Правильна побудова онлайн магазину. URL: 
https://www.bigcommerce.com/articles/ecommerce/create-online-store/ (дата 
звернення 23.04.2026) 
11. Оптимізація онлайн магазину. URL: https://www.optimonk.com/ecommerce-
optimization/ (дата звернення 25.04.2026) 
12. Інтеграція платіжних систем та налаштування доставки. URL: 
https://www.squarespace.com/ (дата звернення 26.04.2026) 
 
ДОДАТОК А 
 
«ЗАТВЕРДЖУЮ» 
Зав. кафедрою КНСА 
_____________ Юрій ТРИУС 
«     »                  2026 р. 
 
 
 
 
 
 
Інтернет-магазин з продажу товарів для настільного тенісу 
 
 
 
Специфiкацiя 
482.ЧДТУ.62503-01 
 
Листів 2 
 
 
 
 
 
 
 
Розробник: В’ячеслав КАШТАН 
  
Керівник: Микола ПІДГОРИЙ 
 
 
 
 
 
 
 
 
Черкаси – 2026 
81 
 
Позначення Найменування Примітка 
   
   
   
   
482.ЧДТУ.62503-01 12 01 Текст програми  
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
 
82 
 
ДОДАТОК Б 
 
«ЗАТВЕРДЖУЮ» 
Зав. кафедрою КНСА 
_____________ Юрій ТРИУС 
«____»                   2026 р. 
 
 
 
 
 
 
Інтернет-магазин з продажу товарів для настільного тенісу 
 
 
 
Текст програми 
482.ЧДТУ.62503-01 12 01 
 
Листів 2 
 
 
 
 
 
 
 
Розробник: В’ячеслав КАШТАН 
  
Керівник: Микола ПІДГОРИЙ 
 
 
 
 
 
 
 
 
Черкаси – 2026 
83 
 
"add_product.html"                                     <option value="" disabled 
{% extends "base.html" %} selected>Оберіть категорію...</option> 
                                     {% for category in 
{% block title %}Додати товар — PingPong categories %} 
Shop{% endblock %}                                         <option value="{{ 
 category.id }}">{{ category.name }}</option> 
{% block content %}                                     {% endfor %} 
<div class="container py-5">                                 </select> 
    <div class="row justify-content-center">                             </div> 
        <div class="col-lg-8 col-xl-7">  
                             <div class="col-md-6"> 
            <div class="mb-4">                                 <label class="form-label fw-
                <a href="{{ url_for('index') }}" semibold">Бренд</label> 
class="text-decoration-none text-muted small d-inline-flex                                 <input type="text" 
align-items-center"> name="brand" class="form-control rounded-3" 
                    <i class="bi bi-arrow-left me-2"></i> placeholder="Наприклад: Donic, Butterfly"> 
Назад до каталогу                             </div> 
                </a>  
            </div>                             <div class="col-md-6"> 
                                 <label class="form-label fw-
            <div class="card shadow-lg border-0 semibold">Колір</label> 
rounded-4 overflow-hidden">                                 <input type="text" 
 name="color" class="form-control rounded-3" 
                <div class="card-header bg-dark text- placeholder="Наприклад: Червоний, Чорний"> 
white py-4 px-4">                             </div> 
                    <h4 class="mb-0 fw-bold">  
                        <i class="bi bi-plus-circle me-2                             <div class="col-12"> 
text-warning"></i>Додати новий товар                                 <label class="form-label fw-
                    </h4> semibold">Зображення товару</label> 
                </div>                                 <input type="file" 
 name="image" class="form-control rounded-3" 
                <div class="card-body p-4 bg-white"> accept="image/*" required> 
                                 <div class="form-text text-
                    <form action="{{ muted small"> 
url_for('add_product') }}" method="POST"                                     <i class="bi bi-info-circle 
enctype="multipart/form-data"> me-1"></i> Будь ласка, оберіть графічний файл (.png, 
 .jpg, .jpeg). Максимальний розмір — 5MB. 
                        <div class="row g-4">                                 </div> 
                             </div> 
                            <div class="col-12">  
                                <label class="form-label fw-                             <div class="col-12"> 
semibold">Назва товару</label>                                 <label class="form-label fw-
                                <input type="text" semibold">Опис товару</label> 
name="name" class="form-control rounded-3"                                 <textarea name="description" 
placeholder="Наприклад: Ракетка Donic Waldner 800" class="form-control rounded-3" rows="4" 
required> placeholder="Детальний опис характеристик 
                            </div> інвентарю..."></textarea> 
                             </div> 
                            <div class="col-md-6">  
                                <label class="form-label fw-                             <div class="col-12 d-flex justify-
semibold">Ціна (грн)</label> content-end gap-2 mt-4"> 
                                <input type="number"                                 <a href="{{ url_for('index') 
name="price" step="0.01" class="form-control rounded-3" }}" class="btn btn-light border rounded-3 px-4"> 
placeholder="0.00" required>                                     Скасувати 
                            </div>                                 </a> 
                                 <button type="submit" 
                            <div class="col-md-6"> class="btn btn-primary rounded-3 px-4 fw-bold"> 
                                <label class="form-label fw-                                     <i class="bi bi-check-lg 
semibold">Категорія</label> me-1"></i> Зберегти товар 
                                <select name="category_id"                                 </button> 
class="form-select rounded-3" required>                             </div> 
 
84 
 
                        </div>                             <br><span class="badge bg-
 secondary-subtle text-secondary border mt-1" style="font-
                    </form> size: 11px;">Юзер ID: {{ order.user_id }}</span> 
                         {% else %} 
                </div>                             <br><span class="badge bg-
            </div> light text-muted border mt-1" style="font-size: 11px;">   
 Замовлення-Гість</span> 
        </div>                         {% endif %} 
    </div>                     </td> 
</div>                     <td class="small"> 
{% endblock %}                         <div><i class="bi bi-telephone-fill 
 text-muted me-1"></i>{{ order.phone }}</div> 
===================================                         <div><i class="bi bi-envelope-fill 
=============== text-muted me-1"></i>{{ order.email }}</div> 
                     </td> 
"admin_orders.html"                     <td class="small"> 
{% extends "base.html" %}                         <div><strong>Спосіб:</strong> 
 {{ order.delivery_method }}</div> 
{% block title %}Управління замовленнями —                         <div class="text-muted text-
PingPong Shop{% endblock %} truncate" style="max-width: 220px;" title="{{ 
 order.address }}"><strong>Адреса:</strong> {{ 
{% block content %} order.address }}</div> 
<div class="container py-5">                         <div class="text-
 muted"><strong>Оплата:</strong> {{ 
    <div class="d-flex justify-content-between order.payment_method }}</div> 
align-items-center mb-4">                     </td> 
        <h2 class="fw-bold mb-0">                     <td class="fw-bold text-success fs-
            <i class="bi bi-box-seam me-2 text- 5">{{ order.total_price }} ₴</td> 
warning"></i>Усі замовлення (Адмінка)                     <td> 
        </h2>                         <form action="{{ 
        <span class="badge bg-dark fs-6 rounded-3 url_for('update_order_status', order_id=order.id) }}" 
px-3 py-2">Всього замовлень: {{ orders|length }}</span> method="POST" class="d-flex align-items-center gap-1"> 
    </div>                             <select name="status" 
 class="form-select form-select-sm fw-semibold border-
    {% if orders %} secondary-subtle py-1" style="width: 145px; font-size: 
    <div class="table-responsive shadow-sm 13px;"> 
rounded-4 border-0">                                 <option value="new" {% if 
        <table class="table table-hover align-middle order.status == 'new' %}selected{% endif %}>   
mb-0 bg-white"> Нове</option> 
            <thead class="table-dark">                                 <option value="processing" 
                <tr> {% if order.status == 'processing' %}selected{% endif 
                    <th>ID</th> 
%}>   В обробці</option> 
                    <th>Дата та час</th> 
                                <option value="shipped" {% 
                    <th>Клієнт</th> 
if order.status == 'shipped' %}selected{% endif %}>   
                    <th>Контакти</th> 
                    <th>Доставка та Оплата</th> Відправлено</option> 
                    <th>Сума</th>                                 <option value="done" {% if 
                    <th>Статус</th> order.status == 'done' %}selected{% endif %}>   
                    <th>Дії</th> Завершено</option> 
                </tr>                             </select> 
            </thead>                             <button type="submit" 
            <tbody> class="btn btn-sm btn-dark px-2 py-1" title="Зберегти 
                {% for order in orders %} новий статус"> 
                <tr>                                 <i class="bi bi-check-lg"></i> 
                    <td class="fw-bold text-                             </button> 
secondary">#{{ order.id }}</td>                         </form> 
                    <td class="small">{{                     </td> 
order.created_at.strftime('%d.%m.%Y %H:%M') }}</td>                     <td> 
                    <td>                         <div class="d-flex gap-1"> 
                        <span class="fw-bold text-                             <a href="{{ 
dark">{{ order.name }}</span> url_for('print_receipt', order_id=order.id) }}" class="btn 
                        {% if order.user_id %} 
85 
 
btn-sm btn-outline-secondary" title="Переглянути та                                             </tbody> 
надрукувати чек" target="_blank">                                         </table> 
                                <i class="bi bi-printer"></i>                                     </div> 
                            </a>                                 </div> 
                            <button class="btn btn-sm btn-                             </div> 
outline-primary" type="button" data-bs-toggle="collapse"                         </div> 
data-bs-target="#items-{{ order.id }}" aria-                     </td> 
expanded="false" title="Переглянути список товарів">                 </tr> 
                                <i class="bi bi-eye"></i>                 {% endfor %} 
                            </button>             </tbody> 
                        </div>         </table> 
                    </td>     </div> 
                </tr>     {% else %} 
  
                <tr class="bg-light-subtle">     <div class="text-center py-5 bg-white rounded-
                    <td colspan="8" class="p-0 border- 4 shadow-sm mt-4"> 
0">         <i class="bi bi-inbox display-4 text-
                        <div class="collapse" id="items-{{ muted"></i> 
order.id }}">         <h4 class="mt-3 text-muted">Список 
                            <div class="p-3 bg-light border- замовлень порожній</h4> 
bottom border-top">         <p class="text-muted mb-0">Щойно 
                                <div class="card card-body клієнти почнуть оформлювати товари, вони з'являться 
border-0 shadow-sm p-3 bg-white"> тут.</p> 
                                    <h6 class="fw-bold text-     </div> 
dark mb-2 small d-flex align-items-center">  
                                        <i class="bi bi-box2-fill     {% endif %} 
text-primary me-2"></i> Вміст замовлення #{{ order.id  
}}: </div> 
                                    </h6> {% endblock %} 
                                    <div class="table-  
responsive m-0"> ===================================
                                        <table class="table table- =============== 
sm table-bordered align-middle m-0 small text-center">  
                                            <thead class="table- "admin_users.html" 
light text-muted"> {% extends "base.html" %} 
                                                <tr>  
                                                    <th class="text- {% block title %}Керування користувачами — 
start">Назва товару</th> Адмінка{% endblock %} 
                                                    <th>Ціна на  
момент купівлі</th> {% block content %} 
                                                    <div class="container py-4"> 
<th>Кількість</th>  
                                                    <th>Сума</th>     <div class="d-flex justify-content-between 
                                                </tr> align-items-center mb-4"> 
                                            </thead>         <h2 class="fw-bold m-0"> 
                                            <tbody>             <i class="bi bi-people me-
                                                {% for item in 2"></i>Користувачі 
order.items %}         </h2> 
                                                <tr>     </div> 
                                                    <td class="text-  
start fw-semibold text-secondary">{{ item.product.name     <div class="card border-0 shadow-sm rounded-
}}</td> 4"> 
                                                    <td>{{         <div class="card-body p-0 table-
item.price_at_purchase }} ₴</td> responsive"> 
                                                    <td><span             <table class="table table-hover align-
class="badge bg-secondary px-2 py-1">x{{ item.quantity middle mb-0"> 
}}</span></td>                 <thead class="table-light"> 
                                                    <td class="fw-                     <tr> 
bold text-dark">{{ item.price_at_purchase * item.quantity                         <th class="px-4 py-3">ID</th> 
}} ₴</td>                         <th class="py-
                                                </tr> 3">Користувач</th> 
                                                {% endfor %}                         <th class="py-3">Email</th> 
86 
 
                        <th class="px-4 py-3 text-                 </tbody> 
end">Дії</th>             </table> 
                    </tr>         </div> 
                </thead>     </div> 
                <tbody>  
                    {% for user in users %} </div> 
                    <tr> {% endblock %} 
                        <td class="px-4 fw-bold text-  
muted">#{{ user.id }}</td> ===================================
                        <td> =============== 
                            {% if user.username == 'admin'  
%} "app.py" 
                                <span class="badge bg-danger from flask import Flask, render_template, request, 
me-1">Адмін</span> redirect, session, url_for, send_file, abort, flash 
                            {% endif %} from models import db, Product, CartItem, Order, 
                            <strong>{{ user.username OrderItem, Category, User 
}}</strong> from config import Config 
 from datetime import datetime, timezone 
                            {% if user.name %} import os 
                                <br><small class="text- from werkzeug.security import 
muted"><i class="bi bi-person me-1"></i>{{ user.name generate_password_hash, check_password_hash 
}}</small> from werkzeug.utils import secure_filename 
                            {% endif %}  
                        </td> app = Flask(__name__) 
                        <td>{{ user.email if user.email else app.config.from_object(Config) 
'<span class="text-muted small">Не вказано</span>'|safe  
}}</td> db.init_app(app) 
                        <td class="px-4 text-end">  
                            {% if user.username != 'admin' Product.image_url = property(lambda self: 
%} self.image if self.image else 
                                <a href="{{ 'https://via.placeholder.com/200') 
url_for('edit_user', user_id=user.id) }}" class="btn btn-sm Category.__str__ = lambda self: self.name 
btn-outline-primary me-1" title="Редагувати"> Category.__repr__ = lambda self: self.name 
                                    <i class="bi bi-pencil"></i>  
Редагувати class WishlistItem: 
                                </a>     def __init__(self, product): 
         self.product = product 
                                <form method="POST"  
action="{{ url_for('delete_user', user_id=user.id) }}" @app.context_processor 
style="display:inline;" onsubmit="return confirm('Ви def inject_now(): 
впевнені, що хочете назавжди видалити цього     return {'current_year': 
користувача?');"> datetime.now(timezone.utc).year} 
                                    <button type="submit"  
class="btn btn-sm btn-outline-danger" title="Видалити"> @app.route('/') 
                                        <i class="bi bi- def index(): 
trash"></i> Видалити     query = Product.query.filter_by(is_active=True) 
                                    </button>  
                                </form>     search_query = request.args.get('q', '').strip() 
                            {% else %}     if search_query: 
                                <button class="btn btn-sm         query = 
btn-outline-secondary query.filter(Product.name.contains(search_query)) 
disabled">Недоторканний</button>  
                            {% endif %}     category_id = request.args.get('category', 
                        </td> '').strip() 
                    </tr>     if category_id and category_id.isdigit(): 
                    {% else %}         query = 
                    <tr> query.filter_by(category_id=int(category_id)) 
                        <td colspan="4" class="text-center  
py-4 text-muted">Немає зареєстрованих     brand_query = request.args.get('brand', '').strip() 
користувачів.</td>     if brand_query: 
                    </tr>         query = query.filter_by(brand=brand_query) 
                    {% endfor %}  
87 
 
    color_query = request.args.get('color', '').strip()             file = request.files['image'] 
    if color_query:             if file and file.filename != '': 
        query = query.filter_by(color=color_query)                 filename = 
 secure_filename(file.filename) 
    products = query.all()                 upload_folder = 
    categories = Category.query.all() app.config.get('UPLOAD_FOLDER', os.path.join('static', 
 'uploads')) 
    distinct_brands =                 if not os.path.exists(upload_folder): 
db.session.query(Product.brand).filter(Product.is_active ==                     os.makedirs(upload_folder) 
True, Product.brand != None,                 file.save(os.path.join(upload_folder, 
                                                             filename)) 
Product.brand != '').distinct().all()                 image_path = 
    distinct_colors = f"/static/uploads/{filename}" 
db.session.query(Product.color).filter(Product.is_active ==  
True, Product.color != None,         new_product = Product( 
                                                                         name=name, price=price, 
Product.color != '').distinct().all() category_id=category_id, 
             brand=brand, color=color, 
    brands = [b[0] for b in distinct_brands] description=description, 
    colors = [c[0] for c in distinct_colors]             image=image_path, is_active=True 
         ) 
    wishlist_ids = session.get('wishlist', [])         db.session.add(new_product) 
         db.session.commit() 
    return render_template(         return redirect(url_for('index')) 
        'index.html',  
        products=products,     return render_template('add_product.html', 
        categories=categories, categories=categories) 
        brands=brands,  
        colors=colors, @app.route('/edit_product/<int:product_id>', 
        wishlist_ids=wishlist_ids methods=['GET', 'POST']) 
    ) def edit_product(product_id): 
     if session.get('username') != 'admin': 
@app.route('/product/<int:product_id>')         abort(403) 
def product_detail(product_id):  
    product =     product = 
Product.query.get_or_404(product_id) Product.query.get_or_404(product_id) 
    if not product.is_active:     categories = Category.query.all() 
        abort(404)  
    wishlist_ids = session.get('wishlist', [])     if request.method == 'POST': 
    return render_template('product_detail.html',         product.name = request.form.get('name') 
product=product, wishlist_ids=wishlist_ids)         product.price = float(request.form.get('price', 
 0)) 
@app.route('/add_product', methods=['GET',         product.category_id = 
'POST']) int(request.form.get('category_id')) 
def add_product():         product.brand = request.form.get('brand', 
    if session.get('username') != 'admin': '').strip() 
        abort(403)         product.color = request.form.get('color', 
 '').strip() 
    categories = Category.query.all()         product.description = 
 request.form.get('description', '') 
    if request.method == 'POST':  
        name = request.form.get('name')         if 'image' in request.files: 
        price = float(request.form.get('price', 0))             file = request.files['image'] 
        category_id =             if file and file.filename != '': 
int(request.form.get('category_id'))                 filename = 
        brand = request.form.get('brand', '').strip() secure_filename(file.filename) 
        color = request.form.get('color', '').strip()                 upload_folder = 
        description = request.form.get('description', app.config.get('UPLOAD_FOLDER', os.path.join('static', 
'') 'uploads')) 
                 if not os.path.exists(upload_folder): 
        image_path = None                     os.makedirs(upload_folder) 
        if 'image' in request.files: 
88 
 
                file.save(os.path.join(upload_folder,  
filename))         if 
                product.image = User.query.filter_by(username=username).first(): 
f"/static/uploads/{filename}"             return render_template('register.html', 
 error="Користувач з таким нікнеймом вже існує.") 
        db.session.commit()         if email and 
        return redirect(url_for('index')) User.query.filter_by(email=email).first(): 
             return render_template('register.html', 
    return render_template('edit_product.html', error="Користувач з таким Email вже існує.") 
product=product, categories=categories)  
         hashed_password = 
@app.route('/delete_product/<int:product_id>', generate_password_hash(password) 
methods=['POST'])         new_user = User(username=username, 
def delete_product(product_id): email=email if email else None, 
    if session.get('username') != 'admin': password=hashed_password) 
        abort(403)         db.session.add(new_user) 
         db.session.commit() 
    product =  
Product.query.get_or_404(product_id)         session['user_id'] = new_user.id 
         session['username'] = new_user.username 
    product.is_active = False         flash("Реєстрація успішна! Ласкаво 
 просимо!  ", "success") 
            return redirect(url_for('index')) 
CartItem.query.filter_by(product_id=product.id).delete()  
     return render_template('register.html') 
    db.session.commit()  
    return redirect(url_for('index')) @app.route('/cart') 
 def cart(): 
@app.route('/login', methods=['GET', 'POST'])     user_id = session.get('user_id') 
def login():     if not user_id: 
    if request.method == 'POST':         flash("Будь ласка, увійдіть, щоб 
        login_input = request.form.get('login_input') переглянути кошик.", "warning") 
        password = request.form.get('password')         return redirect(url_for('login')) 
  
        user = User.query.filter((User.username ==     cart_items = 
login_input) | (User.email == login_input)).first() CartItem.query.filter_by(user_id=user_id).all() 
     total_price = sum(item.product.price * 
        if user and item.quantity for item in cart_items if 
check_password_hash(user.password, password): item.product.is_active) 
            session['user_id'] = user.id  
            session['username'] = user.username     return render_template('cart.html', 
            flash("Успішний вхід!  ", "success") cart_items=cart_items, total_price=total_price) 
            return redirect(url_for('index'))  
        else: @app.route('/add_to_cart/<int:product_id>', 
            flash("Неправильне ім'я methods=['POST']) 
користувача/email або пароль.", "danger") def add_to_cart(product_id): 
     user_id = session.get('user_id') 
    return render_template('login.html')     if not user_id: 
         flash("Спочатку увійдіть в акаунт.", 
@app.route('/logout') "warning") 
def logout():         return redirect(url_for('login')) 
    session.clear()  
    flash("Ви вийшли з акаунту.", "info")     product = 
    return redirect(url_for('index')) Product.query.get_or_404(product_id) 
     if not product.is_active: 
@app.route('/register', methods=['GET', 'POST'])         abort(400) 
def register():  
    if request.method == 'POST':     cart_item = 
        username = CartItem.query.filter_by(user_id=user_id, 
request.form.get('username').strip() product_id=product_id).first() 
        email = request.form.get('email').strip()  
        password = request.form.get('password')     if cart_item: 
89 
 
        cart_item.quantity += 1     total_price = sum(item.product.price * 
    else: item.quantity for item in cart_items) 
        cart_item = CartItem(user_id=user_id,  
product_id=product_id, quantity=1)     if request.method == 'POST': 
        db.session.add(cart_item)         name = request.form.get('name') 
         phone = request.form.get('phone') 
    db.session.commit()         email = request.form.get('email') 
    return redirect(request.referrer or         address = request.form.get('address') 
url_for('index'))         delivery_method = 
 request.form.get('delivery_method') 
@app.route('/update_cart/<int:item_id>/<string:a         payment_method = 
ction>', methods=['POST']) request.form.get('payment_method') 
def update_cart(item_id, action):  
    user_id = session.get('user_id')         order = Order( 
    if not user_id:             user_id=user_id, total_price=total_price, 
        abort(401) status='new', 
             name=name, phone=phone, email=email if 
    item = CartItem.query.get_or_404(item_id) email else None, 
    if item.user_id != user_id:             address=address, 
        abort(403) delivery_method=delivery_method, 
 payment_method=payment_method 
    if action == 'increase':         ) 
        item.quantity += 1         db.session.add(order) 
    elif action == 'decrease':         db.session.flush() 
        if item.quantity > 1:  
            item.quantity -= 1         for item in cart_items: 
        else:             order_item = OrderItem( 
            db.session.delete(item)                 order_id=order.id, 
 product_id=item.product_id, 
    db.session.commit()                 quantity=item.quantity, 
    return redirect(url_for('cart')) price_at_purchase=item.product.price 
             ) 
@app.route('/delete_cart_item/<int:item_id>')             db.session.add(order_item) 
def delete_cart_item(item_id):             db.session.delete(item)  # Очищуємо 
    user_id = session.get('user_id') кошик 
    if not user_id:  
        abort(401)         db.session.commit() 
         flash("Замовлення успішно оформлено! 
    item = CartItem.query.get_or_404(item_id)  ", "success") 
    if item.user_id != user_id:         return redirect(url_for('my_orders')) 
        abort(403)  
     return render_template('checkout.html', 
    db.session.delete(item) cart_items=cart_items, total_price=total_price) 
    db.session.commit()  
    return redirect(url_for('cart')) @app.route('/my_orders') 
 def my_orders(): 
@app.route('/checkout', methods=['GET', 'POST'])     user_id = session.get('user_id') 
def checkout():     if not user_id: 
    user_id = session.get('user_id')         return redirect(url_for('login')) 
    if not user_id:  
        return redirect(url_for('login'))     orders = 
 Order.query.filter_by(user_id=user_id).order_by(Order.cre
    cart_items = ated_at.desc()).all() 
CartItem.query.filter_by(user_id=user_id).all()     return render_template('my_orders.html', 
    cart_items = [item for item in cart_items if orders=orders) 
item.product.is_active]  
 @app.route('/admin/orders') 
    if not cart_items: def admin_orders(): 
        flash("Ваш кошик порожній.", "warning")     if session.get('username') != 'admin': 
        return redirect(url_for('index'))         abort(403) 
     orders = 
Order.query.order_by(Order.created_at.desc()).all() 
90 
 
    return render_template('admin_orders.html',  
orders=orders)     user = User.query.get_or_404(user_id) 
  
@app.route('/admin/update_order_status/<int:ord     if user.username == 'admin': 
er_id>', methods=['POST'])         flash("Ви не можете редагувати головного 
def update_order_status(order_id): адміністратора!", "danger") 
    if session.get('username') != 'admin':         return redirect(url_for('admin_users')) 
        abort(403)  
     if request.method == 'POST': 
    order = Order.query.get_or_404(order_id)         new_username = 
    new_status = request.form.get('status') request.form.get('username').strip() 
    if new_status:         new_name = request.form.get('name').strip() 
        order.status = new_status # <--- Отримуємо ім'я з форми 
        db.session.commit()         new_email = request.form.get('email').strip() 
        flash(f"Статус замовлення #{order.id}         new_password = 
оновлено.", "success") request.form.get('password') 
  
    return redirect(url_for('admin_orders'))         if new_username != user.username and 
 User.query.filter_by(username=new_username).first(): 
@app.route('/admin/users')             flash("Користувач з таким нікнеймом 
def admin_users(): вже існує.", "danger") 
    if session.get('username') != 'admin':             return redirect(url_for('edit_user', 
        abort(403) user_id=user.id)) 
  
    users =         if new_email and new_email != user.email 
User.query.order_by(User.id.desc()).all() and User.query.filter_by(email=new_email).first(): 
    return render_template('admin_users.html',             flash("Користувач з таким Email вже 
users=users) існує.", "danger") 
             return redirect(url_for('edit_user', 
@app.route('/admin/delete_user/<int:user_id>', user_id=user.id)) 
methods=['POST'])  
def delete_user(user_id):         user.username = new_username 
    if session.get('username') != 'admin':         user.name = new_name if new_name else 
        abort(403) None # <--- Зберігаємо ім'я 
         user.email = new_email if new_email else 
    user = User.query.get_or_404(user_id) None 
  
    if user.username == 'admin':         if new_password: 
        flash("Ви не можете видалити головного             user.password = 
адміністратора!", "danger") generate_password_hash(new_password) 
        return redirect(url_for('admin_users'))  
         db.session.commit() 
    try:         flash(f"Дані користувача {user.username} 
        успешно оновлено.", "success") 
CartItem.query.filter_by(user_id=user.id).delete()         return redirect(url_for('admin_users')) 
  
        db.session.delete(user)     return render_template('edit_user.html', 
        db.session.commit() user=user) 
        flash(f"Користувача {user.username} @app.route('/receipt/<int:order_id>') 
успішно видалено.", "success") def print_receipt(order_id): 
    except Exception as e:     user_id = session.get('user_id') 
        db.session.rollback()     is_admin = session.get('username') == 'admin' 
        flash("Не вдалося видалити користувача.  
Можливо, у нього є історія замовлень.", "danger")     order = Order.query.get_or_404(order_id) 
  
    return redirect(url_for('admin_users'))     if not is_admin and order.user_id != user_id: 
         abort(403)  # Помилка доступу, якщо це 
@app.route('/admin/edit_user/<int:user_id>', чужий чек 
methods=['GET', 'POST'])  
def edit_user(user_id):     return render_template('print_receipt.html', 
    if session.get('username') != 'admin': order=order) 
        abort(403)  
91 
 
@app.route('/toggle_wishlist/<int:product_id>',         /* Щоб футер завжди був притиснутий до 
methods=['POST']) низу екрана */ 
def toggle_wishlist(product_id):         html, body { height: 100%; } 
    wishlist = session.get('wishlist', [])         body { display: flex; flex-direction: column; } 
         main { flex: 1; } 
    if product_id in wishlist:     </style> 
        wishlist.remove(product_id) </head> 
        in_wishlist = False <body class="bg-light"> 
    else:  
        wishlist.append(product_id) <nav class="navbar navbar-expand-lg navbar-dark 
        in_wishlist = True bg-dark shadow-sm py-3"> 
     <div class="container"> 
    session['wishlist'] = wishlist  
    session.modified = True         <a class="navbar-brand fw-bold fs-4" 
 href="{{ url_for('index') }}"> 
    if request.headers.get('X-Requested-With') ==               PingPong Shop 
'XMLHttpRequest':         </a> 
        return {'in_wishlist': in_wishlist}  
         <button class="navbar-toggler" 
    return redirect(request.referrer or type="button" data-bs-toggle="collapse" data-bs-
url_for('index')) target="#navbarNav"> 
             <span class="navbar-toggler-
@app.route('/wishlist') icon"></span> 
def wishlist_page():         </button> 
    wishlist_ids = session.get('wishlist', [])  
    products =         <div class="collapse navbar-collapse" 
Product.query.filter(Product.id.in_(wishlist_ids), id="navbarNav"> 
Product.is_active == True).all()  
    wishlist_items = [WishlistItem(p) for p in             <ul class="navbar-nav me-auto mb-2 mb-
products] lg-0"> 
                 <li class="nav-item"> 
    return render_template('wishlist.html',                     <a class="nav-link" href="{{ 
wishlist_items=wishlist_items) url_for('index') }}">Каталог</a> 
                 </li> 
if __name__ == '__main__':  
    app.run(debug=True)                 {% if session.get('username') == 'admin' 
 %} 
===================================                     <li class="nav-item"> 
===============                         <a class="nav-link text-warning 
 fw-bold" href="{{ url_for('add_product') }}"> 
"base.html"                             <i class="bi bi-plus-circle"></i> 
<!DOCTYPE html> Додати товар 
<html lang="uk">                         </a> 
<head>                     </li> 
    <meta charset="UTF-8">                     <li class="nav-item"> 
    <title>{% block title %}PingPong Shop{%                         <a class="nav-link text-warning 
endblock %}</title> fw-bold" href="{{ url_for('admin_orders') }}"> 
                             <i class="bi bi-box-seam"></i> 
    <meta name="viewport" Всі замовлення 
content="width=device-width, initial-scale=1">                         </a> 
                     </li> 
    <link                     <li class="nav-item"> 
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css                         <a class="nav-link text-warning 
/bootstrap.min.css" rel="stylesheet"> fw-bold" href="{{ url_for('admin_users') }}"> 
    <link                             <i class="bi bi-people"></i> 
href="https://cdn.jsdelivr.net/npm/bootstrap- Користувачі 
[email protected]/font/bootstrap-icons.css" rel="stylesheet">                         </a> 
    <link rel="stylesheet" href="{{ url_for('static',                     </li> 
filename='css/style.css') }}">                 {% endif %} 
             </ul> 
    <style>  
92 
 
            <ul class="navbar-nav align-items-center                 <p class="small text-muted mb-0">Ваш 
gap-3"> найкращий магазин інвентарю для настільного 
                <li class="nav-item"> тенісу.</p> 
                   <a class="nav-link d-flex align-items-             </div> 
center" href="{{ url_for('wishlist_page') }}">             <div class="col-md-6 text-md-end"> 
                        <i class="bi bi-heart me-1"></i>                 <ul class="list-unstyled small text-
Обране muted"> 
                    </a>                     <li>Email: 
                </li> [email protected]</li> 
                <li class="nav-item">                     <li>Телефон: +380 93 123 45 67</li> 
                    <a class="nav-link d-flex align-items-                 </ul> 
center" href="{{ url_for('cart') }}">             </div> 
                        <i class="bi bi-cart me-1"></i>         </div> 
Кошик         <div class="text-center small text-muted mt-
                    </a> 3"> 
                </li>             © {{ current_year if current_year else 2026 
 }} PingPong Shop. Всі права захищено. 
                {% if 'user_id' in session %}         </div> 
                    <li class="nav-item">     </div> 
                        <a class="nav-link" href="{{ </footer> 
url_for('my_orders') }}">Мої замовлення</a>  
                    </li> <script 
                    <li class="nav-item d-none d-lg- src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/b
block"> ootstrap.bundle.min.js"></script> 
                        <span class="nav-link text-light  
border-start ps-3 ms-2"> <script> 
                            Привіт, <strong>{{ document.addEventListener("DOMContentLoade
session.username }}</strong>! d", function () { 
                        </span>     document.querySelectorAll(".wishlist-
                    </li> btn").forEach(btn => { 
                    <li class="nav-item">         btn.addEventListener("click", async function 
                        <a class="btn btn-outline-light btn- (e) { 
sm ms-lg-2 rounded-3" href="{{ url_for('logout')             e.preventDefault(); 
}}">Вийти</a>             const productId = this.dataset.productId; 
                    </li>  
                {% else %}             const response = await 
                    <li class="nav-item"> fetch(`/toggle_wishlist/${productId}`, { 
                        <a class="nav-link" href="{{                 method: "POST", 
url_for('login') }}">Увійти</a>                 headers: { 
                    </li>                     "X-Requested-With": 
                    <li class="nav-item"> "XMLHttpRequest" 
                        <a class="btn btn-primary btn-sm                 } 
ms-lg-2 rounded-3 fw-bold" href="{{ url_for('register')             }); 
}}">Реєстрація</a>  
                    </li>             const data = await response.json(); 
                {% endif %}             const icon = this.querySelector("i"); 
            </ul>  
             if (data.in_wishlist) { 
        </div>                 icon.classList.remove("bi-heart"); 
    </div>                 icon.classList.add("bi-heart-fill", "text-
</nav> danger"); 
             } else { 
<main>                 icon.classList.remove("bi-heart-fill", 
    {% block content %}{% endblock %} "text-danger"); 
</main>                 icon.classList.add("bi-heart"); 
             } 
<footer class="bg-dark text-white py-4 mt-auto">         }); 
    <div class="container">     }); 
        <div class="row"> }); 
            <div class="col-md-6 mb-3"> </script> 
                <h5 class="fw-bold">PingPong  
Shop</h5> </body> 
93 
 
</html>  
                     </div> 
===================================  
===============                     <div class="item-remove" 
 style="margin-left: 20px;"> 
"cart.html"                         <a href="{{ 
{% extends 'base.html' %} url_for('delete_cart_item', item_id=item.id) }}" class="btn 
 btn-link text-danger p-2 text-decoration-none fw-bold" 
{% block title %}Кошик - PingPong Shop{% onclick="return confirm('Ви впевнені, що хочете 
endblock %} видалити цей товар з кошика?')">  </a> 
                     </div> 
{% block content %}                 </div> 
<div class="container py-4">             {% endfor %} 
    <h2 class="fw-bold mb-4">   Ваш         </div> 
кошик</h2>  
    <hr><br>         <div id="cart-total" style="text-align: right; 
 font-size: 24px; margin-top: 25px;" class="fw-bold"> 
    {% if cart_items %}             Загальна сума: <span class="text-
        <div class="cart-items-list d-flex flex-column primary">{{ total_price }} грн</span> 
gap-3">         </div> 
            {% for item in cart_items %}  
                <div class="cart-item d-flex align-items-         <div style="text-align: right; margin-top: 
center bg-white p-3 rounded-4 shadow-sm" data-price="{{ 20px;"> 
item.product.price }}" data-quantity="{{ item.quantity             <a href="{{ url_for('checkout') }}" 
}}"> class="btn btn-success btn-lg px-4 fw-bold rounded-3 
 shadow-sm"> 
                    <img src="{{ item.product.image_url                   Оформити замовлення 
}}" alt="{{ item.product.name }}" style="width: 100px;             </a> 
height: 100px; object-fit: cover; border-radius: 8px;">         </div> 
  
                    <div class="item-details" style="flex-     {% else %} 
grow: 1; margin-left: 20px;">         <div style="text-align: center; padding: 
                        <h4 class="fw-bold mb-1">{{ 50px;"> 
item.product.name }}</h4>             <i class="bi bi-cart-x display-1 text-
                        <p class="text-muted small mb- muted"></i> 
2">Категорія: {{ item.product.category.name }}</p>             <h3 class="text-muted mt-3">Ваш кошик 
                        <p class="mb-0">Ціна: порожній  </h3> 
<strong>{{ item.product.price }} грн</strong></p>             <p class="text-muted">Перейдіть до 
                    </div> каталогу товарів, щоб обрати інвентар.</p> 
             <a href="{{ url_for('index') }}" class="btn 
                    <div class="item-actions d-flex align- btn-primary mt-3 px-4 fw-bold rounded-3 shadow-sm">До 
items-center gap-2"> каталогу</a> 
         </div> 
                        <form method="POST" action="{{     {% endif %} 
url_for('update_cart', item_id=item.id, action='decrease')  
}}" style="margin: 0;"> </div> 
                            <button type="submit" {% endblock %} 
class="btn btn-sm btn-outline-secondary cart-  
btn">  </button> ===================================
                        </form> =============== 
  
                        <span class="fw-bold fs-5 px- "checkout.html" 
2">{{ item.quantity }}</span> {% extends "base.html" %} 
  
                        <form method="POST" action="{{ {% block title %}Оформлення замовлення — 
url_for('update_cart', item_id=item.id, action='increase') PingPong Shop{% endblock %} 
}}" style="margin: 0;">  
                            <button type="submit" {% block content %} 
class="btn btn-sm btn-outline-secondary cart- <div class="container py-4"> 
btn">  </button>  
                        </form>     <h2 class="fw-bold mb-4"> 
94 
 
        <i class="bi bi-credit-card me-2 text-                                 <option value="Нова 
primary"></i>Оформлення замовлення Пошта">Нова Пошта (у відділення)</option> 
    </h2>                                 <option 
 value="Кур'єр">Адресна доставка кур'єром</option> 
    <form method="POST" action="{{                                 <option 
url_for('checkout') }}"> value="Самовивіз">Самовивіз з магазину</option> 
        <div class="row g-4">                             </select> 
                         </div> 
            <div class="col-lg-7">  
                         <div> 
                <div class="card border-0 shadow-sm                             <label class="form-label small 
rounded-4 mb-4"> text-muted">Адреса (Місто, відділення або 
                    <div class="card-body p-4"> вулиця)</label> 
                        <h5 class="fw-bold mb-3">                             <input type="text" 
                            <i class="bi bi-person me-2 text-                                    name="address" 
secondary"></i>Контактні дані                                    class="form-control 
                        </h5> rounded-3" 
                                    placeholder="Київ, 
                        <div class="row g-3"> Відділення №1 або вул. Хрещатик 1, кв. 5" 
                            <div class="col-md-6">                                    required> 
                                <label class="form-label small                         </div> 
text-muted">Ім'я одержувача</label>                     </div> 
                                <input type="text"                 </div> 
name="name" class="form-control rounded-3"  
placeholder="Іван Іванов" required>                 <div class="card border-0 shadow-sm 
                            </div> rounded-4"> 
                     <div class="card-body p-4"> 
                            <div class="col-md-6">                         <h5 class="fw-bold mb-3"> 
                                <label class="form-label small                             <i class="bi bi-wallet2 me-2 
text-muted">Номер телефону</label> text-secondary"></i>Спосіб оплати 
                                <input type="tel"                         </h5> 
name="phone" class="form-control rounded-3"  
placeholder="+380" required>                         <div class="form-check p-3 border 
                            </div> rounded-3 mb-2 d-flex align-items-center"> 
                             <input class="form-check-input 
                            <div class="col-12"> ms-0 me-3" type="radio" name="payment_method" 
                                <label class="form-label small id="pay_card" value="card" required> 
text-muted">Електронна пошта (Email)</label>                             <label class="form-check-label 
                                <input type="email" w-100 stretched-link" for="pay_card"> 
name="email" class="form-control rounded-3"                                 <strong>Карткою 
placeholder="[email protected]" required> онлайн</strong> <span class="text-muted small d-
                            </div> block">Оплата через LiqPay / Visa / MasterCard</span> 
                        </div>                             </label> 
                    </div>                         </div> 
                </div>  
                         <div class="form-check p-3 border 
                <div class="card border-0 shadow-sm rounded-3 d-flex align-items-center"> 
rounded-4 mb-4">                             <input class="form-check-input 
                    <div class="card-body p-4"> ms-0 me-3" type="radio" name="payment_method" 
                        <h5 class="fw-bold mb-3"> id="pay_cash" value="cash"> 
                            <i class="bi bi-truck me-2 text-                             <label class="form-check-label 
secondary"></i>Доставка w-100 stretched-link" for="pay_cash"> 
                        </h5>                                 <strong>Готівкою при 
 отриманні</strong> <span class="text-muted small d-
                        <div class="mb-3"> block">Оплата у відділенні або кур'єру</span> 
                            <label class="form-label small                             </label> 
text-muted">Спосіб доставки</label>                         </div> 
                            <select                     </div> 
name="delivery_method" class="form-select rounded-3"                 </div> 
required>  
                                <option value="">Оберіть             </div> 
спосіб доставки...</option>  
95 
 
            <div class="col-lg-5"> {% endblock %} 
                <div class="card border-0 shadow-sm  
rounded-4 sticky-top" style="top: 20px;"> ===================================
                    <div class="card-body p-4"> =============== 
  
                        <h5 class="fw-bold mb-3">Ваше "config.py" 
замовлення</h5> import os 
  
                        <div class="order-items-summary BASE_DIR = 
mb-3" style="max-height: 240px; overflow-y: auto;"> os.path.abspath(os.path.dirname(__file__)) 
                            {% if cart_items %}  
                                {% for item in cart_items %} class Config: 
                                <div class="d-flex justify-     SECRET_KEY = 'super-secret-key-change-this' 
content-between align-items-center small mb-2 pb-2  
border-bottom">     SQLALCHEMY_DATABASE_URI = 
                                    <span class="text-truncate 'sqlite:///' + os.path.join(BASE_DIR, 'shop.db') 
me-2" style="max-width: 70%;">  
                                        {{ item.product.name }}     SQLALCHEMY_TRACK_MODIFICATIONS 
<strong class="text-muted">× {{ item.quantity }}</strong> = False 
                                    </span>  
                                    <span class="fw-semibold     UPLOAD_FOLDER = 
text-nowrap">{{ item.product.price * item.quantity }} os.path.join(BASE_DIR, 'static', 'uploads') 
₴</span>  
                                </div>     MAX_CONTENT_LENGTH = 5 * 1024 * 1024 
                                {% endfor %}  
                            {% else %}     RECEIPT_FOLDER = 
                                <p class="text-muted os.path.join(BASE_DIR, 'static', 'receipts') 
small">Товари завантажуються...</p>  
                            {% endif %}     PAYMENT_METHODS = [ 
                        </div>         ('cash', 'Оплата при отриманні'), 
         ('card', 'Карткою онлайн') 
                        <div class="d-flex justify-content-     ] 
between align-items-center fw-bold fs-5 mb-4">  
                            <span>Разом до     ORDER_STATUSES = [ 
сплати:</span>         ('new', 'Нове'), 
                            <span class="text-primary">{{         ('processing', 'В обробці'), 
total_price if total_price else 0 }} ₴</span>         ('shipped', 'Відправлено'), 
                        </div>         ('done', 'Завершено') 
     ] 
                        <button type="submit" class="btn  
btn-success btn-lg w-100 py-3 fw-bold rounded-3 shadow- ===================================
sm"> =============== 
                            <i class="bi bi-check-circle me-  
2"></i>Підтвердити замовлення "create_db.py" 
                        </button> from app import app 
 from models import db, Category, Product, User, 
                        <div class="text-center mt-3"> Order, OrderItem 
                            <a href="{{ url_for('cart') }}" from datetime import datetime, timezone, 
class="text-decoration-none small text-muted"> timedelta 
                                <i class="bi bi-arrow-left me- from werkzeug.security import 
1"></i>Повернутися до кошика generate_password_hash 
                            </a>  
                        </div> def get_image(text, bg_color="2c3e50"): 
     return 
                    </div> f"https://dummyimage.com/400x400/{bg_color}/fff.png&t
                </div> ext={text}" 
            </div>  
 with app.app_context(): 
        </div>     print("Очищення та створення таблиць бази 
    </form> даних...") 
     db.drop_all() 
</div>     db.create_all() 
96 
 
         ("Ракетка Stiga Pro Carbon", "Професійна 
    print("Створення користувачів...") ракетка з вуглецевими шарами для максимальної 
 швидкості.", 3400.0, 
    admin = User(          "/static/uploads/3.png", cat_rackets.id, 
        username="admin", "Stiga", "Чорний"), 
        email="[email protected]",  
                ("Накладка Yasaka Rakza 7", "Легендарна 
password=generate_password_hash("admin_secure_passw накладка для атакуючого стилю.", 1650.0, 
ord"),          "/static/uploads/6.png", cat_rubbers.id, 
        is_admin=True "Yasaka", "Червоний"), 
    )         ("Накладка Butterfly Tenergy 05", "Вибір 
 багатьох світових професіоналів.", 2900.0, 
    user1 = User( "/static/uploads/7.png", cat_rubbers.id, "Butterfly", 
        username="ivan_pingpong", "Чорний"), 
        email="[email protected]",  
                ("Основа DHS Hurricane Long 5", 
password=generate_password_hash("password123"), "Професійна основа, якою грає Ма Лонг.", 6200.0, 
        is_admin=False          get_image("Hurricane+5", "f1c40f"), 
    ) cat_rubbers.id, "DHS", "Дерево"), 
    user2 = User(         ("Основа Butterfly Viscaria", "Класична 
        username="olena_tennis", основа з прекрасним відчуттям м'яча.", 5800.0, 
        email="[email protected]",          get_image("Viscaria", "2980b9"), 
        cat_rubbers.id, "Butterfly", "Блакитний"), 
password=generate_password_hash("olena2026"),  
        is_admin=False         ("М'ячі Nittaku Premium 3*** (3 шт)", 
    ) "Офіційні м'ячі для змагань найвищого рівня.", 450.0, 
    user3 = User(          get_image("Nittaku+Premium", "95a5a6"), 
        username="andriy_go", cat_balls.id, "Nittaku", "Білий"), 
        email="[email protected]",         ("М'ячі Donic Coach P40+ (120 шт)", 
        "Великий набір для тренувань у клубах.", 2800.0, 
password=generate_password_hash("andriy321"),          get_image("Donic+120", "e67e22"), 
        is_admin=False cat_balls.id, "Donic", "Білий"), 
    )  
         ("Стіл Donic Waldner Classic 25", 
    db.session.add_all([admin, user1, user2, user3]) "Професійний турнірний стіл ITTF.", 32000.0, 
    db.session.commit()          get_image("Waldner+Table", "2980b9"), 
 cat_tables.id, "Donic", "Синій"), 
    print("Створення категорій...")         ("Стіл Cornilleau 500X Outdoor", 
    cat_rackets = Category(name="Ракетки") "Всепогодний стіл преміум-класу для вулиці.", 41000.0, 
    cat_balls = Category(name="М'ячі")          get_image("Cornilleau+500X", "7f8c8d"), 
    cat_tables = Category(name="Столи") cat_tables.id, "Cornilleau", "Сірий"), 
    cat_accessories =         ("Сітка Butterfly Europa", "Міцна 
Category(name="Аксесуари") бавовняна сітка з надійним кріпленням.", 1800.0, 
    cat_rubbers = Category(name="Накладки та          get_image("Net+Europa", "34495e"), 
Основи") cat_tables.id, "Butterfly", "Синій"), 
  
    db.session.add_all([cat_rackets, cat_balls,         ("Чохол Butterfly Logo Double", 
cat_tables, cat_accessories, cat_rubbers]) "Подвійний жорсткий чохол для двох ракеток.", 950.0, 
    db.session.commit()          get_image("Case+Butterfly", "c0392b"), 
 cat_accessories.id, "Butterfly", "Червоний"), 
    print("Наповнення каталогу товарів...")         ("Очисник для накладок Donic Combi 
    products_data = [ (90мл)", "Спрей для очищення та догляду за гумою.", 
        ("Ракетка Butterfly Timo Boll Gold", 320.0, 
"Готова ракета для гравців середнього рівня.", 1450.0,          get_image("Donic+Cleaner", "16a085"), 
         "/static/uploads/1.png", cat_rackets.id, cat_accessories.id, "Donic", "Прозорий") 
"Butterfly", "Червоний"),     ] 
  
        ("Ракетка Donic Waldner 700", "Чудовий     db_products = [] 
контроль та обертання.", 980.0,     for name, desc, price, img, cat_id, brand, color 
         "/static/uploads/2.png", cat_rackets.id, in products_data: 
"Donic", "Чорний"),         prod = Product( 
 
97 
 
            name=name, description=desc,  
price=price, image=img, {% block title %}Редагувати товар — PingPong 
            category_id=cat_id, brand=brand, Shop{% endblock %} 
color=color, is_active=True  
        ) {% block content %} 
        db.session.add(prod) <div class="container py-5"> 
        db_products.append(prod)     <div class="row justify-content-center"> 
         <div class="col-lg-8 col-xl-7"> 
    db.session.flush()  # Фіксуємо ID товарів у  
пам'яті для створення замовлень             <div class="mb-4"> 
                 <a href="{{ url_for('index') }}" 
    print("Генерація тестових замовлень...") class="text-decoration-none text-muted small d-inline-flex 
    now = datetime.now(timezone.utc) align-items-center"> 
                     <i class="bi bi-arrow-left me-2"></i> 
    order1 = Order( Назад до каталогу 
        user_id=user1.id, name="Іван Іванов",                 </a> 
phone="+380931112233",             </div> 
email="[email protected]",  
        address="Київ, Відділення Нової Пошти             <div class="card shadow-lg border-0 
№45", delivery_method="Нова Пошта", rounded-4 overflow-hidden"> 
payment_method="Оплата при отриманні",                 <div class="card-header bg-dark text-
        status="new", total_price=1930.0, white py-4 px-4"> 
created_at=now - timedelta(hours=2)                     <h4 class="mb-0 fw-bold"> 
    )                         <i class="bi bi-pencil-square me-2 
    db.session.add(order1) text-warning"></i>Редагувати товар 
    db.session.flush()                     </h4> 
                 </div> 
    db.session.add_all([  
        OrderItem(order_id=order1.id,                 <div class="card-body p-4 bg-white"> 
product_id=db_products[1].id, quantity=1,                     <form action="{{ 
price_at_purchase=980.0), url_for('edit_product', product_id=product.id) }}" 
        OrderItem(order_id=order1.id, method="POST" enctype="multipart/form-data"> 
product_id=db_products[12].id, quantity=1,                         <div class="row g-4"> 
price_at_purchase=950.0)  
    ])                             <div class="col-12"> 
                                 <label class="form-label fw-
    order2 = Order( semibold">Назва товару</label> 
        user_id=user2.id, name="Олена Ткаченко",                                 <input type="text" 
phone="+380998887766", email="[email protected]", name="name" class="form-control rounded-3" value="{{ 
        address="Львів, вул. Стрийська, 10, кв 5", product.name }}" required> 
delivery_method="Кур'єр", payment_method="Карткою                             </div> 
онлайн",  
        status="processing", total_price=1450.0,                             <div class="col-md-6"> 
created_at=now - timedelta(days=1)                                 <label class="form-label fw-
    ) semibold">Ціна (грн)</label> 
    db.session.add(order2)                                 <input type="number" 
    db.session.flush() name="price" step="0.01" class="form-control rounded-3" 
 value="{{ product.price }}" required> 
    db.session.add_all([                             </div> 
        OrderItem(order_id=order2.id,  
product_id=db_products[6].id, quantity=1,                             <div class="col-md-6"> 
price_at_purchase=1450.0)                                 <label class="form-label fw-
    ]) semibold">Категорія</label> 
                                 <select name="category_id" 
    db.session.commit() class="form-select rounded-3" required> 
    print("Базу даних успішно ініціалізовано!")                                     {% for category in 
 categories %} 
===================================                                         <option value="{{ 
=============== category.id }}" {% if category.id == product.category_id 
 %}selected{% endif %}> 
"edit_product.html"                                             {{ category.name }} 
{% extends "base.html" %}                                         </option> 
98 
 
                                    {% endfor %} </div> 
                                </select> {% endblock %} 
                            </div>  
 ===================================
                            <div class="col-md-6"> =============== 
                                <label class="form-label fw-  
semibold">Бренд</label> "edit_user.html" 
                                <input type="text" {% extends "base.html" %} 
name="brand" class="form-control rounded-3" value="{{  
product.brand or '' }}" placeholder="Наприклад: Donic"> {% block title %}Редагування користувача — 
                            </div> Адмінка{% endblock %} 
  
                            <div class="col-md-6"> {% block content %} 
                                <label class="form-label fw- <div class="container py-5"> 
semibold">Колір</label>     <div class="row justify-content-center"> 
                                <input type="text"         <div class="col-md-6"> 
name="color" class="form-control rounded-3" value="{{             <div class="card border-0 shadow-sm 
product.color or '' }}" placeholder="Наприклад: rounded-4"> 
Червоний">                 <div class="card-body p-4 p-md-5"> 
                            </div>  
                     <div class="d-flex align-items-center 
                            <div class="col-12"> mb-4"> 
                                <label class="form-label fw-                         <a href="{{ url_for('admin_users') 
semibold">Нове зображення (необов'язково)</label> }}" class="btn btn-outline-secondary btn-sm me-3"> 
                                <input type="file"                             <i class="bi bi-arrow-left"></i> 
name="image" class="form-control rounded-3" Назад 
accept="image/*">                         </a> 
                                <div class="form-text text-                         <h3 class="fw-bold m-
muted small"> 0">Редагування користувача</h3> 
                                    Залиште порожнім, якщо                     </div> 
не хочете змінювати поточне фото.  
                                </div>                     {% with messages = 
                            </div> get_flashed_messages(with_categories=true) %} 
                         {% if messages %} 
                            <div class="col-12">                             {% for category, message in 
                                <label class="form-label fw- messages %} 
semibold">Опис товару</label>                                 <div class="alert alert-{{ 
                                <textarea name="description" category }} rounded-3">{{ message }}</div> 
class="form-control rounded-3" rows="4">{{                             {% endfor %} 
product.description }}</textarea>                         {% endif %} 
                            </div>                     {% endwith %} 
  
                            <div class="col-12 d-flex justify-                     <form method="POST"> 
content-end gap-2 mt-4">                         <div class="mb-3"> 
                                <a href="{{ url_for('index')                             <label for="username" 
}}" class="btn btn-light border rounded-3 px-4"> class="form-label fw-bold">Нікнейм (для входу)</label> 
                                    Скасувати                             <input type="text" class="form-
                                </a> control" id="username" name="username" value="{{ 
                                <button type="submit" user.username }}" required> 
class="btn btn-warning rounded-3 px-4 fw-bold">                         </div> 
                                    <i class="bi bi-save me-  
1"></i> Оновити товар                         <div class="mb-3"> 
                                </button>                             <label for="name" class="form-
                            </div> label fw-bold">Ім'я користувача</label> 
                             <input type="text" class="form-
                        </div> control" id="name" name="name" value="{{ user.name or 
                    </form> '' }}" placeholder="Наприклад: Олександр"> 
                </div>                             <div class="form-
            </div> text">Справжнє ім'я користувача для відображення на 
 сайті та в замовленнях.</div> 
        </div>                         </div> 
    </div>  
99 
 
                        <div class="mb-3">             <div class="card border-0 shadow-sm 
                            <label for="email" class="form- rounded-4 p-4 sticky-top" style="top: 20px;"> 
label fw-bold">Email</label>                 <h5 class="fw-bold mb-3"><i class="bi 
                            <input type="email" bi-funnel text-primary me-2"></i> Фільтри</h5> 
class="form-control" id="email" name="email" value="{{  
user.email or '' }}">                 <form method="GET" action="{{ 
                            <div class="form-text">Можна url_for('index') }}"> 
залишити порожнім.</div>                     <div class="mb-3"> 
                        </div>                         <label class="form-label small fw-
 bold text-muted">Пошук за назвою</label> 
                        <div class="mb-4">                         <input type="text" name="q" 
                            <label for="password" placeholder="Введіть слово..." value="{{ 
class="form-label fw-bold text-danger">Новий пароль request.args.get('q', '') }}" class="form-control rounded-3"> 
(необов'язково)</label>                     </div> 
                            <input type="password"  
class="form-control" id="password" name="password"                     <div class="mb-3"> 
placeholder="Залиште порожнім, щоб не змінювати">                         <label class="form-label small fw-
                            <div class="form-text">Введіть bold text-muted">Категорія</label> 
пароль сюди, лише якщо хочете його змінити.</div>                         <select name="category" 
                        </div> class="form-select rounded-3"> 
                             <option value="">Усі 
                        <div class="d-grid"> категорії</option> 
                            <button type="submit"                             {% for cat in categories %} 
class="btn btn-primary fw-bold py-2">                                 <option value="{{ cat.id }}" 
                                <i class="bi bi-save me- {% if request.args.get('category')|int == cat.id 
2"></i> Зберегти зміни %}selected{% endif %}> 
                            </button>                                     {{ cat.name }} 
                        </div>                                 </option> 
                    </form>                             {% endfor %} 
                         </select> 
                </div>                     </div> 
            </div>  
        </div>                     <div class="mb-3"> 
    </div>                         <label class="form-label small fw-
</div> bold text-muted">Бренд</label> 
{% endblock %}                         <select name="brand" 
 class="form-select rounded-3"> 
===================================                             <option value="">Усі 
=============== бренди</option> 
                             {% for b in brands %} 
"index.html"                                 <option value="{{ b }}" {% if 
{% extends 'base.html' %} request.args.get('brand') == b %}selected{% endif %}>{{ b 
 }}</option> 
{% block title %}Головна - PingPong Shop{%                             {% endfor %} 
endblock %}                         </select> 
                     </div> 
{% block content %}  
<div class="container py-4">                     <div class="mb-3"> 
                         <label class="form-label small fw-
    <div class="mb-4"> bold text-muted">Колір</label> 
        <img src="{{ url_for('static',                         <select name="color" class="form-
filename='img/baner.png') }}" select rounded-3"> 
onerror="this.src='https://dummyimage.com/1200x300/2c                             <option value="">Усі 
3e50/fff.png&text=PingPong+Shop+Banner'" кольори</option> 
alt="PingPong Shop Банер" class="img-fluid rounded-4                             {% for c in colors %} 
shadow-sm w-100" style="height: 300px; object-fit:                                 <option value="{{ c }}" {% if 
cover;"> request.args.get('color') == c %}selected{% endif %}>{{ c 
    </div> }}</option> 
                             {% endfor %} 
    <div class="row">                         </select> 
        <div class="col-lg-3 mb-4">                     </div> 
 
100 
 
                    <div class="mb-4">                                 </h5> 
                        <label class="form-label small fw-  
bold text-muted">Ціна (грн)</label>                                 <p class="fw-bold text-danger 
                        <div class="d-flex gap-2"> fs-5 mb-3 mt-auto">{{ product.price }} грн</p> 
                            <input type="number"  
name="min_price" class="form-control rounded-3 text-                                 <div class="d-flex gap-2 mb-
center" placeholder="Від" value="{{ 2"> 
request.args.get('min_price', '') }}">                                     <form action="{{ 
                            <span class="align-self-center url_for('add_to_cart', product_id=product.id) }}" 
text-muted">-</span> method="POST" style="flex: 1; margin: 0;"> 
                            <input type="number"                                         <button type="submit" 
name="max_price" class="form-control rounded-3 text- class="btn btn-primary w-100 add-to-cart py-2 rounded-3 
center" placeholder="До" value="{{ fw-semibold">   В кошик</button> 
request.args.get('max_price', '') }}">                                     </form> 
                        </div>  
                    </div>                                     <button class="btn btn-
 outline-secondary wishlist-btn px-3 rounded-3" data-
                    <button type="submit" class="btn product-id="{{ product.id }}"> 
btn-primary w-100 fw-bold rounded-3 mb-                                         {% if product.id in 
2">Застосувати</button> wishlist_ids %} 
                    <a href="{{ url_for('index') }}"                                             <i class="bi bi-heart-
class="btn btn-light border w-100 fw-bold rounded- fill text-danger"></i> 
3">Скинути</a>                                         {% else %} 
                </form>                                             <i class="bi bi-
            </div> heart"></i> 
        </div>                                         {% endif %} 
                                     </button> 
        <div class="col-lg-9">                                 </div> 
            <div class="row row-cols-1 row-cols-md-2  
row-cols-xl-3 g-4">                                 {% if session.get('username') 
                {% for product in products %} == 'admin' %} 
                    <div class="col">                                 <div class="d-flex gap-2 mt-2 
                        <div class="card h-100 border-0 pt-2 border-top"> 
shadow-sm rounded-4 p-3 position-relative">                                     <a href="{{ 
 url_for('edit_product', product_id=product.id) }}" 
                            <a href="{{ class="btn btn-sm btn-outline-warning flex-fill fw-bold"> 
url_for('product_detail', product_id=product.id) }}">                                         <i class="bi bi-pencil-
                                <img src="{{ square"></i> 
product.image_url }}" alt="{{ product.name }}"                                     </a> 
class="card-img-top rounded-4" style="height: 220px;                                     <form action="{{ 
object-fit: cover;"> url_for('delete_product', product_id=product.id) }}" 
                            </a> method="POST" class="flex-fill" onsubmit="return 
 confirm('Ви впевнені, що хочете видалити цей товар?');" 
                            <div class="mt-3 d-flex flex- style="margin: 0;"> 
column h-100">                                         <button type="submit" 
                                <p class="text-muted small class="btn btn-sm btn-outline-danger w-100 fw-bold"> 
mb-1">                                             <i class="bi bi-
                                    {% if product.category trash"></i> 
%}{{ product.category.name }}{% else %}Без                                         </button> 
категорії{% endif %}                                     </form> 
                                    {% if product.brand %} •                                 </div> 
<span class="fw-semibold">{{ product.brand                                 {% endif %} 
}}</span>{% endif %}  
                                </p>                             </div> 
                         </div> 
                                <h5 class="fw-bold mb-2 text-                     </div> 
dark">                 {% else %} 
                                    <a href="{{                     <div class="col-12 text-center py-5"> 
url_for('product_detail', product_id=product.id) }}"                         <i class="bi bi-search display-3 
class="text-decoration-none text-dark"> text-muted"></i> 
                                        {{ product.name }}                         <h3 class="text-muted mt-3">За 
                                    </a> цими фільтрами нічого не знайдено</h3> 
101 
 
                        <a href="{{ url_for('index') }}"  
class="btn btn-outline-primary mt-3">Скинути                         <div class="form-check mb-3"> 
фільтри</a>                             <input class="form-check-input" 
                    </div>                                    type="checkbox" 
                {% endfor %}                                    name="remember" 
            </div>                                    id="remember"> 
        </div>                             <label class="form-check-label" 
    </div> for="remember"> 
</div>                                 Запам'ятати мене 
{% endblock %}                             </label> 
                         </div> 
===================================  
===============                         <button type="submit" 
                                 class="btn btn-primary w-100 
"login.html" py-2 fw-bold"> 
{% extends "base.html" %}                             Увійти 
                         </button> 
{% block title %}Вхід — PingPong Shop{%  
endblock %}                     </form> 
  
{% block content %}                     <div class="text-center mt-4 small"> 
<div class="container py-5">                         <span class="text-muted">Немає 
 акаунту?</span> 
    <div class="row justify-content-center">                         <a href="{{ url_for('register') }}" 
        <div class="col-md-6 col-lg-5"> class="fw-bold text-decoration-none"> 
                             Зареєструватися 
            <div class="card border-0 shadow-lg                         </a> 
rounded-4">                     </div> 
                <div class="card-body p-4">  
                 </div> 
                    <div class="text-center mb-4">             </div> 
                        <i class="bi bi-person-circle  
display-5 text-primary"></i>         </div> 
                        <h3 class="fw-bold mt-2">Вхід в     </div> 
акаунт</h3>  
                        <p class="text-muted </div> 
small">Увійдіть, щоб продовжити покупки</p> {% endblock %} 
                    </div>  
 ===================================
                    <form method="POST" action="{{ =============== 
url_for('login') }}">  
 "main.js" 
                        <div class="mb-3"> let searchTimeout; 
                          <label class="form-label">Email  
або Ім'я користувача</label> const searchInput = 
            <input type="text" document.querySelector('input[name="q"]'); 
           name="login_input"  
           class="form-control" if (searchInput) { 
           placeholder="admin або     searchInput.addEventListener('input', function 
[email protected]" () { 
           required>         clearTimeout(searchTimeout); 
                        </div>  
         searchTimeout = setTimeout(() => { 
                        <div class="mb-3">             this.form.submit(); 
                            <label class="form-         }, 500); 
label">Пароль</label>     }); 
                            <input type="password" } 
                                   name="password"  
                                   class="form-control" const categorySelect = 
                                   placeholder="••••••••" document.querySelector('select[name="category"]'); 
                                   required>  
                        </div> if (categorySelect) { 
102 
 
    categorySelect.addEventListener('change',     }); 
function () {  
        this.form.submit();     const totalElement = 
    }); document.getElementById('cart-total'); 
}     if (totalElement) { 
         totalElement.innerText = total.toFixed(2) + ' 
const addToCartButtons = грн'; 
document.querySelectorAll('.add-to-cart');     } 
 } 
addToCartButtons.forEach(button => {  
    button.addEventListener('click', function (e) { updateTotal(); 
        alert("Товар додано в кошик  ");  
    }); ===================================
}); =============== 
  
const cartButtons = "models.py" 
document.querySelectorAll('.cart-btn'); from flask_sqlalchemy import SQLAlchemy 
 from datetime import datetime, timezone 
cartButtons.forEach(btn => {  
    btn.addEventListener('click', function () { db = SQLAlchemy() 
        btn.classList.add('clicked');  
 class User(db.Model): 
        setTimeout(() => {     id = db.Column(db.Integer, primary_key=True) 
            btn.classList.remove('clicked');     username = db.Column(db.String(100), 
        }, 150); unique=True, nullable=False) 
    });     email = db.Column(db.String(120), 
}); unique=True, nullable=True) 
     password = db.Column(db.String(200), 
document.querySelectorAll('a[href^="#"]').forEac nullable=False) 
h(anchor => {     is_admin = db.Column(db.Boolean, 
    anchor.addEventListener('click', function (e) { default=False, nullable=False) 
        e.preventDefault();  
 class Category(db.Model): 
        const target =     id = db.Column(db.Integer, primary_key=True) 
document.querySelector(this.getAttribute('href'));     name = db.Column(db.String(100), 
        if (target) { nullable=False) 
            target.scrollIntoView({  
                behavior: 'smooth'     products = db.relationship('Product', 
            }); backref='category', lazy=True) 
        }  
    }); class Product(db.Model): 
});     id = db.Column(db.Integer, primary_key=True) 
     name = db.Column(db.String(200), 
setTimeout(() => { nullable=False) 
    const alerts =     description = db.Column(db.Text) 
document.querySelectorAll('.alert');     price = db.Column(db.Float, nullable=False) 
     image = db.Column(db.String(300)) 
    alerts.forEach(alert => {     category_id = db.Column(db.Integer, 
        alert.style.opacity = '0'; db.ForeignKey('category.id'), nullable=False) 
        setTimeout(() => alert.remove(), 500);  
    });     brand = db.Column(db.String(100), 
}, 3000); nullable=True) 
     color = db.Column(db.String(50), 
function updateTotal() { nullable=True) 
    let total = 0;  
     is_active = db.Column(db.Boolean, 
    document.querySelectorAll('.cart- default=True, nullable=False) 
item').forEach(item => {  
        const price = parseFloat(item.dataset.price); class CartItem(db.Model): 
        const qty = parseInt(item.dataset.quantity);     id = db.Column(db.Integer, primary_key=True) 
     user_id = db.Column(db.Integer, 
        total += price * qty; db.ForeignKey('user.id'), nullable=True) 
103 
 
    session_id = db.Column(db.String(100), <div class="container py-4"> 
nullable=True)  # Для неавторизованих користувачів  
    product_id = db.Column(db.Integer,     <h2 class="fw-bold mb-4"> 
db.ForeignKey('product.id'), nullable=False)         <i class="bi bi-receipt me-2"></i>Мої 
    quantity = db.Column(db.Integer, default=1, замовлення 
nullable=False)     </h2> 
  
    product = db.relationship('Product')     {% if orders %} 
  
class Order(db.Model):     <div class="row g-4"> 
    id = db.Column(db.Integer, primary_key=True)  
    user_id = db.Column(db.Integer,         {% for order in orders %} 
db.ForeignKey('user.id'), nullable=True)         <div class="col-12"> 
    total_price = db.Column(db.Float,  
nullable=False)             <div class="card border-0 shadow-sm 
    status = db.Column(db.String(50), rounded-4"> 
default='new', nullable=False)                 <div class="card-body"> 
    created_at = db.Column(db.DateTime,  
default=lambda: datetime.now(timezone.utc))                     <div class="d-flex flex-column flex-
 md-row justify-content-between align-items-md-center mb-
    name = db.Column(db.String(200), 3 gap-2"> 
nullable=False)                         <div> 
    phone = db.Column(db.String(50),                             <h6 class="fw-bold mb-1"> 
nullable=False)                                 Замовлення #{{ order.id }} 
    email = db.Column(db.String(100),                             </h6> 
nullable=True)                             <small class="text-muted"> 
    address = db.Column(db.Text, nullable=False)                                 {{ 
    delivery_method = db.Column(db.String(100), order.created_at.strftime('%d.%m.%Y %H:%M') }} 
default="Самовивіз")                             </small> 
    payment_method = db.Column(db.String(50),                         </div> 
nullable=False)  
                         <div class="text-md-end d-flex 
    items = db.relationship('OrderItem', flex-column align-items-md-end gap-2"> 
backref='order', cascade="all, delete-orphan", lazy=True)                             <span class="badge 
                                 {% if order.status == 'new' 
class OrderItem(db.Model): %}bg-warning 
    id = db.Column(db.Integer, primary_key=True)                                 {% elif order.status == 
    order_id = db.Column(db.Integer, 'processing' %}bg-info 
db.ForeignKey('order.id'), nullable=False)                                 {% elif order.status == 
 'shipped' %}bg-primary 
    product_id = db.Column(db.Integer,                                 {% elif order.status == 
db.ForeignKey('product.id', ondelete="SET NULL"), 'completed' %}bg-success 
nullable=True)                                 {% else %}bg-secondary 
                                 {% endif %}"> 
    quantity = db.Column(db.Integer,                                 {{ order.status }} 
nullable=False)                             </span> 
  
    price_at_purchase = db.Column(db.Float,                             <strong>{{ order.total_price }} 
nullable=False) ₴</strong> 
  
    product = db.relationship('Product')                             <a href="{{ 
 url_for('print_receipt', order_id=order.id) }}" 
=================================== target="_blank" class="btn btn-sm btn-outline-secondary 
=============== rounded-3"> 
                                 <i class="bi bi-printer me-
"my_orders.html" 1"></i>Друк чека 
{% extends "base.html" %}                             </a> 
                         </div> 
{% block title %}Мої замовлення — PingPong                     </div> 
Shop{% endblock %}  
                     <div class="mb-3"> 
{% block content %}                         {% for item in order.items %} 
104 
 
                        <div class="d-flex justify-content- ===================================
between small mb-2"> =============== 
                            <span>  
                                {{ item.product.name }} × {{ "print_receipt.html" 
item.quantity }} <!DOCTYPE html> 
                            </span> <html lang="uk"> 
                            <span> <head> 
                                {{ item.product.price *     <meta charset="UTF-8"> 
item.quantity }} ₴     <title>Чек замовлення #{{ order.id }}</title> 
                            </span>  
                        </div>     <style> 
                        {% endfor %}         body { 
                    </div>             font-family: Arial, sans-serif; 
             color: #000; 
                    <hr>             margin: 0; 
             padding: 20px; 
                    <div class="row small text-muted">             background: #fff; 
         } 
                        <div class="col-md-6 mb-2">  
                                    .receipt { 
<strong>Доставка:</strong><br>             max-width: 600px; 
                            {{ order.delivery_method             margin: 0 auto; 
}}<br>             border: 1px solid #ccc; 
                            {{ order.address }}             padding: 20px; 
                        </div>             border-radius: 10px; 
         } 
                        <div class="col-md-6 mb-2">  
                            <strong>Оплата:</strong><br>         h1, h2, h3 { 
                            {{ order.payment_method }}             margin: 0; 
                        </div>         } 
  
                    </div>         .header { 
             text-align: center; 
                </div>             margin-bottom: 20px; 
            </div>         } 
  
        </div>         .header h2 { 
        {% endfor %}             margin-bottom: 5px; 
         } 
    </div>  
         .info { 
    {% else %}             margin-bottom: 15px; 
             font-size: 14px; 
    <div class="text-center py-5">         } 
        <i class="bi bi-receipt display-4 text-  
muted"></i>         .info div { 
        <h4 class="mt-3">У вас ще немає             margin-bottom: 4px; 
замовлень</h4>         } 
        <p class="text-muted">Перейдіть до  
каталогу, щоб зробити перше замовлення</p>         table { 
             width: 100%; 
        <a href="{{ url_for('index') }}" class="btn             border-collapse: collapse; 
btn-primary mt-3 px-4">             margin-top: 15px; 
            До каталогу         } 
        </a>  
    </div>         th, td { 
             padding: 8px 5px; 
    {% endif %}             border-bottom: 1px solid #ddd; 
             font-size: 14px; 
</div>         } 
{% endblock %}  
         .text-right { 
105 
 
            text-align: right;         </thead> 
        }         <tbody> 
             {% for item in order.items %} 
        .total {             <tr> 
            text-align: right;                 <td>{{ item.product.name }}</td> 
            font-size: 18px;                 <td class="text-right">{{ item.quantity 
            font-weight: bold; }}</td> 
            margin-top: 20px;                 <td class="text-right">{{ 
        } item.product.price }} ₴</td> 
                 <td class="text-right">{{ 
        .footer { item.product.price * item.quantity }} ₴</td> 
            text-align: center;             </tr> 
            margin-top: 30px;             {% endfor %} 
            font-size: 12px;         </tbody> 
            color: #777;     </table> 
        }  
     <div class="total"> 
        @media print {         Всього: {{ order.total_price }} ₴ 
            body { padding: 0; }     </div> 
            .receipt { border: none; }  
        }     <div class="footer"> 
    </style>         Чек є офіційним підтвердженням 
</head> замовлення в PingPong Shop.<br> 
<body>         З питань сервісу звертайтесь: 
 [email protected] 
<div class="receipt">     </div> 
    <div class="header"> </div> 
        <h2>PingPong Shop</h2>  
        <div>Дякуємо за покупку!</div> </body> 
        <small>Замовлення #{{ order.id }} від {{ </html> 
order.created_at.strftime('%d.%m.%Y %H:%M')  
}}</small> ===================================
    </div> =============== 
  
    <div class="info"> "product_detail.html" 
        <div><strong>Клієнт:</strong> {{ {% extends "base.html" %} 
order.name }}</div>  
        <div><strong>Телефон:</strong> {{ {% block title %}{{ product.name }} — PingPong 
order.phone }}</div> Shop{% endblock %} 
        <div><strong>Email:</strong> {{  
order.email }}</div> </div> {% block content %} 
 <div class="container py-4"> 
    <div class="info">  
        <div><strong>Доставка:</strong> {{     <div class="row g-4"> 
order.delivery_method }}</div>  
        <div><strong>Адреса:</strong> {{         <div class="col-md-6"> 
order.address }}</div>             <div class="position-relative"> 
    </div>  
                 <button class="btn btn-light position-
    <div class="info"> absolute top-0 end-0 m-2 rounded-circle shadow-sm 
        <div><strong>Оплата:</strong> {{ wishlist-btn" 
order.payment_method }}</div>                         data-product-id="{{ product.id 
    </div> }}"> 
                     <i class="bi 
    <table>                         {% if product.id in wishlist_ids %} 
        <thead>                             bi-heart-fill text-danger 
            <tr>                         {% else %} 
                <th>Товар</th>                             bi-heart 
                <th class="text-right">К-сть</th>                         {% endif %}"></i> </button> 
                <th class="text-right">Ціна</th>  
                <th class="text-right">Сума</th>                 <img src="{{ product.image_url }}" 
            </tr> 
106 
 
                     class="img-fluid rounded-4 shadow-  
sm w-100"             <div class="card border-0 shadow-lg 
                     alt="{{ product.name }}" rounded-4"> 
                     style="max-height: 500px; object-fit:                 <div class="card-body p-4"> 
cover;"> </div>  
        </div>                     <div class="text-center mb-4"> 
                         <i class="bi bi-person-plus display-
        <div class="col-md-6 d-flex flex-column"> 5 text-success"></i> 
                         <h3 class="fw-bold mt-
            <h2 class="fw-bold mb-2">{{ 2">Створення акаунту</h3> 
product.name }}</h2>                         <p class="text-muted 
 small">Зареєструйтесь, щоб робити покупки</p> 
            <div class="text-muted mb-3">                     </div> 
                Категорія: <strong>{{  
product.category.name }}</strong>                     {% if error %} 
            </div>                     <div class="alert alert-danger text-
 center shadow-sm rounded-3 py-2"> 
            <div class="fs-4 fw-bold text-primary mb-                         <i class="bi bi-exclamation-
4"> triangle-fill me-2"></i>{{ error }} 
                {{ product.price }} ₴                     </div> 
            </div>                     {% endif %} 
  
            <p class="mb-4 text-secondary">                     <form method="POST" action="{{ 
                {{ product.description or "Опис товару url_for('register') }}" onsubmit="return 
відсутній." }} checkPasswords()"> 
            </p>  
                         <div class="mb-3"> 
            <div class="d-grid gap-3 mt-auto">                             <label class="form-label">Ім'я 
 користувача</label> 
                <form method="POST" action="{{                             <input type="text" 
url_for('add_to_cart', product_id=product.id) }}">                                    name="username" 
                    <button type="submit" class="btn                                    class="form-control" 
btn-primary btn-lg w-100 add-to-cart">                                    placeholder="Ваш 
                        <i class="bi bi-cart-plus me- нікнейм" 
2"></i>Додати в кошик                                    required> 
                    </button>                         </div> 
                </form>  
                         <div class="mb-3"> 
                <a href="{{ url_for('index') }}"                             <label class="form-
class="btn btn-outline-secondary btn-lg w-100"> label">Email</label> 
                    Повернутися до каталогу                             <input type="email" 
                </a>                                    name="email" 
            </div>                                    class="form-control" 
        </div>                                    
    </div> placeholder="[email protected]" 
</div>                                    required> 
{% endblock %}                         </div> 
  
===================================                         <div class="mb-3"> 
===============                             <label class="form-
 label">Пароль</label> 
"register.html"                             <input type="password" 
{% extends "base.html" %}                                    name="password" 
                                    id="password" 
{% block title %}Реєстрація — PingPong                                    class="form-control" 
Shop{% endblock %}                                    placeholder="••••••••" 
                                    required> 
{% block content %}                         </div> 
<div class="container py-5">  
                         <div class="mb-3"> 
    <div class="row justify-content-center">                             <label class="form-
        <div class="col-md-6 col-lg-5"> label">Підтвердження пароля</label> 
107 
 
                            <input type="password"     } else { 
                                   name="confirm_password"         confirmInput.classList.remove("is-invalid"); 
                                   id="confirm_password"         return true; // Дозволяємо відправку 
                                   class="form-control"     } 
                                   placeholder="••••••••" } 
                                   required>  
                            <div class="invalid-feedback"> // Прибираємо помилку, коли користувач 
                                Паролі не співпадають! починає вводити текст знову 
                            </div> document.getElementById("confirm_password").
                        </div> addEventListener("input", function() { 
     this.classList.remove("is-invalid"); 
                        <div class="form-check mb-3"> }); 
                            <input class="form-check-input" </script> 
                                   type="checkbox" {% endblock %} 
                                   required>  
                            <label class="form-check- ===================================
label"> =============== 
                                Я погоджуюсь з умовами  
користування "style.css" 
                            </label> * { 
                        </div>     margin: 0; 
     padding: 0; 
                        <button type="submit"     box-sizing: border-box; 
                                class="btn btn-success w-100 } 
py-2 fw-bold">  
                            Зареєструватися body { 
                        </button>     font-family: Arial, sans-serif; 
     background: #f5f7fa; 
                    </form>     color: #333; 
 } 
                    <div class="text-center mt-4 small">  
                        <span class="text-muted">Вже є header { 
акаунт?</span>     background: #2c3e50; 
                        <a href="{{ url_for('login') }}"     color: white; 
class="fw-bold text-decoration-none">     padding: 15px 30px; 
                            Увійти } 
                        </a>  
                    </div> header h1 { 
     margin-bottom: 10px; 
                </div> } 
            </div>  
 nav a { 
        </div>     color: white; 
    </div>     margin-right: 15px; 
     text-decoration: none; 
</div>     font-weight: bold; 
 } 
<script>  
function checkPasswords() { nav a:hover { 
    const password =     text-decoration: underline; 
document.getElementById("password").value; } 
    const confirmPassword =  
document.getElementById("confirm_password").value; .container { 
    const confirmInput =     padding: 20px 30px; 
document.getElementById("confirm_password"); } 
  
    if (password !== confirmPassword) { .search-form { 
        // Якщо не збігаються - показуємо червону     margin-bottom: 20px; 
рамку і текст помилки (клас Bootstrap) } 
        confirmInput.classList.add("is-invalid");  
        return false; // Зупиняємо відправку форми .search-form input, 
на сервер .search-form select { 
108 
 
    padding: 8px;  
    margin-right: 10px; .cart-btn { 
}     padding: 5px 10px; 
     margin: 0 5px; 
.products {     border: none; 
    display: grid;     background: #ddd; 
    grid-template-columns: repeat(auto-fill,     cursor: pointer; 
minmax(250px, 1fr));     border-radius: 5px; 
    gap: 20px; } 
}  
 .cart-btn.clicked { 
.product-card {     transform: scale(1.2); 
    background: white; } 
    border-radius: 12px;  
    padding: 15px; #cart-total { 
    box-shadow: 0 4px 10px rgba(0,0,0,0.08);     font-size: 20px; 
    transition: 0.3s;     font-weight: bold; 
}     margin-top: 20px; 
 } 
.product-card.hovered {  
    transform: translateY(-5px); form input, 
} form select { 
     display: block; 
.product-card img {     margin-bottom: 10px; 
    width: 100%;     padding: 10px; 
    height: 180px;     width: 300px; 
    object-fit: cover; } 
    border-radius: 10px;  
    margin-bottom: 10px; form button { 
}     background: #27ae60; 
     color: white; 
.product-card h3 {     border: none; 
    margin-bottom: 5px;     padding: 10px 15px; 
}     border-radius: 8px; 
     cursor: pointer; 
.product-card p { } 
    margin-bottom: 10px;  
} form button:hover { 
     background: #219150; 
.product-card button { } 
    background: #3498db;  
    color: white; .order { 
    border: none;     background: white; 
    padding: 10px;     padding: 15px; 
    border-radius: 8px;     margin-bottom: 15px; 
    cursor: pointer;     border-radius: 10px; 
    width: 100%; } 
}  
 .status { 
.product-card button:hover {     padding: 5px 10px; 
    background: #2980b9;     border-radius: 5px; 
}     color: white; 
     font-size: 12px; 
.cart-item { } 
    background: white;  
    padding: 15px; .status-new { 
    margin-bottom: 10px;     background: gray; 
    border-radius: 10px; } 
    display: flex;  
    justify-content: space-between; .status-processing { 
    align-items: center;     background: orange; 
} } 
109 
 
     <div class="row g-4" id="wishlist-container"> 
.status-shipped {  
    background: blue;         {% for item in wishlist_items %} 
}         <div class="col-6 col-md-4 col-lg-3 wishlist-
 item" data-id="{{ item.product.id }}"> 
.status-done {  
    background: green;             <div class="card h-100 border-0 shadow-
} sm rounded-4 position-relative overflow-hidden"> 
  
.admin-order {                 <button class="btn btn-light btn-sm 
    background: #fff; position-absolute top-0 end-0 m-2 rounded-circle shadow-
    padding: 15px; sm remove-btn" 
    margin-bottom: 10px;                         data-id="{{ item.product.id }}" 
    border-left: 5px solid #3498db; style="z-index: 10;"> 
}                     <i class="bi bi-heart-fill text-
 danger"></i> 
.alert {                 </button> 
    padding: 10px;  
    background: #2ecc71;                 <a href="{{ url_for('product_detail', 
    color: white; product_id=item.product.id) }}" class="text-decoration-
    margin-bottom: 10px; none text-dark"> 
    border-radius: 5px;                     <img src="{{ item.product.image_url 
    transition: 0.5s; }}" 
}                          class="card-img-top" 
                          alt="{{ item.product.name }}" 
@media (max-width: 768px) {                          style="height: 200px; object-fit: 
    .products { cover;"> 
        grid-template-columns: 1fr;                 </a> 
    }  
                 <div class="card-body d-flex flex-
    form input, column p-3"> 
    form select {                     <h6 class="fw-bold mb-1 text-
        width: 100%; truncate"> 
    }                         <a href="{{ 
 url_for('product_detail', product_id=item.product.id) }}" 
    .cart-item { class="text-decoration-none text-dark hover-primary"> 
        flex-direction: column;                             {{ item.product.name }} 
        align-items: flex-start;                         </a> 
    }                     </h6> 
}                     <p class="text-muted small mb-2 text-
 truncate"> 
===================================                         {{ item.product.category.name }} 
===============                     </p> 
  
"wishlist.html"                     <div class="mt-auto"> 
{% extends "base.html" %}                         <div class="fw-bold text-danger 
 mb-2"> 
{% block title %}Обране — PingPong Shop{%                             {{ item.product.price }} грн 
endblock %}                         </div> 
  
{% block content %}                         <form action="{{ 
<div class="container py-4"> url_for('add_to_cart', product_id=item.product.id) }}" 
 method="POST" style="margin: 0;"> 
    <div class="d-flex justify-content-between                             <button type="submit" 
align-items-center mb-4"> class="btn btn-primary btn-sm w-100 py-2 rounded-3 fw-
        <h2 class="fw-bold mb-0"> semibold"> 
            <i class="bi bi-heart me-2 text-                                   В кошик 
danger"></i>Моє обране                             </button> 
        </h2>                         </form> 
    </div>                     </div> 
                 </div> 
    {% if wishlist_items %}             </div> 
110 
 
  
        </div>                             const emptyHTML = ` 
        {% endfor %}                                 <div class="text-center py-5" 
 id="empty-state"> 
    </div>                                     <i class="bi bi-heart 
 display-1 text-muted"></i> 
    {% else %}                                     <h3 class="text-muted mt-
 3">Список обраного порожній  </h3> 
    <div class="text-center py-5" id="empty-                                     <p class="text-
state"> muted">Додайте товари, які вам сподобалися, щоб не 
        <i class="bi bi-heart display-1 text- загубити їх.</p> 
muted"></i>                                     <a href="/" class="btn btn-
        <h3 class="text-muted mt-3">Список primary mt-3 px-4 fw-bold rounded-3 shadow-sm"> 
обраного порожній  </h3>                                         Перейти до каталогу 
        <p class="text-muted">Додайте товари, які                                     </a> 
вам сподобалися, щоб не загурити їх.</p>                                 </div> 
        <a href="{{ url_for('index') }}" class="btn                             `; 
btn-primary mt-3 px-4 fw-bold rounded-3 shadow-sm">                             
            Перейти до каталогу document.querySelector('.container').insertAdjacentHTML
        </a> ('beforeend', emptyHTML); 
    </div>                         } 
                     }, 300); 
    {% endif %}                 } 
             }); 
</div>         }); 
     }); 
<script> }); 
document.addEventListener("DOMContentLoade </script> 
d", function () { {% endblock %} 
    document.querySelectorAll('.remove-
btn').forEach(btn => { 
        btn.addEventListener('click', function (e) { 
            e.preventDefault(); 
 
            const productId = this.dataset.id; 
            const card = this.closest('.wishlist-item'); 
 
            fetch(`/toggle_wishlist/${productId}`, { 
                method: 'POST', 
                headers: { 
                    'X-Requested-With': 
'XMLHttpRequest' 
                } 
            }) 
            .then(res => res.json()) 
            .then(data => { 
                if (!data.in_wishlist) { 
                    // Анімація зникнення картки 
                    card.style.transition = 'all 0.3s ease'; 
                    card.style.opacity = '0'; 
                    card.style.transform = 'scale(0.8)'; 
 
                    setTimeout(() => { 
                        card.remove(); 
 
                        // Перевіряємо, чи залишилися 
ще товари в обраному 
                        if 
(document.querySelectorAll('.wishlist-item').length === 0) 
{ 
                            
document.getElementById('wishlist-container')?.remove();