Please use this identifier to cite or link to this item:
https://er.chdtu.edu.ua/handle/ChSTU/6030| Title: | Веб-застосунок для оренди житла |
| Authors: | Оксамитна, Любов Павлівна Дащук, Олег Віталійович |
| Keywords: | ОРЕНДА ЖИТЛА,;ВЕБ-ЗАСТОСУНОК,;ПРОЄКТУВАННЯ,;УПРАВЛІННЯ,;ФУНКЦІОНАЛЬНІСТЬ,;ОПТИМІЗАЦІЯ. |
| Issue Date: | 11-Jun-2025 |
| Abstract: | Актуальність теми полягає у тому, що на сьогоднішній день сфера оренди житла посідає одне з найважливіших місць у житті людей. Це зумовлено постійною потребою у комфортному та безпечному помешканні, чи то для постійного проживання, чи для тимчасового перебування. Ефективна організація процесу оренди житла є надзвичайно важливою, адже вона впливає на добробут та стабільність життя людини. Метою кваліфікаційної роботи бакалавра є розробка та впровадження сучасної веб-платформи для автоматизації та оптимізації процесу пошуку та оренди житла, використовуючи технології React/Next.js та Golang. Об’єкт дослідження: процеси взаємодії між потенційними орендарями та власниками житла на ринку нерухомості. Предмет дослідження: веб-застосунок для оренди житла. У ході написання кваліфікаційної роботи бакалавра було реалізовано веб-застосунок для оренди житла. |
| URI: | https://er.chdtu.edu.ua/handle/ChSTU/6030 |
| Appears in Collections: | 122 Комп’ютерні науки (Комп’ютерні науки та прикладне програмування) |
Files in This Item:
| File | Description | Size | Format | |
|---|---|---|---|---|
| Пояснювальна записка_Дащук Олег_КН-2101_2024-2025.pdf Restricted Access | 2.83 MB | Adobe PDF | View/Open Request a copy |
Items in DSpace are protected by copyright, with all rights reserved, unless otherwise indicated.
Extracted text
МІНІСТЕРСТВО ОСВІТИ І НАУКИ УКРАЇНИ
ЧЕРКАСЬКИЙ ДЕРЖАВНИЙ ТЕХНОЛОГІЧНИЙ УНІВЕРСИТЕТ
Факультет інформаційних технологій і систем
Кафедра комп’ютерних наук та системного аналізу
Пояснювальна записка
до кваліфікаційної роботи
бакалавра
(освітньо-кваліфікаційний рівень)
на тему: «Веб-застосунок для оренди житла»
Виконав: студент 4 курсу, групи КН-2101
спеціальності 122 − «Комп’ютерні науки»
(шифр і назва спеціальності)
освітня програма «Комп’ютерні науки та
(назва освітньої програми)
прикладне програмування
Дащук Олег Віталійович
Керівник __________ Оксамитна Л.П.
(прізвище та ініціали)
Рецензент __________ Стась С.В.
(прізвище та ініціали)
Черкаси 2025 рік
Бланк завдання на кваліфікаційну роботу бакалавра студенту
Черкаський державний технологічний університет
Факультет Інформаційних технологій і систем
Кафедра Комп’ютерних наук та системного аналізу
Освітньо-кваліфікаційний рівень Бакалавр
Спеціальність 122 – Комп’ютерні науки
Освітня програма Комп’ютерні науки та прикладне програмування
ЗАТВЕРДЖУЮ
Завідувач кафедри КНСА
_______________ Юрій ТРИУС
«____» _____________ 2025 р.
ЗАВДАННЯ
на кваліфікаційну роботу бакалавра студенту
Дащуку Олегу Віталійовичу
(прізвище, ім’я, по батькові)
1. Тема роботи Веб-застосунок для оренди житла
Керівник роботи Оксамитна Л.П., к.т.н., доцент
(прізвище, ім’я, по батькові, науковий ступінь, вчене звання)
затверджені наказом університету від «25» лютого 2025 р. №53/03-03.
2. Строк подання студентом роботи до 11 червня 2025 року.
3. Вихідні дані до роботи:
Практичні навички роботи з інформаційними ресурсами. Робота з базами даних.
Звіт з виробничої практики. Звіт з переддипломної практики.
4. Зміст пояснювальної записки (перелік питань, що їх належить розробити):
Вступ
4.1. Дослідження предметної області.
4.2. Проєктування та розробка веб-застосунку для оренди житла.
4.3. Програмна реалізація.
Висновки.
5. Перелік додатків (з точним зазначенням назв додатків):
5.1. Додаток А. Специфікація 482.ЧДТУ. 52109-01.
5.2. Додаток Б. Текст програми.
5.4. Презентація у вигляді 20 слайдів.
6. Консультанти розділів роботи
Прізвище, ініціали та Підпис, дата
Розділ посада
завдання видав завдання прийняв
консультанта
7. Дата видачі завдання 17.12.2024
КАЛЕНДАРНИЙ ПЛАН
Строк
№ з/п Назва етапів кваліфікаційної роботи бакалавра виконання Примітка
етапів роботи
1 Видача завдання на кваліфікаційну роботу
17.12.2024 Виконано
бакалавра.
2 Аналіз літературних джерел, об’єкту та предмету
до 12.01.2025 Виконано
дослідження.
3 Написання теоретичного розділу кваліфікаційної
до 18.03.2025 Виконано
роботи бакалавра.
4 Написання аналітичного розділу кваліфікаційної
до 05.04.2025 Виконано
роботи бакалавра.
5 Написання практичних розділів й висновків до
до 01.05.2025 Виконано
кваліфікаційної роботи бакалавра.
6 Передзахист кваліфікаційної роботи бакалавра
26.05.2025 Виконано
на засіданні кафедри КНСА.
7 Подання роботи завідувачу кафедри КНСА. до 11.06.2025 Виконано
8 Захист кваліфікаційної роботи бакалавра. 11.06.2025 Виконано
Студент _____________________________ Олег ДАЩУК
(підпис)
Керівник роботи ____________________________ Любов ОКСАМИТНА
(підпис)
РЕФЕРАТ
Кваліфікаційна робота бакалавра містить: 84 с., 19 рис., 5 таблиць, 6
використаних джерел, 2 додатки.
Актуальність теми. Актуальність теми полягає у тому, що на сьогоднішній
день сфера оренди житла посідає одне з найважливіших місць у житті людей. Це
зумовлено постійною потребою у комфортному та безпечному помешканні, чи то для
постійного проживання, чи для тимчасового перебування. Ефективна організація
процесу оренди житла є надзвичайно важливою, адже вона впливає на добробут та
стабільність життя людини.
Мета роботи і задачі дослідження. Метою кваліфікаційної роботи бакалавра є
розробка та впровадження сучасної веб-платформи для автоматизації та оптимізації
процесу пошуку та оренди житла, використовуючи технології React/Next.js та Golang.
Об’єкт дослідження: процеси взаємодії між потенційними орендарями та
власниками житла на ринку нерухомості.
Предмет дослідження: веб-застосунок для оренди житла.
Методи дослідження. Для досягнення поставленої мети в кваліфікаційній
роботі бакалавра використовувалися наступні методи:
- теоретичний метод (аналіз літератури з проблеми дослідження);
- емпіричний – полягав у спостереженні веб-застосунків для оренжи житла;
- порівняльний – для здійснення порівнянь існуючих аналогів веб-застосунків.
Перелік ключових слів: ОРЕНДА ЖИТЛА, ВЕБ-ЗАСТОСУНОК, РОЗРОБКА,
БАЗИ ДАНИХ, ПРОЄКТУВАННЯ, УПРАВЛІННЯ, ТЕСТУВАННЯ,
ФУНКЦІОНАЛЬНІСТЬ, ОПТИМІЗАЦІЯ.
ABSTRACT
Work contains. 84 pages, 19 figures, 5 tables, 6 sources used, 2 attachments.
Actuality of theme. The relevance of the topic arises from the fact that today the
sphere of housing rental occupies one of the separate places in people's lives. This is due to
the constant need for comfortable and safe living, whether for permanent or temporary stay.
Effective organization of the housing rental process is extremely important, as it affects the
well-being and stability of a person's life.
Purpose and tasks of the research. The purpose of the bachelor's qualification work
is to develop and implement a modern web platform to automate and optimize the process
of searching for and renting housing, using React/Next.js and Golang technologies.
The object of research. processes of interaction between potential tenants and
homeowners in the real estate market.
The subject of research: web application for renting housing.
Research methods: To achieve the purpose of the bachelor’s qualification work, the
following methods were applied.
- theoretical method (analysis of literature on the research problem);
- empirical – consisted in observing web applications to renting housing;
- comparative – to make comparisons of existing analogues of web applications
The list of keywords: HOUSING RENTAL, WEB APPLICATION,
DEVELOPMENT, DATABASES, DESIGN, MANAGEMENT, TESTING,
FUNCTIONALITY, OPTIMIZATION.
7
ЗМІСТ
ПЕРЕЛІК УМОВНИХ ПОЗНАЧЕНЬ, СИМВОЛІВ, СКОРОЧЕНЬ І ТЕРМІНІВ 8
ВСТУП 9
1 АНАЛІЗ ПРЕДМЕТНОЇ ОБЛАСТІ 11
1.1 Огляд існуючих рішень 11
1.1.1 Аналіз платформи «flatfy» 11
1.1.2 Аналіз платформи «OLX.ua» 12
1.1.3 Аналіз веб-сайту «Booking.com» 14
1.2 Розробка вимог до веб-застосунку оренди житла 16
Висновки до розділу 1 17
2 ПРОЄКТУВАННЯ ВЕБ-ЗАСТОСУНКУ ДЛЯ ОРЕНДИ ЖИТЛА 18
2.1 Опис технологій для розробки 18
2.2 Проєктування архітектури бази даних 19
2.3 Проєктування сторінок веб-застосунку 24
Висновки до розділу 2 27
3 ПРОГРАМНА РЕАЛІЗАЦІЯ 28
3.1 Реалізація функцій веб-застосунку оренди житла 28
3.2 Реалізація зовнішнього UI/UX – дизайну 32
Висновки до розділу 3 34
ВИСНОВКИ 35
СПИСОК ВИКОРИСТАНИХ ДЖЕРЕЛ 36
Додаток А. Специфікація 482.ЧДТУ. 52109-01 37
Додаток Б. Текст програми 39
8
ПЕРЕЛІК УМОВНИХ ПОЗНАЧЕНЬ, СИМВОЛІВ, СКОРОЧЕНЬ І
ТЕРМІНІВ
БД – база даних.
CSS – мова стилізації, яка визначає зовнішній вигляд елементів
веб-сторінок.
JavaScript – мова програмування для додавання інтерактивності до
веб-сторінок.
PostgreSQL – це потужна об'єктно-реляційна система баз даних з
відкритим кодом.
UI – користувацький інтерфейс для взаємодії користувача із
системою.
UX – досвід користувача, який відображає враження від
взаємодії з додатком.
HTTP – протокол передачі даних для обміну інформацією між
клієнтом і сервером.
API – інтерфейс для взаємодії між різними компонентами
програмного забезпечення.
SQL – мова для запитів до баз даних, яка використовується для
управління та маніпуляції даними.
9
ВСТУП
Сфера оренди житла є надзвичайно актуальною та динамічною, охоплюючи
інтереси широкого кола користувачів – від студентів і туристів до власників
нерухомості та агентств. Розробка сучасного веб-застосунку для оренди житла має
на меті не лише спростити процес пошуку та розміщення оголошень, а й надати
інструменти для ефективного аналізу ринку, уподобань користувачів та оптимізації
взаємодії між орендодавцями й орендарями.
У сфері безпеки та довіри, веб-застосунок може допомагати виявляти фейкові
оголошення, запобігати шахрайству та забезпечувати захист персональних даних
користувачів.
Для власників житла та агентів з нерухомості аналітика користувацької
активності дозволяє краще зрозуміти попит, встановлювати конкурентоспроможні
ціни, адаптувати пропозиції під очікування цільової аудиторії.
Для орендарів – це можливість швидше знаходити релевантні варіанти завдяки
системі рекомендацій, фільтрів, рейтингових оцінок та відгуків.
У бізнес-контексті такий застосунок може стати джерелом цінної інформації
про ринкові тенденції, сезонні коливання попиту, популярні локації тощо. Глибокий
аналіз користувацької поведінки дозволяє розробникам і власникам платформи
постійно вдосконалювати функціональність, оптимізувати сервіс та підвищувати
рівень користувацького досвіду.
Попри те, що ринок орендних платформ активно розвивається, більшість
наявних рішень усе ще мають обмеження у функціональності, зручності інтерфейсу
та адаптивності до потреб різних категорій користувачів. Часто інтерфейси
перевантажені, а можливості фільтрації та взаємодії з сервісом – обмежені або
складні для новачків.
Існуючі платформи не завжди відповідають очікуванням щодо простоти,
швидкодії, гнучкості та доступності, що ускладнює використання таких сервісів
звичайними користувачами. Це створює потребу у розробці нового веб-застосунку
для оренди житла, який поєднує інтуїтивно зрозумілий інтерфейс, гнучкі
10
можливості налаштування, адаптивний дизайн та потужні інструменти аналітики.
Застосування інноваційних підходів до обробки даних у поєднанні з
сучасними методами візуалізації дозволить ефективно виявляти тенденції ринку,
адаптувати пропозиції до потреб користувачів і забезпечити зручний та зрозумілий
процес взаємодії з платформою для орендодавців і орендарів.
Актуальність кваліфікаційної роботи бакалавра.
Актуальність теми полягає у тому, що на сьогоднішній день сфера оренди
житла посідає одне з найважливіших місць у житті людей. Це зумовлено постійною
потребою у комфортному та безпечному помешканні, чи то для постійного
проживання, чи для тимчасового перебування. Ефективна організація процесу
оренди житла є надзвичайно важливою, адже вона впливає на добробут та
стабільність життя людини.
Метою кваліфікаційної роботи бакалавра є розробка та впровадження
сучасної веб-платформи для автоматизації та оптимізації процесу пошуку та оренди
житла, використовуючи технології React/Next.js та Golang.
Об’єкт дослідження: процеси взаємодії між потенційними орендарями та
власниками житла на ринку нерухомості.
Предмет дослідження: веб-застосунок для оренди житла.
Методи дослідження. Для досягнення поставленої мети в кваліфікаційній
роботі бакалавра використовувалися наступні методи:
− теоретичний метод (аналіз літератури з проблеми дослідження);
− емпіричний – полягав у спостереженні веб-застосунків для оренди житла;
− порівняльний – для здійснення порівнянь існуючих аналогів веб-застосунків.
11
1 АНАЛІЗ ПРЕДМЕТНОЇ ОБЛАСТІ
1.1 Огляд існуючих рішень
На ринку існує кілька популярних рішень, які пропонують сервіси для оренди
житла. Найбільш популярними серед таких систем вважають «Flatfy.ua», «OLX.ua»,
«Booking.com».
1.1.1 Аналіз платформи «flatfy»
Flatfy.ua [1] – це агрегатор оголошень про нерухомість в Україні, який збирає
інформацію з різноманітних джерел (ріелторські компанії, дошки оголошень,
приватні оголошення). Він доступний через веб-версію, а також має мобільні
додатки для iOS та Android. Flatfy.ua вирізняється своїм комплексним підходом до
пошуку нерухомості, об'єднуючи пропозиції купівлі, продажу та оренди житла. Він
спрямований на спрощення процесу пошуку нерухомості для потенційних покупців
та орендарів, надаючи централізований доступ до великої кількості оголошень.
Цей сервіс ідеально підходить для тих, хто шукає нерухомість, дозволяючи
користувачам швидко знайти потрібний варіант за допомогою зручних фільтрів (тип
нерухомості, ціна, розташування, кількість кімнат). Крім того, він часто пропонує
додаткову інформацію, таку як розташування об'єкта на карті, фотографії та
контактні дані продавця/орендодавця. Однак, оскільки Flatfy.ua є агрегатором,
точність та актуальність деяких оголошень може залежати від вихідних джерел, а
також спеціалізовані функції для оформлення документів або юридичної підтримки
відсутні.
Хоча Flatfy.ua є потужним інструментом для пошуку нерухомості, він також має
певні недоліки:
− Наявність неактуальних оголошень: оскільки Flatfy.ua агрегує
інформацію з багатьох джерел, іноді трапляються оголошення, які вже неактуальні
або є дублікатами.
− Якість оголошень може відрізнятися: інформація в оголошеннях може
бути неповною або неточною, оскільки вона залежить від оригінального джерела.
12
− Відсутність прямого зв'язку з власниками: Flatfy.ua не є платформою для
прямого зв'язку між орендарями/покупцями та власниками. Для контакту, зазвичай,
потрібно переходити на оригінальне джерело оголошення.
− Обмежені фільтри для специфічних потреб: хоча основні фільтри
присутні, для дуже специфічних запитів (наприклад, житло з доступом для людей з
інвалідністю або певні архітектурні особливості) може бракувати деталізації.
Рисунок 1.1 – Головна сторінка платформи «Flatfy.ua» [1]
1.1.2 Аналіз платформи «OLX.ua»
OLX.ua [2] – це один із найпопулярніших онлайн-сервісів для розміщення
оголошень в Україні. Він доступний через веб-версію, а також має мобільні додатки
для iOS та Android. OLX.ua вирізняється своїм широким спектром категорій, від
нерухомості та автомобілів до електроніки та дитячих товарів, а також зручними
функціями для пошуку та продажу. Він спрямований на спрощення процесу купівлі-
продажу та оренди товарів та послуг між приватними особами.
Цей сервіс ідеально підходить для тих, хто шукає або пропонує товари чи
послуги локально, дозволяючи користувачам швидко знайти потрібне оголошення у
своєму регіоні. Крім того, він пропонує зручні інструменти для комунікації між
покупцями та продавцями. Однак, як і на будь-якій великій платформі оголошень,
13
іноді можуть траплятися недобросовісні продавці, а функціональність безкоштовної
версії може мати певні обмеження щодо кількості оголошень або просування.
Рисунок 1.2 – Головна сторінка сайту «OLX.ua» [2]
Цей сервіс ідеально підходить для тих, хто шукає або пропонує товари чи
послуги локально, дозволяючи користувачам швидко знайти потрібне оголошення у
своєму регіоні. Крім того, він пропонує зручні інструменти для комунікації між
покупцями та продавцями.
OLX.ua має і недоліки:
− Відсутність перевірки послуг та товарів.
− Перевантажений дизайн.
1.1.3 Аналіз веб-сайту «Booking.com»
Booking.com [3] – це один із найпопулярніших у світі онлайн-сервісів для
бронювання житла. Він доступний через веб-версію, а також має зручні мобільні
додатки для iOS та Android. Booking.com вирізняється своїм широким вибором
пропозицій, що охоплює готелі, апартаменти, вілли та інші типи помешкань по
14
всьому світу. Він спрямований на спрощення процесу пошуку та бронювання житла
для подорожей та відряджень.
Цей сервіс ідеально підходить для тих, хто планує подорожі, дозволяючи
користувачам швидко знайти та забронювати помешкання, яке відповідає їхнім
потребам та бюджету. Крім того, він пропонує зручні інструменти для фільтрації
результатів за різними критеріями (ціна, рейтинг, розташування, зручності) та
інтегрується з системами онлайн-оплати. Однак, іноді можуть виникати проблеми з
комунікацією між гостями та об'єктами розміщення, а також функціональність
безкоштовної версії може бути обмежена порівняно з програмами лояльності або
преміум-підписками.
Переваги Booking.com:
− Широкий вибір житла: величезна база готелів, апартаментів, хостелів та
інших типів помешкань по всьому світу.
− Зручні фільтри пошуку: детальні параметри для швидкого пошуку
житла за ціною, рейтингом, розташуванням, зручностями та іншими критеріями.
− Відгуки та рейтинги: доступ до мільйонів відгуків від реальних
користувачів, що допомагає зробити поінформований вибір.
− Мобільний додаток: повноцінний функціонал у зручному мобільному
додатку, що дозволяє бронювати на ходу та отримувати сповіщення.
− Безкоштовна відміна: можливість безкоштовної відміни бронювання
для багатьох об'єктів, що забезпечує гнучкість у плануванні подорожей.
− Цінові пропозиції та акції: часті знижки, спеціальні пропозиції та
програма лояльності Genius для постійних клієнтів.
Недоліки Booking.com:
− Приховані комісії: деякі додаткові збори можуть бути не відразу
помітно, що може призвести до несподіваних витрат.
− Проблеми з підтримкою: іноді користувачі стикаються з труднощами
при вирішенні питань з технічною підтримкою або об'єктами розміщення.
15
− Залежність від об'єктів розміщення: якість послуг може сильно
відрізнятися залежно від конкретного готелю або власника апартаментів.
− Перевантажений інтерфейс: для деяких користувачів велика кількість
інформації та опцій може здатися занадто насиченою, що ускладнює навігацію.
Рисунок 1.3 – Головна сторінка веб-сайту «Booking.com» [3]
1.2 Розробка вимог до веб-застосунку для оренди житла
Веб-додаток для оренди житла є потужним інструментом у розпорядженні
сучасного користувача. Його можливості надавати зручний доступ до пропозицій,
аналізувати попит, ціни та відгуки орендарів дозволяють значно покращити процес
пошуку та вибору житла. Водночас важливо зважати як на переваги таких рішень,
так і на можливі виклики, пов’язані з їх використанням.
Багато існуючих платформ для оренди житла є дорогими або надто складними
для новачків. Їхній інтерфейс часто перевантажений функціями й даними, які не
завжди відповідають потребам користувачів. Відсутність чіткого структурування та
пріоритетів у поданні інформації може призводити до плутанини та ускладнювати
процес пошуку житла.
16
Детальний аналіз існуючих проблем і переваг дозволяє сформулювати
ключові вимоги до веб-застосунку для оренди житла, що розробляється. Серед
основних функціональних і технічних характеристик:
- Можливість вводу та виведення інформації про оренду житла.
- Інтуїтивно зрозумілий інтерфейс.
- Обробка та аналіз інформації.
- Використання облікових записів та хешування паролів для забезпечення
безпеки веб-застосунку.
- Можливість масштабування без обмежень щодо кількості користувачів або
інтегрованих сервісів.
- Використання сучасних технологій для розробки веб-застосунку для
забезпечення стабільності, швидкодії та зручності підтримки веб-
застосунку.
Висновки до розділу 1
У даному розділі детально проаналізовано провідні системи, платформи та
ресурси для оренди житла для визначення основних переваг та недоліків як
комерційних, так і безкоштовних сервісів. Поглиблений аналіз дозволяє
узагальнити, що більшість функцій систем є надлишковими, а користувачі віддають
перевагу більш «простому».
Також було сформовано основні функціональні вимоги до веб-застосунку для
оренди житла.
17
2 ПРОЄКТУВАННЯ ВЕБ-ЗАСТОСУНКУ ДЛЯ ОРЕНДИ ЖИТЛА
2.1 Опис технологій для розробки
Для забезпечення максимальної якості розробки веб-застосунку для оренди
житла були застосовані стек технології, що дозволяють швидко розробляти
функціональні рішення з належним інтерфейсом. Основними мовами
програмування, які використовувалися при розробці, були: Golang, React, Next.js,
JavaScript, HTML, CSS.
Основою для розробки веб-застосунку став легкий та зручний
мікрофреймворк для веб-розробки на Golang Gorilla/mux.
Gorilla/mux – це мікро фреймворк для розробки веб-застосунків на Golang,
який відрізняється своєю простотою та високою гнучкістю. Він дозволяє швидко
створювати як невеликі прототипи, так і повноцінні веб-застосунки, не покладаючи
суворих обмежень на архітектуру проєкту. Завдяки використанню шаблонізатора
створювати динамічні HTML-сторінки стає простим і інтуїтивно зрозумілим
завданням. Вбудований сервер розробки дозволяє працювати в режимі
налагодження, що значно полегшує виправлення помилок під час створення
проєкту.
Використання Gorilla/mux та бібліотек для бекенду дозволило оптимізувати
роботу веб-застосунку, зокрема:
- sqlc: забезпечує генерацію коду на основі sql схем та запитів.
- database/sql: стандартна бібліотека для роботи із базами даних в Golang.
- pgx: драйвер PostgreSQL для database/sql.
- air: інструмент для перезавантаження програм Go, який автоматично
перезавантажує сервер щоразу, коли вносяться зміни до коду, покращуючи робочий
процес розробки.
- clerk-sdk-go: інтезрація Clerk для Golang.
18
Для створення, сучасних, зручних сторінок веб-застосунку (фронтенду)
використовувалися:
- React – JavaScript-бібліотека фронтенду, яка має на меті зробити
створення користувацьких інтерфейсів на основі компонентів.
- Tailwind CSS – безкоштовного фреймворку для розробки інтерфейсів,
який створює список "допоміжних" CSS-класів, які можна використовувати для
стилізації кожного елемента шляхом змішування та зіставлення.
- shadcn/ui — набір гарно розроблених, доступних компонентів та
платформа для розповсюдження коду. Працює з вашими улюбленими фреймворками
та моделями штучного інтелекту. Відкритий вихідний код.
- react query — потужна бібліотека для надсилання запитів на сервер та
кешування даних.
Одним із обов'язкових елементів будь-якого веб-застосунку є база даних (БД),
а саме PostgreSQL. PostgreSQL – це безкоштовна система керування реляційними
базами даних з відкритим вихідним кодом, яка робить акцент на розширюваності та
відповідності SQL. PostgreSQL пропонує транзакції з атомарністю, узгодженістю,
ізоляцією, властивостями довговічності, автоматично оновлюваними
представленнями, матеріалізованими представленнями, тригерами, зовнішніми
ключами та збереженими процедурами. Вона підтримується всіма основними
операційними системами, включаючи Windows, Linux, macOS, FreeBSD та
OpenBSD, і обробляє широкий спектр робочих навантажень — від окремих машин
до сховищ даних, озер даних або веб-сервісів з багатьма одночасними
користувачами.
2.2 Проєктування архітектури бази даних
Для реалізації всіх функцій, пов’язаних зі збором, обробкою та відображенням
інформації, критично важливо спроєктувати та інтегрувати надійну базу даних. Вона
повинна забезпечувати зберігання облікових записів користувачів, даних про їхню
активність, а також пов'язану інформацію — оголошення про оренду, зображення і
19
взаємодій. Така структура дозволить ефективно управляти контентом, забезпечити
швидкий доступ до потрібної інформації та підтримувати персоналізований досвід
користувача.
Спроєктована база даних складається з достатньої кількості об’єктів, зокрема
наступних таблиць:
Таблиця: «User» зберігає інформацію про користувачів системи, що дозволяє
їм реєструватися, входити та керувати власними даними.
Таблиця 2.1 – Структура таблиці «User»
Назва елемента Тип даних Опис
id UUID Первинний ключ. Унікальний
ідентифікатор користувача.
created_at TIMESTAMPTZ Час створення запису
updated_at TIMESTAMPTZ Останній час, коли було оновлено
дані
clerk_id VARCHAR(100) ID користувача в Clerk
Таблиця «Apartment» зберігає деталі про житло.
Таблиця 2.2 – Структура таблиці «Apartment»
Назва елемента Тип даних Опис
id UUID Унікальний ідентифікатор посту.
created_at TIMESTAMPTZ Час створення запису
updated_at TIMESTAMPTZ Останній час, коли було оновлено дані
user_id Integer Зовнішній ключ, який посилається на id
користувача у таблиці user. Вказує, хто
створив пост.
name TEXT Назва житла
description TEXT Опис житла
address TEXT Адреса
image_urls TEXT[] Картинки житла
max_guests Integer Максимальна кількість людей для
заселення.
owner_id UUID Зовнішній ключ, який посилається на id
користувача у таблиці user. Вказує, хто
створив житло.
20
Таблиця «Booking» зберігає дані про бронювання житла.
Таблиця 2.3 – Структура таблиці «Booking»
Назва Тип даних Опис
елемента
id UUID Унікальний ідентифікатор
user_id UUID Зовнішній ключ, який посилається на id
користувача у таблиці user. Вказує, хто
забронював житло.
created_at TIMESTAMPTZ Час створення запису
updated_at TIMESTAMPTZ Останній час, коли було оновлено дані
start_date TIMESTAMPTZ Дата публікації відео (рядок 'YYYY-MM-DD')
end_date TIMESTAMPTZ Назва відеоплатформи (наприклад, "YouTube",
"TikTok")
apartment_id UUID Зовнішній ключ, який посилається на id
користувача у таблиці apartment. Вказує, яке
житло було орендовано.
Таблиця «apartment_ratings» зберігає дані про оцінки користувачів до
відповідного житла.
Таблиця 2.4 – Структура таблиці «apartment_ratings»
Назва елемента Тип даних Опис
id UUID Унікальний ідентифікатор
user_id UUID Зовнішній ключ, який посилається на
id користувача у таблиці user. Вказує,
хто поставив оцінку.
created_at TIMESTAMPTZ Час створення запису
updated_at TIMESTAMPTZ Останній час, коли було оновлено
дані
rating Small Integer Оцінка, яку надав користувач
apartment_id UUID Зовнішній ключ, який посилається на
id користувача у таблиці apartment.
Вказує житло, якому поставили
оцінку.
Таблиця «apartment_comments» зберігає записи про відгуки та коментарі, яку надали
користувачі до відповідного житла.
21
Таблиця 2.5 – Структура таблиці «apartment_comments»
Назва елемента Тип даних Опис
id Integer Унікальний ідентифікатор
user_id Integer Зовнішній ключ, який посилається на id
користувача у таблиці user. Вказує, хто
залишив коментар.
created_at TIMESTAMPTZ Час створення запису
updated_at TIMESTAMPTZ Останній час, коли було оновлено дані
content TEXT Коментар користувача
apartment_id UUID Зовнішній ключ, який посилається на id
користувача у таблиці apartment. Вказує
житло, якому написали коментра.
Після проєктування таблиць, можна сформувати діаграму зв’язків бази даних.
При створенні веб-застосунку за допомогою фреймворка є певна особливість
взаємодії. Flask-застосунок використовує SQLAlchemy як ORM (Object-Relational
Mapper) та SQLite як базу даних. Це популярне поєднання для невеликих та середніх
проєктів, оскільки SQLite є легкою, файловою базою даних, що не потребує
окремого сервера.
Інформація про базу даних таблиці та поля генерується безпосередньо з файлів
sql/migration/*.sql. Кожена структура в цьому файлі (наприклад, User, Apartment,
ApartmentComment, ApartmentRating, Booking) відповідає таблиці в базі даних, з
доданою частиною коду (рис. 2.1).
Для ініціалізації бази даних використовується файл __init__.py. Цей файл
відповідає за створення та налаштування Flask-додатку, включаючи ініціалізацію
бази даних.
22
Рисунок 2.1 – Вигляд структури з доданою частиною коду
conn, err := pgxpool.New(ctx, fmt.Sprintf("postgres://%s:%s@%s:%d",
cfg.Postgresql.User, cfg.Postgresql.Password, cfg.Postgresql.Host, cfg.Postgresql.Port)):
Це найважливіший рядок для конфігурації бази даних. Він вказує pgx, як
підєднатися до бази даних. Після створення формується файл бази даних із
вказаними параметрами у models.go, з’являється можливість переглянути зв'язки
(рис. 2.2).
Рисунок 2.2 – Діаграма зв’язків бази даних
23
2.3 Проєктування сторінок веб-застосунку
Проєктування сторінок є важливим етапом для зовнішнього вигляду та
розуміння функцій, які будуть виконуватися.
Для реалізації проєкту було створено сторінки: «home.tsx», «registration.tsx»,
«login.tsx», «listing.tsx».
Сторінка «home.tsx» – головна сторінка сайту.
На сторінці відображаються:
хедер із лінками;
футер з копірайтом та контактами;
− форма;
− форма.
Сторінка «login.html» – сторінка, призначена для входу користувачів в систему.
На сторінці відображаються:
В центрі сторінки – форма авторизації.
Всередині форми:
− Поле логіну (username).
− Поле паролю (password).
− Кнопка “Увійти”.
− Посилання “Створити акаунт”, яке веде на сторінку реєстрації (/register)
− Що виконується:
− Після натискання кнопки “Увійти”:
− Дані передаються на POST /login.
− Якщо логін і пароль правильні – відбувається авторизація (login_user).
− Користувача перекидає на /dashboard.
Сторінка «dashboard.html» – Головна сторінка після входу (Дашборд).
На сторінці відображаються:
Заголовок: «Аналітика».
Перший блок: 4 картки зі статистикою:
− Кількість постів.
24
− Кількість відео.
− Кількість сторіс.
− Кількість підписників.
Далі є 12 інтерактивних графіків (Chart.js):
За днями:
− динаміка постів, лайків, репостів;
− динаміка відео, переглядів, лайків, репостів;
− динаміка сторіс, переглядів, відповідей;
− динаміка підписників.
− За платформами кругові діаграми для:
− постів;
− відео;
− сторіс;
− підписників.
Що виконується:
− Після завантаження сторінки запускається JavaScript:
− Графіки створюються через Chart.js з даних, переданих у chart_data.
− Якщо в даних є значення – графік створюється.
− Якщо немає – виводиться повідомлення “Немає даних для діаграми ...”
Сторінка «statistics.html» – Сторінка статистики.
На сторінці відображаються:
− Заголовок: «Статистика за даними».
− Вгорі – форма з фільтром типу контенту:
− Обирається один з: пости / відео / сторіс / підписники.
Під фільтром таблиця вибраними даними з відповідними колонками:
Що виконується:
При зміні фільтра (типу контенту) – форма автоматично надсилається
(onchange="this.form.submit()").
У відповідь сервер повертає оновлену таблицю з даними.
Сторінка «register.html» – сторінка реєстрації нового користувача.
25
Що відображається:
Заголовок: «Створення акаунту».
В центрі – форма з полями:
− Ім’я.
− Прізвище.
− Електронна пошта.
− Логін.
− Пароль.
− Кнопка “Створити акаунт”.
Що виконується:
Коли користувач заповнює форму та натискає кнопку:
Дані відправляються на POST /register.
У бекенді:
− створюється об’єкт User;
− пароль хешується через generate_password_hash;
− новий користувач зберігається у базу;
− Після реєстрації – редірект на /login з повідомленням "Користувач створений".
Сторінка «add.html» – Сторінка додавання нових записів.
Що відображається:
Заголовок сторінки: "Додати дані - Статистика Соцмереж".
Заголовок: "Додати дані".
Чотири окремі секції/картки.
Картка з синім заголовком. Містить форму для додавання посту: Поля:
"Дата", "Платформа", "Заголовок", "Лайки", "Репости". Кнопка "Додати пост".
Картка із зеленим заголовком. Містить форму для додавання відео:
Поля: "Дата", "Платформа", "Заголовок", "Перегляди", "Лайки", "Репости". Кнопка
"Додати відео".
Картка з помаранчевим заголовком. Містить форму для додавання
сторіс: Поля: "Дата", "Платформа", "Заголовок (опціонально)", "Перегляди",
"Відповіді". Кнопка "Додати сторіс".
26
Картка з блакитним заголовком. Містить форму для додавання даних
про підписників: Поля: "Дата", "Платформа", "Кількість". Кнопка "Додати
підписників".
Кнопка “Зберегти” відправляє форму на POST /add.
Висновки до розділу 2
У процесі розробки веб-застосунку для оренди житла детально розглянуто
стек технологій, що дозволяє швидко розробляти функціональні рішення з
належним інтерфейсом.
Розглянутий Gorilla/mux мікро фреймворк для розробки веб-застосунків на
Golang та його білітіотук: sqlc.
Розглянуто взаємодію з Gorilla/mux з базою даних за допомогою database/sql.
Спроєктована реляційна база даних для реалізації веб-застосунку.
Розглянуто використання бази даних з фреймворком.
Детально спроєктовані сторінки веб-застосунку для оренди житла з описом
елементів та функцій.
27
3 ПРОГРАМНА РЕАЛІЗАЦІЯ
3.1 Реалізація функцій веб-застосунку для оренди житла
Початковим етапом програмної реалізації є створення проєкту базової
структури папок середовища.
Далі створюємо модуль командою go mod init github.com/SerhioTis/book-
apartment-app.
Далі встановлюємо бібліотеки «go mod tidy».
Створення основного файлу веб-застосунку main.go – це центральний файл
для створення та конфігурації Golang-застосунку, а також ініціалізації залежностей.
Визначаємо моделі бази даних models.py та створюємо файл.
Прописуємо значення для таблиць бази даних.
Рисунок 3.1 – Код моделі житла
28
Рисунок 3.2 – Код моделі коментарів
Рисунок 3.3 – Код моделі оцінки житла
Рисунок 3.4 – Код моделі бронювань
Обов’язковим є визначення маршрутів та логіки веб-застосунку, описавши їх
в handlers.go. Цей файл міститиме всі маршрути (URL-адреси) веб-застосунку та
логіку обробки запитів.
29
Рисунок 3.6 – Код структури маршрутів
Рисунок 3.7 – Код маршрутів житла
30
Рисунок 3.8 – Код маршрутів бронювання житла
Також був створений веб хук, який буде приймати запити із Clerk, щоб
асинхронно отримувати дані про користувачів від Clerk.
Рисунок 3.9 – Код веб хука Clerk
31
3.2 Реалізація зовнішнього UI/UX – дизайну
Головна сторінка веб-застосунку для оренди житла зображена на рисунку 3.10.
Для перегляду веб-застосунку потрібно авторизуватися.
Рисунок 3.10 – Вікно авторизації веб-застосунку
Якщо немає облікового запису, то потрібно перейти на сторінку реєстрації та
ввести дані для реєстрації (рис. 3.11).
Рисунок 3.11 – Вікно реєстрації
Після авторизації відкриється головна сторінка (рис. 3.12).
32
Рисунок 3.12 – Головна сторінка веб-застосунку для оренди житла
На головній сторінці можна перейти на сторінку із списком житла.
Рисунок 3.13 – Сторінка із списком житла
Обравши житло, можна переглянути деталі та оформити оренду.
33
Рисунок 3.14 – Сторінка перегляду та замовлення житла
Висновки до розділу 3
У даному розділі було детально описано процес реалізації серверної та
клієнтської частини проєкту, налаштування проєкту на базі фреймворка
Gorilla/mux.
Розглянуто реалізацію функцій та зовнішнього UI/UX – дизайну веб-
застосунку для оренди житла.
34
ВИСНОВКИ
У ході написання кваліфікаційної роботи бакалавра було реалізовано веб-
застосунок для оренди житла.
У кваліфікаційній роботі бакалавра детально проаналізовано провідні системи
для оренди житла з метою визначення основних переваг та недоліків, як
комерційних, так і безкоштовних сервісів. Поглиблений аналіз дозволив зробити
висновки, що більшість функцій систем є надлишковими, а користувачі віддають
перевагу більш «простому».
Також було сформовано основні функціональні вимоги до веб-застосунку для
оренди житла.
Розглянутий Gorilla/mux мікро фреймворк для розробки веб-застосунків на
Golang, а також взаємодію з Gorilla/mux з базою даних за допомогою sqlc та pgx.
Спроєктована реляційна база даних для реалізації веб-застосунку та
розглянуто використання її з фреймворком.
Детально спроєктовані сторінки веб-застосунку для оренди житла з описом
елементів та функцій.
Мета роботи досягнута. У результаті виконання роботи були досягнуті всі цілі,
визначені на етапі розробки вимог і проєктування та розроблено готовий додаток
для використання.
35
СПИСОК ВИКОРИСТАНИХ ДЖЕРЕЛ
1. Документація clerk. URL: https://clerk.com/docs (дата звернення 15.05.2025).
2. Мануал golang. URL: https://go.dev/learn/ (дата звернення 15.05.2025).
3. React документація. URL: https://react.dev/learn (дата звернення 15.05.2025).
4. Документація Next.js. URL: https://nextjs.org/docs (дата звернення 15.05.2025).
5. Вікіпедія вільна енциклопедія. URL:
https://en.wikipedia.org/wiki/React_(software) (дата звернення 15.05.2025).
6. Специфікація OpenAPI. URL: https://spec.openapis.org/oas/v3.1.1.html (дата
звернення 15.05.2025).
36
ДОДАТОК А
Затверджую
Зав. кафедри КНСА,
______________ Юрій ТРИУС
«____»____________2025 р.
ВЕБ-ЗАСТОСУНОК ДЛЯ ОРЕНДИ ЖИТЛА
Специфікація
482.ЧДТУ. 52109-01
Листів 2
Розробник ____________________ Олег ДАЩУК
Керівник ____________________ Любов ОКСАМИТНА
Черкаси – 2025
37
482.ЧДТУ. 52109-01
Позначення Найменування Примітка
Документація
482.ЧДТУ. 52109-01 12 01 Текст програми
38
ДОДАТОК Б
ВЕБ-ЗАСТОСУНОК ДЛЯ ОРЕНДИ ЖИТЛА
Текст програми
482.ЧДТУ. 52109-01 12 01
Листів 45
Розробник _____________ Олег ДАЩУК
Черкаси – 2025
main.go User string `mapstructure:"user"`
Password string `mapstructure:"password"`
package main }
import "github.com/SerhioTis/book-apartment- func ReadConfig(path string) (Config, error) {
app/pkg/app" v := viper.New()
v.SetConfigFile(path)
func main() { if err := v.ReadInConfig(); err != nil {
app.Run() return Config{}, err
} }
config.go var config Config
if err := v.Unmarshal(&config); err != nil {
package config return Config{}, err
}
import (
"github.com/spf13/viper" return config, nil
) }
type Config struct { dto.go
HTTP HTTPConfig
Postgresql PostgresqlConfig package dto
Clerk ClerkConfig
} type ClerkWebhookEventType string
type HTTPConfig struct { const (
Host string `mapstructure:"host"` ClerkWebhookTypeUserCreated
Port int `mapstructure:"port"` ClerkWebhookEventType = "user.created"
} ClerkWebhookTypeUserUpdated
ClerkWebhookEventType = "user.updated"
type ClerkConfig struct { ClerkWebhookTypeUserDeleted
SecretKey string ClerkWebhookEventType = "user.deleted"
`mapstructure:"secret_key"` )
PublicKey string
`mapstructure:"public_key"` type ClerkWebhookRequest struct {
WebhookSigningSecret string Data ClerkWebhookData `json:"data"`
`mapstructure:"webhook_signing_secret"` Timestamp int64 `json:"timestamp"`
} Type ClerkWebhookEventType `json:"type"`
}
type PostgresqlConfig struct {
Host string `mapstructure:"host"` type ClerkWebhookData struct {
Port int `mapstructure:"port"` ID string `json:"id"`
40
} len(apartments))
for i, apartment := range apartments {
apartment_commet_handler.go res[i] = api.ApartmentComment{
Id: apartment.ID,
package handler CreatedAt:
apartment.CreatedAt,
import ( UpdatedAt:
"context" apartment.UpdatedAt,
UserId: apartment.UserID,
"github.com/SerhioTis/book-apartment-app/api" ApartmentId:
"github.com/SerhioTis/book-apartment- apartment.ApartmentID,
app/internal/repository" Content: apartment.Content,
"github.com/clerk/clerk-sdk-go/v2" }
"github.com/google/uuid" }
) return res, nil
}
func (h *Handler) ListApartmentComments(ctx
context.Context, request func (h *Handler) CreateApartmentComment(ctx
api.ListApartmentCommentsRequestObject) context.Context, request
(api.ListApartmentCommentsResponseObject, error) { api.CreateApartmentCommentRequestObject)
limit := int32(10) (api.CreateApartmentCommentResponseObject, error) {
if request.Params.Limit != nil { claims, ok :=
limit = *request.Params.Limit clerk.SessionClaimsFromContext(ctx)
} if !ok {
offset := int32(0) return
if request.Params.Page != nil { api.CreateApartmentComment401JSONResponse{
offset = *request.Params.Page * limit
} UnauthorizedErrorJSONResponse:
api.UnauthorizedErrorJSONResponse{
apartments, err := Access:
h.service.ListApartmentComments(ctx, "unauthorized",
repository.ListApartmentCommentsParams{ },
ApartmentID: request.ApartmentId, }, nil
Limit: limit, }
Offset: offset,
}) clerkID, err := uuid.Parse(claims.Subject)
if err != nil { if err != nil {
return nil, err return nil, err
} }
res := createdApartmentComment, err :=
make(api.ListApartmentComments200JSONResponse, h.service.CreateApartmentComment(ctx,
41
repository.CreateApartmentCommentParams{
ClerkID: clerkID, clerkID, err := uuid.Parse(claims.Subject)
ApartmentID: request.ApartmentId, if err != nil {
Content: request.Body.Comment, return nil, err
}) }
if err != nil {
return nil, err updatedApartmentComment, err :=
} h.service.UpdateApartmentCommentByID(ctx,
repository.UpdateApartmentCommentByIDParams{
return ID: request.CommentId,
api.CreateApartmentComment200JSONResponse{ ClerkID: clerkID,
Id: createdApartmentComment.ID, Content: request.Body.Comment,
CreatedAt: })
createdApartmentComment.CreatedAt, if err != nil {
UpdatedAt: return nil, err
createdApartmentComment.UpdatedAt, }
UserId:
createdApartmentComment.UserID, return
ApartmentId: api.UpdateCommentByID200JSONResponse{
createdApartmentComment.ApartmentID, Id:
Content: updatedApartmentComment.ID,
createdApartmentComment.Content, CreatedAt:
}, nil updatedApartmentComment.CreatedAt,
} UpdatedAt:
updatedApartmentComment.UpdatedAt,
func (h Handler) UpdateCommentByID(ctx UserId:
context.Context, request updatedApartmentComment.UserID,
api.UpdateCommentByIDRequestObject) ApartmentId:
(api.UpdateCommentByIDResponseObject, error) { updatedApartmentComment.ApartmentID,
claims, ok := Content:
clerk.SessionClaimsFromContext(ctx) updatedApartmentComment.Content,
if !ok { }, nil
return }
api.UpdateCommentByID401JSONResponse{
func (h *Handler) DeleteCommentByID(ctx
UnauthorizedErrorJSONResponse: context.Context, request
api.UnauthorizedErrorJSONResponse{ api.DeleteCommentByIDRequestObject)
Access: (api.DeleteCommentByIDResponseObject, error) {
"unauthorized", claims, ok :=
}, clerk.SessionClaimsFromContext(ctx)
}, nil if !ok {
} return
42
api.DeleteCommentByID401JSONResponse{
package handler
UnauthorizedErrorJSONResponse:
api.UnauthorizedErrorJSONResponse{ import (
Access: "context"
"unauthorized",
}, "github.com/SerhioTis/book-apartment-app/api"
}, nil "github.com/SerhioTis/book-apartment-
} app/internal/repository"
"github.com/clerk/clerk-sdk-go/v2"
clerkID, err := uuid.Parse(claims.Subject) "github.com/google/uuid"
if err != nil { "github.com/jackc/pgx/v5/pgtype"
return nil, err )
}
func (h *Handler) CreateApartment(ctx
deletedApartmentComment, err := context.Context, request
h.service.DeleteApartmentCommentByID(ctx, api.CreateApartmentRequestObject)
repository.DeleteApartmentCommentByIDParams{ (api.CreateApartmentResponseObject, error) {
ID: request.CommentId, claims, ok :=
ClerkID: clerkID, clerk.SessionClaimsFromContext(ctx)
}) if !ok {
if err != nil { return
return nil, err api.CreateApartment401JSONResponse{
}
UnauthorizedErrorJSONResponse:
return api.UnauthorizedErrorJSONResponse{
api.DeleteCommentByID200JSONResponse{ Access:
Id: deletedApartmentComment.ID, "unauthorized",
CreatedAt: },
deletedApartmentComment.CreatedAt, }, nil
UpdatedAt: }
deletedApartmentComment.UpdatedAt,
UserId: clerkID, err := uuid.Parse(claims.Subject)
deletedApartmentComment.UserID, if err != nil {
ApartmentId: return nil, err
deletedApartmentComment.ApartmentID, }
Content:
deletedApartmentComment.Content, createdApartment, err :=
}, nil h.service.CreateApartment(ctx,
} repository.CreateApartmentParams{
ClerkID: clerkID,
apartment_handler.go Name: request.Body.Name,
43
Description: request.Body.Description, }, nil
Address: request.Body.Address, }
ImageUrls: request.Body.ImageUrls,
MaxGuests: clerkID, err := uuid.Parse(claims.Subject)
int32(request.Body.MaxGuests), if err != nil {
}) return nil, err
if err != nil { }
return nil, err
} updatedApartment, err :=
h.service.UpdateApartmentByID(ctx,
return api.CreateApartment200JSONResponse{ repository.UpdateApartmentByIDParams{
Id: createdApartment.ID, ClerkID: clerkID,
CreatedAt: ID: request.ApartmentId,
createdApartment.CreatedAt, Name: request.Body.Name,
UpdatedAt: Description: request.Body.Description,
createdApartment.UpdatedAt, Address: request.Body.Address,
Name: createdApartment.Name, ImageUrls: request.Body.ImageUrls,
Description: MaxGuests:
createdApartment.Description, int32(request.Body.MaxGuests),
Address: createdApartment.Address, })
ImageUrls: if err != nil {
createdApartment.ImageUrls, return nil, err
MaxGuests: }
int(createdApartment.MaxGuests),
}, nil return
} api.UpdateApartmentByID200JSONResponse{
Id: updatedApartment.ID,
func (h *Handler) UpdateApartmentByID(ctx CreatedAt:
context.Context, request updatedApartment.CreatedAt,
api.UpdateApartmentByIDRequestObject) UpdatedAt:
(api.UpdateApartmentByIDResponseObject, error) { updatedApartment.UpdatedAt,
claims, ok := Name: updatedApartment.Name,
clerk.SessionClaimsFromContext(ctx) Description:
if !ok { updatedApartment.Description,
return Address: updatedApartment.Address,
api.UpdateApartmentByID401JSONResponse{ ImageUrls:
updatedApartment.ImageUrls,
UnauthorizedErrorJSONResponse: MaxGuests:
api.UnauthorizedErrorJSONResponse{ int(updatedApartment.MaxGuests),
Access: }, nil
"unauthorized", }
},
44
func (h *Handler) DeleteApartmentByID(ctx deletedApartment.Description,
context.Context, request Address: deletedApartment.Address,
api.DeleteApartmentByIDRequestObject) ImageUrls:
(api.DeleteApartmentByIDResponseObject, error) { deletedApartment.ImageUrls,
claims, ok := MaxGuests:
clerk.SessionClaimsFromContext(ctx) int(deletedApartment.MaxGuests),
if !ok { }, nil
return }
api.DeleteApartmentByID401JSONResponse{
func (h *Handler) GetApartmentByID(ctx
UnauthorizedErrorJSONResponse: context.Context, request
api.UnauthorizedErrorJSONResponse{ api.GetApartmentByIDRequestObject)
Access: (api.GetApartmentByIDResponseObject, error) {
"unauthorized", apartment, err :=
}, h.service.GetApartmentByID(ctx, request.ApartmentId)
}, nil if err != nil {
} return nil, err
}
clerkID, err := uuid.Parse(claims.Subject) return
if err != nil { api.GetApartmentByID200JSONResponse{
return nil, err Id: apartment.ID,
} CreatedAt: apartment.CreatedAt,
UpdatedAt: apartment.UpdatedAt,
deletedApartment, err := Name: apartment.Name,
h.service.DeleteApartmentByID(ctx, Description: apartment.Description,
repository.DeleteApartmentByIDParams{ Address: apartment.Address,
ClerkID: clerkID, ImageUrls: apartment.ImageUrls,
ID: request.ApartmentId, MaxGuests: int(apartment.MaxGuests),
}) }, nil
if err != nil { }
return nil, err
} func (h *Handler) ListApartments(ctx
context.Context, request
return api.ListApartmentsRequestObject)
api.DeleteApartmentByID200JSONResponse{ (api.ListApartmentsResponseObject, error) {
Id: deletedApartment.ID, limit := int32(10)
CreatedAt: if request.Params.Limit != nil {
deletedApartment.CreatedAt, limit = int32(*request.Params.Limit)
UpdatedAt: }
deletedApartment.UpdatedAt, offset := int32(0)
Name: deletedApartment.Name, if request.Params.Page != nil {
Description: offset = int32(*request.Params.Page) *
45
limit return api.ListApartments200JSONResponse{
} Apartments: &res,
var fromDate, toDate pgtype.Date }, nil
if request.Params.AvailableDuration != nil { }
fromDate =
convertTimeToPgTypeDate(request.Params.AvailableDur apartment_rating_handler.go
ation.FromDate)
toDate = package handler
convertTimeToPgTypeDate(request.Params.AvailableDur
ation.ToDate) import (
} "context"
apartments, err := h.service.ListApartments(ctx, "github.com/SerhioTis/book-apartment-app/api"
repository.ListApartmentsParams{ "github.com/SerhioTis/book-apartment-
Name: request.Params.Name, app/internal/repository"
Address: request.Params.Address, "github.com/clerk/clerk-sdk-go/v2"
MaxGuests: request.Params.MaxGuests, "github.com/google/uuid"
FromDate: fromDate, )
ToDate: toDate,
Limit: limit, func (h *Handler)
Offset: offset, RateApartmentByApartmentID(ctx context.Context,
}) request api.RateApartmentByApartmentIDRequestObject)
if err != nil { (api.RateApartmentByApartmentIDResponseObject, error)
return nil, err {
} claims, ok :=
clerk.SessionClaimsFromContext(ctx)
res := make([]api.ListApartmentsResponseItem, if !ok {
len(apartments)) return
for i, apartment := range apartments { api.RateApartmentByApartmentID401JSONResponse{
res[i] =
api.ListApartmentsResponseItem{ UnauthorizedErrorJSONResponse:
Id: apartment.ID, api.UnauthorizedErrorJSONResponse{
Name: apartment.Name, Access:
Description: "unauthorized",
apartment.Description, },
Address: apartment.Address, }, nil
ImageUrls: }
apartment.ImageUrls,
MaxGuests: clerkID, err := uuid.Parse(claims.Subject)
int(apartment.MaxGuests), if err != nil {
} return nil, err
} }
46
if err != nil {
rating, err := h.service.RateApartment(ctx, return nil, err
repository.RateApartmentParams{ }
ClerkID: clerkID,
ApartmentID: request.ApartmentId, rating, err :=
Rating: int16(request.Body.Rating), h.service.GetUserApartmentRating(ctx,
}) repository.GetUserApartmentRatingParams{
if err != nil { ClerkID: clerkID,
return nil, err ApartmentID: request.ApartmentId,
} })
if err != nil {
return return nil, err
api.RateApartmentByApartmentID200JSONResponse{ }
Id: rating.ID,
CreatedAt: rating.CreatedAt, return
UpdatedAt: rating.UpdatedAt, api.GetUserRatingByApartmentID200JSONResponse{
ApartmentId: rating.ApartmentID, Id: rating.ID,
Rating: int(rating.Rating), CreatedAt: rating.CreatedAt,
UserId: rating.UserID, UpdatedAt: rating.UpdatedAt,
}, nil ApartmentId: rating.ApartmentID,
} Rating: int(rating.Rating),
UserId: rating.UserID,
func (h *Handler) }, nil
GetUserRatingByApartmentID(ctx context.Context, }
request api.GetUserRatingByApartmentIDRequestObject)
(api.GetUserRatingByApartmentIDResponseObject, error) func (h *Handler)
{ GetApartmentAverageRating(ctx context.Context, request
claims, ok := api.GetApartmentAverageRatingRequestObject)
clerk.SessionClaimsFromContext(ctx) (api.GetApartmentAverageRatingResponseObject, error) {
if !ok { rating, err := h.service.GetApartmentRating(ctx,
return request.ApartmentId)
api.GetUserRatingByApartmentID401JSONResponse{ if err != nil {
return nil, err
UnauthorizedErrorJSONResponse: }
api.UnauthorizedErrorJSONResponse{
Access: return
"unauthorized", api.GetApartmentAverageRating200JSONResponse{
}, Rating: rating.Avg,
}, nil Count: rating.Count,
} }, nil
}
clerkID, err := uuid.Parse(claims.Subject)
47
booking_handler.go repository.CreateBookingParams{
ClerkID: clerkID,
package handler ApartmentID: request.ApartmentId,
StartDate: request.Body.StartDate,
import ( EndDate: request.Body.EndDate,
"context" })
if err != nil {
"github.com/SerhioTis/book-apartment-app/api" return nil, err
"github.com/SerhioTis/book-apartment- }
app/internal/repository"
"github.com/clerk/clerk-sdk-go/v2" return
"github.com/google/uuid" api.BookApartmentByApartmentID200JSONResponse{
"github.com/jackc/pgx/v5/pgtype" Id: createdBooking.ID,
) CreatedAt: createdBooking.CreatedAt,
UpdatedAt:
func (h *Handler) createdBooking.UpdatedAt,
BookApartmentByApartmentID(ctx context.Context, StartDate: createdBooking.StartDate,
request EndDate: createdBooking.EndDate,
api.BookApartmentByApartmentIDRequestObject) UserId: createdBooking.UserID,
(api.BookApartmentByApartmentIDResponseObject, ApartmentId:
error) { createdBooking.ApartmentID,
claims, ok := }, nil
clerk.SessionClaimsFromContext(ctx) }
if !ok {
return func (h *Handler) UpdateBookingByID(ctx
api.BookApartmentByApartmentID401JSONResponse{ context.Context, request
api.UpdateBookingByIDRequestObject)
UnauthorizedErrorJSONResponse: (api.UpdateBookingByIDResponseObject, error) {
api.UnauthorizedErrorJSONResponse{ claims, ok :=
Access: clerk.SessionClaimsFromContext(ctx)
"unauthorized", if !ok {
}, return
}, nil api.UpdateBookingByID401JSONResponse{
}
UnauthorizedErrorJSONResponse:
clerkID, err := uuid.Parse(claims.Subject) api.UnauthorizedErrorJSONResponse{
if err != nil { Access:
return nil, err "unauthorized",
} },
}, nil
createdBooking, err := }
h.service.BookApartment(ctx,
48
clerkID, err := uuid.Parse(claims.Subject) api.UnauthorizedErrorJSONResponse{
if err != nil { Access:
return nil, err "unauthorized",
} },
}, nil
updatedBooking, err := }
h.service.UpdateBookingByID(ctx,
repository.UpdateBookingByIDParams{ clerkID, err := uuid.Parse(claims.Subject)
ClerkID: clerkID, if err != nil {
ID: request.BookingId, return nil, err
StartDate: request.Body.StartDate, }
EndDate: request.Body.EndDate,
}) deletedBooking, err :=
if err != nil { h.service.DeleteBookingByID(ctx,
return nil, err repository.DeleteBookingByIDParams{
} ClerkID: clerkID,
ID: request.BookingId,
return })
api.UpdateBookingByID200JSONResponse{ if err != nil {
Id: updatedBooking.ID, return nil, err
CreatedAt: updatedBooking.CreatedAt, }
UpdatedAt:
updatedBooking.UpdatedAt, return
StartDate: updatedBooking.StartDate, api.DeleteBookingByID200JSONResponse{
EndDate: updatedBooking.EndDate, Id: deletedBooking.ID,
UserId: updatedBooking.UserID, CreatedAt: deletedBooking.CreatedAt,
ApartmentId: UpdatedAt:
updatedBooking.ApartmentID, deletedBooking.UpdatedAt,
}, nil StartDate: deletedBooking.StartDate,
} EndDate: deletedBooking.EndDate,
UserId: deletedBooking.UserID,
func (h *Handler) DeleteBookingByID(ctx ApartmentId:
context.Context, request deletedBooking.ApartmentID,
api.DeleteBookingByIDRequestObject) }, nil
(api.DeleteBookingByIDResponseObject, error) { }
claims, ok :=
clerk.SessionClaimsFromContext(ctx) func (h *Handler) ListUsersBooking(ctx
if !ok { context.Context, request
return api.ListUsersBookingRequestObject)
api.DeleteBookingByID401JSONResponse{ (api.ListUsersBookingResponseObject, error) {
claims, ok :=
UnauthorizedErrorJSONResponse: clerk.SessionClaimsFromContext(ctx)
49
if !ok {
return func (h *Handler) ListApartmentBookings(ctx
api.ListUsersBooking401JSONResponse{ context.Context, request
api.ListApartmentBookingsRequestObject)
UnauthorizedErrorJSONResponse: (api.ListApartmentBookingsResponseObject, error) {
api.UnauthorizedErrorJSONResponse{ var fromDate, toDate pgtype.Date
Access: if request.Params.AvailableDuration != nil {
"unauthorized", fromDate =
}, convertTimeToPgTypeDate(request.Params.AvailableDur
}, nil ation.FromDate)
} toDate =
convertTimeToPgTypeDate(request.Params.AvailableDur
clerkID, err := uuid.Parse(claims.Subject) ation.ToDate)
if err != nil { }
return nil, err
} apartments, err :=
h.service.ListApartmentBookings(ctx,
bookings, err := h.service.ListUserBookings(ctx, repository.ListApartmentBookingsParams{
clerkID) ApartmentID: request.ApartmentId,
if err != nil { FromDate: fromDate,
return nil, err ToDate: toDate,
} })
if err != nil {
res := return nil, err
make(api.ListUsersBooking200JSONResponse, }
len(bookings))
for i, booking := range bookings { res :=
res[i] = api.Booking{ make(api.ListApartmentBookings200JSONResponse,
Id: booking.ID, len(apartments))
CreatedAt: for i, apartment := range apartments {
booking.CreatedAt, res[i] = api.Booking{
UpdatedAt: Id: apartment.ID,
booking.UpdatedAt, CreatedAt:
StartDate: booking.StartDate, apartment.CreatedAt,
EndDate: booking.EndDate, UpdatedAt:
UserId: booking.UserID, apartment.UpdatedAt,
ApartmentId: StartDate:
booking.ApartmentID, apartment.StartDate,
} EndDate:
} apartment.EndDate,
return res, nil UserId: apartment.UserID,
} ApartmentId:
50
apartment.ApartmentID, body", slog.Any("err", err))
} w.WriteHeader(http.StatusBadRequest)
} return
return res, nil }
}
switch request.Type {
clerk_webhook.go case dto.ClerkWebhookTypeUserCreated:
if err :=
package handler h.service.CreateUser(r.Context(), request.Data.ID); err !=
nil {
import ( h.log.Error("failed to create
"encoding/json" user", slog.Any("err", err))
"io"
"log/slog" w.WriteHeader(http.StatusInternalServerError)
"net/http" return
}
"github.com/SerhioTis/book-apartment- case dto.ClerkWebhookTypeUserDeleted:
app/internal/dto" if err :=
) h.service.DeleteUser(r.Context(), request.Data.ID); err !=
nil {
func (h *Handler) WebhooksClerk(w h.log.Error("failed to delete
http.ResponseWriter, r *http.Request) { user", slog.Any("err", err))
payload, err := io.ReadAll(r.Body)
if err != nil { w.WriteHeader(http.StatusInternalServerError)
h.log.Error("failed to read request body", return
slog.Any("err", err)) }
w.WriteHeader(http.StatusBadRequest) }
return
} w.WriteHeader(http.StatusOK)
}
if err := h.svixWebhookVerifier.Verify(payload,
r.Header); err != nil { handler.go
h.log.Error("failed to verify webhook",
slog.Any("err", err)) package handler
w.WriteHeader(http.StatusBadRequest)
return import (
} "context"
"log/slog"
var request dto.ClerkWebhookRequest "strings"
if err := json.Unmarshal(payload, &request); err "time"
!= nil {
h.log.Error("failed to unmarshal request "github.com/SerhioTis/book-apartment-
51
app/internal/service" ai.RequestValidationInput.Request =
"github.com/clerk/clerk-sdk-go/v2" ai.RequestValidationInput.Request.WithContext(clerk.Con
"github.com/clerk/clerk-sdk-go/v2/jwt" textWithSessionClaims(ctx, claims))
"github.com/getkin/kin-openapi/openapi3filter" return nil
"github.com/jackc/pgx/v5/pgtype" }
svix "github.com/svix/svix-webhooks/go"
) func convertTimeToPgTypeDate(t *time.Time)
pgtype.Date {
type Handler struct { if t == nil || t.IsZero() {
log *slog.Logger return pgtype.Date{}
service *service.Service }
svixWebhookVerifier *svix.Webhook
} return pgtype.Date{Time: *t, Valid: true}
}
func NewHandler(
log *slog.Logger, apartment_comment.go
service *service.Service,
svixWebhookVerifier *svix.Webhook, package service
) *Handler {
return &Handler{ import (
log: log, "context"
service: service, "log/slog"
svixWebhookVerifier:
svixWebhookVerifier, "github.com/SerhioTis/book-apartment-
} app/internal/repository"
} "github.com/google/uuid"
)
func (h *Handler) AuthMiddleware(ctx
context.Context, ai *openapi3filter.AuthenticationInput) func (s *Service) CreateApartmentComment(ctx
error { context.Context, arg
authHeader := repository.CreateApartmentCommentParams)
ai.RequestValidationInput.Request.Header.Get("Authoriza (repository.ApartmentComment, error) {
tion") apartmentComment, err :=
sessionToken := strings.TrimPrefix(authHeader, s.repo.CreateApartmentComment(ctx, arg)
"Bearer ") if err != nil {
claims, err := jwt.Verify(ctx, &jwt.VerifyParams{ s.log.Error("failed to create apartment
Token: sessionToken, comment", slog.Any("err", err))
}) return repository.ApartmentComment{},
if err != nil { err
return ai.NewError(err) }
}
return apartmentComment, nil
52
} comments", slog.Any("err", err))
return nil, err
func (s *Service) }
UpdateApartmentCommentByID(ctx context.Context, arg
repository.UpdateApartmentCommentByIDParams) return apartmentComments, nil
(repository.ApartmentComment, error) { }
apartmentComment, err :=
s.repo.UpdateApartmentCommentByID(ctx, arg) func (s *Service)
if err != nil { GetApartmentCommentByID(ctx context.Context, id
s.log.Error("failed to update apartment uuid.UUID) (repository.ApartmentComment, error) {
comment", slog.Any("err", err)) apartmentComment, err :=
return repository.ApartmentComment{}, s.repo.GetApartmentCommentByID(ctx, id)
err if err != nil {
} s.log.Error("failed to get apartment
comment", slog.Any("err", err))
return apartmentComment, nil return repository.ApartmentComment{},
} err
}
func (s *Service)
DeleteApartmentCommentByID(ctx context.Context, arg return apartmentComment, nil
repository.DeleteApartmentCommentByIDParams) }
(repository.ApartmentComment, error) {
apartmentComment, err := apartment_rating.go
s.repo.DeleteApartmentCommentByID(ctx, arg)
if err != nil { package service
s.log.Error("failed to delete apartment
comment", slog.Any("err", err)) import (
return repository.ApartmentComment{}, "context"
err "log/slog"
}
"github.com/SerhioTis/book-apartment-
return apartmentComment, nil app/internal/repository"
} "github.com/google/uuid"
)
func (s *Service) ListApartmentComments(ctx
context.Context, arg func (s *Service) RateApartment(ctx
repository.ListApartmentCommentsParams) context.Context, arg repository.RateApartmentParams)
([]repository.ApartmentComment, error) { (repository.ApartmentRating, error) {
apartmentComments, err := rating, err := s.repo.RateApartment(ctx, arg)
s.repo.ListApartmentComments(ctx, arg) if err != nil {
if err != nil { s.log.Error("failed to create apartment
s.log.Error("failed to get apartment rating", slog.Any("err", err))
53
return repository.ApartmentRating{}, err if err != nil {
} s.log.Error("failed to get apartment
ratings", slog.Any("err", err))
return rating, nil return
} repository.GetAverageApartmentRatingRow{}, err
}
func (s *Service)
RemoveApartmentRatingByID(ctx context.Context, arg return rating, nil
repository.DeleteApartmentRatingByIDParams) }
(repository.ApartmentRating, error) {
rating, err := func (s *Service) GetUserApartmentRating(ctx
s.repo.DeleteApartmentRatingByID(ctx, arg) context.Context, arg
if err != nil { repository.GetUserApartmentRatingParams)
s.log.Error("failed to delete apartment (repository.ApartmentRating, error) {
rating", slog.Any("err", err)) rating, err := s.repo.GetUserApartmentRating(ctx,
return repository.ApartmentRating{}, err arg)
} if err != nil {
s.log.Error("failed to get user apartment
return rating, nil rating", slog.Any("err", err))
} return repository.ApartmentRating{}, err
}
func (s *Service)
RemoveApartmentRatingByApartmentID(ctx return rating, nil
context.Context, arg }
repository.DeleteApartmentRatingByApartmentIDParams)
(repository.ApartmentRating, error) { apartment.go
rating, err :=
s.repo.DeleteApartmentRatingByApartmentID(ctx, arg) package service
if err != nil {
s.log.Error("failed to delete apartment import (
rating", slog.Any("err", err)) "context"
return repository.ApartmentRating{}, err "log/slog"
}
"github.com/SerhioTis/book-apartment-
return rating, nil app/internal/repository"
} "github.com/google/uuid"
)
func (s *Service) GetApartmentRating(ctx
context.Context, apartmentID uuid.UUID) func (s *Service) CreateApartment(ctx
(repository.GetAverageApartmentRatingRow, error) { context.Context, apartmentParams
rating, err := repository.CreateApartmentParams)
s.repo.GetAverageApartmentRating(ctx, apartmentID) (repository.Apartment, error) {
54
apartment, err := s.repo.CreateApartment(ctx, context.Context, id uuid.UUID) (repository.Apartment,
apartmentParams) error) {
if err != nil { apartment, err := s.repo.GetApartmentByID(ctx,
s.log.Error("failed to create apartment", id)
slog.Any("err", err)) if err != nil {
return repository.Apartment{}, err s.log.Error("failed to get apartment",
} slog.Any("err", err))
return repository.Apartment{}, err
return apartment, nil }
}
return apartment, nil
func (s *Service) UpdateApartmentByID(ctx }
context.Context, arg
repository.UpdateApartmentByIDParams) func (s *Service) ListApartments(ctx
(repository.Apartment, error) { context.Context, arg repository.ListApartmentsParams)
apartment, err := ([]repository.ListApartmentsRow, error) {
s.repo.UpdateApartmentByID(ctx, arg) apartments, err := s.repo.ListApartments(ctx, arg)
if err != nil { if err != nil {
s.log.Error("failed to update apartment", s.log.Error("failed to get apartments",
slog.String("id", arg.ID.String()), slog.Any("err", err)) slog.Any("err", err))
return repository.Apartment{}, err return nil, err
} }
return apartment, nil return apartments, nil
} }
func (s *Service) DeleteApartmentByID(ctx func (s *Service) ListApartmentsByOwnerID(ctx
context.Context, arg context.Context, ownerID uuid.UUID)
repository.DeleteApartmentByIDParams) ([]repository.ListUsersApartmentsRow, error) {
(repository.Apartment, error) { apartments, err :=
deletedApartment, err := s.repo.ListUsersApartments(ctx, ownerID)
s.repo.DeleteApartmentByID(ctx, arg) if err != nil {
if err != nil { s.log.Error("failed to get apartments",
s.log.Error("failed to delete apartment", slog.Any("err", err))
slog.String("id", arg.ID.String()), slog.Any("err", err)) return nil, err
return repository.Apartment{}, err }
}
return apartments, nil
return deletedApartment, nil }
}
booking.go
func (s *Service) GetApartmentByID(ctx
55
package service repository.DeleteBookingByIDParams)
(repository.Booking, error) {
import ( deletedBooking, err :=
"context" s.repo.DeleteBookingByID(ctx, arg)
"log/slog" if err != nil {
s.log.Error("failed to cancel booking",
"github.com/SerhioTis/book-apartment- slog.Any("err", err))
app/internal/repository" return repository.Booking{}, err
"github.com/google/uuid" }
)
return deletedBooking, nil
func (s *Service) BookApartment(ctx }
context.Context, arg repository.CreateBookingParams)
(repository.Booking, error) { func (s *Service) ListUserBookings(ctx
apartmentBooking, err := context.Context, clerkID uuid.UUID)
s.repo.CreateBooking(ctx, arg) ([]repository.Booking, error) {
if err != nil { bookings, err := s.repo.ListUsersBookings(ctx,
s.log.Error("failed to book apartment", clerkID)
slog.Any("err", err)) if err != nil {
return repository.Booking{}, err s.log.Error("failed to get user bookings",
} slog.Any("err", err))
return nil, err
return apartmentBooking, nil }
}
return bookings, nil
func (s *Service) UpdateBookingByID(ctx }
context.Context, arg
repository.UpdateBookingByIDParams) func (s *Service) ListApartmentBookings(ctx
(repository.Booking, error) { context.Context, arg
booking, err := s.repo.UpdateBookingByID(ctx, repository.ListApartmentBookingsParams)
arg) ([]repository.Booking, error) {
if err != nil { bookings, err :=
s.log.Error("failed to update booking", s.repo.ListApartmentBookings(ctx, arg)
slog.Any("err", err)) if err != nil {
return repository.Booking{}, err s.log.Error("failed to get apartment
} bookings", slog.Any("err", err))
return nil, err
return booking, nil }
}
return bookings, nil
func (s *Service) DeleteBookingByID(ctx }
context.Context, arg
56
service.go {
s.log.Error("failed to create user",
package service slog.Any("err", err))
return err
import ( }
"log/slog"
s.log.Info("user created", slog.String("clerk_id",
"github.com/SerhioTis/book-apartment- clerkID))
app/internal/repository" return nil
"github.com/clerk/clerk-sdk-go/v2/user" }
)
func (s *Service) DeleteUser(ctx context.Context,
type Service struct { clerkID string) error {
log *slog.Logger if err := s.repo.DeleteUserByClerkID(ctx,
repo *repository.Queries clerkID); err != nil {
clerkUserClient *user.Client s.log.Error("failed to delete user",
} slog.Any("err", err))
return err
func NewService( }
log *slog.Logger,
repo *repository.Queries, s.log.Info("user deleted", slog.String("clerk_id",
clerkUserClient *user.Client, clerkID))
) *Service { return nil
return &Service{ }
log: log,
repo: repo, app.go
clerkUserClient: clerkUserClient,
} package app
}
import (
user.go "context"
"errors"
package service "fmt"
"log/slog"
import ( "net"
"context" "net/http"
"log/slog" "os"
) "os/signal"
"strconv"
func (s *Service) CreateUser(ctx context.Context, "syscall"
clerkID string) error { "time"
if err := s.repo.CreateUser(ctx, clerkID); err != nil
57
"github.com/SerhioTis/book-apartment- conn, err := pgxpool.New(ctx, connString)
app/config" if err != nil {
"github.com/SerhioTis/book-apartment- logger.Error("failed to connect to
app/internal/handler" database", slog.Any("err", err))
"github.com/SerhioTis/book-apartment- return
app/internal/repository" }
"github.com/SerhioTis/book-apartment- defer conn.Close()
app/internal/service"
httpserver "github.com/SerhioTis/book- if err := conn.Ping(ctx); err != nil {
apartment-app/pkg/server/http" logger.Error("failed to ping database",
slog.Any("err", err))
"github.com/clerk/clerk-sdk-go/v2" return
"github.com/clerk/clerk-sdk-go/v2/user" }
"github.com/jackc/pgx/v5/pgxpool"
svix "github.com/svix/svix-webhooks/go" clerkClientConfig := &clerk.ClientConfig{
) BackendConfig: clerk.BackendConfig{
Key: &cfg.Clerk.SecretKey,
func Run() { },
ctx, cancel := }
signal.NotifyContext(context.Background(), os.Interrupt, clerkUserClient :=
syscall.SIGINT, syscall.SIGTERM) user.NewClient(clerkClientConfig)
defer cancel()
svixWebhookVerifier, err :=
logger := svix.NewWebhook(cfg.Clerk.WebhookSigningSecret)
slog.New(slog.NewJSONHandler(os.Stdout, if err != nil {
&slog.HandlerOptions{ logger.Error("failed to create svix
Level: slog.LevelDebug, webhook verifier", slog.Any("err", err))
})) return
slog.SetDefault(logger) }
cfg, err := repo := repository.New(conn)
config.ReadConfig("config/config.yaml") service := service.NewService(logger, repo,
if err != nil { clerkUserClient)
logger.Error("failed to read config", handler := handler.NewHandler(logger, service,
slog.Any("err", err)) svixWebhookVerifier)
return
} httpAddr := net.JoinHostPort(cfg.HTTP.Host,
strconv.Itoa(cfg.HTTP.Port))
connString := httpServer, err := httpserver.NewServer(httpAddr,
fmt.Sprintf("postgres://%s:%s@%s:%d", handler)
cfg.Postgresql.User, cfg.Postgresql.Password, if err != nil {
cfg.Postgresql.Host, cfg.Postgresql.Port) logger.Error("failed to create http
58
server", slog.Any("err", err)) "net/http"
return "time"
}
"github.com/SerhioTis/book-apartment-app/api"
go func() { "github.com/SerhioTis/book-apartment-
logger.Info("starting http server", "addr", app/internal/handler"
httpAddr) "github.com/getkin/kin-openapi/openapi3"
if err := httpServer.Run(); err != nil { "github.com/getkin/kin-openapi/openapi3filter"
cancel() "github.com/gorilla/mux"
if !errors.Is(err, nethttpmiddleware "github.com/oapi-
http.ErrServerClosed) { codegen/nethttp-middleware"
logger.Error("failed to )
run http server", slog.Any("err", err))
} type Server struct {
s *http.Server
return }
}
}() func NewServer(addr string, h *handler.Handler)
(*Server, error) {
<-ctx.Done() spec, err :=
openapi3.NewLoader().LoadFromFile("./api/openapi.yaml
// graceful shutdown ")
ctx, cancel = if err != nil {
context.WithTimeout(context.Background(), return nil, err
5*time.Second) }
defer cancel() spec.Servers = nil
slog.Info("graceful shutdown")
r := mux.NewRouter()
if err := httpServer.Stop(ctx); err != nil { r.HandleFunc("/webhooks/clerk",
logger.Error("failed to stop http server", h.WebhooksClerk)
slog.Any("err", err)) r.Use(mux.CORSMethodMiddleware(r))
} mw :=
nethttpmiddleware.OapiRequestValidatorWithOptions(spe
conn.Close() c, &nethttpmiddleware.Options{
} Options: openapi3filter.Options{
AuthenticationFunc:
http.go h.AuthMiddleware,
},
package httpserver })
r.Use(mw)
import (
"context" si := api.NewStrictHandler(h, nil)
59
handler := api.HandlerFromMux(si, r) 20250508121053_create_users_table.sql
s := &http.Server{
Addr: addr, -- +goose Up
Handler: handler, -- +goose StatementBegin
WriteTimeout: 15 * time.Second, CREATE TABLE users (
ReadTimeout: 15 * time.Second, id UUID PRIMARY KEY DEFAULT
IdleTimeout: 60 * time.Second, gen_random_uuid(),
} created_at TIMESTAMPTZ NOT NULL
DEFAULT NOW(),
return &Server{ updated_at TIMESTAMPTZ NOT NULL
s: s, DEFAULT NOW(),
}, nil clerk_id VARCHAR(100) NOT NULL
} UNIQUE
);
func (s *Server) Run() error {
return s.s.ListenAndServe() CREATE TRIGGER update_users_updated_at
} BEFORE UPDATE ON users
FOR EACH ROW
func (s *Server) Stop(ctx context.Context) error { EXECUTE PROCEDURE
return s.s.Shutdown(ctx) update_updated_at_column();
}
-- +goose StatementEnd
20250508111929_create_update_updated_at_colu
mn_function.sql -- +goose Down
-- +goose StatementBegin
-- +goose Up DROP TRIGGER update_users_updated_at ON
-- +goose StatementBegin users;
CREATE FUNCTION DROP TABLE users;
update_updated_at_column() -- +goose StatementEnd
RETURNS TRIGGER AS $$
BEGIN 20250508184238_create_apartment_table.sql
NEW.updated_at = now();
RETURN NEW; -- +goose Up
END; -- +goose StatementBegin
$$ LANGUAGE plpgsql; CREATE TABLE apartments (
-- +goose StatementEnd id UUID PRIMARY KEY DEFAULT
gen_random_uuid(),
-- +goose Down created_at TIMESTAMPTZ NOT NULL
-- +goose StatementBegin DEFAULT NOW(),
DROP FUNCTION update_updated_at_column; updated_at TIMESTAMPTZ NOT NULL
-- +goose StatementEnd DEFAULT NOW(),
name TEXT NOT NULL,
60
description TEXT NOT NULL, apartments(id) ON DELETE CASCADE NOT NULL,
address TEXT NOT NULL, CONSTRAINT
image_urls TEXT[], unique_user_apartment_rating_constraint UNIQUE
max_guests INTEGER NOT NULL, (user_id, apartment_id)
owner_id UUID REFERENCES users(id) ON );
DELETE CASCADE NOT NULL
); CREATE TRIGGER
update_apartment_ratings_updated_at
CREATE TRIGGER BEFORE UPDATE ON apartment_ratings
update_apartments_updated_at FOR EACH ROW
BEFORE UPDATE ON apartments EXECUTE PROCEDURE
FOR EACH ROW update_updated_at_column();
EXECUTE PROCEDURE -- +goose StatementEnd
update_updated_at_column();
-- +goose StatementEnd -- +goose Down
-- +goose StatementBegin
-- +goose Down DROP TRIGGER
-- +goose StatementBegin update_apartment_ratings_updated_at ON
DROP TRIGGER update_apartments_updated_at apartment_ratings;
ON apartments; DROP TABLE apartment_ratings;
DROP TABLE apartments; -- +goose StatementEnd
-- +goose StatementEnd
20250522190543_create_apartment_comments_t
20250508184511_create_booking_table.sql able.sql
20250509002723_create_apartment_ratings_table -- +goose Up
.sql -- +goose StatementBegin
CREATE TABLE apartment_comments (
-- +goose Up id UUID PRIMARY KEY DEFAULT
-- +goose StatementBegin gen_random_uuid(),
CREATE TABLE apartment_ratings ( created_at TIMESTAMPTZ NOT NULL
id UUID PRIMARY KEY DEFAULT DEFAULT NOW(),
gen_random_uuid(), updated_at TIMESTAMPTZ NOT NULL
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
DEFAULT NOW(), content TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL user_id UUID REFERENCES users(id) ON
DEFAULT NOW(), DELETE CASCADE NOT NULL,
rating SMALLINT NOT NULL CHECK apartment_id UUID REFERENCES
(rating >= 1 AND rating <= 5), apartments(id) ON DELETE CASCADE NOT NULL
user_id UUID REFERENCES users(id) ON );
DELETE CASCADE NOT NULL,
apartment_id UUID REFERENCES CREATE TRIGGER
61
update_apartment_comments_updated_at -- name: ListApartmentComments :many
BEFORE UPDATE ON apartment_comments SELECT * FROM apartment_comments
FOR EACH ROW WHERE apartment_id = $1
EXECUTE PROCEDURE LIMIT $2
update_updated_at_column(); OFFSET $3;
-- +goose StatementEnd
-- name: GetApartmentCommentByID :one
-- +goose Down SELECT * FROM apartment_comments
-- +goose StatementBegin WHERE id = $1;
DROP TRIGGER
update_apartment_comments_updated_at ON apartment_rating.sql
apartment_comments;
DROP TABLE apartment_comments; -- name: RateApartment :one
-- +goose StatementEnd INSERT INTO apartment_ratings (rating,
apartment_id, user_id)
apartment_comment.sql SELECT $1, $2, users.id
FROM users
-- name: CreateApartmentComment :one WHERE users.clerk_id = @clerk_id::UUID
INSERT INTO apartment_comments (content, ON CONFLICT
apartment_id, user_id) (unique_user_apartment_rating_constraint) DO UPDATE
SELECT $1, $2, users.id SET rating = EXCLUDED.rating
FROM users RETURNING *;
WHERE users.clerk_id = @clerk_id::UUID
RETURNING *; -- name: DeleteApartmentRatingByID :one
DELETE FROM apartment_ratings
-- name: UpdateApartmentCommentByID :one USING users
UPDATE apartment_comments WHERE apartment_ratings.id = $1 AND
SET content = $2 users.clerk_id = @clerk_id::UUID AND
FROM users apartment_ratings.user_id = users.id
WHERE apartment_comments.id = $1 RETURNING *;
AND users.clerk_id = @clerk_id::UUID AND
apartment_comments.user_id = users.id -- name: DeleteApartmentRatingByApartmentID
RETURNING apartment_comments.*; :one
DELETE FROM apartment_ratings
-- name: DeleteApartmentCommentByID :one USING users
DELETE FROM apartment_comments WHERE apartment_ratings.apartment_id = $1
USING users AND users.clerk_id = @clerk_id::UUID AND
WHERE apartment_comments.id = $1 AND apartment_ratings.user_id = users.id
users.clerk_id = @clerk_id::UUID AND RETURNING *;
apartment_comments.user_id = users.id
RETURNING *; -- name: GetAverageApartmentRating :one
SELECT apartment_id, AVG(rating), COUNT(*)
62
FROM apartment_ratings -- name: ListApartments :many
WHERE apartment_id = $1; SELECT id, name, description, address,
image_urls, owner_id, max_guests FROM apartments
-- name: GetUserApartmentRating :one WHERE (sqlc.narg(name)::VARCHAR IS NOT
SELECT apartment_ratings.* FROM NULL OR name ILIKE '%' ||
apartment_ratings sqlc.narg(name)::VARCHAR || '%')
JOIN users ON users.id = AND (sqlc.narg(address)::VARCHAR IS NOT
apartment_ratings.user_id NULL OR address ILIKE '%' || @address::VARCHAR ||
WHERE apartment_ratings.apartment_id = $1 '%')
AND users.clerk_id = @clerk_id::UUID; AND (sqlc.narg(max_guests)::INT IS NOT
NULL OR max_guests >= sqlc.narg(max_guests)::INT)
apartment.sql AND (
(sqlc.narg(from_date)::DATE IS NULL
-- name: CreateApartment :one AND sqlc.narg(to_date)::DATE IS NULL)
INSERT INTO apartments (name, description, OR NOT EXISTS (
address, image_urls, max_guests, owner_id) SELECT 1
SELECT $1, $2, $3, $4, $5, users.id FROM bookings b
FROM users WHERE b.apartment_id = apartments.id
WHERE clerk_id = @clerk_id::UUID AND (b.start_date, b.end_date)
RETURNING *; OVERLAPS (sqlc.narg(from_date)::DATE,
sqlc.narg(to_date)::DATE)
-- name: UpdateApartmentByID :one )
UPDATE apartments )
SET name = $2, description = $3, address = $4, ORDER BY created_at
image_urls = $5, max_guests = $6 LIMIT $1
FROM users OFFSET $2;
WHERE apartments.id = $1
AND users.clerk_id = @clerk_id::UUID AND -- name: ListUsersApartments :many
apartments.owner_id = users.id SELECT apartments.id, apartments.name,
RETURNING apartments.*; apartments.description, apartments.address,
apartments.image_urls, apartments.owner_id FROM
-- name: DeleteApartmentByID :one apartments
DELETE FROM apartments JOIN users ON users.id = apartments.owner_id
USING users WHERE users.clerk_id = @clerk_id::UUID AND
WHERE apartments.id = $1 apartments.owner_id = users.id;
AND users.clerk_id = @clerk_id::UUID AND
apartments.owner_id = users.id booking.sql
RETURNING *;
-- name: CreateBooking :one
-- name: GetApartmentByID :one INSERT INTO bookings (start_date, end_date,
SELECT * FROM apartments WHERE id = $1; apartment_id, user_id)
SELECT $1, $2, $3, users.id
63
FROM users INSERT INTO users (clerk_id)
WHERE clerk_id = @clerk_id::UUID VALUES ($1);
RETURNING *;
-- name: DeleteUserByClerkID :exec
-- name: UpdateBookingByID :one DELETE FROM users
UPDATE bookings WHERE clerk_id = $1;
SET start_date = $2, end_date = $3
FROM users apartment_comment.sql.go
WHERE bookings.id = $1
AND users.clerk_id = @clerk_id::UUID AND // Code generated by sqlc. DO NOT EDIT.
apartments.owner_id = users.id // versions:
RETURNING bookings.*; // sqlc v1.28.0
// source: apartment_comment.sql
-- name: DeleteBookingByID :one
DELETE FROM bookings package repository
USING users
WHERE bookings.id = $1 import (
AND users.clerk_id = @clerk_id::UUID AND "context"
bookings.user_id = users.id
RETURNING *; "github.com/google/uuid"
)
-- name: GetBookingByID :one
SELECT id, start_date, end_date, apartment_id, const createApartmentComment = `-- name:
user_id FROM bookings WHERE id = $1; CreateApartmentComment :one
INSERT INTO apartment_comments (content,
-- name: ListUsersBookings :many apartment_id, user_id)
SELECT bookings.* FROM bookings SELECT $1, $2, users.id
JOIN users FROM users
ON bookings.user_id = users.id WHERE users.clerk_id = $3::UUID
WHERE users.clerk_id = @clerk_id::UUID; RETURNING id, created_at, updated_at, content,
user_id, apartment_id
-- name: ListApartmentBookings :many `
SELECT bookings.* FROM bookings
WHERE apartment_id = $1 type CreateApartmentCommentParams struct {
AND (start_date, end_date) OVERLAPS Content string `db:"content" json:"content"`
(COALESCE(sqlc.narg(from_date)::DATE, NOW() - ApartmentID uuid.UUID `db:"apartment_id"
INTERVAL '1 month'), json:"apartment_id"`
COALESCE(sqlc.narg(to_date)::DATE, NOW())); ClerkID uuid.UUID `db:"clerk_id"
json:"clerk_id"`
user.sql }
-- name: CreateUser :exec func (q *Queries) CreateApartmentComment(ctx
64
context.Context, arg CreateApartmentCommentParams) err := row.Scan(
(ApartmentComment, error) { &i.ID,
row := q.db.QueryRow(ctx, &i.CreatedAt,
createApartmentComment, arg.Content, arg.ApartmentID, &i.UpdatedAt,
arg.ClerkID) &i.Content,
var i ApartmentComment &i.UserID,
err := row.Scan( &i.ApartmentID,
&i.ID, )
&i.CreatedAt, return i, err
&i.UpdatedAt, }
&i.Content,
&i.UserID, const getApartmentCommentByID = `-- name:
&i.ApartmentID, GetApartmentCommentByID :one
) SELECT id, created_at, updated_at, content,
return i, err user_id, apartment_id FROM apartment_comments
} WHERE id = $1
`
const deleteApartmentCommentByID = `-- name:
DeleteApartmentCommentByID :one func (q *Queries)
DELETE FROM apartment_comments GetApartmentCommentByID(ctx context.Context, id
USING users uuid.UUID) (ApartmentComment, error) {
WHERE apartment_comments.id = $1 AND row := q.db.QueryRow(ctx,
users.clerk_id = $2::UUID AND getApartmentCommentByID, id)
apartment_comments.user_id = users.id var i ApartmentComment
RETURNING id, created_at, updated_at, content, err := row.Scan(
user_id, apartment_id &i.ID,
` &i.CreatedAt,
&i.UpdatedAt,
type DeleteApartmentCommentByIDParams &i.Content,
struct { &i.UserID,
ID uuid.UUID `db:"id" json:"id"` &i.ApartmentID,
ClerkID uuid.UUID `db:"clerk_id" )
json:"clerk_id"` return i, err
} }
func (q *Queries) const listApartmentComments = `-- name:
DeleteApartmentCommentByID(ctx context.Context, arg ListApartmentComments :many
DeleteApartmentCommentByIDParams) SELECT id, created_at, updated_at, content,
(ApartmentComment, error) { user_id, apartment_id FROM apartment_comments
row := q.db.QueryRow(ctx, WHERE apartment_id = $1
deleteApartmentCommentByID, arg.ID, arg.ClerkID) LIMIT $2
var i ApartmentComment OFFSET $3
65
` UPDATE apartment_comments
SET content = $2
type ListApartmentCommentsParams struct { FROM users
ApartmentID uuid.UUID `db:"apartment_id" WHERE apartment_comments.id = $1
json:"apartment_id"` AND users.clerk_id = $3::UUID AND
Limit int32 `db:"limit" json:"limit"` apartment_comments.user_id = users.id
Offset int32 `db:"offset" json:"offset"` RETURNING apartment_comments.id,
} apartment_comments.created_at,
apartment_comments.updated_at,
func (q *Queries) ListApartmentComments(ctx apartment_comments.content,
context.Context, arg ListApartmentCommentsParams) apartment_comments.user_id,
([]ApartmentComment, error) { apartment_comments.apartment_id
rows, err := q.db.Query(ctx, `
listApartmentComments, arg.ApartmentID, arg.Limit,
arg.Offset) type UpdateApartmentCommentByIDParams
if err != nil { struct {
return nil, err ID uuid.UUID `db:"id" json:"id"`
} Content string `db:"content" json:"content"`
defer rows.Close() ClerkID uuid.UUID `db:"clerk_id"
var items []ApartmentComment json:"clerk_id"`
for rows.Next() { }
var i ApartmentComment
if err := rows.Scan( func (q *Queries)
&i.ID, UpdateApartmentCommentByID(ctx context.Context, arg
&i.CreatedAt, UpdateApartmentCommentByIDParams)
&i.UpdatedAt, (ApartmentComment, error) {
&i.Content, row := q.db.QueryRow(ctx,
&i.UserID, updateApartmentCommentByID, arg.ID, arg.Content,
&i.ApartmentID, arg.ClerkID)
); err != nil { var i ApartmentComment
return nil, err err := row.Scan(
} &i.ID,
items = append(items, i) &i.CreatedAt,
} &i.UpdatedAt,
if err := rows.Err(); err != nil { &i.Content,
return nil, err &i.UserID,
} &i.ApartmentID,
return items, nil )
} return i, err
}
const updateApartmentCommentByID = `--
name: UpdateApartmentCommentByID :one apartment_rating.sql.go
66
err := row.Scan(
// Code generated by sqlc. DO NOT EDIT. &i.ID,
// versions: &i.CreatedAt,
// sqlc v1.28.0 &i.UpdatedAt,
// source: apartment_rating.sql &i.Rating,
&i.UserID,
package repository &i.ApartmentID,
)
import ( return i, err
"context" }
"github.com/google/uuid" const deleteApartmentRatingByID = `-- name:
) DeleteApartmentRatingByID :one
DELETE FROM apartment_ratings
const deleteApartmentRatingByApartmentID = `- USING users
- name: DeleteApartmentRatingByApartmentID :one WHERE apartment_ratings.id = $1 AND
DELETE FROM apartment_ratings users.clerk_id = $2::UUID AND
USING users apartment_ratings.user_id = users.id
WHERE apartment_ratings.apartment_id = $1 RETURNING id, created_at, updated_at, rating,
AND users.clerk_id = $2::UUID AND user_id, apartment_id
apartment_ratings.user_id = users.id `
RETURNING id, created_at, updated_at, rating,
user_id, apartment_id type DeleteApartmentRatingByIDParams struct {
` ID uuid.UUID `db:"id" json:"id"`
ClerkID uuid.UUID `db:"clerk_id"
type json:"clerk_id"`
DeleteApartmentRatingByApartmentIDParams struct { }
ApartmentID uuid.UUID `db:"apartment_id"
json:"apartment_id"` func (q *Queries)
ClerkID uuid.UUID `db:"clerk_id" DeleteApartmentRatingByID(ctx context.Context, arg
json:"clerk_id"` DeleteApartmentRatingByIDParams) (ApartmentRating,
} error) {
row := q.db.QueryRow(ctx,
func (q *Queries) deleteApartmentRatingByID, arg.ID, arg.ClerkID)
DeleteApartmentRatingByApartmentID(ctx var i ApartmentRating
context.Context, arg err := row.Scan(
DeleteApartmentRatingByApartmentIDParams) &i.ID,
(ApartmentRating, error) { &i.CreatedAt,
row := q.db.QueryRow(ctx, &i.UpdatedAt,
deleteApartmentRatingByApartmentID, arg.ApartmentID, &i.Rating,
arg.ClerkID) &i.UserID,
var i ApartmentRating &i.ApartmentID,
67
)
return i, err type GetUserApartmentRatingParams struct {
} ApartmentID uuid.UUID `db:"apartment_id"
json:"apartment_id"`
const getAverageApartmentRating = `-- name: ClerkID uuid.UUID `db:"clerk_id"
GetAverageApartmentRating :one json:"clerk_id"`
SELECT apartment_id, AVG(rating), COUNT(*) }
FROM apartment_ratings
WHERE apartment_id = $1 func (q *Queries) GetUserApartmentRating(ctx
` context.Context, arg GetUserApartmentRatingParams)
(ApartmentRating, error) {
type GetAverageApartmentRatingRow struct { row := q.db.QueryRow(ctx,
ApartmentID uuid.UUID `db:"apartment_id" getUserApartmentRating, arg.ApartmentID, arg.ClerkID)
json:"apartment_id"` var i ApartmentRating
Avg float64 `db:"avg" json:"avg"` err := row.Scan(
Count int64 `db:"count" json:"count"` &i.ID,
} &i.CreatedAt,
&i.UpdatedAt,
func (q *Queries) &i.Rating,
GetAverageApartmentRating(ctx context.Context, &i.UserID,
apartmentID uuid.UUID) &i.ApartmentID,
(GetAverageApartmentRatingRow, error) { )
row := q.db.QueryRow(ctx, return i, err
getAverageApartmentRating, apartmentID) }
var i GetAverageApartmentRatingRow
err := row.Scan(&i.ApartmentID, &i.Avg, const rateApartment = `-- name: RateApartment
&i.Count) :one
return i, err INSERT INTO apartment_ratings (rating,
} apartment_id, user_id)
SELECT $1, $2, users.id
const getUserApartmentRating = `-- name: FROM users
GetUserApartmentRating :one WHERE users.clerk_id = $3::UUID
SELECT apartment_ratings.id, ON CONFLICT
apartment_ratings.created_at, (unique_user_apartment_rating_constraint) DO UPDATE
apartment_ratings.updated_at, apartment_ratings.rating, SET rating = EXCLUDED.rating
apartment_ratings.user_id, apartment_ratings.apartment_id RETURNING id, created_at, updated_at, rating,
FROM apartment_ratings user_id, apartment_id
JOIN users ON users.id = `
apartment_ratings.user_id
WHERE apartment_ratings.apartment_id = $1 type RateApartmentParams struct {
AND users.clerk_id = $2::UUID Rating int16 `db:"rating" json:"rating"`
` ApartmentID uuid.UUID `db:"apartment_id"
68
json:"apartment_id"` SELECT $1, $2, $3, $4, $5, users.id
ClerkID uuid.UUID `db:"clerk_id" FROM users
json:"clerk_id"` WHERE clerk_id = $6::UUID
} RETURNING id, created_at, updated_at, name,
description, address, image_urls, max_guests, owner_id
func (q *Queries) RateApartment(ctx `
context.Context, arg RateApartmentParams)
(ApartmentRating, error) { type CreateApartmentParams struct {
row := q.db.QueryRow(ctx, rateApartment, Name string `db:"name" json:"name"`
arg.Rating, arg.ApartmentID, arg.ClerkID) Description string `db:"description"
var i ApartmentRating json:"description"`
err := row.Scan( Address string `db:"address" json:"address"`
&i.ID, ImageUrls []string `db:"image_urls"
&i.CreatedAt, json:"image_urls"`
&i.UpdatedAt, MaxGuests int32 `db:"max_guests"
&i.Rating, json:"max_guests"`
&i.UserID, ClerkID uuid.UUID `db:"clerk_id"
&i.ApartmentID, json:"clerk_id"`
) }
return i, err
} func (q *Queries) CreateApartment(ctx
context.Context, arg CreateApartmentParams) (Apartment,
apartment.sql.go error) {
row := q.db.QueryRow(ctx, createApartment,
// Code generated by sqlc. DO NOT EDIT. arg.Name,
// versions: arg.Description,
// sqlc v1.28.0 arg.Address,
// source: apartment.sql arg.ImageUrls,
arg.MaxGuests,
package repository arg.ClerkID,
)
import ( var i Apartment
"context" err := row.Scan(
&i.ID,
"github.com/google/uuid" &i.CreatedAt,
"github.com/jackc/pgx/v5/pgtype" &i.UpdatedAt,
) &i.Name,
&i.Description,
const createApartment = `-- name: &i.Address,
CreateApartment :one &i.ImageUrls,
INSERT INTO apartments (name, description, &i.MaxGuests,
address, image_urls, max_guests, owner_id) &i.OwnerID,
69
) GetApartmentByID :one
return i, err SELECT id, created_at, updated_at, name,
} description, address, image_urls, max_guests, owner_id
FROM apartments WHERE id = $1
const deleteApartmentByID = `-- name: `
DeleteApartmentByID :one
DELETE FROM apartments func (q *Queries) GetApartmentByID(ctx
USING users context.Context, id uuid.UUID) (Apartment, error) {
WHERE apartments.id = $1 row := q.db.QueryRow(ctx, getApartmentByID,
AND users.clerk_id = $2::UUID AND id)
apartments.owner_id = users.id var i Apartment
RETURNING id, created_at, updated_at, name, err := row.Scan(
description, address, image_urls, max_guests, owner_id &i.ID,
` &i.CreatedAt,
&i.UpdatedAt,
type DeleteApartmentByIDParams struct { &i.Name,
ID uuid.UUID `db:"id" json:"id"` &i.Description,
ClerkID uuid.UUID `db:"clerk_id" &i.Address,
json:"clerk_id"` &i.ImageUrls,
} &i.MaxGuests,
&i.OwnerID,
func (q *Queries) DeleteApartmentByID(ctx )
context.Context, arg DeleteApartmentByIDParams) return i, err
(Apartment, error) { }
row := q.db.QueryRow(ctx,
deleteApartmentByID, arg.ID, arg.ClerkID) const listApartments = `-- name: ListApartments
var i Apartment :many
err := row.Scan( SELECT id, name, description, address,
&i.ID, image_urls, owner_id, max_guests FROM apartments
&i.CreatedAt, WHERE ($3::VARCHAR IS NOT NULL OR
&i.UpdatedAt, name ILIKE '%' || $3::VARCHAR || '%')
&i.Name, AND ($4::VARCHAR IS NOT NULL OR
&i.Description, address ILIKE '%' || $4::VARCHAR || '%')
&i.Address, AND ($5::INT IS NOT NULL OR max_guests
&i.ImageUrls, >= $5::INT)
&i.MaxGuests, AND (
&i.OwnerID, ($6::DATE IS NULL AND $7::DATE IS
) NULL)
return i, err OR NOT EXISTS (
} SELECT 1
FROM bookings b
const getApartmentByID = `-- name: WHERE b.apartment_id = apartments.id
70
AND (b.start_date, b.end_date) arg.Name,
OVERLAPS ($6::DATE, $7::DATE) arg.Address,
) arg.MaxGuests,
) arg.FromDate,
ORDER BY created_at arg.ToDate,
LIMIT $1 )
OFFSET $2 if err != nil {
` return nil, err
}
type ListApartmentsParams struct { defer rows.Close()
Limit int32 `db:"limit" json:"limit"` var items []ListApartmentsRow
Offset int32 `db:"offset" json:"offset"` for rows.Next() {
Name *string `db:"name" json:"name"` var i ListApartmentsRow
Address *string `db:"address" json:"address"` if err := rows.Scan(
MaxGuests *int32 `db:"max_guests" &i.ID,
json:"max_guests"` &i.Name,
FromDate pgtype.Date `db:"from_date" &i.Description,
json:"from_date"` &i.Address,
ToDate pgtype.Date `db:"to_date" &i.ImageUrls,
json:"to_date"` &i.OwnerID,
} &i.MaxGuests,
); err != nil {
type ListApartmentsRow struct { return nil, err
ID uuid.UUID `db:"id" json:"id"` }
Name string `db:"name" json:"name"` items = append(items, i)
Description string `db:"description" }
json:"description"` if err := rows.Err(); err != nil {
Address string `db:"address" json:"address"` return nil, err
ImageUrls []string `db:"image_urls" }
json:"image_urls"` return items, nil
OwnerID uuid.UUID `db:"owner_id" }
json:"owner_id"`
MaxGuests int32 `db:"max_guests" const listUsersApartments = `-- name:
json:"max_guests"` ListUsersApartments :many
} SELECT apartments.id, apartments.name,
apartments.description, apartments.address,
func (q *Queries) ListApartments(ctx apartments.image_urls, apartments.owner_id FROM
context.Context, arg ListApartmentsParams) apartments
([]ListApartmentsRow, error) { JOIN users ON users.id = apartments.owner_id
rows, err := q.db.Query(ctx, listApartments, WHERE users.clerk_id = $1::UUID AND
arg.Limit, apartments.owner_id = users.id
arg.Offset, `
71
type ListUsersApartmentsRow struct { const updateApartmentByID = `-- name:
ID uuid.UUID `db:"id" json:"id"` UpdateApartmentByID :one
Name string `db:"name" json:"name"` UPDATE apartments
Description string `db:"description" SET name = $2, description = $3, address = $4,
json:"description"` image_urls = $5, max_guests = $6
Address string `db:"address" json:"address"` FROM users
ImageUrls []string `db:"image_urls" WHERE apartments.id = $1
json:"image_urls"` AND users.clerk_id = $7::UUID AND
OwnerID uuid.UUID `db:"owner_id" apartments.owner_id = users.id
json:"owner_id"` RETURNING apartments.id,
} apartments.created_at, apartments.updated_at,
apartments.name, apartments.description,
func (q *Queries) ListUsersApartments(ctx apartments.address, apartments.image_urls,
context.Context, clerkID uuid.UUID) apartments.max_guests, apartments.owner_id
([]ListUsersApartmentsRow, error) { `
rows, err := q.db.Query(ctx, listUsersApartments,
clerkID) type UpdateApartmentByIDParams struct {
if err != nil { ID uuid.UUID `db:"id" json:"id"`
return nil, err Name string `db:"name" json:"name"`
} Description string `db:"description"
defer rows.Close() json:"description"`
var items []ListUsersApartmentsRow Address string `db:"address" json:"address"`
for rows.Next() { ImageUrls []string `db:"image_urls"
var i ListUsersApartmentsRow json:"image_urls"`
if err := rows.Scan( MaxGuests int32 `db:"max_guests"
&i.ID, json:"max_guests"`
&i.Name, ClerkID uuid.UUID `db:"clerk_id"
&i.Description, json:"clerk_id"`
&i.Address, }
&i.ImageUrls,
&i.OwnerID, func (q *Queries) UpdateApartmentByID(ctx
); err != nil { context.Context, arg UpdateApartmentByIDParams)
return nil, err (Apartment, error) {
} row := q.db.QueryRow(ctx,
items = append(items, i) updateApartmentByID,
} arg.ID,
if err := rows.Err(); err != nil { arg.Name,
return nil, err arg.Description,
} arg.Address,
return items, nil arg.ImageUrls,
} arg.MaxGuests,
72
arg.ClerkID, start_date, end_date, apartment_id, user_id
) `
var i Apartment
err := row.Scan( type CreateBookingParams struct {
&i.ID, StartDate time.Time `db:"start_date"
&i.CreatedAt, json:"start_date"`
&i.UpdatedAt, EndDate time.Time `db:"end_date"
&i.Name, json:"end_date"`
&i.Description, ApartmentID uuid.UUID `db:"apartment_id"
&i.Address, json:"apartment_id"`
&i.ImageUrls, ClerkID uuid.UUID `db:"clerk_id"
&i.MaxGuests, json:"clerk_id"`
&i.OwnerID, }
)
return i, err func (q *Queries) CreateBooking(ctx
} context.Context, arg CreateBookingParams) (Booking,
error) {
booking.sql.go row := q.db.QueryRow(ctx, createBooking,
arg.StartDate,
// Code generated by sqlc. DO NOT EDIT. arg.EndDate,
// versions: arg.ApartmentID,
// sqlc v1.28.0 arg.ClerkID,
// source: booking.sql )
var i Booking
package repository err := row.Scan(
&i.ID,
import ( &i.CreatedAt,
"context" &i.UpdatedAt,
"time" &i.StartDate,
&i.EndDate,
"github.com/google/uuid" &i.ApartmentID,
"github.com/jackc/pgx/v5/pgtype" &i.UserID,
) )
return i, err
const createBooking = `-- name: CreateBooking }
:one
INSERT INTO bookings (start_date, end_date, const deleteBookingByID = `-- name:
apartment_id, user_id) DeleteBookingByID :one
SELECT $1, $2, $3, users.id DELETE FROM bookings
FROM users USING users
WHERE clerk_id = $4::UUID WHERE bookings.id = $1
RETURNING id, created_at, updated_at, AND users.clerk_id = $2::UUID AND
73
bookings.user_id = users.id json:"apartment_id"`
RETURNING id, created_at, updated_at, UserID uuid.UUID `db:"user_id"
start_date, end_date, apartment_id, user_id json:"user_id"`
` }
type DeleteBookingByIDParams struct { func (q *Queries) GetBookingByID(ctx
ID uuid.UUID `db:"id" json:"id"` context.Context, id uuid.UUID) (GetBookingByIDRow,
ClerkID uuid.UUID `db:"clerk_id" error) {
json:"clerk_id"` row := q.db.QueryRow(ctx, getBookingByID, id)
} var i GetBookingByIDRow
err := row.Scan(
func (q *Queries) DeleteBookingByID(ctx &i.ID,
context.Context, arg DeleteBookingByIDParams) &i.StartDate,
(Booking, error) { &i.EndDate,
row := q.db.QueryRow(ctx, deleteBookingByID, &i.ApartmentID,
arg.ID, arg.ClerkID) &i.UserID,
var i Booking )
err := row.Scan( return i, err
&i.ID, }
&i.CreatedAt,
&i.UpdatedAt, const listApartmentBookings = `-- name:
&i.StartDate, ListApartmentBookings :many
&i.EndDate, SELECT bookings.id, bookings.created_at,
&i.ApartmentID, bookings.updated_at, bookings.start_date,
&i.UserID, bookings.end_date, bookings.apartment_id,
) bookings.user_id FROM bookings
return i, err WHERE apartment_id = $1
} AND (start_date, end_date) OVERLAPS
(COALESCE($2::DATE, NOW() - INTERVAL '1
const getBookingByID = `-- name: month'), COALESCE($3::DATE, NOW()))
GetBookingByID :one `
SELECT id, start_date, end_date, apartment_id,
user_id FROM bookings WHERE id = $1 type ListApartmentBookingsParams struct {
` ApartmentID uuid.UUID `db:"apartment_id"
json:"apartment_id"`
type GetBookingByIDRow struct { FromDate pgtype.Date `db:"from_date"
ID uuid.UUID `db:"id" json:"id"` json:"from_date"`
StartDate time.Time `db:"start_date" ToDate pgtype.Date `db:"to_date"
json:"start_date"` json:"to_date"`
EndDate time.Time `db:"end_date" }
json:"end_date"`
ApartmentID uuid.UUID `db:"apartment_id" func (q *Queries) ListApartmentBookings(ctx
74
context.Context, arg ListApartmentBookingsParams) func (q *Queries) ListUsersBookings(ctx
([]Booking, error) { context.Context, clerkID uuid.UUID) ([]Booking, error) {
rows, err := q.db.Query(ctx, rows, err := q.db.Query(ctx, listUsersBookings,
listApartmentBookings, arg.ApartmentID, arg.FromDate, clerkID)
arg.ToDate) if err != nil {
if err != nil { return nil, err
return nil, err }
} defer rows.Close()
defer rows.Close() var items []Booking
var items []Booking for rows.Next() {
for rows.Next() { var i Booking
var i Booking if err := rows.Scan(
if err := rows.Scan( &i.ID,
&i.ID, &i.CreatedAt,
&i.CreatedAt, &i.UpdatedAt,
&i.UpdatedAt, &i.StartDate,
&i.StartDate, &i.EndDate,
&i.EndDate, &i.ApartmentID,
&i.ApartmentID, &i.UserID,
&i.UserID, ); err != nil {
); err != nil { return nil, err
return nil, err }
} items = append(items, i)
items = append(items, i) }
} if err := rows.Err(); err != nil {
if err := rows.Err(); err != nil { return nil, err
return nil, err }
} return items, nil
return items, nil }
}
const updateBookingByID = `-- name:
const listUsersBookings = `-- name: UpdateBookingByID :one
ListUsersBookings :many UPDATE bookings
SELECT bookings.id, bookings.created_at, SET start_date = $2, end_date = $3
bookings.updated_at, bookings.start_date, FROM users
bookings.end_date, bookings.apartment_id, WHERE bookings.id = $1
bookings.user_id FROM bookings AND users.clerk_id = $4::UUID AND
JOIN users apartments.owner_id = users.id
ON bookings.user_id = users.id RETURNING bookings.id, bookings.created_at,
WHERE users.clerk_id = $1::UUID bookings.updated_at, bookings.start_date,
` bookings.end_date, bookings.apartment_id,
bookings.user_id
75
` import (
"context"
type UpdateBookingByIDParams struct {
ID uuid.UUID `db:"id" json:"id"` "github.com/jackc/pgx/v5"
StartDate time.Time `db:"start_date" "github.com/jackc/pgx/v5/pgconn"
json:"start_date"` )
EndDate time.Time `db:"end_date"
json:"end_date"` type DBTX interface {
ClerkID uuid.UUID `db:"clerk_id" Exec(context.Context, string, ...interface{})
json:"clerk_id"` (pgconn.CommandTag, error)
} Query(context.Context, string, ...interface{})
(pgx.Rows, error)
func (q *Queries) UpdateBookingByID(ctx QueryRow(context.Context, string, ...interface{})
context.Context, arg UpdateBookingByIDParams) pgx.Row
(Booking, error) { }
row := q.db.QueryRow(ctx, updateBookingByID,
arg.ID, func New(db DBTX) *Queries {
arg.StartDate, return &Queries{db: db}
arg.EndDate, }
arg.ClerkID,
) type Queries struct {
var i Booking db DBTX
err := row.Scan( }
&i.ID,
&i.CreatedAt, func (q *Queries) WithTx(tx pgx.Tx) *Queries {
&i.UpdatedAt, return &Queries{
&i.StartDate, db: tx,
&i.EndDate, }
&i.ApartmentID, }
&i.UserID, models.go
)
return i, err // Code generated by sqlc. DO NOT EDIT.
} // versions:
// sqlc v1.28.0
db.go
package repository
// Code generated by sqlc. DO NOT EDIT.
// versions: import (
// sqlc v1.28.0 "time"
package repository "github.com/google/uuid"
)
76
json:"apartment_id"`
type Apartment struct { }
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" type Booking struct {
json:"created_at"` ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" CreatedAt time.Time `db:"created_at"
json:"updated_at"` json:"created_at"`
Name string `db:"name" json:"name"` UpdatedAt time.Time `db:"updated_at"
Description string `db:"description" json:"updated_at"`
json:"description"` StartDate time.Time `db:"start_date"
Address string `db:"address" json:"address"` json:"start_date"`
ImageUrls []string `db:"image_urls" EndDate time.Time `db:"end_date"
json:"image_urls"` json:"end_date"`
MaxGuests int32 `db:"max_guests" ApartmentID uuid.UUID `db:"apartment_id"
json:"max_guests"` json:"apartment_id"`
OwnerID uuid.UUID `db:"owner_id" UserID uuid.UUID `db:"user_id"
json:"owner_id"` json:"user_id"`
} }
type ApartmentComment struct { src/app/page.tsx
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" import Link from "next/link";
json:"created_at"` import { Header } from "@/components/header";
UpdatedAt time.Time `db:"updated_at" import SearchForm from "@/components/search-
json:"updated_at"` form";
Content string `db:"content" json:"content"`
UserID uuid.UUID `db:"user_id" export default function Page() {
json:"user_id"` return (
ApartmentID uuid.UUID `db:"apartment_id" <div className="flex min-h-screen flex-col
json:"apartment_id"` pl-6">
} <Header />
<main className="flex-1">
type ApartmentRating struct { <section className="from-background to-
ID uuid.UUID `db:"id" json:"id"` muted relative bg-gradient-to-b">
CreatedAt time.Time `db:"created_at" <div className="container flex flex-col
json:"created_at"` items-center justify-center space-y-6 py-16 text-center
UpdatedAt time.Time `db:"updated_at" md:py-24">
json:"updated_at"` <h1 className="text-3xl font-bold
Rating int16 `db:"rating" json:"rating"` tracking-tighter sm:text-4xl md:text-5xl lg:text-6xl">
UserID uuid.UUID `db:"user_id" Find Your Perfect <span
json:"user_id"` className="text-primary">Apartment</span>
ApartmentID uuid.UUID `db:"apartment_id" </h1>
77
<p className="text-muted-foreground rounded-lg border p-6 shadow-sm">
mx-auto max-w-[700px] md:text-xl"> <h3 className="text-xl font-
Browse thousands of apartments and find bold">Verified Listings</h3>
your new home with ease. <p className="text-muted-foreground
</p> mt-2">
<div className="mt-6 w-full max-w- All our listings are verified to ensure
3xl"> you get exactly what
<SearchForm /> you see.
</div> </p>
</div> </div>
</section> </div>
<section className="py-12 md:py-24"> </div>
<div className="container"> </section>
<h2 className="mb-8 text-center text-2xl </main>
font-bold tracking-tighter md:text-3xl"> <footer className="border-t py-6 md:py-8">
Why Choose Us <div className="container flex flex-col
</h2> items-center justify-between gap-4 md:flex-row">
<div className="grid gap-6 md:grid- <div className="flex items-center gap-2
cols-2 lg:grid-cols-3"> text-lg font-bold">
<div className="bg-background <span className="text-
rounded-lg border p-6 shadow-sm"> primary">Apartment</span>
<h3 className="text-xl font- <span>Finder</span>
bold">Wide Selection</h3> </div>
<p className="text-muted-foreground <p className="text-muted-foreground
mt-2"> text-center text-sm">
Browse thousands of apartments © {new Date().getFullYear()}
across different locations, ApartmentFinder. All rights reserved.
sizes, and price ranges. </p>
</p> <div className="flex gap-4">
</div> <Link
<div className="bg-background href="/about"
rounded-lg border p-6 shadow-sm"> className="text-muted-foreground
<h3 className="text-xl font- hover:text-foreground text-sm"
bold">Easy Booking</h3> >
<p className="text-muted-foreground About
mt-2"> </Link>
Book your apartment with just a few <Link
clicks and secure your new href="/contact"
home instantly. className="text-muted-foreground
</p> hover:text-foreground text-sm"
</div> >
<div className="bg-background Contact
78
</Link> <Link
<Link href="/"
href="/terms" className="flex items-center gap-2 text-
className="text-muted-foreground xl font-bold"
hover:text-foreground text-sm" >
> <span className="text-
Terms primary">Apartment</span>
</Link> <span className="text-
<Link secondary">Finder</span>
href="/privacy" </Link>
className="text-muted-foreground </div>
hover:text-foreground text-sm" <nav className="hidden items-center gap-
> 6 md:flex">
Privacy <Link
</Link> href="/listings"
</div> className="hover:text-primary text-sm
</div> font-medium transition-colors"
</footer> >
</div> Browse Apartments
); </Link>
} <Link href="/about" className="text-
primary text-sm font-medium">
src/app/about/page.tsx About Us
</Link>
import Image from "next/image"; <Link
import Link from "next/link"; href="/contact"
import { Users, Home, Shield, Award, Clock, className="hover:text-primary text-sm
MapPin } from "lucide-react"; font-medium transition-colors"
>
import { Button } from Contact
"@/components/ui/button"; </Link>
import { MobileNav } from </nav>
"@/components/mobile-nav"; <div className="flex items-center gap-4">
<Link
export default function Page() { href="/login"
return ( className="hover:text-primary hidden
<div className="flex min-h-screen flex-col"> text-sm font-medium md:inline-flex"
<header className="border-b"> >
<div className="container flex h-16 items- Log in
center justify-between py-4"> </Link>
<div className="flex items-center gap-2"> <Button asChild>
<MobileNav /> <Link href="/signup">Sign up</Link>
79
</Button> Us</Link>
</div> </Button>
</div> </div>
</header> </div>
<main className="flex-1"> </div>
<section className="relative overflow- </section>
hidden py-20 md:py-32"> <section className="bg-muted/30 py-16
<div className="absolute inset-0 z-0"> md:py-24">
<Image <div className="container">
<div className="grid items-center gap-
src="/placeholder.svg?height=1080&width=1920" 12 md:grid-cols-2">
alt="Modern apartment building" <div>
fill <h2 className="mb-6 text-2xl font-
className="object-cover" bold md:text-3xl">
priority Our Story
/> </h2>
<div className="from-background/90 to- <div className="text-muted-
background/70 absolute inset-0 bg-gradient-to-r" /> foreground space-y-4">
</div> <p>
<div className="relative z-10 container"> Founded in 2020, ApartmentFinder
<div className="max-w-2xl"> was born from a simple
<h1 className="mb-6 text-3xl font-bold idea: finding a place to live should be
tracking-tight md:text-5xl"> exciting, not
Connecting People with Their Perfect exhausting. Our founders
Homes experienced firsthand the
</h1> challenges of apartment hunting –
<p className="text-muted-foreground from misleading listings
mb-8 text-lg md:text-xl"> to complicated booking processes.
ApartmentFinder is on a mission to </p>
make finding and booking <p>
apartments a seamless, enjoyable We set out to create a platform that
experience for everyone. brings transparency,
</p> trust, and simplicity to the apartment
<div className="flex flex-wrap gap-4"> rental process. What
<Button asChild size="lg" started as a small team with big
className="btn-primary"> dreams has grown into a
<Link href="/listings">Browse trusted platform connecting
Apartments</Link> thousands of people with their
</Button> perfect homes.
<Button asChild variant="outline" </p>
size="lg"> <p>
<Link href="/contact">Contact Today, we're proud to offer a curated
80
selection of quality <div className="feature-icon mb-4">
apartments across major cities, with <Shield className="h-6 w-6" />
verified listings, </div>
seamless booking, and exceptional <h3 className="mb-2 text-xl font-
customer support at every semibold">
step of your journey. Trust & Transparency
</p> </h3>
</div> <p className="text-muted-
</div> foreground">
<div className="relative h-[400px] We verify all listings and provide
overflow-hidden rounded-lg shadow-lg"> honest, detailed
<Image information so you can make confident
decisions.
src="/placeholder.svg?height=800&width=600" </p>
alt="Our team" </div>
fill
className="object-cover" <div className="bg-card rounded-lg
/> border p-6 shadow-sm">
</div> <div className="feature-icon-
</div> secondary mb-4">
</div> <Users className="h-6 w-6" />
</section> </div>
<section className="py-16 md:py-24"> <h3 className="mb-2 text-xl font-
<div className="container"> semibold">Customer First</h3>
<div className="mb-16 text-center"> <p className="text-muted-
<h2 className="mb-4 text-2xl font-bold foreground">
md:text-3xl"> Your satisfaction drives everything we
Our Values do. We're committed to
</h2> providing exceptional service at every
<p className="text-muted-foreground step.
mx-auto max-w-2xl"> </p>
These core principles guide everything </div>
we do, from how we build
our platform to how we interact with <div className="bg-card rounded-lg
our customers and partners. border p-6 shadow-sm">
</p> <div className="feature-icon-accent
</div> mb-4">
<Home className="h-6 w-6" />
<div className="grid gap-8 sm:grid-cols- </div>
2 lg:grid-cols-3"> <h3 className="mb-2 text-xl font-
<div className="bg-card rounded-lg semibold">Quality Living</h3>
border p-6 shadow-sm"> <p className="text-muted-
81
foreground"> <Award className="h-6 w-6" />
We believe everyone deserves a great </div>
place to live, and we're <h3 className="mb-2 text-xl font-
dedicated to making that possible. semibold">Innovation</h3>
</p> <p className="text-muted-
</div> foreground">
We continuously improve our platform
<div className="bg-card rounded-lg with new features and
border p-6 shadow-sm"> technologies to enhance your
<div className="feature-icon mb-4"> experience.
<Clock className="h-6 w-6" /> </p>
</div> </div>
<h3 className="mb-2 text-xl font- </div>
semibold">Efficiency</h3> </div>
<p className="text-muted- </section>
foreground"> <section className="bg-muted/30 py-16
We streamline the apartment hunting md:py-24">
process, saving you time <div className="container">
and reducing stress. <div className="mb-16 text-center">
</p> <h2 className="mb-4 text-2xl font-bold
</div> md:text-3xl">
Meet Our Team
<div className="bg-card rounded-lg </h2>
border p-6 shadow-sm"> <p className="text-muted-foreground
<div className="feature-icon- mx-auto max-w-2xl">
secondary mb-4"> The passionate people behind
<MapPin className="h-6 w-6" /> ApartmentFinder who are dedicated
</div> to transforming how you find your next
<h3 className="mb-2 text-xl font- home.
semibold">Local Expertise</h3> </p>
<p className="text-muted- </div>
foreground"> <div className="grid gap-8 sm:grid-cols-
Our team has deep knowledge of local 2 lg:grid-cols-4">
markets to help you find {[
the perfect neighborhood. {
</p> name: "Sarah Johnson",
</div> role: "Founder & CEO",
image:
<div className="bg-card rounded-lg "/placeholder.svg?height=300&width=300",
border p-6 shadow-sm"> },
<div className="feature-icon-accent {
mb-4"> name: "Michael Chen",
82
role: "CTO", <div className="text-center">
image: <div className="text-primary mb-2
"/placeholder.svg?height=300&width=300", text-4xl font-bold">
}, 10,000+
{ </div>
name: "Emily Rodriguez", <p className="text-muted-
role: "Head of Operations", foreground">Apartments Listed</p>
image: </div>
"/placeholder.svg?height=300&width=300", <div className="text-center">
}, <div className="text-primary mb-2
{ text-4xl font-bold">50+</div>
name: "David Kim", <p className="text-muted-
role: "Lead Designer", foreground">Cities Covered</p>
image: </div>
"/placeholder.svg?height=300&width=300", <div className="text-center">
}, <div className="text-primary mb-2
].map((member, index) => ( text-4xl font-bold">
<div key={index} className="text- 25,000+
center"> </div>
<div className="relative mx-auto <p className="text-muted-
mb-4 h-48 w-48 overflow-hidden rounded-full"> foreground">Happy Customers</p>
<Image </div>
src={member.image || <div className="text-center">
"/placeholder.svg"} <div className="text-primary mb-2
alt={member.name} text-4xl font-bold">4.9</div>
fill <p className="text-muted-
className="object-cover" foreground">Average Rating</p>
/> </div>
</div> </div>
<h3 className="text-lg font- </div>
semibold">{member.name}</h3> </section>
<p className="text-muted- <section className="bg-primary text-
foreground">{member.role}</p> primary-foreground py-16 md:py-24">
</div> <div className="container text-center">
))} <h2 className="mb-4 text-2xl font-bold
</div> md:text-3xl">
</div> Ready to Find Your Perfect Apartment?
</section> </h2>
<section className="py-16 md:py-24"> <p className="text-primary-
<div className="container"> foreground/80 mx-auto mb-8 max-w-2xl">
<div className="grid gap-8 sm:grid-cols- Join thousands of satisfied customers
2 lg:grid-cols-4"> who have found their dream
83
homes through ApartmentFinder. className="text-secondary-100
</p> transition-colors hover:text-white"
<Button asChild size="lg" >
variant="secondary"> About Us
<Link href="/listings">Start Browsing </Link>
Now</Link> </li>
</Button> <li>
</div> <Link
</section> href="/contact"
</main> className="text-secondary-100
<footer className="bg-secondary-900 py-12 transition-colors hover:text-white"
text-white"> >
<div className="container"> Contact
<div className="grid grid-cols-1 gap-8 </Link>
md:grid-cols-4"> </li>
<div> </ul>
<h3 className="mb-4 text-lg font-bold </div>
text-white"> <div>
ApartmentFinder <h3 className="mb-4 text-lg font-bold
</h3> text-white">Legal</h3>
<p className="text-secondary-100"> <ul className="space-y-2">
Find your perfect apartment with ease. <li>
Browse thousands of <Link
verified listings. href="/terms"
</p> className="text-secondary-100
</div> transition-colors hover:text-white"
<div> >
<h3 className="mb-4 text-lg font-bold Terms of Service
text-white">Quick Links</h3> </Link>
<ul className="space-y-2"> </li>
<li> <li>
<Link <Link
href="/listings" href="/privacy"
className="text-secondary-100 className="text-secondary-100
transition-colors hover:text-white" transition-colors hover:text-white"
> >
Browse Apartments Privacy Policy
</Link> </Link>
</li> </li>
<li> <li>
<Link <Link
href="/about" href="/cookies"
84
className="text-secondary-100
transition-colors hover:text-white"
>
Cookie Policy
</Link>
</li>
</ul>
</div>
<div>
<h3 className="mb-4 text-lg font-bold
text-white">Newsletter</h3>
<p className="text-secondary-100 mb-
4">
Subscribe to our newsletter for the latest
updates and offers.
</p>
<form className="flex gap-2">
<input
type="email"
placeholder="Your email"
className="bg-secondary-800
border-secondary-700 placeholder:text-secondary-400
focus:border-primary-400 max-w-[220px] rounded-md p-2
text-white"
/>
<Button type="submit"
className="btn-primary">
Subscribe
</Button>
</form>
</div>
</div>
<div className="border-secondary-700
text-secondary-300 mt-8 border-t pt-8 text-center">
<p>© 2023 ApartmentFinder. All rights
reserved.</p>
</div>
</div>
</footer>
</div>
);
}