From f1b660744ad18b55669476ca7a6d300788f242a8 Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Tue, 6 May 2025 12:41:38 +0300 Subject: [PATCH 01/54] delete README.md --- README.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index 8b13789..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ - From 763ffb6bf8c64f8b1cb5052405d8dbe6f92e3779 Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Tue, 6 May 2025 13:46:17 +0300 Subject: [PATCH 02/54] =?UTF-8?q?=D1=81reate=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..683892e --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# Описание проекта + + **Cardly** - это сервис для обмена коллекционными карточками и наборами, включая карточки с изображениями животных, пейзажей и т.д. + +--- + +## Команда (ТП 5-1) + + +- **[Доброва Анна](https://github.com/dobrayAnika)** - *Team Lead, PM* +- **[Чершнев Евгений](https://github.com/floyzzzy)** - *Аналитик* +- **[Григорьев Иван](https://github.com/ChipoDev)** - *Backend-разработчик, Frontend-разработчик* +- **[Папонов Данил](https://github.com/danil13231212341)** - *Дизайнер* +- **[Бирюков Дмитрий](https://github.com/birbik)** - *Тестировщик* +- **[Наумов Никита](https://github.com/capti)** - *DevOps* + +--- + +## Сервисы + +- Miro [Roadmap](https://miro.com/app/board/uXjVINPrEUY=/) +- Figma [User paths](https://www.figma.com/board/s0O3zvAPgI4DXJF2BDwrAS/user-paths?node-id=0-1&p=f&t=3ufcMhcMNEwCXd1o-0) +- Figma [UI-kit](https://www.figma.com/design/JEGceh2Gm2ZW494FGIaT0A/Cardly-Brandbook?node-id=44-59&p=f&t=d6eTvzQ74P8yNRGe-0) | [Design](https://www.figma.com/design/ljUhVgNlQLElUQvEOcWSBz/Makets?node-id=0-1&t=3v5P2D8Ki25GG22M-1) | [BrandBook](https://www.figma.com/design/JEGceh2Gm2ZW494FGIaT0A/Cardly-Brandbook?node-id=0-1&t=OAp4Ihb40HiQLx4m-1) +- Jira [Jira-Task Manager](https://id.atlassian.com/invite/p/jira-software?id=QN1WxX0URg-5Gy9WJ0o66w) + +--- + +## Документация + +- Техническое задание [PDF](https://github.com/capti/Cardly/blob/main/Documentation/%D0%A2%D0%B5%D1%85%D0%BD%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5%20%D0%B7%D0%B0%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5.pdf) | [DOCX](https://github.com/capti/Cardly/blob/main/Documentation/%D0%A2%D0%B5%D1%85%D0%BD%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5%20%D0%B7%D0%B0%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5.docx) +- Roadmap [PDF](https://github.com/capti/Cardly/blob/main/Documentation/roadmap.pdf) | [DOCX](https://github.com/capti/Cardly/blob/main/Documentation/roadmap.docx) +- Целевая аудитория и рынок [PDF](https://github.com/capti/Cardly/blob/main/Documentation/%D0%A6%D0%B5%D0%BB%D0%B5%D0%B2%D0%B0%D1%8F%20%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D1%8F%20%D0%B8%20%D1%80%D1%8B%D0%BD%D0%BE%D0%BA.pdf) | [DOCX](https://github.com/capti/Cardly/blob/main/Documentation/%D0%A6%D0%B5%D0%BB%D0%B5%D0%B2%D0%B0%D1%8F%20%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D1%8F%20%D0%B8%20%D1%80%D1%8B%D0%BD%D0%BE%D0%BA.docx) +- Ограничения проекта [PDF](https://github.com/capti/Cardly/blob/main/Documentation/%D0%9E%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B0.pdf) | [DOCX](https://github.com/capti/Cardly/blob/main/Documentation/%D0%9E%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B0.docx) +- SWOT-анализ [PDF](https://github.com/capti/Cardly/blob/main/Documentation/SWOT-%D0%B0%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7.pdf) | [DOCX](https://github.com/capti/Cardly/blob/main/Documentation/SWOT-%D0%B0%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7.docx) +- Технические риски [PDF](https://github.com/capti/Cardly/blob/main/Documentation/%D0%A2%D0%B5%D1%85%D0%BD%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B5%20%D1%80%D0%B8%D1%81%D0%BA%D0%B8.pdf) | [DOCX](https://github.com/capti/Cardly/blob/main/Documentation/%D0%A2%D0%B5%D1%85%D0%BD%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B5%20%D1%80%D0%B8%D1%81%D0%BA%D0%B8.docx) +- BrandBook [PDF](https://github.com/capti/Cardly/blob/main/Documentation/brandbook.pdf) +- [Финансовая модель](https://github.com/capti/Cardly/tree/main/Documentation/Financial%20model) +- [Диаграммы](https://github.com/capti/Cardly/tree/main/Documentation/Diagrams) +- [Пользовательские пути](https://www.figma.com/board/s0O3zvAPgI4DXJF2BDwrAS/user-scenario?node-id=0-1&t=WPJXqXWfhTH9b5QX-1) +- [Swagger API](https://github.com/capti/Cardly/blob/main/Documentation/Swagger/cardly.yaml) + + + +--- + +## Презентация проекта + +- [Презентация технического задания - 1 аттестация](https://github.com/capti/Cardly/blob/main/Documentation/%D0%9F%D1%80%D0%B5%D0%B7%D0%B5%D0%BD%D1%82%D0%B0%D1%86%D0%B8%D1%8F.pdf) +- [Презентация технического задания - 2 аттестация](https://github.com/capti/Cardly/blob/main/Documentation/%D0%9F%D1%80%D0%B5%D0%B7%D0%B5%D0%BD%D1%82%D0%B0%D1%86%D0%B8%D1%8F%202%20%D1%8D%D1%82%D0%B0%D0%BF.pdf) + +## Видео защиты проекта +- [Видеопрезентация технического задания RUTUB](https://rutube.ru/video/private/495ed0d28afb267b57f242186af0053f/?p=t5f3gbQUlFE5zIx2hcnJZQ) +- [Видеопрезентация MVP RUTUB](https://rutube.ru/video/private/abbadde6a376d9174a7dde41d90ad139/?p=KPUl4SrlU9Tk7hcOFwXyZQ) + +--- + +## Проверка + +**Мы оценили** + +- [Наш чек-лист 1](https://github.com/capti/Cardly/blob/main/Documentation/%D0%A4%D0%B8%D0%B4%D0%B1%D1%8D%D0%BA.pdf) +- [Наш чек-лист 2](https://github.com/capti/Cardly/blob/main/Documentation/%D0%A2%D0%9F.%20%D0%A7%D0%B5%D0%BA%D0%BB%D0%B8%D1%81%D1%82%202%20%D1%8D%D1%82%D0%B0%D0%BF.pdf) + +**Нас оценили** + +- [Команда 5-3. 1 аттестация](https://github.com/TrefflyTeam/documentation/blob/main/%D0%A0%D0%B5%D0%B7%D1%83%D0%BB%D1%8C%D1%82%D0%B0%D1%82%D1%8B%20%D0%BE%D1%86%D0%B5%D0%BD%D0%B8%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F%20%D0%BE%D1%82%20%D0%BA%D0%BE%D0%BC%D0%B0%D0%BD%D0%B4%D1%8B%205.3.pdf) +- [Команда 5-4. 1 аттестация](https://github.com/TP-Jobsy/jobsy-docs/blob/main/%D0%A7%D0%B5%D0%BA%D0%BB%D0%B8%D1%81%D1%82%201%20%D1%8D%D1%82%D0%B0%D0%BF.pdf) | +- [Команда 6-1. 1 аттестация](https://gitlab.usr0.ru/tailoredtastes/tailoredtastes-documentation/-/tree/master/%D0%9A%D1%80%D0%BE%D1%81%D1%81-%D0%BF%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%BA%D0%B0?ref_type=heads) | [Команда 6-1. 2 аттестация](https://gitlab.usr0.ru/tailoredtastes/tailoredtastes-documentation/-/blob/master/%D0%9A%D1%80%D0%BE%D1%81%D1%81-%D0%BF%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%BA%D0%B0/%D0%9A%D1%80%D0%BE%D1%81%D1%81-%D0%BF%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%BA%D0%B0%20%D0%A2%D0%9F%206.1%202%20%D1%8D%D1%82%D0%B0%D0%BF.pdf) +- [Команда 6-2. 1 аттестация](https://github.com/AlexanderLaptev/Taskbench/blob/main/docs/%D0%A7%D0%B5%D0%BA%D0%BB%D0%B8%D1%81%D1%82%201%20%D1%8D%D1%82%D0%B0%D0%BF.pdf) | [Команда 6-2. 2 аттестация](https://github.com/AlexanderLaptev/Taskbench/blob/main/docs/%D0%A7%D0%B5%D0%BA%D0%BB%D0%B8%D1%81%D1%82%202%20%D1%8D%D1%82%D0%B0%D0%BF.pdf) From bc16c5e5ae6f6aa89963c75acd37ac4d0599a8ef Mon Sep 17 00:00:00 2001 From: birbik Date: Fri, 16 May 2025 11:53:38 +0300 Subject: [PATCH 03/54] FCCX-90 add card_model, update api_service --- frontend/lib/models/card_model.dart | 51 ++++++++ frontend/lib/services/api_service.dart | 174 +++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 frontend/lib/models/card_model.dart diff --git a/frontend/lib/models/card_model.dart b/frontend/lib/models/card_model.dart new file mode 100644 index 0000000..b63233e --- /dev/null +++ b/frontend/lib/models/card_model.dart @@ -0,0 +1,51 @@ +class CardModel { + final int id; + final String name; + final String description; + final String imageUrl; + final int rarity; + final String collection; + final String type; + final int disassemblePrice; + final int quantity; + + CardModel({ + required this.id, + required this.name, + required this.description, + required this.imageUrl, + required this.rarity, + required this.collection, + required this.type, + required this.disassemblePrice, + required this.quantity, + }); + + factory CardModel.fromJson(Map json) { + return CardModel( + id: json['id'] as int, + name: json['name'] as String, + description: json['description'] as String, + imageUrl: json['imageUrl'] as String, + rarity: json['rarity'] as int, + collection: json['collection'] as String, + type: json['type'] as String, + disassemblePrice: json['disassemblePrice'] as int, + quantity: json['quantity'] as int? ?? 1, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'description': description, + 'imageUrl': imageUrl, + 'rarity': rarity, + 'collection': collection, + 'type': type, + 'disassemblePrice': disassemblePrice, + 'quantity': quantity, + }; + } +} \ No newline at end of file diff --git a/frontend/lib/services/api_service.dart b/frontend/lib/services/api_service.dart index 9818f4c..2cb6c35 100644 --- a/frontend/lib/services/api_service.dart +++ b/frontend/lib/services/api_service.dart @@ -2,6 +2,8 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import '../models/user_model.dart'; +import '../models/card_model.dart'; +import '../models/shop_model.dart'; class ApiService { static const String baseUrl = 'http://87.236.23.130:8080/api'; @@ -12,6 +14,9 @@ class ApiService { static const String forgotPasswordUrl = '$baseUrl/auth/forgot-password'; static const String resetPasswordUrl = '$baseUrl/auth/reset-password'; + // Карты + static const String cardsUrl = '$baseUrl/cards'; + static const String shopUrl = '$baseUrl/shop'; Future> getHeaders() async { final token = await getSavedToken(); @@ -304,4 +309,173 @@ class ApiService { throw Exception('Ошибка сети: ${e.toString()}'); } } + + // Карты + Future> getUserInventory({String sortBy = 'rarity'}) async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$cardsUrl/inventory?sortBy=$sortBy'), + headers: headers, + ); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => CardModel.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении инвентаря: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future getCardDetails(int cardId) async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$cardsUrl/$cardId'), + headers: headers, + ); + + if (response.statusCode == 200) { + return CardModel.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при получении деталей карты: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future disassembleCard(int cardId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$cardsUrl/$cardId/disassemble'), + headers: headers, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return data['coinsReceived'] as int; + } else { + throw Exception('Ошибка при разборе карты: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + // Магазин + Future> getAllPacks() async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$shopUrl/packs'), + headers: headers, + ); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => PackModel.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении списка наборов: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future getPackDetails(int packId) async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$shopUrl/packs/$packId'), + headers: headers, + ); + + if (response.statusCode == 200) { + return PackModel.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при получении деталей набора: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future buyPack(int packId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$shopUrl/packs/$packId/buy'), + headers: headers, + ); + + if (response.statusCode == 200) { + return PurchasePackResponse.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при покупке набора: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> getAllCoinOffers() async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$shopUrl/coins/offers'), + headers: headers, + ); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => CoinOfferModel.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении списка предложений монет: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future getCoinOfferDetails(int offerId) async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$shopUrl/coins/offers/$offerId'), + headers: headers, + ); + + if (response.statusCode == 200) { + return CoinOfferModel.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при получении деталей предложения монет: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future purchaseCoins(int offerId, String redirectUrl) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$shopUrl/coins/offers/$offerId/purchase'), + headers: headers, + body: json.encode({'redirectUrl': redirectUrl}), + ); + + if (response.statusCode == 200) { + return PurchaseCoinsResponse.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при покупке монет: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } } \ No newline at end of file From f4886ef00da348f3d6f2d51fc7ddc3800ff7b75c Mon Sep 17 00:00:00 2001 From: birbik Date: Fri, 16 May 2025 11:54:41 +0300 Subject: [PATCH 04/54] FCCX-73 add shop_model --- frontend/lib/models/shop_model.dart | 112 ++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 frontend/lib/models/shop_model.dart diff --git a/frontend/lib/models/shop_model.dart b/frontend/lib/models/shop_model.dart new file mode 100644 index 0000000..a99bd30 --- /dev/null +++ b/frontend/lib/models/shop_model.dart @@ -0,0 +1,112 @@ +import 'card_model.dart'; + +class PackModel { + final int id; + final String name; + final String imageUrl; + final int price; + final List cards; + + PackModel({ + required this.id, + required this.name, + required this.imageUrl, + required this.price, + required this.cards, + }); + + factory PackModel.fromJson(Map json) { + return PackModel( + id: json['id'] as int, + name: json['name'] as String, + imageUrl: json['imageUrl'] as String, + price: json['price'] as int, + cards: (json['cards'] as List) + .map((card) => CardModel.fromJson(card)) + .toList(), + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'imageUrl': imageUrl, + 'price': price, + 'cards': cards.map((card) => card.toJson()).toList(), + }; + } +} + +class CoinOfferModel { + final int id; + final String name; + final String? description; + final int coinsAmount; + final double price; + final String imageUrl; + + CoinOfferModel({ + required this.id, + required this.name, + this.description, + required this.coinsAmount, + required this.price, + required this.imageUrl, + }); + + factory CoinOfferModel.fromJson(Map json) { + return CoinOfferModel( + id: json['id'] as int, + name: json['name'] as String, + description: json['description'] as String?, + coinsAmount: json['coinsAmount'] as int, + price: json['price'] as double, + imageUrl: json['imageUrl'] as String, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'description': description, + 'coinsAmount': coinsAmount, + 'price': price, + 'imageUrl': imageUrl, + }; + } +} + +class PurchasePackResponse { + final List receivedCards; + final int newBalance; + + PurchasePackResponse({ + required this.receivedCards, + required this.newBalance, + }); + + factory PurchasePackResponse.fromJson(Map json) { + return PurchasePackResponse( + receivedCards: (json['receivedCards'] as List) + .map((card) => CardModel.fromJson(card)) + .toList(), + newBalance: json['newBalance'] as int, + ); + } +} + +class PurchaseCoinsResponse { + final String paymentUrl; + + PurchaseCoinsResponse({ + required this.paymentUrl, + }); + + factory PurchaseCoinsResponse.fromJson(Map json) { + return PurchaseCoinsResponse( + paymentUrl: json['paymentUrl'] as String, + ); + } +} \ No newline at end of file From 1638081773c99a4031351cf097a9cadca44302ff Mon Sep 17 00:00:00 2001 From: birbik Date: Fri, 16 May 2025 11:55:33 +0300 Subject: [PATCH 05/54] FCCX-91 update profile_screen --- frontend/lib/views/profile_screen.dart | 295 +++++++++++++++---------- 1 file changed, 184 insertions(+), 111 deletions(-) diff --git a/frontend/lib/views/profile_screen.dart b/frontend/lib/views/profile_screen.dart index 18f25e6..f5796ec 100644 --- a/frontend/lib/views/profile_screen.dart +++ b/frontend/lib/views/profile_screen.dart @@ -3,7 +3,7 @@ import 'home_screen.dart'; import 'shop_screen.dart'; import 'exchanges_screen.dart'; import 'inventory_screen.dart'; -import 'settings_screen.dart'; +import 'settings_screen.dart' show SettingsDialog; class ProfileScreen extends StatelessWidget { const ProfileScreen({super.key}); @@ -15,14 +15,7 @@ class ProfileScreen extends StatelessWidget { appBar: AppBar( backgroundColor: const Color(0xFFFFF4E3), elevation: 0, - title: const Text( - 'Профиль игрока', - style: TextStyle( - color: Colors.black45, - fontSize: 16.0, - fontWeight: FontWeight.w400, - ), - ), + title: null, centerTitle: true, leading: Padding( padding: const EdgeInsets.only(left: 16.0), @@ -41,7 +34,7 @@ class ProfileScreen extends StatelessWidget { child: const Icon( Icons.arrow_back, color: Colors.black, - size: 22.0, + size: 29.0, ), ), ), @@ -52,16 +45,13 @@ class ProfileScreen extends StatelessWidget { child: Container( width: 40.0, height: 40.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 2), - ), child: InkWell( borderRadius: BorderRadius.circular(20.0), onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SettingsScreen()), + showDialog( + context: context, + barrierColor: Colors.black.withOpacity(0.2), + builder: (context) => const SettingsDialog(), ); }, child: Image.asset( @@ -80,23 +70,23 @@ class ProfileScreen extends StatelessWidget { // Аватар пользователя Container( - width: 100.0, - height: 100.0, + width: 140.0, + height: 140.0, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Colors.black, width: 2), ), child: const CircleAvatar( - backgroundColor: Colors.white, + backgroundColor: const Color(0xFFFFF4E3), child: Icon( Icons.person_outline, - size: 60.0, + size: 90.0, color: Colors.black, ), ), ), - const SizedBox(height: 12.0), + const SizedBox(height: 16.0), // Имя пользователя const Text( @@ -118,7 +108,7 @@ class ProfileScreen extends StatelessWidget { ), ), - const SizedBox(height: 20.0), + const SizedBox(height: 16.0), // Коллекция карточек (5 карточек в ряд) Padding( @@ -132,39 +122,51 @@ class ProfileScreen extends StatelessWidget { ), ), - const SizedBox(height: 20.0), + const SizedBox(height: 15.0), - // Достижения - Container( - margin: const EdgeInsets.symmetric(horizontal: 16.0), - padding: const EdgeInsets.symmetric(vertical: 16.0), - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildAchievement(), - _buildAchievement(), - _buildAchievement(), - _buildAchievement(), - Container( - padding: const EdgeInsets.all(8.0), - alignment: Alignment.center, - child: const Text( - 'Все достижения →', - style: TextStyle( - fontSize: 12.0, - fontWeight: FontWeight.bold, + // Блок достижений + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Container( + height: 130, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(8.0), + ), + child: Stack( + children: [ + Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(4, (index) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Image.asset( + 'assets/icons/достижение.png', + height: 70, + width: 70, + fit: BoxFit.contain, + ), + )), ), ), - ), - ], + const Positioned( + right: 12, + bottom: 8, + child: Text( + 'Все достижения →', + style: TextStyle( + fontSize: 13.0, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + ], + ), ), ), - const SizedBox(height: 40.0), + const SizedBox(height: 90.0), // Статистика Container( @@ -228,31 +230,27 @@ class ProfileScreen extends StatelessWidget { onTap: (index) { switch (index) { case 0: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, ); break; case 1: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const InventoryScreen()), - (route) => false, ); break; case 2: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, ); break; case 3: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, ); break; } @@ -262,22 +260,69 @@ class ProfileScreen extends StatelessWidget { type: BottomNavigationBarType.fixed, selectedItemColor: Colors.black, unselectedItemColor: Colors.black54, + showSelectedLabels: true, + showUnselectedLabels: true, + selectedIconTheme: const IconThemeData( + size: 28, + ), + unselectedIconTheme: const IconThemeData( + size: 24, + ), + selectedLabelStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + unselectedLabelStyle: const TextStyle( + fontSize: 11, + ), items: [ BottomNavigationBarItem( icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Гл.меню', + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/главная.png', height: 24), + ), + label: 'Главная', ), BottomNavigationBarItem( icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/Инвентарь.png', height: 24), + ), + label: 'Инвентарь', ), BottomNavigationBarItem( icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/магазин.png', height: 24), + ), + label: 'Магазин', ), BottomNavigationBarItem( icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/обменник.png', height: 24), + ), + label: 'Обменник', ), ], ), @@ -289,60 +334,88 @@ class ProfileScreen extends StatelessWidget { // Карточка в профиле Widget _buildCard() { - return Container( - width: 60.0, - height: 84.0, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 2), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Container( - margin: const EdgeInsets.only(top: 4.0, left: 4.0, right: 4.0), - decoration: const BoxDecoration( - color: Color(0xFFEDD6B0), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(4.0), - topRight: Radius.circular(4.0), + return SizedBox( + width: 74, + height: 112, + child: AspectRatio( + aspectRatio: 3/4, + child: Stack( + children: [ + // Внешняя тонкая черная рамка + Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + ), + // Прослойка цвета карточки + Padding( + padding: const EdgeInsets.all(2.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(7), ), ), ), - ), - Container( - height: 24.0, - margin: const EdgeInsets.only(left: 4.0, right: 4.0, bottom: 4.0), - decoration: const BoxDecoration( - color: Color(0xFFEDD6B0), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(4.0), - bottomRight: Radius.circular(4.0), + // Внутренняя тонкая черная рамка + Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.black, width: 2), + ), ), ), - ), - ], - ), - ); - } - - // Иконка достижения - Widget _buildAchievement() { - return Container( - padding: const EdgeInsets.all(8.0), - child: Column( - children: const [ - Icon( - Icons.emoji_events_outlined, - size: 24.0, - ), - Icon( - Icons.star_border, - size: 16.0, - ), - ], + // Основная карточка с отделением редкости + Padding( + padding: const EdgeInsets.all(6.0), + child: Column( + children: [ + Expanded( + flex: 8, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(5.0), + topRight: Radius.circular(5.0), + ), + ), + ), + ), + Container( + height: 3, + color: Colors.black, + ), + Container( + height: 20, + decoration: const BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(5.0), + bottomRight: Radius.circular(5.0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: List.generate(4, (i) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0), + child: Image.asset( + 'assets/icons/редкость.png', + height: 10, + ), + )), + ), + ), + ], + ), + ), + ], + ), ), ); } From 52119ef9408fcfcc985ffd15df459486231aae41 Mon Sep 17 00:00:00 2001 From: birbik Date: Fri, 16 May 2025 11:56:10 +0300 Subject: [PATCH 06/54] FCCX-92 update settings_screen --- frontend/lib/views/settings_screen.dart | 424 +++++++++++------------- 1 file changed, 202 insertions(+), 222 deletions(-) diff --git a/frontend/lib/views/settings_screen.dart b/frontend/lib/views/settings_screen.dart index 0cdae76..81fae0b 100644 --- a/frontend/lib/views/settings_screen.dart +++ b/frontend/lib/views/settings_screen.dart @@ -1,280 +1,260 @@ import 'package:flutter/material.dart'; import 'home_screen.dart'; -class SettingsScreen extends StatefulWidget { - const SettingsScreen({super.key}); +class SettingsDialog extends StatefulWidget { + const SettingsDialog({super.key}); @override - State createState() => _SettingsScreenState(); + State createState() => _SettingsDialogState(); } -class _SettingsScreenState extends State { +class _SettingsDialogState extends State { bool _notificationsEnabled = true; bool _exchangesDeclineEnabled = true; bool _inventoryDisplayEnabled = true; + String? _hintText; + bool _showHint = false; + + void _showHintMessage(String text) { + setState(() { + _hintText = text; + _showHint = true; + }); + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + setState(() { + _showHint = false; + }); + } + }); + } + @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон - body: Stack( - children: [ - // Основное содержимое - Container( - margin: const EdgeInsets.only(top: 40.0), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(12.0), - border: Border.all(color: Colors.black, width: 1), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Заголовок "Настройки" - const Padding( - padding: EdgeInsets.only(top: 16.0, left: 24.0), - child: Text( - 'Настройки', - style: TextStyle( - fontSize: 22.0, - fontWeight: FontWeight.bold, + return Center( + child: Material( + color: Colors.transparent, + child: Container( + width: MediaQuery.of(context).size.width * 0.88, + height: MediaQuery.of(context).size.height * 0.75, + decoration: BoxDecoration( + color: const Color(0xFFF6E6D0), + borderRadius: BorderRadius.circular(10.0), + border: Border.all(color: Colors.black, width: 2), + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + const Center( + child: Text( + 'Настройки', + style: TextStyle( + fontFamily: 'Roboto', + fontSize: 28.0, + fontWeight: FontWeight.w400, + ), + ), ), - ), + const SizedBox(height: 24.0), + _buildSettingItem( + title: 'Уведомления', + hasSwitch: true, + switchValue: _notificationsEnabled, + onChanged: (value) { + setState(() { + _notificationsEnabled = value; + }); + }, + ), + const SizedBox(height: 16.0), + _buildSettingItem( + title: 'Отклонение обменов', + hasSwitch: true, + switchValue: _exchangesDeclineEnabled, + hasInfo: true, + onInfoTap: () => _showHintMessage('Подсказка по отклонению обменов'), + onChanged: (value) { + setState(() { + _exchangesDeclineEnabled = value; + }); + }, + ), + const SizedBox(height: 16.0), + _buildSettingItem( + title: 'Показ инвентаря', + hasSwitch: true, + switchValue: _inventoryDisplayEnabled, + hasInfo: true, + onInfoTap: () => _showHintMessage('Подсказка по показу инвентаря'), + onChanged: (value) { + setState(() { + _inventoryDisplayEnabled = value; + }); + }, + ), + const Spacer(), + Padding( + padding: const EdgeInsets.only(bottom: 26.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 18.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + ), + child: const Text( + 'Выйти из аккаунта', + style: TextStyle( + fontFamily: 'Roboto', + fontSize: 18.0, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ], ), - - // Подсказка - Align( - alignment: Alignment.centerRight, + ), + Positioned( + top: -1, + right: -1, + child: GestureDetector( + onTap: () => Navigator.of(context).pop(), child: Container( - margin: const EdgeInsets.only(right: 16.0, top: 8.0), - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + width: 48, + height: 48, decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8.0), + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10.0), + border: Border.all(color: Colors.black, width: 2), ), - child: const Text( - 'Текст\nподсказки', - style: TextStyle( - fontSize: 12.0, - color: Colors.black87, + child: const Center( + child: Icon( + Icons.close_rounded, + color: Colors.black, + size: 48.0, ), - textAlign: TextAlign.center, ), ), ), - - const SizedBox(height: 16.0), - - // Опция "Уведомления" - _buildSettingItem( - title: 'Уведомления', - hasSwitch: false, - onTap: () { - // Действие при нажатии на опцию "Уведомления" - }, - ), - - // Опция "Отклонение обменов" - _buildSettingItem( - title: 'Отклонение обменов', - hasSwitch: true, - switchValue: _exchangesDeclineEnabled, - hasInfo: true, - onChanged: (value) { - setState(() { - _exchangesDeclineEnabled = value; - }); - }, - ), - - // Опция "Показ инвентаря" - _buildSettingItem( - title: 'Показ инвентаря', - hasSwitch: true, - switchValue: _inventoryDisplayEnabled, - hasInfo: true, - onChanged: (value) { - setState(() { - _inventoryDisplayEnabled = value; - }); - }, - ), - - const Spacer(), - - // Кнопка "Выйти из аккаунта" - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - onPressed: () { - // Логика выхода из аккаунта - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), + ), + if (_showHint && _hintText != null) + Positioned( + top: 0, + right: 0, + child: Material( + color: Colors.transparent, + child: Container( + width: 100, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), ), - ), - child: const Text( - 'Выйти из аккаунта', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, + child: Text( + _hintText!, + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'Roboto', + fontSize: 12, + color: Colors.black, + ), ), ), ), ), - ], - ), - ), - - // Кнопка закрытия (X) - Positioned( - top: 50.0, - right: 16.0, - child: Container( - width: 40.0, - height: 40.0, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 1), - ), - child: InkWell( - borderRadius: BorderRadius.circular(8.0), - onTap: () { - Navigator.pop(context); - }, - child: const Icon( - Icons.close, - color: Colors.black, - size: 22.0, - ), - ), - ), - ), - - // Нижняя навигационная панель - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: 0, - onTap: (index) { - if (index == 0) { // Только для главного меню - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Гл.меню', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', - ), - ], - ), - ), + ], ), - ], + ), ), ); } - - // Строка настройки + Widget _buildSettingItem({ required String title, bool hasSwitch = false, bool hasInfo = false, bool switchValue = false, - VoidCallback? onTap, Function(bool)? onChanged, + VoidCallback? onInfoTap, }) { - return InkWell( - onTap: hasSwitch ? null : onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), - child: Row( - children: [ - // Название настройки - Text( - title, - style: const TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, - ), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), + child: Row( + children: [ + Text( + title, + style: const TextStyle( + fontFamily: 'Roboto', + fontSize: 18.0, + fontWeight: FontWeight.w400, ), - - // Иконка информации - if (hasInfo) - Container( + ), + if (hasInfo) + GestureDetector( + onTap: onInfoTap, + child: Container( margin: const EdgeInsets.only(left: 8.0), - width: 20.0, - height: 20.0, + width: 22.0, + height: 22.0, decoration: const BoxDecoration( color: Colors.black, shape: BoxShape.circle, ), child: const Center( child: Text( - 'i', + '!', style: TextStyle( color: Colors.white, - fontSize: 12.0, - fontWeight: FontWeight.bold, + fontFamily: 'Roboto', + fontSize: 16.0, + fontWeight: FontWeight.w500, ), ), ), ), - - const Spacer(), - - // Переключатель - if (hasSwitch) - Container( - width: 26.0, - height: 26.0, + ), + const Spacer(), + if (hasSwitch) + GestureDetector( + onTap: () => onChanged?.call(!switchValue), + child: Container( + width: 56.0, + height: 32.0, decoration: BoxDecoration( - color: switchValue ? Colors.white : Colors.transparent, - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 1), + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(32.0), + ), + child: Align( + alignment: switchValue ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.all(4.0), + width: 24.0, + height: 24.0, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: switchValue + ? const Icon(Icons.check, color: Colors.black, size: 16) + : null, + ), ), - child: switchValue - ? const Icon( - Icons.check, - color: Colors.green, - size: 18.0, - ) - : null, ), - ], - ), + ), + ], ), ); } From 84249150af0d16c631ac170331247ec347138839 Mon Sep 17 00:00:00 2001 From: birbik Date: Fri, 16 May 2025 11:58:08 +0300 Subject: [PATCH 07/54] FCCX-92 add icon achievement --- ...\320\266\320\265\320\275\320\270\320\265.png" | Bin 0 -> 6334 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 "frontend/assets/icons/\320\264\320\276\321\201\321\202\320\270\320\266\320\265\320\275\320\270\320\265.png" diff --git "a/frontend/assets/icons/\320\264\320\276\321\201\321\202\320\270\320\266\320\265\320\275\320\270\320\265.png" "b/frontend/assets/icons/\320\264\320\276\321\201\321\202\320\270\320\266\320\265\320\275\320\270\320\265.png" new file mode 100644 index 0000000000000000000000000000000000000000..fbc08f40b80492370f6bafb54b2d66bbe8bf3de5 GIT binary patch literal 6334 zcmb7IS5y;Bw@smk;)ftof*`$wAR=fehTeNe>BU5`fnP2FuwT@F$d~9 z1OosU#Qp+kU@5u{0Psv0XlqzU0Jn;mC!spI18hZ{Euz{=anZsMVLD;Aba`P}1A51c%&@r(-(j-gkKVbMwV# z%OBm}52`vBEe~G&p0=Ud94vI$#BAD9kJS&_^~XW~zcBHSb2h;&zFm{&QcRHhjZA+X zi;JZ+qnM?A!M4>l4VZ(6TB9DWQ5v*D+jj-1S3qC!6KG7Dp<4qtJ~$?OT~1|`Q6(X* z;%xV}i~UuTD(!roHJf3`)*`l}nj<{ELIW*o=X?Q%3@o5k@7*_W7PPDQm)50HPd;vR z5xb;68)54ps58aph^I-PTs$o!_nDf{kL#MPbYK6M`ZfY2LW zI%Y?o24CC>84v1a4n)HhY!}fv+}#A99r=m!;a16%NA$X}?IfsdWX(7RRTz)7 zk%>jAp0a_UNFTyY^Oi^W{P3`Ipl_o2JjVg9dlRk~wrZk8CU|abX-A5t(@$aEy>*kufb3PjKO3on&)!X|b?D>nlN2!ej%;pqv3=rE}GI57LPx zAivCSO}dBe@h%^aqdCh1J*$D{B+AiJPez#^f#MNYx=qyWDs3$DD5Pc{X6fboNy=nl z2N+4sNqJuybfm}G8$-M&*=}n|JG~8k44>+NIwgO2eAq?3H2#w3Y0fw(w}q<1ZxtSN zRFR-E#FO#c`|a7}EvJ`p%k4C7K3^C38`A(>aT=Sg*9V0#553FNQ2UAiP`AcH2m^@V z3k}u2O9C-YO!3)-2kWp_R=4DREf91H3zJ#N5CptGNWJ|LItq{--anf*eWZ=`4TGP&N`gGB3%%FNw&$jim zPo{EgsBxb9!sUr)f)s;cPo_D5_)grsS#if^xcgsM)^_x{HU_&R#v6lG{giN4nqn$n zcWZWQ2Vh2hqtxi~3JO>pcB_2zwvjVdf8&XKe?+ZZN2gDS))h{{eN*#Qdo3hZ0EQ7s z#Ko#$3^$+QQ3ya>p>eI;L-9JmYv+z4_~ws*zEsv~S%w3=vt6$87bW`m z%--i}i;}Ea>JgmF&~0(~Vex8*pigg)y!Z5hrUxVO+3&6S>GAm0{B6r}joJhUz>anp18e6=zP7ztZs|H;EftHB+=7H!S`9gi_kxzmBdxGqXz$;JKTG=Y=2gUcL1onLcB_cI&L^U%QBt5n}$q z1}M|8J~r>AN-T8jyy2LaO1j{~3S*vWs(v`^Wv=8B^IUtgTEh=Rris zjgCaYFOjbYiA7pH5N+vM&{0fQ@$EagnCvtSf?$AoOAx)&Zo07gM+uwCODT|+*si=( z3+D=7^WLa6yP6)61P|%8hVv^*y3xgr&C-(rc=xSZ$KpIJcp7k=UWBs>(@q?P896xt z(m~+Z2OXCKnmEUDEjZz5&dtIVCwU%}!aIN&sJUHWkY8+fD_dja7|+(vhZGG6h1V-IMcDmJvd*9<5@tG;V7HPTcE3+A z`MEL@E4x(JB9{VP_a?7p=wAt*g!yGmfkwh`$go6Yq=0Xc=XW zmT;x-fe}u2&!#1zOkk6amX~2g_M2A}5cJbZgk5&8)Z;AxK!Lj*labN#>q;X`3J z6)l2+MYWuDjXogdS$*l1fd1R2Ez;fOd3Zu=)l@Rvs5zD)l25;>YXrN1c4ZXe;7VTZ ze;aRr;h&`F4DTJf+ML4=$&Nu{_JK!7)oj@KwP2FBP_0Z$J%tPUA6X|{!!r7izQO2M(B{pPlPb21-{i!vE{_MPi zqf#`O#FC<%D4(5CzxmjAtokUpbuiZAd-dnzw%DawOc5_Xwa&g~)Y;aL-HuW$Ppn_Q6b&{?<%L5ORjVrKT zA-9w%KI8VMT%n|UA%Wo%Gv^6Rv@!$v{m^gxk<7mCJ&QH2DI%bCf(T!6z8YKBz5Q8d zuZ}zx%_9T7irH%Ty5qJ@y)2W?Et2HD*Zz!VOkx=NLzLdyoF@9z**By(W?Y(ht&Ptb z@wuK>f~}pSf7u&Y03n=Ht~>vEAdcaIhfuyFe5iYo;r5x~A|#Ko*w6+0g#+M|&o4O~ z#50$FbQ6>w5#g|4;Oueifl2s)*|lmD)4Z#;@{wr0L2Kn0G(4U_o~P!PyWFcS&9xvM z@sl<)1X&@2$}%$jd5w>K_$v+!hx8Y`t8m>bxGvlB;NFYp-Wm07Ud4+2j98klIOMXL zJzk2=m`Xrj?qQI9{ToAIwPcMHx2NMQ&M9FfBPw@cN$+u!daEA$VW zJiEoh3&x9>*50%=W)!| zFOgNVl;Qib<#x9}FIlnr-9&1fr!3SiN5hFJhOrI&>es#NW3}YBS;=oYkh~$+HJ#LQ z<$NOL%75E%&-x9ntu@^2!m;u_GQTfF19_p-!^vaq z@&zo{SN{FjTj=us z%STx`2=;aaSLnK-0z%Pv?Fy%zSV0NybFqktgT?IpGoe&`S}dZF=+^^jTQKZ|xBn_b z%YzmfYULcgm3Aa)+~jYZSbp$N$wFd645Te;(XfY*YBqj)K)#i2sq&n|+!iZAHHjBb zhqrXRikGAOU|C{+Md?VLVcBKSp=dD$hmIR`jN%Q;8xvIfDD5!z8B{iDhk!C`v^B#KLy!qo?l@KWL#t5)8`Mo)vi=`ovi#x(%D!kuthaG z61G{#*U`oT zbNc&fPU|n-s;^gI>2nPs5VdeN8t~B3xh$_CON)a&Z~!24Ee&orAG=12@KJ{M4R1*-D8!IPxqn8{b2 z5~tVAV0I2`5Nknt>@iZPu2Xm{4L0(mEW#HmWaa?Jb7U2dg>ZU(v%Hz<;=Ns4&alPe zRQSbvLth5v)I0J00r@)+9`At6Mv0?5h0s0E`&N4dW#vR)GUL>1**c=+^Hyz16)Qtb zscYp!>!z|X^^Fbg!~f#v`WzYj6wMvb>TDsKE-75ZsxNZuYcK!F!u`Wh-Fg(nX@1T{ z6kUc=fRyEuewkU8o{1DMo`zU_eKFd2n=-Q@Vl;85I1=rCM{M?1=jvpL=r!unrBLM0 zx5AhU>#O2jV#+O{}NojBU2!RrN#EAAR$S3V8<9KZPL)8I#ask`oR@f)yq zN8U2_R6>4oN3O0c)a^uQY$D5a<2Bo^%C0V6a2}1GvcG0nT41l97$ZqoZk!nP`Uq8(mR&*%Ix3Rmj7*{$S98G+snjGANd+|R+@{#<^LIsps~n>W{z_zGKj`q z8^REQAN&De)!Wzb`;6-R5mh*8$5AVvm$-K@%CTDScjT1-+d`uRpV-l{!bC;R%;k_6R;E~?0nt0kfmvnB(!3ou?!wbDV0Hz}zE z?b;qhzw=Af#b>^+IURU@%3%@ZBA<8hZFW$fLHm6VV-dPly#WKC~w!r4Vx1!HcX$7T6!B7W|oO;g@) z6^C;^#?OV#HnoiO)sK_C%m{bXpfi0|7aH}iWb6sT>9|}Yhm1C6gw1IEPZ3`wQ^_Uv z?3-+p#$%=JiWE(K>K^1*m% z_v?1klcJ9E)O%WJ>-|28LS^=?eC))&zXplO?xlR~WckXLc3`!n;QT+vz&W&()7A{K z%Nvj%-P?TJles&M@*Vj3O7~)m(L8niX`Xg?N^PAp5B4P*cCjNUr>8bKqoJ$w!rDbq zG;a4&toUp3s`S3A_)^sxyPwU&j<4QK-Q;(;l;LtiraY|XVZxO(1}!j8VasDTg|VR> zB_-h{u%eq+%8b7Is~Vy|V8aB_Rfsd8!3Ea@3K? z&Rs%RRX6mIcZQAFCpuA!4i|z4a@n&@mc6u&U~GFEmD1Jo?i{wx+D=*Sx8r&%zBJ;c z_7geI*9<$zn*Hy`4wL!p!YGsHGfCCO^Zk{^UhAxJs(IaVJhCjwB&f zxTj5im;9-{&1>$`qEM3(+~nPyPp7AXXGsDFc#W(lbM-6%u$QYKYUsSJ;iAoByD7VD zHHGHcwcPkj^<+at)$P+rkN}5bNkyjkJJY3bQn7I@%`!~BQb(&2Geh>1DS63$k-M_9 z%kM~J!G%6Cj)?8x0W~+DeCYCgg83xhknL~?vsAcbq890H*J_A{2`O+kmBN)X3E?Bk#}C z4{jLEZa6HY(+$N3L)$J5YNgV>MEP8FV(+_^9eU?n@~-fF+{5g~D77O?;>QcO*|^?2 zF7%v;OhPUstovLSFgBCAUF&d<8c{DWXjrKI`BRv2iL0@#uAlY8`$wNu*l!4%UpoGG z)iqFgjB{;n!Ng~xUXV$1?$hkd0Cyhtw_$g7uQ(>N!UsLK2;uA*+R-5|UsFk)Dtp5s z!Kor@=9gv5=F9pRL|ZDXZ_>Cd6M2{3k(W-mW`Nn_*m|tWp@#?MsW5(Gwd)lGiOmfc z@hXfc)@;Nd`EG5idljwv(!p|X0ioY}SV}+|UV;Fv`77;SACsh(azT+}bkFC+ETT?S zIowPJt%L_XUH$61KG$3}xYX3;5Qw;t_C9vLw-xD}%{-N2pim1`e^b)fW_v776gWJ3 z5PTfg-d2JRLQ8zUx-ClHvuWkWo6*QU>fu&`vTxDKZ@jkBYaclS)U`^rBmY^rT0Ocq zHLbCXCQVPTO8o$3f*f7xx}vjSXV7 z8;dm@X(6fOv@Q3bTCwPJaoKx?5bJEc>_I4(vRZ0*$ExhNJC_Elv}I_Y&CStL`3oI> z*TIv)1BOld!mxViyO1X z3Zd(~Bo2Hqd zE^>h@JCmB*-Jkav3sym=eUg?%j>r;s*)fGf*}*s2Lsv)WR)LZ4(soR@_&l!U;Zf+j zHT25{Ex__v)uJ@UJOFy#Pui3*JmG%aXPkfixe(FNqrNJ|wc#Dj-uAF0#9MoI)U?F? w7$#!TWH&aCsvO6T648NZ|38D6MSpgwZyp^!ooWB!@87?Hj;VI7rfdBF08O>bh5!Hn literal 0 HcmV?d00001 From ccfcd0275bd564294b3fc886b85662b8255a8e44 Mon Sep 17 00:00:00 2001 From: birbik Date: Fri, 16 May 2025 12:01:46 +0300 Subject: [PATCH 08/54] FCCX-70 update page switching --- frontend/lib/views/exchanges_screen.dart | 9 +++------ frontend/lib/views/home_screen.dart | 9 +++------ frontend/lib/views/inventory_screen.dart | 9 +++------ frontend/lib/views/shop_screen.dart | 11 ++++------- 4 files changed, 13 insertions(+), 25 deletions(-) diff --git a/frontend/lib/views/exchanges_screen.dart b/frontend/lib/views/exchanges_screen.dart index abbad54..eb0f86b 100644 --- a/frontend/lib/views/exchanges_screen.dart +++ b/frontend/lib/views/exchanges_screen.dart @@ -240,24 +240,21 @@ class _ExchangesScreenState extends State with SingleTickerProv }); switch (index) { case 0: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, ); break; case 1: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const InventoryScreen()), - (route) => false, ); break; case 2: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, ); break; } diff --git a/frontend/lib/views/home_screen.dart b/frontend/lib/views/home_screen.dart index f5637cc..3019a9b 100644 --- a/frontend/lib/views/home_screen.dart +++ b/frontend/lib/views/home_screen.dart @@ -243,24 +243,21 @@ class _HomeScreenState extends State { }); switch (index) { case 1: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const InventoryScreen()), - (route) => false, ); break; case 2: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, ); break; case 3: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, ); break; } diff --git a/frontend/lib/views/inventory_screen.dart b/frontend/lib/views/inventory_screen.dart index dca8cab..bac4471 100644 --- a/frontend/lib/views/inventory_screen.dart +++ b/frontend/lib/views/inventory_screen.dart @@ -206,24 +206,21 @@ class _InventoryScreenState extends State { }); switch (index) { case 0: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, ); break; case 2: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, ); break; case 3: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, ); break; } diff --git a/frontend/lib/views/shop_screen.dart b/frontend/lib/views/shop_screen.dart index 052d481..77022f2 100644 --- a/frontend/lib/views/shop_screen.dart +++ b/frontend/lib/views/shop_screen.dart @@ -16,7 +16,7 @@ class ShopScreen extends StatefulWidget { class _ShopScreenState extends State with SingleTickerProviderStateMixin { late TabController _tabController; int _currentIndex = 2; - int _selectedTab = 1; + int _selectedTab = 0; final List _sets = [ 'Ценник 1', @@ -159,24 +159,21 @@ class _ShopScreenState extends State with SingleTickerProviderStateM }); switch (index) { case 0: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, ); break; case 1: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const InventoryScreen()), - (route) => false, ); break; case 3: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, ); break; } From 298d2f9ce1ec401ca4f32a1e6f24226f115cdbdf Mon Sep 17 00:00:00 2001 From: birbik Date: Fri, 16 May 2025 18:25:28 +0300 Subject: [PATCH 09/54] FCCX-93 update user search --- frontend/lib/views/exchanges_screen.dart | 10 +- frontend/lib/views/home_screen.dart | 12 +- frontend/lib/views/inventory_screen.dart | 12 +- frontend/lib/views/search_players_screen.dart | 370 +++++++----------- frontend/lib/views/shop_screen.dart | 14 +- 5 files changed, 179 insertions(+), 239 deletions(-) diff --git a/frontend/lib/views/exchanges_screen.dart b/frontend/lib/views/exchanges_screen.dart index eb0f86b..87ffb2e 100644 --- a/frontend/lib/views/exchanges_screen.dart +++ b/frontend/lib/views/exchanges_screen.dart @@ -5,6 +5,7 @@ import 'home_screen.dart'; import 'shop_screen.dart'; import 'inventory_screen.dart'; import 'profile_screen.dart'; +import 'search_players_screen.dart'; class ExchangesScreen extends StatefulWidget { const ExchangesScreen({super.key}); @@ -47,7 +48,7 @@ class _ExchangesScreenState extends State with SingleTickerProv child: Container( margin: const EdgeInsets.symmetric(horizontal: 16.0), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(12.0), ), child: TabBar( @@ -119,10 +120,15 @@ class _ExchangesScreenState extends State with SingleTickerProv ), Container( - child: IconButton( icon: Image.asset('assets/icons/поиск.png', height: 32), onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const SearchPlayersModal(), + ); }, ), ), diff --git a/frontend/lib/views/home_screen.dart b/frontend/lib/views/home_screen.dart index 3019a9b..c366897 100644 --- a/frontend/lib/views/home_screen.dart +++ b/frontend/lib/views/home_screen.dart @@ -50,11 +50,11 @@ class _HomeScreenState extends State { color: Colors.black, ), onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SearchPlayersScreen(), - ), + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const SearchPlayersModal(), ); }, ), @@ -97,7 +97,7 @@ class _HomeScreenState extends State { itemBuilder: (context, index) { return Container( decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(12.0), ), ); diff --git a/frontend/lib/views/inventory_screen.dart b/frontend/lib/views/inventory_screen.dart index bac4471..d536661 100644 --- a/frontend/lib/views/inventory_screen.dart +++ b/frontend/lib/views/inventory_screen.dart @@ -4,6 +4,7 @@ import 'shop_screen.dart'; import 'exchanges_screen.dart'; import 'profile_screen.dart'; import 'card_detail_screen.dart'; +import 'search_players_screen.dart'; class InventoryScreen extends StatefulWidget { const InventoryScreen({super.key}); @@ -47,7 +48,7 @@ class _InventoryScreenState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(10.0), ), child: Row( @@ -68,7 +69,14 @@ class _InventoryScreenState extends State { ), IconButton( icon: Image.asset('assets/icons/поиск.png', height: 32), - onPressed: () {}, + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const SearchPlayersModal(), + ); + }, ), ], ), diff --git a/frontend/lib/views/search_players_screen.dart b/frontend/lib/views/search_players_screen.dart index 101a995..d25a3d8 100644 --- a/frontend/lib/views/search_players_screen.dart +++ b/frontend/lib/views/search_players_screen.dart @@ -1,219 +1,88 @@ import 'package:flutter/material.dart'; -class SearchPlayersScreen extends StatefulWidget { - const SearchPlayersScreen({super.key}); - - @override - State createState() => _SearchPlayersScreenState(); -} - -class _SearchPlayersScreenState extends State { - final TextEditingController _searchController = TextEditingController(); - final List _players = [ - PlayerItem(name: 'Cardly', cards: 5), - PlayerItem(name: 'Cardly1', cards: 5), - PlayerItem(name: 'Cardly2', cards: 5), - PlayerItem(name: 'Cardly3', cards: 5), - PlayerItem(name: 'Cardly4', cards: 5), - ]; - - int _currentIndex = 0; - - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } +class SearchPlayersModal extends StatelessWidget { + const SearchPlayersModal({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон - appBar: AppBar( - backgroundColor: const Color(0xFFEDD6B0), - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black), - onPressed: () => Navigator.pop(context), - ), - title: const Text( - 'Cardly', - style: TextStyle(color: Colors.black), - ), - actions: [ - IconButton( - icon: const Icon(Icons.close, color: Colors.black), - onPressed: () => Navigator.pop(context), - ), - ], - bottom: PreferredSize( - preferredSize: const Size.fromHeight(48.0), - child: Icon( - Icons.keyboard_arrow_down, - color: Colors.black, - size: 24.0, + final List players = [ + PlayerItem(name: 'Cardly', cards: 5), + PlayerItem(name: 'Cardly1', cards: 5), + PlayerItem(name: 'Cardly2', cards: 5), + PlayerItem(name: 'Cardly3', cards: 5), + PlayerItem(name: 'Cardly4', cards: 5), + ]; + final TextEditingController _searchController = TextEditingController(); + final double modalHeight = MediaQuery.of(context).size.height * 0.6; + return Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 330), + child: Container( + height: modalHeight, + width: MediaQuery.of(context).size.width * 0.98, + decoration: const BoxDecoration( + color: Color(0xFFEAD7C3), + borderRadius: BorderRadius.all(Radius.circular(10)), ), - ), - ), - body: Column( - children: [ - // Основное содержимое - список игроков - Expanded( - child: Container( - color: const Color(0xFFEDD6B0), - child: ListView.builder( - itemCount: _players.length, - itemBuilder: (context, index) { - return PlayerListItem(player: _players[index]); - }, - ), - ), - ), - - // Кнопка "Создай свою уникальную карточку" - Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: const Text( - 'Создай свою уникальную карточку', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - - // Кнопки "Квесты" и "Новости" - Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 16.0), - child: Row( - children: [ - // Кнопка "Квесты" - Expanded( - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(left: 8, right: 8, top: 8, bottom: 0), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () => Navigator.of(context).pop(), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - 'assets/icons/квесты.png', - height: 24, - color: Colors.black, + Expanded( + child: Container( + height: 36, + margin: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: const Color(0xFFFFF4E3), + borderRadius: BorderRadius.circular(8), ), - const SizedBox(height: 4.0), - const Text( - 'Квесты', - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, + child: TextField( + controller: _searchController, + style: const TextStyle( + fontFamily: 'Roboto', + fontSize: 16, + color: Colors.black, + ), + decoration: const InputDecoration( + border: InputBorder.none, + hintText: 'Введите ник пользователя', + hintStyle: TextStyle( + fontFamily: 'Roboto', + fontSize: 16, + color: Colors.black, + ), + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ), - ], - ), - ), - ), - - const SizedBox(width: 12.0), - - // Кнопка "Новости" - Expanded( - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), ), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - 'assets/icons/новости.png', - height: 24, - color: Colors.black, - ), - const SizedBox(height: 4.0), - const Text( - 'Новости', - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - ), - ), - ], + IconButton( + icon: const Icon(Icons.close, color: Colors.black), + onPressed: () => _searchController.clear(), ), - ), + ], ), - ], - ), - ), - - // Нижняя навигационная панель - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16.0), - topRight: Radius.circular(16.0), ), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - setState(() { - _currentIndex = index; - }); - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Гл.меню', + const Divider(height: 1, color: Colors.black26), + Expanded( + child: ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 0), + itemCount: players.length, + itemBuilder: (context, index) { + return PlayerListItem(player: players[index]); + }, ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', - ), - ], - ), + ), + ], ), - ], + ), ), ); } @@ -223,65 +92,114 @@ class _SearchPlayersScreenState extends State { class PlayerItem { final String name; final int cards; - + PlayerItem({required this.name, required this.cards}); } // Виджет элемента списка игроков class PlayerListItem extends StatelessWidget { final PlayerItem player; - + const PlayerListItem({super.key, required this.player}); - + @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 10.0), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Аватар пользователя + // Аватар пользователя как в profile_screen Container( - width: 40, - height: 40, + width: 44, + height: 44, decoration: BoxDecoration( shape: BoxShape.circle, + color: const Color(0xFFEAD7C3), border: Border.all(color: Colors.black, width: 2), ), - child: Image.asset( - 'assets/icons/профиль.png', - height: 24, - color: Colors.black, + child: const Center( + child: Icon(Icons.person_outline, size: 28, color: Colors.black), ), ), - - const SizedBox(width: 16.0), - + const SizedBox(width: 12.0), // Имя пользователя Text( player.name, style: const TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, + fontFamily: 'Roboto', + fontSize: 18.0, + fontWeight: FontWeight.w400, + color: Colors.black, ), ), - const Spacer(), - - // Коллекция карточек пользователя + // Карточки пользователя (макет из инвентаря, но без фото) Row( children: List.generate( player.cards, - (index) => Container( - margin: const EdgeInsets.only(left: 4.0), - width: 32, - height: 42, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(4.0), - ), + (index) => Padding( + padding: const EdgeInsets.only(left: 6.0), + child: CardMockup(), + ), + ), + ), + ], + ), + ); + } +} + +// Макет карточки как в инвентаре, но без фото +class CardMockup extends StatelessWidget { + const CardMockup({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 32, + height: 44, + child: Stack( + children: [ + // Внешняя черная рамка + Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.black, width: 2), + ), + ), + // Прослойка цвета карточки + Padding( + padding: const EdgeInsets.all(2.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(3), ), ), ), + // Внутренняя черная рамка + Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(2), + border: Border.all(color: Colors.black, width: 1), + ), + ), + ), + // Разделительная линия + Positioned( + bottom: 12, + left: 3, + right: 3, + child: Container( + height: 2, + color: Colors.black, + ), + ), ], ), ); diff --git a/frontend/lib/views/shop_screen.dart b/frontend/lib/views/shop_screen.dart index 77022f2..22732c3 100644 --- a/frontend/lib/views/shop_screen.dart +++ b/frontend/lib/views/shop_screen.dart @@ -5,6 +5,7 @@ import 'inventory_screen.dart'; import 'shop_set_details_screen.dart'; import 'shop_coin_details_screen.dart'; import 'profile_screen.dart'; +import 'search_players_screen.dart'; class ShopScreen extends StatefulWidget { const ShopScreen({super.key}); @@ -72,7 +73,7 @@ class _ShopScreenState extends State with SingleTickerProviderStateM Container( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(10.0), ), child: Row( @@ -93,7 +94,14 @@ class _ShopScreenState extends State with SingleTickerProviderStateM ), IconButton( icon: Image.asset('assets/icons/поиск.png', height: 32), - onPressed: () {}, + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const SearchPlayersModal(), + ); + }, ), ], ), @@ -103,7 +111,7 @@ class _ShopScreenState extends State with SingleTickerProviderStateM padding: const EdgeInsets.all(16.0), child: Container( decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(12.0), ), child: TabBar( From 481f16140bd7515bf500f923eecd1c5c9b9d2e56 Mon Sep 17 00:00:00 2001 From: dobrayAnika Date: Sat, 17 May 2025 13:47:17 +0300 Subject: [PATCH 10/54] FCCX-110 add achievements --- frontend/lib/models/achievement_model.dart | 43 +++++++++++++ frontend/lib/views/achievements_screen.dart | 69 +++++++++++++++++++++ frontend/lib/views/profile_screen.dart | 25 +++++--- 3 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 frontend/lib/models/achievement_model.dart create mode 100644 frontend/lib/views/achievements_screen.dart diff --git a/frontend/lib/models/achievement_model.dart b/frontend/lib/models/achievement_model.dart new file mode 100644 index 0000000..796475a --- /dev/null +++ b/frontend/lib/models/achievement_model.dart @@ -0,0 +1,43 @@ +class Achievement { + final String id; + final String title; + final String description; + final String requirement; + final String iconPath; + final bool isCompleted; + final double progress; + + Achievement({ + required this.id, + required this.title, + required this.description, + required this.requirement, + required this.iconPath, + this.isCompleted = false, + this.progress = 0.0, + }); + + factory Achievement.fromJson(Map json) { + return Achievement( + id: json['id'] as String, + title: json['title'] as String, + description: json['description'] as String, + requirement: json['requirement'] as String, + iconPath: json['iconPath'] as String, + isCompleted: json['isCompleted'] as bool? ?? false, + progress: (json['progress'] as num?)?.toDouble() ?? 0.0, + ); + } + + Map toJson() { + return { + 'id': id, + 'title': title, + 'description': description, + 'requirement': requirement, + 'iconPath': iconPath, + 'isCompleted': isCompleted, + 'progress': progress, + }; + } +} \ No newline at end of file diff --git a/frontend/lib/views/achievements_screen.dart b/frontend/lib/views/achievements_screen.dart new file mode 100644 index 0000000..d30928c --- /dev/null +++ b/frontend/lib/views/achievements_screen.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import '../models/achievement_model.dart'; + +class AchievementsScreen extends StatelessWidget { + const AchievementsScreen({super.key}); + + @override + Widget build(BuildContext context) { + // TODO: Replace with actual achievements data + final List achievements = List.generate( + 10, + (index) => Achievement( + id: 'achievement_$index', + title: 'Достижение ${index + 1}', + description: 'Описание достижения ${index + 1}', + requirement: 'что нужно для достижения', + iconPath: 'assets/icons/достижение.png', + isCompleted: false, + progress: 0.0, + ), + ); + + return Scaffold( + backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон + body: SafeArea( + child: Column( + children: [ + // Back button and title + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Container( + width: 40.0, + height: 40.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFD6A067), + ), + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + Navigator.pop(context); + }, + child: const Icon( + Icons.arrow_back, + color: Colors.black, + size: 29.0, + ), + ), + ), + const SizedBox(width: 16), + const Text( + 'Достижения', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + // ... existing code ... + ], + ), + ), + ); + } +} diff --git a/frontend/lib/views/profile_screen.dart b/frontend/lib/views/profile_screen.dart index f5796ec..43dbcae 100644 --- a/frontend/lib/views/profile_screen.dart +++ b/frontend/lib/views/profile_screen.dart @@ -4,6 +4,7 @@ import 'shop_screen.dart'; import 'exchanges_screen.dart'; import 'inventory_screen.dart'; import 'settings_screen.dart' show SettingsDialog; +import 'achievements_screen.dart'; class ProfileScreen extends StatelessWidget { const ProfileScreen({super.key}); @@ -149,15 +150,25 @@ class ProfileScreen extends StatelessWidget { )), ), ), - const Positioned( + Positioned( right: 12, bottom: 8, - child: Text( - 'Все достижения →', - style: TextStyle( - fontSize: 13.0, - fontWeight: FontWeight.normal, - color: Colors.black, + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AchievementsScreen(), + ), + ); + }, + child: const Text( + 'Все достижения →', + style: TextStyle( + fontSize: 13.0, + fontWeight: FontWeight.normal, + color: Colors.black, + ), ), ), ), From 1f32fb9f4e3839d7d8e2e09ad79e375336d5d3b4 Mon Sep 17 00:00:00 2001 From: dobrayAnika Date: Sat, 17 May 2025 19:32:55 +0300 Subject: [PATCH 11/54] FCCX-110 update achievements_screen --- frontend/lib/views/achievements_screen.dart | 62 ++++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/frontend/lib/views/achievements_screen.dart b/frontend/lib/views/achievements_screen.dart index d30928c..3dccd85 100644 --- a/frontend/lib/views/achievements_screen.dart +++ b/frontend/lib/views/achievements_screen.dart @@ -6,7 +6,6 @@ class AchievementsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - // TODO: Replace with actual achievements data final List achievements = List.generate( 10, (index) => Achievement( @@ -60,7 +59,66 @@ class AchievementsScreen extends StatelessWidget { ], ), ), - // ... existing code ... + // Achievements list + Expanded( + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: achievements.length, + itemBuilder: (context, index) { + final achievement = achievements[index]; + return Card( + margin: const EdgeInsets.only(bottom: 8), + color: const Color(0xFFEDD6B0), + elevation: 0, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + // Achievement icon using asset + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + shape: BoxShape.circle, + ), + child: Image.asset( + achievement.iconPath, + width: 40, + height: 40, + fit: BoxFit.contain, + ), + ), + const SizedBox(width: 16), + // Achievement details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + achievement.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + achievement.requirement, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), ], ), ), From 61f981e7d9584c79b4b42d57ac159b4bbc046af3 Mon Sep 17 00:00:00 2001 From: dobrayAnika Date: Sat, 17 May 2025 19:39:25 +0300 Subject: [PATCH 12/54] FCCX-111 update news_datail_screen --- frontend/lib/views/news_detail_screen.dart | 24 ------- frontend/lib/views/news_screen.dart | 73 +++++++++++++--------- 2 files changed, 42 insertions(+), 55 deletions(-) diff --git a/frontend/lib/views/news_detail_screen.dart b/frontend/lib/views/news_detail_screen.dart index 3e64327..16bcc79 100644 --- a/frontend/lib/views/news_detail_screen.dart +++ b/frontend/lib/views/news_detail_screen.dart @@ -162,30 +162,6 @@ class _NewsDetailScreenState extends State { ), ], ), - - // Кнопка закрытия - Positioned( - top: 0, - right: 0, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(4.0), - ), - child: IconButton( - icon: const Icon(Icons.close, color: Colors.black), - onPressed: () { - Navigator.pop(context); - }, - constraints: const BoxConstraints( - minWidth: 36, - minHeight: 36, - ), - padding: EdgeInsets.zero, - iconSize: 20, - ), - ), - ), ], ), ), diff --git a/frontend/lib/views/news_screen.dart b/frontend/lib/views/news_screen.dart index b829e51..8c82577 100644 --- a/frontend/lib/views/news_screen.dart +++ b/frontend/lib/views/news_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'home_screen.dart'; import 'shop_screen.dart'; import 'exchanges_screen.dart'; +import 'news_detail_screen.dart'; class NewsScreen extends StatefulWidget { const NewsScreen({super.key}); @@ -203,41 +204,51 @@ class _NewsScreenState extends State { // Метод для отображения элемента новости Widget _buildNewsItem(NewsItem news) { - return Container( - margin: const EdgeInsets.only(bottom: 16.0), - padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Левая часть - заголовок - Expanded( - flex: 1, - child: Text( - news.title, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14.0, + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NewsDetailScreen(news: news), + ), + ); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 16.0), + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Левая часть - заголовок + Expanded( + flex: 1, + child: Text( + news.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.0, + ), ), ), - ), - - const SizedBox(width: 16.0), - - // Правая часть - текст - Expanded( - flex: 1, - child: Text( - news.content, - style: const TextStyle( - fontSize: 14.0, + + const SizedBox(width: 16.0), + + // Правая часть - текст + Expanded( + flex: 1, + child: Text( + news.content, + style: const TextStyle( + fontSize: 14.0, + ), ), ), - ), - ], + ], + ), ), ); } From e3eb316bfa5428a578157b0aa7b908629f672ed2 Mon Sep 17 00:00:00 2001 From: dobrayAnika Date: Sat, 17 May 2025 23:31:14 +0300 Subject: [PATCH 13/54] FCCX-112 add profile_image_dialog --- frontend/lib/views/profile_image_dialog.dart | 104 +++++++++++++++++++ frontend/lib/views/profile_screen.dart | 36 ++++--- 2 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 frontend/lib/views/profile_image_dialog.dart diff --git a/frontend/lib/views/profile_image_dialog.dart b/frontend/lib/views/profile_image_dialog.dart new file mode 100644 index 0000000..1772e0b --- /dev/null +++ b/frontend/lib/views/profile_image_dialog.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +class ProfileImageUploadDialog extends StatelessWidget { + const ProfileImageUploadDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + child: Container( + width: 369, + height: 559, + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.black, width: 2), + ), + child: Stack( + children: [ + // Close button (X) + Positioned( + top: -1, + right: -1, + child: GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10.0), + border: Border.all(color: Colors.black, width: 2), + ), + child: const Center( + child: Icon( + Icons.close_rounded, + color: Colors.black, + size: 48.0, + ), + ), + ), + ), + ), + // Upload button at the bottom + Positioned( + bottom: 20, + left: 0, + right: 0, + child: Center( + child: Container( + width: 289, + height: 59, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.black, width: 2), + ), + child: TextButton( + onPressed: () { + // TODO: Implement image upload functionality + }, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Загрузить изображение', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + // Profile icon + Center( + child: Container( + width: 140.0, + height: 140.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.black, width: 2), + ), + child: const CircleAvatar( + backgroundColor: Color(0xFFFFF4E3), + child: Icon( + Icons.person_outline, + size: 90.0, + color: Colors.black, + ), + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/views/profile_screen.dart b/frontend/lib/views/profile_screen.dart index 43dbcae..0567e83 100644 --- a/frontend/lib/views/profile_screen.dart +++ b/frontend/lib/views/profile_screen.dart @@ -5,6 +5,7 @@ import 'exchanges_screen.dart'; import 'inventory_screen.dart'; import 'settings_screen.dart' show SettingsDialog; import 'achievements_screen.dart'; +import 'profile_image_dialog.dart'; class ProfileScreen extends StatelessWidget { const ProfileScreen({super.key}); @@ -70,19 +71,28 @@ class ProfileScreen extends StatelessWidget { const SizedBox(height: 20.0), // Аватар пользователя - Container( - width: 140.0, - height: 140.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 2), - ), - child: const CircleAvatar( - backgroundColor: const Color(0xFFFFF4E3), - child: Icon( - Icons.person_outline, - size: 90.0, - color: Colors.black, + GestureDetector( + onTap: () { + showDialog( + context: context, + barrierColor: Colors.black.withOpacity(0.2), + builder: (context) => const ProfileImageUploadDialog(), + ); + }, + child: Container( + width: 140.0, + height: 140.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.black, width: 2), + ), + child: const CircleAvatar( + backgroundColor: Color(0xFFFFF4E3), + child: Icon( + Icons.person_outline, + size: 90.0, + color: Colors.black, + ), ), ), ), From db25c10cd4069d4855b9f520351e394b20bad785 Mon Sep 17 00:00:00 2001 From: danil13231212341 Date: Sun, 18 May 2025 14:03:16 +0300 Subject: [PATCH 14/54] FCCX-115 add shop and exchanges screens --- .../lib/views/exchange_details_screen.dart | 57 ++- frontend/lib/views/exchanges_screen.dart | 129 +++++- frontend/lib/views/pack_content_screen.dart | 40 +- frontend/lib/views/pack_open_screen.dart | 10 +- .../lib/views/shop_coin_details_screen.dart | 407 +++++++++-------- frontend/lib/views/shop_screen.dart | 140 +++--- .../lib/views/shop_set_content_screen.dart | 262 ++++++----- .../lib/views/shop_set_details_screen.dart | 413 ++++++++++-------- 8 files changed, 888 insertions(+), 570 deletions(-) diff --git a/frontend/lib/views/exchange_details_screen.dart b/frontend/lib/views/exchange_details_screen.dart index 4c588c4..8c01134 100644 --- a/frontend/lib/views/exchange_details_screen.dart +++ b/frontend/lib/views/exchange_details_screen.dart @@ -377,9 +377,60 @@ class _ExchangeDetailsScreenState extends State { foregroundColor: isDecline ? Colors.red : Colors.green, ), onPressed: () { - // Выполнить действие - Navigator.of(context).pop(); - Navigator.of(context).pop(); // Вернуться на предыдущий экран + Navigator.of(context).pop(); // Закрыть диалог + + // Показать сообщение в зависимости от действия + String messageText; + if (message == 'Вы уверены, что хотите отменить свой обмен?') { + messageText = 'Обмен отменен'; + } else { + messageText = isDecline ? 'Обмен отклонен' : 'Обмен успешно осуществлен'; + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Container( + width: 367, + height: 61, + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(15.0), + ), + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), + child: Center( + child: Text( + messageText, + style: const TextStyle( + color: Colors.black, + fontSize: 18.0, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ), + ), + backgroundColor: Colors.transparent, + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height - 200, + left: 16, + right: 16, + ), + elevation: 0, + ), + ); + + if (!isDecline && message != 'Вы уверены, что хотите отменить свой обмен?') { + // Вернуться на экран обменов при принятии + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const ExchangesScreen()), + (route) => false, + ); + } else { + Navigator.of(context).pop(); // Просто вернуться назад при отклонении или отмене + } // Здесь можно добавить вызов API для обработки обмена }, diff --git a/frontend/lib/views/exchanges_screen.dart b/frontend/lib/views/exchanges_screen.dart index 87ffb2e..3671833 100644 --- a/frontend/lib/views/exchanges_screen.dart +++ b/frontend/lib/views/exchanges_screen.dart @@ -373,19 +373,52 @@ class _ExchangesScreenState extends State with SingleTickerProv margin: const EdgeInsets.only(bottom: 16.0), padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(8.0), ), child: Row( children: [ - Container( - width: 60.0, - height: 80.0, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 1), - ), + Stack( + children: [ + Container( + width: 45.0, + height: 60.0, + decoration: BoxDecoration( + color: const Color(0xFFD9A76A), + borderRadius: BorderRadius.circular(4.0), + border: Border.all( + color: Colors.black, + width: 2.0, + ), + ), + child: Container( + margin: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.0), + border: Border.all( + color: Colors.black, + width: 2.0, + ), + ), + child: Column( + children: [ + Expanded( + flex: 7, + child: Container(), + ), + Container( + height: 2.0, + color: Colors.black, + ), + Expanded( + flex: 3, + child: Container(), + ), + ], + ), + ), + ), + ], ), Expanded( @@ -400,22 +433,80 @@ class _ExchangesScreenState extends State with SingleTickerProv Stack( children: [ Container( - width: 60.0, - height: 80.0, + width: 45.0, + height: 60.0, margin: const EdgeInsets.only(top: 4.0, left: 4.0), decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 1), + color: const Color(0xFFD9A76A), + borderRadius: BorderRadius.circular(4.0), + border: Border.all( + color: Colors.black, + width: 2.0, + ), + ), + child: Container( + margin: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.0), + border: Border.all( + color: Colors.black, + width: 2.0, + ), + ), + child: Column( + children: [ + Expanded( + flex: 7, + child: Container(), + ), + Container( + height: 2.0, + color: Colors.black, + ), + Expanded( + flex: 3, + child: Container(), + ), + ], + ), ), ), Container( - width: 60.0, - height: 80.0, + width: 45.0, + height: 60.0, decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 1), + color: const Color(0xFFD9A76A), + borderRadius: BorderRadius.circular(4.0), + border: Border.all( + color: Colors.black, + width: 2.0, + ), + ), + child: Container( + margin: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.0), + border: Border.all( + color: Colors.black, + width: 2.0, + ), + ), + child: Column( + children: [ + Expanded( + flex: 7, + child: Container(), + ), + Container( + height: 2.0, + color: Colors.black, + ), + Expanded( + flex: 3, + child: Container(), + ), + ], + ), ), ), ], diff --git a/frontend/lib/views/pack_content_screen.dart b/frontend/lib/views/pack_content_screen.dart index c2f460a..43034e6 100644 --- a/frontend/lib/views/pack_content_screen.dart +++ b/frontend/lib/views/pack_content_screen.dart @@ -24,17 +24,53 @@ class PackContentScreen extends StatelessWidget { child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, + childAspectRatio: 80 / 120, crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, - childAspectRatio: 0.7, ), itemCount: 12, itemBuilder: (context, index) { return Container( + width: 80.0, + height: 120.0, decoration: BoxDecoration( color: const Color(0xFFD6A067), borderRadius: BorderRadius.circular(6.0), - border: Border.all(color: Colors.black, width: 1), + border: Border.all(color: Colors.black, width: 2), + ), + child: Stack( + children: [ + // Основная рамка карты + Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.black, width: 2), + borderRadius: BorderRadius.circular(6.0), + ), + ), + // Внутренняя рамка карты + Positioned( + top: 5, + left: 5, + right: 5, + bottom: 5, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.black, width: 2), + borderRadius: BorderRadius.circular(4.0), + ), + ), + ), + // Линия разделения карты + Positioned( + left: 5, + right: 5, + bottom: 40, + child: Container( + height: 1, + color: Colors.black, + ), + ), + ], ), ); }, diff --git a/frontend/lib/views/pack_open_screen.dart b/frontend/lib/views/pack_open_screen.dart index 931f63b..a474e10 100644 --- a/frontend/lib/views/pack_open_screen.dart +++ b/frontend/lib/views/pack_open_screen.dart @@ -16,7 +16,7 @@ class PackOpenScreen extends StatelessWidget { icon: const Icon(Icons.arrow_back, color: Colors.black), onPressed: () => Navigator.pop(context), ), - title: Text('Открытие пака', style: const TextStyle(color: Colors.black)), + title: const Text('Открытие набора', style: TextStyle(color: Colors.black)), centerTitle: true, ), body: Center( @@ -25,8 +25,8 @@ class PackOpenScreen extends StatelessWidget { children: [ // Картинка пака (заглушка) Container( - width: 120, - height: 200, + width: 300, + height: 470, decoration: BoxDecoration( color: Colors.black, borderRadius: BorderRadius.circular(16), @@ -34,7 +34,7 @@ class PackOpenScreen extends StatelessWidget { child: const Center( child: Text( '?', - style: TextStyle(fontSize: 64, color: Colors.white), + style: TextStyle(fontSize: 128, color: Colors.white), ), ), ), @@ -57,7 +57,7 @@ class PackOpenScreen extends StatelessWidget { ), ), child: const Text( - 'Открыть пак', + 'Открыть набор', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ), diff --git a/frontend/lib/views/shop_coin_details_screen.dart b/frontend/lib/views/shop_coin_details_screen.dart index a70887c..e11bcf5 100644 --- a/frontend/lib/views/shop_coin_details_screen.dart +++ b/frontend/lib/views/shop_coin_details_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'home_screen.dart'; import 'shop_screen.dart'; import 'exchanges_screen.dart'; +import 'profile_screen.dart'; +import 'search_players_screen.dart'; class ShopCoinDetailsScreen extends StatefulWidget { final String coinName; @@ -15,9 +17,26 @@ class ShopCoinDetailsScreen extends StatefulWidget { State createState() => _ShopCoinDetailsScreenState(); } -class _ShopCoinDetailsScreenState extends State { +class _ShopCoinDetailsScreenState extends State with SingleTickerProviderStateMixin { int _currentIndex = 2; // Индекс вкладки "Магазин" в нижней навигации - + late TabController _tabController; + final List _coins = [ + 'Ценник 1', + 'Ценник 2', + ]; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this, initialIndex: 1); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -25,48 +44,58 @@ class _ShopCoinDetailsScreenState extends State { appBar: AppBar( backgroundColor: const Color(0xFFFFF4E3), elevation: 0, - leading: Container( - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 2), - ), - child: const Icon( - Icons.person_outline, - color: Colors.black, - ), - ), - title: Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(16.0), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: const [ - Text( - '1000', - style: TextStyle( - color: Colors.black, - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(width: 6.0), - Icon( - Icons.monetization_on, - color: Colors.amber, - size: 20.0, - ), - ], + leading: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Container( + width: 40.0, + height: 40.0, + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ProfileScreen()), + ); + }, + child: Image.asset('assets/icons/профиль.png', height: 22), + ), ), ), + title: null, centerTitle: true, actions: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(10.0), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '1000', + style: const TextStyle( + color: Colors.black, + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 6.0), + Image.asset('assets/icons/монеты.png', height: 20), + ], + ), + ), IconButton( - icon: const Icon(Icons.search, color: Colors.black), - onPressed: () {}, + icon: Image.asset('assets/icons/поиск.png', height: 32), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const SearchPlayersModal(), + ); + }, ), ], ), @@ -77,161 +106,195 @@ class _ShopCoinDetailsScreenState extends State { padding: const EdgeInsets.all(16.0), child: Container( decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(12.0), ), - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: () { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - ); - }, - child: Text( - 'Наборы', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - color: Colors.grey[600], - ), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - border: Border( - bottom: BorderSide( - color: Colors.black, - width: 3.0, - ), - ), - ), - child: const Text( - 'Монеты', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - color: Colors.black, - ), + child: TabBar( + controller: _tabController, + indicator: const BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.all(Radius.circular(12.0)), + border: Border( + bottom: BorderSide( + color: Colors.black, + width: 3.0, ), ), + ), + labelColor: Colors.black, + unselectedLabelColor: Colors.black, + indicatorSize: TabBarIndicatorSize.tab, + tabs: const [ + Tab(text: 'Наборы'), + Tab(text: 'Монеты'), ], + onTap: (index) { + if (index == 0) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const ShopScreen()), + ); + } + }, ), ), ), // Основное содержимое - детали монеты Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 1), - ), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - const SizedBox(height: 10.0), - // Превью монеты - Container( - height: 150, - width: double.infinity, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), - child: Center( - child: Text( - widget.coinName, - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, + child: Center( + child: SizedBox( + width: 360.0, + height: 492.0, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8.0), + border: Border.all(color: Colors.black, width: 3), + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(top: 96.0, left: 16.0, right: 16.0, bottom: 16.0), + child: Column( + children: [ + // Превью монеты + Center( + child: Container( + height: 150.0, + width: 150.0, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(8.0), ), ), ), - ), - const SizedBox(height: 10.0), - // Название и цена - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.coinName, - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), + const SizedBox(height: 7.0), + Text( + widget.coinName, + style: const TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + color: Colors.black, ), - const Text( - 'Цена', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, + textAlign: TextAlign.center, + ), + const SizedBox(height: 10.0), + // Название и цена + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.coinName, + style: const TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), ), - ), - ], - ), - const SizedBox(height: 20.0), - // Кнопка покупки - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Вы купили ${widget.coinName}')), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), + const Text( + 'Цена', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + ), ), - ), - child: const Text( - 'Купить', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, + ], + ), + const SizedBox(height: 20.0), + // Кнопка покупки + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + Future.delayed(const Duration(seconds: 3), () { + Navigator.of(context).pop(); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const ShopScreen()), + ); + }); + return Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 50), + Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: const Text( + 'Набор успешно приобретен', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + }, + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 16.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + child: const Text( + 'Купить', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), ), ), ), - ), - ], + ], + ), ), - ), - // Кнопка закрытия - Positioned( - top: 8.0, - right: 8.0, - child: GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - padding: const EdgeInsets.all(4.0), - decoration: BoxDecoration( - color: Colors.amber, - borderRadius: BorderRadius.circular(4.0), - ), - child: const Icon( - Icons.close, - color: Colors.black, - size: 18.0, + // Кнопка закрытия + Positioned( + top: -1, + right: -1, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 48.0, + height: 48.0, + decoration: BoxDecoration( + color: const Color(0xFFD9A76A), + border: Border.all( + color: Colors.black, + width: 1.0, + ), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(8.0), + bottomLeft: Radius.circular(8.0), + ), + ), + child: const Icon( + Icons.close, + color: Colors.black, + size: 32.0, + weight: 900, + ), ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/frontend/lib/views/shop_screen.dart b/frontend/lib/views/shop_screen.dart index 22732c3..8891c7e 100644 --- a/frontend/lib/views/shop_screen.dart +++ b/frontend/lib/views/shop_screen.dart @@ -20,14 +20,15 @@ class _ShopScreenState extends State with SingleTickerProviderStateM int _selectedTab = 0; final List _sets = [ - 'Ценник 1', - 'Ценник 2', - 'Ценник 3', - 'Ценник 4', + 'Название набора 1', + 'Название набора 2', + 'Название набора 3', + 'Название набора 4', ]; final List _coins = [ 'Ценник 1', + 'Ценник 2', ]; @override @@ -263,48 +264,42 @@ class _ShopScreenState extends State with SingleTickerProviderStateM } Widget _buildSetsTab() { - return GridView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 16.0, - mainAxisSpacing: 16.0, - childAspectRatio: 1.0, - ), + return ListView.separated( + padding: const EdgeInsets.fromLTRB(16.0, 60.0, 16.0, 16.0), itemCount: _sets.length, + separatorBuilder: (context, index) => const SizedBox(height: 30.0), itemBuilder: (context, index) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShopSetDetailsScreen(setName: _sets[index]), - ), - ); - }, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - _sets[index], - style: const TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - textAlign: TextAlign.center, + return Column( + children: [ + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ShopSetDetailsScreen(setName: _sets[index]), ), + ); + }, + child: Container( + width: 321.0, + height: 85.0, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10.0), ), - ], + ), ), - ), + const SizedBox(height: 7.0), + Text( + _sets[index], + style: const TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + ], ); }, ); @@ -316,44 +311,43 @@ class _ShopScreenState extends State with SingleTickerProviderStateM child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, - crossAxisSpacing: 16.0, - mainAxisSpacing: 16.0, - childAspectRatio: 1.0, + crossAxisSpacing: 15.0, + mainAxisSpacing: 15.0, + childAspectRatio: 150 / 175, ), itemCount: _coins.length, itemBuilder: (context, index) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShopCoinDetailsScreen(coinName: _coins[index]), - ), - ); - }, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - _coins[index], - style: const TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - textAlign: TextAlign.center, + return Column( + children: [ + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ShopCoinDetailsScreen(coinName: _coins[index]), ), + ); + }, + child: Container( + width: 150.0, + height: 150.0, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(8.0), ), - ], + ), ), - ), + const SizedBox(height: 7.0), + Text( + _coins[index], + style: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + ], ); }, ), diff --git a/frontend/lib/views/shop_set_content_screen.dart b/frontend/lib/views/shop_set_content_screen.dart index 7f51b7f..f83c800 100644 --- a/frontend/lib/views/shop_set_content_screen.dart +++ b/frontend/lib/views/shop_set_content_screen.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'home_screen.dart'; import 'shop_screen.dart'; import 'exchanges_screen.dart'; +import 'inventory_screen.dart'; +import 'package:flutter/rendering.dart'; class ShopSetContentScreen extends StatefulWidget { final String setName; @@ -16,36 +19,64 @@ class ShopSetContentScreen extends StatefulWidget { } class _ShopSetContentScreenState extends State { - int _currentIndex = 2; // Индекс вкладки "Магазин" в нижней навигации - + void _handleNavigation(int index) { + if (index != 2) { // Если нажата не иконка магазина + switch (index) { + case 0: + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const HomeScreen()), + (route) => false, + ); + break; + case 1: + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const InventoryScreen()), + (route) => false, + ); + break; + case 3: + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const ExchangesScreen()), + (route) => false, + ); + break; + } + } + } + @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон - appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), - elevation: 0, - leading: Container( - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: const Color(0xFFD6A067), - ), - child: IconButton( + backgroundColor: const Color(0xFFFFF4E3), + appBar: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.black), - onPressed: () => Navigator.pop(context), - ), - ), - title: const Text( - 'В наборе содержатся:', - style: TextStyle( - color: Colors.black, - fontSize: 18.0, + onPressed: () => Navigator.of(context).pop(), ), + systemOverlayStyle: SystemUiOverlayStyle.dark, ), ), body: Column( children: [ + const SizedBox(height: 28.0), + const Text( + 'В наборе содержится:', + style: TextStyle( + color: Colors.black, + fontSize: 24.0, + fontWeight: FontWeight.bold, + fontFamily: 'Roboto', + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 28.0), // Сетка карточек Expanded( child: Padding( @@ -53,13 +84,55 @@ class _ShopSetContentScreenState extends State { child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, - childAspectRatio: 0.7, + childAspectRatio: 80 / 120, crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, ), - itemCount: 12, // 12 карточек в сетке (3x4) + itemCount: 12, itemBuilder: (context, index) { - return _buildCardItem(); + return Container( + width: 80.0, + height: 120.0, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(6.0), + border: Border.all(color: Colors.black, width: 2), + ), + child: Stack( + children: [ + // Основная рамка карты + Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.black, width: 2), + borderRadius: BorderRadius.circular(6.0), + ), + ), + // Внутренняя рамка карты + Positioned( + top: 5, + left: 5, + right: 5, + bottom: 5, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.black, width: 2), + borderRadius: BorderRadius.circular(4.0), + ), + ), + ), + // Линия разделения карты + Positioned( + left: 5, + right: 5, + bottom: 40, + child: Container( + height: 1, + color: Colors.black, + ), + ), + ], + ), + ); }, ), ), @@ -69,66 +142,84 @@ class _ShopSetContentScreenState extends State { Container( decoration: const BoxDecoration( color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16.0), - topRight: Radius.circular(16.0), + border: Border( + top: BorderSide( + color: Colors.black, + width: 1.0, + ), ), ), child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - - // Навигация в зависимости от выбранного индекса - switch (index) { - case 0: // Главное меню - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 2: // Магазин - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: // Обменчик - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, + currentIndex: 2, + onTap: _handleNavigation, backgroundColor: Colors.transparent, elevation: 0, type: BottomNavigationBarType.fixed, selectedItemColor: Colors.black, unselectedItemColor: Colors.black54, - items: const [ + showSelectedLabels: true, + showUnselectedLabels: true, + selectedIconTheme: const IconThemeData( + size: 28, + ), + unselectedIconTheme: const IconThemeData( + size: 24, + ), + selectedLabelStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + unselectedLabelStyle: const TextStyle( + fontSize: 11, + ), + items: [ BottomNavigationBarItem( - icon: Icon(Icons.home), + icon: Image.asset('assets/icons/главная.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/главная.png', height: 24), + ), label: 'Главная', ), BottomNavigationBarItem( - icon: Icon(Icons.book), - label: '', + icon: Image.asset('assets/icons/Инвентарь.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/Инвентарь.png', height: 24), + ), + label: 'Инвентарь', ), BottomNavigationBarItem( - icon: Icon(Icons.storefront), + icon: Image.asset('assets/icons/магазин.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/магазин.png', height: 24), + ), label: 'Магазин', ), BottomNavigationBarItem( - icon: Icon(Icons.people), - label: '', + icon: Image.asset('assets/icons/обменник.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/обменник.png', height: 24), + ), + label: 'Обменник', ), ], ), @@ -137,41 +228,4 @@ class _ShopSetContentScreenState extends State { ), ); } - - // Метод построения элемента карточки - Widget _buildCardItem() { - return Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(6.0), - border: Border.all(color: Colors.black, width: 1), - ), - child: Column( - children: [ - // Верхняя часть карточки - Expanded( - flex: 2, - child: Container( - width: double.infinity, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - border: Border( - bottom: BorderSide(color: Colors.black, width: 1), - ), - ), - ), - ), - - // Нижняя часть карточки - Expanded( - flex: 1, - child: Container( - width: double.infinity, - color: const Color(0xFFD6A067), - ), - ), - ], - ), - ); - } } \ No newline at end of file diff --git a/frontend/lib/views/shop_set_details_screen.dart b/frontend/lib/views/shop_set_details_screen.dart index 103680f..bb73002 100644 --- a/frontend/lib/views/shop_set_details_screen.dart +++ b/frontend/lib/views/shop_set_details_screen.dart @@ -4,6 +4,8 @@ import 'home_screen.dart'; import 'shop_screen.dart'; import 'exchanges_screen.dart'; import 'pack_open_screen.dart'; +import 'profile_screen.dart'; +import 'search_players_screen.dart'; class ShopSetDetailsScreen extends StatefulWidget { final String setName; @@ -17,9 +19,22 @@ class ShopSetDetailsScreen extends StatefulWidget { State createState() => _ShopSetDetailsScreenState(); } -class _ShopSetDetailsScreenState extends State { +class _ShopSetDetailsScreenState extends State with SingleTickerProviderStateMixin { int _currentIndex = 2; // Индекс вкладки "Магазин" в нижней навигации - + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this, initialIndex: 0); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -27,48 +42,58 @@ class _ShopSetDetailsScreenState extends State { appBar: AppBar( backgroundColor: const Color(0xFFFFF4E3), elevation: 0, - leading: Container( - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 2), - ), - child: const Icon( - Icons.person_outline, - color: Colors.black, - ), - ), - title: Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(16.0), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: const [ - Text( - '1000', - style: TextStyle( - color: Colors.black, - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(width: 6.0), - Icon( - Icons.monetization_on, - color: Colors.amber, - size: 20.0, - ), - ], + leading: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Container( + width: 40.0, + height: 40.0, + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ProfileScreen()), + ); + }, + child: Image.asset('assets/icons/профиль.png', height: 22), + ), ), ), + title: null, centerTitle: true, actions: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(10.0), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '1000', + style: const TextStyle( + color: Colors.black, + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 6.0), + Image.asset('assets/icons/монеты.png', height: 20), + ], + ), + ), IconButton( - icon: const Icon(Icons.search, color: Colors.black), - onPressed: () {}, + icon: Image.asset('assets/icons/поиск.png', height: 32), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const SearchPlayersModal(), + ); + }, ), ], ), @@ -79,186 +104,190 @@ class _ShopSetDetailsScreenState extends State { padding: const EdgeInsets.all(16.0), child: Container( decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(12.0), ), - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - border: Border( - bottom: BorderSide( - color: Colors.black, - width: 3.0, - ), - ), - ), - child: const Text( - 'Наборы', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - ), - Text( - 'Монеты', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - color: Colors.grey[600], + child: TabBar( + controller: _tabController, + indicator: const BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.all(Radius.circular(12.0)), + border: Border( + bottom: BorderSide( + color: Colors.black, + width: 3.0, ), ), + ), + labelColor: Colors.black, + unselectedLabelColor: Colors.black, + indicatorSize: TabBarIndicatorSize.tab, + tabs: const [ + Tab(text: 'Наборы'), + Tab(text: 'Монеты'), ], + onTap: (index) { + if (index == 1) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const ShopScreen()), + ); + } + }, ), ), ), // Основное содержимое - детали набора Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 1), - ), - child: Stack( - children: [ - // Содержимое карточки набора - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - // Превью набора - Container( - height: 150, - width: double.infinity, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), + child: Center( + child: SizedBox( + width: 360.0, + height: 492.0, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8.0), + border: Border.all(color: Colors.black, width: 3), + ), + child: Stack( + children: [ + // Содержимое карточки набора + Padding( + padding: const EdgeInsets.only(top: 55.0, left: 16.0, right: 16.0, bottom: 16.0), + child: Column( + children: [ + // Превью набора + Container( + height: 85.0, + width: 321.0, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(8.0), + ), ), - ), - - const SizedBox(height: 10.0), - - // Название и цена - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.setName, - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, + const SizedBox(height: 10.0), + // Название и цена + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.setName, + style: const TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), ), - ), - const Text( - 'Цена', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, + const Text( + 'Цена', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + ), ), - ), - ], - ), - - const SizedBox(height: 20.0), - - // Кнопка просмотра содержимого - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShopSetContentScreen(setName: widget.setName), + ], + ), + + const SizedBox(height: 20.0), + + // Кнопка просмотра содержимого + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ShopSetContentScreen(setName: widget.setName), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 16.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), ), - ), - child: const Text( - 'Посмотреть Содержимое', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, + child: const Text( + 'Посмотреть Содержимое', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), ), ), ), - ), - - const SizedBox(height: 12.0), - - // Кнопка покупки - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - // После покупки переходим на экран открытия пака - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PackOpenScreen(setName: widget.setName), + + const SizedBox(height: 12.0), + + // Кнопка покупки + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PackOpenScreen( + setName: widget.setName, + ), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 16.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), ), - ), - child: const Text( - 'Купить', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, + child: const Text( + 'Купить', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), ), ), ), - ), - ], + ], + ), ), - ), - - // Кнопка закрытия - Positioned( - top: 8.0, - right: 8.0, - child: GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - padding: const EdgeInsets.all(4.0), - decoration: BoxDecoration( - color: Colors.amber, - borderRadius: BorderRadius.circular(4.0), - ), - child: const Icon( - Icons.close, - color: Colors.black, - size: 18.0, + + // Кнопка закрытия + Positioned( + top: -1, + right: -1, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 48.0, + height: 48.0, + decoration: BoxDecoration( + color: const Color(0xFFD9A76A), + border: Border.all( + color: Colors.black, + width: 1.0, + ), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(8.0), + bottomLeft: Radius.circular(8.0), + ), + ), + child: const Icon( + Icons.close, + color: Colors.black, + size: 32.0, + weight: 900, + ), ), ), ), - ), - ], + ], + ), ), ), ), From 67dcf0daa8aad890c2cbf33b08631885695a4088 Mon Sep 17 00:00:00 2001 From: birbik Date: Sun, 18 May 2025 16:55:29 +0300 Subject: [PATCH 15/54] FCCX-116 update quests_screen --- frontend/lib/views/home_screen.dart | 7 +- frontend/lib/views/quests_screen.dart | 412 +++++++------------------- 2 files changed, 106 insertions(+), 313 deletions(-) diff --git a/frontend/lib/views/home_screen.dart b/frontend/lib/views/home_screen.dart index c366897..1498be9 100644 --- a/frontend/lib/views/home_screen.dart +++ b/frontend/lib/views/home_screen.dart @@ -148,12 +148,7 @@ class _HomeScreenState extends State { Expanded( child: ElevatedButton( onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const QuestsScreen(), - ), - ); + showQuestsDialog(context); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFD6A067), diff --git a/frontend/lib/views/quests_screen.dart b/frontend/lib/views/quests_screen.dart index 2fb8aed..fae86f1 100644 --- a/frontend/lib/views/quests_screen.dart +++ b/frontend/lib/views/quests_screen.dart @@ -4,137 +4,85 @@ import 'shop_screen.dart'; import 'exchanges_screen.dart'; import 'news_screen.dart'; -class QuestsScreen extends StatefulWidget { - const QuestsScreen({super.key}); +class QuestsDialogContent extends StatefulWidget { + const QuestsDialogContent({super.key}); @override - State createState() => _QuestsScreenState(); + State createState() => _QuestsDialogContentState(); } -class _QuestsScreenState extends State with SingleTickerProviderStateMixin { - int _currentIndex = 0; // Индекс для нижней навигации (главное меню) +class _QuestsDialogContentState extends State with SingleTickerProviderStateMixin { late TabController _tabController; - int _selectedTab = 1; // По умолчанию открываем вкладку "Недельные квесты" - + int _selectedTab = 0; + @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this, initialIndex: _selectedTab); } - + @override void dispose() { _tabController.dispose(); super.dispose(); } - + @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон - appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), - elevation: 0, - leading: Container( - margin: const EdgeInsets.all(8.0), + return Center( + child: Material( + color: Colors.transparent, + child: Container( + width: 380, + constraints: const BoxConstraints(maxHeight: 450), decoration: BoxDecoration( - shape: BoxShape.circle, + color: const Color(0xFFFFF4E3), + borderRadius: BorderRadius.circular(10.0), border: Border.all(color: Colors.black, width: 2), ), - child: Image.asset( - 'assets/icons/профиль.png', - height: 24, - color: Colors.black, - ), - ), - actions: [ - IconButton( - icon: Image.asset( - 'assets/icons/поиск.png', - height: 24, - color: Colors.black, - ), - onPressed: () {}, - ), - IconButton( - icon: Image.asset( - 'assets/icons/уведомления.png', - height: 24, - color: Colors.black, - ), - onPressed: () {}, - ), - ], - ), - body: Column( - children: [ - // Основная карточка с квестами - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black54, width: 1), - ), - child: Stack( + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(top: 0.0, right: 0.0), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Column( - children: [ - // Заголовок - Container( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(7.0), - topRight: Radius.circular(7.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Выполнено квестов 0/5', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - border: Border.all(color: Colors.black, width: 1), - ), - child: const Text( - 'Награда', - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - ), - ), - ), - ], + // Верхняя панель с прогрессом и наградой + Padding( + padding: const EdgeInsets.only(top: 12.0, right: 128.0), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10.0), + ), + child: const Text( + 'Выполнено квестов 0/5', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, ), ), - - // Вкладки - TabBar( + ), + ), + // TabBar как в магазине + Padding( + padding: const EdgeInsets.all(16.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(12.0), + ), + child: TabBar( controller: _tabController, - labelColor: Colors.black, - unselectedLabelColor: Colors.black54, - indicatorColor: Colors.transparent, + dividerColor: Colors.transparent, indicator: const BoxDecoration( color: Color(0xFFD6A067), - border: Border( - bottom: BorderSide( - color: Colors.black, - width: 3.0, - ), - ), + borderRadius: BorderRadius.all(Radius.circular(10.0)), ), + labelColor: Colors.black, + unselectedLabelColor: Colors.black, + indicatorSize: TabBarIndicatorSize.tab, tabs: const [ Tab(text: 'Ежедневные квесты'), Tab(text: 'Недельные квесты'), @@ -145,235 +93,70 @@ class _QuestsScreenState extends State with SingleTickerProviderSt }); }, ), - - // Содержимое вкладок - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - // Вкладка с ежедневными квестами - _buildDailyQuestsTab(), - - // Вкладка с недельными квестами - _buildWeeklyQuestsTab(), - ], - ), - ), - ], + ), ), - - // Кнопка закрытия в правом верхнем углу - Positioned( - top: 12.0, - right: 12.0, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(4.0), - ), - child: InkWell( - onTap: () { - Navigator.pop(context); - }, - child: const Icon( - Icons.close, - color: Colors.black, - size: 24.0, - ), - ), + // Содержимое вкладок + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildDailyQuestsTab(), + _buildWeeklyQuestsTab(), + ], ), ), ], ), ), - ), - ), - - const SizedBox(height: 20.0), - - // Кнопки "Квесты" и "Новости" - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0), - child: Row( - children: [ - // Кнопка "Квесты" - Expanded( - child: ElevatedButton( - onPressed: () { - // Уже на экране квестов - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 12.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/icons/квесты.png', - height: 20.0, - color: Colors.black, - ), - const SizedBox(width: 8.0), - const Text( - 'Квесты', - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ), - - const SizedBox(width: 16.0), - - // Кнопка "Новости" - Expanded( - child: ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const NewsScreen(), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 12.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/icons/новости.png', - height: 20.0, - color: Colors.black, - ), - const SizedBox(width: 8.0), - const Text( - 'Новости', - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - ), - ), - ], + // Кнопка закрытия + Positioned( + top: -2.0, + right: -2.0, + child: InkWell( + onTap: () => Navigator.of(context).pop(), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10.0), + border: Border.all(color: Colors.black, width: 2), ), + padding: const EdgeInsets.all(4.0), + child: const Icon(Icons.close, color: Colors.black, size: 32), ), ), - ], - ), - ), - - const SizedBox(height: 10.0), - - // Нижняя навигационная панель - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 2: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Гл.меню', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', - ), - ], - ), + ), + ], ), - ], + ), ), ); } - - // Вкладка с ежедневными квестами + Widget _buildDailyQuestsTab() { return ListView.builder( padding: const EdgeInsets.all(16.0), - itemCount: 6, + itemCount: 5, itemBuilder: (context, index) { return _buildQuestItem('Ежедневный квест', 200); }, ); } - - // Вкладка с недельными квестами + Widget _buildWeeklyQuestsTab() { return ListView.builder( padding: const EdgeInsets.all(16.0), - itemCount: 4, + itemCount: 5, itemBuilder: (context, index) { return _buildQuestItem('Недельный квест', 1000); }, ); } - - // Элемент квеста + Widget _buildQuestItem(String title, int reward) { return Container( margin: const EdgeInsets.only(bottom: 12.0), child: Row( children: [ - // Название квеста Expanded( child: Text( title, @@ -383,16 +166,15 @@ class _QuestsScreenState extends State with SingleTickerProviderSt ), ), ), - - // Награда Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 7.0), decoration: BoxDecoration( color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(16.0), + borderRadius: BorderRadius.circular(10.0), ), child: Row( children: [ + const SizedBox(width: 12.0), Text( reward.toString(), style: const TextStyle( @@ -401,10 +183,10 @@ class _QuestsScreenState extends State with SingleTickerProviderSt ), ), const SizedBox(width: 4.0), - const Icon( - Icons.monetization_on, - color: Colors.amber, - size: 16.0, + Image.asset( + 'assets/icons/монеты.png', + height: 20, + width: 20, ), ], ), @@ -413,4 +195,20 @@ class _QuestsScreenState extends State with SingleTickerProviderSt ), ); } +} + +// Функция для показа модального окна квестов +void showQuestsDialog(BuildContext context) { + showDialog( + context: context, + barrierDismissible: true, + builder: (context) => Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 40.0), + child: const SizedBox( + width: 380, + child: QuestsDialogContent(), + ), + ), + ); } \ No newline at end of file From 2673933c65cb70711a9d7ea8791d4f865b15617d Mon Sep 17 00:00:00 2001 From: danil13231212341 Date: Sun, 18 May 2025 21:04:09 +0300 Subject: [PATCH 16/54] FCCX-115 add create exchange --- frontend/lib/views/create_card_screen.dart | 207 ++++--- .../lib/views/create_exchange_screen.dart | 566 ++++++++++++++++-- frontend/lib/views/shop_screen.dart | 2 +- .../lib/views/shop_set_details_screen.dart | 2 +- 4 files changed, 641 insertions(+), 136 deletions(-) diff --git a/frontend/lib/views/create_card_screen.dart b/frontend/lib/views/create_card_screen.dart index 18e9caf..08ae860 100644 --- a/frontend/lib/views/create_card_screen.dart +++ b/frontend/lib/views/create_card_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'home_screen.dart'; import 'shop_screen.dart'; import 'exchanges_screen.dart'; +import 'inventory_screen.dart'; +import 'profile_screen.dart'; class CreateCardScreen extends StatefulWidget { const CreateCardScreen({super.key}); @@ -12,32 +14,42 @@ class CreateCardScreen extends StatefulWidget { class _CreateCardScreenState extends State { bool _showCategories = false; - int _currentIndex = 0; // Установлено 0 для главного меню + int _currentIndex = 0; final List _categories = ['Категория 1', 'Категория 2', 'Категория 3', 'Категория 4']; String _selectedCategory = 'Пример категории'; @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон + backgroundColor: const Color(0xFFFFF4E3), appBar: AppBar( backgroundColor: const Color(0xFFFFF4E3), elevation: 0, - leading: Container( - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 2), - ), - child: const Icon( - Icons.person_outline, - color: Colors.black, + leading: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Container( + width: 40.0, + height: 40.0, + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ProfileScreen()), + ); + }, + child: Image.asset('assets/icons/профиль.png', height: 22), + ), ), ), actions: [ IconButton( - icon: const Icon(Icons.search, color: Colors.black), - onPressed: () {}, + icon: Image.asset( + 'assets/icons/уведомления.png', + height: 36, + color: Colors.black, + ), + onPressed: null, ), ], ), @@ -86,10 +98,9 @@ class _CreateCardScreenState extends State { fontSize: 16.0, ), ), - // Значок стрелки меняется в зависимости от состояния Icon( - _showCategories ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, - color: Colors.black + _showCategories ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, + color: Colors.black, ), ], ), @@ -100,17 +111,13 @@ class _CreateCardScreenState extends State { ), ), - // Основное содержимое - макет карточки или список категорий + // Основное содержимое Expanded( child: Stack( + alignment: Alignment.center, children: [ - // Макет карточки всегда показывается в фоне - Padding( - padding: const EdgeInsets.all(16.0), - child: _buildCardTemplate(), - ), + _buildCardTemplate(), - // Список категорий показывается поверх, если _showCategories = true if (_showCategories) Positioned( top: 16.0, @@ -164,36 +171,40 @@ class _CreateCardScreenState extends State { Container( decoration: const BoxDecoration( color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16.0), - topRight: Radius.circular(16.0), + border: Border( + top: BorderSide( + color: Colors.black, + width: 1.0, + ), ), ), child: BottomNavigationBar( currentIndex: _currentIndex, onTap: (index) { if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - - // Навигация в зависимости от выбранного индекса switch (index) { - case 0: // Главное меню + case 0: Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (context) => const HomeScreen()), (route) => false, ); break; - case 2: // Магазин + case 1: + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const InventoryScreen()), + (route) => false, + ); + break; + case 2: Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (context) => const ShopScreen()), (route) => false, ); break; - case 3: // Обменчик + case 3: Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (context) => const ExchangesScreen()), @@ -208,22 +219,69 @@ class _CreateCardScreenState extends State { type: BottomNavigationBarType.fixed, selectedItemColor: Colors.black, unselectedItemColor: Colors.black54, - items: const [ + showSelectedLabels: true, + showUnselectedLabels: true, + selectedIconTheme: const IconThemeData( + size: 28, + ), + unselectedIconTheme: const IconThemeData( + size: 24, + ), + selectedLabelStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + unselectedLabelStyle: const TextStyle( + fontSize: 11, + ), + items: [ BottomNavigationBarItem( - icon: Icon(Icons.home), + icon: Image.asset('assets/icons/главная.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/главная.png', height: 24), + ), label: 'Главная', ), BottomNavigationBarItem( - icon: Icon(Icons.book), - label: '', + icon: Image.asset('assets/icons/Инвентарь.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/Инвентарь.png', height: 24), + ), + label: 'Инвентарь', ), BottomNavigationBarItem( - icon: Icon(Icons.storefront), - label: '', + icon: Image.asset('assets/icons/магазин.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/магазин.png', height: 24), + ), + label: 'Магазин', ), BottomNavigationBarItem( - icon: Icon(Icons.people), - label: '', + icon: Image.asset('assets/icons/обменник.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/обменник.png', height: 24), + ), + label: 'Обменник', ), ], ), @@ -233,66 +291,55 @@ class _CreateCardScreenState extends State { ); } - // Метод построения шаблона карточки Widget _buildCardTemplate() { return Container( + width: 300, + height: 450, decoration: BoxDecoration( color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 2), + borderRadius: BorderRadius.circular(12.0), + border: Border.all(color: Colors.black, width: 3), ), - child: Column( - children: [ - // Верхняя часть карточки - Expanded( - flex: 2, - child: Container( - width: double.infinity, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - border: Border( - bottom: BorderSide(color: Colors.black, width: 2), - ), - ), + child: Container( + margin: const EdgeInsets.all(10), + decoration: BoxDecoration( + border: Border.all(color: Colors.black, width: 3), + borderRadius: BorderRadius.circular(8.0), + ), + child: Column( + children: [ + Expanded( + flex: 7, + child: Container(), ), - ), - - // Нижняя часть карточки - Expanded( - flex: 1, - child: Container( - width: double.infinity, - color: const Color(0xFFD6A067), + Container( + height: 3, + color: Colors.black, ), - ), - ], + Expanded( + flex: 3, + child: Container(), + ), + ], + ), ), ); } - // Метод построения элемента категории Widget _buildCategoryItem(String category) { - return GestureDetector( + return InkWell( onTap: () { setState(() { _selectedCategory = category; _showCategories = false; }); }, - child: Container( - width: double.infinity, - margin: const EdgeInsets.only(bottom: 8.0), - padding: const EdgeInsets.symmetric(vertical: 12.0), - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(4.0), - ), - alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), child: Text( category, style: const TextStyle( fontSize: 16.0, - fontWeight: FontWeight.w500, color: Colors.black, ), ), diff --git a/frontend/lib/views/create_exchange_screen.dart b/frontend/lib/views/create_exchange_screen.dart index a8d9a5b..97531a6 100644 --- a/frontend/lib/views/create_exchange_screen.dart +++ b/frontend/lib/views/create_exchange_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'home_screen.dart'; import 'shop_screen.dart'; import 'exchanges_screen.dart'; +import 'inventory_screen.dart'; +import 'profile_screen.dart'; class CreateExchangeScreen extends StatefulWidget { const CreateExchangeScreen({super.key}); @@ -12,6 +14,64 @@ class CreateExchangeScreen extends StatefulWidget { class _CreateExchangeScreenState extends State { int _currentIndex = 3; + Map? selectedTopCard; + List> selectedExchangeCards = []; + + void _showTopCardSelectionDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + backgroundColor: const Color(0xFFFFF4E3), + child: Container( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height * 0.8, + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Выберите вашу карту', + style: TextStyle( + fontSize: 20.0, + fontFamily: 'Jost', + color: Colors.black, + ), + ), + const SizedBox(height: 16.0), + Expanded( + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 0.7, + crossAxisSpacing: 10.0, + mainAxisSpacing: 10.0, + ), + itemCount: 40, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + setState(() { + selectedTopCard = { + 'id': index, + 'name': 'Карта $index', + }; + }); + Navigator.pop(context); + }, + child: _buildCollectionCard(index), + ); + }, + ), + ), + ], + ), + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -21,9 +81,9 @@ class _CreateExchangeScreenState extends State { elevation: 0, leading: Container( margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( + decoration: const BoxDecoration( shape: BoxShape.circle, - color: const Color(0xFFD6A067), + color: Color(0xFFD6A067), ), child: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.black), @@ -34,36 +94,156 @@ class _CreateExchangeScreenState extends State { 'Создание обмена', style: TextStyle( color: Colors.black, - fontSize: 18.0, + fontSize: 20.0, + fontFamily: 'Jost', ), ), ), body: Column( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 0.7, - crossAxisSpacing: 10.0, - mainAxisSpacing: 10.0, - ), - itemCount: 12, - itemBuilder: (context, index) { - return _buildCardItem(); - }, + // Заголовок "Ваша карта для обмена" + const Padding( + padding: EdgeInsets.only(top: 20.0), + child: Text( + 'Ваша карта для обмена', + style: TextStyle( + color: Colors.black, + fontSize: 20.0, + fontFamily: 'Jost', ), ), ), - + // Карточка для выбора своей карты (одноразовый выбор) + Padding( + padding: const EdgeInsets.only(top: 10.0), + child: selectedTopCard == null + ? GestureDetector( + onTap: _showTopCardSelectionDialog, + child: _buildCardPlaceholder(), + ) + : _buildCollectionCard( + selectedTopCard!['id'], + onRemove: () { + setState(() { + selectedTopCard = null; + }); + }, + ), + ), + const SizedBox(height: 20.0), + // Текст для карт обмена + const Column( + children: [ + Text( + 'Карта, на которую вы', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + fontSize: 20.0, + fontFamily: 'Jost', + ), + ), + SizedBox(height: 5.0), + Text( + 'готовы произвести обмен', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + fontSize: 20.0, + fontFamily: 'Jost', + ), + ), + ], + ), + const SizedBox(height: 10.0), + // Ряды карточек для обмена (многоразовый выбор) + Expanded( + child: _buildExchangeCardsRow(), + ), + // Кнопка создания обмена Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( - onPressed: () { - Navigator.pop(context); - }, + onPressed: (selectedTopCard == null || selectedExchangeCards.isEmpty) + ? () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Container( + width: 367, + height: 61, + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(15.0), + ), + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), + child: const Center( + child: Text( + 'Добавьте карточки для обмена', + style: TextStyle( + color: Colors.black, + fontSize: 18.0, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + ), + textAlign: TextAlign.center, + ), + ), + ), + backgroundColor: Colors.transparent, + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height - 120, + left: 16, + right: 16, + ), + elevation: 0, + ), + ); + } + : () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Container( + width: 367, + height: 61, + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(15.0), + ), + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), + child: const Center( + child: Text( + 'Обмен создан', + style: TextStyle( + color: Colors.black, + fontSize: 18.0, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + ), + textAlign: TextAlign.center, + ), + ), + ), + backgroundColor: Colors.transparent, + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height - 200, + left: 16, + right: 16, + ), + elevation: 0, + ), + ); + + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const ExchangesScreen()), + (route) => false, + ); + }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFD6A067), foregroundColor: Colors.black, @@ -71,33 +251,34 @@ class _CreateExchangeScreenState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.0), ), + disabledBackgroundColor: const Color(0xFFD6A067).withOpacity(0.7), + disabledForegroundColor: Colors.black.withOpacity(0.5), ), child: const Text( 'Создать обмен', style: TextStyle( - fontSize: 16.0, + fontSize: 20.0, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), ), ), - + // Нижняя навигация Container( decoration: const BoxDecoration( color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16.0), - topRight: Radius.circular(16.0), + border: Border( + top: BorderSide( + color: Colors.black, + width: 1.0, + ), ), ), child: BottomNavigationBar( currentIndex: _currentIndex, onTap: (index) { if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { case 0: Navigator.pushAndRemoveUntil( @@ -106,6 +287,13 @@ class _CreateExchangeScreenState extends State { (route) => false, ); break; + case 1: + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const InventoryScreen()), + (route) => false, + ); + break; case 2: Navigator.pushAndRemoveUntil( context, @@ -128,22 +316,71 @@ class _CreateExchangeScreenState extends State { type: BottomNavigationBarType.fixed, selectedItemColor: Colors.black, unselectedItemColor: Colors.black54, - items: const [ + showSelectedLabels: true, + showUnselectedLabels: true, + selectedIconTheme: const IconThemeData( + size: 24, + ), + unselectedIconTheme: const IconThemeData( + size: 24, + ), + selectedLabelStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + fontFamily: 'Jost', + ), + unselectedLabelStyle: const TextStyle( + fontSize: 12, + fontFamily: 'Jost', + ), + items: [ BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'Гл.меню', + icon: Image.asset('assets/icons/главная.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/главная.png', height: 24), + ), + label: 'Главная', ), BottomNavigationBarItem( - icon: Icon(Icons.book), - label: '', + icon: Image.asset('assets/icons/Инвентарь.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/Инвентарь.png', height: 24), + ), + label: 'Инвентарь', ), BottomNavigationBarItem( - icon: Icon(Icons.storefront), + icon: Image.asset('assets/icons/магазин.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/магазин.png', height: 24), + ), label: 'Магазин', ), BottomNavigationBarItem( - icon: Icon(Icons.people), - label: 'Обменчик', + icon: Image.asset('assets/icons/обменник.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/обменник.png', height: 24), + ), + label: 'Обменник', ), ], ), @@ -153,37 +390,258 @@ class _CreateExchangeScreenState extends State { ); } - Widget _buildCardItem() { + Widget _buildCollectionCard(int index, {bool isSelected = true, Function()? onRemove}) { + return Stack( + children: [ + Container( + width: 80, + height: 120, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(6.0), + border: Border.all(color: Colors.black, width: 2), + ), + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.black, width: 2), + borderRadius: BorderRadius.circular(6.0), + ), + ), + Positioned( + top: 5, + left: 5, + right: 5, + bottom: 5, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.black, width: 2), + borderRadius: BorderRadius.circular(4.0), + ), + ), + ), + Positioned( + left: 5, + right: 5, + bottom: 30, + child: Container( + height: 1, + color: Colors.black, + ), + ), + Center( + child: Text( + 'Карта $index', + style: const TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'Jost', + ), + ), + ), + ], + ), + ), + if (isSelected && onRemove != null) + Positioned( + top: -5, + right: -5, + child: GestureDetector( + onTap: onRemove, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: const Color(0xFFFFF4E3), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.black, width: 1), + ), + child: const Center( + child: Icon( + Icons.close, + size: 14, + color: Colors.black, + ), + ), + ), + ), + ), + ], + ); + } + + Widget _buildCardPlaceholder() { return Container( + width: 80, + height: 120, decoration: BoxDecoration( color: const Color(0xFFD6A067), borderRadius: BorderRadius.circular(6.0), - border: Border.all(color: Colors.black, width: 1), + border: Border.all(color: Colors.black, width: 2), ), - child: Column( + child: Stack( children: [ - Expanded( - flex: 2, + Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.black, width: 2), + borderRadius: BorderRadius.circular(6.0), + ), + ), + Positioned( + top: 5, + left: 5, + right: 5, + bottom: 5, child: Container( - width: double.infinity, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - border: Border( - bottom: BorderSide(color: Colors.black, width: 1), - ), + decoration: BoxDecoration( + border: Border.all(color: Colors.black, width: 2), + borderRadius: BorderRadius.circular(4.0), ), ), ), - - Expanded( - flex: 1, + Positioned( + left: 5, + right: 5, + bottom: 30, child: Container( - width: double.infinity, - color: const Color(0xFFD6A067), + height: 1, + color: Colors.black, + ), + ), + Center( + child: Icon( + Icons.add, + size: 24, + color: Colors.black.withOpacity(0.5), ), ), ], ), ); } + + Widget _buildExchangeCardsRow() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Первый ряд карточек (максимум 4) + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + ...selectedExchangeCards.take(4).map((card) => Padding( + padding: const EdgeInsets.only(right: 10.0), + child: _buildCollectionCard( + card['id'], + onRemove: () { + setState(() { + selectedExchangeCards.remove(card); + }); + }, + ), + )).toList(), + if (selectedExchangeCards.length < 4) + GestureDetector( + onTap: _showLargeCardSelectionDialog, + child: _buildCardPlaceholder(), + ), + ], + ), + ), + ), + // Второй ряд карточек (максимум 4) + if (selectedExchangeCards.length >= 4) + Padding( + padding: const EdgeInsets.only(left: 16.0, top: 10.0), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + ...selectedExchangeCards.skip(4).take(4).map((card) => Padding( + padding: const EdgeInsets.only(right: 10.0), + child: _buildCollectionCard( + card['id'], + onRemove: () { + setState(() { + selectedExchangeCards.remove(card); + }); + }, + ), + )).toList(), + if (selectedExchangeCards.length < 8) + GestureDetector( + onTap: _showLargeCardSelectionDialog, + child: _buildCardPlaceholder(), + ), + ], + ), + ), + ), + ], + ); + } + + void _showLargeCardSelectionDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + backgroundColor: const Color(0xFFFFF4E3), + child: Container( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height * 0.8, + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Выберите карту для обмена', + style: TextStyle( + fontSize: 20.0, + fontFamily: 'Jost', + color: Colors.black, + ), + ), + const SizedBox(height: 16.0), + Expanded( + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 0.7, + crossAxisSpacing: 10.0, + mainAxisSpacing: 10.0, + ), + itemCount: 40, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + if (selectedExchangeCards.length < 8) { + setState(() { + selectedExchangeCards.add({ + 'id': index, + 'name': 'Карта $index', + }); + }); + Navigator.pop(context); + } + }, + child: Container( + width: 80, + height: 120, + child: _buildCollectionCard(index), + ), + ); + }, + ), + ), + ], + ), + ), + ); + }, + ); + } } \ No newline at end of file diff --git a/frontend/lib/views/shop_screen.dart b/frontend/lib/views/shop_screen.dart index 8891c7e..67d3038 100644 --- a/frontend/lib/views/shop_screen.dart +++ b/frontend/lib/views/shop_screen.dart @@ -282,7 +282,7 @@ class _ShopScreenState extends State with SingleTickerProviderStateM }, child: Container( width: 321.0, - height: 85.0, + height: 120.0, decoration: BoxDecoration( color: const Color(0xFFD6A067), borderRadius: BorderRadius.circular(10.0), diff --git a/frontend/lib/views/shop_set_details_screen.dart b/frontend/lib/views/shop_set_details_screen.dart index bb73002..761d3d7 100644 --- a/frontend/lib/views/shop_set_details_screen.dart +++ b/frontend/lib/views/shop_set_details_screen.dart @@ -159,7 +159,7 @@ class _ShopSetDetailsScreenState extends State with Single children: [ // Превью набора Container( - height: 85.0, + height: 120.0, width: 321.0, decoration: BoxDecoration( color: const Color(0xFFD6A067), From 6beb1393fe012a0e718683fda843dea39f112f69 Mon Sep 17 00:00:00 2001 From: danil13231212341 Date: Tue, 20 May 2025 00:33:30 +0300 Subject: [PATCH 17/54] FCCX-115 add exchange details --- .../lib/views/exchange_details_screen.dart | 547 ++++++------------ frontend/lib/views/exchanges_screen.dart | 54 +- 2 files changed, 232 insertions(+), 369 deletions(-) diff --git a/frontend/lib/views/exchange_details_screen.dart b/frontend/lib/views/exchange_details_screen.dart index 8c01134..f6e6c5c 100644 --- a/frontend/lib/views/exchange_details_screen.dart +++ b/frontend/lib/views/exchange_details_screen.dart @@ -1,356 +1,217 @@ import 'package:flutter/material.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; import 'exchanges_screen.dart'; -class ExchangeDetailsScreen extends StatefulWidget { +class ExchangeDetailsScreen extends StatelessWidget { const ExchangeDetailsScreen({super.key}); - @override - State createState() => _ExchangeDetailsScreenState(); -} - -class _ExchangeDetailsScreenState extends State { - int _currentIndex = 3; // Индекс вкладки "Обмены" в нижней навигации - @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон - appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black), - onPressed: () => Navigator.pop(context), - ), - title: const Text( - 'Детали обмена', - style: TextStyle( - color: Colors.black, - fontSize: 20.0, - fontWeight: FontWeight.bold, - ), - ), + return Dialog( + backgroundColor: const Color(0xFFFFF4E3), + insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), + side: const BorderSide(color: Colors.black, width: 2), ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Информация об обмене - Card( - color: const Color(0xFFD6A067), - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 24.0), + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 346, maxWidth: 346, minHeight: 0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Дата и никнейм в одной строке + Padding( + padding: const EdgeInsets.only(top: 4.0, left: 2.0, right: 54.0, bottom: 16.0), + child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - 'Обмен #12345', - style: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, + '12.05.2023', + style: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + color: Colors.black, ), ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10.0, - vertical: 4.0, - ), - decoration: BoxDecoration( - color: Colors.orange, - borderRadius: BorderRadius.circular(20.0), - ), - child: const Text( - 'В ожидании', - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - color: Colors.black, - ), + Text( + 'CardMaster475', + style: const TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + color: Colors.black87, ), ), ], ), - const SizedBox(height: 12.0), - const Text( - 'Дата создания: 12.05.2023', - style: TextStyle(fontSize: 14.0), - ), - const SizedBox(height: 4.0), - Row( - children: const [ - Icon(Icons.person, size: 16.0), - SizedBox(width: 4.0), - Text( - 'Пользователь: CardMaster2000', - style: TextStyle(fontSize: 14.0), + ), + // Карточки и статус как на скрине + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Stack из двух карточек (стопка) + Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + left: 6, + top: -4, + child: _buildCardItem('Карточка 2', 'Обычная', width: 48, height: 64), + ), + _buildCardItem('Карточка 1', 'Обычная', width: 48, height: 64), + ], + ), + // Статус по центру + Expanded( + child: Center( + child: Text( + 'В ожидании', + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 20, + fontFamily: 'Jost', + ), + ), + ), ), + // Карточка пользователя + _buildCardItem('Карточка пользователя', 'Редкая', width: 48, height: 64), ], ), - const SizedBox(height: 4.0), - Row( - children: const [ - Icon(Icons.swap_horiz, size: 16.0), - SizedBox(width: 4.0), - Text( - 'Тип: 2 карточки на 1 карточку', - style: TextStyle(fontSize: 14.0), + ), + const SizedBox(height: 12.0), + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 729, + height: 60, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + fontFamily: 'Jost', + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + ), + onPressed: () { + _showConfirmationDialog(context, 'Принять обмен?', false); + }, + child: const Text('Принять'), + ), + ), + const SizedBox(height: 12.0), + SizedBox( + width: 729, + height: 60, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + fontFamily: 'Jost', + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + ), + onPressed: () { + _showConfirmationDialog(context, 'Отклонить обмен?', true); + }, + child: const Text('Отклонить'), + ), ), ], ), - ], - ), - ), - ), - - const SizedBox(height: 20.0), - - // Секция "Мои карточки" - const Text( - 'Мои карточки для обмена:', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12.0), - SizedBox( - height: 180, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: 2, - itemBuilder: (context, index) { - return _buildCardItem('Карточка ${index + 1}', 'Редкость: Обычная'); - }, - ), - ), - - const SizedBox(height: 20.0), - - // Секция "Карточки пользователя" - const Text( - 'Карточки пользователя для обмена:', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12.0), - SizedBox( - height: 180, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: 1, - itemBuilder: (context, index) { - return _buildCardItem('Карточка пользователя', 'Редкость: Редкая'); - }, + ), + ], ), ), - - const SizedBox(height: 24.0), - - // Кнопки действий - Row( - children: [ - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - onPressed: () { - // Логика отклонения обмена - _showConfirmationDialog('Отклонить обмен?', true); - }, - child: const Text('Отклонить'), - ), + ), + // Крестик в стиле магазина с внутренним отступом + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.black, width: 3), ), - const SizedBox(width: 12.0), - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - onPressed: () { - // Логика принятия обмена - _showConfirmationDialog('Принять обмен?', false); - }, - child: const Text('Принять'), + child: const Center( + child: Icon( + Icons.close, + color: Colors.black, + size: 28, ), ), - ], - ), - - const SizedBox(height: 16.0), - - // Кнопка отмены (для своих обменов) - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 12.0), - minimumSize: const Size(double.infinity, 48.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - onPressed: () { - // Логика отмены обмена - _showConfirmationDialog('Вы уверены, что хотите отменить свой обмен?', true); - }, - child: const Text( - 'Отменить обмен', - style: TextStyle(fontWeight: FontWeight.bold), ), ), - ], - ), - ), - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - - // Навигация в зависимости от выбранного индекса - switch (index) { - case 0: // Главное меню - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 2: // Магазин - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: // Обмены - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', - ), - ], - ), + ), + ], ), ); } - - // Метод для создания виджета карточки - Widget _buildCardItem(String title, String rarity) { + + Widget _buildCardItem(String title, String rarity, {double width = 80, double height = 120}) { return Container( - width: 120.0, - margin: const EdgeInsets.only(right: 12.0), + width: width, + height: height, + margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0), + alignment: Alignment.center, decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black54, width: 1), + color: const Color(0xFFD9A76A), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Изображение карточки (заглушка) - Container( - height: 100.0, - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(8.0), - topRight: Radius.circular(8.0), + child: Container( + margin: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: const Color(0xFFD9A76A), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.black, width: 1.5), + ), + child: Stack( + children: [ + // Горизонтальная линия внизу + Positioned( + left: 0, + right: 0, + bottom: height * 0.2, + child: Container( + height: 2, + color: Colors.black.withOpacity(0.5), ), ), - child: const Center( - child: Icon(Icons.image, size: 40.0, color: Colors.black54), - ), - ), - // Информация о карточке - Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4.0), - Text( - rarity, - style: const TextStyle( - fontSize: 12.0, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], + ], + ), ), ); } - - // Метод для отображения диалога подтверждения - void _showConfirmationDialog(String message, bool isDecline) { + + void _showConfirmationDialog(BuildContext context, String message, bool isDecline) { showDialog( context: context, builder: (BuildContext context) { @@ -377,62 +238,18 @@ class _ExchangeDetailsScreenState extends State { foregroundColor: isDecline ? Colors.red : Colors.green, ), onPressed: () { - Navigator.of(context).pop(); // Закрыть диалог - - // Показать сообщение в зависимости от действия - String messageText; - if (message == 'Вы уверены, что хотите отменить свой обмен?') { - messageText = 'Обмен отменен'; - } else { - messageText = isDecline ? 'Обмен отклонен' : 'Обмен успешно осуществлен'; - } - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Container( - width: 367, - height: 61, - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(15.0), - ), - padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), - child: Center( - child: Text( - messageText, - style: const TextStyle( - color: Colors.black, - fontSize: 18.0, - fontWeight: FontWeight.w500, - ), - textAlign: TextAlign.center, - ), - ), - ), - backgroundColor: Colors.transparent, - duration: const Duration(seconds: 2), - behavior: SnackBarBehavior.floating, - margin: EdgeInsets.only( - bottom: MediaQuery.of(context).size.height - 200, - left: 16, - right: 16, + Navigator.of(context).pop(); + Navigator.of(context).pop(); + // Передаю текст уведомления на ExchangesScreen + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => ExchangesScreen( + notification: isDecline ? 'Обмен отклонен' : 'Обмен принят', ), - elevation: 0, ), + (route) => false, ); - - if (!isDecline && message != 'Вы уверены, что хотите отменить свой обмен?') { - // Вернуться на экран обменов при принятии - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - } else { - Navigator.of(context).pop(); // Просто вернуться назад при отклонении или отмене - } - - // Здесь можно добавить вызов API для обработки обмена }, child: Text(isDecline ? 'Отклонить' : 'Принять'), ), diff --git a/frontend/lib/views/exchanges_screen.dart b/frontend/lib/views/exchanges_screen.dart index 3671833..291e978 100644 --- a/frontend/lib/views/exchanges_screen.dart +++ b/frontend/lib/views/exchanges_screen.dart @@ -8,7 +8,8 @@ import 'profile_screen.dart'; import 'search_players_screen.dart'; class ExchangesScreen extends StatefulWidget { - const ExchangesScreen({super.key}); + final String? notification; + const ExchangesScreen({super.key, this.notification}); @override State createState() => _ExchangesScreenState(); @@ -24,6 +25,44 @@ class _ExchangesScreenState extends State with SingleTickerProv void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); + // Показываем Snackbar, если notification не null + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.notification != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Container( + width: 367, + height: 61, + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(15.0), + ), + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), + child: Center( + child: Text( + widget.notification!, + style: const TextStyle( + color: Colors.black, + fontSize: 18.0, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ), + ), + backgroundColor: Colors.transparent, + duration: const Duration(seconds: 5), + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height - 200, + left: 16, + right: 16, + ), + elevation: 0, + ), + ); + } + }); } @override @@ -364,9 +403,16 @@ class _ExchangesScreenState extends State with SingleTickerProv Widget _buildCardExchangeItem() { return GestureDetector( onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ExchangeDetailsScreen()), + showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: ExchangeDetailsScreen(), + ); + }, ); }, child: Container( From 5949fc98182f3c4603f0e1d2a6de90fc69cd775c Mon Sep 17 00:00:00 2001 From: birbik Date: Tue, 20 May 2025 00:52:03 +0300 Subject: [PATCH 18/54] FCCX-117 add profile other user, report screen --- ...0\260\320\273\320\276\320\261\320\260.png" | Bin 0 -> 1755 bytes frontend/lib/views/profile_image_dialog.dart | 2 +- frontend/lib/views/profile_screen.dart | 305 +++++++++++++++--- frontend/lib/views/search_players_screen.dart | 107 +++--- 4 files changed, 331 insertions(+), 83 deletions(-) create mode 100644 "frontend/assets/icons/\320\266\320\260\320\273\320\276\320\261\320\260.png" diff --git "a/frontend/assets/icons/\320\266\320\260\320\273\320\276\320\261\320\260.png" "b/frontend/assets/icons/\320\266\320\260\320\273\320\276\320\261\320\260.png" new file mode 100644 index 0000000000000000000000000000000000000000..46bc45057b4203a65df69d9ecd5967f1b0eba327 GIT binary patch literal 1755 zcmY*a2{fBo8~(m8l9Vc`WyV@9e>>5RMwg)~iq_sC1hqx&M%x*?s#+4A)*9OsRcnM6 zt&V@Dh7yffYMTn$OlgDIf>4Aal*+{4o|*ICd*1sz?>*0Z&pr2f&$-FAHs<>zlq3KE z*k@^Bf)}CZZtOvb)``5xGa>-P@aE@%UzsXAQF7bQ+0x(I8aOKAdjL2n1Hg7Aq6vUX z0Q`R(0IWdee>fgQf6+hykaz_U`=W6b;qJ&3g{b-q!YJUsCsLq))SeXBzkIihwwKNp zfq1ZmOBeurb$B;GOZM6~?iinN3DBXUWLr>%;%0XJchGT}m3wpY(9|(A!GN#E5j(<$JNjz7sfw+KA+x)Q zFdFMP@+nY&<=pS(HAc*jKC+Ma=omTpZGOL+qBA+wk)b%oWymv#@f=trj00N~V}i(5 zJJ>ZxMJ9vFm)vUzI&JHOuWjeMpCheHLsdz1uLmT0?dcy>%;CWU22%Nh$bnu?%9A9a zHos&v=Kc3I)`R;_YNtyKL2+f5=(@2lX{Ce460?3~cjiOzK%~AtBW!UfN}Aa9vQHN6 zMhnp=Oa(zDnCtT2;Tc|W?;6fLEUHcO=gJ-zY&IM#DZ&TqsGzjsaNt-gXNG-r@|Z13BqXmurpS+ z7&D}1h34Ujfrj*Sk8G%MDWM!lTzmL3#&G}rj4l3P=2ZDF-8&@Vx0w~vp=~j+&hBAE zM-ZmRzJreBUL+gTKdTA;e1}vAz-dQ+HhQFcx}H_AALAq*)FP}qDD^mDIU{<^##{3ma={m2$r`I(Tt9m)6rWyElf-On#6X_N+$3e z)QPf8=8rhtx@aJw0n)QLE3WKMnZs;Q0uPvQlhb86Hak+RJonxEasabJF~hI_LdzPf+> z0ZC&vfQ~8!7V9et$A_i1>}?EsSAE{9m()HP$?c1e513CBwIXSFfv;B{bU>4W(dt#y zkYgwcVTlMBV7n&B%?PpD5%8LJ32xF0!&-@POSWPPO7@}W@(*5t2 zFT3Fgv*jGBoD{#>?qr5V7A6FKIPg5Yt8I+k{{bZ!G>M@JXHtk_3(hvTldEze?;E}8 zO3TU&!H1`_FKCR$yS*51a)Ilng}!kPuC0GA<%f!-2um*&FH^*`vqyFMuIj)rqpA1& z{>YD8tz1t zk#EWbMy)550>z$gslQbtR#F{VsHl%5PPT^$Rjo|MtUU)(ch=hfGzH4lIM=rn_5yP} zOSd(r0#%umW-(Eul{R~UmxnjH(v!fZkfBBMxn83s!>&Yj+uXp;dHdq-n7W&l&vNlb zD)lZ|NKH?1^(L{VQ^zKnULPXFA-2Uf^!UdppI${AeIJwlOp<$z7o*7g$^vtI3Jt2ApKA&w|a)yJ{*5t0AZNqr>b#&@ const SettingsDialog(), - ); - }, - child: Image.asset( - 'assets/icons/настройки.png', - color: Colors.black, - height: 22.0, + if (isOtherUser) + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Container( + width: 40.0, + height: 40.0, + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + showDialog( + context: context, + barrierColor: Colors.black.withOpacity(0.2), + builder: (context) => ReportDialog(playerName: playerName ?? ''), + ); + }, + child: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(3.14159), + child: Image.asset( + 'assets/icons/жалоба.png', + height: 22.0, + color: Colors.black, + ), + ), + ), + ), + ) + else + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Container( + width: 40.0, + height: 40.0, + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + showDialog( + context: context, + barrierColor: Colors.black.withOpacity(0.2), + builder: (context) => const SettingsDialog(), + ); + }, + child: Image.asset( + 'assets/icons/настройки.png', + color: Colors.black, + height: 22.0, + ), ), ), ), - ), ], ), body: Column( @@ -72,7 +113,7 @@ class ProfileScreen extends StatelessWidget { // Аватар пользователя GestureDetector( - onTap: () { + onTap: isOtherUser ? null : () { showDialog( context: context, barrierColor: Colors.black.withOpacity(0.2), @@ -100,9 +141,9 @@ class ProfileScreen extends StatelessWidget { const SizedBox(height: 16.0), // Имя пользователя - const Text( - 'Ник', - style: TextStyle( + Text( + isOtherUser ? playerName ?? '' : 'Ник', + style: const TextStyle( fontSize: 18.0, fontWeight: FontWeight.bold, ), @@ -111,9 +152,9 @@ class ProfileScreen extends StatelessWidget { const SizedBox(height: 4.0), // ID пользователя - const Text( - '######', - style: TextStyle( + Text( + isOtherUser ? playerId ?? '' : '######', + style: const TextStyle( fontSize: 16.0, color: Colors.black54, ), @@ -172,9 +213,9 @@ class ProfileScreen extends StatelessWidget { ), ); }, - child: const Text( - 'Все достижения →', - style: TextStyle( + child: Text( + isOtherUser ? 'Достижения →' : 'Все достижения →', + style: const TextStyle( fontSize: 13.0, fontWeight: FontWeight.normal, color: Colors.black, @@ -196,8 +237,8 @@ class ProfileScreen extends StatelessWidget { children: [ // Собрано карт Row( - children: const [ - Text( + children: [ + const Text( 'Собрано карт: ', style: TextStyle( fontSize: 16.0, @@ -205,8 +246,8 @@ class ProfileScreen extends StatelessWidget { ), ), Text( - '****', - style: TextStyle( + isOtherUser ? (cardsCollected?.toString() ?? '0') : '****', + style: const TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, ), @@ -218,8 +259,8 @@ class ProfileScreen extends StatelessWidget { // Собрано коллекций Row( - children: const [ - Text( + children: [ + const Text( 'Собрано коллекций: ', style: TextStyle( fontSize: 16.0, @@ -227,14 +268,43 @@ class ProfileScreen extends StatelessWidget { ), ), Text( - '***', - style: TextStyle( + isOtherUser ? (collectionsCollected?.toString() ?? '0') : '***', + style: const TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, ), ), ], ), + if (isOtherUser) ...[ + const SizedBox(height: 18.0), + // Кнопка посмотреть инвентарь + Align( + alignment: Alignment.centerLeft, + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => InventoryScreen( + isOtherUser: true, + playerName: playerName ?? '', + playerId: playerId ?? '', + ), + ), + ); + }, + child: const Text( + 'Посмотреть инвентарь →', + style: TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + ), + ), + ], ], ), ), @@ -440,4 +510,159 @@ class ProfileScreen extends StatelessWidget { ), ); } +} + +class ReportDialog extends StatefulWidget { + final String playerName; + const ReportDialog({super.key, required this.playerName}); + + @override + State createState() => _ReportDialogState(); +} + +class _ReportDialogState extends State { + int _selectedReason = 0; + final TextEditingController _controller = TextEditingController(); + final List _reasons = [ + 'причина 1', + 'причина 2', + 'причина 3', + ]; + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: const Color(0xFFEAD7C3), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Container( + width: 350, + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFD6A067), + ), + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () => Navigator.pop(context), + child: const Icon(Icons.arrow_back, color: Colors.black, size: 29), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Подать жалобу на пользователя ${widget.playerName}', + style: const TextStyle(fontSize: 17), + ), + ), + ], + ), + const SizedBox(height: 18), + ...List.generate(_reasons.length, (i) => Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: Row( + children: [ + Text( + _reasons[i], + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + ), + const SizedBox(width: 12), + Radio( + value: i, + groupValue: _selectedReason, + onChanged: (val) => setState(() => _selectedReason = val!), + activeColor: Colors.black, + ), + ], + ), + )), + const SizedBox(height: 12), + TextField( + controller: _controller, + maxLines: 5, + decoration: InputDecoration( + hintText: 'Комментарий', + filled: true, + fillColor: const Color(0xFFFBF6EF), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.all(16), + ), + ), + const SizedBox(height: 18), + SizedBox( + width: double.infinity, + height: 54, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + textStyle: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + ), + onPressed: () { + Navigator.pop(context); + showTopReportSuccess(context); + }, + child: const Text('Отправить жалобу'), + ), + ), + ], + ), + ), + ); + } +} + +void showTopReportSuccess(BuildContext context) { + final overlay = Overlay.of(context); + final overlayEntry = OverlayEntry( + builder: (context) => Positioned( + top: 40, + left: 16, + right: 16, + child: Material( + color: Colors.transparent, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.18), + blurRadius: 8, + offset: const Offset(0, 6), + ), + ], + ), + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 5), + child: const Center( + child: Text( + 'Жалоба успешно отправлена', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + ), + ), + ), + ), + ); + overlay.insert(overlayEntry); + Future.delayed(const Duration(seconds: 3), () => overlayEntry.remove()); } \ No newline at end of file diff --git a/frontend/lib/views/search_players_screen.dart b/frontend/lib/views/search_players_screen.dart index d25a3d8..a0106de 100644 --- a/frontend/lib/views/search_players_screen.dart +++ b/frontend/lib/views/search_players_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'profile_screen.dart'; class SearchPlayersModal extends StatelessWidget { const SearchPlayersModal({super.key}); @@ -6,11 +7,11 @@ class SearchPlayersModal extends StatelessWidget { @override Widget build(BuildContext context) { final List players = [ - PlayerItem(name: 'Cardly', cards: 5), - PlayerItem(name: 'Cardly1', cards: 5), - PlayerItem(name: 'Cardly2', cards: 5), - PlayerItem(name: 'Cardly3', cards: 5), - PlayerItem(name: 'Cardly4', cards: 5), + PlayerItem(name: 'Cardly', cards: 5, id: '123456'), + PlayerItem(name: 'Cardly1', cards: 5, id: '123457'), + PlayerItem(name: 'Cardly2', cards: 5, id: '123458'), + PlayerItem(name: 'Cardly3', cards: 5, id: '123459'), + PlayerItem(name: 'Cardly4', cards: 5, id: '123460'), ]; final TextEditingController _searchController = TextEditingController(); final double modalHeight = MediaQuery.of(context).size.height * 0.6; @@ -92,8 +93,13 @@ class SearchPlayersModal extends StatelessWidget { class PlayerItem { final String name; final int cards; + final String id; - PlayerItem({required this.name, required this.cards}); + PlayerItem({ + required this.name, + required this.cards, + required this.id, + }); } // Виджет элемента списка игроков @@ -104,47 +110,64 @@ class PlayerListItem extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 10.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Аватар пользователя как в profile_screen - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: const Color(0xFFEAD7C3), - border: Border.all(color: Colors.black, width: 2), - ), - child: const Center( - child: Icon(Icons.person_outline, size: 28, color: Colors.black), + return InkWell( + onTap: () { + Navigator.pop(context); // Закрываем модальное окно поиска + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProfileScreen( + isOtherUser: true, + playerName: player.name, + playerId: player.id, + cardsCollected: player.cards, + collectionsCollected: 2, // Примерное значение, в реальном приложении должно приходить с сервера ), ), - const SizedBox(width: 12.0), - // Имя пользователя - Text( - player.name, - style: const TextStyle( - fontFamily: 'Roboto', - fontSize: 18.0, - fontWeight: FontWeight.w400, - color: Colors.black, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 10.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Аватар пользователя как в profile_screen + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFEAD7C3), + border: Border.all(color: Colors.black, width: 2), + ), + child: const Center( + child: Icon(Icons.person_outline, size: 28, color: Colors.black), + ), ), - ), - const Spacer(), - // Карточки пользователя (макет из инвентаря, но без фото) - Row( - children: List.generate( - player.cards, - (index) => Padding( - padding: const EdgeInsets.only(left: 6.0), - child: CardMockup(), + const SizedBox(width: 12.0), + // Имя пользователя + Text( + player.name, + style: const TextStyle( + fontFamily: 'Roboto', + fontSize: 18.0, + fontWeight: FontWeight.w400, + color: Colors.black, ), ), - ), - ], + const Spacer(), + // Карточки пользователя (макет из инвентаря, но без фото) + Row( + children: List.generate( + player.cards, + (index) => Padding( + padding: const EdgeInsets.only(left: 6.0), + child: CardMockup(), + ), + ), + ), + ], + ), ), ); } From bc43689bc2372d8ffbdfbdcf4f3cb2c12920c077 Mon Sep 17 00:00:00 2001 From: birbik Date: Tue, 20 May 2025 00:53:16 +0300 Subject: [PATCH 19/54] FCCX-119 add inventory other user --- frontend/lib/views/inventory_screen.dart | 1106 +++++----------------- 1 file changed, 227 insertions(+), 879 deletions(-) diff --git a/frontend/lib/views/inventory_screen.dart b/frontend/lib/views/inventory_screen.dart index d536661..780cbf2 100644 --- a/frontend/lib/views/inventory_screen.dart +++ b/frontend/lib/views/inventory_screen.dart @@ -7,7 +7,16 @@ import 'card_detail_screen.dart'; import 'search_players_screen.dart'; class InventoryScreen extends StatefulWidget { - const InventoryScreen({super.key}); + final bool isOtherUser; + final String? playerName; + final String? playerId; + + const InventoryScreen({ + super.key, + this.isOtherUser = false, + this.playerName, + this.playerId, + }); @override State createState() => _InventoryScreenState(); @@ -25,26 +34,45 @@ class _InventoryScreenState extends State { appBar: AppBar( backgroundColor: const Color(0xFFFFF4E3), elevation: 0, - leading: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Container( - width: 40.0, - height: 40.0, - child: InkWell( - borderRadius: BorderRadius.circular(20.0), - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ProfileScreen()), - ); - }, - child: Image.asset('assets/icons/профиль.png', height: 22), + automaticallyImplyLeading: false, + titleSpacing: 0, + title: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Container( + width: 40.0, + height: 40.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFD6A067), + ), + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + Navigator.pop(context); + }, + child: const Icon( + Icons.arrow_back, + color: Colors.black, + size: 29.0, + ), + ), + ), ), - ), + const SizedBox(width: 8), + Text( + widget.isOtherUser ? 'Инвентарь ${widget.playerName}' : 'Инвентарь', + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 18.0, + ), + ), + ], ), - title: null, - centerTitle: true, - actions: [ + centerTitle: false, + actions: widget.isOtherUser ? null : [ Container( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), decoration: BoxDecoration( @@ -201,901 +229,221 @@ class _InventoryScreenState extends State { ), ], ), - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 0: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - ); - break; - case 2: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - ); - break; - case 3: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - showSelectedLabels: true, - showUnselectedLabels: true, - selectedIconTheme: const IconThemeData( - size: 28, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/магазин.png', height: 24), - ), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), + bottomNavigationBar: widget.isOtherUser + ? null + : Container( + decoration: const BoxDecoration( + color: Color(0xFFD6A067), + ), + child: BottomNavigationBar( + currentIndex: _currentIndex, + onTap: (index) { + if (index != _currentIndex) { + setState(() { + _currentIndex = index; + }); + switch (index) { + case 0: + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const HomeScreen()), + ); + break; + case 2: + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const ShopScreen()), + ); + break; + case 3: + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const ExchangesScreen()), + ); + break; + } + } + }, + backgroundColor: Colors.transparent, + elevation: 0, + type: BottomNavigationBarType.fixed, + selectedItemColor: Colors.black, + unselectedItemColor: Colors.black54, + showSelectedLabels: true, + showUnselectedLabels: true, + selectedIconTheme: const IconThemeData( + size: 28, + ), + unselectedIconTheme: const IconThemeData( + size: 24, + ), + selectedLabelStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + unselectedLabelStyle: const TextStyle( + fontSize: 11, + ), + items: [ + BottomNavigationBarItem( + icon: Image.asset('assets/icons/главная.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/главная.png', height: 24), + ), + label: 'Главная', + ), + BottomNavigationBarItem( + icon: Image.asset('assets/icons/Инвентарь.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/Инвентарь.png', height: 24), + ), + label: 'Инвентарь', + ), + BottomNavigationBarItem( + icon: Image.asset('assets/icons/магазин.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/магазин.png', height: 24), + ), + label: 'Магазин', + ), + BottomNavigationBarItem( + icon: Image.asset('assets/icons/обменник.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/обменник.png', height: 24), + ), + label: 'Обменник', + ), + ], ), - label: 'Обменник', ), - ], - ), - ), ); } Widget _buildCardItem({int index = 0}) { - if (index == 0) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), + return InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CardDetailScreen( + cardIndex: index, + showExchangeButton: widget.isOtherUser, ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - // Внешняя тонкая черная рамка - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - // Прослойка цвета карточки - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - // Внутренняя тонкая черная рамка - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - // Основная карточка - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Image.asset( - 'assets/images/chameleon.jpg', - fit: BoxFit.cover, - width: double.infinity, - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(4, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), - ), - ), - ], ), - ), - ); - } else if (index == 1) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), - ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - // Внешняя тонкая черная рамка - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - // Прослойка цвета карточки - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - // Внутренняя тонкая черная рамка - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - // Основная карточка - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Image.asset( - 'assets/images/kiwi.jpg', - fit: BoxFit.cover, - width: double.infinity, - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(4, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), - ), + ); + }, + child: AspectRatio( + aspectRatio: 3/4, + child: Stack( + children: [ + // Внешняя тонкая черная рамка + Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), ), - ], - ), - ), - ); - } else if (index == 2) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - // Внешняя тонкая черная рамка - Container( + // Прослойка цвета карточки + Padding( + padding: const EdgeInsets.all(2.0), + child: Container( decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - // Прослойка цвета карточки - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(7), ), ), - // Внутренняя тонкая черная рамка - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - // Основная карточка - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Image.asset( - 'assets/images/platypus.jpg', - fit: BoxFit.cover, - width: double.infinity, - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(4, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } else if (index == 3) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - // Внешняя тонкая черная рамка - Container( + // Внутренняя тонкая черная рамка + Padding( + padding: const EdgeInsets.all(4.0), + child: Container( decoration: BoxDecoration( color: Colors.transparent, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.black, width: 2), ), ), - // Прослойка цвета карточки - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - // Внутренняя тонкая черная рамка - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - // Основная карточка - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Image.asset( - 'assets/images/pantera.jpg', - fit: BoxFit.cover, - width: double.infinity, - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(4, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - - } else if (index == 4) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - Container( + // Основная карточка + Padding( + padding: const EdgeInsets.all(6.0), + child: Container( decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(5.0), ), - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Image.asset( - 'assets/images/white_bear.jpg', - fit: BoxFit.cover, - width: double.infinity, - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), + child: Column( + children: [ + Expanded( + flex: 8, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(5.0), + topRight: Radius.circular(5.0), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(3, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, + child: Container( + color: const Color(0xFFEAD7C3), + child: const Center( + child: Text( + 'Нет изображения', + style: TextStyle(color: Colors.black45, fontSize: 12), ), - )), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - }else if (index == 5) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), - ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Image.asset( - 'assets/images/camel.jpg', - fit: BoxFit.cover, - width: double.infinity, ), ), ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(3, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), + ), + Container( + height: 3, + color: Colors.black, + ), + Container( + height: 32, + decoration: const BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(5.0), + bottomRight: Radius.circular(5.0), ), ), - ], - ), - ), - ), - ], - ), - ), - ); - } else if (index == 6) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), - ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: List.generate(4, (i) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0), child: Image.asset( - 'assets/images/zebra.jpg', - fit: BoxFit.cover, - width: double.infinity, - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), + 'assets/icons/редкость.png', + height: 14, ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(3, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), + )), ), - ], - ), - ), - ), - ], - ), - ), - ); - } else if (index == 7) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), - ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), + ), + ], ), ), - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Image.asset( - 'assets/images/elephant.jpg', - fit: BoxFit.cover, - width: double.infinity, - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(3, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), ), - ), - ], - ), + ], ), - ); - //};// else { - //return Container( - //decoration: BoxDecoration( - // color: const Color(0xFFD6A067), - // borderRadius: BorderRadius.circular(8.0), - // border: Border.all(color: Colors.black, width: 2), - //), - //); - } - return const SizedBox.shrink(); + ), + ); } } \ No newline at end of file From da04e6b846255949902d957f7a4a45ab19ca91ab Mon Sep 17 00:00:00 2001 From: birbik Date: Tue, 20 May 2025 00:54:37 +0300 Subject: [PATCH 20/54] FCCX-120 add detailed viewing of another user's card --- frontend/lib/views/card_detail_screen.dart | 204 +++++++++------------ 1 file changed, 86 insertions(+), 118 deletions(-) diff --git a/frontend/lib/views/card_detail_screen.dart b/frontend/lib/views/card_detail_screen.dart index 8a62603..afa2d92 100644 --- a/frontend/lib/views/card_detail_screen.dart +++ b/frontend/lib/views/card_detail_screen.dart @@ -2,74 +2,25 @@ import 'package:flutter/material.dart'; class CardDetailScreen extends StatelessWidget { final int cardIndex; + final bool showExchangeButton; - const CardDetailScreen({Key? key, required this.cardIndex}) : super(key: key); + const CardDetailScreen({Key? key, required this.cardIndex, this.showExchangeButton = false}) : super(key: key); @override Widget build(BuildContext context) { // Данные для карточек final List> cardData = [ { - 'image': 'assets/images/chameleon.jpg', - 'desc': 'Хамелеон — семейство ящериц, приспособленных к древесному образу жизни, способных менять окраску тела.', - 'name': 'Хамелеон', - 'type': 'Звери', + 'name': 'Название', + 'desc': 'Описание карточки', + 'type': 'Тип', 'rarity': '4', }, - { - 'image': 'assets/images/kiwi.jpg', - 'desc': 'Киви — семейство нелетающих птиц отряда кивиобразных.', - 'name': 'Киви', - 'type': 'Птицы', - 'rarity': '4', - }, - { - 'image': 'assets/images/platypus.jpg', - 'desc': 'Утконос — водоплавающее млекопитающее отряда однопроходных, обитающее в Австралии.', - 'name': 'Утконос', - 'type': 'Звери', - 'rarity': '4', - }, - { - 'image': 'assets/images/pantera.jpg', - 'desc': 'Пантера — крупный хищник из семейства кошачьих. В некоторых культурах черная пантера считается священным животным.', - 'name': 'Пантера', - 'type': 'Звери', - 'rarity': '4', - }, - { - 'image': 'assets/images/white_bear.jpg', - 'desc': 'Белый медведь — хищное млекопитающее семейства медвежьих, близкий родственник бурого медведя.', - 'name': 'Белый медведь', - 'type': 'Звери', - 'rarity': '3', - }, - { - 'image': 'assets/images/camel.jpg', - 'desc': 'Верблюд — млекопитающие семейства верблюдовых.', - 'name': 'Верблюд', - 'type': 'Звери', - 'rarity': '3', - }, - { - 'image': 'assets/images/zebra.jpg', - 'desc': 'Зебра — африканская дикая лошадь, отличающаяся характерным полосатым окрасом шкуры.', - 'name': 'Зебра', - 'type': 'Звери', - 'rarity': '3', - }, - { - 'image': 'assets/images/elephant.jpg', - 'desc': 'Слон — самое крупное наземное животное на Земле. Хобот слона — это многофункциональный инструмент.', - 'name': 'Слон', - 'type': 'Звери', - 'rarity': '3', - }, + // ... можно добавить больше заглушек ... ]; final data = (cardIndex < cardData.length) ? cardData[cardIndex] : cardData[0]; - final String imagePath = data['image']!; - final String description = data['desc']!; final String name = data['name']!; + final String description = data['desc']!; final String type = data['type']!; final int rarity = int.tryParse(data['rarity'] ?? '0') ?? 0; @@ -137,13 +88,14 @@ class CardDetailScreen extends StatelessWidget { margin: const EdgeInsets.symmetric(horizontal: 0.0, vertical: 4.0), decoration: BoxDecoration( border: Border.all(color: Colors.black, width: 3), + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(10), ), - child: ClipRect( - child: Image.asset( - imagePath, - fit: BoxFit.fill, - width: double.infinity, - height: double.infinity, + // Здесь был Image.asset, теперь просто пустой контейнер-заглушка + child: const Center( + child: Text( + 'Нет изображения', + style: TextStyle(color: Colors.black45, fontSize: 16), ), ), ), @@ -199,74 +151,90 @@ class CardDetailScreen extends StatelessWidget { ), Padding( padding: const EdgeInsets.only(bottom: 32.0, left: 16.0, right: 16.0, top: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), - ), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: showExchangeButton + ? ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), ), - onPressed: () {}, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text( - 'Разобрать', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), - ), - SizedBox(width: 6), - ], + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + ), + onPressed: () {}, + child: const Text( + 'Предложить обмен', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), + ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), ), - Row( + onPressed: () {}, + child: Column( mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text( - '150', - style: TextStyle(fontSize: 15, fontWeight: FontWeight.w400), - textAlign: TextAlign.center, + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Разобрать', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), + ), + SizedBox(width: 6), + ], ), - SizedBox(width: 4), - Image.asset( - 'assets/icons/монеты.png', - height: 18, + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + '150', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w400), + textAlign: TextAlign.center, + ), + SizedBox(width: 4), + Image.asset( + 'assets/icons/монеты.png', + height: 18, + ), + ], ), ], ), - ], - ), - ), - ), - SizedBox(width: 16), - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), ), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), ), - onPressed: () {}, - child: const Text( - 'Обменять', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), + SizedBox(width: 16), + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + ), + onPressed: () {}, + child: const Text( + 'Обменять', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), + ), + ), ), - ), + ], ), - ], - ), ), ], ), From 850b9ea7f6703a6c353f473a781241cf1438ed8c Mon Sep 17 00:00:00 2001 From: birbik Date: Tue, 20 May 2025 00:59:09 +0300 Subject: [PATCH 21/54] FCCX-120 update card detail screen --- frontend/lib/views/card_detail_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/lib/views/card_detail_screen.dart b/frontend/lib/views/card_detail_screen.dart index afa2d92..6c3faa4 100644 --- a/frontend/lib/views/card_detail_screen.dart +++ b/frontend/lib/views/card_detail_screen.dart @@ -157,7 +157,7 @@ class CardDetailScreen extends StatelessWidget { backgroundColor: const Color(0xFFD6A067), foregroundColor: Colors.black, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), + borderRadius: BorderRadius.circular(10.0), ), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), ), From 52c85671a332be49ca1f703673f99ad60457b4d8 Mon Sep 17 00:00:00 2001 From: birbik Date: Tue, 20 May 2025 15:59:35 +0300 Subject: [PATCH 22/54] FCCX-119 update inventory screen, update tabbar in shop and exchanges screen --- frontend/lib/views/exchanges_screen.dart | 13 ++--- frontend/lib/views/inventory_screen.dart | 70 ++++++++++++------------ frontend/lib/views/shop_screen.dart | 9 +-- 3 files changed, 42 insertions(+), 50 deletions(-) diff --git a/frontend/lib/views/exchanges_screen.dart b/frontend/lib/views/exchanges_screen.dart index 291e978..fa507ce 100644 --- a/frontend/lib/views/exchanges_screen.dart +++ b/frontend/lib/views/exchanges_screen.dart @@ -92,18 +92,13 @@ class _ExchangesScreenState extends State with SingleTickerProv ), child: TabBar( controller: _tabController, - labelColor: Colors.black, - unselectedLabelColor: Colors.black, + dividerColor: Colors.transparent, indicator: const BoxDecoration( color: Color(0xFFD6A067), - borderRadius: BorderRadius.all(Radius.circular(12.0)), - border: Border( - bottom: BorderSide( - color: Colors.black, - width: 3.0, - ), - ), + borderRadius: BorderRadius.all(Radius.circular(10.0)), ), + labelColor: Colors.black, + unselectedLabelColor: Colors.black, indicatorSize: TabBarIndicatorSize.tab, tabs: const [ Tab(text: 'Обмен'), diff --git a/frontend/lib/views/inventory_screen.dart b/frontend/lib/views/inventory_screen.dart index 780cbf2..a85a19b 100644 --- a/frontend/lib/views/inventory_screen.dart +++ b/frontend/lib/views/inventory_screen.dart @@ -36,41 +36,43 @@ class _InventoryScreenState extends State { elevation: 0, automaticallyImplyLeading: false, titleSpacing: 0, - title: Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Container( - width: 40.0, - height: 40.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: const Color(0xFFD6A067), - ), - child: InkWell( - borderRadius: BorderRadius.circular(20.0), - onTap: () { - Navigator.pop(context); - }, - child: const Icon( - Icons.arrow_back, - color: Colors.black, - size: 29.0, + title: widget.isOtherUser + ? Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Container( + width: 40.0, + height: 40.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFD6A067), + ), + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + Navigator.pop(context); + }, + child: const Icon( + Icons.arrow_back, + color: Colors.black, + size: 29.0, + ), + ), + ), ), - ), - ), - ), - const SizedBox(width: 8), - Text( - widget.isOtherUser ? 'Инвентарь ${widget.playerName}' : 'Инвентарь', - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 18.0, - ), - ), - ], - ), + const SizedBox(width: 8), + Text( + 'Инвентарь ${widget.playerName}', + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 18.0, + ), + ), + ], + ) + : const SizedBox.shrink(), centerTitle: false, actions: widget.isOtherUser ? null : [ Container( diff --git a/frontend/lib/views/shop_screen.dart b/frontend/lib/views/shop_screen.dart index 67d3038..1f428f3 100644 --- a/frontend/lib/views/shop_screen.dart +++ b/frontend/lib/views/shop_screen.dart @@ -117,15 +117,10 @@ class _ShopScreenState extends State with SingleTickerProviderStateM ), child: TabBar( controller: _tabController, + dividerColor: Colors.transparent, indicator: const BoxDecoration( color: Color(0xFFD6A067), - borderRadius: BorderRadius.all(Radius.circular(12.0)), - border: Border( - bottom: BorderSide( - color: Colors.black, - width: 3.0, - ), - ), + borderRadius: BorderRadius.all(Radius.circular(10.0)), ), labelColor: Colors.black, unselectedLabelColor: Colors.black, From 7a46fe8405d7b2a3c7f3d9ce7398b53307b6cd1f Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Wed, 21 May 2025 14:22:06 +0300 Subject: [PATCH 23/54] FCCX-121 Speeding up Docker image builds --- .gitignore | 1 + backend/.dockerignore | 29 +++++++++++++++++++++++++++++ backend/.gitignore | 1 + backend/Dockerfile | 23 ++++++++++++++++------- backend/docker-compose.yml | 2 -- 5 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 .gitignore create mode 100644 backend/.dockerignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbe9c82 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode/ \ No newline at end of file diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..ba87d4e --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,29 @@ +# IntelliJ IDEA / VSCode +.idea/ +.vscode/ + +# Kotlin/Gradle +.gradle/ +build/ +out/ +!gradle/wrapper/gradle-wrapper.jar + +# OS garbage +.DS_Store +Thumbs.db + +# Logs and temp +*.log +*.tmp +*.swp + +# Git +.git +.gitignore + +# Docker-related +docker-compose.override.yml + +# Test artifacts +test-results/ +coverage/ diff --git a/backend/.gitignore b/backend/.gitignore index 5a979af..45cb293 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -4,6 +4,7 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +*.log ### STS ### .apt_generated diff --git a/backend/Dockerfile b/backend/Dockerfile index fd2a066..e6a4aa2 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,18 +1,27 @@ -FROM eclipse-temurin:17-jdk as build +FROM eclipse-temurin:17-jdk AS build WORKDIR /app COPY gradlew . COPY gradle gradle -COPY build.gradle . -COPY settings.gradle . -COPY src src +COPY build.gradle settings.gradle ./ -# Предоставляем права на исполнение градл-враппера RUN chmod +x ./gradlew -RUN ./gradlew build -x test +RUN mkdir -p /app/.gradle + +# Кэшируем скачанный gradle +RUN chown -R root:root /app/.gradle + +RUN ./gradlew --no-daemon --gradle-user-home=/app/.gradle dependencies || true + +COPY src src + +# Собираем проект с тем же кэшем +RUN ./gradlew --no-daemon --gradle-user-home=/app/.gradle build -x test + +# Финальный образ FROM eclipse-temurin:17-jre WORKDIR /app COPY --from=build /app/build/libs/*.jar app.jar -ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index a0b4613..deecaea 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -1,5 +1,3 @@ -version: '4.4' - services: app: build: . From 6e553042aaef7b835aa584be2a45c0a5d8d73183 Mon Sep 17 00:00:00 2001 From: birbik Date: Wed, 21 May 2025 22:58:26 +0300 Subject: [PATCH 24/54] FCCX-125 update screens --- frontend/lib/views/achievements_screen.dart | 6 +- frontend/lib/views/card_detail_screen.dart | 434 +++++----- frontend/lib/views/create_card_screen.dart | 17 +- .../lib/views/create_exchange_screen.dart | 780 ++++++++++-------- .../lib/views/exchange_details_screen.dart | 393 ++++++--- frontend/lib/views/exchanges_screen.dart | 752 ++++++++++------- frontend/lib/views/home_screen.dart | 14 +- frontend/lib/views/inventory_screen.dart | 2 +- frontend/lib/views/news_detail_screen.dart | 324 +++----- frontend/lib/views/news_screen.dart | 160 ++-- frontend/lib/views/pack_content_screen.dart | 105 ++- frontend/lib/views/profile_screen.dart | 258 +++--- frontend/lib/views/search_players_screen.dart | 194 +++-- .../lib/views/shop_coin_details_screen.dart | 110 +-- frontend/lib/views/shop_screen.dart | 4 +- .../lib/views/shop_set_content_screen.dart | 160 ++-- .../lib/views/shop_set_details_screen.dart | 72 +- 17 files changed, 2204 insertions(+), 1581 deletions(-) diff --git a/frontend/lib/views/achievements_screen.dart b/frontend/lib/views/achievements_screen.dart index 3dccd85..a215a81 100644 --- a/frontend/lib/views/achievements_screen.dart +++ b/frontend/lib/views/achievements_screen.dart @@ -20,7 +20,7 @@ class AchievementsScreen extends StatelessWidget { ); return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон + backgroundColor: const Color(0xFFFBF6EF), // Бежевый фон body: SafeArea( child: Column( children: [ @@ -37,7 +37,7 @@ class AchievementsScreen extends StatelessWidget { color: const Color(0xFFD6A067), ), child: InkWell( - borderRadius: BorderRadius.circular(20.0), + borderRadius: BorderRadius.circular(10.0), onTap: () { Navigator.pop(context); }, @@ -68,7 +68,7 @@ class AchievementsScreen extends StatelessWidget { final achievement = achievements[index]; return Card( margin: const EdgeInsets.only(bottom: 8), - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), elevation: 0, child: Padding( padding: const EdgeInsets.all(16.0), diff --git a/frontend/lib/views/card_detail_screen.dart b/frontend/lib/views/card_detail_screen.dart index 6c3faa4..5d4f178 100644 --- a/frontend/lib/views/card_detail_screen.dart +++ b/frontend/lib/views/card_detail_screen.dart @@ -1,10 +1,23 @@ import 'package:flutter/material.dart'; -class CardDetailScreen extends StatelessWidget { +class CardDetailScreen extends StatefulWidget { final int cardIndex; final bool showExchangeButton; + final bool isFromShop; - const CardDetailScreen({Key? key, required this.cardIndex, this.showExchangeButton = false}) : super(key: key); + const CardDetailScreen({ + Key? key, + required this.cardIndex, + this.showExchangeButton = false, + this.isFromShop = false, + }) : super(key: key); + + @override + State createState() => _CardDetailScreenState(); +} + +class _CardDetailScreenState extends State { + bool isFavorite = false; @override Widget build(BuildContext context) { @@ -18,7 +31,7 @@ class CardDetailScreen extends StatelessWidget { }, // ... можно добавить больше заглушек ... ]; - final data = (cardIndex < cardData.length) ? cardData[cardIndex] : cardData[0]; + final data = (widget.cardIndex < cardData.length) ? cardData[widget.cardIndex] : cardData[0]; final String name = data['name']!; final String description = data['desc']!; final String type = data['type']!; @@ -27,214 +40,265 @@ class CardDetailScreen extends StatelessWidget { return Scaffold( backgroundColor: const Color(0xFFFFF4E3), body: SafeArea( - child: Column( + child: Stack( children: [ - Row( + Column( children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: InkWell( - onTap: () => Navigator.pop(context), - borderRadius: BorderRadius.circular(30), - child: Container( - width: 48, - height: 48, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - shape: BoxShape.circle, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: InkWell( + onTap: () => Navigator.pop(context), + borderRadius: BorderRadius.circular(20.0), + child: Container( + width: 40.0, + height: 40.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFD6A067), + ), + child: const Icon( + Icons.arrow_back, + color: Colors.black, + size: 29.0, + ), + ), ), - child: const Icon(Icons.arrow_back, color: Colors.black, size: 32), ), - ), + ], ), - ], - ), - Expanded( - child: Center( - child: Container( - width: 340, - height: 520, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(18.0), - border: Border.all(color: Colors.black, width: 6), - ), - child: Container( - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(14.0), - border: Border.all(color: Colors.black, width: 3), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - flex: 3, // 5% - child: Center( - child: Text( - name, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.w500, - color: Colors.black, - ), - ), - ), + Expanded( + child: Center( + child: Container( + width: 340, + height: 520, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(18.0), + border: Border.all(color: Colors.black, width: 6), + ), + child: Container( + margin: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(14.0), + border: Border.all(color: Colors.black, width: 3), ), - Expanded( - flex: 36, // 60% - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 0.0, vertical: 4.0), - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 3), - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + flex: 3, // 5% + child: Center( + child: Text( + name, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + ), ), - // Здесь был Image.asset, теперь просто пустой контейнер-заглушка - child: const Center( - child: Text( - 'Нет изображения', - style: TextStyle(color: Colors.black45, fontSize: 16), + Expanded( + flex: 36, // 60% + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 0.0, vertical: 4.0), + decoration: BoxDecoration( + border: Border.all(color: Colors.black, width: 3), + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(10), + ), + child: const Center( + child: Text( + 'Нет изображения', + style: TextStyle(color: Colors.black45, fontSize: 16), + ), + ), ), ), - ), - ), - Expanded( - flex: 18, // 30% - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0), - child: Text( - description, - style: const TextStyle(fontSize: 17, color: Colors.black), - textAlign: TextAlign.left, + Expanded( + flex: 18, // 30% + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0), + child: Text( + description, + style: const TextStyle(fontSize: 17, color: Colors.black), + textAlign: TextAlign.left, + ), + ), ), - ), - ), - Expanded( - flex: 3, // 5% - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 0.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Row( - children: List.generate( - rarity, - (index) => Padding( - padding: const EdgeInsets.only(right: 4.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 24, + Expanded( + flex: 3, // 5% + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 0.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + children: List.generate( + rarity, + (index) => Padding( + padding: const EdgeInsets.only(right: 4.0), + child: Image.asset( + 'assets/icons/редкость.png', + height: 24, + ), + ), ), ), - ), - ), - const Spacer(), - Text( - type, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - color: Colors.black, - ), + const Spacer(), + Text( + type, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + ], ), - ], + ), ), - ), + ], ), - ], + ), ), ), ), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 32.0, left: 16.0, right: 16.0, top: 8.0), - child: showExchangeButton - ? ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - ), - onPressed: () {}, - child: const Text( - 'Предложить обмен', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), - ), - ) - : Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), - ), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + Padding( + padding: const EdgeInsets.only(bottom: 32.0, left: 16.0, right: 16.0, top: 8.0), + child: widget.showExchangeButton + ? ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), ), - onPressed: () {}, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text( - 'Разобрать', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + ), + onPressed: () {}, + child: const Text( + 'Предложить обмен', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), + ), + ) + : widget.isFromShop + ? const SizedBox.shrink() + : Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), ), - SizedBox(width: 6), - ], + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + ), + onPressed: () { + // TODO: Implement logic to disassemble the card and add coins. + // 1. Get the current coin balance. + // 2. Add 150 coins to the balance. + // 3. Update the coin balance in your state management or data source. + + // After disassembling, navigate back to the inventory screen. + Navigator.pop(context); // Assumes CardDetailScreen was pushed from InventoryScreen + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Разобрать', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), + ), + SizedBox(width: 6), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + '150', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w400), + textAlign: TextAlign.center, + ), + SizedBox(width: 4), + Image.asset( + 'assets/icons/монеты.png', + height: 18, + ), + ], + ), + ], + ), ), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - '150', - style: TextStyle(fontSize: 15, fontWeight: FontWeight.w400), - textAlign: TextAlign.center, - ), - SizedBox(width: 4), - Image.asset( - 'assets/icons/монеты.png', - height: 18, + ), + SizedBox(width: 16), + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), ), - ], + padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 25), + ), + onPressed: () {}, + child: const Text( + 'Обменять', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), + ), ), - ], - ), - ), - ), - SizedBox(width: 16), - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), ), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - ), - onPressed: () {}, - child: const Text( - 'Обменять', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), - ), + ], ), - ), - ], + ), + ], + ), + Positioned( + top: 111, // Adjust this value to move the star vertically + right: -4, // Adjust this value to move the star horizontally + child: InkWell( + onTap: () { + setState(() { + isFavorite = !isFavorite; + print('isFavorite: $isFavorite'); // Added for debugging + + // TODO: Integrate your favorite state management here. + // If isFavorite is true, add widget.cardIndex to the user's favorite list. + // If isFavorite is false, remove widget.cardIndex from the user's favorite list. + // Example (using a hypothetical function saveFavoriteCardId and removeFavoriteCardId): + // if (isFavorite) { + // saveFavoriteCardId(widget.cardIndex); + // } else { + // removeFavoriteCardId(widget.cardIndex); + // } + }); + }, + child: SizedBox( + width: 60.0, // Increased tap area + height: 60.0, // Increased tap area + child: Center( + child: Icon( + isFavorite ? Icons.star : Icons.star_border, + color: Colors.black, + size: 46.0, // Keep original icon size + ), ), + ), + ), ), ], ), diff --git a/frontend/lib/views/create_card_screen.dart b/frontend/lib/views/create_card_screen.dart index 08ae860..7341db5 100644 --- a/frontend/lib/views/create_card_screen.dart +++ b/frontend/lib/views/create_card_screen.dart @@ -16,14 +16,14 @@ class _CreateCardScreenState extends State { bool _showCategories = false; int _currentIndex = 0; final List _categories = ['Категория 1', 'Категория 2', 'Категория 3', 'Категория 4']; - String _selectedCategory = 'Пример категории'; + String _selectedCategory = 'Выберите категорию'; @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, leading: Padding( padding: const EdgeInsets.only(left: 16.0), @@ -96,6 +96,7 @@ class _CreateCardScreenState extends State { style: const TextStyle( color: Colors.black, fontSize: 16.0, + fontFamily: 'Jost', ), ), Icon( @@ -161,6 +162,7 @@ class _CreateCardScreenState extends State { style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), @@ -171,12 +173,6 @@ class _CreateCardScreenState extends State { Container( decoration: const BoxDecoration( color: Color(0xFFD6A067), - border: Border( - top: BorderSide( - color: Colors.black, - width: 1.0, - ), - ), ), child: BottomNavigationBar( currentIndex: _currentIndex, @@ -230,9 +226,11 @@ class _CreateCardScreenState extends State { selectedLabelStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), unselectedLabelStyle: const TextStyle( fontSize: 11, + fontFamily: 'Jost', ), items: [ BottomNavigationBarItem( @@ -341,6 +339,7 @@ class _CreateCardScreenState extends State { style: const TextStyle( fontSize: 16.0, color: Colors.black, + fontFamily: 'Jost', ), ), ), diff --git a/frontend/lib/views/create_exchange_screen.dart b/frontend/lib/views/create_exchange_screen.dart index 97531a6..080a060 100644 --- a/frontend/lib/views/create_exchange_screen.dart +++ b/frontend/lib/views/create_exchange_screen.dart @@ -6,7 +6,9 @@ import 'inventory_screen.dart'; import 'profile_screen.dart'; class CreateExchangeScreen extends StatefulWidget { - const CreateExchangeScreen({super.key}); + final ExchangeItem? initialExchangeItem; + + const CreateExchangeScreen({super.key, this.initialExchangeItem}); @override State createState() => _CreateExchangeScreenState(); @@ -17,7 +19,23 @@ class _CreateExchangeScreenState extends State { Map? selectedTopCard; List> selectedExchangeCards = []; - void _showTopCardSelectionDialog() { + @override + void initState() { + super.initState(); + if (widget.initialExchangeItem != null && widget.initialExchangeItem!.otherUserOfferedCardIds.isNotEmpty) { + selectedTopCard = { + 'id': widget.initialExchangeItem!.otherUserOfferedCardIds[0], + 'name': 'Карта ${widget.initialExchangeItem!.otherUserOfferedCardIds[0]}', + }; + } + } + + void _showCardSelectionDialog({ + required String title, + required Function(Map) onCardSelected, + required bool Function(int) isCardSelected, + required bool Function() canSelectMore, + }) { showDialog( context: context, builder: (BuildContext context) { @@ -26,52 +44,118 @@ class _CreateExchangeScreenState extends State { child: Container( width: MediaQuery.of(context).size.width * 0.9, height: MediaQuery.of(context).size.height * 0.8, - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, + child: Stack( + clipBehavior: Clip.none, children: [ - const Text( - 'Выберите вашу карту', - style: TextStyle( - fontSize: 20.0, - fontFamily: 'Jost', - color: Colors.black, + Container( + height: MediaQuery.of(context).size.height * 0.8, + padding: const EdgeInsets.only(top: 24.0, bottom: 16.0, left: 16.0, right: 16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 20.0, + fontFamily: 'Jost', + color: Colors.black, + ), + ), + const SizedBox(height: 16.0), + Expanded( + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 0.7, + crossAxisSpacing: 10.0, + mainAxisSpacing: 10.0, + ), + itemCount: 40, + itemBuilder: (context, index) { + bool isSelected = isCardSelected(index); + return GestureDetector( + onTap: () { + if (!isSelected && canSelectMore()) { + onCardSelected({ + 'id': index, + 'name': 'Карта $index', + }); + Navigator.pop(context); + } + }, + child: _buildExchangeCardVisual( + width: 100, + height: 140, + content: Center(child: Text('Карта $index', style: const TextStyle(color: Colors.black, fontSize: 10, fontFamily: 'Jost'),)), + ), + ); + }, + ), + ), + ], ), ), - const SizedBox(height: 16.0), - Expanded( - child: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 0.7, - crossAxisSpacing: 10.0, - mainAxisSpacing: 10.0, + // Close button in the top right corner + Positioned( + top: 0, + right: -2, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + child: const Center( + child: Icon( + Icons.close, + color: Colors.black, + size: 20, + ), + ), ), - itemCount: 40, - itemBuilder: (context, index) { - return GestureDetector( - onTap: () { - setState(() { - selectedTopCard = { - 'id': index, - 'name': 'Карта $index', - }; - }); - Navigator.pop(context); - }, - child: _buildCollectionCard(index), - ); - }, ), ), ], ), ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), ); }, ); } + void _showTopCardSelectionDialog() { + _showCardSelectionDialog( + title: 'Выберите вашу карту', + onCardSelected: (card) { + setState(() { + selectedTopCard = card; + }); + }, + isCardSelected: (index) => selectedTopCard?['id'] == index, + canSelectMore: () => true, + ); + } + + void _showLargeCardSelectionDialog() { + _showCardSelectionDialog( + title: 'Выберите карту для обмена', + onCardSelected: (card) { + setState(() { + selectedExchangeCards.add(card); + }); + }, + isCardSelected: (index) => selectedExchangeCards.any((card) => card['id'] == index), + canSelectMore: () => selectedExchangeCards.length < 8, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -90,24 +174,17 @@ class _CreateExchangeScreenState extends State { onPressed: () => Navigator.pop(context), ), ), - title: const Text( - 'Создание обмена', - style: TextStyle( - color: Colors.black, - fontSize: 20.0, - fontFamily: 'Jost', - ), - ), + ), body: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ // Заголовок "Ваша карта для обмена" - const Padding( - padding: EdgeInsets.only(top: 20.0), + Padding( + padding: const EdgeInsets.only(top: 20.0), child: Text( - 'Ваша карта для обмена', - style: TextStyle( + widget.initialExchangeItem != null ? 'Вы получите' : 'Ваша карта для обмена', + style: const TextStyle( color: Colors.black, fontSize: 20.0, fontFamily: 'Jost', @@ -120,35 +197,41 @@ class _CreateExchangeScreenState extends State { child: selectedTopCard == null ? GestureDetector( onTap: _showTopCardSelectionDialog, - child: _buildCardPlaceholder(), + child: _buildExchangeCardVisual( + width: 78, + height: 112, + content: Center( + child: Icon( + Icons.add, + size: 20, + color: Colors.black.withOpacity(0.5), + ), + ), + ), ) - : _buildCollectionCard( - selectedTopCard!['id'], - onRemove: () { - setState(() { - selectedTopCard = null; - }); - }, + : _buildExchangeCardVisual( + width: 78, + height: 112, + content: Center(child: Text('Карта ${selectedTopCard!['id']}', style: const TextStyle(color: Colors.black, fontSize: 10, fontFamily: 'Jost'),)), ), ), const SizedBox(height: 20.0), // Текст для карт обмена - const Column( + Column( children: [ Text( - 'Карта, на которую вы', + widget.initialExchangeItem != null ? 'Вы готовы' : 'Карта, на которую вы', textAlign: TextAlign.center, - style: TextStyle( + style: const TextStyle( color: Colors.black, fontSize: 20.0, fontFamily: 'Jost', ), ), - SizedBox(height: 5.0), Text( - 'готовы произвести обмен', + widget.initialExchangeItem != null ? 'предложить взамен' : 'готовы произвести обмен', textAlign: TextAlign.center, - style: TextStyle( + style: const TextStyle( color: Colors.black, fontSize: 20.0, fontFamily: 'Jost', @@ -165,85 +248,128 @@ class _CreateExchangeScreenState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( - onPressed: (selectedTopCard == null || selectedExchangeCards.isEmpty) - ? () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Container( - width: 367, - height: 61, - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(15.0), - ), - padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), - child: const Center( - child: Text( - 'Добавьте карточки для обмена', - style: TextStyle( - color: Colors.black, - fontSize: 18.0, - fontWeight: FontWeight.w500, - fontFamily: 'Jost', + onPressed: widget.initialExchangeItem != null + ? () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Container( + width: 367, + height: 61, + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(15.0), + ), + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), + child: const Center( + child: Text( + 'Обмен успешно отправлен', + style: TextStyle( + color: Colors.black, + fontSize: 18.0, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, ), ), - ), - backgroundColor: Colors.transparent, - duration: const Duration(seconds: 2), - behavior: SnackBarBehavior.floating, - margin: EdgeInsets.only( - bottom: MediaQuery.of(context).size.height - 120, - left: 16, - right: 16, - ), - elevation: 0, - ), - ); - } - : () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Container( - width: 367, - height: 61, - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(15.0), + backgroundColor: Colors.transparent, + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height - 200, + left: 16, + right: 16, ), - padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), - child: const Center( - child: Text( - 'Обмен создан', - style: TextStyle( - color: Colors.black, - fontSize: 18.0, - fontWeight: FontWeight.w500, - fontFamily: 'Jost', + elevation: 0, + ), + ); + + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const ExchangesScreen(initialTabIndex: 1)), + (route) => false, + ); + } + : (selectedTopCard == null || selectedExchangeCards.isEmpty) + ? () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Container( + width: 367, + height: 61, + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(15.0), + ), + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), + child: const Center( + child: Text( + 'Добавьте карточки для обмена', + style: TextStyle( + color: Colors.black, + fontSize: 18.0, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + ), + textAlign: TextAlign.center, + ), + ), + ), + backgroundColor: Colors.transparent, + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height - 120, + left: 16, + right: 16, ), - textAlign: TextAlign.center, + elevation: 0, ), - ), - ), - backgroundColor: Colors.transparent, - duration: const Duration(seconds: 2), - behavior: SnackBarBehavior.floating, - margin: EdgeInsets.only( - bottom: MediaQuery.of(context).size.height - 200, - left: 16, - right: 16, - ), - elevation: 0, - ), - ); - - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - }, + ); + } + : () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Container( + width: 367, + height: 61, + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(15.0), + ), + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), + child: const Center( + child: Text( + 'Обмен создан', + style: TextStyle( + color: Colors.black, + fontSize: 18.0, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + ), + textAlign: TextAlign.center, + ), + ), + ), + backgroundColor: Colors.transparent, + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height - 200, + left: 16, + right: 16, + ), + elevation: 0, + ), + ); + + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const ExchangesScreen()), + (route) => false, + ); + }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFD6A067), foregroundColor: Colors.black, @@ -254,9 +380,9 @@ class _CreateExchangeScreenState extends State { disabledBackgroundColor: const Color(0xFFD6A067).withOpacity(0.7), disabledForegroundColor: Colors.black.withOpacity(0.5), ), - child: const Text( - 'Создать обмен', - style: TextStyle( + child: Text( + widget.initialExchangeItem != null ? 'Предложить обмен' : 'Создать обмен', + style: const TextStyle( fontSize: 20.0, fontWeight: FontWeight.bold, fontFamily: 'Jost', @@ -268,46 +394,42 @@ class _CreateExchangeScreenState extends State { Container( decoration: const BoxDecoration( color: Color(0xFFD6A067), - border: Border( - top: BorderSide( - color: Colors.black, - width: 1.0, - ), - ), ), child: BottomNavigationBar( currentIndex: _currentIndex, onTap: (index) { - if (index != _currentIndex) { - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 1: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const InventoryScreen()), - (route) => false, - ); - break; - case 2: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; + if (widget.initialExchangeItem == null) { + if (index != _currentIndex) { + switch (index) { + case 0: + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const HomeScreen()), + (route) => false, + ); + break; + case 1: + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const InventoryScreen()), + (route) => false, + ); + break; + case 2: + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const ShopScreen()), + (route) => false, + ); + break; + case 3: + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const ExchangesScreen()), + (route) => false, + ); + break; + } } } }, @@ -390,132 +512,135 @@ class _CreateExchangeScreenState extends State { ); } - Widget _buildCollectionCard(int index, {bool isSelected = true, Function()? onRemove}) { - return Stack( - children: [ - Container( - width: 80, - height: 120, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(6.0), - border: Border.all(color: Colors.black, width: 2), - ), - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 2), - borderRadius: BorderRadius.circular(6.0), - ), - ), - Positioned( - top: 5, - left: 5, - right: 5, - bottom: 5, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 2), - borderRadius: BorderRadius.circular(4.0), - ), - ), - ), - Positioned( - left: 5, - right: 5, - bottom: 30, - child: Container( - height: 1, - color: Colors.black, - ), - ), - Center( - child: Text( - 'Карта $index', - style: const TextStyle( - color: Colors.black, - fontSize: 12, - fontFamily: 'Jost', - ), - ), - ), - ], - ), - ), - if (isSelected && onRemove != null) - Positioned( - top: -5, - right: -5, - child: GestureDetector( - onTap: onRemove, - child: Container( - width: 20, - height: 20, - decoration: BoxDecoration( - color: const Color(0xFFFFF4E3), - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.black, width: 1), - ), - child: const Center( - child: Icon( - Icons.close, - size: 14, - color: Colors.black, - ), - ), - ), - ), - ), - ], - ); - } + Widget _buildExchangeCardVisual({Widget? content, double width = 80, double height = 120, VoidCallback? onRemove}) { + // Calculate internal heights based on the provided height + double totalNonContentHeight = 3 + 3 + 3 + 2 + 1; // Borders, padding, and line from inventory card + double availableContentHeight = height - totalNonContentHeight; + double imageHeight = availableContentHeight * 0.65; // Approximate proportion for image area + double rarityHeight = availableContentHeight * 0.2; // Approximate proportion for rarity area + + // Ensure calculated heights are non-negative + imageHeight = imageHeight > 0 ? imageHeight : 0; + rarityHeight = rarityHeight > 0 ? rarityHeight : 0; - Widget _buildCardPlaceholder() { return Container( - width: 80, - height: 120, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(6.0), - border: Border.all(color: Colors.black, width: 2), - ), + width: width, + height: height, child: Stack( + clipBehavior: Clip.none, children: [ + // Внешняя тонкая черная рамка Container( decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.black, width: 2), - borderRadius: BorderRadius.circular(6.0), ), ), - Positioned( - top: 5, - left: 5, - right: 5, - bottom: 5, + // Прослойка цвета карточки + Padding( + padding: const EdgeInsets.all(2.0), child: Container( decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 2), - borderRadius: BorderRadius.circular(4.0), + color: const Color(0xFFD6A067), // Use the same color as inventory card + borderRadius: BorderRadius.circular(7), ), ), ), - Positioned( - left: 5, - right: 5, - bottom: 30, + // Внутренняя тонкая черная рамка + Padding( + padding: const EdgeInsets.all(4.0), child: Container( - height: 1, - color: Colors.black, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.black, width: 2), + ), ), ), - Center( - child: Icon( - Icons.add, - size: 24, - color: Colors.black.withOpacity(0.5), + // Основная карточка + Padding( + padding: const EdgeInsets.all(6.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), // Use the same color as inventory card + borderRadius: BorderRadius.circular(5.0), + ), + child: Column( + children: [ + Expanded( + flex: 8, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(5.0), + topRight: Radius.circular(5.0), + ), + child: Container( + color: const Color(0xFFEAD7C3), // Background color for image area + height: imageHeight, + child: content ?? const Center( // Use provided content or default placeholder + child: Text( + 'Нет изображения', + style: TextStyle(color: Colors.black45, fontSize: 10), // Adjusted font size + textAlign: TextAlign.center, + ), + ), + ), + ), + ), + Container( + height: 3, + color: Colors.black, // Separator color + ), + Container( + height: rarityHeight, + decoration: const BoxDecoration( + color: Color(0xFFD6A067), // Rarity section color + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(5.0), + bottomRight: Radius.circular(5.0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: List.generate(4, (i) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0), + child: Image.asset( + 'assets/icons/редкость.png', // Rarity icon + height: rarityHeight > 0 ? rarityHeight * 0.5 : 0, // Adjust icon size proportionally + ), + )), + ), + ), + ], + ), ), ), + // Positioned close button - now a direct child of the outer Stack + if (onRemove != null) // Only show if onRemove callback is provided + Positioned( + top: 0, // Adjust position as needed + right: -3, // Adjust position as needed + child: GestureDetector( + onTap: onRemove, + child: Container( + width: 16, // Adjusted size + height: 16, // Adjusted size + decoration: BoxDecoration( + color: const Color(0xFFFFF4E3), + borderRadius: BorderRadius.circular(3), // Adjusted radius + border: Border.all(color: Colors.black, width: 1), // Match style + ), + child: const Center( + child: Icon( + Icons.close, + size: 12, // Adjusted icon size + color: Colors.black, // Match color + ), + ), + ), + ), + ), ], ), ); @@ -534,8 +659,10 @@ class _CreateExchangeScreenState extends State { children: [ ...selectedExchangeCards.take(4).map((card) => Padding( padding: const EdgeInsets.only(right: 10.0), - child: _buildCollectionCard( - card['id'], + child: _buildExchangeCardVisual( + width: 78, + height: 112, + content: Center(child: Text('Карта ${card['id']}', style: const TextStyle(color: Colors.black, fontSize: 10, fontFamily: 'Jost'),)), onRemove: () { setState(() { selectedExchangeCards.remove(card); @@ -546,7 +673,17 @@ class _CreateExchangeScreenState extends State { if (selectedExchangeCards.length < 4) GestureDetector( onTap: _showLargeCardSelectionDialog, - child: _buildCardPlaceholder(), + child: _buildExchangeCardVisual( + width: 78, + height: 112, + content: Center( + child: Icon( + Icons.add, + size: 20, + color: Colors.black.withOpacity(0.5), + ), + ), + ), ), ], ), @@ -562,8 +699,10 @@ class _CreateExchangeScreenState extends State { children: [ ...selectedExchangeCards.skip(4).take(4).map((card) => Padding( padding: const EdgeInsets.only(right: 10.0), - child: _buildCollectionCard( - card['id'], + child: _buildExchangeCardVisual( + width: 78, + height: 112, + content: Center(child: Text('Карта ${card['id']}', style: const TextStyle(color: Colors.black, fontSize: 10, fontFamily: 'Jost'),)), onRemove: () { setState(() { selectedExchangeCards.remove(card); @@ -574,7 +713,17 @@ class _CreateExchangeScreenState extends State { if (selectedExchangeCards.length < 8) GestureDetector( onTap: _showLargeCardSelectionDialog, - child: _buildCardPlaceholder(), + child: _buildExchangeCardVisual( + width: 78, + height: 112, + content: Center( + child: Icon( + Icons.add, + size: 20, + color: Colors.black.withOpacity(0.5), + ), + ), + ), ), ], ), @@ -583,65 +732,4 @@ class _CreateExchangeScreenState extends State { ], ); } - - void _showLargeCardSelectionDialog() { - showDialog( - context: context, - builder: (BuildContext context) { - return Dialog( - backgroundColor: const Color(0xFFFFF4E3), - child: Container( - width: MediaQuery.of(context).size.width * 0.9, - height: MediaQuery.of(context).size.height * 0.8, - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'Выберите карту для обмена', - style: TextStyle( - fontSize: 20.0, - fontFamily: 'Jost', - color: Colors.black, - ), - ), - const SizedBox(height: 16.0), - Expanded( - child: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 0.7, - crossAxisSpacing: 10.0, - mainAxisSpacing: 10.0, - ), - itemCount: 40, - itemBuilder: (context, index) { - return GestureDetector( - onTap: () { - if (selectedExchangeCards.length < 8) { - setState(() { - selectedExchangeCards.add({ - 'id': index, - 'name': 'Карта $index', - }); - }); - Navigator.pop(context); - } - }, - child: Container( - width: 80, - height: 120, - child: _buildCollectionCard(index), - ), - ); - }, - ), - ), - ], - ), - ), - ); - }, - ); - } } \ No newline at end of file diff --git a/frontend/lib/views/exchange_details_screen.dart b/frontend/lib/views/exchange_details_screen.dart index f6e6c5c..86a739c 100644 --- a/frontend/lib/views/exchange_details_screen.dart +++ b/frontend/lib/views/exchange_details_screen.dart @@ -2,13 +2,25 @@ import 'package:flutter/material.dart'; import 'exchanges_screen.dart'; class ExchangeDetailsScreen extends StatelessWidget { - const ExchangeDetailsScreen({super.key}); + final ExchangeItem exchangeItem; + + const ExchangeDetailsScreen({super.key, required this.exchangeItem}); @override Widget build(BuildContext context) { + // Helper to get status text + String getStatusText(ExchangeStatus status) { + switch (status) { + case ExchangeStatus.pending: return 'Ожидает\nподтверждения'; + case ExchangeStatus.waiting: return 'Ожидает\nдействий'; + case ExchangeStatus.approved: return 'Завершен'; + case ExchangeStatus.rejected: return 'Отклонен'; + } + } + return Dialog( - backgroundColor: const Color(0xFFFFF4E3), - insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40), + backgroundColor: const Color(0xFFEAD7C3), + insetPadding: EdgeInsets.zero, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.0), side: const BorderSide(color: Colors.black, width: 2), @@ -17,8 +29,8 @@ class ExchangeDetailsScreen extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 24.0), - child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 346, maxWidth: 346, minHeight: 0), + child: SizedBox( + width: MediaQuery.of(context).size.width * 1.6, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -40,7 +52,7 @@ class ExchangeDetailsScreen extends StatelessWidget { ), ), Text( - 'CardMaster475', + exchangeItem.nickname, style: const TextStyle( fontSize: 15.0, fontWeight: FontWeight.w500, @@ -57,23 +69,35 @@ class ExchangeDetailsScreen extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Stack из двух карточек (стопка) - Stack( - clipBehavior: Clip.none, - children: [ - Positioned( - left: 6, - top: -4, - child: _buildCardItem('Карточка 2', 'Обычная', width: 48, height: 64), - ), - _buildCardItem('Карточка 1', 'Обычная', width: 48, height: 64), - ], - ), + // Cards offered by the current user (stacked if more than one) + exchangeItem.myOfferedCardIds.length > 1 + ? Stack( + clipBehavior: Clip.none, // Allow cards to overlap + children: [ + // Show up to 3 cards in the stack (visual representation) + Positioned( + left: 12, // Adjust position for stacking + top: 0, // Adjust position for stacking + child: _buildCardItem('Card Name', 'Rarity', width: 48, height: 64), // TODO: Use actual card details from cardId + ), + Positioned( + left: 6, // Adjust position for stacking + top: 0, // Adjust position for stacking + child: _buildCardItem('Card Name', 'Rarity', width: 48, height: 64), // TODO: Use actual card details from cardId + ), + // Always show the front card if count is at least 1 + _buildCardItem('Card Name', 'Rarity', width: 48, height: 64), // TODO: Use actual card details from cardId + ], + ) + : (exchangeItem.myOfferedCardIds.isNotEmpty + ? _buildCardItem('Card Name', 'Rarity', width: 48, height: 64) // Single card + : SizedBox.shrink()), // No cards + // Статус по центру Expanded( child: Center( child: Text( - 'В ожидании', + getStatusText(exchangeItem.status), // Use the exchange status style: const TextStyle( color: Colors.black, fontWeight: FontWeight.bold, @@ -83,65 +107,141 @@ class ExchangeDetailsScreen extends StatelessWidget { ), ), ), - // Карточка пользователя - _buildCardItem('Карточка пользователя', 'Редкая', width: 48, height: 64), + // Cards offered by the other user (stacked if more than one) + exchangeItem.otherUserOfferedCardIds.length > 1 + ? Stack( + clipBehavior: Clip.none, // Allow cards to overlap + children: [ + // Show up to 3 cards in the stack (visual representation) + Positioned( + left: 12, // Adjust position for stacking + top: 0, // Adjust position for stacking + child: _buildCardItem('Card Name', 'Rarity', width: 48, height: 64), // TODO: Use actual card details from cardId + ), + Positioned( + left: 6, // Adjust position for stacking + top: 0, // Adjust position for stacking + child: _buildCardItem('Card Name', 'Rarity', width: 48, height: 64), // TODO: Use actual card details from cardId + ), + // Always show the front card if count is at least 1 + _buildCardItem('Card Name', 'Rarity', width: 48, height: 64), // TODO: Use actual card details from cardId + ], + ) + : (exchangeItem.otherUserOfferedCardIds.isNotEmpty + ? _buildCardItem('Card Name', 'Rarity', width: 48, height: 64) // Single card + : SizedBox.shrink()), // No cards ], ), ), const SizedBox(height: 12.0), - Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 729, - height: 60, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - textStyle: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - fontFamily: 'Jost', - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), + // Conditionally display buttons based on status + if (exchangeItem.status == ExchangeStatus.waiting) // Show both buttons if waiting for actions + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: double.infinity, + height: 60, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + fontFamily: 'Jost', + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), ), + onPressed: () { + Navigator.of(context).pop(); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => ExchangesScreen( + notification: 'Обмен принят', + ), + ), + (route) => false, + ); + }, + child: const Text('Принять'), ), - onPressed: () { - _showConfirmationDialog(context, 'Принять обмен?', false); - }, - child: const Text('Принять'), ), - ), - const SizedBox(height: 12.0), - SizedBox( - width: 729, - height: 60, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - textStyle: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - fontFamily: 'Jost', - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), + const SizedBox(height: 12.0), + SizedBox( + width: double.infinity, + height: 60, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + fontFamily: 'Jost', + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), ), + onPressed: () { + Navigator.of(context).pop(); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => ExchangesScreen( + notification: 'Обмен отклонен', + ), + ), + (route) => false, + ); + }, + child: const Text('Отклонить'), ), - onPressed: () { - _showConfirmationDialog(context, 'Отклонить обмен?', true); - }, - child: const Text('Отклонить'), ), + ], + ), + ) + else if (exchangeItem.status == ExchangeStatus.pending) // Show only Cancel button if pending + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: SizedBox( + width: double.infinity, + height: 60, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + fontFamily: 'Jost', + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + ), + onPressed: () { + Navigator.of(context).pop(); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => ExchangesScreen( + notification: 'Обмен отклонен', + ), + ), + (route) => false, + ); + }, + child: const Text('Отменить'), // Text for cancelling pending exchange ), - ], + ), ), - ), ], ), ), @@ -175,87 +275,106 @@ class ExchangeDetailsScreen extends StatelessWidget { ); } - Widget _buildCardItem(String title, String rarity, {double width = 80, double height = 120}) { + Widget _buildCardItem(String title, String rarity, {double width = 48, double height = 64}) { + // Precise calculation of vertical space consumed by non-content elements: + // Outer border (top + bottom): 1.5 + 1.5 = 3 + // Padding (top + bottom) within outer border: 1.5 + 1.5 = 3 + // Inner border (top + bottom) after first padding: 1.5 + 1.5 = 3 + // Padding (top + bottom) after inner border: 1.0 + 1.0 = 2 + // Separator line: 1 + double totalNonContentHeight = 3 + 3 + 3 + 2 + 1; // Total vertical space used by borders, padding, and line + + double availableContentHeight = height - totalNonContentHeight; // Height remaining for image and rarity areas + + // Distribute available height based on desired proportions (approx. 65/35 split) + double imageHeight = availableContentHeight * 0.65; + double rarityHeight = availableContentHeight * 0.2; + + // Ensure calculated heights are non-negative + imageHeight = imageHeight > 0 ? imageHeight : 0; + rarityHeight = rarityHeight > 0 ? rarityHeight : 0; + return Container( width: width, height: height, - margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0), alignment: Alignment.center, decoration: BoxDecoration( - color: const Color(0xFFD9A76A), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), + color: Colors.transparent, + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.black, width: 1.5), ), - child: Container( - margin: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: const Color(0xFFD9A76A), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 1.5), - ), - child: Stack( - children: [ - // Горизонтальная линия внизу - Positioned( - left: 0, - right: 0, - bottom: height * 0.2, - child: Container( - height: 2, - color: Colors.black.withOpacity(0.5), - ), - ), - ], - ), - ), - ); - } - - void _showConfirmationDialog(BuildContext context, String message, bool isDecline) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - backgroundColor: const Color(0xFFFFF4E3), - title: Text(message), - content: Text( - isDecline - ? 'Это действие невозможно будет отменить.' - : 'Карточки будут переданы между участниками обмена.', + child: Padding( + padding: const EdgeInsets.all(0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(3), ), - actions: [ - TextButton( - style: TextButton.styleFrom( - foregroundColor: Colors.black, - ), - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Отмена'), - ), - TextButton( - style: TextButton.styleFrom( - foregroundColor: isDecline ? Colors.red : Colors.green, + child: Padding( + padding: const EdgeInsets.all(1.5), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(2), + border: Border.all(color: Colors.black, width: 1.5), ), - onPressed: () { - Navigator.of(context).pop(); - Navigator.of(context).pop(); - // Передаю текст уведомления на ExchangesScreen - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - builder: (context) => ExchangesScreen( - notification: isDecline ? 'Обмен отклонен' : 'Обмен принят', - ), + child: Padding( + padding: EdgeInsets.zero, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(1.0), ), - (route) => false, - ); - }, - child: Text(isDecline ? 'Отклонить' : 'Принять'), + child: Column( + children: [ + // Image placeholder area + Container( + height: imageHeight, // Use precisely calculated height + decoration: const BoxDecoration( + color: Color(0xFFEAD7C3), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(1.0), + topRight: Radius.circular(1.0), + ), + ), + child: const Center( + child: Text( + 'Нет \n изоб', + style: TextStyle(color: Colors.black45, fontSize: 7), + textAlign: TextAlign.center, + ), + ), + ), + Container( + height: 1, + color: Colors.black, + ), + // Rarity icons area + Container( + height: rarityHeight, // Use precisely calculated height + decoration: const BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(1.0), + bottomRight: Radius.circular(1.0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(4, (i) => Image.asset( + 'assets/icons/редкость.png', + height: rarityHeight > 0 ? rarityHeight * 0.7 : 0, // Adjust icon size proportionally + )), + ), + ), + ], + ), + ), + ), ), - ], - ); - }, + ), + ), + ), ); } } \ No newline at end of file diff --git a/frontend/lib/views/exchanges_screen.dart b/frontend/lib/views/exchanges_screen.dart index fa507ce..e0793e4 100644 --- a/frontend/lib/views/exchanges_screen.dart +++ b/frontend/lib/views/exchanges_screen.dart @@ -6,10 +6,12 @@ import 'shop_screen.dart'; import 'inventory_screen.dart'; import 'profile_screen.dart'; import 'search_players_screen.dart'; +import 'exchange_proposal_screen.dart'; class ExchangesScreen extends StatefulWidget { final String? notification; - const ExchangesScreen({super.key, this.notification}); + final int? initialTabIndex; + const ExchangesScreen({super.key, this.notification, this.initialTabIndex}); @override State createState() => _ExchangesScreenState(); @@ -24,7 +26,7 @@ class _ExchangesScreenState extends State with SingleTickerProv @override void initState() { super.initState(); - _tabController = TabController(length: 2, vsync: this); + _tabController = TabController(length: 2, vsync: this, initialIndex: widget.initialTabIndex ?? 0); // Показываем Snackbar, если notification не null WidgetsBinding.instance.addPostFrameCallback((_) { if (widget.notification != null) { @@ -34,7 +36,7 @@ class _ExchangesScreenState extends State with SingleTickerProv width: 367, height: 61, decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(15.0), ), padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), @@ -51,7 +53,7 @@ class _ExchangesScreenState extends State with SingleTickerProv ), ), backgroundColor: Colors.transparent, - duration: const Duration(seconds: 5), + duration: const Duration(seconds: 1), behavior: SnackBarBehavior.floating, margin: EdgeInsets.only( bottom: MediaQuery.of(context).size.height - 200, @@ -74,9 +76,9 @@ class _ExchangesScreenState extends State with SingleTickerProv @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, title: Container( padding: const EdgeInsets.all(8.0), @@ -108,163 +110,178 @@ class _ExchangesScreenState extends State with SingleTickerProv ), ), ), - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, + body: Stack( children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const CreateExchangeScreen()), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 12.0), - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const CreateExchangeScreen()), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 12.0), + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + children: [ + Text( + 'Создать обмен', + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 6.0), + Container( + width: 20.0, + height: 20.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.black, width: 1.0), + ), + child: const Icon(Icons.add, size: 18.0), + ), + ], + ), + ), ), - child: Row( - children: [ - Text( - 'Создать обмен', - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, + + Container( + child: IconButton( + icon: Image.asset('assets/icons/поиск.png', height: 32), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const SearchPlayersModal(), + ); + }, + ), + ), + ], + ), + ), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () { + setState(() { + _showSortOptions = !_showSortOptions; + }); + }, + child: Row( + children: [ + const Text( + 'Сортировка', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), ), - ), - SizedBox(width: 6.0), - Container( - width: 20.0, - height: 20.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 1.0), + const SizedBox(width: 4.0), + Icon( + _showSortOptions ? Icons.arrow_drop_up : Icons.arrow_drop_down, + color: Colors.black, ), - child: const Icon(Icons.add, size: 18.0), - ), - ], + ], + ), ), - ), + ], ), - - Container( - child: IconButton( - icon: Image.asset('assets/icons/поиск.png', height: 32), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => const SearchPlayersModal(), - ); - }, - ), + ), + + const SizedBox(height: 8.0), + + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildExchangesTab(), + _buildMyExchangesTab(), + ], ), - ], - ), + ), + ], ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - InkWell( - onTap: () { - setState(() { - _showSortOptions = !_showSortOptions; - }); - }, - child: Row( - children: [ - const Text( - 'Сортировка', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 4.0), - Icon( - _showSortOptions ? Icons.arrow_drop_up : Icons.arrow_drop_down, - color: Colors.black, - ), - ], - ), - ), - - if (_showSortOptions) - Container( - margin: const EdgeInsets.only(top: 8.0), - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), + // Sorting options dropdown (Positioned overlay) + if (_showSortOptions) + Positioned( + top: 110, // Approximate position below the Sorting text + left: 16, // Align with the left padding + child: Container( + width: 120.0, // Set a fixed width to match inventory_screen dropdown size + padding: const EdgeInsets.all(12.0), + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), // Background color from inventory_screen + borderRadius: BorderRadius.circular(10.0), // Border radius from inventory_screen + boxShadow: [ // Box shadow from inventory_screen + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - InkWell( - onTap: () { - setState(() { - _sortOption = 'По дате'; - _showSortOptions = false; - }); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - 'По дате', - style: TextStyle( - fontSize: 14.0, - fontWeight: _sortOption == 'По дате' ? FontWeight.bold : FontWeight.normal, - ), - ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () { + setState(() { + _sortOption = 'По дате'; // Option 1 + _showSortOptions = false; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + 'По дате', + style: TextStyle( + fontSize: 14.0, + fontWeight: _sortOption == 'По дате' ? FontWeight.bold : FontWeight.normal, ), ), - const Divider(), - InkWell( - onTap: () { - setState(() { - _sortOption = 'По редкости'; - _showSortOptions = false; - }); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - 'По редкости', - style: TextStyle( - fontSize: 14.0, - fontWeight: _sortOption == 'По редкости' ? FontWeight.bold : FontWeight.normal, - ), - ), + ), + ), + InkWell( + onTap: () { + setState(() { + _sortOption = 'По редкости'; // Option 2 + _showSortOptions = false; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + 'По редкости', + style: TextStyle( + fontSize: 14.0, + fontWeight: _sortOption == 'По редкости' ? FontWeight.bold : FontWeight.normal, ), ), - ], + ), ), - ), - ], - ), - ), - - const SizedBox(height: 8.0), - - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - _buildExchangesTab(), - _buildMyExchangesTab(), - ], + ], + ), + ), ), - ), ], ), bottomNavigationBar: Container( @@ -377,184 +394,355 @@ class _ExchangesScreenState extends State with SingleTickerProv Widget _buildExchangesTab() { return ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - itemCount: 5, + padding: const EdgeInsets.all(12.0), + itemCount: 5, itemBuilder: (context, index) { - return _buildCardExchangeItem(); + // Alternate between 1 and 3 for demonstration counts + final myCardCount = (index % 2 == 0) ? 1 : 5; + final otherUserCardCount = 1; // Modified to always be 1 + // Use dummy data for demonstration in this tab + final dummyNickname = 'User ${(index % 3) + 1}'; + final dummyExchangeItem = ExchangeItem( + date: 'N/A', // Dummy date + status: ExchangeStatus.pending, // Dummy status + myOfferedCardIds: List.generate(myCardCount, (i) => i + 1), // Dummy IDs + otherUserOfferedCardIds: List.generate(otherUserCardCount, (i) => i + 11), // Dummy IDs + nickname: dummyNickname, + ); + return _buildCardExchangeItem( + myOfferedCount: myCardCount, + otherUserOfferedCount: otherUserCardCount, + otherUserName: dummyNickname, + exchangeItem: dummyExchangeItem, + isMyExchange: false, + showLeftPlus: index == 0, // Show plus only on the left card of the first item + ); }, ); } Widget _buildMyExchangesTab() { + // Using dummy ExchangeItem data for demonstration + final List myExchanges = [ + ExchangeItem(date: '2023-10-27', status: ExchangeStatus.pending, myOfferedCardIds: [1], otherUserOfferedCardIds: [5], nickname: 'Alex'), + ExchangeItem(date: '2023-10-26', status: ExchangeStatus.approved, myOfferedCardIds: [2, 3, 4], otherUserOfferedCardIds: [6], nickname: 'Maria'), + ExchangeItem(date: '2023-10-25', status: ExchangeStatus.rejected, myOfferedCardIds: [7], otherUserOfferedCardIds: [8, 9, 10], nickname: 'Ivan'), + ExchangeItem(date: '2023-10-24', status: ExchangeStatus.waiting, myOfferedCardIds: [11, 12], otherUserOfferedCardIds: [14], nickname: 'David'), // Modified to prevent stack on right side + ]; + return ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - itemCount: 3, + padding: const EdgeInsets.all(12.0), + itemCount: myExchanges.length, itemBuilder: (context, index) { - return _buildCardExchangeItem(); + final exchange = myExchanges[index]; + return _buildCardExchangeItem( + myOfferedCount: exchange.myOfferedCardIds.length, + otherUserOfferedCount: exchange.otherUserOfferedCardIds.length, + otherUserName: exchange.nickname, + exchangeItem: exchange, + isMyExchange: true, // Set to true for this tab + ); }, ); } - Widget _buildCardExchangeItem() { + Widget _buildCardExchangeItem({ + int myOfferedCount = 1, + int otherUserOfferedCount = 1, + required String otherUserName, + required ExchangeItem exchangeItem, + bool isMyExchange = false, + bool showLeftPlus = false, + }) { + // Helper to get status text + String getStatusText(ExchangeStatus status) { + switch (status) { + case ExchangeStatus.pending: return 'Ожидает\nподтверждения'; + case ExchangeStatus.waiting: return 'Ожидает\nдействий'; + case ExchangeStatus.approved: return 'Завершен'; + case ExchangeStatus.rejected: return 'Отклонен'; + } + } + return GestureDetector( onTap: () { - showDialog( - context: context, - barrierDismissible: true, - builder: (BuildContext context) { - return Dialog( - backgroundColor: Colors.transparent, - elevation: 0, - child: ExchangeDetailsScreen(), + if (!isMyExchange) { // If in 'Обмен' tab + if (showLeftPlus) { // If the left card has a plus icon + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CreateExchangeScreen(initialExchangeItem: exchangeItem), // Pass the exchangeItem + ), ); - }, - ); + } else { // If in 'Обмен' tab and no plus icon (multiple cards) + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ExchangeProposalScreen(exchangeItem: exchangeItem), // Navigate to ExchangeProposalScreen + ), + ); + } + } else { // If in 'Мои обмены' tab, show ExchangeDetailsScreen dialog + showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: ExchangeDetailsScreen(exchangeItem: exchangeItem), + ); + }, + ); + } }, child: Container( - margin: const EdgeInsets.only(bottom: 16.0), + margin: const EdgeInsets.only(bottom: 12.0), padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(8.0), ), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Stack( - children: [ - Container( - width: 45.0, - height: 60.0, - decoration: BoxDecoration( - color: const Color(0xFFD9A76A), - borderRadius: BorderRadius.circular(4.0), - border: Border.all( - color: Colors.black, - width: 2.0, - ), - ), - child: Container( - margin: const EdgeInsets.all(2.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(2.0), - border: Border.all( - color: Colors.black, - width: 2.0, - ), - ), - child: Column( - children: [ - Expanded( - flex: 7, - child: Container(), - ), - Container( - height: 2.0, - color: Colors.black, - ), - Expanded( - flex: 3, - child: Container(), - ), - ], - ), - ), - ), - ], + // First card (offered by current user) - Stacked if 2 or more + SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: Column( + children: [ + // Offset to align with right card visual (conditional based on tab) + SizedBox(height: 24.0), // Always apply height for alignment + myOfferedCount >= 2 + ? Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + left: 16, + top: 0, + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: showLeftPlus), + ), + ), + ), + Positioned( + left: 8, + top: 0, + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: showLeftPlus), + ), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: showLeftPlus), + ), + ), + ], + ) + : (myOfferedCount == 1 + ? AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: showLeftPlus), + ) + : SizedBox.shrink()), + ], + ), ), + // Swap icon or Status text Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Icon(Icons.sync_alt, size: 24.0), + children: [ + isMyExchange + ? Text( + getStatusText(exchangeItem.status), + style: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + textAlign: TextAlign.center, + ) + : Padding( + padding: EdgeInsets.only(top: isMyExchange ? 18.0 : 0.0), + child: const Icon(Icons.sync_alt, size: 24.0), + ), ], ), ), - Stack( - children: [ - Container( - width: 45.0, - height: 60.0, - margin: const EdgeInsets.only(top: 4.0, left: 4.0), - decoration: BoxDecoration( - color: const Color(0xFFD9A76A), - borderRadius: BorderRadius.circular(4.0), - border: Border.all( - color: Colors.black, - width: 2.0, - ), - ), - child: Container( - margin: const EdgeInsets.all(2.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(2.0), - border: Border.all( - color: Colors.black, - width: 2.0, + // Second card(s) block (offered by other user) + SizedBox( + width: MediaQuery.of(context).size.width * 0.2 + (otherUserOfferedCount > 1 ? 16 : 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + children: [ + Text( + otherUserName, + style: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, ), + const SizedBox(height: 4.0), + ], + ), + otherUserOfferedCount >= 2 + ? Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + left: 16, + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: false), // Always false for right side + ), + ), + ), + if (otherUserOfferedCount >= 2) + Positioned( + left: 8, + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: false), // Always false for right side + ), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: false), // Always false for right side + ), + ), + ], + ) + : (otherUserOfferedCount == 1 + ? AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: false), // Always false for right side + ) + : SizedBox.shrink()), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildSingleCardVisual({bool showPlus = false}) { + return Stack( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + ), + Padding( + padding: const EdgeInsets.all(2.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(7), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.black, width: 2), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(6.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(5.0), + ), + child: Column( + children: [ + Expanded( + flex: 8, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(5.0), + topRight: Radius.circular(5.0), ), - child: Column( - children: [ - Expanded( - flex: 7, - child: Container(), - ), - Container( - height: 2.0, - color: Colors.black, - ), - Expanded( - flex: 3, - child: Container(), - ), - ], + child: Container( + color: const Color(0xFFEAD7C3), + child: showPlus + ? const Center( + child: Icon( + Icons.add, + color: Colors.black, + size: 32, + ), + ) + : const Center( + child: Text( + 'Нет изображения', + style: TextStyle(color: Colors.black45, fontSize: 12), + ), + ), ), ), ), Container( - width: 45.0, - height: 60.0, - decoration: BoxDecoration( - color: const Color(0xFFD9A76A), - borderRadius: BorderRadius.circular(4.0), - border: Border.all( - color: Colors.black, - width: 2.0, + height: 3, + color: Colors.black, + ), + Container( + height: 32, + decoration: const BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(5.0), + bottomRight: Radius.circular(5.0), ), ), - child: Container( - margin: const EdgeInsets.all(2.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(2.0), - border: Border.all( - color: Colors.black, - width: 2.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: List.generate(4, (i) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0), + child: Image.asset( + 'assets/icons/редкость.png', + height: 14, ), - ), - child: Column( - children: [ - Expanded( - flex: 7, - child: Container(), - ), - Container( - height: 2.0, - color: Colors.black, - ), - Expanded( - flex: 3, - child: Container(), - ), - ], - ), + )), ), ), ], ), - ], + ), ), - ), + ], ); } } @@ -569,15 +757,15 @@ enum ExchangeStatus { class ExchangeItem { final String date; final ExchangeStatus status; - final int fromCards; - final int toCards; + final List myOfferedCardIds; + final List otherUserOfferedCardIds; final String nickname; ExchangeItem({ required this.date, required this.status, - required this.fromCards, - required this.toCards, + required this.myOfferedCardIds, + required this.otherUserOfferedCardIds, required this.nickname, }); } \ No newline at end of file diff --git a/frontend/lib/views/home_screen.dart b/frontend/lib/views/home_screen.dart index 1498be9..138cf8c 100644 --- a/frontend/lib/views/home_screen.dart +++ b/frontend/lib/views/home_screen.dart @@ -21,9 +21,9 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, leading: Padding( padding: const EdgeInsets.only(left: 16.0), @@ -78,6 +78,7 @@ class _HomeScreenState extends State { style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), ), @@ -132,7 +133,8 @@ class _HomeScreenState extends State { 'Создай свою уникальную карточку', style: TextStyle( fontSize: 20.0, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w400, + fontFamily: 'Roboto', ), ), ), @@ -171,7 +173,8 @@ class _HomeScreenState extends State { 'Квесты', style: TextStyle( fontSize: 18.0, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w400, + fontFamily: 'Roboto', ), ), ], @@ -212,7 +215,8 @@ class _HomeScreenState extends State { 'Новости', style: TextStyle( fontSize: 18.0, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w400, + fontFamily: 'Roboto', ), ), ], diff --git a/frontend/lib/views/inventory_screen.dart b/frontend/lib/views/inventory_screen.dart index a85a19b..4f8fe93 100644 --- a/frontend/lib/views/inventory_screen.dart +++ b/frontend/lib/views/inventory_screen.dart @@ -175,7 +175,7 @@ class _InventoryScreenState extends State { child: Container( padding: const EdgeInsets.all(12.0), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(8.0), boxShadow: [ BoxShadow( diff --git a/frontend/lib/views/news_detail_screen.dart b/frontend/lib/views/news_detail_screen.dart index 16bcc79..468ee5e 100644 --- a/frontend/lib/views/news_detail_screen.dart +++ b/frontend/lib/views/news_detail_screen.dart @@ -1,10 +1,14 @@ import 'package:flutter/material.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; -import 'exchanges_screen.dart'; -import 'news_screen.dart'; +import 'news_screen.dart'; // Import NewsItem class -class NewsDetailScreen extends StatefulWidget { +// Remove unused imports +// import 'home_screen.dart'; +// import 'shop_screen.dart'; +// import 'exchanges_screen.dart'; +// import 'profile_screen.dart'; +// import 'search_players_screen.dart'; + +class NewsDetailScreen extends StatelessWidget { final NewsItem news; const NewsDetailScreen({ @@ -12,230 +16,114 @@ class NewsDetailScreen extends StatefulWidget { required this.news, }); - @override - State createState() => _NewsDetailScreenState(); -} - -class _NewsDetailScreenState extends State { - int _currentIndex = 1; // Индекс вкладки "Новости" в нижней навигации - @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон - appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), - elevation: 0, - leading: Container( - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 2), - ), - child: Image.asset( - 'assets/icons/профиль.png', - height: 24, - color: Colors.black, - ), - ), - title: const Text( - 'Новости', - style: TextStyle( - color: Colors.black, - fontSize: 24.0, - fontWeight: FontWeight.bold, - ), - ), - centerTitle: true, - actions: [ - IconButton( - icon: Image.asset( - 'assets/icons/поиск.png', - height: 24, - color: Colors.black, - ), - onPressed: () {}, - ), - IconButton( - icon: Image.asset( - 'assets/icons/уведомления.png', - height: 24, - color: Colors.black, - ), - onPressed: () {}, - ), - ], + return Dialog( + backgroundColor: const Color(0xFFEAD7C3), // Match dialog background color style + insetPadding: EdgeInsets.zero, // Remove default dialog padding + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), // Match border radius style + side: const BorderSide(color: Colors.black, width: 2), // Match border style ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Кнопка возврата - Align( - alignment: Alignment.centerLeft, - child: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - shape: BoxShape.circle, + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 24.0), // Adjust padding as needed + child: SingleChildScrollView( // Keep SingleChildScrollView for content that might overflow + child: Column( + mainAxisSize: MainAxisSize.min, // Use min size for column + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // News content - Title, Image, Full Content, Date + Center( // Center the title + child: Text( + news.title, // Use news object directly in StatelessWidget + style: const TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), ), - child: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black), - onPressed: () { - Navigator.pop(context); - }, + + const SizedBox(height: 24.0), // Keep spacing + + // Картинка + Container( + height: 180, + width: double.infinity, + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(8.0), + border: Border.all(color: Colors.black, width: 1.0), // Added thin black border + ), + alignment: Alignment.center, + child: const Center( // Use Center to center the text + child: Text( + 'Нет изображения', // Use the placeholder text from inventory card + style: TextStyle( + color: Colors.black45, // Use the text color from inventory card + fontSize: 16.0, // Adjust font size as needed for this larger area + // fontWeight: FontWeight.bold, // Remove bold fontWeight + ), + textAlign: TextAlign.center, + ), + ), ), - ), + + const SizedBox(height: 24.0), // Keep spacing + + // Расширенное описание + Text( + news.fullContent, // Use news object directly + style: const TextStyle( + fontSize: 16.0, + ), + ), + + const SizedBox(height: 16.0), // Keep spacing + + // Дата + Align( + alignment: Alignment.centerRight, + child: Text( + 'Дата публикации: ${news.date}', // Use news object directly + style: const TextStyle( + fontSize: 14.0, + fontStyle: FontStyle.italic, + color: Colors.black54, + ), + ), + ), + ], ), - - const SizedBox(height: 16.0), - - // Карточка детальной новости - Container( - width: double.infinity, - padding: const EdgeInsets.all(16.0), + ), + ), + // Close button in the top right corner + Positioned( + top: 0, // Adjust position as needed + right: 0, // Adjust position as needed + child: GestureDetector( + onTap: () => Navigator.pop(context), // Close the dialog + child: Container( + width: 48, // Match size from exchange_details_screen.dart + height: 48, // Match size from exchange_details_screen.dart decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), + color: Color(0xFFD6A067), // Match color + borderRadius: BorderRadius.circular(10), // Match border radius + border: Border.all(color: Colors.black, width: 3), // Match border style ), - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Заголовок - Center( - child: Text( - widget.news.title, - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - ), - - const SizedBox(height: 24.0), - - // Картинка - Container( - height: 180, - width: double.infinity, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), - alignment: Alignment.center, - child: const Text( - 'Картинка к новости', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - ), - - const SizedBox(height: 24.0), - - // Расширенное описание - Text( - widget.news.fullContent, - style: const TextStyle( - fontSize: 16.0, - ), - ), - - const SizedBox(height: 16.0), - - // Дата - Align( - alignment: Alignment.centerRight, - child: Text( - 'Дата публикации: ${widget.news.date}', - style: const TextStyle( - fontSize: 14.0, - fontStyle: FontStyle.italic, - color: Colors.black54, - ), - ), - ), - ], - ), - ], + child: const Center( + child: Icon( + Icons.close, + color: Colors.black, + size: 28, // Match icon size + ), ), ), - ], - ), - ), - ), - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 1: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const NewsScreen()), - (route) => false, - ); - break; - case 2: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Гл.меню', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', - ), - ], - ), + ), + ], ), ); } diff --git a/frontend/lib/views/news_screen.dart b/frontend/lib/views/news_screen.dart index 8c82577..d863bb5 100644 --- a/frontend/lib/views/news_screen.dart +++ b/frontend/lib/views/news_screen.dart @@ -3,6 +3,8 @@ import 'home_screen.dart'; import 'shop_screen.dart'; import 'exchanges_screen.dart'; import 'news_detail_screen.dart'; +import 'profile_screen.dart'; +import 'search_players_screen.dart'; class NewsScreen extends StatefulWidget { const NewsScreen({super.key}); @@ -12,7 +14,7 @@ class NewsScreen extends StatefulWidget { } class _NewsScreenState extends State { - int _currentIndex = 1; // Индекс для нижней навигации (книга) + int _currentIndex = 0; // Changed from 1 to 0 to select Home tab // Примерные данные новостей (без использования типа DateTime для избежания ошибок) final List _newsItems = [ @@ -45,38 +47,50 @@ class _NewsScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон + backgroundColor: const Color(0xFFFBF6EF), // Бежевый фон appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, - leading: Container( - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 2), - ), - child: Image.asset( - 'assets/icons/профиль.png', - height: 24, - color: Colors.black, + leading: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Container( + width: 40.0, + height: 40.0, + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ProfileScreen()), + ); + }, + child: Image.asset('assets/icons/профиль.png', height: 22), + ), ), ), actions: [ IconButton( icon: Image.asset( 'assets/icons/поиск.png', - height: 24, + height: 32, color: Colors.black, ), - onPressed: () {}, + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const SearchPlayersModal(), + ); + }, ), IconButton( icon: Image.asset( 'assets/icons/уведомления.png', - height: 24, + height: 36, color: Colors.black, ), - onPressed: () {}, + onPressed: null, ), ], ), @@ -85,12 +99,13 @@ class _NewsScreenState extends State { children: [ // Кнопка возврата и заголовок Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.only(left: 12.0, right: 12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Кнопка назад Container( + margin: const EdgeInsets.only(left: 0.0), decoration: const BoxDecoration( color: Color(0xFFD6A067), shape: BoxShape.circle, @@ -151,24 +166,24 @@ class _NewsScreenState extends State { }); switch (index) { case 0: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, ); break; + case 1: + // Current screen is NewsScreen, do nothing or handle appropriately + break; case 2: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, ); break; case 3: - Navigator.pushAndRemoveUntil( + Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, ); break; } @@ -179,22 +194,69 @@ class _NewsScreenState extends State { type: BottomNavigationBarType.fixed, selectedItemColor: Colors.black, unselectedItemColor: Colors.black54, + showSelectedLabels: true, + showUnselectedLabels: true, + selectedIconTheme: const IconThemeData( + size: 28, + ), + unselectedIconTheme: const IconThemeData( + size: 24, + ), + selectedLabelStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + unselectedLabelStyle: const TextStyle( + fontSize: 11, + ), items: [ BottomNavigationBarItem( icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Гл.меню', + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/главная.png', height: 24), + ), + label: 'Главная', ), BottomNavigationBarItem( icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/Инвентарь.png', height: 24), + ), + label: 'Инвентарь', ), BottomNavigationBarItem( icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/магазин.png', height: 24), + ), + label: 'Магазин', ), BottomNavigationBarItem( icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/обменник.png', height: 24), + ), + label: 'Обменник', ), ], ), @@ -206,45 +268,37 @@ class _NewsScreenState extends State { Widget _buildNewsItem(NewsItem news) { return GestureDetector( onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => NewsDetailScreen(news: news), - ), + showDialog( + context: context, + builder: (context) => NewsDetailScreen(news: news), ); }, child: Container( margin: const EdgeInsets.only(bottom: 16.0), padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(8.0), ), - child: Row( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Левая часть - заголовок - Expanded( - flex: 1, - child: Text( - news.title, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14.0, - ), + // Заголовок + Text( + news.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.0, ), ), - const SizedBox(width: 16.0), + const SizedBox(height: 8.0), - // Правая часть - текст - Expanded( - flex: 1, - child: Text( - news.content, - style: const TextStyle( - fontSize: 14.0, - ), + // Текст + Text( + news.content, + style: const TextStyle( + fontSize: 14.0, ), ), ], diff --git a/frontend/lib/views/pack_content_screen.dart b/frontend/lib/views/pack_content_screen.dart index 43034e6..5963964 100644 --- a/frontend/lib/views/pack_content_screen.dart +++ b/frontend/lib/views/pack_content_screen.dart @@ -9,10 +9,11 @@ class PackContentScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, + automaticallyImplyLeading: false, title: const Text('Выпавшие карты:', style: TextStyle(color: Colors.black)), centerTitle: true, ), @@ -33,41 +34,91 @@ class PackContentScreen extends StatelessWidget { return Container( width: 80.0, height: 120.0, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(6.0), - border: Border.all(color: Colors.black, width: 2), - ), child: Stack( children: [ - // Основная рамка карты + // Внешняя тонкая черная рамка Container( decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.black, width: 2), - borderRadius: BorderRadius.circular(6.0), ), ), - // Внутренняя рамка карты - Positioned( - top: 5, - left: 5, - right: 5, - bottom: 5, + // Прослойка цвета карточки + Padding( + padding: const EdgeInsets.all(2.0), child: Container( decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(7), + ), + ), + ), + // Внутренняя тонкая черная рамка + Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.black, width: 2), - borderRadius: BorderRadius.circular(4.0), ), ), ), - // Линия разделения карты - Positioned( - left: 5, - right: 5, - bottom: 40, + // Основная карточка + Padding( + padding: const EdgeInsets.all(6.0), child: Container( - height: 1, - color: Colors.black, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(5.0), + ), + child: Column( + children: [ + Expanded( + flex: 8, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(5.0), + topRight: Radius.circular(5.0), + ), + child: Container( + color: const Color(0xFFEAD7C3), + child: const Center( + child: Text( + 'Нет изображения', + style: TextStyle(color: Colors.black45, fontSize: 12), + ), + ), + ), + ), + ), + Container( + height: 3, + color: Colors.black, + ), + Container( + height: 32, + decoration: const BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(5.0), + bottomRight: Radius.circular(5.0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: List.generate(4, (i) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0), + child: Image.asset( + 'assets/icons/редкость.png', + height: 14, + ), + )), + ), + ), + ], + ), ), ), ], @@ -96,7 +147,7 @@ class PackContentScreen extends StatelessWidget { foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(vertical: 16.0), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), + borderRadius: BorderRadius.circular(10.0), ), ), child: const Text('Вернуться в магазин', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), @@ -118,13 +169,13 @@ class PackContentScreen extends StatelessWidget { foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(vertical: 16.0), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), + borderRadius: BorderRadius.circular(10.0), ), ), - child: const Text('Перейти в Инвентарь', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + child: const Text('Перейти в инвентарь', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), ), ), - const SizedBox(height: 16), + const SizedBox(height: 120), ], ), ), diff --git a/frontend/lib/views/profile_screen.dart b/frontend/lib/views/profile_screen.dart index 123dfd4..281f5eb 100644 --- a/frontend/lib/views/profile_screen.dart +++ b/frontend/lib/views/profile_screen.dart @@ -13,6 +13,7 @@ class ProfileScreen extends StatelessWidget { final String? playerId; final int? cardsCollected; final int? collectionsCollected; + final List favoriteCardIds; const ProfileScreen({ super.key, @@ -21,6 +22,7 @@ class ProfileScreen extends StatelessWidget { this.playerId, this.cardsCollected, this.collectionsCollected, + this.favoriteCardIds = const [], }); @override @@ -168,8 +170,8 @@ class ProfileScreen extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate( - 5, - (index) => _buildCard(), + favoriteCardIds.length, + (index) => _buildCard(favoriteCardIds[index]), ), ), ), @@ -424,7 +426,7 @@ class ProfileScreen extends StatelessWidget { } // Карточка в профиле - Widget _buildCard() { + Widget _buildCard(int cardId) { return SizedBox( width: 74, height: 112, @@ -461,48 +463,60 @@ class ProfileScreen extends StatelessWidget { ), ), ), - // Основная карточка с отделением редкости + // Основная карточка Padding( padding: const EdgeInsets.all(6.0), - child: Column( - children: [ - Expanded( - flex: 8, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(5.0), + ), + child: Column( + children: [ + Expanded( + flex: 8, + child: ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(5.0), topRight: Radius.circular(5.0), ), + child: Container( + color: const Color(0xFFEAD7C3), + child: const Center( + child: Text( + 'Нет изображения', + style: TextStyle(color: Colors.black45, fontSize: 12), + ), + ), + ), ), ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 20, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), + Container( + height: 3, + color: Colors.black, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(4, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 10, + Container( + height: 20, + decoration: const BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(5.0), + bottomRight: Radius.circular(5.0), ), - )), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: List.generate(4, (i) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0), + child: Image.asset( + 'assets/icons/редкость.png', + height: 10, + ), + )), + ), ), - ), - ], + ], + ), ), ), ], @@ -533,94 +547,114 @@ class _ReportDialogState extends State { Widget build(BuildContext context) { return Dialog( backgroundColor: const Color(0xFFEAD7C3), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - child: Container( - width: 350, - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: const Color(0xFFD6A067), - ), - child: InkWell( - borderRadius: BorderRadius.circular(20.0), - onTap: () => Navigator.pop(context), - child: const Icon(Icons.arrow_back, color: Colors.black, size: 29), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Подать жалобу на пользователя ${widget.playerName}', - style: const TextStyle(fontSize: 17), - ), - ), - ], - ), - const SizedBox(height: 18), - ...List.generate(_reasons.length, (i) => Padding( - padding: const EdgeInsets.symmetric(vertical: 6.0), - child: Row( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + insetPadding: EdgeInsets.zero, + child: Stack( + children: [ + Container( + width: 360, + height: 600, + padding: const EdgeInsets.all(20), + child: Padding( + padding: const EdgeInsets.only(top: 50.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - _reasons[i], - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + Row( + children: [ + const SizedBox(width: 12), + Expanded( + child: Text( + 'Подать жалобу на пользователя\n${widget.playerName}', + style: const TextStyle(fontSize: 17), + textAlign: TextAlign.center, + ), + ), + ], ), - const SizedBox(width: 12), - Radio( - value: i, - groupValue: _selectedReason, - onChanged: (val) => setState(() => _selectedReason = val!), - activeColor: Colors.black, + const SizedBox(height: 18), + ...List.generate(_reasons.length, (i) => Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: Row( + children: [ + Text( + _reasons[i], + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + ), + const SizedBox(width: 12), + Radio( + value: i, + groupValue: _selectedReason, + onChanged: (val) => setState(() => _selectedReason = val!), + activeColor: Colors.black, + ), + ], + ), + )), + const SizedBox(height: 12), + TextField( + controller: _controller, + maxLines: 5, + decoration: InputDecoration( + hintText: 'Комментарий', + filled: true, + fillColor: const Color(0xFFFBF6EF), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.all(16), + ), + ), + const SizedBox(height: 18), + SizedBox( + width: double.infinity, + height: 54, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + textStyle: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + ), + onPressed: () { + Navigator.pop(context); + showTopReportSuccess(context); + }, + child: const Text('Отправить жалобу'), + ), ), ], ), - )), - const SizedBox(height: 12), - TextField( - controller: _controller, - maxLines: 5, - decoration: InputDecoration( - hintText: 'Комментарий', - filled: true, - fillColor: const Color(0xFFFBF6EF), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.all(16), - ), ), - const SizedBox(height: 18), - SizedBox( - width: double.infinity, - height: 54, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + ), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.black, width: 3), + ), + child: const Center( + child: Icon( + Icons.close, + color: Colors.black, + size: 28, ), - textStyle: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), ), - onPressed: () { - Navigator.pop(context); - showTopReportSuccess(context); - }, - child: const Text('Отправить жалобу'), ), ), - ], - ), + ), + ], ), ); } diff --git a/frontend/lib/views/search_players_screen.dart b/frontend/lib/views/search_players_screen.dart index a0106de..7d54958 100644 --- a/frontend/lib/views/search_players_screen.dart +++ b/frontend/lib/views/search_players_screen.dart @@ -47,7 +47,7 @@ class SearchPlayersModal extends StatelessWidget { child: TextField( controller: _searchController, style: const TextStyle( - fontFamily: 'Roboto', + fontFamily: 'Jost', fontSize: 16, color: Colors.black, ), @@ -55,7 +55,7 @@ class SearchPlayersModal extends StatelessWidget { border: InputBorder.none, hintText: 'Введите ник пользователя', hintStyle: TextStyle( - fontFamily: 'Roboto', + fontFamily: 'Jost', fontSize: 16, color: Colors.black, ), @@ -127,46 +127,46 @@ class PlayerListItem extends StatelessWidget { ); }, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 10.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Аватар пользователя как в profile_screen - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: const Color(0xFFEAD7C3), - border: Border.all(color: Colors.black, width: 2), - ), - child: const Center( - child: Icon(Icons.person_outline, size: 28, color: Colors.black), - ), + padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 10.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Аватар пользователя как в profile_screen + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFEAD7C3), + border: Border.all(color: Colors.black, width: 2), ), - const SizedBox(width: 12.0), - // Имя пользователя - Text( - player.name, - style: const TextStyle( - fontFamily: 'Roboto', - fontSize: 18.0, - fontWeight: FontWeight.w400, - color: Colors.black, - ), + child: const Center( + child: Icon(Icons.person_outline, size: 28, color: Colors.black), ), - const Spacer(), - // Карточки пользователя (макет из инвентаря, но без фото) - Row( - children: List.generate( - player.cards, - (index) => Padding( - padding: const EdgeInsets.only(left: 6.0), - child: CardMockup(), - ), + ), + const SizedBox(width: 12.0), + // Имя пользователя + Text( + player.name, + style: const TextStyle( + fontFamily: 'Jost', + fontSize: 18.0, + fontWeight: FontWeight.w400, + color: Colors.black, + ), + ), + const Spacer(), + // Карточки пользователя (макет из инвентаря, но без фото) + Row( + children: List.generate( + player.cards, + (index) => Padding( + padding: const EdgeInsets.only(left: 6.0), + child: CardMockup(), ), ), - ], + ), + ], ), ), ); @@ -179,51 +179,95 @@ class CardMockup extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - width: 32, - height: 44, - child: Stack( - children: [ - // Внешняя черная рамка - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.black, width: 2), - ), - ), - // Прослойка цвета карточки - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(3), - ), - ), + double width = 32; + double height = 44; + + // Precise calculation of vertical space consumed by non-content elements + double totalNonContentHeight = 3 + 3 + 3 + 2 + 1; // Total vertical space used by borders, padding, and line + double availableContentHeight = height - totalNonContentHeight; + double imageHeight = availableContentHeight * 0.65; + double rarityHeight = availableContentHeight * 0.2; + + return Container( + width: width, + height: height, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.black, width: 1.5), + ), + child: Padding( + padding: const EdgeInsets.all(0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(3), ), - // Внутренняя черная рамка - Padding( - padding: const EdgeInsets.all(4.0), + child: Padding( + padding: const EdgeInsets.all(1.5), child: Container( decoration: BoxDecoration( color: Colors.transparent, borderRadius: BorderRadius.circular(2), - border: Border.all(color: Colors.black, width: 1), + border: Border.all(color: Colors.black, width: 1.5), + ), + child: Padding( + padding: EdgeInsets.zero, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(1.0), + ), + child: Column( + children: [ + // Image placeholder area + Container( + height: imageHeight, + decoration: const BoxDecoration( + color: Color(0xFFEAD7C3), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(1.0), + topRight: Radius.circular(1.0), + ), + ), + child: const Center( + child: Text( + 'Нет \n изоб', + style: TextStyle(color: Colors.black45, fontSize: 7), + textAlign: TextAlign.center, + ), + ), + ), + Container( + height: 1, + color: Colors.black, + ), + // Rarity icons area + Container( + height: rarityHeight, + decoration: const BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(1.0), + bottomRight: Radius.circular(1.0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(4, (i) => Image.asset( + 'assets/icons/редкость.png', + height: rarityHeight > 0 ? rarityHeight * 0.7 : 0, + )), + ), + ), + ], + ), + ), ), ), ), - // Разделительная линия - Positioned( - bottom: 12, - left: 3, - right: 3, - child: Container( - height: 2, - color: Colors.black, - ), - ), - ], + ), ), ); } diff --git a/frontend/lib/views/shop_coin_details_screen.dart b/frontend/lib/views/shop_coin_details_screen.dart index e11bcf5..098cd29 100644 --- a/frontend/lib/views/shop_coin_details_screen.dart +++ b/frontend/lib/views/shop_coin_details_screen.dart @@ -40,9 +40,9 @@ class _ShopCoinDetailsScreenState extends State with Sing @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, leading: Padding( padding: const EdgeInsets.only(left: 16.0), @@ -111,15 +111,10 @@ class _ShopCoinDetailsScreenState extends State with Sing ), child: TabBar( controller: _tabController, + dividerColor: Colors.transparent, indicator: const BoxDecoration( color: Color(0xFFD6A067), - borderRadius: BorderRadius.all(Radius.circular(12.0)), - border: Border( - bottom: BorderSide( - color: Colors.black, - width: 3.0, - ), - ), + borderRadius: BorderRadius.all(Radius.circular(10.0)), ), labelColor: Colors.black, unselectedLabelColor: Colors.black, @@ -147,7 +142,7 @@ class _ShopCoinDetailsScreenState extends State with Sing height: 492.0, child: Container( decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(8.0), border: Border.all(color: Colors.black, width: 3), ), @@ -178,70 +173,55 @@ class _ShopCoinDetailsScreenState extends State with Sing ), textAlign: TextAlign.center, ), - const SizedBox(height: 10.0), - // Название и цена - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.coinName, - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), - ), - const Text( - 'Цена', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - const SizedBox(height: 20.0), + const SizedBox(height: 60.0), // Кнопка покупки SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - Future.delayed(const Duration(seconds: 3), () { - Navigator.of(context).pop(); - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - ); - }); - return Dialog( - backgroundColor: Colors.transparent, - elevation: 0, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 50), - Container( - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), + final overlay = Overlay.of(context); + final overlayEntry = OverlayEntry( + builder: (context) => Positioned( + top: 40, + left: 16, + right: 16, + child: Material( + color: Colors.transparent, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.18), + blurRadius: 8, + offset: const Offset(0, 6), ), - child: const Text( - 'Набор успешно приобретен', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), + ], + ), + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 5), + child: const Center( + child: Text( + 'Монеты успешно приобретены', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.black, ), ), - ], + ), ), - ); - }, + ), + ), + ); + overlay.insert(overlayEntry); + Future.delayed(const Duration(seconds: 3), () { + overlayEntry.remove(); + }); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const ShopScreen()), ); }, style: ElevatedButton.styleFrom( diff --git a/frontend/lib/views/shop_screen.dart b/frontend/lib/views/shop_screen.dart index 1f428f3..ee500be 100644 --- a/frontend/lib/views/shop_screen.dart +++ b/frontend/lib/views/shop_screen.dart @@ -46,9 +46,9 @@ class _ShopScreenState extends State with SingleTickerProviderStateM @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, leading: Padding( padding: const EdgeInsets.only(left: 16.0), diff --git a/frontend/lib/views/shop_set_content_screen.dart b/frontend/lib/views/shop_set_content_screen.dart index f83c800..8ade5d3 100644 --- a/frontend/lib/views/shop_set_content_screen.dart +++ b/frontend/lib/views/shop_set_content_screen.dart @@ -5,6 +5,7 @@ import 'shop_screen.dart'; import 'exchanges_screen.dart'; import 'inventory_screen.dart'; import 'package:flutter/rendering.dart'; +import 'card_detail_screen.dart'; class ShopSetContentScreen extends StatefulWidget { final String setName; @@ -47,6 +48,115 @@ class _ShopSetContentScreenState extends State { } } + Widget _buildCardItem({int index = 0}) { + return InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CardDetailScreen( + cardIndex: index, + showExchangeButton: false, + isFromShop: true, + ), + ), + ); + }, + child: AspectRatio( + aspectRatio: 3/4, + child: Stack( + children: [ + // Внешняя тонкая черная рамка + Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + ), + // Прослойка цвета карточки + Padding( + padding: const EdgeInsets.all(2.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(7), + ), + ), + ), + // Внутренняя тонкая черная рамка + Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.black, width: 2), + ), + ), + ), + // Основная карточка + Padding( + padding: const EdgeInsets.all(6.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(5.0), + ), + child: Column( + children: [ + Expanded( + flex: 8, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(5.0), + topRight: Radius.circular(5.0), + ), + child: Container( + color: const Color(0xFFEAD7C3), + child: const Center( + child: Text( + 'Нет изображения', + style: TextStyle(color: Colors.black45, fontSize: 12), + ), + ), + ), + ), + ), + Container( + height: 3, + color: Colors.black, + ), + Container( + height: 32, + decoration: const BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(5.0), + bottomRight: Radius.circular(5.0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: List.generate(4, (i) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0), + child: Image.asset( + 'assets/icons/редкость.png', + height: 14, + ), + )), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -84,55 +194,13 @@ class _ShopSetContentScreenState extends State { child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, - childAspectRatio: 80 / 120, - crossAxisSpacing: 10.0, - mainAxisSpacing: 10.0, + childAspectRatio: 0.7, + crossAxisSpacing: 8.0, + mainAxisSpacing: 12.0, ), itemCount: 12, itemBuilder: (context, index) { - return Container( - width: 80.0, - height: 120.0, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(6.0), - border: Border.all(color: Colors.black, width: 2), - ), - child: Stack( - children: [ - // Основная рамка карты - Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 2), - borderRadius: BorderRadius.circular(6.0), - ), - ), - // Внутренняя рамка карты - Positioned( - top: 5, - left: 5, - right: 5, - bottom: 5, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 2), - borderRadius: BorderRadius.circular(4.0), - ), - ), - ), - // Линия разделения карты - Positioned( - left: 5, - right: 5, - bottom: 40, - child: Container( - height: 1, - color: Colors.black, - ), - ), - ], - ), - ); + return _buildCardItem(index: index); }, ), ), diff --git a/frontend/lib/views/shop_set_details_screen.dart b/frontend/lib/views/shop_set_details_screen.dart index 761d3d7..da539d1 100644 --- a/frontend/lib/views/shop_set_details_screen.dart +++ b/frontend/lib/views/shop_set_details_screen.dart @@ -38,9 +38,9 @@ class _ShopSetDetailsScreenState extends State with Single @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон + backgroundColor: const Color(0xFFFBF6EF), // Бежевый фон appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, leading: Padding( padding: const EdgeInsets.only(left: 16.0), @@ -109,15 +109,10 @@ class _ShopSetDetailsScreenState extends State with Single ), child: TabBar( controller: _tabController, + dividerColor: Colors.transparent, indicator: const BoxDecoration( color: Color(0xFFD6A067), - borderRadius: BorderRadius.all(Radius.circular(12.0)), - border: Border( - bottom: BorderSide( - color: Colors.black, - width: 3.0, - ), - ), + borderRadius: BorderRadius.all(Radius.circular(10.0)), ), labelColor: Colors.black, unselectedLabelColor: Colors.black, @@ -146,7 +141,7 @@ class _ShopSetDetailsScreenState extends State with Single height: 492.0, child: Container( decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(8.0), border: Border.all(color: Colors.black, width: 3), ), @@ -188,7 +183,7 @@ class _ShopSetDetailsScreenState extends State with Single ], ), - const SizedBox(height: 20.0), + const SizedBox(height: 70.0), // Кнопка просмотра содержимого SizedBox( @@ -211,7 +206,7 @@ class _ShopSetDetailsScreenState extends State with Single ), ), child: const Text( - 'Посмотреть Содержимое', + 'Посмотреть содержимое', style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, @@ -337,22 +332,69 @@ class _ShopSetDetailsScreenState extends State with Single type: BottomNavigationBarType.fixed, selectedItemColor: Colors.black, unselectedItemColor: Colors.black54, + showSelectedLabels: true, + showUnselectedLabels: true, + selectedIconTheme: const IconThemeData( + size: 28, + ), + unselectedIconTheme: const IconThemeData( + size: 24, + ), + selectedLabelStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + unselectedLabelStyle: const TextStyle( + fontSize: 11, + ), items: [ BottomNavigationBarItem( icon: Image.asset('assets/icons/главная.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/главная.png', height: 24), + ), label: 'Главная', ), BottomNavigationBarItem( icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/Инвентарь.png', height: 24), + ), + label: 'Инвентарь', ), BottomNavigationBarItem( icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/магазин.png', height: 24), + ), + label: 'Магазин', ), BottomNavigationBarItem( icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/обменник.png', height: 24), + ), + label: 'Обменник', ), ], ), From 600f86dc466039fb9ea711929c5ea99050b5e6b1 Mon Sep 17 00:00:00 2001 From: birbik Date: Wed, 21 May 2025 23:03:48 +0300 Subject: [PATCH 25/54] FCCX-126 add exchange proposal screen --- .../lib/views/exchange_proposal_screen.dart | 283 ++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 frontend/lib/views/exchange_proposal_screen.dart diff --git a/frontend/lib/views/exchange_proposal_screen.dart b/frontend/lib/views/exchange_proposal_screen.dart new file mode 100644 index 0000000..30a52ab --- /dev/null +++ b/frontend/lib/views/exchange_proposal_screen.dart @@ -0,0 +1,283 @@ +import 'package:flutter/material.dart'; +import 'package:cardly/views/create_exchange_screen.dart'; // Import to reuse _buildExchangeCardVisual +import 'package:cardly/views/exchanges_screen.dart'; // Import ExchangeItem + +class ExchangeProposalScreen extends StatelessWidget { + final ExchangeItem exchangeItem; + + const ExchangeProposalScreen({super.key, required this.exchangeItem}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFFFF4E3), // Match background color + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: Container( + margin: const EdgeInsets.all(8.0), + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Color(0xFFD6A067), + ), + child: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () => Navigator.pop(context), // Back button + ), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Title: Вы получите: + const Text( + 'Вы получите:', + style: TextStyle( + fontSize: 20.0, + fontFamily: 'Jost', + color: Colors.black, + ), + ), + const SizedBox(height: 10.0), + // Other user's offered card + // TODO: Display actual card based on passed data + _buildExchangeCardVisual( // Reuse card visual + width: 80, // Approximate size from image + height: 120, // Approximate size from image + content: Center(child: Text('Карта \n${exchangeItem.otherUserOfferedCardIds.isNotEmpty ? exchangeItem.otherUserOfferedCardIds[0] : 'N/A'}', textAlign: TextAlign.center, style: const TextStyle(color: Colors.black, fontSize: 10, fontFamily: 'Jost'),)), + ), + const SizedBox(height: 20.0), + // Title: Пользователь хочет получить: + const Text( + 'Пользователь хочет получить:', + style: TextStyle( + fontSize: 20.0, + fontFamily: 'Jost', + color: Colors.black, + ), + ), + const SizedBox(height: 10.0), + // Your available cards section + SizedBox( // Fixed height container for the card section + height: 300.0, // Set a fixed height to control button position + child: Center( // Keep the Center widget to center the Wrap block horizontally within the SizedBox + child: Wrap( + alignment: WrapAlignment.center, // Center items in the wrap layout + spacing: 10.0, // Horizontal space between cards + runSpacing: 10.0, // Vertical space between rows of cards + children: exchangeItem.myOfferedCardIds.map((cardId) { + // TODO: Display your actual cards from inventory + return _buildExchangeCardVisual( + width: 80, // Match size with the one above + height: 120, // Match size + content: Center(child: Text('Ваша\nКарта $cardId', textAlign: TextAlign.center, style: const TextStyle(color: Colors.black, fontSize: 10, fontFamily: 'Jost'),)), + ); + }).toList(), + ), + ), + ), + const SizedBox(height: 130.0), // Increased space before buttons + // Buttons + ElevatedButton( + onPressed: () { + // TODO: Implement logic for 'Обменяться картой из списка' + Navigator.pop(context); // Close ExchangeProposalScreen + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => const ExchangesScreen( + notification: 'Обмен успешно совершен', // Notification message + ), + ), + (route) => false, + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + child: Text( + exchangeItem.myOfferedCardIds.length == 1 ? 'Обменяться картой из списка' : 'Обменяться картами из списка', + style: const TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.bold, + fontFamily: 'Jost', + ), + ), + ), + const SizedBox(height: 10.0), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CreateExchangeScreen( + initialExchangeItem: exchangeItem, + ), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + child: Text( + exchangeItem.myOfferedCardIds.length == 1 ? 'Предложить другую карту' : 'Предложить другие карты', + style: const TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.bold, + fontFamily: 'Jost', + ), + ), + ), + ], + ), + ), + ); + } +} + +// Reuse the _buildExchangeCardVisual from create_exchange_screen.dart +Widget _buildExchangeCardVisual({Widget? content, double width = 80, double height = 120, VoidCallback? onRemove}) { + // Calculate internal heights based on the provided height + double totalNonContentHeight = 3 + 3 + 3 + 2 + 1; // Borders, padding, and line from inventory card + double availableContentHeight = height - totalNonContentHeight; + double imageHeight = availableContentHeight * 0.65; // Approximate proportion for image area + double rarityHeight = availableContentHeight * 0.2; // Approximate proportion for rarity area + + // Ensure calculated heights are non-negative + imageHeight = imageHeight > 0 ? imageHeight : 0; + rarityHeight = rarityHeight > 0 ? rarityHeight : 0; + + return Container( + width: width, + height: height, + child: Stack( + clipBehavior: Clip.none, + children: [ + // Внешняя тонкая черная рамка + Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + ), + // Прослойка цвета карточки + Padding( + padding: const EdgeInsets.all(2.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), // Use the same color as inventory card + borderRadius: BorderRadius.circular(7), + ), + ), + ), + // Внутренняя тонкая черная рамка + Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.black, width: 2), + ), + ), + ), + // Основная карточка + Padding( + padding: const EdgeInsets.all(6.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), // Use the same color as inventory card + borderRadius: BorderRadius.circular(5.0), + ), + child: Column( + children: [ + Expanded( + flex: 8, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(5.0), + topRight: Radius.circular(5.0), + ), + child: Container( + color: const Color(0xFFEAD7C3), // Background color for image area + height: imageHeight, + child: content ?? const Center( // Use provided content or default placeholder + child: Text( + 'Нет изображения', + style: TextStyle(color: Colors.black45, fontSize: 10), // Adjusted font size + textAlign: TextAlign.center, + ), + ), + ), + ), + ), + Container( + height: 3, + color: Colors.black, // Separator color + ), + Container( + height: rarityHeight, + decoration: const BoxDecoration( + color: Color(0xFFD6A067), // Rarity section color + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(5.0), + bottomRight: Radius.circular(5.0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: List.generate(4, (i) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0), + child: Image.asset( + 'assets/icons/редкость.png', // Rarity icon + height: rarityHeight > 0 ? rarityHeight * 0.5 : 0, // Adjust icon size proportionally + ), + )), + ), + ), + ], + ), + ), + ), + // Positioned close button - now a direct child of the outer Stack + if (onRemove != null) // Only show if onRemove callback is provided + Positioned( + top: 0, // Adjust position as needed + right: -3, // Adjust position as needed + child: GestureDetector( + onTap: onRemove, + child: Container( + width: 16, // Adjusted size + height: 16, // Adjusted size + decoration: BoxDecoration( + color: const Color(0xFFFFF4E3), + borderRadius: BorderRadius.circular(3), // Adjusted radius + border: Border.all(color: Colors.black, width: 1), // Match style + ), + child: const Center( + child: Icon( + Icons.close, + size: 12, // Adjusted icon size + color: Colors.black, // Match color + ), + ), + ), + ), + ), + ], + ), + ); + } \ No newline at end of file From ba81b3f3b72f43cb50c636c45b9596710501cb98 Mon Sep 17 00:00:00 2001 From: birbik Date: Wed, 21 May 2025 23:05:45 +0300 Subject: [PATCH 26/54] FCCX-127 add authorization dialog --- frontend/lib/views/authorization_dialog.dart | 89 ++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 frontend/lib/views/authorization_dialog.dart diff --git a/frontend/lib/views/authorization_dialog.dart b/frontend/lib/views/authorization_dialog.dart new file mode 100644 index 0000000..9777a85 --- /dev/null +++ b/frontend/lib/views/authorization_dialog.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; + +class AuthorizationDialog extends StatelessWidget { + const AuthorizationDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: const Color(0xFFFFF4E3), // Match background color + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: Stack( + clipBehavior: Clip.none, // Allows positioned elements outside the stack bounds + children: [ + // Main content container + Container( + padding: const EdgeInsets.only(top: 24.0, bottom: 16.0, left: 16.0, right: 16.0), + child: Column( + mainAxisSize: MainAxisSize.min, // Use minimum space vertically + crossAxisAlignment: CrossAxisAlignment.stretch, // Stretch children horizontally + children: [ + // Title Text + const Text( + 'Для продолжения необходимо\nавторизоваться', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18.0, // Adjusted font size slightly for better fit + fontFamily: 'Jost', // Assuming Jost font based on other screens + color: Colors.black, + ), + ), + const SizedBox(height: 20.0), // Space between text and button + // Button + ElevatedButton( + onPressed: () { + // TODO: Implement navigation to login screen + // Navigator.push(context, MaterialPageRoute(builder: (context) => LoginScreen())); + // Navigator.pop(context); // Close dialog after navigation + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), // Match button color + foregroundColor: Colors.black, + minimumSize: const Size(double.infinity, 50), // Full width button + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), // Rounded corners + ), + ), + child: const Text( + 'Перейти на вход', + style: TextStyle( + fontSize: 18.0, // Match text style + fontWeight: FontWeight.bold, + fontFamily: 'Jost', + ), + ), + ), + ], + ), + ), + // Close button in the top right corner + Positioned( + top: -10, // Adjust position to be slightly outside/on border + right: -10, // Adjust position to be slightly outside/on border + child: GestureDetector( + onTap: () => Navigator.pop(context), // Close dialog + child: Container( + width: 36, // Match size from other dialogs + height: 36, // Match size + decoration: BoxDecoration( + color: Color(0xFFD6A067), // Match color from other dialogs + borderRadius: BorderRadius.circular(8), // Match radius + border: Border.all(color: Colors.black, width: 2), // Match border + ), + child: const Center( + child: Icon( + Icons.close, + color: Colors.black, // Match color + size: 20, // Match size + ), + ), + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file From db7f2ef39913c3ea6eec673b6eec9001c89cd01c Mon Sep 17 00:00:00 2001 From: birbik Date: Thu, 22 May 2025 16:07:11 +0300 Subject: [PATCH 27/54] FCCX-128 add notifications screen --- frontend/lib/views/notifications_modal.dart | 114 ++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 frontend/lib/views/notifications_modal.dart diff --git a/frontend/lib/views/notifications_modal.dart b/frontend/lib/views/notifications_modal.dart new file mode 100644 index 0000000..2599e89 --- /dev/null +++ b/frontend/lib/views/notifications_modal.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; + +class NotificationsModal extends StatelessWidget { + const NotificationsModal({super.key}); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: const Color(0xFFFFF4E3), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height * 0.8, + padding: const EdgeInsets.only(top: 24.0, bottom: 16.0, left: 16.0, right: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Уведомления', + style: TextStyle( + fontSize: 20.0, + fontFamily: 'Jost', + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16.0), + Expanded( + child: ListView.builder( + itemCount: 10, // Dummy count for demonstration + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.only(bottom: 12.0), + padding: const EdgeInsets.all(12.0), + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(8.0), + border: Border.all(color: Colors.black, width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + '${index + 1} мин назад', + style: const TextStyle( + color: Colors.black54, + fontSize: 12.0, + ), + ), + ], + ), + const SizedBox(height: 8), + const Text( + 'Заголовок уведомления', + style: TextStyle( + color: Colors.black, + fontSize: 16.0, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + const Text( + 'Описание уведомления. Здесь может быть текст любой длины, описывающий суть уведомления.', + style: TextStyle( + color: Colors.black87, + fontSize: 14.0, + ), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ), + // Close button in the top right corner + Positioned( + top: -1, + right: -1, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + child: const Center( + child: Icon( + Icons.close, + color: Colors.black, + size: 20, + ), + ), + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file From b8c48af6ee02f3c6709442789e786957c6e44fef Mon Sep 17 00:00:00 2001 From: birbik Date: Thu, 22 May 2025 16:08:06 +0300 Subject: [PATCH 28/54] FCCX-129 add ability to swap cards from inventory --- frontend/lib/views/inventory_screen.dart | 135 +++++++++++++++++------ 1 file changed, 99 insertions(+), 36 deletions(-) diff --git a/frontend/lib/views/inventory_screen.dart b/frontend/lib/views/inventory_screen.dart index 4f8fe93..5ce363c 100644 --- a/frontend/lib/views/inventory_screen.dart +++ b/frontend/lib/views/inventory_screen.dart @@ -10,12 +10,14 @@ class InventoryScreen extends StatefulWidget { final bool isOtherUser; final String? playerName; final String? playerId; + final String? collectionName; const InventoryScreen({ super.key, this.isOtherUser = false, this.playerName, this.playerId, + this.collectionName, }); @override @@ -30,9 +32,9 @@ class _InventoryScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, automaticallyImplyLeading: false, titleSpacing: 0, @@ -68,13 +70,67 @@ class _InventoryScreenState extends State { color: Colors.black, fontWeight: FontWeight.bold, fontSize: 18.0, + fontFamily: 'Jost', ), ), ], ) - : const SizedBox.shrink(), + : widget.collectionName != null + ? Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Container( + width: 40.0, + height: 40.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFD6A067), + ), + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + Navigator.pop(context); + }, + child: const Icon( + Icons.arrow_back, + color: Colors.black, + size: 29.0, + ), + ), + ), + ), + const SizedBox(width: 8), + Text( + 'Коллекция ${widget.collectionName}', + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 18.0, + fontFamily: 'Jost', + ), + ), + ], + ) + : Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Container( + width: 40.0, + height: 40.0, + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ProfileScreen()), + ); + }, + child: Image.asset('assets/icons/профиль.png', height: 22), + ), + ), + ), centerTitle: false, - actions: widget.isOtherUser ? null : [ + actions: widget.isOtherUser ? null : widget.collectionName != null ? null : [ Container( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), decoration: BoxDecoration( @@ -90,6 +146,7 @@ class _InventoryScreenState extends State { color: Colors.black, fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), SizedBox(width: 6.0), @@ -115,40 +172,42 @@ class _InventoryScreenState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: Row( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - child: InkWell( - onTap: () { - setState(() { - _showSortOptions = !_showSortOptions; - }); - }, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Сортировка:', - style: const TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, + if (widget.collectionName == null) // Only show sorting if not viewing a collection + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: InkWell( + onTap: () { + setState(() { + _showSortOptions = !_showSortOptions; + }); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Сортировка:', + style: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.bold, + fontFamily: 'Jost', + ), ), - ), - const SizedBox(width: 6.0), - Icon( - _showSortOptions ? Icons.arrow_drop_up : Icons.arrow_drop_down, - color: Colors.black, - ), - ], + const SizedBox(width: 6.0), + Icon( + _showSortOptions ? Icons.arrow_drop_up : Icons.arrow_drop_down, + color: Colors.black, + ), + ], + ), ), ), - ), - ], + ], + ), ), - ), Expanded( child: GridView.builder( @@ -202,6 +261,7 @@ class _InventoryScreenState extends State { style: TextStyle( fontSize: 14.0, fontWeight: _sortOption == 'По редкости' ? FontWeight.bold : FontWeight.normal, + fontFamily: 'Jost', ), ), ), @@ -221,6 +281,7 @@ class _InventoryScreenState extends State { style: TextStyle( fontSize: 14.0, fontWeight: _sortOption == 'По коллекциям' ? FontWeight.bold : FontWeight.normal, + fontFamily: 'Jost', ), ), ), @@ -281,10 +342,12 @@ class _InventoryScreenState extends State { ), selectedLabelStyle: const TextStyle( fontSize: 12, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), unselectedLabelStyle: const TextStyle( fontSize: 11, + fontFamily: 'Jost', ), items: [ BottomNavigationBarItem( @@ -409,7 +472,7 @@ class _InventoryScreenState extends State { child: const Center( child: Text( 'Нет изображения', - style: TextStyle(color: Colors.black45, fontSize: 12), + style: TextStyle(color: Colors.black45, fontSize: 12, fontFamily: 'Jost'), ), ), ), From 40545a7ed12d68b56f6bef333bd27bec3434ebb3 Mon Sep 17 00:00:00 2001 From: birbik Date: Thu, 22 May 2025 16:10:21 +0300 Subject: [PATCH 29/54] FCCX-130 update fonts and colors --- frontend/lib/main.dart | 3 +- frontend/lib/views/achievements_screen.dart | 3 + frontend/lib/views/auth_screen.dart | 53 +-- frontend/lib/views/authorization_dialog.dart | 2 +- frontend/lib/views/card_detail_screen.dart | 32 +- .../lib/views/change_password_screen.dart | 7 + .../lib/views/create_exchange_screen.dart | 16 +- .../lib/views/email_verification_screen.dart | 8 + .../lib/views/exchange_details_screen.dart | 104 +++--- .../lib/views/exchange_proposal_screen.dart | 6 +- frontend/lib/views/exchanges_screen.dart | 337 +++++++++--------- .../lib/views/forgot_password_screen.dart | 5 + .../forgot_password_verification_screen.dart | 8 + frontend/lib/views/login_screen.dart | 11 + frontend/lib/views/news_detail_screen.dart | 116 +++--- frontend/lib/views/news_screen.dart | 25 +- frontend/lib/views/pack_content_screen.dart | 8 +- frontend/lib/views/pack_open_screen.dart | 69 ---- frontend/lib/views/profile_image_dialog.dart | 1 + frontend/lib/views/profile_screen.dart | 12 +- frontend/lib/views/search_players_screen.dart | 2 +- frontend/lib/views/settings_screen.dart | 13 +- .../lib/views/shop_coin_details_screen.dart | 61 +++- frontend/lib/views/shop_screen.dart | 9 +- .../lib/views/shop_set_content_screen.dart | 8 +- .../lib/views/shop_set_details_screen.dart | 11 +- 26 files changed, 517 insertions(+), 413 deletions(-) delete mode 100644 frontend/lib/views/pack_open_screen.dart diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 80b9e26..eb339f4 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -6,6 +6,7 @@ import 'views/auth_screen.dart'; import 'views/email_verification_screen.dart'; import 'views/forgot_password_screen.dart'; import 'views/change_password_screen.dart'; +import 'views/home_screen.dart'; void main() { runApp(const MyApp()); @@ -79,7 +80,7 @@ class MyApp extends StatelessWidget { elevation: 0, ), ), - home: const SplashScreen(), + home: const HomeScreen(), routes: { '/auth': (context) => const AuthScreen(), '/login': (context) => const AuthScreen(initialTabIndex: 0), diff --git a/frontend/lib/views/achievements_screen.dart b/frontend/lib/views/achievements_screen.dart index a215a81..fe483f4 100644 --- a/frontend/lib/views/achievements_screen.dart +++ b/frontend/lib/views/achievements_screen.dart @@ -54,6 +54,7 @@ class AchievementsScreen extends StatelessWidget { style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), ], @@ -99,6 +100,7 @@ class AchievementsScreen extends StatelessWidget { style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), const SizedBox(height: 4), @@ -107,6 +109,7 @@ class AchievementsScreen extends StatelessWidget { style: TextStyle( fontSize: 14, color: Colors.grey[600], + fontFamily: 'Jost', ), ), ], diff --git a/frontend/lib/views/auth_screen.dart b/frontend/lib/views/auth_screen.dart index 21c68ac..154bd09 100644 --- a/frontend/lib/views/auth_screen.dart +++ b/frontend/lib/views/auth_screen.dart @@ -108,6 +108,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM final isLoading = authController.isLoading; return Scaffold( + backgroundColor: const Color(0xFFFBF6EF), body: Column( children: [ // Логотип и название @@ -115,7 +116,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM Image.asset( 'assets/icons/карты.png', height: 80, - color: const Color(0xFFD9A76A), + color: const Color(0xFFDBAA76), ), const SizedBox(height: 10), const SizedBox(height: 10), @@ -123,8 +124,9 @@ class _AuthScreenState extends State with SingleTickerProviderStateM 'Cardly', style: TextStyle( fontSize: 32, - color: Color(0xFFD9A76A), + color: Color(0xFFDBAA76), fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), const SizedBox(height: 30), @@ -133,11 +135,12 @@ class _AuthScreenState extends State with SingleTickerProviderStateM Container( margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(8), ), child: TabBar( controller: _tabController, + dividerColor: Colors.transparent, indicatorSize: TabBarIndicatorSize.tab, indicator: BoxDecoration( color: const Color(0xFFD6A067), @@ -152,6 +155,12 @@ class _AuthScreenState extends State with SingleTickerProviderStateM Tab(text: 'Войти'), Tab(text: 'Создать'), ], + labelStyle: const TextStyle( + fontFamily: 'Jost', + ), + unselectedLabelStyle: const TextStyle( + fontFamily: 'Jost', + ), ), ), @@ -173,6 +182,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -180,7 +190,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM controller: _loginEmailController, decoration: const InputDecoration( filled: true, - fillColor: Color(0xFFEDD6B0), + fillColor: Color(0xFFEAD7C3), border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(8)), @@ -206,6 +216,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -213,7 +224,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM controller: _loginPasswordController, decoration: InputDecoration( filled: true, - fillColor: const Color(0xFFEDD6B0), + fillColor: const Color(0xFFEAD7C3), border: const OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(8)), @@ -261,6 +272,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 14, color: Colors.black, + fontFamily: 'Jost', ), ), ), @@ -273,13 +285,13 @@ class _AuthScreenState extends State with SingleTickerProviderStateM child: ElevatedButton( onPressed: isLoading ? null : _login, style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), + backgroundColor: const Color(0xFFDBAA76), foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), - disabledBackgroundColor: const Color(0xFFD6A067).withOpacity(0.7), + disabledBackgroundColor: const Color(0xFFDBAA76).withOpacity(0.7), ), child: isLoading ? const SizedBox( @@ -295,6 +307,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), @@ -316,6 +329,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -323,7 +337,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM controller: _nicknameController, decoration: const InputDecoration( filled: true, - fillColor: Color(0xFFEDD6B0), + fillColor: Color(0xFFEAD7C3), border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(8)), @@ -345,6 +359,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -352,7 +367,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM controller: _registerEmailController, decoration: const InputDecoration( filled: true, - fillColor: Color(0xFFEDD6B0), + fillColor: Color(0xFFEAD7C3), border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(8)), @@ -378,6 +393,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -385,7 +401,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM controller: _registerPasswordController, decoration: InputDecoration( filled: true, - fillColor: const Color(0xFFEDD6B0), + fillColor: const Color(0xFFEAD7C3), border: const OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(8)), @@ -422,6 +438,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -429,7 +446,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM controller: _confirmPasswordController, decoration: InputDecoration( filled: true, - fillColor: const Color(0xFFEDD6B0), + fillColor: const Color(0xFFEAD7C3), border: const OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(8)), @@ -466,13 +483,13 @@ class _AuthScreenState extends State with SingleTickerProviderStateM child: ElevatedButton( onPressed: isLoading ? null : _register, style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), + backgroundColor: const Color(0xFFDBAA76), foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), - disabledBackgroundColor: const Color(0xFFD6A067).withOpacity(0.7), + disabledBackgroundColor: const Color(0xFFDBAA76).withOpacity(0.7), ), child: isLoading ? const SizedBox( @@ -488,6 +505,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), @@ -500,15 +518,6 @@ class _AuthScreenState extends State with SingleTickerProviderStateM ), ), - Container( - width: 100, - height: 4, - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(2), - ), - ), ], ), ); diff --git a/frontend/lib/views/authorization_dialog.dart b/frontend/lib/views/authorization_dialog.dart index 9777a85..7aaf5bc 100644 --- a/frontend/lib/views/authorization_dialog.dart +++ b/frontend/lib/views/authorization_dialog.dart @@ -51,7 +51,7 @@ class AuthorizationDialog extends StatelessWidget { style: TextStyle( fontSize: 18.0, // Match text style fontWeight: FontWeight.bold, - fontFamily: 'Jost', + fontFamily: 'Roboto', ), ), ), diff --git a/frontend/lib/views/card_detail_screen.dart b/frontend/lib/views/card_detail_screen.dart index 5d4f178..c53c072 100644 --- a/frontend/lib/views/card_detail_screen.dart +++ b/frontend/lib/views/card_detail_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'create_exchange_screen.dart'; class CardDetailScreen extends StatefulWidget { final int cardIndex; @@ -38,7 +39,7 @@ class _CardDetailScreenState extends State { final int rarity = int.tryParse(data['rarity'] ?? '0') ?? 0; return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), body: SafeArea( child: Stack( children: [ @@ -98,6 +99,7 @@ class _CardDetailScreenState extends State { fontSize: 22, fontWeight: FontWeight.w500, color: Colors.black, + fontFamily: 'Jost', ), ), ), @@ -114,7 +116,7 @@ class _CardDetailScreenState extends State { child: const Center( child: Text( 'Нет изображения', - style: TextStyle(color: Colors.black45, fontSize: 16), + style: TextStyle(color: Colors.black45, fontSize: 16, fontFamily: 'Jost'), ), ), ), @@ -125,7 +127,7 @@ class _CardDetailScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0), child: Text( description, - style: const TextStyle(fontSize: 17, color: Colors.black), + style: const TextStyle(fontSize: 17, color: Colors.black, fontFamily: 'Jost'), textAlign: TextAlign.left, ), ), @@ -156,6 +158,7 @@ class _CardDetailScreenState extends State { fontSize: 20, fontWeight: FontWeight.w500, color: Colors.black, + fontFamily: 'Jost', ), ), ], @@ -183,7 +186,7 @@ class _CardDetailScreenState extends State { onPressed: () {}, child: const Text( 'Предложить обмен', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400, fontFamily: 'Roboto'), ), ) : widget.isFromShop @@ -219,7 +222,7 @@ class _CardDetailScreenState extends State { children: [ const Text( 'Разобрать', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400, fontFamily: 'Roboto'), ), SizedBox(width: 6), ], @@ -230,7 +233,11 @@ class _CardDetailScreenState extends State { children: [ const Text( '150', - style: TextStyle(fontSize: 15, fontWeight: FontWeight.w400), + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w400, + fontFamily: 'Roboto', + ), textAlign: TextAlign.center, ), SizedBox(width: 4), @@ -255,10 +262,19 @@ class _CardDetailScreenState extends State { ), padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 25), ), - onPressed: () {}, + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CreateExchangeScreen( + cardId: widget.cardIndex, + ), + ), + ); + }, child: const Text( 'Обменять', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400, fontFamily: 'Roboto'), ), ), ), diff --git a/frontend/lib/views/change_password_screen.dart b/frontend/lib/views/change_password_screen.dart index e3ac38d..0c0cb02 100644 --- a/frontend/lib/views/change_password_screen.dart +++ b/frontend/lib/views/change_password_screen.dart @@ -130,6 +130,7 @@ class _ChangePasswordScreenState extends State { fontSize: 32, color: Color(0xFFD9A76A), fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), const SizedBox(height: 30), @@ -167,6 +168,7 @@ class _ChangePasswordScreenState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 20), @@ -176,6 +178,7 @@ class _ChangePasswordScreenState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -220,6 +223,7 @@ class _ChangePasswordScreenState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -286,6 +290,7 @@ class _ChangePasswordScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), @@ -314,6 +319,7 @@ class _ChangePasswordScreenState extends State { style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), textAlign: TextAlign.center, ), @@ -337,6 +343,7 @@ class _ChangePasswordScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), diff --git a/frontend/lib/views/create_exchange_screen.dart b/frontend/lib/views/create_exchange_screen.dart index 080a060..e3ef492 100644 --- a/frontend/lib/views/create_exchange_screen.dart +++ b/frontend/lib/views/create_exchange_screen.dart @@ -7,8 +7,13 @@ import 'profile_screen.dart'; class CreateExchangeScreen extends StatefulWidget { final ExchangeItem? initialExchangeItem; + final int? cardId; - const CreateExchangeScreen({super.key, this.initialExchangeItem}); + const CreateExchangeScreen({ + super.key, + this.initialExchangeItem, + this.cardId, + }); @override State createState() => _CreateExchangeScreenState(); @@ -27,6 +32,11 @@ class _CreateExchangeScreenState extends State { 'id': widget.initialExchangeItem!.otherUserOfferedCardIds[0], 'name': 'Карта ${widget.initialExchangeItem!.otherUserOfferedCardIds[0]}', }; + } else if (widget.cardId != null) { + selectedTopCard = { + 'id': widget.cardId, + 'name': 'Карта ${widget.cardId}', + }; } } @@ -385,7 +395,7 @@ class _CreateExchangeScreenState extends State { style: const TextStyle( fontSize: 20.0, fontWeight: FontWeight.bold, - fontFamily: 'Jost', + fontFamily: 'Roboto', ), ), ), @@ -581,7 +591,7 @@ class _CreateExchangeScreenState extends State { child: content ?? const Center( // Use provided content or default placeholder child: Text( 'Нет изображения', - style: TextStyle(color: Colors.black45, fontSize: 10), // Adjusted font size + style: TextStyle(color: Colors.black45, fontSize: 10, fontFamily: 'Jost'), // Adjusted font size textAlign: TextAlign.center, ), ), diff --git a/frontend/lib/views/email_verification_screen.dart b/frontend/lib/views/email_verification_screen.dart index dd7ef13..ef813df 100644 --- a/frontend/lib/views/email_verification_screen.dart +++ b/frontend/lib/views/email_verification_screen.dart @@ -129,6 +129,7 @@ class _EmailVerificationScreenState extends State { fontSize: 32, color: Color(0xFFD9A76A), fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), const SizedBox(height: 30), @@ -140,6 +141,7 @@ class _EmailVerificationScreenState extends State { style: TextStyle( fontSize: 24, color: Color(0xFF000000), + fontFamily: 'Jost', ), textAlign: TextAlign.center, ), @@ -160,6 +162,7 @@ class _EmailVerificationScreenState extends State { style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 16), @@ -174,6 +177,8 @@ class _EmailVerificationScreenState extends State { borderRadius: BorderRadius.all(Radius.circular(8)), ), contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + labelStyle: TextStyle(fontFamily: 'Jost'), + hintStyle: TextStyle(fontFamily: 'Jost'), ), keyboardType: TextInputType.number, validator: (value) { @@ -200,6 +205,7 @@ class _EmailVerificationScreenState extends State { style: TextStyle( fontSize: 14, color: _resendTimeLeft > 0 ? Colors.grey : Colors.black, + fontFamily: 'Jost', ), ), ), @@ -234,6 +240,7 @@ class _EmailVerificationScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), @@ -258,6 +265,7 @@ class _EmailVerificationScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), diff --git a/frontend/lib/views/exchange_details_screen.dart b/frontend/lib/views/exchange_details_screen.dart index 86a739c..ab3a6d1 100644 --- a/frontend/lib/views/exchange_details_screen.dart +++ b/frontend/lib/views/exchange_details_screen.dart @@ -73,21 +73,21 @@ class ExchangeDetailsScreen extends StatelessWidget { exchangeItem.myOfferedCardIds.length > 1 ? Stack( clipBehavior: Clip.none, // Allow cards to overlap - children: [ + children: [ // Show up to 3 cards in the stack (visual representation) Positioned( left: 12, // Adjust position for stacking top: 0, // Adjust position for stacking child: _buildCardItem('Card Name', 'Rarity', width: 48, height: 64), // TODO: Use actual card details from cardId ), - Positioned( + Positioned( left: 6, // Adjust position for stacking top: 0, // Adjust position for stacking child: _buildCardItem('Card Name', 'Rarity', width: 48, height: 64), // TODO: Use actual card details from cardId - ), + ), // Always show the front card if count is at least 1 _buildCardItem('Card Name', 'Rarity', width: 48, height: 64), // TODO: Use actual card details from cardId - ], + ], ) : (exchangeItem.myOfferedCardIds.isNotEmpty ? _buildCardItem('Card Name', 'Rarity', width: 48, height: 64) // Single card @@ -136,28 +136,28 @@ class ExchangeDetailsScreen extends StatelessWidget { const SizedBox(height: 12.0), // Conditionally display buttons based on status if (exchangeItem.status == ExchangeStatus.waiting) // Show both buttons if waiting for actions - Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: double.infinity, - height: 60, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - textStyle: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - fontFamily: 'Jost', - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: double.infinity, + height: 60, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + fontFamily: 'Roboto', + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), ), - onPressed: () { + ), + onPressed: () { Navigator.of(context).pop(); Navigator.pushAndRemoveUntil( context, @@ -168,28 +168,28 @@ class ExchangeDetailsScreen extends StatelessWidget { ), (route) => false, ); - }, - child: const Text('Принять'), - ), + }, + child: const Text('Принять'), ), - const SizedBox(height: 12.0), - SizedBox( - width: double.infinity, - height: 60, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - textStyle: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - fontFamily: 'Jost', - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), + ), + const SizedBox(height: 12.0), + SizedBox( + width: double.infinity, + height: 60, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + fontFamily: 'Roboto', ), - onPressed: () { + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + ), + onPressed: () { Navigator.of(context).pop(); Navigator.pushAndRemoveUntil( context, @@ -200,12 +200,12 @@ class ExchangeDetailsScreen extends StatelessWidget { ), (route) => false, ); - }, - child: const Text('Отклонить'), - ), + }, + child: const Text('Отклонить'), ), - ], - ), + ), + ], + ), ) else if (exchangeItem.status == ExchangeStatus.pending) // Show only Cancel button if pending Padding( @@ -220,7 +220,7 @@ class ExchangeDetailsScreen extends StatelessWidget { textStyle: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, - fontFamily: 'Jost', + fontFamily: 'Roboto', ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.0), @@ -241,7 +241,7 @@ class ExchangeDetailsScreen extends StatelessWidget { child: const Text('Отменить'), // Text for cancelling pending exchange ), ), - ), + ), ], ), ), diff --git a/frontend/lib/views/exchange_proposal_screen.dart b/frontend/lib/views/exchange_proposal_screen.dart index 30a52ab..655e9f8 100644 --- a/frontend/lib/views/exchange_proposal_screen.dart +++ b/frontend/lib/views/exchange_proposal_screen.dart @@ -107,7 +107,7 @@ class ExchangeProposalScreen extends StatelessWidget { style: const TextStyle( fontSize: 20.0, fontWeight: FontWeight.bold, - fontFamily: 'Jost', + fontFamily: 'Roboto', ), ), ), @@ -136,7 +136,7 @@ class ExchangeProposalScreen extends StatelessWidget { style: const TextStyle( fontSize: 20.0, fontWeight: FontWeight.bold, - fontFamily: 'Jost', + fontFamily: 'Roboto', ), ), ), @@ -217,7 +217,7 @@ Widget _buildExchangeCardVisual({Widget? content, double width = 80, double heig child: content ?? const Center( // Use provided content or default placeholder child: Text( 'Нет изображения', - style: TextStyle(color: Colors.black45, fontSize: 10), // Adjusted font size + style: TextStyle(color: Colors.black45, fontSize: 10, fontFamily: 'Jost'), // Adjusted font size textAlign: TextAlign.center, ), ), diff --git a/frontend/lib/views/exchanges_screen.dart b/frontend/lib/views/exchanges_screen.dart index e0793e4..2e84a00 100644 --- a/frontend/lib/views/exchanges_screen.dart +++ b/frontend/lib/views/exchanges_screen.dart @@ -47,6 +47,7 @@ class _ExchangesScreenState extends State with SingleTickerProv color: Colors.black, fontSize: 18.0, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), textAlign: TextAlign.center, ), @@ -106,6 +107,8 @@ class _ExchangesScreenState extends State with SingleTickerProv Tab(text: 'Обмен'), Tab(text: 'Мои обмены'), ], + labelStyle: TextStyle(fontFamily: 'Jost'), + unselectedLabelStyle: TextStyle(fontFamily: 'Jost'), ), ), ), @@ -113,95 +116,97 @@ class _ExchangesScreenState extends State with SingleTickerProv body: Stack( children: [ Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const CreateExchangeScreen()), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 12.0), - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const CreateExchangeScreen()), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 12.0), + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + children: [ + Text( + 'Создать обмен', + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.bold, + fontFamily: 'Roboto', + ), ), - child: Row( - children: [ - Text( - 'Создать обмен', - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(width: 6.0), - Container( - width: 20.0, - height: 20.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 1.0), - ), - child: const Icon(Icons.add, size: 18.0), - ), - ], + SizedBox(width: 6.0), + Container( + width: 20.0, + height: 20.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.black, width: 1.0), + ), + child: const Icon(Icons.add, size: 18.0), ), - ), + ], ), - - Container( - child: IconButton( - icon: Image.asset('assets/icons/поиск.png', height: 32), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => const SearchPlayersModal(), - ); - }, - ), - ), - ], + ), ), - ), - - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - InkWell( - onTap: () { - setState(() { - _showSortOptions = !_showSortOptions; - }); - }, - child: Row( - children: [ - const Text( - 'Сортировка', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 4.0), - Icon( - _showSortOptions ? Icons.arrow_drop_up : Icons.arrow_drop_down, - color: Colors.black, - ), - ], + + Container( + child: IconButton( + icon: Image.asset('assets/icons/поиск.png', height: 32), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const SearchPlayersModal(), + ); + }, + ), + ), + ], + ), + ), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () { + setState(() { + _showSortOptions = !_showSortOptions; + }); + }, + child: Row( + children: [ + const Text( + 'Сортировка', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + fontFamily: 'Jost', + ), ), - ), + const SizedBox(width: 4.0), + Icon( + _showSortOptions ? Icons.arrow_drop_up : Icons.arrow_drop_down, + color: Colors.black, + ), + ], + ), + ), ], ), ), @@ -221,14 +226,14 @@ class _ExchangesScreenState extends State with SingleTickerProv ), // Sorting options dropdown (Positioned overlay) - if (_showSortOptions) + if (_showSortOptions) Positioned( top: 110, // Approximate position below the Sorting text left: 16, // Align with the left padding child: Container( width: 120.0, // Set a fixed width to match inventory_screen dropdown size - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( + padding: const EdgeInsets.all(12.0), + decoration: BoxDecoration( color: const Color(0xFFEAD7C3), // Background color from inventory_screen borderRadius: BorderRadius.circular(10.0), // Border radius from inventory_screen boxShadow: [ // Box shadow from inventory_screen @@ -238,50 +243,52 @@ class _ExchangesScreenState extends State with SingleTickerProv offset: const Offset(0, 2), ), ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - InkWell( - onTap: () { - setState(() { + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () { + setState(() { _sortOption = 'По дате'; // Option 1 - _showSortOptions = false; - }); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - 'По дате', - style: TextStyle( - fontSize: 14.0, - fontWeight: _sortOption == 'По дате' ? FontWeight.bold : FontWeight.normal, + _showSortOptions = false; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + 'По дате', + style: TextStyle( + fontSize: 14.0, + fontWeight: _sortOption == 'По дате' ? FontWeight.bold : FontWeight.normal, + fontFamily: 'Jost', + ), + ), ), ), - ), - ), - InkWell( - onTap: () { - setState(() { + InkWell( + onTap: () { + setState(() { _sortOption = 'По редкости'; // Option 2 - _showSortOptions = false; - }); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - 'По редкости', - style: TextStyle( - fontSize: 14.0, - fontWeight: _sortOption == 'По редкости' ? FontWeight.bold : FontWeight.normal, + _showSortOptions = false; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + 'По редкости', + style: TextStyle( + fontSize: 14.0, + fontWeight: _sortOption == 'По редкости' ? FontWeight.bold : FontWeight.normal, + fontFamily: 'Jost', + ), + ), ), ), - ), + ], ), - ], - ), - ), ), + ), ], ), bottomNavigationBar: Container( @@ -483,17 +490,17 @@ class _ExchangesScreenState extends State with SingleTickerProv ); } } else { // If in 'Мои обмены' tab, show ExchangeDetailsScreen dialog - showDialog( - context: context, - barrierDismissible: true, - builder: (BuildContext context) { - return Dialog( - backgroundColor: Colors.transparent, - elevation: 0, + showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + elevation: 0, child: ExchangeDetailsScreen(exchangeItem: exchangeItem), - ); - }, - ); + ); + }, + ); } }, child: Container( @@ -539,10 +546,10 @@ class _ExchangesScreenState extends State with SingleTickerProv ), ), ), - SizedBox( - width: MediaQuery.of(context).size.width * 0.2, - child: AspectRatio( - aspectRatio: 0.7, + SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: AspectRatio( + aspectRatio: 0.7, child: _buildSingleCardVisual(showPlus: showLeftPlus), ), ), @@ -602,47 +609,47 @@ class _ExchangesScreenState extends State with SingleTickerProv ], ), otherUserOfferedCount >= 2 - ? Stack( + ? Stack( clipBehavior: Clip.none, - children: [ - Positioned( - left: 16, - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.2, - child: AspectRatio( - aspectRatio: 0.7, + children: [ + Positioned( + left: 16, + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: AspectRatio( + aspectRatio: 0.7, child: _buildSingleCardVisual(showPlus: false), // Always false for right side ), ), ), if (otherUserOfferedCount >= 2) - Positioned( - left: 8, - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.2, - child: AspectRatio( - aspectRatio: 0.7, - child: _buildSingleCardVisual(showPlus: false), // Always false for right side - ), - ), - ), - SizedBox( + Positioned( + left: 8, + child: SizedBox( width: MediaQuery.of(context).size.width * 0.2, child: AspectRatio( aspectRatio: 0.7, - child: _buildSingleCardVisual(showPlus: false), // Always false for right side + child: _buildSingleCardVisual(showPlus: false), // Always false for right side ), ), - ], - ) + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: false), // Always false for right side + ), + ), + ], + ) : (otherUserOfferedCount == 1 ? AspectRatio( - aspectRatio: 0.7, + aspectRatio: 0.7, child: _buildSingleCardVisual(showPlus: false), // Always false for right side ) : SizedBox.shrink()), ], - ), + ), ), ], ), @@ -706,11 +713,11 @@ class _ExchangesScreenState extends State with SingleTickerProv ), ) : const Center( - child: Text( + child: Text( 'Нет изображения', - style: TextStyle(color: Colors.black45, fontSize: 12), - ), - ), + style: TextStyle(color: Colors.black45, fontSize: 12), + ), + ), ), ), ), diff --git a/frontend/lib/views/forgot_password_screen.dart b/frontend/lib/views/forgot_password_screen.dart index 74946dd..08f9704 100644 --- a/frontend/lib/views/forgot_password_screen.dart +++ b/frontend/lib/views/forgot_password_screen.dart @@ -69,6 +69,7 @@ class _ForgotPasswordScreenState extends State { fontSize: 32, color: Color(0xFFD9A76A), fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), const SizedBox(height: 40), @@ -86,6 +87,7 @@ class _ForgotPasswordScreenState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -99,6 +101,8 @@ class _ForgotPasswordScreenState extends State { borderRadius: BorderRadius.all(Radius.circular(8)), ), contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + labelStyle: TextStyle(fontFamily: 'Jost'), + hintStyle: TextStyle(fontFamily: 'Jost'), ), keyboardType: TextInputType.emailAddress, validator: (value) { @@ -141,6 +145,7 @@ class _ForgotPasswordScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), diff --git a/frontend/lib/views/forgot_password_verification_screen.dart b/frontend/lib/views/forgot_password_verification_screen.dart index c478fcb..03628e7 100644 --- a/frontend/lib/views/forgot_password_verification_screen.dart +++ b/frontend/lib/views/forgot_password_verification_screen.dart @@ -150,6 +150,7 @@ class _ForgotPasswordVerificationScreenState extends State with SingleTickerProviderStat fontSize: 24, color: Color(0xFFD9A76A), fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), const SizedBox(height: 30), @@ -105,6 +106,12 @@ class _LoginScreenState extends State with SingleTickerProviderStat ), labelColor: Colors.black, unselectedLabelColor: Colors.black, + labelStyle: const TextStyle( + fontFamily: 'Jost', + ), + unselectedLabelStyle: const TextStyle( + fontFamily: 'Jost', + ), tabs: const [ Tab(text: 'Войти'), Tab(text: 'Создать'), @@ -130,6 +137,7 @@ class _LoginScreenState extends State with SingleTickerProviderStat style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -163,6 +171,7 @@ class _LoginScreenState extends State with SingleTickerProviderStat style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -221,6 +230,7 @@ class _LoginScreenState extends State with SingleTickerProviderStat style: TextStyle( fontSize: 14, color: Colors.black, + fontFamily: 'Jost', ), ), ), @@ -255,6 +265,7 @@ class _LoginScreenState extends State with SingleTickerProviderStat style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), ), diff --git a/frontend/lib/views/news_detail_screen.dart b/frontend/lib/views/news_detail_screen.dart index 468ee5e..fb53812 100644 --- a/frontend/lib/views/news_detail_screen.dart +++ b/frontend/lib/views/news_detail_screen.dart @@ -15,7 +15,7 @@ class NewsDetailScreen extends StatelessWidget { super.key, required this.news, }); - + @override Widget build(BuildContext context) { return Dialog( @@ -24,81 +24,85 @@ class NewsDetailScreen extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.0), // Match border radius style side: const BorderSide(color: Colors.black, width: 2), // Match border style - ), - child: Stack( - children: [ + ), + child: Stack( + children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 24.0), // Adjust padding as needed child: SingleChildScrollView( // Keep SingleChildScrollView for content that might overflow child: Column( mainAxisSize: MainAxisSize.min, // Use min size for column - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + crossAxisAlignment: CrossAxisAlignment.start, + children: [ // News content - Title, Image, Full Content, Date Center( // Center the title - child: Text( + child: Text( news.title, // Use news object directly in StatelessWidget - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - ), - + style: const TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + fontFamily: 'Jost', + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 24.0), // Keep spacing - - // Картинка - Container( - height: 180, - width: double.infinity, - decoration: BoxDecoration( + + // Картинка + Container( + height: 180, + width: double.infinity, + decoration: BoxDecoration( color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(8.0), + borderRadius: BorderRadius.circular(8.0), border: Border.all(color: Colors.black, width: 1.0), // Added thin black border - ), - alignment: Alignment.center, + ), + alignment: Alignment.center, child: const Center( // Use Center to center the text child: Text( 'Нет изображения', // Use the placeholder text from inventory card - style: TextStyle( + style: TextStyle( color: Colors.black45, // Use the text color from inventory card fontSize: 16.0, // Adjust font size as needed for this larger area // fontWeight: FontWeight.bold, // Remove bold fontWeight + fontFamily: 'Jost', ), textAlign: TextAlign.center, - ), - ), - ), - + ), + ), + ), + const SizedBox(height: 24.0), // Keep spacing - - // Расширенное описание - Text( + + // Расширенное описание + Text( news.fullContent, // Use news object directly - style: const TextStyle( - fontSize: 16.0, - ), - ), - + style: const TextStyle( + fontSize: 16.0, + fontFamily: 'Jost', + ), + ), + const SizedBox(height: 16.0), // Keep spacing - - // Дата - Align( - alignment: Alignment.centerRight, - child: Text( + + // Дата + Align( + alignment: Alignment.centerRight, + child: Text( 'Дата публикации: ${news.date}', // Use news object directly - style: const TextStyle( - fontSize: 14.0, - fontStyle: FontStyle.italic, - color: Colors.black54, - ), + style: const TextStyle( + fontSize: 14.0, + fontStyle: FontStyle.italic, + color: Colors.black54, + fontFamily: 'Jost', + ), + ), + ), + ], ), - ), - ], - ), ), - ), + ), // Close button in the top right corner Positioned( top: 0, // Adjust position as needed @@ -118,12 +122,12 @@ class NewsDetailScreen extends StatelessWidget { Icons.close, color: Colors.black, size: 28, // Match icon size - ), - ), - ), - ), ), - ], + ), + ), + ), + ), + ], ), ); } diff --git a/frontend/lib/views/news_screen.dart b/frontend/lib/views/news_screen.dart index d863bb5..0fd91f5 100644 --- a/frontend/lib/views/news_screen.dart +++ b/frontend/lib/views/news_screen.dart @@ -132,6 +132,7 @@ class _NewsScreenState extends State { fontSize: 24.0, fontWeight: FontWeight.bold, color: Colors.black, + fontFamily: 'Jost', ), ), ), @@ -205,9 +206,11 @@ class _NewsScreenState extends State { selectedLabelStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), unselectedLabelStyle: const TextStyle( fontSize: 11, + fontFamily: 'Jost', ), items: [ BottomNavigationBarItem( @@ -270,7 +273,7 @@ class _NewsScreenState extends State { onTap: () { showDialog( context: context, - builder: (context) => NewsDetailScreen(news: news), + builder: (context) => NewsDetailScreen(news: news), ); }, child: Container( @@ -285,21 +288,23 @@ class _NewsScreenState extends State { children: [ // Заголовок Text( - news.title, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14.0, - ), + news.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.0, + fontFamily: 'Jost', + ), ), const SizedBox(height: 8.0), // Текст Text( - news.content, - style: const TextStyle( - fontSize: 14.0, - ), + news.content, + style: const TextStyle( + fontSize: 14.0, + fontFamily: 'Jost', + ), ), ], ), diff --git a/frontend/lib/views/pack_content_screen.dart b/frontend/lib/views/pack_content_screen.dart index 5963964..d78535b 100644 --- a/frontend/lib/views/pack_content_screen.dart +++ b/frontend/lib/views/pack_content_screen.dart @@ -14,7 +14,7 @@ class PackContentScreen extends StatelessWidget { backgroundColor: const Color(0xFFFBF6EF), elevation: 0, automaticallyImplyLeading: false, - title: const Text('Выпавшие карты:', style: TextStyle(color: Colors.black)), + title: const Text('Выпавшие карты:', style: TextStyle(color: Colors.black, fontFamily: 'Jost')), centerTitle: true, ), body: Column( @@ -87,7 +87,7 @@ class PackContentScreen extends StatelessWidget { child: const Center( child: Text( 'Нет изображения', - style: TextStyle(color: Colors.black45, fontSize: 12), + style: TextStyle(color: Colors.black45, fontSize: 12, fontFamily: 'Jost'), ), ), ), @@ -150,7 +150,7 @@ class PackContentScreen extends StatelessWidget { borderRadius: BorderRadius.circular(10.0), ), ), - child: const Text('Вернуться в магазин', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + child: const Text('Вернуться в магазин', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, fontFamily: 'Roboto')), ), ), const SizedBox(height: 12), @@ -172,7 +172,7 @@ class PackContentScreen extends StatelessWidget { borderRadius: BorderRadius.circular(10.0), ), ), - child: const Text('Перейти в инвентарь', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + child: const Text('Перейти в инвентарь', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, fontFamily: 'Roboto')), ), ), const SizedBox(height: 120), diff --git a/frontend/lib/views/pack_open_screen.dart b/frontend/lib/views/pack_open_screen.dart deleted file mode 100644 index a474e10..0000000 --- a/frontend/lib/views/pack_open_screen.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; -import 'pack_content_screen.dart'; // Экран содержимого пака - -class PackOpenScreen extends StatelessWidget { - final String setName; - const PackOpenScreen({super.key, required this.setName}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), - appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black), - onPressed: () => Navigator.pop(context), - ), - title: const Text('Открытие набора', style: TextStyle(color: Colors.black)), - centerTitle: true, - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Картинка пака (заглушка) - Container( - width: 300, - height: 470, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(16), - ), - child: const Center( - child: Text( - '?', - style: TextStyle(fontSize: 128, color: Colors.white), - ), - ), - ), - const SizedBox(height: 32), - ElevatedButton( - onPressed: () { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => PackContentScreen(setName: setName), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: const Text( - 'Открыть набор', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ), - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/frontend/lib/views/profile_image_dialog.dart b/frontend/lib/views/profile_image_dialog.dart index 35953bb..8128217 100644 --- a/frontend/lib/views/profile_image_dialog.dart +++ b/frontend/lib/views/profile_image_dialog.dart @@ -71,6 +71,7 @@ class ProfileImageUploadDialog extends StatelessWidget { color: Colors.black, fontSize: 16, fontWeight: FontWeight.w500, + fontFamily: 'Roboto', ), ), ), diff --git a/frontend/lib/views/profile_screen.dart b/frontend/lib/views/profile_screen.dart index 281f5eb..d0a7774 100644 --- a/frontend/lib/views/profile_screen.dart +++ b/frontend/lib/views/profile_screen.dart @@ -148,6 +148,7 @@ class ProfileScreen extends StatelessWidget { style: const TextStyle( fontSize: 18.0, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), @@ -159,6 +160,7 @@ class ProfileScreen extends StatelessWidget { style: const TextStyle( fontSize: 16.0, color: Colors.black54, + fontFamily: 'Jost', ), ), @@ -221,6 +223,7 @@ class ProfileScreen extends StatelessWidget { fontSize: 13.0, fontWeight: FontWeight.normal, color: Colors.black, + fontFamily: 'Jost', ), ), ), @@ -245,6 +248,7 @@ class ProfileScreen extends StatelessWidget { style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), Text( @@ -252,6 +256,7 @@ class ProfileScreen extends StatelessWidget { style: const TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), ], @@ -267,6 +272,7 @@ class ProfileScreen extends StatelessWidget { style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), Text( @@ -274,6 +280,7 @@ class ProfileScreen extends StatelessWidget { style: const TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), ], @@ -302,6 +309,7 @@ class ProfileScreen extends StatelessWidget { fontSize: 15.0, fontWeight: FontWeight.w500, color: Colors.black, + fontFamily: 'Jost', ), ), ), @@ -363,10 +371,12 @@ class ProfileScreen extends StatelessWidget { ), selectedLabelStyle: const TextStyle( fontSize: 12, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), unselectedLabelStyle: const TextStyle( fontSize: 11, + fontFamily: 'Jost', ), items: [ BottomNavigationBarItem( diff --git a/frontend/lib/views/search_players_screen.dart b/frontend/lib/views/search_players_screen.dart index 7d54958..8238b6e 100644 --- a/frontend/lib/views/search_players_screen.dart +++ b/frontend/lib/views/search_players_screen.dart @@ -234,7 +234,7 @@ class CardMockup extends StatelessWidget { child: const Center( child: Text( 'Нет \n изоб', - style: TextStyle(color: Colors.black45, fontSize: 7), + style: TextStyle(color: Colors.black45, fontSize: 7, fontFamily: 'Jost'), textAlign: TextAlign.center, ), ), diff --git a/frontend/lib/views/settings_screen.dart b/frontend/lib/views/settings_screen.dart index 81fae0b..d61ee12 100644 --- a/frontend/lib/views/settings_screen.dart +++ b/frontend/lib/views/settings_screen.dart @@ -30,6 +30,7 @@ class _SettingsDialogState extends State { }); } + @override Widget build(BuildContext context) { return Center( @@ -55,7 +56,7 @@ class _SettingsDialogState extends State { child: Text( 'Настройки', style: TextStyle( - fontFamily: 'Roboto', + fontFamily: 'Jost', fontSize: 28.0, fontWeight: FontWeight.w400, ), @@ -104,7 +105,7 @@ class _SettingsDialogState extends State { child: SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: () {}, + onPressed:(){}, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFD6A067), foregroundColor: Colors.black, @@ -116,7 +117,7 @@ class _SettingsDialogState extends State { child: const Text( 'Выйти из аккаунта', style: TextStyle( - fontFamily: 'Roboto', + fontFamily: 'Jost', fontSize: 18.0, fontWeight: FontWeight.w500, ), @@ -167,7 +168,7 @@ class _SettingsDialogState extends State { _hintText!, textAlign: TextAlign.center, style: const TextStyle( - fontFamily: 'Roboto', + fontFamily: 'Jost', fontSize: 12, color: Colors.black, ), @@ -197,7 +198,7 @@ class _SettingsDialogState extends State { Text( title, style: const TextStyle( - fontFamily: 'Roboto', + fontFamily: 'Jost', fontSize: 18.0, fontWeight: FontWeight.w400, ), @@ -218,7 +219,7 @@ class _SettingsDialogState extends State { '!', style: TextStyle( color: Colors.white, - fontFamily: 'Roboto', + fontFamily: 'Jost', fontSize: 16.0, fontWeight: FontWeight.w500, ), diff --git a/frontend/lib/views/shop_coin_details_screen.dart b/frontend/lib/views/shop_coin_details_screen.dart index 098cd29..d7bde16 100644 --- a/frontend/lib/views/shop_coin_details_screen.dart +++ b/frontend/lib/views/shop_coin_details_screen.dart @@ -61,7 +61,7 @@ class _ShopCoinDetailsScreenState extends State with Sing ), ), ), - title: null, + title: Text('Монеты:', style: TextStyle(color: Colors.black, fontFamily: 'Jost')), centerTitle: true, actions: [ Container( @@ -79,6 +79,7 @@ class _ShopCoinDetailsScreenState extends State with Sing color: Colors.black, fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), SizedBox(width: 6.0), @@ -167,9 +168,9 @@ class _ShopCoinDetailsScreenState extends State with Sing Text( widget.coinName, style: const TextStyle( - fontSize: 16.0, + fontSize: 18.0, fontWeight: FontWeight.bold, - color: Colors.black, + fontFamily: 'Jost', ), textAlign: TextAlign.center, ), @@ -237,6 +238,7 @@ class _ShopCoinDetailsScreenState extends State with Sing style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), @@ -321,22 +323,69 @@ class _ShopCoinDetailsScreenState extends State with Sing type: BottomNavigationBarType.fixed, selectedItemColor: Colors.black, unselectedItemColor: Colors.black54, + selectedIconTheme: const IconThemeData( + size: 28, + ), + unselectedIconTheme: const IconThemeData( + size: 24, + ), + selectedLabelStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + fontFamily: 'Jost', + ), + unselectedLabelStyle: const TextStyle( + fontSize: 11, + fontFamily: 'Jost', + ), items: [ BottomNavigationBarItem( icon: Image.asset('assets/icons/главная.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/главная.png', height: 24), + ), label: 'Главная', ), BottomNavigationBarItem( icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/Инвентарь.png', height: 24), + ), + label: 'Инвентарь', ), BottomNavigationBarItem( icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/магазин.png', height: 24), + ), + label: 'Магазин', ), BottomNavigationBarItem( icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/обменник.png', height: 24), + ), + label: 'Обменник', ), ], ), diff --git a/frontend/lib/views/shop_screen.dart b/frontend/lib/views/shop_screen.dart index ee500be..1cce4e5 100644 --- a/frontend/lib/views/shop_screen.dart +++ b/frontend/lib/views/shop_screen.dart @@ -86,6 +86,7 @@ class _ShopScreenState extends State with SingleTickerProviderStateM color: Colors.black, fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), SizedBox(width: 6.0), @@ -129,6 +130,8 @@ class _ShopScreenState extends State with SingleTickerProviderStateM Tab(text: 'Наборы'), Tab(text: 'Монеты'), ], + labelStyle: TextStyle(fontFamily: 'Jost'), + unselectedLabelStyle: TextStyle(fontFamily: 'Jost'), onTap: (index) { setState(() { _selectedTab = index; @@ -198,10 +201,12 @@ class _ShopScreenState extends State with SingleTickerProviderStateM ), selectedLabelStyle: const TextStyle( fontSize: 12, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), unselectedLabelStyle: const TextStyle( fontSize: 11, + fontFamily: 'Jost', ), items: [ BottomNavigationBarItem( @@ -291,6 +296,7 @@ class _ShopScreenState extends State with SingleTickerProviderStateM fontSize: 16.0, fontWeight: FontWeight.bold, color: Colors.black, + fontFamily: 'Jost', ), textAlign: TextAlign.center, ), @@ -339,6 +345,7 @@ class _ShopScreenState extends State with SingleTickerProviderStateM fontSize: 14.0, fontWeight: FontWeight.bold, color: Colors.black, + fontFamily: 'Jost', ), textAlign: TextAlign.center, ), diff --git a/frontend/lib/views/shop_set_content_screen.dart b/frontend/lib/views/shop_set_content_screen.dart index 8ade5d3..fec9b47 100644 --- a/frontend/lib/views/shop_set_content_screen.dart +++ b/frontend/lib/views/shop_set_content_screen.dart @@ -117,7 +117,7 @@ class _ShopSetContentScreenState extends State { child: const Center( child: Text( 'Нет изображения', - style: TextStyle(color: Colors.black45, fontSize: 12), + style: TextStyle(color: Colors.black45, fontSize: 12, fontFamily: 'Jost'), ), ), ), @@ -171,6 +171,8 @@ class _ShopSetContentScreenState extends State { onPressed: () => Navigator.of(context).pop(), ), systemOverlayStyle: SystemUiOverlayStyle.dark, + title: const Text('Выпавшие карты:', style: TextStyle(color: Colors.black, fontFamily: 'Jost')), + centerTitle: true, ), ), body: Column( @@ -182,7 +184,7 @@ class _ShopSetContentScreenState extends State { color: Colors.black, fontSize: 24.0, fontWeight: FontWeight.bold, - fontFamily: 'Roboto', + fontFamily: 'Jost', ), textAlign: TextAlign.center, ), @@ -236,9 +238,11 @@ class _ShopSetContentScreenState extends State { selectedLabelStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), unselectedLabelStyle: const TextStyle( fontSize: 11, + fontFamily: 'Jost', ), items: [ BottomNavigationBarItem( diff --git a/frontend/lib/views/shop_set_details_screen.dart b/frontend/lib/views/shop_set_details_screen.dart index da539d1..808e747 100644 --- a/frontend/lib/views/shop_set_details_screen.dart +++ b/frontend/lib/views/shop_set_details_screen.dart @@ -3,9 +3,9 @@ import 'shop_set_content_screen.dart'; import 'home_screen.dart'; import 'shop_screen.dart'; import 'exchanges_screen.dart'; -import 'pack_open_screen.dart'; import 'profile_screen.dart'; import 'search_players_screen.dart'; +import 'pack_content_screen.dart'; class ShopSetDetailsScreen extends StatefulWidget { final String setName; @@ -77,6 +77,7 @@ class _ShopSetDetailsScreenState extends State with Single color: Colors.black, fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), SizedBox(width: 6.0), @@ -121,6 +122,8 @@ class _ShopSetDetailsScreenState extends State with Single Tab(text: 'Наборы'), Tab(text: 'Монеты'), ], + labelStyle: TextStyle(fontFamily: 'Roboto'), + unselectedLabelStyle: TextStyle(fontFamily: 'Roboto'), onTap: (index) { if (index == 1) { Navigator.pushReplacement( @@ -171,6 +174,7 @@ class _ShopSetDetailsScreenState extends State with Single style: const TextStyle( fontSize: 18.0, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), const Text( @@ -178,6 +182,7 @@ class _ShopSetDetailsScreenState extends State with Single style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), ], @@ -210,6 +215,7 @@ class _ShopSetDetailsScreenState extends State with Single style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), @@ -225,7 +231,7 @@ class _ShopSetDetailsScreenState extends State with Single Navigator.push( context, MaterialPageRoute( - builder: (context) => PackOpenScreen( + builder: (context) => PackContentScreen( setName: widget.setName, ), ), @@ -244,6 +250,7 @@ class _ShopSetDetailsScreenState extends State with Single style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), From 022dbaa9c9038200dd44629a0eb9b720cfe2180e Mon Sep 17 00:00:00 2001 From: birbik Date: Thu, 22 May 2025 16:11:12 +0300 Subject: [PATCH 30/54] =?UTF-8?q?FCCX-131=20add=20ability=20to=20view=20co?= =?UTF-8?q?llections=20from=20=E2=80=9CHome=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/lib/views/home_screen.dart | 55 +++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/frontend/lib/views/home_screen.dart b/frontend/lib/views/home_screen.dart index 138cf8c..b733312 100644 --- a/frontend/lib/views/home_screen.dart +++ b/frontend/lib/views/home_screen.dart @@ -7,6 +7,7 @@ import 'exchanges_screen.dart'; import 'news_screen.dart'; import 'quests_screen.dart'; import 'profile_screen.dart'; +import 'notifications_modal.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -64,7 +65,12 @@ class _HomeScreenState extends State { height: 36, color: Colors.black, ), - onPressed: null, + onPressed: () { + showDialog( + context: context, + builder: (context) => const NotificationsModal(), + ); + }, ), ], ), @@ -96,10 +102,45 @@ class _HomeScreenState extends State { ), itemCount: 8, itemBuilder: (context, index) { - return Container( - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(12.0), + // Dummy collection names for demonstration + final List collectionNames = [ + 'Весна', + 'Лето', + 'Осень', + 'Зима', + 'Горы', + 'Море', + 'Лес', + 'Город' + ]; + + return InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => InventoryScreen( + collectionName: collectionNames[index], + ), + ), + ); + }, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(12.0), + ), + child: Center( + child: Text( + collectionNames[index], + style: const TextStyle( + color: Colors.black, + fontSize: 14, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + ), + ), + ), ), ); }, @@ -277,10 +318,12 @@ class _HomeScreenState extends State { ), selectedLabelStyle: const TextStyle( fontSize: 12, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), unselectedLabelStyle: const TextStyle( fontSize: 11, + fontFamily: 'Jost', ), items: [ BottomNavigationBarItem( From c852d9de999e1af856eed9db5cbcb5c19b346d5d Mon Sep 17 00:00:00 2001 From: birbik Date: Fri, 23 May 2025 15:22:05 +0300 Subject: [PATCH 31/54] FCCX-132 update exchange creation from inventory --- frontend/lib/views/card_detail_screen.dart | 18 +-- .../lib/views/create_exchange_screen.dart | 114 ++++-------------- frontend/lib/views/inventory_screen.dart | 1 + 3 files changed, 35 insertions(+), 98 deletions(-) diff --git a/frontend/lib/views/card_detail_screen.dart b/frontend/lib/views/card_detail_screen.dart index c53c072..8db2712 100644 --- a/frontend/lib/views/card_detail_screen.dart +++ b/frontend/lib/views/card_detail_screen.dart @@ -5,12 +5,14 @@ class CardDetailScreen extends StatefulWidget { final int cardIndex; final bool showExchangeButton; final bool isFromShop; + final bool showFavoriteButton; const CardDetailScreen({ Key? key, required this.cardIndex, this.showExchangeButton = false, this.isFromShop = false, + this.showFavoriteButton = true, }) : super(key: key); @override @@ -284,13 +286,13 @@ class _CardDetailScreenState extends State { ], ), Positioned( - top: 111, // Adjust this value to move the star vertically - right: -4, // Adjust this value to move the star horizontally - child: InkWell( + top: 111, + right: -4, + child: widget.showFavoriteButton ? InkWell( onTap: () { setState(() { isFavorite = !isFavorite; - print('isFavorite: $isFavorite'); // Added for debugging + print('isFavorite: $isFavorite'); // TODO: Integrate your favorite state management here. // If isFavorite is true, add widget.cardIndex to the user's favorite list. @@ -304,17 +306,17 @@ class _CardDetailScreenState extends State { }); }, child: SizedBox( - width: 60.0, // Increased tap area - height: 60.0, // Increased tap area + width: 60.0, + height: 60.0, child: Center( child: Icon( isFavorite ? Icons.star : Icons.star_border, color: Colors.black, - size: 46.0, // Keep original icon size + size: 46.0, ), ), ), - ), + ) : const SizedBox.shrink(), ), ], ), diff --git a/frontend/lib/views/create_exchange_screen.dart b/frontend/lib/views/create_exchange_screen.dart index e3ef492..fb5f448 100644 --- a/frontend/lib/views/create_exchange_screen.dart +++ b/frontend/lib/views/create_exchange_screen.dart @@ -258,8 +258,12 @@ class _CreateExchangeScreenState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( - onPressed: widget.initialExchangeItem != null - ? () { + onPressed: selectedTopCard == null || ((widget.initialExchangeItem != null || widget.cardId == null) && selectedExchangeCards.isEmpty) + ? null // Делаем кнопку неактивной + : () { + // Если мы дошли до сюда, значит, все необходимые проверки пройдены. + // Показываем успешное сообщение и готовимся к переходу. + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Container( @@ -270,10 +274,10 @@ class _CreateExchangeScreenState extends State { borderRadius: BorderRadius.circular(15.0), ), padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), - child: const Center( + child: Center( child: Text( - 'Обмен успешно отправлен', - style: TextStyle( + widget.initialExchangeItem != null ? 'Обмен успешно отправлен' : 'Обмен создан', + style: const TextStyle( color: Colors.black, fontSize: 18.0, fontWeight: FontWeight.w500, @@ -294,92 +298,22 @@ class _CreateExchangeScreenState extends State { elevation: 0, ), ); - - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen(initialTabIndex: 1)), - (route) => false, - ); - } - : (selectedTopCard == null || selectedExchangeCards.isEmpty) - ? () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Container( - width: 367, - height: 61, - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(15.0), - ), - padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), - child: const Center( - child: Text( - 'Добавьте карточки для обмена', - style: TextStyle( - color: Colors.black, - fontSize: 18.0, - fontWeight: FontWeight.w500, - fontFamily: 'Jost', - ), - textAlign: TextAlign.center, - ), - ), - ), - backgroundColor: Colors.transparent, - duration: const Duration(seconds: 2), - behavior: SnackBarBehavior.floating, - margin: EdgeInsets.only( - bottom: MediaQuery.of(context).size.height - 120, - left: 16, - right: 16, - ), - elevation: 0, - ), - ); - } - : () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Container( - width: 367, - height: 61, - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(15.0), - ), - padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), - child: const Center( - child: Text( - 'Обмен создан', - style: TextStyle( - color: Colors.black, - fontSize: 18.0, - fontWeight: FontWeight.w500, - fontFamily: 'Jost', - ), - textAlign: TextAlign.center, - ), - ), - ), - backgroundColor: Colors.transparent, - duration: const Duration(seconds: 2), - behavior: SnackBarBehavior.floating, - margin: EdgeInsets.only( - bottom: MediaQuery.of(context).size.height - 200, - left: 16, - right: 16, - ), - elevation: 0, - ), - ); - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - }, + // Переходим на экран обменов только после успешного показа уведомления + Future.delayed(const Duration(milliseconds: 500), () { + if (context.mounted) { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => ExchangesScreen( + initialTabIndex: widget.initialExchangeItem != null ? 1 : 0, + ), + ), + (route) => false, + ); + } + }); + }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFD6A067), foregroundColor: Colors.black, diff --git a/frontend/lib/views/inventory_screen.dart b/frontend/lib/views/inventory_screen.dart index 5ce363c..e0593d7 100644 --- a/frontend/lib/views/inventory_screen.dart +++ b/frontend/lib/views/inventory_screen.dart @@ -413,6 +413,7 @@ class _InventoryScreenState extends State { builder: (context) => CardDetailScreen( cardIndex: index, showExchangeButton: widget.isOtherUser, + showFavoriteButton: !widget.isOtherUser, ), ), ); From 6ec37dc85bbf9626693edcd5c64819bd5349db4e Mon Sep 17 00:00:00 2001 From: birbik Date: Sun, 25 May 2025 00:09:42 +0300 Subject: [PATCH 32/54] FCCX-135 update displaying favorite cards in the profile --- frontend/lib/views/profile_screen.dart | 120 ++----------------------- 1 file changed, 7 insertions(+), 113 deletions(-) diff --git a/frontend/lib/views/profile_screen.dart b/frontend/lib/views/profile_screen.dart index d0a7774..8a82b0e 100644 --- a/frontend/lib/views/profile_screen.dart +++ b/frontend/lib/views/profile_screen.dart @@ -130,7 +130,7 @@ class ProfileScreen extends StatelessWidget { border: Border.all(color: Colors.black, width: 2), ), child: const CircleAvatar( - backgroundColor: Color(0xFFFFF4E3), + backgroundColor: Color(0xFFFBF6EF), child: Icon( Icons.person_outline, size: 90.0, @@ -172,8 +172,10 @@ class ProfileScreen extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate( - favoriteCardIds.length, - (index) => _buildCard(favoriteCardIds[index]), + 5, // Always generate 5 cards + (index) => _buildCard( + index < favoriteCardIds.length ? favoriteCardIds[index] : -1, + ), ), ), ), @@ -320,118 +322,9 @@ class ProfileScreen extends StatelessWidget { ), const Spacer(), - - // Нижняя навигационная панель - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: 0, - onTap: (index) { - switch (index) { - case 0: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - ); - break; - case 1: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const InventoryScreen()), - ); - break; - case 2: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - ); - break; - case 3: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - ); - break; - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - showSelectedLabels: true, - showUnselectedLabels: true, - selectedIconTheme: const IconThemeData( - size: 28, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, - fontFamily: 'Jost', - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/магазин.png', height: 24), - ), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), - ), - label: 'Обменник', - ), - ], - ), - ), ], ), + bottomNavigationBar: null, ); } @@ -496,6 +389,7 @@ class ProfileScreen extends StatelessWidget { child: Text( 'Нет изображения', style: TextStyle(color: Colors.black45, fontSize: 12), + textAlign: TextAlign.center, ), ), ), From d64d2ef77e6e72d84cfabff062b32eee77f3c4e6 Mon Sep 17 00:00:00 2001 From: birbik Date: Sun, 25 May 2025 00:10:56 +0300 Subject: [PATCH 33/54] FCCX-136 update displaying text in the detailed view of the exchange --- frontend/lib/views/exchange_details_screen.dart | 1 + frontend/lib/views/exchange_proposal_screen.dart | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/lib/views/exchange_details_screen.dart b/frontend/lib/views/exchange_details_screen.dart index ab3a6d1..b472406 100644 --- a/frontend/lib/views/exchange_details_screen.dart +++ b/frontend/lib/views/exchange_details_screen.dart @@ -104,6 +104,7 @@ class ExchangeDetailsScreen extends StatelessWidget { fontSize: 20, fontFamily: 'Jost', ), + textAlign: TextAlign.center, ), ), ), diff --git a/frontend/lib/views/exchange_proposal_screen.dart b/frontend/lib/views/exchange_proposal_screen.dart index 655e9f8..64b358d 100644 --- a/frontend/lib/views/exchange_proposal_screen.dart +++ b/frontend/lib/views/exchange_proposal_screen.dart @@ -10,7 +10,7 @@ class ExchangeProposalScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Match background color + backgroundColor: const Color(0xFFFBF6EF), // Match background color appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, @@ -263,7 +263,7 @@ Widget _buildExchangeCardVisual({Widget? content, double width = 80, double heig width: 16, // Adjusted size height: 16, // Adjusted size decoration: BoxDecoration( - color: const Color(0xFFFFF4E3), + color: const Color(0xFFFBF6EF), borderRadius: BorderRadius.circular(3), // Adjusted radius border: Border.all(color: Colors.black, width: 1), // Match style ), From b6f42d0cc199cb199808176f901f124d154d1e08 Mon Sep 17 00:00:00 2001 From: birbik Date: Sun, 25 May 2025 01:16:58 +0300 Subject: [PATCH 34/54] FCCX-138 update location of tooltips and color in the settings --- frontend/lib/views/settings_screen.dart | 275 ++++++++++++------------ 1 file changed, 142 insertions(+), 133 deletions(-) diff --git a/frontend/lib/views/settings_screen.dart b/frontend/lib/views/settings_screen.dart index d61ee12..d8d90ab 100644 --- a/frontend/lib/views/settings_screen.dart +++ b/frontend/lib/views/settings_screen.dart @@ -21,162 +21,171 @@ class _SettingsDialogState extends State { _hintText = text; _showHint = true; }); - Future.delayed(const Duration(seconds: 2), () { - if (mounted) { - setState(() { - _showHint = false; - }); - } - }); } - @override Widget build(BuildContext context) { - return Center( - child: Material( - color: Colors.transparent, - child: Container( - width: MediaQuery.of(context).size.width * 0.88, - height: MediaQuery.of(context).size.height * 0.75, - decoration: BoxDecoration( - color: const Color(0xFFF6E6D0), - borderRadius: BorderRadius.circular(10.0), - border: Border.all(color: Colors.black, width: 2), - ), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - const Center( - child: Text( - 'Настройки', - style: TextStyle( - fontFamily: 'Jost', - fontSize: 28.0, - fontWeight: FontWeight.w400, + return GestureDetector( + onTapDown: (_) { + if (_showHint) { + setState(() { + _showHint = false; + }); + } + }, + child: Center( + child: Material( + color: Colors.transparent, + child: Container( + width: MediaQuery.of(context).size.width * 0.88, + height: MediaQuery.of(context).size.height * 0.75, + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(10.0), + border: Border.all(color: Colors.black, width: 2), + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + const Center( + child: Text( + 'Настройки', + style: TextStyle( + fontFamily: 'Jost', + fontSize: 28.0, + fontWeight: FontWeight.w400, + ), ), ), - ), - const SizedBox(height: 24.0), - _buildSettingItem( - title: 'Уведомления', - hasSwitch: true, - switchValue: _notificationsEnabled, - onChanged: (value) { - setState(() { - _notificationsEnabled = value; - }); - }, - ), - const SizedBox(height: 16.0), - _buildSettingItem( - title: 'Отклонение обменов', - hasSwitch: true, - switchValue: _exchangesDeclineEnabled, - hasInfo: true, - onInfoTap: () => _showHintMessage('Подсказка по отклонению обменов'), - onChanged: (value) { - setState(() { - _exchangesDeclineEnabled = value; - }); - }, - ), - const SizedBox(height: 16.0), - _buildSettingItem( - title: 'Показ инвентаря', - hasSwitch: true, - switchValue: _inventoryDisplayEnabled, - hasInfo: true, - onInfoTap: () => _showHintMessage('Подсказка по показу инвентаря'), - onChanged: (value) { - setState(() { - _inventoryDisplayEnabled = value; - }); - }, - ), - const Spacer(), - Padding( - padding: const EdgeInsets.only(bottom: 26.0), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed:(){}, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 18.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), + const SizedBox(height: 24.0), + _buildSettingItem( + title: 'Уведомления', + hasSwitch: true, + switchValue: _notificationsEnabled, + onChanged: (value) { + setState(() { + _notificationsEnabled = value; + }); + }, + ), + const SizedBox(height: 16.0), + _buildSettingItem( + title: 'Отклонение обменов', + hasSwitch: true, + switchValue: _exchangesDeclineEnabled, + hasInfo: true, + onInfoTap: () => _showHintMessage('Подсказка по отклонению обменов'), + onChanged: (value) { + setState(() { + _exchangesDeclineEnabled = value; + }); + }, + ), + const SizedBox(height: 16.0), + _buildSettingItem( + title: 'Показ инвентаря', + hasSwitch: true, + switchValue: _inventoryDisplayEnabled, + hasInfo: true, + onInfoTap: () => _showHintMessage('Подсказка по показу инвентаря'), + onChanged: (value) { + setState(() { + _inventoryDisplayEnabled = value; + }); + }, + ), + const Spacer(), + Padding( + padding: const EdgeInsets.only(bottom: 26.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed:(){}, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 18.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), ), - ), - child: const Text( - 'Выйти из аккаунта', - style: TextStyle( - fontFamily: 'Jost', - fontSize: 18.0, - fontWeight: FontWeight.w500, + child: const Text( + 'Выйти из аккаунта', + style: TextStyle( + fontFamily: 'Jost', + fontSize: 18.0, + fontWeight: FontWeight.w500, + ), ), ), ), ), - ), - ], - ), - ), - Positioned( - top: -1, - right: -1, - child: GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(10.0), - border: Border.all(color: Colors.black, width: 2), - ), - child: const Center( - child: Icon( - Icons.close_rounded, - color: Colors.black, - size: 48.0, - ), - ), + ], ), ), - ), - if (_showHint && _hintText != null) Positioned( - top: 0, - right: 0, - child: Material( - color: Colors.transparent, + top: -1, + right: -1, + child: GestureDetector( + onTap: () => Navigator.of(context).pop(), child: Container( - width: 100, - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), + width: 48, + height: 48, decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10), + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10.0), + border: Border.all(color: Colors.black, width: 2), ), - child: Text( - _hintText!, - textAlign: TextAlign.center, - style: const TextStyle( - fontFamily: 'Jost', - fontSize: 12, + child: const Center( + child: Icon( + Icons.close_rounded, color: Colors.black, + size: 44.0, ), ), ), ), ), - ], + if (_showHint && _hintText != null) + Positioned( + left: MediaQuery.of(context).size.width * 0.35, + top: _hintText!.contains('обменов') ? 195 : 255, + child: Material( + color: Colors.transparent, + child: Container( + width: 160, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.black, width: 1), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Text( + _hintText!, + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'Jost', + fontSize: 14, + color: Colors.black, + ), + ), + ), + ), + ), + ], + ), ), ), ), From 663839cef2655a94faa1861d6404fa340c22ba6f Mon Sep 17 00:00:00 2001 From: birbik Date: Sun, 25 May 2025 01:19:08 +0300 Subject: [PATCH 35/54] FCCX-139 update logic of the navbar --- frontend/lib/main.dart | 129 +++++++++++++++++- frontend/lib/views/create_card_screen.dart | 117 +--------------- .../lib/views/create_exchange_screen.dart | 120 +--------------- frontend/lib/views/exchanges_screen.dart | 110 +-------------- frontend/lib/views/home_screen.dart | 122 +---------------- 5 files changed, 135 insertions(+), 463 deletions(-) diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index eb339f4..9aa7526 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -7,6 +7,9 @@ import 'views/email_verification_screen.dart'; import 'views/forgot_password_screen.dart'; import 'views/change_password_screen.dart'; import 'views/home_screen.dart'; +import 'views/inventory_screen.dart'; +import 'views/shop_screen.dart'; +import 'views/exchanges_screen.dart'; void main() { runApp(const MyApp()); @@ -80,7 +83,7 @@ class MyApp extends StatelessWidget { elevation: 0, ), ), - home: const HomeScreen(), + home: const MainScreen(), routes: { '/auth': (context) => const AuthScreen(), '/login': (context) => const AuthScreen(initialTabIndex: 0), @@ -92,3 +95,127 @@ class MyApp extends StatelessWidget { ); } } + +class MainScreen extends StatefulWidget { + const MainScreen({super.key}); + + @override + State createState() => _MainScreenState(); +} + +class _MainScreenState extends State { + int _currentIndex = 0; + final PageController _pageController = PageController(); + + final List _screens = const [ + HomeScreen(), + InventoryScreen(), + ShopScreen(), + ExchangesScreen(), + ]; + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + void _onItemTapped(int index) { + setState(() { + _currentIndex = index; + }); + _pageController.jumpToPage(index); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + children: _screens, + ), + bottomNavigationBar: Container( + decoration: const BoxDecoration( + color: Color(0xFFD6A067), + ), + child: BottomNavigationBar( + currentIndex: _currentIndex, + onTap: _onItemTapped, + backgroundColor: Colors.transparent, + elevation: 0, + type: BottomNavigationBarType.fixed, + selectedItemColor: Colors.black, + unselectedItemColor: Colors.black54, + showSelectedLabels: true, + showUnselectedLabels: true, + selectedIconTheme: const IconThemeData( + size: 28, + ), + unselectedIconTheme: const IconThemeData( + size: 24, + ), + selectedLabelStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + fontFamily: 'Jost', + ), + unselectedLabelStyle: const TextStyle( + fontSize: 11, + fontFamily: 'Jost', + ), + items: [ + BottomNavigationBarItem( + icon: Image.asset('assets/icons/главная.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/главная.png', height: 24), + ), + label: 'Главная', + ), + BottomNavigationBarItem( + icon: Image.asset('assets/icons/Инвентарь.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/Инвентарь.png', height: 24), + ), + label: 'Инвентарь', + ), + BottomNavigationBarItem( + icon: Image.asset('assets/icons/магазин.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/магазин.png', height: 24), + ), + label: 'Магазин', + ), + BottomNavigationBarItem( + icon: Image.asset('assets/icons/обменник.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/обменник.png', height: 24), + ), + label: 'Обменник', + ), + ], + ), + ), + ); + } +} diff --git a/frontend/lib/views/create_card_screen.dart b/frontend/lib/views/create_card_screen.dart index 7341db5..19c0410 100644 --- a/frontend/lib/views/create_card_screen.dart +++ b/frontend/lib/views/create_card_screen.dart @@ -168,124 +168,9 @@ class _CreateCardScreenState extends State { ), ), ), - - // Нижняя навигационная панель - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 1: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const InventoryScreen()), - (route) => false, - ); - break; - case 2: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - showSelectedLabels: true, - showUnselectedLabels: true, - selectedIconTheme: const IconThemeData( - size: 28, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, - fontFamily: 'Jost', - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/магазин.png', height: 24), - ), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), - ), - label: 'Обменник', - ), - ], - ), - ), ], ), + bottomNavigationBar: null, ); } diff --git a/frontend/lib/views/create_exchange_screen.dart b/frontend/lib/views/create_exchange_screen.dart index fb5f448..ee47421 100644 --- a/frontend/lib/views/create_exchange_screen.dart +++ b/frontend/lib/views/create_exchange_screen.dart @@ -169,7 +169,7 @@ class _CreateExchangeScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( backgroundColor: const Color(0xFFFFF4E3), elevation: 0, @@ -334,125 +334,9 @@ class _CreateExchangeScreenState extends State { ), ), ), - // Нижняя навигация - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (widget.initialExchangeItem == null) { - if (index != _currentIndex) { - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 1: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const InventoryScreen()), - (route) => false, - ); - break; - case 2: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - showSelectedLabels: true, - showUnselectedLabels: true, - selectedIconTheme: const IconThemeData( - size: 24, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - unselectedLabelStyle: const TextStyle( - fontSize: 12, - fontFamily: 'Jost', - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/магазин.png', height: 24), - ), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), - ), - label: 'Обменник', - ), - ], - ), - ), ], ), + bottomNavigationBar: null, ); } diff --git a/frontend/lib/views/exchanges_screen.dart b/frontend/lib/views/exchanges_screen.dart index 2e84a00..3364266 100644 --- a/frontend/lib/views/exchanges_screen.dart +++ b/frontend/lib/views/exchanges_screen.dart @@ -1,9 +1,6 @@ import 'package:flutter/material.dart'; import 'exchange_details_screen.dart'; import 'create_exchange_screen.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; -import 'inventory_screen.dart'; import 'profile_screen.dart'; import 'search_players_screen.dart'; import 'exchange_proposal_screen.dart'; @@ -18,7 +15,6 @@ class ExchangesScreen extends StatefulWidget { } class _ExchangesScreenState extends State with SingleTickerProviderStateMixin { - int _currentIndex = 3; late TabController _tabController; String _sortOption = 'По дате'; bool _showSortOptions = false; @@ -291,111 +287,7 @@ class _ExchangesScreenState extends State with SingleTickerProv ), ], ), - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 0: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - ); - break; - case 1: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const InventoryScreen()), - ); - break; - case 2: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - showSelectedLabels: true, - showUnselectedLabels: true, - selectedIconTheme: const IconThemeData( - size: 28, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/магазин.png', height: 24), - ), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), - ), - label: 'Обменник', - ), - ], - ), - ), + bottomNavigationBar: null, ); } diff --git a/frontend/lib/views/home_screen.dart b/frontend/lib/views/home_screen.dart index b733312..a18b483 100644 --- a/frontend/lib/views/home_screen.dart +++ b/frontend/lib/views/home_screen.dart @@ -2,23 +2,14 @@ import 'package:flutter/material.dart'; import 'inventory_screen.dart'; import 'search_players_screen.dart'; import 'create_card_screen.dart'; -import 'shop_screen.dart'; -import 'exchanges_screen.dart'; import 'news_screen.dart'; import 'quests_screen.dart'; import 'profile_screen.dart'; import 'notifications_modal.dart'; -class HomeScreen extends StatefulWidget { +class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); - @override - State createState() => _HomeScreenState(); -} - -class _HomeScreenState extends State { - int _currentIndex = 0; - @override Widget build(BuildContext context) { return Scaffold( @@ -269,114 +260,7 @@ class _HomeScreenState extends State { ), ], ), - - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 1: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const InventoryScreen()), - ); - break; - case 2: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - ); - break; - case 3: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - showSelectedLabels: true, - showUnselectedLabels: true, - selectedIconTheme: const IconThemeData( - size: 28, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, - fontFamily: 'Jost', - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/магазин.png', height: 24), - ), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), - ), - label: 'Обменник', - ), - ], - ), - ), + bottomNavigationBar: null, ); } -} \ No newline at end of file +} \ No newline at end of file From a06684439bffd8dcc84bab462aee199a7a25bdca Mon Sep 17 00:00:00 2001 From: birbik Date: Sun, 25 May 2025 01:20:17 +0300 Subject: [PATCH 36/54] FCCX-140 update store logic --- frontend/lib/views/inventory_screen.dart | 120 +---- .../lib/views/shop_coin_details_screen.dart | 397 ---------------- frontend/lib/views/shop_screen.dart | 442 ++++++++++++------ .../lib/views/shop_set_content_screen.dart | 303 ------------ .../lib/views/shop_set_details_screen.dart | 413 ---------------- 5 files changed, 317 insertions(+), 1358 deletions(-) delete mode 100644 frontend/lib/views/shop_coin_details_screen.dart delete mode 100644 frontend/lib/views/shop_set_content_screen.dart delete mode 100644 frontend/lib/views/shop_set_details_screen.dart diff --git a/frontend/lib/views/inventory_screen.dart b/frontend/lib/views/inventory_screen.dart index e0593d7..e7ccb43 100644 --- a/frontend/lib/views/inventory_screen.dart +++ b/frontend/lib/views/inventory_screen.dart @@ -1,7 +1,4 @@ import 'package:flutter/material.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; -import 'exchanges_screen.dart'; import 'profile_screen.dart'; import 'card_detail_screen.dart'; import 'search_players_screen.dart'; @@ -11,6 +8,7 @@ class InventoryScreen extends StatefulWidget { final String? playerName; final String? playerId; final String? collectionName; + final bool isFromShop; const InventoryScreen({ super.key, @@ -18,6 +16,7 @@ class InventoryScreen extends StatefulWidget { this.playerName, this.playerId, this.collectionName, + this.isFromShop = false, }); @override @@ -25,7 +24,6 @@ class InventoryScreen extends StatefulWidget { } class _InventoryScreenState extends State { - int _currentIndex = 1; String _sortOption = 'По редкости'; bool _showSortOptions = false; @@ -102,7 +100,9 @@ class _InventoryScreenState extends State { ), const SizedBox(width: 8), Text( - 'Коллекция ${widget.collectionName}', + widget.isFromShop + ? 'Содержимое ${widget.collectionName}' + : 'Коллекция ${widget.collectionName}', style: const TextStyle( color: Colors.black, fontWeight: FontWeight.bold, @@ -292,115 +292,7 @@ class _InventoryScreenState extends State { ), ], ), - bottomNavigationBar: widget.isOtherUser - ? null - : Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 0: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - ); - break; - case 2: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - ); - break; - case 3: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - showSelectedLabels: true, - showUnselectedLabels: true, - selectedIconTheme: const IconThemeData( - size: 28, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, - fontFamily: 'Jost', - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/магазин.png', height: 24), - ), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), - ), - label: 'Обменник', - ), - ], - ), - ), + bottomNavigationBar: null, ); } diff --git a/frontend/lib/views/shop_coin_details_screen.dart b/frontend/lib/views/shop_coin_details_screen.dart deleted file mode 100644 index d7bde16..0000000 --- a/frontend/lib/views/shop_coin_details_screen.dart +++ /dev/null @@ -1,397 +0,0 @@ -import 'package:flutter/material.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; -import 'exchanges_screen.dart'; -import 'profile_screen.dart'; -import 'search_players_screen.dart'; - -class ShopCoinDetailsScreen extends StatefulWidget { - final String coinName; - - const ShopCoinDetailsScreen({ - super.key, - required this.coinName, - }); - - @override - State createState() => _ShopCoinDetailsScreenState(); -} - -class _ShopCoinDetailsScreenState extends State with SingleTickerProviderStateMixin { - int _currentIndex = 2; // Индекс вкладки "Магазин" в нижней навигации - late TabController _tabController; - final List _coins = [ - 'Ценник 1', - 'Ценник 2', - ]; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 2, vsync: this, initialIndex: 1); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFBF6EF), - appBar: AppBar( - backgroundColor: const Color(0xFFFBF6EF), - elevation: 0, - leading: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Container( - width: 40.0, - height: 40.0, - child: InkWell( - borderRadius: BorderRadius.circular(20.0), - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ProfileScreen()), - ); - }, - child: Image.asset('assets/icons/профиль.png', height: 22), - ), - ), - ), - title: Text('Монеты:', style: TextStyle(color: Colors.black, fontFamily: 'Jost')), - centerTitle: true, - actions: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(10.0), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '1000', - style: const TextStyle( - color: Colors.black, - fontSize: 16.0, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - ), - SizedBox(width: 6.0), - Image.asset('assets/icons/монеты.png', height: 20), - ], - ), - ), - IconButton( - icon: Image.asset('assets/icons/поиск.png', height: 32), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => const SearchPlayersModal(), - ); - }, - ), - ], - ), - body: Column( - children: [ - // Вкладки "Наборы" и "Монеты" - Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(12.0), - ), - child: TabBar( - controller: _tabController, - dividerColor: Colors.transparent, - indicator: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.all(Radius.circular(10.0)), - ), - labelColor: Colors.black, - unselectedLabelColor: Colors.black, - indicatorSize: TabBarIndicatorSize.tab, - tabs: const [ - Tab(text: 'Наборы'), - Tab(text: 'Монеты'), - ], - onTap: (index) { - if (index == 0) { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - ); - } - }, - ), - ), - ), - // Основное содержимое - детали монеты - Expanded( - child: Center( - child: SizedBox( - width: 360.0, - height: 492.0, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 3), - ), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.only(top: 96.0, left: 16.0, right: 16.0, bottom: 16.0), - child: Column( - children: [ - // Превью монеты - Center( - child: Container( - height: 150.0, - width: 150.0, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), - ), - ), - const SizedBox(height: 7.0), - Text( - widget.coinName, - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 60.0), - // Кнопка покупки - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - final overlay = Overlay.of(context); - final overlayEntry = OverlayEntry( - builder: (context) => Positioned( - top: 40, - left: 16, - right: 16, - child: Material( - color: Colors.transparent, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.18), - blurRadius: 8, - offset: const Offset(0, 6), - ), - ], - ), - padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 5), - child: const Center( - child: Text( - 'Монеты успешно приобретены', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Colors.black, - ), - ), - ), - ), - ), - ), - ); - overlay.insert(overlayEntry); - Future.delayed(const Duration(seconds: 3), () { - overlayEntry.remove(); - }); - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: const Text( - 'Купить', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - fontFamily: 'Roboto', - ), - ), - ), - ), - ], - ), - ), - // Кнопка закрытия - Positioned( - top: -1, - right: -1, - child: GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - width: 48.0, - height: 48.0, - decoration: BoxDecoration( - color: const Color(0xFFD9A76A), - border: Border.all( - color: Colors.black, - width: 1.0, - ), - borderRadius: const BorderRadius.only( - topRight: Radius.circular(8.0), - bottomLeft: Radius.circular(8.0), - ), - ), - child: const Icon( - Icons.close, - color: Colors.black, - size: 32.0, - weight: 900, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ), - // Нижняя навигационная панель - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 2: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - selectedIconTheme: const IconThemeData( - size: 28, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, - fontFamily: 'Jost', - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/магазин.png', height: 24), - ), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), - ), - label: 'Обменник', - ), - ], - ), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/frontend/lib/views/shop_screen.dart b/frontend/lib/views/shop_screen.dart index 1cce4e5..476bebf 100644 --- a/frontend/lib/views/shop_screen.dart +++ b/frontend/lib/views/shop_screen.dart @@ -1,11 +1,8 @@ import 'package:flutter/material.dart'; -import 'home_screen.dart'; -import 'exchanges_screen.dart'; -import 'inventory_screen.dart'; -import 'shop_set_details_screen.dart'; -import 'shop_coin_details_screen.dart'; import 'profile_screen.dart'; import 'search_players_screen.dart'; +import 'inventory_screen.dart'; +import 'pack_content_screen.dart'; class ShopScreen extends StatefulWidget { const ShopScreen({super.key}); @@ -16,7 +13,6 @@ class ShopScreen extends StatefulWidget { class _ShopScreenState extends State with SingleTickerProviderStateMixin { late TabController _tabController; - int _currentIndex = 2; int _selectedTab = 0; final List _sets = [ @@ -43,6 +39,312 @@ class _ShopScreenState extends State with SingleTickerProviderStateM super.dispose(); } + void _showSetDetails(String setName) { + showDialog( + context: context, + builder: (context) => Dialog( + backgroundColor: Colors.transparent, + insetPadding: EdgeInsets.zero, + child: Center( + child: SizedBox( + width: 360.0, + height: 492.0, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(8.0), + border: Border.all(color: Colors.black, width: 3), + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(top: 55.0, left: 16.0, right: 16.0, bottom: 16.0), + child: Column( + children: [ + Container( + height: 120.0, + width: 321.0, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(8.0), + ), + ), + const SizedBox(height: 10.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + setName, + style: const TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + fontFamily: 'Jost', + ), + ), + const Text( + 'Цена', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + ), + ), + ], + ), + const SizedBox(height: 70.0), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => InventoryScreen( + collectionName: setName, + isFromShop: true, + ), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 16.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + child: const Text( + 'Посмотреть содержимое', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + fontFamily: 'Roboto', + ), + ), + ), + ), + const SizedBox(height: 12.0), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PackContentScreen( + setName: setName, + ), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 16.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + child: const Text( + 'Купить', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + fontFamily: 'Roboto', + ), + ), + ), + ), + ], + ), + ), + Positioned( + top: -1, + right: -1, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 48.0, + height: 48.0, + decoration: BoxDecoration( + color: const Color(0xFFD9A76A), + border: Border.all( + color: Colors.black, + width: 1.0, + ), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(8.0), + bottomLeft: Radius.circular(8.0), + ), + ), + child: const Icon( + Icons.close, + color: Colors.black, + size: 32.0, + weight: 900, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + void _showCoinDetails(String coinName) { + showDialog( + context: context, + builder: (context) => Dialog( + backgroundColor: Colors.transparent, + insetPadding: EdgeInsets.zero, + child: Center( + child: SizedBox( + width: 360.0, + height: 492.0, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(8.0), + border: Border.all(color: Colors.black, width: 3), + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(top: 96.0, left: 16.0, right: 16.0, bottom: 16.0), + child: Column( + children: [ + Center( + child: Container( + height: 150.0, + width: 150.0, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + const SizedBox(height: 7.0), + Text( + coinName, + style: const TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + fontFamily: 'Jost', + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 60.0), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + final overlay = Overlay.of(context); + final overlayEntry = OverlayEntry( + builder: (context) => Positioned( + top: 40, + left: 16, + right: 16, + child: Material( + color: Colors.transparent, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.18), + blurRadius: 8, + offset: const Offset(0, 6), + ), + ], + ), + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 5), + child: const Center( + child: Text( + 'Монеты успешно приобретены', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + ), + ), + ), + ), + ); + overlay.insert(overlayEntry); + Future.delayed(const Duration(seconds: 3), () { + overlayEntry.remove(); + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 16.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + child: const Text( + 'Купить', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + fontFamily: 'Roboto', + ), + ), + ), + ), + ], + ), + ), + Positioned( + top: -1, + right: -1, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 48.0, + height: 48.0, + decoration: BoxDecoration( + color: const Color(0xFFD9A76A), + border: Border.all( + color: Colors.black, + width: 1.0, + ), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(8.0), + bottomLeft: Radius.circular(8.0), + ), + ), + child: const Icon( + Icons.close, + color: Colors.black, + size: 32.0, + weight: 900, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -55,7 +357,6 @@ class _ShopScreenState extends State with SingleTickerProviderStateM child: Container( width: 40.0, height: 40.0, - child: InkWell( borderRadius: BorderRadius.circular(20.0), onTap: () { @@ -146,120 +447,13 @@ class _ShopScreenState extends State with SingleTickerProviderStateM controller: _tabController, children: [ _buildSetsTab(), - _buildCoinsTab(), ], ), ), ], ), - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 0: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - ); - break; - case 1: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const InventoryScreen()), - ); - break; - case 3: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - showSelectedLabels: true, - showUnselectedLabels: true, - selectedIconTheme: const IconThemeData( - size: 28, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, - fontFamily: 'Jost', - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/магазин.png', height: 24), - ), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), - ), - label: 'Обменник', - ), - ], - ), - ), + bottomNavigationBar: null, ); } @@ -272,14 +466,7 @@ class _ShopScreenState extends State with SingleTickerProviderStateM return Column( children: [ InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShopSetDetailsScreen(setName: _sets[index]), - ), - ); - }, + onTap: () => _showSetDetails(_sets[index]), child: Container( width: 321.0, height: 120.0, @@ -321,14 +508,7 @@ class _ShopScreenState extends State with SingleTickerProviderStateM return Column( children: [ InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShopCoinDetailsScreen(coinName: _coins[index]), - ), - ); - }, + onTap: () => _showCoinDetails(_coins[index]), child: Container( width: 150.0, height: 150.0, diff --git a/frontend/lib/views/shop_set_content_screen.dart b/frontend/lib/views/shop_set_content_screen.dart deleted file mode 100644 index fec9b47..0000000 --- a/frontend/lib/views/shop_set_content_screen.dart +++ /dev/null @@ -1,303 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; -import 'exchanges_screen.dart'; -import 'inventory_screen.dart'; -import 'package:flutter/rendering.dart'; -import 'card_detail_screen.dart'; - -class ShopSetContentScreen extends StatefulWidget { - final String setName; - - const ShopSetContentScreen({ - super.key, - required this.setName, - }); - - @override - State createState() => _ShopSetContentScreenState(); -} - -class _ShopSetContentScreenState extends State { - void _handleNavigation(int index) { - if (index != 2) { // Если нажата не иконка магазина - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 1: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const InventoryScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - } - - Widget _buildCardItem({int index = 0}) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen( - cardIndex: index, - showExchangeButton: false, - isFromShop: true, - ), - ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - // Внешняя тонкая черная рамка - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - // Прослойка цвета карточки - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - // Внутренняя тонкая черная рамка - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - // Основная карточка - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Container( - color: const Color(0xFFEAD7C3), - child: const Center( - child: Text( - 'Нет изображения', - style: TextStyle(color: Colors.black45, fontSize: 12, fontFamily: 'Jost'), - ), - ), - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(4, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), - appBar: PreferredSize( - preferredSize: const Size.fromHeight(kToolbarHeight), - child: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black), - onPressed: () => Navigator.of(context).pop(), - ), - systemOverlayStyle: SystemUiOverlayStyle.dark, - title: const Text('Выпавшие карты:', style: TextStyle(color: Colors.black, fontFamily: 'Jost')), - centerTitle: true, - ), - ), - body: Column( - children: [ - const SizedBox(height: 28.0), - const Text( - 'В наборе содержится:', - style: TextStyle( - color: Colors.black, - fontSize: 24.0, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 28.0), - // Сетка карточек - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 0.7, - crossAxisSpacing: 8.0, - mainAxisSpacing: 12.0, - ), - itemCount: 12, - itemBuilder: (context, index) { - return _buildCardItem(index: index); - }, - ), - ), - ), - - // Нижняя навигационная панель - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - border: Border( - top: BorderSide( - color: Colors.black, - width: 1.0, - ), - ), - ), - child: BottomNavigationBar( - currentIndex: 2, - onTap: _handleNavigation, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - showSelectedLabels: true, - showUnselectedLabels: true, - selectedIconTheme: const IconThemeData( - size: 28, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, - fontFamily: 'Jost', - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/магазин.png', height: 24), - ), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), - ), - label: 'Обменник', - ), - ], - ), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/frontend/lib/views/shop_set_details_screen.dart b/frontend/lib/views/shop_set_details_screen.dart deleted file mode 100644 index 808e747..0000000 --- a/frontend/lib/views/shop_set_details_screen.dart +++ /dev/null @@ -1,413 +0,0 @@ -import 'package:flutter/material.dart'; -import 'shop_set_content_screen.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; -import 'exchanges_screen.dart'; -import 'profile_screen.dart'; -import 'search_players_screen.dart'; -import 'pack_content_screen.dart'; - -class ShopSetDetailsScreen extends StatefulWidget { - final String setName; - - const ShopSetDetailsScreen({ - super.key, - required this.setName, - }); - - @override - State createState() => _ShopSetDetailsScreenState(); -} - -class _ShopSetDetailsScreenState extends State with SingleTickerProviderStateMixin { - int _currentIndex = 2; // Индекс вкладки "Магазин" в нижней навигации - late TabController _tabController; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 2, vsync: this, initialIndex: 0); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFBF6EF), // Бежевый фон - appBar: AppBar( - backgroundColor: const Color(0xFFFBF6EF), - elevation: 0, - leading: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Container( - width: 40.0, - height: 40.0, - child: InkWell( - borderRadius: BorderRadius.circular(20.0), - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ProfileScreen()), - ); - }, - child: Image.asset('assets/icons/профиль.png', height: 22), - ), - ), - ), - title: null, - centerTitle: true, - actions: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(10.0), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '1000', - style: const TextStyle( - color: Colors.black, - fontSize: 16.0, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - ), - SizedBox(width: 6.0), - Image.asset('assets/icons/монеты.png', height: 20), - ], - ), - ), - IconButton( - icon: Image.asset('assets/icons/поиск.png', height: 32), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => const SearchPlayersModal(), - ); - }, - ), - ], - ), - body: Column( - children: [ - // Вкладки "Наборы" и "Монеты" - Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(12.0), - ), - child: TabBar( - controller: _tabController, - dividerColor: Colors.transparent, - indicator: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.all(Radius.circular(10.0)), - ), - labelColor: Colors.black, - unselectedLabelColor: Colors.black, - indicatorSize: TabBarIndicatorSize.tab, - tabs: const [ - Tab(text: 'Наборы'), - Tab(text: 'Монеты'), - ], - labelStyle: TextStyle(fontFamily: 'Roboto'), - unselectedLabelStyle: TextStyle(fontFamily: 'Roboto'), - onTap: (index) { - if (index == 1) { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - ); - } - }, - ), - ), - ), - - // Основное содержимое - детали набора - Expanded( - child: Center( - child: SizedBox( - width: 360.0, - height: 492.0, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 3), - ), - child: Stack( - children: [ - // Содержимое карточки набора - Padding( - padding: const EdgeInsets.only(top: 55.0, left: 16.0, right: 16.0, bottom: 16.0), - child: Column( - children: [ - // Превью набора - Container( - height: 120.0, - width: 321.0, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), - ), - const SizedBox(height: 10.0), - // Название и цена - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.setName, - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - ), - const Text( - 'Цена', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, - fontFamily: 'Jost', - ), - ), - ], - ), - - const SizedBox(height: 70.0), - - // Кнопка просмотра содержимого - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShopSetContentScreen(setName: widget.setName), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: const Text( - 'Посмотреть содержимое', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - fontFamily: 'Roboto', - ), - ), - ), - ), - - const SizedBox(height: 12.0), - - // Кнопка покупки - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PackContentScreen( - setName: widget.setName, - ), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: const Text( - 'Купить', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - fontFamily: 'Roboto', - ), - ), - ), - ), - ], - ), - ), - - // Кнопка закрытия - Positioned( - top: -1, - right: -1, - child: GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - width: 48.0, - height: 48.0, - decoration: BoxDecoration( - color: const Color(0xFFD9A76A), - border: Border.all( - color: Colors.black, - width: 1.0, - ), - borderRadius: const BorderRadius.only( - topRight: Radius.circular(8.0), - bottomLeft: Radius.circular(8.0), - ), - ), - child: const Icon( - Icons.close, - color: Colors.black, - size: 32.0, - weight: 900, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ), - - // Нижняя навигационная панель - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - - // Навигация в зависимости от выбранного индекса - switch (index) { - case 0: // Главное меню - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 2: // Магазин - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: // Обменчик - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - showSelectedLabels: true, - showUnselectedLabels: true, - selectedIconTheme: const IconThemeData( - size: 28, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/магазин.png', height: 24), - ), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), - ), - label: 'Обменник', - ), - ], - ), - ), - ], - ), - ); - } -} \ No newline at end of file From d7598a7201e4ad563c7deb65247fe10d7204f1c0 Mon Sep 17 00:00:00 2001 From: birbik Date: Sun, 25 May 2025 01:22:33 +0300 Subject: [PATCH 37/54] FCCX-141 remove profile, search and notification icons --- frontend/lib/views/news_screen.dart | 346 ++++++++-------------------- 1 file changed, 90 insertions(+), 256 deletions(-) diff --git a/frontend/lib/views/news_screen.dart b/frontend/lib/views/news_screen.dart index 0fd91f5..3745986 100644 --- a/frontend/lib/views/news_screen.dart +++ b/frontend/lib/views/news_screen.dart @@ -1,265 +1,71 @@ import 'package:flutter/material.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; -import 'exchanges_screen.dart'; import 'news_detail_screen.dart'; import 'profile_screen.dart'; import 'search_players_screen.dart'; -class NewsScreen extends StatefulWidget { +class NewsScreen extends StatelessWidget { const NewsScreen({super.key}); - @override - State createState() => _NewsScreenState(); -} - -class _NewsScreenState extends State { - int _currentIndex = 0; // Changed from 1 to 0 to select Home tab - - // Примерные данные новостей (без использования типа DateTime для избежания ошибок) - final List _newsItems = [ - NewsItem( - title: 'Заголовок к новости', - content: 'Текст новости', - date: '12.05.2023', - fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', - ), - NewsItem( - title: 'Заголовок к новости', - content: 'Текст новости', - date: '10.05.2023', - fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', - ), - NewsItem( - title: 'Заголовок к новости', - content: 'Текст новости', - date: '08.05.2023', - fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', - ), - NewsItem( - title: 'Заголовок к новости', - content: 'Текст новости', - date: '05.05.2023', - fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', - ), - ]; - @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFBF6EF), // Бежевый фон - appBar: AppBar( - backgroundColor: const Color(0xFFFBF6EF), - elevation: 0, - leading: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Container( - width: 40.0, - height: 40.0, - child: InkWell( - borderRadius: BorderRadius.circular(20.0), - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ProfileScreen()), - ); - }, - child: Image.asset('assets/icons/профиль.png', height: 22), - ), - ), - ), - actions: [ - IconButton( - icon: Image.asset( - 'assets/icons/поиск.png', - height: 32, - color: Colors.black, - ), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => const SearchPlayersModal(), - ); - }, - ), - IconButton( - icon: Image.asset( - 'assets/icons/уведомления.png', - height: 36, - color: Colors.black, - ), - onPressed: null, - ), - ], - ), - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Кнопка возврата и заголовок - Padding( - padding: const EdgeInsets.only(left: 12.0, right: 12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + backgroundColor: const Color(0xFFFBF6EF), + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Кнопка назад - Container( - margin: const EdgeInsets.only(left: 0.0), - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - shape: BoxShape.circle, - ), - child: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black), - onPressed: () { - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - }, - ), - ), - - const SizedBox(height: 16.0), - - // Заголовок "Новости" - const Center( - child: Text( - 'Новости', - style: TextStyle( - fontSize: 24.0, - fontWeight: FontWeight.bold, - color: Colors.black, - fontFamily: 'Jost', + Padding( + padding: const EdgeInsets.all(16.0), + child: InkWell( + onTap: () => Navigator.pop(context), + borderRadius: BorderRadius.circular(20.0), + child: Container( + width: 40.0, + height: 40.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFD6A067), + ), + child: const Icon( + Icons.arrow_back, + color: Colors.black, + size: 29.0, + ), ), ), ), ], ), - ), - - const SizedBox(height: 16.0), - - // Список новостей - Expanded( - child: ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - itemCount: _newsItems.length, - itemBuilder: (context, index) { - return _buildNewsItem(_newsItems[index]); - }, - ), - ), - ], - ), - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 0: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - ); - break; - case 1: - // Current screen is NewsScreen, do nothing or handle appropriately - break; - case 2: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - ); - break; - case 3: - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - showSelectedLabels: true, - showUnselectedLabels: true, - selectedIconTheme: const IconThemeData( - size: 28, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, - fontFamily: 'Jost', - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), + + const SizedBox(height: 16.0), + + // Заголовок "Новости" + const Center( + child: Text( + 'Новости', + style: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: Colors.black, + fontFamily: 'Jost', ), - child: Image.asset('assets/icons/магазин.png', height: 24), ), - label: 'Магазин', ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), + + const SizedBox(height: 16.0), + + // Список новостей + Expanded( + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + itemCount: _newsItems.length, + itemBuilder: (context, index) { + return _buildNewsItem(context, _newsItems[index]); + }, ), - label: 'Обменник', ), ], ), @@ -268,12 +74,12 @@ class _NewsScreenState extends State { } // Метод для отображения элемента новости - Widget _buildNewsItem(NewsItem news) { + Widget _buildNewsItem(BuildContext context, NewsItem news) { return GestureDetector( onTap: () { showDialog( context: context, - builder: (context) => NewsDetailScreen(news: news), + builder: (context) => NewsDetailScreen(news: news), ); }, child: Container( @@ -288,23 +94,23 @@ class _NewsScreenState extends State { children: [ // Заголовок Text( - news.title, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14.0, - fontFamily: 'Jost', - ), + news.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.0, + fontFamily: 'Jost', + ), ), const SizedBox(height: 8.0), // Текст Text( - news.content, - style: const TextStyle( - fontSize: 14.0, - fontFamily: 'Jost', - ), + news.content, + style: const TextStyle( + fontSize: 14.0, + fontFamily: 'Jost', + ), ), ], ), @@ -313,6 +119,34 @@ class _NewsScreenState extends State { } } +// Примерные данные новостей +final List _newsItems = [ + NewsItem( + title: 'Заголовок к новости', + content: 'Текст новости', + date: '12.05.2023', + fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', + ), + NewsItem( + title: 'Заголовок к новости', + content: 'Текст новости', + date: '10.05.2023', + fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', + ), + NewsItem( + title: 'Заголовок к новости', + content: 'Текст новости', + date: '08.05.2023', + fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', + ), + NewsItem( + title: 'Заголовок к новости', + content: 'Текст новости', + date: '05.05.2023', + fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', + ), +]; + // Класс для хранения данных новости class NewsItem { final String title; @@ -326,4 +160,4 @@ class NewsItem { required this.date, required this.fullContent, }); -} \ No newline at end of file +} \ No newline at end of file From 5cd6ccd3259de2ac933c8ed1c7c38dc55307a286 Mon Sep 17 00:00:00 2001 From: birbik Date: Sat, 31 May 2025 21:30:03 +0300 Subject: [PATCH 38/54] init commit --- frontend/lib/models/achievement_model.dart | 50 +- frontend/lib/models/card_model.dart | 48 +- frontend/lib/models/news_model.dart | 35 ++ frontend/lib/models/notification_model.dart | 43 ++ frontend/lib/models/quest_model.dart | 47 ++ frontend/lib/models/shop_model.dart | 80 +++ frontend/lib/models/theme_model.dart | 25 + frontend/lib/models/trade_model.dart | 35 ++ frontend/lib/services/api_service.dart | 559 ++++++++++++++++++- frontend/lib/utils/error_formatter.dart | 33 ++ frontend/lib/views/achievements_screen.dart | 190 +++---- frontend/lib/views/card_detail_screen.dart | 402 +++++--------- frontend/lib/views/exchanges_screen.dart | 67 +++ frontend/lib/views/inventory_screen.dart | 345 ++++-------- frontend/lib/views/news_detail_screen.dart | 151 ++--- frontend/lib/views/news_screen.dart | 109 +++- frontend/lib/views/notifications_modal.dart | 124 +++-- frontend/lib/views/pack_content_screen.dart | 120 +--- frontend/lib/views/profile_screen.dart | 112 +++- frontend/lib/views/quests_screen.dart | 139 +++++ frontend/lib/views/shop_screen.dart | 583 ++++++-------------- 21 files changed, 1888 insertions(+), 1409 deletions(-) create mode 100644 frontend/lib/models/news_model.dart create mode 100644 frontend/lib/models/notification_model.dart create mode 100644 frontend/lib/models/quest_model.dart create mode 100644 frontend/lib/models/theme_model.dart create mode 100644 frontend/lib/models/trade_model.dart create mode 100644 frontend/lib/utils/error_formatter.dart diff --git a/frontend/lib/models/achievement_model.dart b/frontend/lib/models/achievement_model.dart index 796475a..d713c5e 100644 --- a/frontend/lib/models/achievement_model.dart +++ b/frontend/lib/models/achievement_model.dart @@ -1,43 +1,47 @@ class Achievement { - final String id; - final String title; + final int achievement_ID; + final String name; final String description; - final String requirement; - final String iconPath; + final String imageURL; + final int progress; + final int maxProgress; final bool isCompleted; - final double progress; + final bool isFavorite; Achievement({ - required this.id, - required this.title, + required this.achievement_ID, + required this.name, required this.description, - required this.requirement, - required this.iconPath, - this.isCompleted = false, - this.progress = 0.0, + required this.imageURL, + required this.progress, + required this.maxProgress, + required this.isCompleted, + required this.isFavorite, }); factory Achievement.fromJson(Map json) { return Achievement( - id: json['id'] as String, - title: json['title'] as String, - description: json['description'] as String, - requirement: json['requirement'] as String, - iconPath: json['iconPath'] as String, - isCompleted: json['isCompleted'] as bool? ?? false, - progress: (json['progress'] as num?)?.toDouble() ?? 0.0, + achievement_ID: json['achievement_ID'], + name: json['name'], + description: json['description'], + imageURL: json['imageURL'], + progress: json['progress'], + maxProgress: json['maxProgress'], + isCompleted: json['isCompleted'], + isFavorite: json['isFavorite'], ); } Map toJson() { return { - 'id': id, - 'title': title, + 'achievement_ID': achievement_ID, + 'name': name, 'description': description, - 'requirement': requirement, - 'iconPath': iconPath, - 'isCompleted': isCompleted, + 'imageURL': imageURL, 'progress': progress, + 'maxProgress': maxProgress, + 'isCompleted': isCompleted, + 'isFavorite': isFavorite, }; } } \ No newline at end of file diff --git a/frontend/lib/models/card_model.dart b/frontend/lib/models/card_model.dart index b63233e..578cccc 100644 --- a/frontend/lib/models/card_model.dart +++ b/frontend/lib/models/card_model.dart @@ -1,51 +1,43 @@ class CardModel { - final int id; + final int card_ID; final String name; final String description; - final String imageUrl; - final int rarity; - final String collection; - final String type; - final int disassemblePrice; - final int quantity; + final String imageURL; + final String rarity; + final bool isInCollection; + final DateTime dateObtained; CardModel({ - required this.id, + required this.card_ID, required this.name, required this.description, - required this.imageUrl, + required this.imageURL, required this.rarity, - required this.collection, - required this.type, - required this.disassemblePrice, - required this.quantity, + this.isInCollection = false, + required this.dateObtained, }); factory CardModel.fromJson(Map json) { return CardModel( - id: json['id'] as int, - name: json['name'] as String, - description: json['description'] as String, - imageUrl: json['imageUrl'] as String, - rarity: json['rarity'] as int, - collection: json['collection'] as String, - type: json['type'] as String, - disassemblePrice: json['disassemblePrice'] as int, - quantity: json['quantity'] as int? ?? 1, + card_ID: json['card_ID'], + name: json['name'], + description: json['description'], + imageURL: json['imageURL'], + rarity: json['rarity'], + isInCollection: json['isInCollection'] ?? false, + dateObtained: DateTime.parse(json['dateObtained']), ); } Map toJson() { return { - 'id': id, + 'card_ID': card_ID, 'name': name, 'description': description, - 'imageUrl': imageUrl, + 'imageURL': imageURL, 'rarity': rarity, - 'collection': collection, - 'type': type, - 'disassemblePrice': disassemblePrice, - 'quantity': quantity, + 'isInCollection': isInCollection, + 'dateObtained': dateObtained.toIso8601String(), }; } } \ No newline at end of file diff --git a/frontend/lib/models/news_model.dart b/frontend/lib/models/news_model.dart new file mode 100644 index 0000000..f3ff519 --- /dev/null +++ b/frontend/lib/models/news_model.dart @@ -0,0 +1,35 @@ +class News { + final int news_ID; + final String title; + final String content; + final String imageURL; + final DateTime publishDate; + + News({ + required this.news_ID, + required this.title, + required this.content, + required this.imageURL, + required this.publishDate, + }); + + factory News.fromJson(Map json) { + return News( + news_ID: json['news_ID'], + title: json['title'], + content: json['content'], + imageURL: json['imageURL'], + publishDate: DateTime.parse(json['publishDate']), + ); + } + + Map toJson() { + return { + 'news_ID': news_ID, + 'title': title, + 'content': content, + 'imageURL': imageURL, + 'publishDate': publishDate.toIso8601String(), + }; + } +} \ No newline at end of file diff --git a/frontend/lib/models/notification_model.dart b/frontend/lib/models/notification_model.dart new file mode 100644 index 0000000..5f367ec --- /dev/null +++ b/frontend/lib/models/notification_model.dart @@ -0,0 +1,43 @@ +class Notification { + final int notification_ID; + final String title; + final String message; + final String type; + final DateTime timestamp; + final bool isRead; + final String? redirectURL; + + Notification({ + required this.notification_ID, + required this.title, + required this.message, + required this.type, + required this.timestamp, + required this.isRead, + this.redirectURL, + }); + + factory Notification.fromJson(Map json) { + return Notification( + notification_ID: json['notification_ID'], + title: json['title'], + message: json['message'], + type: json['type'], + timestamp: DateTime.parse(json['timestamp']), + isRead: json['isRead'], + redirectURL: json['redirectURL'], + ); + } + + Map toJson() { + return { + 'notification_ID': notification_ID, + 'title': title, + 'message': message, + 'type': type, + 'timestamp': timestamp.toIso8601String(), + 'isRead': isRead, + 'redirectURL': redirectURL, + }; + } +} \ No newline at end of file diff --git a/frontend/lib/models/quest_model.dart b/frontend/lib/models/quest_model.dart new file mode 100644 index 0000000..365d536 --- /dev/null +++ b/frontend/lib/models/quest_model.dart @@ -0,0 +1,47 @@ +class Quest { + final int quest_ID; + final String title; + final String description; + final String type; // 'daily' или 'weekly' + final int progress; + final int maxProgress; + final int reward; + final bool isCompleted; + + Quest({ + required this.quest_ID, + required this.title, + required this.description, + required this.type, + required this.progress, + required this.maxProgress, + required this.reward, + required this.isCompleted, + }); + + factory Quest.fromJson(Map json) { + return Quest( + quest_ID: json['quest_ID'], + title: json['title'], + description: json['description'], + type: json['type'], + progress: json['progress'], + maxProgress: json['maxProgress'], + reward: json['reward'], + isCompleted: json['isCompleted'], + ); + } + + Map toJson() { + return { + 'quest_ID': quest_ID, + 'title': title, + 'description': description, + 'type': type, + 'progress': progress, + 'maxProgress': maxProgress, + 'reward': reward, + 'isCompleted': isCompleted, + }; + } +} \ No newline at end of file diff --git a/frontend/lib/models/shop_model.dart b/frontend/lib/models/shop_model.dart index a99bd30..e42f2bb 100644 --- a/frontend/lib/models/shop_model.dart +++ b/frontend/lib/models/shop_model.dart @@ -109,4 +109,84 @@ class PurchaseCoinsResponse { paymentUrl: json['paymentUrl'] as String, ); } +} + +class Pack { + final int pack_ID; + final String name; + final String description; + final String imageURL; + final int price; + final int cardsCount; + final bool isAvailable; + + Pack({ + required this.pack_ID, + required this.name, + required this.description, + required this.imageURL, + required this.price, + required this.cardsCount, + this.isAvailable = true, + }); + + factory Pack.fromJson(Map json) { + return Pack( + pack_ID: json['pack_ID'], + name: json['name'], + description: json['description'], + imageURL: json['imageURL'], + price: json['price'], + cardsCount: json['cardsCount'], + isAvailable: json['isAvailable'] ?? true, + ); + } + + Map toJson() { + return { + 'pack_ID': pack_ID, + 'name': name, + 'description': description, + 'imageURL': imageURL, + 'price': price, + 'cardsCount': cardsCount, + 'isAvailable': isAvailable, + }; + } +} + +class CoinOffer { + final int offer_ID; + final int coins; + final double price; + final bool isPopular; + final bool isBestValue; + + CoinOffer({ + required this.offer_ID, + required this.coins, + required this.price, + this.isPopular = false, + this.isBestValue = false, + }); + + factory CoinOffer.fromJson(Map json) { + return CoinOffer( + offer_ID: json['offer_ID'], + coins: json['coins'], + price: json['price'].toDouble(), + isPopular: json['isPopular'] ?? false, + isBestValue: json['isBestValue'] ?? false, + ); + } + + Map toJson() { + return { + 'offer_ID': offer_ID, + 'coins': coins, + 'price': price, + 'isPopular': isPopular, + 'isBestValue': isBestValue, + }; + } } \ No newline at end of file diff --git a/frontend/lib/models/theme_model.dart b/frontend/lib/models/theme_model.dart new file mode 100644 index 0000000..fdfd04a --- /dev/null +++ b/frontend/lib/models/theme_model.dart @@ -0,0 +1,25 @@ +class Theme { + final int theme_ID; + final String name; + final String description; + final String imageURL; + final int cost; + + Theme({ + required this.theme_ID, + required this.name, + required this.description, + required this.imageURL, + required this.cost, + }); + + factory Theme.fromJson(Map json) { + return Theme( + theme_ID: json['theme_ID'], + name: json['name'], + description: json['description'], + imageURL: json['imageURL'], + cost: json['cost'], + ); + } +} \ No newline at end of file diff --git a/frontend/lib/models/trade_model.dart b/frontend/lib/models/trade_model.dart new file mode 100644 index 0000000..0d33106 --- /dev/null +++ b/frontend/lib/models/trade_model.dart @@ -0,0 +1,35 @@ +import 'card_model.dart'; + +class Trade { + final int trade_ID; + final int initiatorID; + final String initiatorUsername; + final CardModel offeredCard; + final List requestedCards; + final String status; // 'pending', 'accepted', 'rejected', 'cancelled' + final DateTime createdAt; + + Trade({ + required this.trade_ID, + required this.initiatorID, + required this.initiatorUsername, + required this.offeredCard, + required this.requestedCards, + required this.status, + required this.createdAt, + }); + + factory Trade.fromJson(Map json) { + return Trade( + trade_ID: json['trade_ID'], + initiatorID: json['initiatorID'], + initiatorUsername: json['initiatorUsername'], + offeredCard: CardModel.fromJson(json['offeredCard']), + requestedCards: (json['requestedCards'] as List) + .map((card) => CardModel.fromJson(card)) + .toList(), + status: json['status'], + createdAt: DateTime.parse(json['createdAt']), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/services/api_service.dart b/frontend/lib/services/api_service.dart index 2cb6c35..b2f91c3 100644 --- a/frontend/lib/services/api_service.dart +++ b/frontend/lib/services/api_service.dart @@ -4,6 +4,12 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../models/user_model.dart'; import '../models/card_model.dart'; import '../models/shop_model.dart'; +import '../models/theme_model.dart' as app_theme; +import '../models/achievement_model.dart'; +import '../models/news_model.dart'; +import '../models/quest_model.dart'; +import '../models/notification_model.dart'; +import '../models/trade_model.dart'; class ApiService { static const String baseUrl = 'http://87.236.23.130:8080/api'; @@ -368,114 +374,617 @@ class ApiService { } // Магазин - Future> getAllPacks() async { + Future> getAllPacks() async { + try { + final response = await http.get(Uri.parse('$baseUrl/shop/packs')); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => Pack.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении списка наборов: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future getPackDetails(int packId) async { + try { + final response = await http.get( + Uri.parse('$baseUrl/shop/packs/$packId'), + ); + + if (response.statusCode == 200) { + return Pack.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при получении деталей набора: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> buyPack(int packId) async { try { final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/shop/packs/$packId/buy'), + headers: headers, + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else if (response.statusCode == 402) { + final error = json.decode(response.body); + throw Exception('Недостаточно средств. Требуется: ${error['requiredAmount']}'); + } else { + throw Exception('Ошибка при покупке набора: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> openPack(int packId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/shop/packs/$packId/open'), + headers: headers, + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Ошибка при открытии набора: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> getCoinOffers() async { + try { final response = await http.get( - Uri.parse('$shopUrl/packs'), + Uri.parse('$baseUrl/shop/coins/offers'), + ); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => CoinOffer.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении предложений монет: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future getCoinOfferDetails(int offerId) async { + try { + final response = await http.get( + Uri.parse('$baseUrl/shop/coins/offers/$offerId'), + ); + + if (response.statusCode == 200) { + return CoinOffer.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при получении деталей предложения: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future purchaseCoins(int offerId, String redirectUrl) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/shop/coins/offers/$offerId/purchase'), + headers: headers, + body: json.encode({'redirectUrl': redirectUrl}), + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return data['paymentUrl']; + } else { + throw Exception('Ошибка при покупке монет: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + // Генерация карт + Future> getCardGenerationThemes() async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$baseUrl/home/generate-card/themes'), headers: headers, ); if (response.statusCode == 200) { final List jsonList = json.decode(response.body); - return jsonList.map((json) => PackModel.fromJson(json)).toList(); + return jsonList.map((json) => app_theme.Theme.fromJson(json)).toList(); } else { - throw Exception('Ошибка при получении списка наборов: ${response.body}'); + throw Exception('Ошибка при получении тем: ${response.body}'); } } catch (e) { throw Exception('Ошибка сети: $e'); } } - Future getPackDetails(int packId) async { + Future> generateCard(int themeId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/home/generate-card'), + headers: headers, + body: json.encode({'theme_ID': themeId}), + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else if (response.statusCode == 402) { + final error = json.decode(response.body); + throw Exception('Недостаточно средств. Требуется: ${error['requiredAmount']}'); + } else { + throw Exception('Ошибка при генерации карты: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + // Профиль + Future> getProfile() async { try { final headers = await getHeaders(); final response = await http.get( - Uri.parse('$shopUrl/packs/$packId'), + Uri.parse('$baseUrl/profile'), headers: headers, ); if (response.statusCode == 200) { - return PackModel.fromJson(json.decode(response.body)); + return json.decode(response.body); } else { - throw Exception('Ошибка при получении деталей набора: ${response.body}'); + throw Exception('Ошибка при получении профиля: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> getAchievements() async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$baseUrl/profile/achievements'), + headers: headers, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return (data['achievements'] as List) + .map((json) => Achievement.fromJson(json)) + .toList(); + } else { + throw Exception('Ошибка при получении достижений: ${response.body}'); } } catch (e) { throw Exception('Ошибка сети: $e'); } } - Future buyPack(int packId) async { + Future addAchievementToFavorites(int achievementId) async { try { final headers = await getHeaders(); final response = await http.post( - Uri.parse('$shopUrl/packs/$packId/buy'), + Uri.parse('$baseUrl/profile/achievements/$achievementId/favorite-add'), + headers: headers, + ); + + return response.statusCode == 200; + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future removeAchievementFromFavorites(int achievementId) async { + try { + final headers = await getHeaders(); + final response = await http.delete( + Uri.parse('$baseUrl/profile/achievements/$achievementId/favorite-delete'), + headers: headers, + ); + + return response.statusCode == 200; + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> getProfileSettings() async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$baseUrl/profile/settings'), headers: headers, ); if (response.statusCode == 200) { - return PurchasePackResponse.fromJson(json.decode(response.body)); + return json.decode(response.body); } else { - throw Exception('Ошибка при покупке набора: ${response.body}'); + throw Exception('Ошибка при получении настроек: ${response.body}'); } } catch (e) { throw Exception('Ошибка сети: $e'); } } - Future> getAllCoinOffers() async { + Future> updateProfileSettings(Map settings) async { try { final headers = await getHeaders(); + final response = await http.put( + Uri.parse('$baseUrl/profile/settings-change'), + headers: headers, + body: json.encode(settings), + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Ошибка при обновлении настроек: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + // Другие профили + Future> getOtherProfile(int userId) async { + try { + final response = await http.get( + Uri.parse('$baseUrl/other-profile/$userId'), + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Ошибка при получении профиля: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> getOtherProfileAchievements(int userId) async { + try { final response = await http.get( - Uri.parse('$shopUrl/coins/offers'), + Uri.parse('$baseUrl/other-profile/$userId/achievements'), + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return (data['achievements'] as List) + .map((json) => Achievement.fromJson(json)) + .toList(); + } else { + throw Exception('Ошибка при получении достижений: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> getOtherProfileInventory(int userId) async { + try { + final response = await http.get( + Uri.parse('$baseUrl/other-profile/$userId/inventory'), + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else if (response.statusCode == 403) { + throw Exception('Инвентарь скрыт'); + } else { + throw Exception('Ошибка при получении инвентаря: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future reportUser(int userId, String reason, {String? comment}) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/other-profile/$userId/report'), headers: headers, + body: json.encode({ + 'reason': reason, + if (comment != null) 'comment': comment, + }), ); + return response.statusCode == 200; + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + // Новости + Future> getNews() async { + try { + final response = await http.get(Uri.parse('$baseUrl/home/news')); + if (response.statusCode == 200) { final List jsonList = json.decode(response.body); - return jsonList.map((json) => CoinOfferModel.fromJson(json)).toList(); + return jsonList.map((json) => News.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении новостей: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future getNewsDetails(int newsId) async { + try { + final response = await http.get(Uri.parse('$baseUrl/home/news/$newsId')); + + if (response.statusCode == 200) { + return News.fromJson(json.decode(response.body)); } else { - throw Exception('Ошибка при получении списка предложений монет: ${response.body}'); + throw Exception('Ошибка при получении новости: ${response.body}'); } } catch (e) { throw Exception('Ошибка сети: $e'); } } - Future getCoinOfferDetails(int offerId) async { + // Квесты + Future>> getQuests() async { try { final headers = await getHeaders(); final response = await http.get( - Uri.parse('$shopUrl/coins/offers/$offerId'), + Uri.parse('$baseUrl/home/quests'), headers: headers, ); if (response.statusCode == 200) { - return CoinOfferModel.fromJson(json.decode(response.body)); + final data = json.decode(response.body); + return { + 'dailyQuests': (data['dailyQuests'] as List) + .map((json) => Quest.fromJson(json)) + .toList(), + 'weeklyQuests': (data['weeklyQuests'] as List) + .map((json) => Quest.fromJson(json)) + .toList(), + }; } else { - throw Exception('Ошибка при получении деталей предложения монет: ${response.body}'); + throw Exception('Ошибка при получении квестов: ${response.body}'); } } catch (e) { throw Exception('Ошибка сети: $e'); } } - Future purchaseCoins(int offerId, String redirectUrl) async { + Future changeQuestStatus(int questId) async { try { final headers = await getHeaders(); final response = await http.post( - Uri.parse('$shopUrl/coins/offers/$offerId/purchase'), + Uri.parse('$baseUrl/home/quests/$questId/change-status'), headers: headers, - body: json.encode({'redirectUrl': redirectUrl}), ); if (response.statusCode == 200) { - return PurchaseCoinsResponse.fromJson(json.decode(response.body)); + final data = json.decode(response.body); + return Quest.fromJson(data['quest']); } else { - throw Exception('Ошибка при покупке монет: ${response.body}'); + throw Exception('Ошибка при изменении статуса квеста: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> claimQuestReward(String questType) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/home/quests/claim-reward'), + headers: headers, + body: json.encode({'questType': questType}), + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Ошибка при получении награды: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + // Уведомления + Future> getNotifications() async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$baseUrl/home/notifications'), + headers: headers, + ); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => Notification.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении уведомлений: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future getNotificationDetails(int notificationId) async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$baseUrl/home/notifications/$notificationId'), + headers: headers, + ); + + if (response.statusCode == 200) { + return Notification.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при получении уведомления: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future navigateToNotification(int notificationId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/home/notifications/$notificationId/navigate'), + headers: headers, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return data['redirectUrl']; + } else { + throw Exception('Ошибка при переходе по уведомлению: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + // Обмен картами + Future> getAllTrades({String? search}) async { + try { + String url = '$baseUrl/trades'; + if (search != null && search.isNotEmpty) { + url += '?search=$search'; + } + + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => Trade.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении списка обменов: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future getTradeDetails(int tradeId) async { + try { + final response = await http.get( + Uri.parse('$baseUrl/trades/$tradeId'), + ); + + if (response.statusCode == 200) { + return Trade.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при получении деталей обмена: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future initiateTrade(int offeredCardId, List requestedCardIds) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/trades/initiate'), + headers: headers, + body: json.encode({ + 'offeredCardId': offeredCardId, + 'requestedCardId': requestedCardIds, + }), + ); + + if (response.statusCode == 201) { + return Trade.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при создании обмена: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> getMyTrades() async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$baseUrl/trades/my'), + headers: headers, + ); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => Trade.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении моих обменов: ${response.body}'); } } catch (e) { throw Exception('Ошибка сети: $e'); } } + + Future acceptTrade(int tradeId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/trades/$tradeId/accept'), + headers: headers, + ); + + return response.statusCode == 200; + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future rejectTrade(int tradeId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/trades/$tradeId/reject'), + headers: headers, + ); + + return response.statusCode == 200; + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future cancelTrade(int tradeId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/trades/$tradeId/cancel'), + headers: headers, + ); + + return response.statusCode == 200; + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } } \ No newline at end of file diff --git a/frontend/lib/utils/error_formatter.dart b/frontend/lib/utils/error_formatter.dart new file mode 100644 index 0000000..7c0a049 --- /dev/null +++ b/frontend/lib/utils/error_formatter.dart @@ -0,0 +1,33 @@ +class ErrorFormatter { + static String formatError(dynamic error) { + final errorStr = error.toString().toLowerCase(); + + if (errorStr.contains('socketconnection') || + errorStr.contains('connection refused') || + errorStr.contains('network is unreachable') || + errorStr.contains('connection timed out')) { + return 'Ошибка сети. Проверьте подключение к интернету'; + } + + if (errorStr.contains('unauthorized') || errorStr.contains('unauthenticated')) { + return 'Необходима авторизация'; + } + + if (errorStr.contains('not found')) { + return 'Запрашиваемые данные не найдены'; + } + + if (errorStr.contains('permission') || errorStr.contains('forbidden')) { + return 'Нет доступа к запрашиваемым данным'; + } + + // Если это ошибка от нашего API (которую мы специально выбросили с понятным сообщением) + if (error is Exception && !errorStr.contains('exception')) { + // Убираем слово "Exception" из сообщения + return error.toString().replaceAll('Exception: ', ''); + } + + // Для всех остальных случаев + return 'Произошла ошибка. Попробуйте позже'; + } +} \ No newline at end of file diff --git a/frontend/lib/views/achievements_screen.dart b/frontend/lib/views/achievements_screen.dart index fe483f4..82943a1 100644 --- a/frontend/lib/views/achievements_screen.dart +++ b/frontend/lib/views/achievements_screen.dart @@ -1,130 +1,82 @@ import 'package:flutter/material.dart'; import '../models/achievement_model.dart'; +import '../services/api_service.dart'; +import '../utils/error_formatter.dart'; -class AchievementsScreen extends StatelessWidget { - const AchievementsScreen({super.key}); +class AchievementsScreen extends StatefulWidget { + final bool isOtherUser; + final int userId; + + const AchievementsScreen({ + Key? key, + required this.isOtherUser, + required this.userId + }) : super(key: key); @override - Widget build(BuildContext context) { - final List achievements = List.generate( - 10, - (index) => Achievement( - id: 'achievement_$index', - title: 'Достижение ${index + 1}', - description: 'Описание достижения ${index + 1}', - requirement: 'что нужно для достижения', - iconPath: 'assets/icons/достижение.png', - isCompleted: false, - progress: 0.0, - ), - ); + _AchievementsScreenState createState() => _AchievementsScreenState(); +} + +class _AchievementsScreenState extends State { + late bool _isLoading; + late List _achievements; + late String _error; + + @override + void initState() { + super.initState(); + _isLoading = true; + _achievements = []; + _error = ''; + _loadAchievements(); + } + Future _loadAchievements() async { + try { + setState(() { + _isLoading = true; + }); + + final achievements = widget.isOtherUser + ? await ApiService().getOtherProfileAchievements(widget.userId) + : await ApiService().getAchievements(); + + setState(() { + _achievements = achievements; + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } + } + + @override + Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFBF6EF), // Бежевый фон - body: SafeArea( - child: Column( - children: [ - // Back button and title - Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - Container( - width: 40.0, - height: 40.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: const Color(0xFFD6A067), - ), - child: InkWell( - borderRadius: BorderRadius.circular(10.0), - onTap: () { - Navigator.pop(context); - }, - child: const Icon( - Icons.arrow_back, - color: Colors.black, - size: 29.0, - ), - ), - ), - const SizedBox(width: 16), - const Text( - 'Достижения', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - ), - ], - ), - ), - // Achievements list - Expanded( - child: ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16), - itemCount: achievements.length, - itemBuilder: (context, index) { - final achievement = achievements[index]; - return Card( - margin: const EdgeInsets.only(bottom: 8), - color: const Color(0xFFEAD7C3), - elevation: 0, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - // Achievement icon using asset - Container( - width: 40, - height: 40, - decoration: const BoxDecoration( - shape: BoxShape.circle, - ), - child: Image.asset( - achievement.iconPath, - width: 40, - height: 40, - fit: BoxFit.contain, - ), - ), - const SizedBox(width: 16), - // Achievement details - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - achievement.title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - ), - const SizedBox(height: 4), - Text( - achievement.requirement, - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - fontFamily: 'Jost', - ), - ), - ], - ), - ), - ], - ), - ), - ); - }, - ), - ), - ], - ), + appBar: AppBar( + title: Text('Достижения${widget.isOtherUser ? ' игрока' : ''}'), ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _error.isNotEmpty + ? Center(child: Text(_error)) + : ListView.builder( + itemCount: _achievements.length, + itemBuilder: (context, index) { + final achievement = _achievements[index]; + return ListTile( + leading: Image.network(achievement.imageURL), + title: Text(achievement.name), + subtitle: Text(achievement.description), + trailing: achievement.isCompleted + ? const Icon(Icons.check_circle, color: Colors.green) + : const Icon(Icons.lock, color: Colors.grey), + ); + }, + ), ); } } diff --git a/frontend/lib/views/card_detail_screen.dart b/frontend/lib/views/card_detail_screen.dart index 8db2712..c9e7346 100644 --- a/frontend/lib/views/card_detail_screen.dart +++ b/frontend/lib/views/card_detail_screen.dart @@ -1,15 +1,20 @@ import 'package:flutter/material.dart'; +import '../models/card_model.dart'; import 'create_exchange_screen.dart'; +import '../services/api_service.dart'; +import '../utils/error_formatter.dart'; class CardDetailScreen extends StatefulWidget { + final CardModel card; final int cardIndex; final bool showExchangeButton; final bool isFromShop; final bool showFavoriteButton; const CardDetailScreen({ - Key? key, - required this.cardIndex, + Key? key, + required this.card, + required this.cardIndex, this.showExchangeButton = false, this.isFromShop = false, this.showFavoriteButton = true, @@ -24,303 +29,156 @@ class _CardDetailScreenState extends State { @override Widget build(BuildContext context) { - // Данные для карточек - final List> cardData = [ - { - 'name': 'Название', - 'desc': 'Описание карточки', - 'type': 'Тип', - 'rarity': '4', - }, - // ... можно добавить больше заглушек ... - ]; - final data = (widget.cardIndex < cardData.length) ? cardData[widget.cardIndex] : cardData[0]; - final String name = data['name']!; - final String description = data['desc']!; - final String type = data['type']!; - final int rarity = int.tryParse(data['rarity'] ?? '0') ?? 0; - return Scaffold( - backgroundColor: const Color(0xFFFBF6EF), - body: SafeArea( - child: Stack( + appBar: AppBar( + title: Text(widget.card.name), + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: InkWell( - onTap: () => Navigator.pop(context), - borderRadius: BorderRadius.circular(20.0), - child: Container( - width: 40.0, - height: 40.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: const Color(0xFFD6A067), - ), - child: const Icon( - Icons.arrow_back, - color: Colors.black, - size: 29.0, - ), - ), + Image.network(widget.card.imageURL), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.card.name, + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 8), + Text( + 'Редкость: ${widget.card.rarity}', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 16), + Text( + widget.card.description, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 32.0, left: 16.0, right: 16.0, top: 8.0), + child: widget.showExchangeButton + ? ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), ), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), ), - ], - ), - Expanded( - child: Center( - child: Container( - width: 340, - height: 520, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(18.0), - border: Border.all(color: Colors.black, width: 6), - ), - child: Container( - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(14.0), - border: Border.all(color: Colors.black, width: 3), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - flex: 3, // 5% - child: Center( - child: Text( - name, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.w500, - color: Colors.black, - fontFamily: 'Jost', - ), - ), - ), - ), - Expanded( - flex: 36, // 60% - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 0.0, vertical: 4.0), - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 3), - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(10), - ), - child: const Center( - child: Text( - 'Нет изображения', - style: TextStyle(color: Colors.black45, fontSize: 16, fontFamily: 'Jost'), - ), - ), - ), - ), - Expanded( - flex: 18, // 30% - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0), - child: Text( - description, - style: const TextStyle(fontSize: 17, color: Colors.black, fontFamily: 'Jost'), - textAlign: TextAlign.left, - ), + onPressed: () {}, + child: const Text( + 'Предложить обмен', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400, fontFamily: 'Roboto'), + ), + ) + : widget.isFromShop + ? const SizedBox.shrink() + : Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), ), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), ), - Expanded( - flex: 3, // 5% - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 0.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, + onPressed: _disassembleCard, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Row( - children: List.generate( - rarity, - (index) => Padding( - padding: const EdgeInsets.only(right: 4.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 24, - ), - ), - ), - ), - const Spacer(), - Text( - type, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - color: Colors.black, - fontFamily: 'Jost', - ), + const Text( + 'Разобрать', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400, fontFamily: 'Roboto'), ), + SizedBox(width: 6), ], ), - ), - ), - ], - ), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 32.0, left: 16.0, right: 16.0, top: 8.0), - child: widget.showExchangeButton - ? ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - ), - onPressed: () {}, - child: const Text( - 'Предложить обмен', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400, fontFamily: 'Roboto'), - ), - ) - : widget.isFromShop - ? const SizedBox.shrink() - : Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), - ), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - ), - onPressed: () { - // TODO: Implement logic to disassemble the card and add coins. - // 1. Get the current coin balance. - // 2. Add 150 coins to the balance. - // 3. Update the coin balance in your state management or data source. - - // After disassembling, navigate back to the inventory screen. - Navigator.pop(context); // Assumes CardDetailScreen was pushed from InventoryScreen - }, - child: Column( + Row( mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text( - 'Разобрать', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400, fontFamily: 'Roboto'), - ), - SizedBox(width: 6), - ], + const Text( + '150', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w400, + fontFamily: 'Roboto', + ), + textAlign: TextAlign.center, ), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - '150', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w400, - fontFamily: 'Roboto', - ), - textAlign: TextAlign.center, - ), - SizedBox(width: 4), - Image.asset( - 'assets/icons/монеты.png', - height: 18, - ), - ], + SizedBox(width: 4), + Image.asset( + 'assets/icons/монеты.png', + height: 18, ), ], ), + ], + ), + ), + ), + SizedBox(width: 16), + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), ), + padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 25), ), - SizedBox(width: 16), - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CreateExchangeScreen( + cardId: widget.cardIndex, ), - padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 25), ), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CreateExchangeScreen( - cardId: widget.cardIndex, - ), - ), - ); - }, - child: const Text( - 'Обменять', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400, fontFamily: 'Roboto'), - ), - ), + ); + }, + child: const Text( + 'Обменять', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400, fontFamily: 'Roboto'), ), - ], + ), ), - ), - ], - ), - Positioned( - top: 111, - right: -4, - child: widget.showFavoriteButton ? InkWell( - onTap: () { - setState(() { - isFavorite = !isFavorite; - print('isFavorite: $isFavorite'); - - // TODO: Integrate your favorite state management here. - // If isFavorite is true, add widget.cardIndex to the user's favorite list. - // If isFavorite is false, remove widget.cardIndex from the user's favorite list. - // Example (using a hypothetical function saveFavoriteCardId and removeFavoriteCardId): - // if (isFavorite) { - // saveFavoriteCardId(widget.cardIndex); - // } else { - // removeFavoriteCardId(widget.cardIndex); - // } - }); - }, - child: SizedBox( - width: 60.0, - height: 60.0, - child: Center( - child: Icon( - isFavorite ? Icons.star : Icons.star_border, - color: Colors.black, - size: 46.0, + ], ), - ), - ), - ) : const SizedBox.shrink(), ), ], ), ), ); } + + Future _disassembleCard() async { + try { + final coinsReceived = await ApiService().disassembleCard(widget.card.card_ID); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Получено монет: $coinsReceived')), + ); + Navigator.pop(context); // Возвращаемся на предыдущий экран + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } } \ No newline at end of file diff --git a/frontend/lib/views/exchanges_screen.dart b/frontend/lib/views/exchanges_screen.dart index 3364266..5de3b0b 100644 --- a/frontend/lib/views/exchanges_screen.dart +++ b/frontend/lib/views/exchanges_screen.dart @@ -4,6 +4,9 @@ import 'create_exchange_screen.dart'; import 'profile_screen.dart'; import 'search_players_screen.dart'; import 'exchange_proposal_screen.dart'; +import '../services/api_service.dart'; +import '../models/trade_model.dart'; +import '../utils/error_formatter.dart'; class ExchangesScreen extends StatefulWidget { final String? notification; @@ -18,6 +21,8 @@ class _ExchangesScreenState extends State with SingleTickerProv late TabController _tabController; String _sortOption = 'По дате'; bool _showSortOptions = false; + bool _isLoading = false; + String? _error; @override void initState() { @@ -644,6 +649,68 @@ class _ExchangesScreenState extends State with SingleTickerProv ], ); } + + Future _loadTrades() async { + try { + setState(() { + _isLoading = true; + }); + // Implement the logic to load trades from the API + // This is a placeholder and should be replaced with actual implementation + // For example, you can use a Future.delayed to simulate a delay + await Future.delayed(Duration(seconds: 2)); + setState(() { + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } + } + + Future _acceptTrade(Trade trade) async { + try { + await ApiService().acceptTrade(trade.trade_ID); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Обмен принят')), + ); + _loadTrades(); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + + Future _rejectTrade(Trade trade) async { + try { + await ApiService().rejectTrade(trade.trade_ID); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Обмен отклонен')), + ); + _loadTrades(); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + + Future _cancelTrade(Trade trade) async { + try { + await ApiService().cancelTrade(trade.trade_ID); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Обмен отменен')), + ); + _loadTrades(); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } } enum ExchangeStatus { diff --git a/frontend/lib/views/inventory_screen.dart b/frontend/lib/views/inventory_screen.dart index e7ccb43..feb8d5e 100644 --- a/frontend/lib/views/inventory_screen.dart +++ b/frontend/lib/views/inventory_screen.dart @@ -2,11 +2,14 @@ import 'package:flutter/material.dart'; import 'profile_screen.dart'; import 'card_detail_screen.dart'; import 'search_players_screen.dart'; +import '../services/api_service.dart'; +import '../models/card_model.dart'; +import '../utils/error_formatter.dart'; class InventoryScreen extends StatefulWidget { final bool isOtherUser; final String? playerName; - final String? playerId; + final int? playerId; final String? collectionName; final bool isFromShop; @@ -24,9 +27,70 @@ class InventoryScreen extends StatefulWidget { } class _InventoryScreenState extends State { - String _sortOption = 'По редкости'; - bool _showSortOptions = false; - + late bool _isLoading; + late List _cards; + late String _error; + String _sortBy = 'rarity'; + + @override + void initState() { + super.initState(); + _isLoading = true; + _cards = []; + _error = ''; + _loadInventory(); + } + + Future _loadInventory() async { + try { + setState(() { + _isLoading = true; + }); + + final cards = widget.isOtherUser + ? await ApiService().getOtherProfileInventory(widget.playerId ?? 0) + : await ApiService().getUserInventory(sortBy: _sortBy); + + setState(() { + _cards = (cards as List).map((item) => CardModel.fromJson(item)).toList(); + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } + } + + Future _disassembleCard(CardModel card) async { + try { + final coinsReceived = await ApiService().disassembleCard(card.card_ID); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Получено монет: $coinsReceived')), + ); + await _loadInventory(); // Перезагружаем инвентарь + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + + void _showCardDetails(CardModel card) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CardDetailScreen( + card: card, + cardIndex: 0, + showExchangeButton: false, + isFromShop: false, + ), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -167,241 +231,54 @@ class _InventoryScreenState extends State { ), ], ), - body: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.collectionName == null) // Only show sorting if not viewing a collection - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: Row( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - child: InkWell( - onTap: () { - setState(() { - _showSortOptions = !_showSortOptions; - }); - }, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Сортировка:', - style: const TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - ), - const SizedBox(width: 6.0), - Icon( - _showSortOptions ? Icons.arrow_drop_up : Icons.arrow_drop_down, - color: Colors.black, - ), - ], - ), - ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _error.isNotEmpty + ? Center(child: Text(_error)) + : _cards.isEmpty + ? const Center(child: Text('Нет карт')) + : GridView.builder( + padding: const EdgeInsets.all(16.0), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.75, + crossAxisSpacing: 16.0, + mainAxisSpacing: 16.0, ), - ], - ), - ), - - Expanded( - child: GridView.builder( - padding: const EdgeInsets.all(12.0), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 0.7, - crossAxisSpacing: 8.0, - mainAxisSpacing: 12.0, - ), - itemCount: 20, - itemBuilder: (context, index) { - return _buildCardItem(index: index); - }, - ), - ), - ], - ), - - if (_showSortOptions) - Positioned( - top: 50, - left: 16, - child: Container( - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(8.0), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - InkWell( - onTap: () { - setState(() { - _sortOption = 'По редкости'; - _showSortOptions = false; - }); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - 'По редкости', - style: TextStyle( - fontSize: 14.0, - fontWeight: _sortOption == 'По редкости' ? FontWeight.bold : FontWeight.normal, - fontFamily: 'Jost', + itemCount: _cards.length, + itemBuilder: (context, index) { + final card = _cards[index]; + return GestureDetector( + onTap: () => _showCardDetails(card), + onLongPress: widget.isOtherUser ? null : () => _disassembleCard(card), + child: Card( + child: Column( + children: [ + Expanded( + child: Image.network( + card.imageURL, + fit: BoxFit.cover, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Text( + card.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text('Редкость: ${card.rarity}'), + ], + ), + ), + ], + ), ), - ), - ), - ), - const Divider(), - InkWell( - onTap: () { - setState(() { - _sortOption = 'По коллекциям'; - _showSortOptions = false; - }); + ); }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - 'По коллекциям', - style: TextStyle( - fontSize: 14.0, - fontWeight: _sortOption == 'По коллекциям' ? FontWeight.bold : FontWeight.normal, - fontFamily: 'Jost', - ), - ), - ), ), - ], - ), - ), - ), - ], - ), bottomNavigationBar: null, ); } - - Widget _buildCardItem({int index = 0}) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen( - cardIndex: index, - showExchangeButton: widget.isOtherUser, - showFavoriteButton: !widget.isOtherUser, - ), - ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - // Внешняя тонкая черная рамка - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - // Прослойка цвета карточки - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - // Внутренняя тонкая черная рамка - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - // Основная карточка - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Container( - color: const Color(0xFFEAD7C3), - child: const Center( - child: Text( - 'Нет изображения', - style: TextStyle(color: Colors.black45, fontSize: 12, fontFamily: 'Jost'), - ), - ), - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(4, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } } \ No newline at end of file diff --git a/frontend/lib/views/news_detail_screen.dart b/frontend/lib/views/news_detail_screen.dart index fb53812..e0021cf 100644 --- a/frontend/lib/views/news_detail_screen.dart +++ b/frontend/lib/views/news_detail_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'news_screen.dart'; // Import NewsItem class +import '../models/news_model.dart'; +import '../utils/error_formatter.dart'; // Remove unused imports // import 'home_screen.dart'; @@ -9,126 +10,52 @@ import 'news_screen.dart'; // Import NewsItem class // import 'search_players_screen.dart'; class NewsDetailScreen extends StatelessWidget { - final NewsItem news; - + final News news; + const NewsDetailScreen({ - super.key, + Key? key, required this.news, - }); - + }) : super(key: key); + @override Widget build(BuildContext context) { - return Dialog( - backgroundColor: const Color(0xFFEAD7C3), // Match dialog background color style - insetPadding: EdgeInsets.zero, // Remove default dialog padding - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), // Match border radius style - side: const BorderSide(color: Colors.black, width: 2), // Match border style - ), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 24.0), // Adjust padding as needed - child: SingleChildScrollView( // Keep SingleChildScrollView for content that might overflow - child: Column( - mainAxisSize: MainAxisSize.min, // Use min size for column - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // News content - Title, Image, Full Content, Date - Center( // Center the title - child: Text( - news.title, // Use news object directly in StatelessWidget - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - textAlign: TextAlign.center, - ), - ), - - const SizedBox(height: 24.0), // Keep spacing - - // Картинка - Container( - height: 180, - width: double.infinity, - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 1.0), // Added thin black border - ), - alignment: Alignment.center, - child: const Center( // Use Center to center the text - child: Text( - 'Нет изображения', // Use the placeholder text from inventory card - style: TextStyle( - color: Colors.black45, // Use the text color from inventory card - fontSize: 16.0, // Adjust font size as needed for this larger area - // fontWeight: FontWeight.bold, // Remove bold fontWeight - fontFamily: 'Jost', - ), - textAlign: TextAlign.center, - ), - ), - ), - - const SizedBox(height: 24.0), // Keep spacing - - // Расширенное описание - Text( - news.fullContent, // Use news object directly - style: const TextStyle( - fontSize: 16.0, - fontFamily: 'Jost', - ), - ), - - const SizedBox(height: 16.0), // Keep spacing - - // Дата - Align( - alignment: Alignment.centerRight, - child: Text( - 'Дата публикации: ${news.date}', // Use news object directly - style: const TextStyle( - fontSize: 14.0, - fontStyle: FontStyle.italic, - color: Colors.black54, - fontFamily: 'Jost', - ), - ), - ), - ], - ), - ), - ), - // Close button in the top right corner - Positioned( - top: 0, // Adjust position as needed - right: 0, // Adjust position as needed - child: GestureDetector( - onTap: () => Navigator.pop(context), // Close the dialog - child: Container( - width: 48, // Match size from exchange_details_screen.dart - height: 48, // Match size from exchange_details_screen.dart - decoration: BoxDecoration( - color: Color(0xFFD6A067), // Match color - borderRadius: BorderRadius.circular(10), // Match border radius - border: Border.all(color: Colors.black, width: 3), // Match border style - ), - child: const Center( - child: Icon( - Icons.close, - color: Colors.black, - size: 28, // Match icon size - ), - ), + return Scaffold( + appBar: AppBar( + title: Text(news.title), ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (news.imageURL != null) + Image.network( + news.imageURL, + width: double.infinity, + fit: BoxFit.cover, + ), + const SizedBox(height: 16), + Text( + news.title, + style: Theme.of(context).textTheme.headlineMedium, ), + const SizedBox(height: 8), + Text( + 'Опубликовано: ${_formatDate(news.publishDate)}', + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + Text( + news.content, + style: Theme.of(context).textTheme.bodyLarge, ), ], + ), ), ); } + + String _formatDate(DateTime date) { + return '${date.day}.${date.month}.${date.year}'; + } } \ No newline at end of file diff --git a/frontend/lib/views/news_screen.dart b/frontend/lib/views/news_screen.dart index 3745986..c7dd7a0 100644 --- a/frontend/lib/views/news_screen.dart +++ b/frontend/lib/views/news_screen.dart @@ -2,10 +2,69 @@ import 'package:flutter/material.dart'; import 'news_detail_screen.dart'; import 'profile_screen.dart'; import 'search_players_screen.dart'; +import '../services/api_service.dart'; +import '../models/news_model.dart'; +import '../utils/error_formatter.dart'; -class NewsScreen extends StatelessWidget { +class NewsScreen extends StatefulWidget { const NewsScreen({super.key}); + @override + _NewsScreenState createState() => _NewsScreenState(); +} + +class _NewsScreenState extends State { + late bool _isLoading; + late List _news; + late String _error; + + @override + void initState() { + super.initState(); + _isLoading = true; + _news = []; + _error = ''; + _loadNews(); + } + + Future _loadNews() async { + try { + setState(() { + _isLoading = true; + _error = ''; + }); + + final news = await ApiService().getNews(); + + setState(() { + _news = news; + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } + } + + void _openNewsDetails(News news) { + try { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NewsDetailScreen( + news: news, + ), + ), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -59,13 +118,33 @@ class NewsScreen extends StatelessWidget { // Список новостей Expanded( - child: ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - itemCount: _newsItems.length, - itemBuilder: (context, index) { - return _buildNewsItem(context, _newsItems[index]); - }, - ), + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _error.isNotEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(_error), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _loadNews, + child: const Text('Повторить'), + ), + ], + ), + ) + : RefreshIndicator( + onRefresh: _loadNews, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + itemCount: _news.length, + itemBuilder: (context, index) { + final news = _news[index]; + return _buildNewsItem(context, news); + }, + ), + ), ), ], ), @@ -74,14 +153,9 @@ class NewsScreen extends StatelessWidget { } // Метод для отображения элемента новости - Widget _buildNewsItem(BuildContext context, NewsItem news) { + Widget _buildNewsItem(BuildContext context, News news) { return GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (context) => NewsDetailScreen(news: news), - ); - }, + onTap: () => _openNewsDetails(news), child: Container( margin: const EdgeInsets.only(bottom: 16.0), padding: const EdgeInsets.all(16.0), @@ -117,6 +191,11 @@ class NewsScreen extends StatelessWidget { ), ); } + + String _formatDate(String dateStr) { + final date = DateTime.parse(dateStr); + return '${date.day}.${date.month}.${date.year}'; + } } // Примерные данные новостей diff --git a/frontend/lib/views/notifications_modal.dart b/frontend/lib/views/notifications_modal.dart index 2599e89..e5e3746 100644 --- a/frontend/lib/views/notifications_modal.dart +++ b/frontend/lib/views/notifications_modal.dart @@ -1,8 +1,60 @@ import 'package:flutter/material.dart'; +import '../services/api_service.dart'; +import '../models/notification_model.dart' as app_notification; +import '../utils/error_formatter.dart'; -class NotificationsModal extends StatelessWidget { +class NotificationsModal extends StatefulWidget { const NotificationsModal({super.key}); + @override + _NotificationsModalState createState() => _NotificationsModalState(); +} + +class _NotificationsModalState extends State { + late bool _isLoading; + late List _notifications; + late String _error; + + @override + void initState() { + super.initState(); + _isLoading = true; + _notifications = []; + _error = ''; + _loadNotifications(); + } + + Future _loadNotifications() async { + try { + setState(() { + _isLoading = true; + }); + + final notifications = await ApiService().getNotifications(); + + setState(() { + _notifications = notifications; + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } + } + + Future _handleNotificationTap(app_notification.Notification notification) async { + try { + final redirectUrl = await ApiService().navigateToNotification(notification.notification_ID); + Navigator.pushNamed(context, redirectUrl); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + @override Widget build(BuildContext context) { return Dialog( @@ -30,56 +82,25 @@ class NotificationsModal extends StatelessWidget { ), ), const SizedBox(height: 16.0), - Expanded( - child: ListView.builder( - itemCount: 10, // Dummy count for demonstration - itemBuilder: (context, index) { - return Container( - margin: const EdgeInsets.only(bottom: 12.0), - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 1), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - '${index + 1} мин назад', - style: const TextStyle( - color: Colors.black54, - fontSize: 12.0, - ), + _isLoading + ? const Center(child: CircularProgressIndicator()) + : _error.isNotEmpty + ? Center(child: Text(_error)) + : _notifications.isEmpty + ? const Center(child: Text('Нет уведомлений')) + : Expanded( + child: ListView.builder( + itemCount: _notifications.length, + itemBuilder: (context, index) { + final notification = _notifications[index]; + return ListTile( + title: Text(notification.title), + subtitle: Text(_formatDate(notification.timestamp.toIso8601String())), + onTap: () => _handleNotificationTap(notification), + ); + }, ), - ], - ), - const SizedBox(height: 8), - const Text( - 'Заголовок уведомления', - style: TextStyle( - color: Colors.black, - fontSize: 16.0, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 4), - const Text( - 'Описание уведомления. Здесь может быть текст любой длины, описывающий суть уведомления.', - style: TextStyle( - color: Colors.black87, - fontSize: 14.0, ), - ), - ], - ), - ); - }, - ), - ), ], ), ), @@ -111,4 +132,9 @@ class NotificationsModal extends StatelessWidget { ), ); } + + String _formatDate(String dateStr) { + final date = DateTime.parse(dateStr); + return '${date.day}.${date.month}.${date.year} ${date.hour}:${date.minute}'; + } } \ No newline at end of file diff --git a/frontend/lib/views/pack_content_screen.dart b/frontend/lib/views/pack_content_screen.dart index d78535b..1eb6b0f 100644 --- a/frontend/lib/views/pack_content_screen.dart +++ b/frontend/lib/views/pack_content_screen.dart @@ -1,10 +1,17 @@ import 'package:flutter/material.dart'; +import '../models/card_model.dart'; import 'shop_screen.dart'; import 'inventory_screen.dart'; class PackContentScreen extends StatelessWidget { final String setName; - const PackContentScreen({super.key, required this.setName}); + final List cards; + + const PackContentScreen({ + Key? key, + required this.setName, + required this.cards, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -23,102 +30,35 @@ class PackContentScreen extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(16.0), child: GridView.builder( + padding: const EdgeInsets.all(16.0), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 80 / 120, - crossAxisSpacing: 10.0, - mainAxisSpacing: 10.0, + crossAxisCount: 2, + childAspectRatio: 0.75, + crossAxisSpacing: 16.0, + mainAxisSpacing: 16.0, ), - itemCount: 12, + itemCount: cards.length, itemBuilder: (context, index) { - return Container( - width: 80.0, - height: 120.0, - child: Stack( + final card = cards[index]; + return Card( + child: Column( children: [ - // Внешняя тонкая черная рамка - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - // Прослойка цвета карточки - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - // Внутренняя тонкая черная рамка - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), + Expanded( + child: Image.network( + card.imageURL, + fit: BoxFit.cover, ), ), - // Основная карточка Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Container( - color: const Color(0xFFEAD7C3), - child: const Center( - child: Text( - 'Нет изображения', - style: TextStyle(color: Colors.black45, fontSize: 12, fontFamily: 'Jost'), - ), - ), - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(4, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Text( + card.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text('Редкость: ${card.rarity}'), + ], ), ), ], diff --git a/frontend/lib/views/profile_screen.dart b/frontend/lib/views/profile_screen.dart index 8a82b0e..f764296 100644 --- a/frontend/lib/views/profile_screen.dart +++ b/frontend/lib/views/profile_screen.dart @@ -6,8 +6,10 @@ import 'inventory_screen.dart'; import 'settings_screen.dart' show SettingsDialog; import 'achievements_screen.dart'; import 'profile_image_dialog.dart'; +import '../services/api_service.dart'; +import '../utils/error_formatter.dart'; -class ProfileScreen extends StatelessWidget { +class ProfileScreen extends StatefulWidget { final bool isOtherUser; final String? playerName; final String? playerId; @@ -25,6 +27,85 @@ class ProfileScreen extends StatelessWidget { this.favoriteCardIds = const [], }); + @override + State createState() => _ProfileScreenState(); +} + +class _ProfileScreenState extends State { + late bool _isLoading; + late String _error; + late Map _userStats; + late List _favoriteCards; + late List _favoriteAchievements; + late List _achievements; + late Map _settings; + + @override + void initState() { + super.initState(); + _isLoading = true; + _error = ''; + _settings = {}; + _loadProfileData(); + } + + Future _loadProfileData() async { + try { + setState(() { + _isLoading = true; + }); + + final profileData = await ApiService().getProfile(); + final achievements = await ApiService().getAchievements(); + + setState(() { + _userStats = profileData['userStats']; + _favoriteCards = profileData['favoriteCards']; + _favoriteAchievements = profileData['favoriteAchievements']; + _achievements = achievements; + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } + } + + Future _toggleAchievementFavorite(int achievementId, bool isFavorite) async { + try { + bool success; + if (isFavorite) { + success = await ApiService().removeAchievementFromFavorites(achievementId); + } else { + success = await ApiService().addAchievementToFavorites(achievementId); + } + + if (success) { + await _loadProfileData(); + } + } catch (e) { + // Показать ошибку пользователю + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + + Future _updateSettings() async { + try { + await ApiService().updateProfileSettings(_settings); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Настройки успешно сохранены')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -57,7 +138,7 @@ class ProfileScreen extends StatelessWidget { ), ), actions: [ - if (isOtherUser) + if (widget.isOtherUser) Padding( padding: const EdgeInsets.only(right: 16.0), child: Container( @@ -69,7 +150,7 @@ class ProfileScreen extends StatelessWidget { showDialog( context: context, barrierColor: Colors.black.withOpacity(0.2), - builder: (context) => ReportDialog(playerName: playerName ?? ''), + builder: (context) => ReportDialog(playerName: widget.playerName ?? ''), ); }, child: Transform( @@ -115,7 +196,7 @@ class ProfileScreen extends StatelessWidget { // Аватар пользователя GestureDetector( - onTap: isOtherUser ? null : () { + onTap: widget.isOtherUser ? null : () { showDialog( context: context, barrierColor: Colors.black.withOpacity(0.2), @@ -144,7 +225,7 @@ class ProfileScreen extends StatelessWidget { // Имя пользователя Text( - isOtherUser ? playerName ?? '' : 'Ник', + widget.isOtherUser ? widget.playerName ?? '' : 'Ник', style: const TextStyle( fontSize: 18.0, fontWeight: FontWeight.bold, @@ -156,7 +237,7 @@ class ProfileScreen extends StatelessWidget { // ID пользователя Text( - isOtherUser ? playerId ?? '' : '######', + widget.isOtherUser ? widget.playerId ?? '' : '######', style: const TextStyle( fontSize: 16.0, color: Colors.black54, @@ -174,7 +255,7 @@ class ProfileScreen extends StatelessWidget { children: List.generate( 5, // Always generate 5 cards (index) => _buildCard( - index < favoriteCardIds.length ? favoriteCardIds[index] : -1, + index < widget.favoriteCardIds.length ? widget.favoriteCardIds[index] : -1, ), ), ), @@ -215,12 +296,15 @@ class ProfileScreen extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => const AchievementsScreen(), + builder: (context) => AchievementsScreen( + isOtherUser: widget.isOtherUser, + userId: widget.isOtherUser ? int.parse(widget.playerId ?? '0') : 0, + ), ), ); }, child: Text( - isOtherUser ? 'Достижения →' : 'Все достижения →', + widget.isOtherUser ? 'Достижения →' : 'Все достижения →', style: const TextStyle( fontSize: 13.0, fontWeight: FontWeight.normal, @@ -254,7 +338,7 @@ class ProfileScreen extends StatelessWidget { ), ), Text( - isOtherUser ? (cardsCollected?.toString() ?? '0') : '****', + widget.isOtherUser ? (_userStats['cardsCollected']?.toString() ?? '0') : '****', style: const TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, @@ -278,7 +362,7 @@ class ProfileScreen extends StatelessWidget { ), ), Text( - isOtherUser ? (collectionsCollected?.toString() ?? '0') : '***', + widget.isOtherUser ? (_userStats['collectionsCollected']?.toString() ?? '0') : '***', style: const TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, @@ -287,7 +371,7 @@ class ProfileScreen extends StatelessWidget { ), ], ), - if (isOtherUser) ...[ + if (widget.isOtherUser) ...[ const SizedBox(height: 18.0), // Кнопка посмотреть инвентарь Align( @@ -299,8 +383,8 @@ class ProfileScreen extends StatelessWidget { MaterialPageRoute( builder: (context) => InventoryScreen( isOtherUser: true, - playerName: playerName ?? '', - playerId: playerId ?? '', + playerName: widget.playerName ?? '', + playerId: widget.playerId != null ? int.tryParse(widget.playerId!) : null, ), ), ); diff --git a/frontend/lib/views/quests_screen.dart b/frontend/lib/views/quests_screen.dart index fae86f1..7690680 100644 --- a/frontend/lib/views/quests_screen.dart +++ b/frontend/lib/views/quests_screen.dart @@ -3,6 +3,9 @@ import 'home_screen.dart'; import 'shop_screen.dart'; import 'exchanges_screen.dart'; import 'news_screen.dart'; +import '../services/api_service.dart'; +import '../models/quest_model.dart'; +import '../utils/error_formatter.dart'; class QuestsDialogContent extends StatefulWidget { const QuestsDialogContent({super.key}); @@ -211,4 +214,140 @@ void showQuestsDialog(BuildContext context) { ), ), ); +} + +class QuestsScreen extends StatefulWidget { + const QuestsScreen({super.key}); + + @override + State createState() => _QuestsScreenState(); +} + +class _QuestsScreenState extends State { + late bool _isLoading; + late Map> _quests; + late String _error; + + @override + void initState() { + super.initState(); + _isLoading = true; + _quests = { + 'dailyQuests': [], + 'weeklyQuests': [] + }; + _error = ''; + _loadQuests(); + } + + Future _loadQuests() async { + try { + setState(() { + _isLoading = true; + }); + + final quests = await ApiService().getQuests(); + + setState(() { + _quests = quests; + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } + } + + Future _toggleQuestStatus(Quest quest) async { + try { + final updatedQuest = await ApiService().changeQuestStatus(quest.quest_ID); + await _loadQuests(); // Перезагружаем все квесты для обновления UI + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + + Future _claimReward(String questType) async { + try { + final result = await ApiService().claimQuestReward(questType); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Получено: ${result['receivedCoins']} монет')), + ); + await _loadQuests(); // Перезагружаем квесты после получения награды + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Квесты'), + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _error.isNotEmpty + ? Center(child: Text(_error)) + : SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildQuestSection('Ежедневные квесты', _quests['dailyQuests']!, 'daily'), + _buildQuestSection('Еженедельные квесты', _quests['weeklyQuests']!, 'weekly'), + ], + ), + ), + ); + } + + Widget _buildQuestSection(String title, List quests, String questType) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + title, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: quests.length, + itemBuilder: (context, index) { + final quest = quests[index]; + return ListTile( + title: Text(quest.title), + subtitle: Text(quest.description), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('${quest.progress}/${quest.maxProgress}'), + Checkbox( + value: quest.isCompleted, + onChanged: (bool? value) => _toggleQuestStatus(quest), + ), + ], + ), + ); + }, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: () => _claimReward(questType), + child: const Text('Получить награду'), + ), + ), + ], + ); + } } \ No newline at end of file diff --git a/frontend/lib/views/shop_screen.dart b/frontend/lib/views/shop_screen.dart index 476bebf..c3ed9b3 100644 --- a/frontend/lib/views/shop_screen.dart +++ b/frontend/lib/views/shop_screen.dart @@ -3,6 +3,10 @@ import 'profile_screen.dart'; import 'search_players_screen.dart'; import 'inventory_screen.dart'; import 'pack_content_screen.dart'; +import '../services/api_service.dart'; +import '../models/shop_model.dart'; +import '../models/card_model.dart'; +import '../utils/error_formatter.dart'; class ShopScreen extends StatefulWidget { const ShopScreen({super.key}); @@ -11,338 +15,83 @@ class ShopScreen extends StatefulWidget { State createState() => _ShopScreenState(); } -class _ShopScreenState extends State with SingleTickerProviderStateMixin { - late TabController _tabController; - int _selectedTab = 0; - - final List _sets = [ - 'Название набора 1', - 'Название набора 2', - 'Название набора 3', - 'Название набора 4', - ]; - - final List _coins = [ - 'Ценник 1', - 'Ценник 2', - ]; +class _ShopScreenState extends State { + late bool _isLoading; + late List _packs; + late List _coinOffers; + late String _error; @override void initState() { super.initState(); - _tabController = TabController(length: 2, vsync: this, initialIndex: _selectedTab); + _isLoading = true; + _packs = []; + _coinOffers = []; + _error = ''; + _loadShopData(); } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); + + Future _loadShopData() async { + try { + setState(() { + _isLoading = true; + }); + + final packs = await ApiService().getAllPacks(); + final coinOffers = await ApiService().getCoinOffers(); + + setState(() { + _packs = packs; + _coinOffers = coinOffers; + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } } - void _showSetDetails(String setName) { - showDialog( - context: context, - builder: (context) => Dialog( - backgroundColor: Colors.transparent, - insetPadding: EdgeInsets.zero, - child: Center( - child: SizedBox( - width: 360.0, - height: 492.0, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 3), - ), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.only(top: 55.0, left: 16.0, right: 16.0, bottom: 16.0), - child: Column( - children: [ - Container( - height: 120.0, - width: 321.0, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), - ), - const SizedBox(height: 10.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - setName, - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - ), - const Text( - 'Цена', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, - fontFamily: 'Jost', - ), - ), - ], - ), - const SizedBox(height: 70.0), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => InventoryScreen( - collectionName: setName, - isFromShop: true, - ), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: const Text( - 'Посмотреть содержимое', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - fontFamily: 'Roboto', - ), - ), - ), - ), - const SizedBox(height: 12.0), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PackContentScreen( - setName: setName, - ), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: const Text( - 'Купить', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - fontFamily: 'Roboto', - ), - ), - ), - ), - ], - ), - ), - Positioned( - top: -1, - right: -1, - child: GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - width: 48.0, - height: 48.0, - decoration: BoxDecoration( - color: const Color(0xFFD9A76A), - border: Border.all( - color: Colors.black, - width: 1.0, - ), - borderRadius: const BorderRadius.only( - topRight: Radius.circular(8.0), - bottomLeft: Radius.circular(8.0), - ), - ), - child: const Icon( - Icons.close, - color: Colors.black, - size: 32.0, - weight: 900, - ), - ), - ), - ), - ], - ), - ), + Future _buyPack(Pack pack) async { + try { + final result = await ApiService().buyPack(pack.pack_ID); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Набор успешно куплен!')), + ); + + // Открываем экран с содержимым набора + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PackContentScreen( + setName: pack.name, + cards: (result['receivedCards'] as List).map((item) => CardModel.fromJson(item)).toList(), ), ), - ), - ); + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } } - void _showCoinDetails(String coinName) { - showDialog( - context: context, - builder: (context) => Dialog( - backgroundColor: Colors.transparent, - insetPadding: EdgeInsets.zero, - child: Center( - child: SizedBox( - width: 360.0, - height: 492.0, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 3), - ), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.only(top: 96.0, left: 16.0, right: 16.0, bottom: 16.0), - child: Column( - children: [ - Center( - child: Container( - height: 150.0, - width: 150.0, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), - ), - ), - const SizedBox(height: 7.0), - Text( - coinName, - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 60.0), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.pop(context); - final overlay = Overlay.of(context); - final overlayEntry = OverlayEntry( - builder: (context) => Positioned( - top: 40, - left: 16, - right: 16, - child: Material( - color: Colors.transparent, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.18), - blurRadius: 8, - offset: const Offset(0, 6), - ), - ], - ), - padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 5), - child: const Center( - child: Text( - 'Монеты успешно приобретены', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Colors.black, - ), - ), - ), - ), - ), - ), - ); - overlay.insert(overlayEntry); - Future.delayed(const Duration(seconds: 3), () { - overlayEntry.remove(); - }); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: const Text( - 'Купить', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - fontFamily: 'Roboto', - ), - ), - ), - ), - ], - ), - ), - Positioned( - top: -1, - right: -1, - child: GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - width: 48.0, - height: 48.0, - decoration: BoxDecoration( - color: const Color(0xFFD9A76A), - border: Border.all( - color: Colors.black, - width: 1.0, - ), - borderRadius: const BorderRadius.only( - topRight: Radius.circular(8.0), - bottomLeft: Radius.circular(8.0), - ), - ), - child: const Icon( - Icons.close, - color: Colors.black, - size: 32.0, - weight: 900, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ), - ); + Future _buyCoinOffer(CoinOffer offer) async { + try { + final paymentUrl = await ApiService().purchaseCoins( + offer.offer_ID, + 'cardly://payment-callback', // URL схема для возврата в приложение + ); + + // Здесь нужно реализовать открытие URL для оплаты + // Это может быть WebView или переход в браузер + + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } } @override @@ -408,131 +157,109 @@ class _ShopScreenState extends State with SingleTickerProviderStateM ), ], ), - body: Column( + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _error.isNotEmpty + ? Center(child: Text(_error)) + : SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(12.0), - ), - child: TabBar( - controller: _tabController, - dividerColor: Colors.transparent, - indicator: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.all(Radius.circular(10.0)), - ), - labelColor: Colors.black, - unselectedLabelColor: Colors.black, - indicatorSize: TabBarIndicatorSize.tab, - tabs: const [ - Tab(text: 'Наборы'), - Tab(text: 'Монеты'), - ], - labelStyle: TextStyle(fontFamily: 'Jost'), - unselectedLabelStyle: TextStyle(fontFamily: 'Jost'), - onTap: (index) { - setState(() { - _selectedTab = index; - }); - }, - ), - ), - ), - - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - _buildSetsTab(), - _buildCoinsTab(), - ], - ), - ), - ], + _buildPacksSection(), + _buildCoinOffersSection(), + ], + ), ), bottomNavigationBar: null, ); } - Widget _buildSetsTab() { - return ListView.separated( - padding: const EdgeInsets.fromLTRB(16.0, 60.0, 16.0, 16.0), - itemCount: _sets.length, - separatorBuilder: (context, index) => const SizedBox(height: 30.0), - itemBuilder: (context, index) { + Widget _buildPacksSection() { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - InkWell( - onTap: () => _showSetDetails(_sets[index]), - child: Container( - width: 321.0, - height: 120.0, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(10.0), - ), - ), - ), - const SizedBox(height: 7.0), + const Padding( + padding: EdgeInsets.all(16.0), + child: Text( + 'Наборы карт', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.all(16.0), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.75, + crossAxisSpacing: 16.0, + mainAxisSpacing: 16.0, + ), + itemCount: _packs.length, + itemBuilder: (context, index) { + final pack = _packs[index]; + return Card( + child: Column( + children: [ + Expanded( + child: Image.network( + pack.imageURL, + fit: BoxFit.cover, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ Text( - _sets[index], - style: const TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - color: Colors.black, - fontFamily: 'Jost', + pack.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text('${pack.price} монет'), + ElevatedButton( + onPressed: () => _buyPack(pack), + child: const Text('Купить'), + ), + ], + ), + ), + ], ), - textAlign: TextAlign.center, - ), - ], - ); - }, + ); + }, + ), + ], ); } - - Widget _buildCoinsTab() { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 15.0, - mainAxisSpacing: 15.0, - childAspectRatio: 150 / 175, - ), - itemCount: _coins.length, - itemBuilder: (context, index) { + + Widget _buildCoinOffersSection() { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - InkWell( - onTap: () => _showCoinDetails(_coins[index]), - child: Container( - width: 150.0, - height: 150.0, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), - ), - ), - const SizedBox(height: 7.0), - Text( - _coins[index], - style: const TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - color: Colors.black, - fontFamily: 'Jost', - ), - textAlign: TextAlign.center, + const Padding( + padding: EdgeInsets.all(16.0), + child: Text( + 'Монеты', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _coinOffers.length, + itemBuilder: (context, index) { + final offer = _coinOffers[index]; + return ListTile( + title: Text('${offer.coins} монет'), + subtitle: Text('${offer.price} ₽'), + trailing: ElevatedButton( + onPressed: () => _buyCoinOffer(offer), + child: const Text('Купить'), ), - ], ); }, ), + ], ); } } \ No newline at end of file From 75f179a9032ae37e1df7846cc7cb5fc2c011774b Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Mon, 2 Jun 2025 02:01:01 +0300 Subject: [PATCH 39/54] FCCX-145 update structure and yet others updates --- Documentation/Swagger/cardly.yaml | 751 ++-- backend/.dockerignore | 2 + backend/.env.example | 38 + backend/.gitignore | 1 + backend/build.gradle | 28 +- .../grafana/dashboards/App Observitility.json | 681 ++++ backend/configs/grafana/datasources.yaml | 17 + backend/configs/prometheus.yml | 12 + backend/db/migration/V1__init_tables.sql | 195 + backend/docker-compose.yml | 100 +- .../main/kotlin/ru/vsu/app/AppApplication.kt | 11 - .../src/main/kotlin/ru/vsu/app/Application.kt | 22 + .../kotlin/ru/vsu/app/config/OpenApiConfig.kt | 8 +- .../vsu/app/config/PublicEndpointsConfig.kt | 24 + .../ru/vsu/app/config/SecurityConfig.kt | 16 +- .../ru/vsu/app/controller/AdminController.kt | 699 ++++ .../app/controller/AuthController (old).kt | 127 + .../ru/vsu/app/controller/AuthController.kt | 444 +- .../ru/vsu/app/controller/CardController.kt | 119 +- .../ru/vsu/app/controller/HomeController.kt | 272 ++ .../vsu/app/controller/InventoryController.kt | 261 ++ .../app/controller/OtherProfileController.kt | 166 + .../vsu/app/controller/ProfileController.kt | 180 + .../app/controller/ShopController (old).kt | 90 + .../ru/vsu/app/controller/ShopController.kt | 228 +- .../ru/vsu/app/controller/TradesController.kt | 187 + .../kotlin/ru/vsu/app/dto/AchievementDto.kt | 43 + .../src/main/kotlin/ru/vsu/app/dto/AuthDto.kt | 90 - .../src/main/kotlin/ru/vsu/app/dto/CardDto.kt | 84 +- .../kotlin/ru/vsu/app/dto/CardResponse.kt | 12 - .../kotlin/ru/vsu/app/dto/CoinOfferDto.kt | 48 + .../kotlin/ru/vsu/app/dto/CollectionDto.kt | 41 + .../src/main/kotlin/ru/vsu/app/dto/NewsDto.kt | 43 + .../kotlin/ru/vsu/app/dto/NotificationDto.kt | 43 + .../src/main/kotlin/ru/vsu/app/dto/PackDto.kt | 45 + .../ru/vsu/app/dto/PaymentCallbackDto.kt | 64 + .../main/kotlin/ru/vsu/app/dto/PaymentDto.kt | 70 + .../main/kotlin/ru/vsu/app/dto/QuestDto.kt | 61 + .../main/kotlin/ru/vsu/app/dto/ReportDto.kt | 71 + .../src/main/kotlin/ru/vsu/app/dto/ShopDto.kt | 35 - .../main/kotlin/ru/vsu/app/dto/ThemeDto.kt | 35 + .../main/kotlin/ru/vsu/app/dto/TradeDto.kt | 57 + .../src/main/kotlin/ru/vsu/app/dto/UserDto.kt | 79 + .../kotlin/ru/vsu/app/dto/UserSettingsDto.kt | 35 + .../kotlin/ru/vsu/app/dto/UserStatsDto.kt | 31 + .../AdminReportsReportIDPutRequest.kt | 80 + ...AdminTradesTradeIDInvalidatePostRequest.kt | 27 + .../AdminUsersUserIDBanPostRequest.kt | 55 + ...sersUserIDQuestsQuestIDResetPostRequest.kt | 31 + .../requests/HomeGenerateCardPostRequest.kt | 27 + .../HomeQuestsClaimRewardPostRequest.kt | 47 + .../requests/InventoryDestroyPostRequest.kt | 28 + .../vsu/app/dto/requests/LoginUserRequest.kt | 32 + .../ru/vsu/app/dto/requests/NewsRequest.kt | 22 + .../OtherProfileUserIDReportPostRequest.kt | 53 + .../app/dto/requests/RegisterUserRequest.kt | 38 + .../requests/RequestPasswordResetRequest.kt | 28 + .../requests/ResendVerificationCodeRequest.kt | 27 + .../app/dto/requests/ResetPasswordRequest.kt | 37 + ...opCoinsOffersOfferIdPurchasePostRequest.kt | 28 + .../dto/requests/TradesInitiatePostRequest.kt | 31 + .../app/dto/requests/VerifyEmailRequest.kt | 32 + .../admin/AdminStatsGet200Response.kt | 55 + ...nTradesTradeIDInvalidatePost200Response.kt | 27 + .../AdminUsersUserIDBanPost200Response.kt | 31 + ...UserIDQuestsQuestIDResetPost200Response.kt | 31 + .../AdminUsersUserIDUnbanPost200Response.kt | 27 + .../responses/auth/AuthCheckGet401Response.kt | 31 + .../auth/login/LoginUser400Response.kt | 27 + .../auth/login/LoginUser401Response.kt | 27 + .../auth/register/RegisterUser200Response.kt | 29 + .../auth/register/RegisterUser400Response.kt | 25 + .../auth/register/RegisterUser409Response.kt | 24 + .../RequestPasswordReset200Response.kt | 31 + .../RequestPasswordReset400Response.kt | 27 + .../RequestPasswordReset404Response.kt | 27 + .../ResetPassword200Response.kt | 27 + .../ResetPassword400Response.kt | 27 + .../ResendVerificationCode200Response.kt | 29 + .../verifyemail/VerifyEmail400Response.kt | 31 + .../verifyemail/VerifyEmail410Response.kt | 27 + .../responses/common/InternalServerError.kt | 31 + .../home/HomeGenerateCardPost200Response.kt | 33 + .../home/HomeGenerateCardPost402Response.kt | 31 + .../responses/home/HomeNewsGet404Response.kt | 27 + ...nsNotificationIDNavigatePost200Response.kt | 27 + ...nsNotificationIDNavigatePost404Response.kt | 27 + .../HomeQuestsClaimRewardPost200Response.kt | 37 + .../HomeQuestsClaimRewardPost400Response.kt | 27 + .../home/HomeQuestsGet200Response.kt | 34 + ...uestsQuestIDChangeStatusPost200Response.kt | 33 + .../home/HomeSearchGet400Response.kt | 27 + .../home/HomeSearchGet404Response.kt | 27 + .../app/dto/responses/home/NewsResponse.kt | 31 + ...ventoryCardCardIDFavoriteGet200Response.kt | 27 + ...ventoryCardCardIDQuantityGet200Response.kt | 35 + ...oryCardCardIDTradeCancelPost200Response.kt | 27 + ...toryCardCardIDTradeStatusGet200Response.kt | 27 + .../InventoryDestroyPost200Response.kt | 31 + .../InventoryFavoritesCountGet200Response.kt | 31 + .../inventory/InventoryGet200Response.kt | 43 + ...eCardCardIDInitiateTradePost200Response.kt | 27 + ...eCardCardIDInitiateTradePost400Response.kt | 27 + ...eCardCardIDInitiateTradePost409Response.kt | 31 + ...therProfileCardCardIDViewGet200Response.kt | 29 + .../OtherProfileUserIDGet200Response.kt | 55 + ...herProfileUserIDInventoryGet200Response.kt | 35 + ...herProfileUserIDInventoryGet403Response.kt | 27 + ...OtherProfileUserIDReportPost200Response.kt | 33 + ...chievementsFavoritesCountGet200Response.kt | 32 + .../ProfileAchievementsGet200Response.kt | 29 + .../profile/ProfileGet200Response.kt | 44 + ...insOffersOfferIdPurchasePost200Response.kt | 28 + .../shop/ShopPacksPackIdBuyPost200Response.kt | 33 + .../shop/ShopPacksPackIdBuyPost402Response.kt | 31 + .../ShopPacksPackIdOpenPost200Response.kt | 33 + .../ShopPaymentsProcessPost200Response.kt | 31 + .../TradesTradeIdAcceptPost200Response.kt | 27 + .../TradesTradeIdAcceptPost403Response.kt | 27 + .../TradesTradeIdCancelPost200Response.kt | 27 + .../trades/TradesTradeIdGet404Response.kt | 27 + .../TradesTradeIdRejectPost200Response.kt | 27 + .../ru/vsu/app/mapper/AchievementMapper.kt | 16 + .../kotlin/ru/vsu/app/mapper/CardMapper.kt | 19 + .../ru/vsu/app/mapper/NotificationMapper.kt | 16 + .../kotlin/ru/vsu/app/mapper/UserMapper.kt | 28 + .../kotlin/ru/vsu/app/metrics/AuthMetrics.kt | 96 + .../kotlin/ru/vsu/app/metrics/MetricTags.kt | 6 + .../ru/vsu/app/metrics/MetricsRegistry.kt | 18 + .../ru/vsu/app/model/AchievementEntity.kt | 26 + .../src/main/kotlin/ru/vsu/app/model/Card.kt | 36 - .../kotlin/ru/vsu/app/model/CardEntity.kt | 56 + .../{CoinOffer.kt => CoinOfferEntity.kt} | 2 +- .../ru/vsu/app/model/CollectionEntity.kt | 22 + .../kotlin/ru/vsu/app/model/NewsEntity.kt | 16 + .../ru/vsu/app/model/NotificationEntity.kt | 29 + .../vsu/app/model/{Pack.kt => PackEntity.kt} | 4 +- .../kotlin/ru/vsu/app/model/QuestEntity.kt | 42 + .../kotlin/ru/vsu/app/model/ReportEntity.kt | 37 + .../src/main/kotlin/ru/vsu/app/model/User.kt | 31 - .../kotlin/ru/vsu/app/model/UserEntity.kt | 84 + .../ru/vsu/app/model/UserStatsEntity.kt | 22 + .../app/repository/AchievementRepository.kt | 10 + .../ru/vsu/app/repository/CardRepository.kt | 13 +- .../vsu/app/repository/CoinOfferRepository.kt | 4 +- .../app/repository/CollectionRepository.kt | 10 + .../ru/vsu/app/repository/NewsRepository.kt | 8 + .../app/repository/NotificationRepository.kt | 10 + .../ru/vsu/app/repository/PackRepository.kt | 4 +- .../ru/vsu/app/repository/QuestRepository.kt | 11 + .../ru/vsu/app/repository/ReportRepository.kt | 10 + .../ru/vsu/app/repository/UserRepository.kt | 27 +- .../vsu/app/repository/UserStatsRepository.kt | 11 + .../app/security/JwtAuthenticationFilter.kt | 45 +- .../kotlin/ru/vsu/app/service/AuthService.kt | 165 + .../kotlin/ru/vsu/app/service/CardService.kt | 69 - .../kotlin/ru/vsu/app/service/EmailService.kt | 8 +- .../ru/vsu/app/service/InventoryService.kt | 83 + .../kotlin/ru/vsu/app/service/JwtService.kt | 13 +- .../kotlin/ru/vsu/app/service/NewsService.kt | 49 + .../kotlin/ru/vsu/app/service/ShopService.kt | 310 +- .../kotlin/ru/vsu/app/service/UserService.kt | 301 +- .../ru/vsu/app/util/OptionalExtensions.kt | 5 + .../src/main/resources/application.properties | 52 +- backend/src/main/resources/openapi.yaml | 3573 +++++++++++++++++ .../kotlin/ru/vsu/app/api/AdminApiTest.kt | 470 +++ .../test/kotlin/ru/vsu/app/api/AuthApiTest.kt | 154 + .../test/kotlin/ru/vsu/app/api/HomeApiTest.kt | 177 + .../kotlin/ru/vsu/app/api/InventoryApiTest.kt | 173 + .../ru/vsu/app/api/OtherProfileApiTest.kt | 124 + .../kotlin/ru/vsu/app/api/ProfileApiTest.kt | 108 + .../test/kotlin/ru/vsu/app/api/ShopApiTest.kt | 130 + .../kotlin/ru/vsu/app/api/TradesApiTest.kt | 116 + 173 files changed, 13716 insertions(+), 1255 deletions(-) create mode 100644 backend/.env.example create mode 100644 backend/configs/grafana/dashboards/App Observitility.json create mode 100644 backend/configs/grafana/datasources.yaml create mode 100644 backend/configs/prometheus.yml create mode 100644 backend/db/migration/V1__init_tables.sql delete mode 100644 backend/src/main/kotlin/ru/vsu/app/AppApplication.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/Application.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/config/PublicEndpointsConfig.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/controller/AuthController (old).kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/controller/HomeController.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/controller/OtherProfileController.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/controller/ProfileController.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/controller/ShopController (old).kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/controller/TradesController.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/AchievementDto.kt delete mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/AuthDto.kt delete mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/CardResponse.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/CoinOfferDto.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/CollectionDto.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/NewsDto.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/NotificationDto.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/PackDto.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/PaymentCallbackDto.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/PaymentDto.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/QuestDto.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/ReportDto.kt delete mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/ShopDto.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/ThemeDto.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/TradeDto.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/UserDto.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/UserSettingsDto.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/UserStatsDto.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminReportsReportIDPutRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminTradesTradeIDInvalidatePostRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminUsersUserIDBanPostRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminUsersUserIDQuestsQuestIDResetPostRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/HomeGenerateCardPostRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/HomeQuestsClaimRewardPostRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/InventoryDestroyPostRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/LoginUserRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/NewsRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/OtherProfileUserIDReportPostRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/RegisterUserRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/RequestPasswordResetRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/ResendVerificationCodeRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/ResetPasswordRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/ShopCoinsOffersOfferIdPurchasePostRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/TradesInitiatePostRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/VerifyEmailRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminStatsGet200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminTradesTradeIDInvalidatePost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDBanPost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDQuestsQuestIDResetPost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDUnbanPost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/AuthCheckGet401Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser400Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser401Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser400Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser409Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset400Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset404Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/ResetPassword200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/ResetPassword400Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/ResendVerificationCode200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/VerifyEmail400Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/VerifyEmail410Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/common/InternalServerError.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeGenerateCardPost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeGenerateCardPost402Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNewsGet404Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNotificationsNotificationIDNavigatePost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNotificationsNotificationIDNavigatePost404Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsClaimRewardPost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsClaimRewardPost400Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsGet200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsQuestIDChangeStatusPost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeSearchGet400Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeSearchGet404Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/home/NewsResponse.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDFavoriteGet200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDQuantityGet200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDTradeCancelPost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDTradeStatusGet200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryDestroyPost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryFavoritesCountGet200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost400Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost409Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDViewGet200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDGet200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDInventoryGet200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDInventoryGet403Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDReportPost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileAchievementsFavoritesCountGet200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileAchievementsGet200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileGet200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopCoinsOffersOfferIdPurchasePost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdBuyPost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdBuyPost402Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdOpenPost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPaymentsProcessPost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdAcceptPost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdAcceptPost403Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdCancelPost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdGet404Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdRejectPost200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/mapper/AchievementMapper.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/mapper/CardMapper.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/mapper/NotificationMapper.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/mapper/UserMapper.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/metrics/AuthMetrics.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/metrics/MetricTags.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/metrics/MetricsRegistry.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/model/AchievementEntity.kt delete mode 100644 backend/src/main/kotlin/ru/vsu/app/model/Card.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/model/CardEntity.kt rename backend/src/main/kotlin/ru/vsu/app/model/{CoinOffer.kt => CoinOfferEntity.kt} (94%) create mode 100644 backend/src/main/kotlin/ru/vsu/app/model/CollectionEntity.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/model/NewsEntity.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/model/NotificationEntity.kt rename backend/src/main/kotlin/ru/vsu/app/model/{Pack.kt => PackEntity.kt} (88%) create mode 100644 backend/src/main/kotlin/ru/vsu/app/model/QuestEntity.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/model/ReportEntity.kt delete mode 100644 backend/src/main/kotlin/ru/vsu/app/model/User.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/model/UserEntity.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/model/UserStatsEntity.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/repository/AchievementRepository.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/repository/CollectionRepository.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/repository/NewsRepository.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/repository/NotificationRepository.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/repository/QuestRepository.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/repository/ReportRepository.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/repository/UserStatsRepository.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/service/AuthService.kt delete mode 100644 backend/src/main/kotlin/ru/vsu/app/service/CardService.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/service/InventoryService.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/service/NewsService.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/util/OptionalExtensions.kt create mode 100644 backend/src/main/resources/openapi.yaml create mode 100644 backend/src/test/kotlin/ru/vsu/app/api/AdminApiTest.kt create mode 100644 backend/src/test/kotlin/ru/vsu/app/api/AuthApiTest.kt create mode 100644 backend/src/test/kotlin/ru/vsu/app/api/HomeApiTest.kt create mode 100644 backend/src/test/kotlin/ru/vsu/app/api/InventoryApiTest.kt create mode 100644 backend/src/test/kotlin/ru/vsu/app/api/OtherProfileApiTest.kt create mode 100644 backend/src/test/kotlin/ru/vsu/app/api/ProfileApiTest.kt create mode 100644 backend/src/test/kotlin/ru/vsu/app/api/ShopApiTest.kt create mode 100644 backend/src/test/kotlin/ru/vsu/app/api/TradesApiTest.kt diff --git a/Documentation/Swagger/cardly.yaml b/Documentation/Swagger/cardly.yaml index 0e9b37b..84fd093 100644 --- a/Documentation/Swagger/cardly.yaml +++ b/Documentation/Swagger/cardly.yaml @@ -1,6 +1,6 @@ openapi: 3.0.0 info: - title: Cadrly API + title: Cardly API version: 0.1.0 description: Приложение для обмена коллекционными карточками @@ -45,20 +45,20 @@ paths: email: type: string format: email - example: "user@example.com" + example: 'user@example.com' description: Email пользователя username: type: string minLength: 3 maxLength: 25 - example: "Коллекционер_123" - description: "Имя пользователя (3-25 символов)" + example: 'Коллекционер_123' + description: 'Имя пользователя (3-25 символов)' password: type: string format: password minLength: 8 - example: "Password123!" - description: "Пароль (минимум 8 символов)" + example: 'Password123!' + description: 'Пароль (минимум 8 символов)' required: [email, username, password] responses: 200: @@ -70,10 +70,10 @@ paths: properties: tempToken: type: string - example: "temp_abc123" + example: 'temp_abc123' message: type: string - example: "Код подтверждения отправлен на email" + example: 'Код подтверждения отправлен на email' 400: description: Неверный формат данных content: @@ -83,7 +83,7 @@ paths: properties: error: type: string - example: "Некорректный формат введенных данных" + example: 'Некорректный формат введенных данных' 409: description: Пользователь уже существует content: @@ -93,7 +93,7 @@ paths: properties: error: type: string - example: "Пользователь с таким email уже существует" + example: 'Пользователь с таким email уже существует' 500: $ref: '#/components/responses/ServerError' @@ -114,13 +114,13 @@ paths: properties: tempToken: type: string - example: "temp_abc123" - description: "Временный токен из ответа регистрации" + example: 'temp_abc123' + description: 'Временный токен из ответа регистрации' code: type: string pattern: "^\\d{6}$" - example: "123456" - description: "6-значный код подтверждения" + example: '123456' + description: '6-значный код подтверждения' required: [tempToken, code] responses: 200: @@ -129,7 +129,7 @@ paths: Set-Cookie: schema: type: string - example: "session_id=abcde12345; HttpOnly; Path=/; Secure" + example: 'session_id=abcde12345; HttpOnly; Path=/; Secure' content: application/json: schema: @@ -143,7 +143,7 @@ paths: properties: error: type: string - example: "Неверный код подтверждения" + example: 'Неверный код подтверждения' canRetry: type: boolean example: true @@ -156,7 +156,7 @@ paths: properties: error: type: string - example: "Код устарел, запросите новый" + example: 'Код устарел, запросите новый' 500: $ref: '#/components/responses/ServerError' @@ -175,7 +175,7 @@ paths: properties: tempToken: type: string - example: "temp_abc123" + example: 'temp_abc123' required: [tempToken] responses: 200: @@ -187,12 +187,12 @@ paths: properties: message: type: string - example: "Новый код подтверждения отправлен" + example: 'Новый код подтверждения отправлен' 400: $ref: '#/components/responses/BadRequest' 500: $ref: '#/components/responses/ServerError' - + /auth/login: post: tags: [Authentication] @@ -211,11 +211,11 @@ paths: email: type: string format: email - example: "user@example.com" + example: 'user@example.com' password: type: string format: password - example: "SecurePass123!" + example: 'SecurePass123!' required: [email, password] responses: 200: @@ -224,7 +224,7 @@ paths: Set-Cookie: schema: type: string - example: "session_id=abcde12345; HttpOnly; Path=/; Secure" + example: 'session_id=abcde12345; HttpOnly; Path=/; Secure' content: application/json: schema: @@ -238,8 +238,8 @@ paths: properties: error: type: string - example: "Неверный фортмат email" - + example: 'Неверный формат email' + 401: description: Неверные учетные данные content: @@ -249,10 +249,10 @@ paths: properties: error: type: string - example: "Неверный email или пароль" + example: 'Неверный email или пароль' 500: $ref: '#/components/responses/ServerError' - + /auth/forgot-password: post: tags: [Authentication] @@ -269,7 +269,7 @@ paths: email: type: string format: email - example: "user@example.com" + example: 'user@example.com' required: [email] responses: 200: @@ -281,10 +281,10 @@ paths: properties: resetToken: type: string - example: "reset_xyz789" + example: 'reset_xyz789' message: type: string - example: "Код подтверждения отправлен на email" + example: 'Код подтверждения отправлен на email' 400: description: Неверный формат данных content: @@ -294,7 +294,7 @@ paths: properties: error: type: string - example: "Некорректный формат emaila" + example: 'Некорректный формат emaila' 404: description: Пользователь с указанным email не найден content: @@ -304,10 +304,10 @@ paths: properties: error: type: string - example: "Пользователь с таким email не зарегистрирован" + example: 'Пользователь с таким email не зарегистрирован' 500: $ref: '#/components/responses/ServerError' - + /auth/reset-password: post: tags: [Authentication] @@ -323,16 +323,16 @@ paths: properties: resetToken: type: string - example: "reset_xyz789" + example: 'reset_xyz789' code: type: string pattern: "^\\d{6}$" - example: "654321" + example: '654321' newPassword: type: string format: password minLength: 8 - example: "NewSecurePass123!" + example: 'NewSecurePass123!' required: [resetToken, code, newPassword] responses: 200: @@ -344,7 +344,7 @@ paths: properties: message: type: string - example: "Пароль успешно изменен" + example: 'Пароль успешно изменен' 400: description: Неверный код или токен content: @@ -354,10 +354,10 @@ paths: properties: error: type: string - example: "Неверный код подтверждения" + example: 'Неверный код подтверждения' 500: $ref: '#/components/responses/ServerError' - + /auth/check: get: tags: [Authentication] @@ -386,10 +386,10 @@ paths: example: false message: type: string - example: "Требуется авторизация" + example: 'Требуется авторизация' 500: $ref: '#/components/responses/ServerError' - + /auth/refresh: post: tags: [Authentication] @@ -406,7 +406,7 @@ paths: Set-Cookie: schema: type: string - example: "session_id=new_abcde12345; HttpOnly; Path=/; Secure" + example: 'session_id=new_abcde12345; HttpOnly; Path=/; Secure' content: application/json: schema: @@ -415,7 +415,7 @@ paths: $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /auth/logout: post: tags: [Authentication] @@ -432,10 +432,10 @@ paths: Set-Cookie: schema: type: string - example: "session_id=; HttpOnly; Path=/; Secure; Expires=Thu, 01 Jan 1970 00:00:00 GMT" + example: 'session_id=; HttpOnly; Path=/; Secure; Expires=Thu, 01 Jan 1970 00:00:00 GMT' 500: $ref: '#/components/responses/ServerError' - + /profile: get: tags: [Profile] @@ -455,35 +455,35 @@ paths: type: object properties: userStats: - $ref: "#/components/schemas/UserStats" + $ref: '#/components/schemas/UserStats' favoriteCards: type: array items: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' maxItems: 5 favoriteAchievements: type: array items: - $ref: "#/components/schemas/Achievement" + $ref: '#/components/schemas/Achievement' maxItems: 4 - user_ID: + user_id: type: integer example: 123 - description: "ID пользователя" + description: 'ID пользователя' username: type: string - example: "Коллекционер_123" - description: "Имя пользователя" + example: 'Коллекционер_123' + description: 'Имя пользователя' avatar_url: type: string format: url - example: "https://example.com/avatars/user123.jpg" - description: "Ссылка на аватар пользователя" + example: 'https://example.com/avatars/user123.jpg' + description: 'Ссылка на аватар пользователя' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /profile/achievements: get: tags: [Profile] @@ -503,10 +503,10 @@ paths: achievements: type: array items: - $ref: "#/components/schemas/Achievement" + $ref: '#/components/schemas/Achievement' 500: $ref: '#/components/responses/ServerError' - + /profile/achievements/favorites/count: get: tags: [Profile] @@ -532,7 +532,7 @@ paths: example: true 500: $ref: '#/components/responses/ServerError' - + /profile/achievements/{achievement_ID}/favorite-add: post: tags: [Profile] @@ -554,7 +554,7 @@ paths: description: Невозможно добавить в избранное (лимит достигнут) 500: $ref: '#/components/responses/ServerError' - + /profile/achievements/{achievement_ID}/favorite-delete: delete: tags: [Profile] @@ -574,7 +574,7 @@ paths: description: Успешно удалено из избранного 500: $ref: '#/components/responses/ServerError' - + /profile/settings: get: tags: [Profile] @@ -589,10 +589,10 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/UserSettings" + $ref: '#/components/schemas/UserSettings' 500: $ref: '#/components/responses/ServerError' - + /profile/settings-change: put: tags: [Profile] @@ -606,18 +606,18 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/UserSettings" + $ref: '#/components/schemas/UserSettings' responses: 200: description: Настройки обновлены content: application/json: schema: - $ref: "#/components/schemas/UserSettings" + $ref: '#/components/schemas/UserSettings' 500: $ref: '#/components/responses/ServerError' - - /other-profile/{user_ID}: + + /other-profile/{user_id}: get: tags: [OtherProfile] summary: Получение данных профиля другого пользователя @@ -627,7 +627,7 @@ paths: - Проверяет настройки приватности инвентаря parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -640,30 +640,30 @@ paths: type: object properties: userStats: - $ref: "#/components/schemas/UserStats" + $ref: '#/components/schemas/UserStats' favoriteCards: type: array items: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' maxItems: 5 favoriteAchievements: type: array items: - $ref: "#/components/schemas/Achievement" + $ref: '#/components/schemas/Achievement' maxItems: 4 inventoryVisible: type: boolean example: true id: type: integer - example: "456" + example: '456' username: type: string - example: "Игрок_456" + example: 'Игрок_456' 500: $ref: '#/components/responses/ServerError' - - /other-profile/{user_ID}/achievements: + + /other-profile/{user_id}/achievements: get: tags: [OtherProfile] summary: Получение списка достижений другого пользователя @@ -671,7 +671,7 @@ paths: Возвращает полный список достижений с текущим статусом выполнения parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -686,11 +686,11 @@ paths: achievements: type: array items: - $ref: "#/components/schemas/Achievement" + $ref: '#/components/schemas/Achievement' 500: $ref: '#/components/responses/ServerError' - /other-profile/{user_ID}/inventory: + /other-profile/{user_id}/inventory: get: tags: [OtherProfile] summary: Просмотр инвентаря другого пользователя @@ -698,7 +698,7 @@ paths: Возвращает инвентарь пользователя, если он не скрыт в настройках parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -713,11 +713,11 @@ paths: cards: type: array items: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' collections: type: array items: - $ref: "#/components/schemas/Collection" + $ref: '#/components/schemas/Collection' 403: description: Инвентарь скрыт content: @@ -727,10 +727,10 @@ paths: properties: error: type: string - example: "Пользователь скрыл свой инвентарь" + example: 'Пользователь скрыл свой инвентарь' 500: $ref: '#/components/responses/ServerError' - + /other-profile/inventory/sort: post: tags: [OtherProfile] @@ -740,7 +740,7 @@ paths: (по редкости, по коллекциям, механизм сортировки подробнее описан в инвентаре) parameters: - in: query - name: user_ID + name: user_id required: true schema: type: integer @@ -765,10 +765,10 @@ paths: inventory: type: array items: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' 500: $ref: '#/components/responses/ServerError' - + /other-profile/card/{card_ID}/view: get: tags: [OtherProfile] @@ -796,10 +796,10 @@ paths: type: object properties: card: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' 500: $ref: '#/components/responses/ServerError' - + /other-profile/card/{card_ID}/initiate-trade: post: tags: [OtherProfile] @@ -830,7 +830,7 @@ paths: properties: redirectUrl: type: string - example: "/trades/create?requested_card=123&owner=456" + example: '/trades/create?requested_card=123&owner=456' 400: description: Невозможно предложить обмен content: @@ -840,7 +840,7 @@ paths: properties: error: type: string - example: "Недостаточно карт для обмена" + example: 'Недостаточно карт для обмена' 409: description: Конфликт при попытке обмена content: @@ -850,25 +850,25 @@ paths: properties: error: type: string - example: "Невозможно предложить обмен - у владельца только один экземпляр этой карты" + example: 'Невозможно предложить обмен - у владельца только один экземпляр этой карты' cardQuantity: type: integer example: 1 - description: "Количество экземпляров карты у владельца" + description: 'Количество экземпляров карты у владельца' 500: $ref: '#/components/responses/ServerError' - - /other-profile/{user_ID}/report: + + /other-profile/{user_id}/report: post: tags: [OtherProfile] summary: Подача жалобы на пользователя description: | - Перенаправляет на страницу создания жалобы. + Перенаправляет на страницу создания жалобы. security: - bearerAuth: [] parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -882,15 +882,15 @@ paths: properties: reason: type: string - enum: ["Неуместный никнейм", "Неуместный аватар", "Другое"] - example: "Неуместный никнейм" - description: "Причина жалобы" + enum: ['Неуместный никнейм', 'Неуместный аватар', 'Другое'] + example: 'Неуместный никнейм' + description: 'Причина жалобы' comment: type: string maxLength: 500 nullable: true - example: "Никнейм содержит нецензурные слова" - description: "Дополнительные комментарии" + example: 'Никнейм содержит нецензурные слова' + description: 'Дополнительные комментарии' required: [reason] responses: 200: @@ -902,12 +902,12 @@ paths: properties: redirectUrl: type: string - example: "/report/create?reported_user=123" + example: '/report/create?reported_user=123' reportDraft: - $ref: "#/components/schemas/Report" + $ref: '#/components/schemas/Report' 500: $ref: '#/components/responses/ServerError' - + /inventory: get: tags: [Inventory] @@ -916,7 +916,7 @@ paths: Загружает данные инвентаря пользователя. - Проверяет авторизацию пользователя - Если пользователь не авторизован - возвращает ошибку 401 - - Если авторизован - возвращает список его карт, список избранных карт и баланс + - Если авторизован - возвращает список его карт, список избранных карт и баланс security: - bearerAuth: [] responses: @@ -930,24 +930,24 @@ paths: inventory: type: array items: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' favoriteCards: type: array items: type: integer - description: "ID избранных карт" + description: 'ID избранных карт' collections: type: array items: - $ref: "#/components/schemas/Collection" + $ref: '#/components/schemas/Collection' balance: type: integer example: 2500 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /inventory/card/{card_ID}/quantity: get: tags: [Inventory] @@ -1008,7 +1008,7 @@ paths: example: true 500: $ref: '#/components/responses/ServerError' - + /inventory/card/{card_ID}/favorite: get: tags: [Inventory] @@ -1079,7 +1079,7 @@ paths: description: Карта успешно удалена из избранного 500: $ref: '#/components/responses/ServerError' - + /inventory/sort: post: tags: [Inventory] @@ -1114,10 +1114,10 @@ paths: cards: type: array items: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' 500: $ref: '#/components/responses/ServerError' - + /inventory/destroy: post: tags: [Inventory] @@ -1137,7 +1137,7 @@ paths: properties: card_ID: type: integer - minimum: 1 + minimum: 1 example: 501 required: [card_ID] responses: @@ -1150,7 +1150,7 @@ paths: properties: newBalance: type: integer - example: 2550 + example: 2550 destroyedCardId: type: integer example: 501 @@ -1182,10 +1182,10 @@ paths: isOnTrade: type: boolean example: true - description: "Выставлена ли карта на обмен" + description: 'Выставлена ли карта на обмен' 500: $ref: '#/components/responses/ServerError' - + /inventory/put-on-trade: post: tags: [Inventory] @@ -1205,7 +1205,7 @@ paths: properties: card_ID: type: integer - minimum: 1 + minimum: 1 example: 501 required: [card_ID] responses: @@ -1218,12 +1218,12 @@ paths: properties: redirectUrl: type: string - example: "/trades/create?requested_card=123&owner=456" + example: '/trades/create?requested_card=123&owner=456' 400: description: Невозможно выставить карту на обмен 500: $ref: '#/components/responses/ServerError' - + /inventory/card/{card_ID}/trade-cancel: post: tags: [Inventory] @@ -1247,9 +1247,9 @@ paths: properties: message: type: string - example: "Карта успешно снята с обмена" + example: 'Карта успешно снята с обмена' 500: - $ref: '#/components/responses/ServerError' + $ref: '#/components/responses/ServerError' /home/search: get: tags: [Home] @@ -1270,7 +1270,7 @@ paths: schema: type: array items: - $ref: "#/components/schemas/User" + $ref: '#/components/schemas/User' 400: description: Неверный запрос content: @@ -1280,7 +1280,7 @@ paths: properties: error: type: string - example: "Введите минимум 3 символа для поиска" + example: 'Введите минимум 3 символа для поиска' 404: description: Пользователь не найден content: @@ -1290,10 +1290,10 @@ paths: properties: error: type: string - example: "Пользователь с введеными данными не найден" + example: 'Пользователь с введеными данными не найден' 500: $ref: '#/components/responses/ServerError' - + /home/notifications: get: tags: [Home] @@ -1310,12 +1310,12 @@ paths: schema: type: array items: - $ref: "#/components/schemas/Notification" + $ref: '#/components/schemas/Notification' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /home/notifications/{notification_ID}: get: tags: [Home] @@ -1331,19 +1331,19 @@ paths: schema: type: integer example: 123 - description: "ID уведомления" + description: 'ID уведомления' responses: 200: description: Детальная информация об уведомлении content: application/json: schema: - $ref: "#/components/schemas/Notification" + $ref: '#/components/schemas/Notification' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: - $ref: '#/components/responses/ServerError' - + $ref: '#/components/responses/ServerError' + /home/notifications/{notification_ID}/navigate: post: tags: [Home] @@ -1368,7 +1368,7 @@ paths: properties: redirectUrl: type: string - example: "/trades/123" + example: '/trades/123' 404: description: Предложение обмена не найдено content: @@ -1378,7 +1378,7 @@ paths: properties: error: type: string - example: "Данное предложение обмена уже не существует" + example: 'Данное предложение обмена уже не существует' 500: $ref: '#/components/responses/ServerError' /home/generate-card/themes: @@ -1397,7 +1397,7 @@ paths: schema: type: array items: - $ref: "#/components/schemas/Theme" + $ref: '#/components/schemas/Theme' 500: $ref: '#/components/responses/ServerError' /home/generate-card: @@ -1418,7 +1418,7 @@ paths: theme_ID: type: integer example: 901 - description: "ID выбранной темы" + description: 'ID выбранной темы' required: [theme_ID] responses: 200: @@ -1429,7 +1429,7 @@ paths: type: object properties: card: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' newBalance: type: integer example: 1500 @@ -1442,7 +1442,7 @@ paths: properties: error: type: string - example: "Недостаточно средств для генерации карты" + example: 'Недостаточно средств для генерации карты' requiredAmount: type: integer example: 1000 @@ -1462,7 +1462,7 @@ paths: schema: type: array items: - $ref: "#/components/schemas/News" + $ref: '#/components/schemas/News' 404: description: Новостей не найдено content: @@ -1472,7 +1472,7 @@ paths: properties: error: type: string - example: "Новостей пока нет" + example: 'Новостей пока нет' 500: $ref: '#/components/responses/ServerError' /home/news/{news_ID}: @@ -1493,7 +1493,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/News" + $ref: '#/components/schemas/News' 500: $ref: '#/components/responses/ServerError' /home/quests: @@ -1515,13 +1515,13 @@ paths: dailyQuests: type: array items: - $ref: "#/components/schemas/Quest" + $ref: '#/components/schemas/Quest' weeklyQuests: type: array items: - $ref: "#/components/schemas/Quest" + $ref: '#/components/schemas/Quest' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' /home/quests/{quest_ID}/change-status: @@ -1539,7 +1539,7 @@ paths: schema: type: integer example: 123 - description: "ID квеста" + description: 'ID квеста' responses: 200: description: Статус квеста успешно изменен @@ -1549,15 +1549,15 @@ paths: type: object properties: quest: - $ref: "#/components/schemas/Quest" + $ref: '#/components/schemas/Quest' message: type: string - example: "Статус квеста успешно изменен" + example: 'Статус квеста успешно изменен' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /home/quests/claim-reward: post: tags: [Home] @@ -1576,7 +1576,7 @@ paths: questType: type: string enum: [daily, weekly] - example: "daily" + example: 'daily' required: [questType] responses: 200: @@ -1592,7 +1592,7 @@ paths: receivedPacks: type: array items: - $ref: "#/components/schemas/Pack" + $ref: '#/components/schemas/Pack' newBalance: type: integer example: 2000 @@ -1605,7 +1605,7 @@ paths: properties: error: type: string - example: "Выполнены не все квесты, необходимые для получения награды" + example: 'Выполнены не все квесты, необходимые для получения награды' 500: $ref: '#/components/responses/ServerError' /trades: @@ -1629,7 +1629,7 @@ paths: schema: type: array items: - $ref: "#/components/schemas/Trade" + $ref: '#/components/schemas/Trade' 500: $ref: '#/components/responses/ServerError' /trades/{trade_id}: @@ -1650,7 +1650,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Trade" + $ref: '#/components/schemas/Trade' 404: description: Предложение обмена не найдено content: @@ -1660,7 +1660,7 @@ paths: properties: error: type: string - example: "Предложение обмена не найдено" + example: 'Предложение обмена не найдено' 500: $ref: '#/components/responses/ServerError' /trades/initiate: @@ -1685,7 +1685,7 @@ paths: requestedCardId: type: array items: - type: integer + type: integer description: ID карт, на которые пользователь готов совершить "быстрый обмен" example: [100, 200] required: [receivingUserId, requestedCardId, offeredCardId] @@ -1695,7 +1695,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Trade" + $ref: '#/components/schemas/Trade' 400: description: Невозможно создать предложение обмена content: @@ -1705,9 +1705,9 @@ paths: properties: error: type: string - example: "Недостаточно карт для обмена" + example: 'Недостаточно карт для обмена' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' /trades/my: @@ -1725,7 +1725,7 @@ paths: schema: type: integer example: 123 - description: "ID текущего пользователя" + description: 'ID текущего пользователя' responses: 200: description: Список моих обменов @@ -1734,9 +1734,9 @@ paths: schema: type: array items: - $ref: "#/components/schemas/Trade" + $ref: '#/components/schemas/Trade' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' /trades/{trade_id}/accept: @@ -1763,10 +1763,10 @@ paths: properties: message: type: string - example: "Обмен успешно завершен" - + example: 'Обмен успешно завершен' + 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 403: description: Нет прав для принятия этого обмена content: @@ -1776,10 +1776,10 @@ paths: properties: error: type: string - example: "Вы не можете принять этот обмен" + example: 'Вы не можете принять этот обмен' 500: $ref: '#/components/responses/ServerError' - + /trades/{trade_id}/reject: post: tags: [Trades] @@ -1804,12 +1804,12 @@ paths: properties: message: type: string - example: "Обмен отклонен" + example: 'Обмен отклонен' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /trades/{trade_id}/cancel: post: tags: [Trades] @@ -1834,12 +1834,12 @@ paths: properties: message: type: string - example: "Обмен отменен" + example: 'Обмен отменен' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /shop/packs: get: tags: [Shop] @@ -1854,7 +1854,7 @@ paths: schema: type: array items: - $ref: "#/components/schemas/Pack" + $ref: '#/components/schemas/Pack' 500: $ref: '#/components/responses/ServerError' @@ -1876,7 +1876,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Pack" + $ref: '#/components/schemas/Pack' 500: $ref: '#/components/responses/ServerError' /shop/packs/{pack_id}/buy: @@ -1906,11 +1906,11 @@ paths: receivedCards: type: array items: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' newBalance: type: integer 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 402: description: Недостаточно средств content: @@ -1920,7 +1920,7 @@ paths: properties: error: type: string - example: "Недостаточно средств на балансе" + example: 'Недостаточно средств на балансе' requiredAmount: type: integer example: 1000 @@ -1942,7 +1942,7 @@ paths: schema: type: integer example: 1 - description: "ID набора карт" + description: 'ID набора карт' responses: 200: description: Набор успешно открыт @@ -1954,17 +1954,17 @@ paths: receivedCards: type: array items: - $ref: "#/components/schemas/Card" - description: "Карты, полученные из набора" + $ref: '#/components/schemas/Card' + description: 'Карты, полученные из набора' newCardsAdded: type: boolean example: true - description: "Были ли карты успешно добавлены в инвентарь" + description: 'Были ли карты успешно добавлены в инвентарь' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /shop/coins/offers: get: tags: [Shop] @@ -1979,7 +1979,7 @@ paths: schema: type: array items: - $ref: "#/components/schemas/CoinOffer" + $ref: '#/components/schemas/CoinOffer' 500: $ref: '#/components/responses/ServerError' /shop/coins/offers/{offer_id}: @@ -2000,7 +2000,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/CoinOffer" + $ref: '#/components/schemas/CoinOffer' 500: $ref: '#/components/responses/ServerError' /shop/coins/offers/{offer_id}/purchase: @@ -2027,7 +2027,7 @@ paths: redirectUrl: type: string format: uri - example: "https://cardly.ru/shop/payment-callback" + example: 'https://cardly.ru/shop/payment-callback' required: [redirectUrl] responses: 200: @@ -2041,15 +2041,15 @@ paths: type: string format: uri 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' /shop/payments/process: post: tags: [Shop] - summary: Обработка платежа + summary: Обработка платежа description: | - обработка результата платежа от платежного шлюза + обработка результата платежа от платежного шлюза security: - bearerAuth: [] requestBody: @@ -2057,7 +2057,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/PaymentCallback" + $ref: '#/components/schemas/PaymentCallback' responses: 200: description: Платеж обработан @@ -2208,7 +2208,7 @@ paths: 403: $ref: '#/components/responses/Forbidden' 500: - $ref: '#/components/responses/ServerError' + $ref: '#/components/responses/ServerError' delete: tags: [Admin] summary: Удаление коллекцию @@ -2287,7 +2287,7 @@ paths: 403: $ref: '#/components/responses/Forbidden' 500: - $ref: '#/components/responses/ServerError' + $ref: '#/components/responses/ServerError' delete: tags: [Admin] summary: Удаление набор @@ -2335,7 +2335,7 @@ paths: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/ServerError' - + get: tags: [Admin] summary: Получение списка предложений покупки монет @@ -2357,7 +2357,7 @@ paths: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/ServerError' - + /admin/coin-offers/{offer_id}: put: tags: [Admin] @@ -2389,8 +2389,8 @@ paths: 403: $ref: '#/components/responses/Forbidden' 500: - $ref: '#/components/responses/ServerError' - + $ref: '#/components/responses/ServerError' + delete: tags: [Admin] summary: Удаление предложения покупки монет @@ -2469,7 +2469,7 @@ paths: 403: $ref: '#/components/responses/Forbidden' 500: - $ref: '#/components/responses/ServerError' + $ref: '#/components/responses/ServerError' delete: tags: [Admin] summary: Удаление новости @@ -2548,7 +2548,7 @@ paths: 403: $ref: '#/components/responses/Forbidden' 500: - $ref: '#/components/responses/ServerError' + $ref: '#/components/responses/ServerError' delete: tags: [Admin] summary: Удаление достижения @@ -2627,7 +2627,7 @@ paths: properties: message: type: string - example: "Обмен успешно аннулирован" + example: 'Обмен успешно аннулирован' 401: $ref: '#/components/responses/Unauthorized' 403: @@ -2704,20 +2704,20 @@ paths: status: type: string enum: [pending, reviewed, resolved] - example: "resolved" + example: 'resolved' action: type: string enum: [none, warning, temporary_ban, permanent_ban] - example: "temporary_ban" + example: 'temporary_ban' banDurationDays: type: integer nullable: true example: 7 - description: "Длительность бана в днях (только для temporary_ban)" + description: 'Длительность бана в днях (только для temporary_ban)' comment: type: string nullable: true - example: "Пользователь получил временный бан на 7 дней за нарушение правил" + example: 'Пользователь получил временный бан на 7 дней за нарушение правил' required: [status, action] responses: 200: @@ -2734,7 +2734,7 @@ paths: description: Жалоба не найдена 500: $ref: '#/components/responses/ServerError' - /admin/users/{user_ID}/ban: + /admin/users/{user_id}/ban: post: tags: [Admin] summary: Блокировка пользователя @@ -2743,7 +2743,7 @@ paths: - bearerAuth: [] parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -2757,15 +2757,15 @@ paths: banType: type: string enum: [temporary, permanent] - example: "temporary" + example: 'temporary' durationDays: type: integer nullable: true example: 7 - description: "Длительность бана в днях" + description: 'Длительность бана в днях' reason: type: string - example: "Нарушение правил сообщества" + example: 'Нарушение правил сообщества' required: [banType, reason] responses: 200: @@ -2777,12 +2777,12 @@ paths: properties: message: type: string - example: "Пользователь успешно заблокирован" + example: 'Пользователь успешно заблокирован' banEndDate: type: string format: date-time nullable: true - example: "2024-06-20T12:00:00Z" + example: '2024-06-20T12:00:00Z' 401: $ref: '#/components/responses/Unauthorized' 403: @@ -2791,7 +2791,7 @@ paths: description: Пользователь не найден 500: $ref: '#/components/responses/ServerError' - /admin/users/{user_ID}/unban: + /admin/users/{user_id}/unban: post: tags: [Admin] summary: Разблокировка пользователя @@ -2800,7 +2800,7 @@ paths: - bearerAuth: [] parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -2814,7 +2814,7 @@ paths: properties: message: type: string - example: "Пользователь успешно разблокирован" + example: 'Пользователь успешно разблокирован' 401: $ref: '#/components/responses/Unauthorized' 403: @@ -2823,7 +2823,7 @@ paths: description: Пользователь не найден 500: $ref: '#/components/responses/ServerError' - /admin/users/{user_ID}/delete: + /admin/users/{user_id}/delete: delete: tags: [Admin] summary: Удаление пользователя @@ -2832,7 +2832,7 @@ paths: - bearerAuth: [] parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -2846,8 +2846,8 @@ paths: 404: description: Пользователь не найден 500: - $ref: '#/components/responses/ServerError' - /admin/users/{user_ID}/inventory/{card_ID}: + $ref: '#/components/responses/ServerError' + /admin/users/{user_id}/inventory/{card_ID}: delete: tags: [Admin] summary: Удаление карты из инвентаря пользователя @@ -2856,7 +2856,7 @@ paths: - bearerAuth: [] parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -2876,7 +2876,7 @@ paths: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/ServerError' - /admin/users/{user_ID}/achievements/{achievement_ID}: + /admin/users/{user_id}/achievements/{achievement_ID}: delete: tags: [Admin] summary: Отзыв достижения у пользователя @@ -2885,7 +2885,7 @@ paths: - bearerAuth: [] parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -2905,7 +2905,7 @@ paths: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/ServerError' - /admin/users/{user_ID}/quests/{quest_ID}/reset: + /admin/users/{user_id}/quests/{quest_ID}/reset: post: tags: [Admin] summary: Сброс выполнения квеста @@ -2914,7 +2914,7 @@ paths: - bearerAuth: [] parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -2948,18 +2948,18 @@ paths: properties: message: type: string - example: "Квест успешно сброшен" + example: 'Квест успешно сброшен' rewardsRemoved: type: boolean example: true 400: - description: Неверный запрос + description: Неверный запрос 401: $ref: '#/components/responses/Unauthorized' 403: $ref: '#/components/responses/Forbidden' 500: - $ref: '#/components/responses/ServerError' + $ref: '#/components/responses/ServerError' /admin/stats: get: tags: [Admin] @@ -3004,7 +3004,7 @@ paths: 403: $ref: '#/components/responses/Forbidden' 500: - $ref: '#/components/responses/ServerError' + $ref: '#/components/responses/ServerError' components: schemas: News: @@ -3013,27 +3013,28 @@ components: news_ID: type: integer example: 1 - description: "Уникальный идентификатор новости" + description: 'Уникальный идентификатор новости' title: type: string - example: "Новое обновление системы" - description: "Заголовок новости" + example: 'Новое обновление системы' + description: 'Заголовок новости' content: type: string - example: "Мы добавили новые карты в коллекцию" - description: "Содержание новости" + example: 'Мы добавили новые карты в коллекцию' + description: 'Содержание новости' pictures: type: array items: type: string format: url - example: ["https://example.com/news1.jpg", "https://example.com/news2.jpg"] - description: "Ссылки на изображения" + example: + ['https://example.com/news1.jpg', 'https://example.com/news2.jpg'] + description: 'Ссылки на изображения' datePosted: type: string format: date-time - example: "2024-05-20T14:48:00Z" - description: "Дата публикации" + example: '2024-05-20T14:48:00Z' + description: 'Дата публикации' required: - news_ID - title @@ -3046,24 +3047,24 @@ components: achievement_ID: type: integer example: 101 - description: "Уникальный идентификатор достижения" + description: 'Уникальный идентификатор достижения' name: type: string - example: "Мастер коллекционирования" - description: "Название достижения" + example: 'Мастер коллекционирования' + description: 'Название достижения' imageURL: type: string format: url - example: "https://example.com/achievements/master.png" - description: "Ссылка на изображение" + example: 'https://example.com/achievements/master.png' + description: 'Ссылка на изображение' description: type: string - example: "Соберите 10 полных коллекций" - description: "Описание достижения" + example: 'Соберите 10 полных коллекций' + description: 'Описание достижения' isUnlocked: type: boolean example: false - description: "Статус получения" + description: 'Статус получения' required: - achievement_ID - name @@ -3077,26 +3078,26 @@ components: notification_ID: type: integer example: 201 - description: "Уникальный идентификатор уведомления" + description: 'Уникальный идентификатор уведомления' user: - $ref: "#/components/schemas/User" - description: "Пользователь, которому отправлено уведомление" + $ref: '#/components/schemas/User' + description: 'Пользователь, которому отправлено уведомление' message: type: string - example: "Ваша карта была успешно обменяна" - description: "Текст уведомления" + example: 'Ваша карта была успешно обменяна' + description: 'Текст уведомления' links: type: array items: type: string format: url - example: ["https://example.com/exchange-details"] - description: "Ссылка на подробности обмена" + example: ['https://example.com/exchange-details'] + description: 'Ссылка на подробности обмена' notificationDateTime: type: string format: date-time - example: "2024-05-20T15:30:00Z" - description: "Дата и время уведомления" + example: '2024-05-20T15:30:00Z' + description: 'Дата и время уведомления' required: - notification_ID - user @@ -3109,27 +3110,27 @@ components: report_ID: type: integer example: 301 - description: "Уникальный идентификатор жалобы" + description: 'Уникальный идентификатор жалобы' reporter: - $ref: "#/components/schemas/User" - description: "Пользователь, отправивший жалобу" + $ref: '#/components/schemas/User' + description: 'Пользователь, отправивший жалобу' reportedUser: - $ref: "#/components/schemas/User" - description: "Пользователь, на которого пожаловались" + $ref: '#/components/schemas/User' + description: 'Пользователь, на которого пожаловались' reportDateTime: type: string format: date-time - example: "2024-05-20T16:45:00Z" - description: "Дата и время жалобы" + example: '2024-05-20T16:45:00Z' + description: 'Дата и время жалобы' reason: type: string - example: "Непристойный контент" - description: "непристойный никнейм" + example: 'Непристойный контент' + description: 'непристойный никнейм' status: type: string - enum: ["На рассмотрении", "Подтверждено", "Отклонено"] - example: "На расмотрении" - description: "Статус рассмотрения" + enum: ['На рассмотрении', 'Подтверждено', 'Отклонено'] + example: 'На расмотрении' + description: 'Статус рассмотрения' required: - report_ID - reporter @@ -3144,25 +3145,25 @@ components: pack_ID: type: integer example: 401 - description: "Уникальный идентификатор набора" + description: 'Уникальный идентификатор набора' name: type: string - example: "Стартовый набор" - description: "Название набора" + example: 'Стартовый набор' + description: 'Название набора' imageURL: type: string format: url - example: "https://example.com/packs/starter.png" - description: "Ссылка на изображение" + example: 'https://example.com/packs/starter.png' + description: 'Ссылка на изображение' cards: type: array items: - $ref: "#/components/schemas/Card" - description: "Карты в наборе" + $ref: '#/components/schemas/Card' + description: 'Карты в наборе' price: type: integer example: 1000 - description: "Цена" + description: 'Цена' required: - pack_ID - name @@ -3176,38 +3177,38 @@ components: card_ID: type: integer example: 501 - description: "Уникальный идентификатор карты" + description: 'Уникальный идентификатор карты' name: type: string - example: "Ледяной феникс" - description: "Название карты" + example: 'Ледяной феникс' + description: 'Название карты' imageURL: type: string format: url - example: "https://example.com/cards/pheonix.png" - description: "Ссылка на изображение" + example: 'https://example.com/cards/pheonix.png' + description: 'Ссылка на изображение' description: type: string - example: "Ледяной Феникс — это мифическое существо, воплощающее силу зимы и вечного обновления. Его перья сверкают как морозный утренний иней, а глаза светятся холодным синим светом." - description: "Описание карты" + example: 'Ледяной Феникс — это мифическое существо, воплощающее силу зимы и вечного обновления. Его перья сверкают как морозный утренний иней, а глаза светятся холодным синим светом.' + description: 'Описание карты' rarity: type: string - enum: ["Обычная", "Редкая", "Эпическая", "Легендарная", "Уникальная"] - example: "Редкая" - description: "Редкость карты" + enum: ['Обычная', 'Редкая', 'Эпическая', 'Легендарная', 'Уникальная'] + example: 'Редкая' + description: 'Редкость карты' min_price: type: integer example: 50 - description: "Минимальная цена - используется для разбора карточки" + description: 'Минимальная цена - используется для разбора карточки' theme: type: string nullable: true - example: "Мифическое существо" - description: "Тематика, на которую была сгенерирвана карта (только для уникальных карт)" + example: 'Мифическое существо' + description: 'Тематика, на которую была сгенерирвана карта (только для уникальных карт)' isGenerated: type: boolean example: false - description: "Флаг показывающий сгенерированная ли карта (true для уникальных, false для остальных)" + description: 'Флаг показывающий сгенерированная ли карта (true для уникальных, false для остальных)' required: - card_ID - name @@ -3219,65 +3220,65 @@ components: User: type: object properties: - user_ID: + user_id: type: integer example: 1 - description: "Уникальный идентификатор" + description: 'Уникальный идентификатор' username: type: string - example: "Коллекционер_123" - description: "Имя пользователя" + example: 'Коллекционер_123' + description: 'Имя пользователя' email: type: string format: email - example: "user@example.com" - description: "Электронная почта" + example: 'user@example.com' + description: 'Электронная почта' password: type: string format: password writeOnly: true - description: "Пароль" + description: 'Пароль' favoriteCards: type: array items: - $ref: "#/components/schemas/Card" - description: "Избранные карты" + $ref: '#/components/schemas/Card' + description: 'Избранные карты' onChange: type: array items: - $ref: "#/components/schemas/Card" - description: "Карты на обмен" + $ref: '#/components/schemas/Card' + description: 'Карты на обмен' inventoryCards: type: array items: - $ref: "#/components/schemas/Card" - description: "Карты в инвентаре" + $ref: '#/components/schemas/Card' + description: 'Карты в инвентаре' balance: type: integer example: 2500 - description: "Баланс" + description: 'Баланс' avatar_url: type: string format: url - example: "https://example.com/avatars/user1.png" - description: "Аватар пользователя" + example: 'https://example.com/avatars/user1.png' + description: 'Аватар пользователя' achievements: type: array items: - $ref: "#/components/schemas/Achievement" - description: "Достижения" + $ref: '#/components/schemas/Achievement' + description: 'Достижения' favoriteAchievements: type: array items: - $ref: "#/components/schemas/Achievement" - description: "Избранные достижения" + $ref: '#/components/schemas/Achievement' + description: 'Избранные достижения' notifications: type: array items: - $ref: "#/components/schemas/Notification" - description: "Уведомления" + $ref: '#/components/schemas/Notification' + description: 'Уведомления' required: - - user_ID + - user_id - username - email - password @@ -3291,31 +3292,31 @@ components: payment_ID: type: integer example: 601 - description: "Уникальный идентификатор платежа" + description: 'Уникальный идентификатор платежа' user: - $ref: "#/components/schemas/User" - description: "Пользователь" + $ref: '#/components/schemas/User' + description: 'Пользователь' paymentSUM: type: number format: double example: 199.99 - description: "Сумма платежа" + description: 'Сумма платежа' bankCardData: type: array items: type: string writeOnly: true - description: "Данные карты (только для записи)" + description: 'Данные карты (только для записи)' paymentStatus: type: string - enum: ["В обработке", "Оплачено", "Ошибка"] - example: "Оплачено" - description: "Статус платежа" + enum: ['В обработке', 'Оплачено', 'Ошибка'] + example: 'Оплачено' + description: 'Статус платежа' paymentDateTime: type: string format: date-time - example: "2024-05-20T17:30:00Z" - description: "Дата и время платежа" + example: '2024-05-20T17:30:00Z' + description: 'Дата и время платежа' required: - payment_ID - user @@ -3329,32 +3330,32 @@ components: trade_ID: type: integer example: 701 - description: "Уникальный идентификатор обмена" + description: 'Уникальный идентификатор обмена' offeringUser: - $ref: "#/components/schemas/User" - description: "Пользователь, предлагающий обмен" + $ref: '#/components/schemas/User' + description: 'Пользователь, предлагающий обмен' offeringCards: type: array items: - $ref: "#/components/schemas/Card" - description: "Предлагаемые карты" + $ref: '#/components/schemas/Card' + description: 'Предлагаемые карты' receivingUser: - $ref: "#/components/schemas/User" - description: "Пользователь, получающий предложение" + $ref: '#/components/schemas/User' + description: 'Пользователь, получающий предложение' receivingCards: type: array items: - $ref: "#/components/schemas/Card" - description: "Запрашиваемые карты" + $ref: '#/components/schemas/Card' + description: 'Запрашиваемые карты' isConfirmed: type: boolean example: false - description: "Статус подтверждения" + description: 'Статус подтверждения' tradeDateTime: type: string format: date-time - example: "2024-05-20T18:00:00Z" - description: "Дата и время обмена" + example: '2024-05-20T18:00:00Z' + description: 'Дата и время обмена' required: - trade_ID - offeringUser @@ -3370,21 +3371,21 @@ components: collection_ID: type: integer example: 801 - description: "Уникальный идентификатор коллекции" + description: 'Уникальный идентификатор коллекции' name: type: string - example: "Драконы" - description: "Название коллекции" + example: 'Драконы' + description: 'Название коллекции' cards: type: array items: - $ref: "#/components/schemas/Card" - description: "Карты в коллекции" + $ref: '#/components/schemas/Card' + description: 'Карты в коллекции' imageURL: type: string format: url - example: "https://example.com/collections/dragons.png" - description: "Обложка коллекции" + example: 'https://example.com/collections/dragons.png' + description: 'Обложка коллекции' required: - collection_ID - name @@ -3397,19 +3398,19 @@ components: theme_ID: type: integer example: 901 - description: "Уникальный идентификатор темы" + description: 'Уникальный идентификатор темы' name: type: string - example: "Мифические существа" - description: "Название темы" + example: 'Мифические существа' + description: 'Название темы' description: type: string - example: "Создайте изображение мифического существа" - description: "Описание темы" + example: 'Создайте изображение мифического существа' + description: 'Описание темы' required: - theme_ID - name - + UserStats: type: object properties: @@ -3428,7 +3429,7 @@ components: example: 1 name: type: string - example: "Стартовый набор" + example: 'Стартовый набор' coinsAmount: type: integer example: 100 @@ -3442,7 +3443,7 @@ components: description: type: string nullable: true - example: "Стартовый набор. Вы можете получить..." + example: 'Стартовый набор. Вы можете получить...' PaymentCallback: type: object @@ -3451,7 +3452,7 @@ components: type: string status: type: string - enum: ["Успешно", "Ошибка", "В процессе"] + enum: ['Успешно', 'Ошибка', 'В процессе'] amount: type: number currency: @@ -3463,22 +3464,21 @@ components: - status - amount - UserSettings: type: object properties: notificationsEnabled: type: boolean example: true - description: "Включены ли уведомления" + description: 'Включены ли уведомления' showInventory: type: boolean example: false - description: "Виден ли инвентарь другим пользователям" + description: 'Виден ли инвентарь другим пользователям' autoDeclineTrades: type: boolean example: false - description: "Автоматически отклонять все входящие предложения обмена" + description: 'Автоматически отклонять все входящие предложения обмена' required: - notificationsEnabled - showInventory @@ -3490,40 +3490,40 @@ components: quest_ID: type: integer example: 1 - description: "Уникальный идентификатор квеста" + description: 'Уникальный идентификатор квеста' name: type: string - example: "Соберите 5 карт" - description: "Название квеста" + example: 'Соберите 5 карт' + description: 'Название квеста' description: type: string - example: "Соберите 5 карт из коллекции Драконы" - description: "Описание квеста" + example: 'Соберите 5 карт из коллекции Драконы' + description: 'Описание квеста' progress: type: integer example: 3 - description: "Текущий прогресс" + description: 'Текущий прогресс' target: type: integer example: 5 - description: "Целевое значение" + description: 'Целевое значение' rewardCoins: type: integer example: 200 - description: "Награда в монетах" + description: 'Награда в монетах' rewardPacks: type: array items: - $ref: "#/components/schemas/Pack" - description: "Награда в наборах" + $ref: '#/components/schemas/Pack' + description: 'Награда в наборах' isCompleted: type: boolean example: false - description: "Выполнен ли квест" + description: 'Выполнен ли квест' isClaimed: type: boolean example: false - description: "Получена ли награда" + description: 'Получена ли награда' required: - quest_ID - name @@ -3537,10 +3537,10 @@ components: responses: BadRequest: description: Неверный формат запроса - + Unauthorized: description: Ошибка аутентификации - + ServerError: description: Что-то пошло не так content: @@ -3550,24 +3550,23 @@ components: properties: error: type: string - example: "Что-то пошло не так, попробуйте позже" + example: 'Что-то пошло не так, попробуйте позже' code: type: string - example: "INTERNAL_SERVER_ERROR" + example: 'INTERNAL_SERVER_ERROR' timestamp: type: string format: date-time - example: "2024-05-20T12:00:00Z" + example: '2024-05-20T12:00:00Z' - InvalidToken: description: Невалидный токен - + Forbidden: description: Доступ запрещен - + securitySchemes: bearerAuth: type: http scheme: bearer - bearerFormat: JWT \ No newline at end of file + bearerFormat: JWT diff --git a/backend/.dockerignore b/backend/.dockerignore index ba87d4e..2b66d13 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -1,3 +1,5 @@ +.env.example + # IntelliJ IDEA / VSCode .idea/ .vscode/ diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..3bcf4a6 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,38 @@ +### --- DATABASE CONFIG --- ### +DB_URL=jdbc:postgresql://localhost:5432/cardly +DB_USERNAME=your_db_username +DB_PASSWORD=your_db_password +DB_DRIVER=org.postgresql.Driver + +### --- JPA CONFIG --- ### +JPA_DDL_AUTO=update +JPA_DIALECT=org.hibernate.dialect.PostgreSQLDialect +JPA_SHOW_SQL=true + +### --- METRICS / PROMETHEUS --- ### +MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE=* +METRICS_PROMETHEUS_ENABLED=true + +### --- JWT CONFIG --- ### +JWT_SECRET=your_super_secret_jwt_key +JWT_EXPIRATION=2592000000 + +### --- SERVER CONFIG --- ### +SERVER_PORT=8080 + +### --- LOGGING CONFIG --- ### +SPRING_SECURITY_LOG_LEVEL=INFO + +### --- MAIL CONFIG --- ### +EMAIL_HOST=smtp.yandex.ru +EMAIL_PORT=587 +EMAIL_USERNAME=your_email@yandex.ru +EMAIL_PASSWORD=your_email_password +EMAIL_SMTP_AUTH=true +EMAIL_SMTP_STARTTLS_ENABLE=true + +### --- SWAGGER CONFIG --- ### +SWAGGER_UI_PATH=/swagger-ui.html +API_DOCS_PATH=/v3/api-docs +SWAGGER_OPERATIONS_SORTER=method +SWAGGER_TAGS_SORTER=alpha diff --git a/backend/.gitignore b/backend/.gitignore index 45cb293..4886d99 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ *.log +*.env ### STS ### .apt_generated diff --git a/backend/build.gradle b/backend/build.gradle index 997b35b..e2865c2 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -26,28 +26,42 @@ repositories { } dependencies { + // Core Spring + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-mail' - implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' - implementation 'org.jetbrains.kotlin:kotlin-reflect' - implementation 'io.jsonwebtoken:jjwt-api:0.11.5' - implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' - implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'io.github.cdimascio:dotenv-java:3.0.0' // OpenAPI 3.0 (Swagger) для документации implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' implementation 'org.springdoc:springdoc-openapi-starter-common:2.3.0' - compileOnly 'org.projectlombok:lombok' + // DB runtimeOnly 'org.postgresql:postgresql' + + // Метрики + implementation 'io.micrometer:micrometer-registry-prometheus' + + // Kotlin + Jackson + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' + implementation 'org.jetbrains.kotlin:kotlin-reflect' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // Lombok (только на этапе компиляции) + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + + // Тестирование testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' testImplementation 'org.springframework.security:spring-security-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } kotlin { diff --git a/backend/configs/grafana/dashboards/App Observitility.json b/backend/configs/grafana/dashboards/App Observitility.json new file mode 100644 index 0000000..93e3e9e --- /dev/null +++ b/backend/configs/grafana/dashboards/App Observitility.json @@ -0,0 +1,681 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 4, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 13, + "panels": [], + "title": "Аналитика по авторизации", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "expr": "auth_register_duration_seconds", + "refId": "A" + } + ], + "title": "📈 Продолжительность регистрации", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 7 + }, + "id": 4, + "options": { + "displayMode": "gradient", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "color" + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "code", + "expr": "auth_register_attempt_total", + "fullMetaSearch": false, + "includeNullMetadata": true, + "range": true, + "refId": "Всего регистрационных запросов", + "useBackend": false + }, + { + "expr": "auth_register_conflict_total", + "refId": "B" + }, + { + "expr": "auth_register_failure_total", + "refId": "C" + } + ], + "title": "🔐 Статистика аутентификации", + "type": "bargauge" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 13 + }, + "id": 12, + "panels": [], + "title": "Аналитика машины", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 14 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "application_ready_time_seconds", + "refId": "A" + } + ], + "title": "Время готовности приложения", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 14 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "expr": "application_started_time_seconds", + "refId": "A" + } + ], + "title": "🚀 Время запуска приложения", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "process_cpu_usage{instance=\"app:8080\", job=\"app\"}" + }, + "properties": [ + { + "id": "displayName", + "value": "Process cpu usage" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "system_cpu_usage{instance=\"app:8080\", job=\"app\"}" + }, + "properties": [ + { + "id": "displayName", + "value": "System cpu usage" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 14 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "process_cpu_usage", + "range": true, + "refId": "A" + }, + { + "expr": "system_cpu_usage", + "refId": "B" + } + ], + "title": "🔥 Использование процессора", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 12, + "x": 12, + "y": 20 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "code", + "expr": "max(process_uptime_seconds{job=\"app\", instance=\"app:8080\"})", + "fullMetaSearch": false, + "includeNullMetadata": true, + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "🕒 Время работы процесса", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 12, + "x": 0, + "y": 22 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "expr": "http_server_requests_active_seconds", + "refId": "A" + } + ], + "title": "📊 Активные HTTP-запросы", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 25 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "expr": "http_server_requests_seconds", + "refId": "A" + } + ], + "title": "🌐 Время обработки HTTP-запросов", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "10s", + "schemaVersion": 41, + "tags": ["app", "system", "monitoring"], + "templating": { + "list": [] + }, + "time": { + "from": "2025-05-25T15:28:11.694Z", + "to": "2025-05-26T03:28:11.694Z" + }, + "timepicker": {}, + "timezone": "browser", + "title": "🔥 Cardly", + "uid": "aemzyawh463nke", + "version": 21 +} diff --git a/backend/configs/grafana/datasources.yaml b/backend/configs/grafana/datasources.yaml new file mode 100644 index 0000000..9a4ee8c --- /dev/null +++ b/backend/configs/grafana/datasources.yaml @@ -0,0 +1,17 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + basicAuth: false + isDefault: true + editable: true + jsonData: + tlsSkipVerify: true + httpMethod: POST + timeInterval: 1s + maxLines: 10000 + minInterval: 1s + readOnly: false \ No newline at end of file diff --git a/backend/configs/prometheus.yml b/backend/configs/prometheus.yml new file mode 100644 index 0000000..d084519 --- /dev/null +++ b/backend/configs/prometheus.yml @@ -0,0 +1,12 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'app' + metrics_path: '/actuator/prometheus' + static_configs: + - targets: ['app:8080'] + + - job_name: 'node-exporter' + static_configs: + - targets: ['node-exporter:9100'] diff --git a/backend/db/migration/V1__init_tables.sql b/backend/db/migration/V1__init_tables.sql new file mode 100644 index 0000000..1da3848 --- /dev/null +++ b/backend/db/migration/V1__init_tables.sql @@ -0,0 +1,195 @@ +-- Таблица users +CREATE TABLE users ( + user_id SERIAL PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + username VARCHAR(255) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + is_enabled BOOLEAN NOT NULL DEFAULT FALSE, + activation_token VARCHAR(255), + password_reset_token VARCHAR(255), + password_reset_token_expiry BIGINT, + balance INTEGER NOT NULL DEFAULT 1000, + avatar_url VARCHAR(255) +); + +-- Таблица collections +CREATE TABLE collections ( + collection_id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + image_url VARCHAR(255) NOT NULL +); + +-- Таблица cards +CREATE TABLE cards ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + image_url VARCHAR(255) NOT NULL, + rarity VARCHAR(50) NOT NULL, + disassemble_price INTEGER NOT NULL, + is_generated BOOLEAN NOT NULL, + description TEXT, + theme VARCHAR(255), + collection_id INTEGER, + owner_id INTEGER, + CONSTRAINT fk_collection FOREIGN KEY (collection_id) REFERENCES collections(collection_id), + CONSTRAINT fk_owner FOREIGN KEY (owner_id) REFERENCES users(user_id) +); + +-- Таблица achievements +CREATE TABLE achievements ( + achievement_id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + image_url VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + is_unlocked BOOLEAN NOT NULL DEFAULT FALSE +); + +-- Таблица notifications +CREATE TABLE notifications ( + notification_id SERIAL PRIMARY KEY, + user_id INT NOT NULL, + message TEXT NOT NULL, + notification_date_time TIMESTAMP NOT NULL, + CONSTRAINT fk_user FOREIGN KEY(user_id) REFERENCES users(user_id) +); + +-- Таблица packs +CREATE TABLE packs ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + image_url VARCHAR(255) NOT NULL, + price INT NOT NULL +); + +-- Таблица quests +CREATE TABLE quests ( + quest_id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + progress INT NOT NULL, + target INT NOT NULL, + reward_coins INT NOT NULL, + is_completed BOOLEAN NOT NULL, + is_claimed BOOLEAN NOT NULL +); + +-- Таблица user_inventory_cards (многие ко многим) +CREATE TABLE user_inventory_cards ( + user_id INTEGER NOT NULL, + card_id INTEGER NOT NULL, + PRIMARY KEY (user_id, card_id), + CONSTRAINT fk_inventory_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + CONSTRAINT fk_inventory_card FOREIGN KEY (card_id) REFERENCES cards(id) ON DELETE CASCADE +); + +-- Таблица user_favorite_cards (многие ко многим) +CREATE TABLE user_favorite_cards ( + user_id INTEGER NOT NULL, + card_id INTEGER NOT NULL, + PRIMARY KEY (user_id, card_id), + CONSTRAINT fk_favorite_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + CONSTRAINT fk_favorite_card FOREIGN KEY (card_id) REFERENCES cards(id) ON DELETE CASCADE +); + +-- Таблица user_onchange_cards (многие ко многим) +CREATE TABLE user_onchange_cards ( + user_id INTEGER NOT NULL, + card_id INTEGER NOT NULL, + PRIMARY KEY (user_id, card_id), + CONSTRAINT fk_onchange_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + CONSTRAINT fk_onchange_card FOREIGN KEY (card_id) REFERENCES cards(id) ON DELETE CASCADE +); + +-- Таблица user_achievements (многие ко многим) +CREATE TABLE user_achievements ( + user_id INTEGER NOT NULL, + achievement_id INTEGER NOT NULL, + PRIMARY KEY (user_id, achievement_id), + CONSTRAINT fk_achievements_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + CONSTRAINT fk_achievements_achievement FOREIGN KEY (achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE +); + +-- Таблица user_favorite_achievements (многие ко многим) +CREATE TABLE user_favorite_achievements ( + user_id INTEGER NOT NULL, + achievement_id INTEGER NOT NULL, + PRIMARY KEY (user_id, achievement_id), + CONSTRAINT fk_favorite_achievements_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + CONSTRAINT fk_favorite_achievements_achievement FOREIGN KEY (achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE +); + +-- Таблица user_notifications (многие ко многим) +CREATE TABLE user_notifications ( + user_id INTEGER NOT NULL, + notification_id INTEGER NOT NULL, + PRIMARY KEY (user_id, notification_id), + CONSTRAINT fk_notifications_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + CONSTRAINT fk_notifications_notification FOREIGN KEY (notification_id) REFERENCES notifications(notification_id) ON DELETE CASCADE +); + +-- Таблица pack_cards +CREATE TABLE pack_cards ( + pack_id BIGINT NOT NULL, + card_id INT NOT NULL, + CONSTRAINT fk_pack FOREIGN KEY (pack_id) REFERENCES packs(id) ON DELETE CASCADE, + CONSTRAINT fk_card FOREIGN KEY (card_id) REFERENCES cards(id) ON DELETE CASCADE, + PRIMARY KEY (pack_id, card_id) +); + +-- Таблица quest_reward_packs +CREATE TABLE quest_reward_packs ( + quest_id INT NOT NULL, + pack_id BIGINT NOT NULL, + CONSTRAINT fk_quest FOREIGN KEY (quest_id) REFERENCES quests(quest_id) ON DELETE CASCADE, + CONSTRAINT fk_pack FOREIGN KEY (pack_id) REFERENCES packs(id) ON DELETE CASCADE, + PRIMARY KEY (quest_id, pack_id) +); + +-- Таблица coin_offers +CREATE TABLE coin_offers ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + coins_amount INT NOT NULL, + price DOUBLE PRECISION NOT NULL, + image_url VARCHAR(255) NOT NULL, + description VARCHAR(1000) +); + +-- Таблица news_entity +CREATE TABLE news_entity ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL +); + +-- Таблица notification_links +CREATE TABLE notification_links ( + notification_id INT NOT NULL, + link TEXT NOT NULL, + CONSTRAINT fk_notification FOREIGN KEY(notification_id) REFERENCES notifications(notification_id) +); + +-- Таблица reports +CREATE TABLE reports ( + id BIGSERIAL PRIMARY KEY, + reporter_id INT NOT NULL, + reported_user_id INT NOT NULL, + report_date_time TIMESTAMP NOT NULL, + reason TEXT NOT NULL, + status VARCHAR(50) NOT NULL, + + CONSTRAINT fk_reporter FOREIGN KEY (reporter_id) REFERENCES users(user_id) ON DELETE CASCADE, + CONSTRAINT fk_reported_user FOREIGN KEY (reported_user_id) REFERENCES users(user_id) ON DELETE CASCADE +); + +-- Таблица user_stats +CREATE TABLE user_stats ( + id BIGSERIAL PRIMARY KEY, + total_cards INT NOT NULL DEFAULT 0, + completed_collections INT NOT NULL DEFAULT 0, + user_id INT NOT NULL UNIQUE, + + CONSTRAINT fk_user_stats_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE +); diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index deecaea..56582a8 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -1,18 +1,19 @@ services: app: + container_name: cardly_app build: . ports: - "8080:8080" - environment: - - DB_HOST=db - - DB_PORT=5432 - - DB_NAME=auth_db - - DB_USER=appuser - - DB_PASSWORD=password + env_file: + - .env depends_on: - db + networks: + - backend + - monitoring db: + container_name: cardly_db image: postgres:14 ports: - "5432:5432" @@ -21,7 +22,90 @@ services: - POSTGRES_USER=appuser - POSTGRES_PASSWORD=password volumes: - - postgres-data:/var/lib/postgresql/data + - postgres_data:/var/lib/postgresql/data + networks: + - backend + + prometheus: + container_name: cardly_prometheus + image: prom/prometheus + volumes: + - ./configs/prometheus.yml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + networks: + - monitoring + + grafana: + container_name: cardly_grafana + image: grafana/grafana + ports: + - "3000:3000" + volumes: + - ./configs/grafana/datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml + - grafana_data:/var/lib/grafana + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + networks: + - monitoring + + node-exporter: + container_name: cardly_node_exporter + image: prom/node-exporter + ports: + - "9100:9100" + networks: + - monitoring + restart: unless-stopped + pid: "host" + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($|/)' + flyway: + container_name: cardly_flyway + image: flyway/flyway + depends_on: + - db + command: -url=jdbc:postgresql://db:5432/auth_db -user=appuser -password=password -connectRetries=10 migrate + volumes: + - ./db/migration:/flyway/sql + networks: + - backend + # НЕ запускаем автоматически + deploy: + replicas: 0 + + pgadmin: + container_name: cardly_pgadmin + image: dpage/pgadmin4 + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: admin@example.com + PGADMIN_DEFAULT_PASSWORD: admin + ports: + - "5050:80" + volumes: + - pgadmin_data:/var/lib/pgadmin + depends_on: + - db + networks: + - backend + volumes: - postgres-data: \ No newline at end of file + postgres_data: + grafana_data: + pgadmin_data: + +networks: + backend: + driver: bridge + monitoring: + driver: bridge \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/AppApplication.kt b/backend/src/main/kotlin/ru/vsu/app/AppApplication.kt deleted file mode 100644 index 95b267d..0000000 --- a/backend/src/main/kotlin/ru/vsu/app/AppApplication.kt +++ /dev/null @@ -1,11 +0,0 @@ -package ru.vsu.app - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication - -@SpringBootApplication -class AppApplication - -fun main(args: Array) { - runApplication(*args) -} diff --git a/backend/src/main/kotlin/ru/vsu/app/Application.kt b/backend/src/main/kotlin/ru/vsu/app/Application.kt new file mode 100644 index 0000000..dd8a1ea --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/Application.kt @@ -0,0 +1,22 @@ +package ru.vsu.app + +import io.github.cdimascio.dotenv.Dotenv +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class Application + +fun main(args: Array) { + val dotenv = Dotenv.configure() + .ignoreIfMissing() + .load() + + dotenv.entries().forEach { entry -> + if (System.getenv(entry.key) == null && System.getProperty(entry.key) == null) { + System.setProperty(entry.key, entry.value) + } + } + + runApplication(*args) +} diff --git a/backend/src/main/kotlin/ru/vsu/app/config/OpenApiConfig.kt b/backend/src/main/kotlin/ru/vsu/app/config/OpenApiConfig.kt index 9df3c64..822b9d8 100644 --- a/backend/src/main/kotlin/ru/vsu/app/config/OpenApiConfig.kt +++ b/backend/src/main/kotlin/ru/vsu/app/config/OpenApiConfig.kt @@ -32,13 +32,13 @@ class OpenApiConfig { private fun getApiInfo(): Info { return Info() - .title("REST API Приложения") - .description("API для аутентификации, сброса пароля и получения информации о пользователе") + .title("REST API Cardly") + .description("API для взаимодействия с приложением Cardly") .version("1.0") .contact( Contact() - .name("Anastasia") - .email("example@example.com") + .name("Anna Dobrova") + .email("anyaadobrova@gmail.com") ) } } \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/config/PublicEndpointsConfig.kt b/backend/src/main/kotlin/ru/vsu/app/config/PublicEndpointsConfig.kt new file mode 100644 index 0000000..bff084b --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/config/PublicEndpointsConfig.kt @@ -0,0 +1,24 @@ +package ru.vsu.app.config + +import org.springframework.stereotype.Component + +@Component +class PublicEndpointsConfig { + val endpoints = listOf( + "/api/auth/register", + "/api/auth/login", + "/api/auth/forgot-password", + "/api/auth/reset-password", + "/api/auth/activate", + "/api/auth/resend-code", + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html", + "/webjars/**", + "/actuator/prometheus", + "/error" + ) + val passiveTokenEndpoints = listOf( + "/api/auth/check" + ) +} diff --git a/backend/src/main/kotlin/ru/vsu/app/config/SecurityConfig.kt b/backend/src/main/kotlin/ru/vsu/app/config/SecurityConfig.kt index b5ae8d5..f967653 100644 --- a/backend/src/main/kotlin/ru/vsu/app/config/SecurityConfig.kt +++ b/backend/src/main/kotlin/ru/vsu/app/config/SecurityConfig.kt @@ -15,12 +15,14 @@ import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import ru.vsu.app.security.JwtAuthenticationFilter +import ru.vsu.app.config.PublicEndpointsConfig @Configuration @EnableWebSecurity class SecurityConfig( private val jwtAuthFilter: JwtAuthenticationFilter, - private val userDetailsService: UserDetailsService + private val userDetailsService: UserDetailsService, + private val publicEndpointsConfig: PublicEndpointsConfig ) { @Bean @@ -28,17 +30,7 @@ class SecurityConfig( http .csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers( - "/api/auth/register", - "/api/auth/login", - "/api/auth/forgot-password", - "/api/auth/reset-password", - "/api/auth/activate", - "/v3/api-docs/**", - "/swagger-ui/**", - "/swagger-ui.html", - "/webjars/**" - ).permitAll() + it.requestMatchers(*publicEndpointsConfig.endpoints.toTypedArray(), *publicEndpointsConfig.passiveTokenEndpoints.toTypedArray()).permitAll() .anyRequest().authenticated() } .sessionManagement { diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt new file mode 100644 index 0000000..6b85ed2 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt @@ -0,0 +1,699 @@ +package ru.vsu.app.controller + +import ru.vsu.app.dto.AchievementDto +import ru.vsu.app.dto.requests.AdminReportsReportIDPutRequest +import ru.vsu.app.dto.responses.admin.AdminStatsGet200Response +import ru.vsu.app.dto.responses.admin.AdminTradesTradeIDInvalidatePost200Response +import ru.vsu.app.dto.requests.AdminTradesTradeIDInvalidatePostRequest +import ru.vsu.app.dto.responses.admin.AdminUsersUserIDBanPost200Response +import ru.vsu.app.dto.requests.AdminUsersUserIDBanPostRequest +import ru.vsu.app.dto.responses.admin.AdminUsersUserIDQuestsQuestIDResetPost200Response +import ru.vsu.app.dto.requests.AdminUsersUserIDQuestsQuestIDResetPostRequest +import ru.vsu.app.dto.responses.admin.AdminUsersUserIDUnbanPost200Response +import ru.vsu.app.dto.CardDto +import ru.vsu.app.dto.CoinOfferDto +import ru.vsu.app.dto.CollectionDto +import ru.vsu.app.dto.NewsDto +import ru.vsu.app.dto.PackDto +import ru.vsu.app.dto.responses.common.InternalServerError +import ru.vsu.app.dto.ReportDto +import ru.vsu.app.dto.TradeDto +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + +import kotlin.collections.List +import kotlin.collections.Map + +@SecurityRequirement(name = "Bearer Authentication") +@RestController +@Validated +@RequestMapping("\${api.base-path:/api}") +@Tag(name = "Admin", description = "Функции администратора") +class AdminController() { + + @Operation( + summary = "Удаление достижения", + operationId = "adminAchievementsAchievementIDDelete", + description = """Удаляет достижение из системы""", + responses = [ + ApiResponse(responseCode = "204", description = "Достижение успешно удалено"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/achievements/{achievement_ID}"], + produces = ["application/json"] + ) + fun adminAchievementsAchievementIDDelete(@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Обновление достижения", + operationId = "adminAchievementsAchievementIDPut", + description = """Обновляет информацию о достижении""", + responses = [ + ApiResponse(responseCode = "200", description = "Достижение успешно обновлено", content = [Content(schema = Schema(implementation = AchievementDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.PUT], + value = ["/admin/achievements/{achievement_ID}"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminAchievementsAchievementIDPut(@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody achievement: AchievementDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Создание достижения", + operationId = "adminAchievementsPost", + description = """Создает новое достижение в системе""", + responses = [ + ApiResponse(responseCode = "201", description = "Достижение успешно создано", content = [Content(schema = Schema(implementation = AchievementDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/achievements"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminAchievementsPost(@Parameter(description = "", required = true) @Valid @RequestBody achievement: AchievementDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Удаление карты", + operationId = "adminCardsCardIDDelete", + description = """Удаляет карту из системы""", + responses = [ + ApiResponse(responseCode = "204", description = "Карта успешно удалена"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/cards/{card_ID}"], + produces = ["application/json"] + ) + fun adminCardsCardIDDelete(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Обновление карты", + operationId = "adminCardsCardIDPut", + description = """Обновляет информацию о карте""", + responses = [ + ApiResponse(responseCode = "200", description = "Карта успешно обновлена", content = [Content(schema = Schema(implementation = CardDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.PUT], + value = ["/admin/cards/{card_ID}"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminCardsCardIDPut(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody card: CardDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Создание новой карты", + operationId = "adminCardsPost", + description = """Создает новую карту в системе""", + responses = [ + ApiResponse(responseCode = "201", description = "Карта успешно создана", content = [Content(schema = Schema(implementation = CardDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/cards"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminCardsPost(@Parameter(description = "", required = true) @Valid @RequestBody card: CardDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение списка предложений покупки монет", + operationId = "adminCoinOffersGet", + description = """Возвращает список всех предложений покупки монет""", + responses = [ + ApiResponse(responseCode = "200", description = "Список предложений успешно получен", content = [Content(array = ArraySchema(schema = Schema(implementation = CoinOfferDto::class)))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/admin/coin-offers"], + produces = ["application/json"] + ) + fun adminCoinOffersGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Удаление предложения покупки монет", + operationId = "adminCoinOffersOfferIdDelete", + description = """Удаляет предложение покупки монет из системы""", + responses = [ + ApiResponse(responseCode = "204", description = "Предложение успешно удалено"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/coin-offers/{offer_id}"], + produces = ["application/json"] + ) + fun adminCoinOffersOfferIdDelete(@Parameter(description = "", required = true) @PathVariable("offer_id") offerId: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Обновление предложения покупки монет", + operationId = "adminCoinOffersOfferIdPut", + description = """Обновляет информацию о предложении покупки монет""", + responses = [ + ApiResponse(responseCode = "200", description = "Предложение успешно обновлено", content = [Content(schema = Schema(implementation = CoinOfferDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.PUT], + value = ["/admin/coin-offers/{offer_id}"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminCoinOffersOfferIdPut(@Parameter(description = "", required = true) @PathVariable("offer_id") offerId: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody coinOffer: CoinOfferDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Создание предложения покупки монет", + operationId = "adminCoinOffersPost", + description = """Создает новое предложение покупки монет в системе""", + responses = [ + ApiResponse(responseCode = "201", description = "Предложение успешно создано", content = [Content(schema = Schema(implementation = CoinOfferDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/coin-offers"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminCoinOffersPost(@Parameter(description = "", required = true) @Valid @RequestBody coinOffer: CoinOfferDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Удаление коллекцию", + operationId = "adminCollectionsCollectionIDDelete", + description = """Удаляет коллекцию из системы""", + responses = [ + ApiResponse(responseCode = "204", description = "Коллекция успешно удалена"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/collections/{collection_ID}"], + produces = ["application/json"] + ) + fun adminCollectionsCollectionIDDelete(@Parameter(description = "", required = true) @PathVariable("collection_ID") collectionID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Обновление коллекции", + operationId = "adminCollectionsCollectionIDPut", + description = """Обновляет информацию о коллекции""", + responses = [ + ApiResponse(responseCode = "200", description = "Коллекция успешно обновлена", content = [Content(schema = Schema(implementation = CollectionDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.PUT], + value = ["/admin/collections/{collection_ID}"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminCollectionsCollectionIDPut(@Parameter(description = "", required = true) @PathVariable("collection_ID") collectionID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody collection: CollectionDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Создание новой коллекции", + operationId = "adminCollectionsPost", + description = """Создает новую коллекцию карт""", + responses = [ + ApiResponse(responseCode = "201", description = "Коллекция успешно создана", content = [Content(schema = Schema(implementation = CollectionDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/collections"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminCollectionsPost(@Parameter(description = "", required = true) @Valid @RequestBody collection: CollectionDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Удаление новости", + operationId = "adminNewsNewsIDDelete", + description = """Удаляет новость из системы""", + responses = [ + ApiResponse(responseCode = "204", description = "Новость успешно удалена"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/news/{news_ID}"], + produces = ["application/json"] + ) + fun adminNewsNewsIDDelete(@Parameter(description = "", required = true) @PathVariable("news_ID") newsID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Обновление новости", + operationId = "adminNewsNewsIDPut", + description = """Обновляет информацию о новости""", + responses = [ + ApiResponse(responseCode = "200", description = "Новость успешно обновлена", content = [Content(schema = Schema(implementation = NewsDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.PUT], + value = ["/admin/news/{news_ID}"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminNewsNewsIDPut(@Parameter(description = "", required = true) @PathVariable("news_ID") newsID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody news: NewsDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Создание новости", + operationId = "adminNewsPost", + description = """Создает новую новость в системе""", + responses = [ + ApiResponse(responseCode = "201", description = "Новость успешно создана", content = [Content(schema = Schema(implementation = NewsDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/news"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminNewsPost(@Parameter(description = "", required = true) @Valid @RequestBody news: NewsDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Удаление набор", + operationId = "adminPacksPackIDDelete", + description = """Удаляет набор из системы""", + responses = [ + ApiResponse(responseCode = "204", description = "Набор успешно удален"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/packs/{pack_ID}"], + produces = ["application/json"] + ) + fun adminPacksPackIDDelete(@Parameter(description = "", required = true) @PathVariable("pack_ID") packID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Обновление набора", + operationId = "adminPacksPackIDPut", + description = """Обновляет информацию о наборе""", + responses = [ + ApiResponse(responseCode = "200", description = "Набор успешно обновлен", content = [Content(schema = Schema(implementation = PackDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.PUT], + value = ["/admin/packs/{pack_ID}"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminPacksPackIDPut(@Parameter(description = "", required = true) @PathVariable("pack_ID") packID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody pack: PackDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Создание нового набора", + operationId = "adminPacksPost", + description = """Создает новый набор карт""", + responses = [ + ApiResponse(responseCode = "201", description = "Набор успешно создан", content = [Content(schema = Schema(implementation = PackDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/packs"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminPacksPost(@Parameter(description = "", required = true) @Valid @RequestBody pack: PackDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Просмотр списка жалоб", + operationId = "adminReportsGet", + description = """Возвращает список всех жалоб в системе""", + responses = [ + ApiResponse(responseCode = "200", description = "Список жалоб", content = [Content(array = ArraySchema(schema = Schema(implementation = ReportDto::class)))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/admin/reports"], + produces = ["application/json"] + ) + fun adminReportsGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Просмотр деталей жалобы", + operationId = "adminReportsReportIDGet", + description = """Возвращает полную информацию о жалобе""", + responses = [ + ApiResponse(responseCode = "200", description = "Детали жалобы", content = [Content(schema = Schema(implementation = ReportDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/admin/reports/{report_ID}"], + produces = ["application/json"] + ) + fun adminReportsReportIDGet(@Parameter(description = "", required = true) @PathVariable("report_ID") reportID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Обновление статуса жалобы", + operationId = "adminReportsReportIDPut", + description = """Обновляет статус жалобы и применяет меры к пользователю""", + responses = [ + ApiResponse(responseCode = "200", description = "Статус жалобы обновлен", content = [Content(schema = Schema(implementation = ReportDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "404", description = "Жалоба не найдена"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.PUT], + value = ["/admin/reports/{report_ID}"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminReportsReportIDPut(@Parameter(description = "", required = true) @PathVariable("report_ID") reportID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody adminReportsReportIDPutRequest: AdminReportsReportIDPutRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение статистики системы", + operationId = "adminStatsGet", + description = """Возвращает статистику по пользователям, картам, обменам и т.д.""", + responses = [ + ApiResponse(responseCode = "200", description = "Статистика системы", content = [Content(schema = Schema(implementation = AdminStatsGet200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/admin/stats"], + produces = ["application/json"] + ) + fun adminStatsGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение списка обменов", + operationId = "adminTradesGet", + description = """Возвращает список всех обменов в системе""", + responses = [ + ApiResponse(responseCode = "200", description = "Список обменов", content = [Content(array = ArraySchema(schema = Schema(implementation = TradeDto::class)))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/admin/trades"], + produces = ["application/json"] + ) + fun adminTradesGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Признание обмена недействительным", + operationId = "adminTradesTradeIDInvalidatePost", + description = """Помечает обмен как недействительный и возвращает карты участникам +""", + responses = [ + ApiResponse(responseCode = "200", description = "Обмен аннулирован", content = [Content(schema = Schema(implementation = AdminTradesTradeIDInvalidatePost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/trades/{trade_ID}/invalidate"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminTradesTradeIDInvalidatePost(@Parameter(description = "", required = true) @PathVariable("trade_ID") tradeID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody adminTradesTradeIDInvalidatePostRequest: AdminTradesTradeIDInvalidatePostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Отзыв достижения у пользователя", + operationId = "adminUsersUserIDAchievementsAchievementIDDelete", + description = """Удаляет достижение из списка полученных пользователем""", + responses = [ + ApiResponse(responseCode = "204", description = "Достижение успешно отозвано"), + ApiResponse(responseCode = "400", description = "Неверный запрос"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/users/{user_id}/achievements/{achievement_ID}"], + produces = ["application/json"] + ) + fun adminUsersUserIDAchievementsAchievementIDDelete(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Блокировка пользователя", + operationId = "adminUsersUserIDBanPost", + description = """Блокирует пользователя на указанный срок или навсегда""", + responses = [ + ApiResponse(responseCode = "200", description = "Пользователь заблокирован", content = [Content(schema = Schema(implementation = AdminUsersUserIDBanPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "404", description = "Пользователь не найден"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/users/{user_id}/ban"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminUsersUserIDBanPost(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody adminUsersUserIDBanPostRequest: AdminUsersUserIDBanPostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Удаление пользователя", + operationId = "adminUsersUserIDDeleteDelete", + description = """Полностью удаляет аккаунт пользователя из системы""", + responses = [ + ApiResponse(responseCode = "204", description = "Пользователь успешно удален"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "404", description = "Пользователь не найден"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/users/{user_id}/delete"], + produces = ["application/json"] + ) + fun adminUsersUserIDDeleteDelete(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Удаление карты из инвентаря пользователя", + operationId = "adminUsersUserIDInventoryCardIDDelete", + description = """Удаляет конкретную карту из коллекции пользователя""", + responses = [ + ApiResponse(responseCode = "204", description = "Карта успешно удалена из инвентаря"), + ApiResponse(responseCode = "400", description = "Неверный запрос"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/users/{user_id}/inventory/{card_ID}"], + produces = ["application/json"] + ) + fun adminUsersUserIDInventoryCardIDDelete(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Сброс выполнения квеста", + operationId = "adminUsersUserIDQuestsQuestIDResetPost", + description = """Сбрасывает статус выполнения квеста для пользователя""", + responses = [ + ApiResponse(responseCode = "200", description = "Квест успешно сброшен", content = [Content(schema = Schema(implementation = AdminUsersUserIDQuestsQuestIDResetPost200Response::class))]), + ApiResponse(responseCode = "400", description = "Неверный запрос"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/users/{user_id}/quests/{quest_ID}/reset"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun adminUsersUserIDQuestsQuestIDResetPost(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @PathVariable("quest_ID") questID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody adminUsersUserIDQuestsQuestIDResetPostRequest: AdminUsersUserIDQuestsQuestIDResetPostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Разблокировка пользователя", + operationId = "adminUsersUserIDUnbanPost", + description = """Снимает блокировку с пользователя""", + responses = [ + ApiResponse(responseCode = "200", description = "Пользователь разблокирован", content = [Content(schema = Schema(implementation = AdminUsersUserIDUnbanPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "404", description = "Пользователь не найден"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/users/{user_id}/unban"], + produces = ["application/json"] + ) + fun adminUsersUserIDUnbanPost(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/AuthController (old).kt b/backend/src/main/kotlin/ru/vsu/app/controller/AuthController (old).kt new file mode 100644 index 0000000..393daf1 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/controller/AuthController (old).kt @@ -0,0 +1,127 @@ +// package ru.vsu.app.controller + +// import io.swagger.v3.oas.annotations.Operation +// import io.swagger.v3.oas.annotations.media.Content +// import io.swagger.v3.oas.annotations.media.Schema +// import io.swagger.v3.oas.annotations.responses.ApiResponse as SwaggerApiResponse +// import io.swagger.v3.oas.annotations.responses.ApiResponses +// import io.swagger.v3.oas.annotations.tags.Tag +// import org.springframework.http.HttpStatus +// import org.springframework.http.ResponseEntity +// import org.springframework.security.core.annotation.AuthenticationPrincipal +// import org.springframework.security.core.userdetails.UserDetails +// import org.springframework.web.bind.annotation.* +// import ru.vsu.app.dto.* +// import ru.vsu.app.service.UserService + +// @RestController +// @RequestMapping("/api/auth") +// @Tag(name = "Аутентификация", description = "API для регистрации, входа и управления аккаунтом") +// class AuthController(private val userService: UserService) { + +// @Operation(summary = "Регистрация нового пользователя") +// @ApiResponses(value = [ +// SwaggerApiResponse(responseCode = "200", description = "Пользователь успешно зарегистрирован"), +// SwaggerApiResponse(responseCode = "400", description = "Некорректные данные или пользователь уже существует") +// ]) +// @PostMapping("/register") +// fun register(@RequestBody request: RegisterRequest): ResponseEntity { +// val response = userService.register(request) +// return if (response.success) { +// ResponseEntity.ok(response) +// } else { +// ResponseEntity.badRequest().body(response) +// } +// } + +// @Operation(summary = "Активация аккаунта по коду") +// @ApiResponses(value = [ +// SwaggerApiResponse(responseCode = "200", description = "Аккаунт успешно активирован"), +// SwaggerApiResponse(responseCode = "400", description = "Неверный код активации") +// ]) +// @PostMapping("/activate") +// fun activateAccount(@RequestBody request: ActivateAccountRequest): ResponseEntity { +// val response = userService.activateAccount(request) +// return if (response.success) { +// ResponseEntity.ok(response) +// } else { +// ResponseEntity.badRequest().body(response) +// } +// } + +// @Operation(summary = "Вход в систему") +// @ApiResponses(value = [ +// SwaggerApiResponse( +// responseCode = "200", +// description = "Успешная аутентификация, возвращает JWT токен", +// content = [Content(schema = Schema(implementation = LoginResponse::class))] +// ), +// SwaggerApiResponse( +// responseCode = "401", +// description = "Неверные учетные данные", +// content = [Content(schema = Schema(implementation = ApiResponse::class))] +// ) +// ]) +// @PostMapping("/login") +// fun login(@RequestBody request: LoginRequest): ResponseEntity { +// val response = userService.login(request) +// return if (response != null) { +// ResponseEntity.ok(response) +// } else { +// ResponseEntity.status(HttpStatus.UNAUTHORIZED) +// .body(ApiResponse("Неверные учетные данные или аккаунт не активирован", false)) +// } +// } + +// @Operation(summary = "Получение информации о текущем пользователе") +// @ApiResponses(value = [ +// SwaggerApiResponse( +// responseCode = "200", +// description = "Информация о пользователе", +// content = [Content(schema = Schema(implementation = UserInfoResponse::class))] +// ), +// SwaggerApiResponse(responseCode = "401", description = "Неавторизованный доступ") +// ]) +// @GetMapping("/user-info") +// fun getUserInfo(@AuthenticationPrincipal userDetails: UserDetails): ResponseEntity { +// val response = userService.getUserInfo(userDetails.username) +// return ResponseEntity.ok(response) +// } + +// @Operation(summary = "Запрос на сброс пароля") +// @ApiResponses(value = [ +// SwaggerApiResponse( +// responseCode = "200", +// description = "Код для сброса пароля отправлен на email", +// content = [Content(schema = Schema(implementation = ApiResponse::class))] +// ) +// ]) +// @PostMapping("/forgot-password") +// fun forgotPassword(@RequestBody request: ForgotPasswordRequest): ResponseEntity { +// val response = userService.forgotPassword(request) +// return ResponseEntity.ok(response) +// } + +// @Operation(summary = "Установка нового пароля по коду из email") +// @ApiResponses(value = [ +// SwaggerApiResponse( +// responseCode = "200", +// description = "Пароль успешно изменен", +// content = [Content(schema = Schema(implementation = ApiResponse::class))] +// ), +// SwaggerApiResponse( +// responseCode = "400", +// description = "Неверный код или код устарел", +// content = [Content(schema = Schema(implementation = ApiResponse::class))] +// ) +// ]) +// @PostMapping("/reset-password") +// fun resetPassword(@RequestBody request: ResetPasswordRequest): ResponseEntity { +// val response = userService.resetPassword(request) +// return if (response.success) { +// ResponseEntity.ok(response) +// } else { +// ResponseEntity.badRequest().body(response) +// } +// } +// } \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt index 598741e..5360144 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt @@ -1,127 +1,361 @@ package ru.vsu.app.controller -import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.media.Content -import io.swagger.v3.oas.annotations.media.Schema -import io.swagger.v3.oas.annotations.responses.ApiResponse as SwaggerApiResponse -import io.swagger.v3.oas.annotations.responses.ApiResponses +import ru.vsu.app.dto.responses.auth.login.LoginUser400Response +import ru.vsu.app.dto.responses.auth.login.LoginUser401Response +import ru.vsu.app.dto.requests.LoginUserRequest +import ru.vsu.app.dto.responses.auth.AuthCheckGet401Response +import ru.vsu.app.dto.responses.auth.register.RegisterUser200Response +import ru.vsu.app.dto.responses.auth.register.RegisterUser400Response +import ru.vsu.app.dto.responses.auth.register.RegisterUser409Response +import ru.vsu.app.dto.responses.common.InternalServerError +import ru.vsu.app.dto.requests.RegisterUserRequest +import ru.vsu.app.dto.responses.auth.requestpassword.RequestPasswordReset200Response +import ru.vsu.app.dto.responses.auth.requestpassword.RequestPasswordReset400Response +import ru.vsu.app.dto.responses.auth.requestpassword.RequestPasswordReset404Response +import ru.vsu.app.dto.requests.RequestPasswordResetRequest +import ru.vsu.app.dto.responses.auth.verifyemail.ResendVerificationCode200Response +import ru.vsu.app.dto.requests.ResendVerificationCodeRequest +import ru.vsu.app.dto.responses.auth.requestpassword.ResetPassword200Response +import ru.vsu.app.dto.responses.auth.requestpassword.ResetPassword400Response +import ru.vsu.app.dto.requests.ResetPasswordRequest +import ru.vsu.app.dto.UserDto +import ru.vsu.app.dto.responses.auth.verifyemail.VerifyEmail400Response +import ru.vsu.app.dto.responses.auth.verifyemail.VerifyEmail410Response +import ru.vsu.app.dto.requests.VerifyEmailRequest + +import ru.vsu.app.service.AuthService + +import ru.vsu.app.metrics.AuthMetrics + +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity -import org.springframework.security.core.annotation.AuthenticationPrincipal -import org.springframework.security.core.userdetails.UserDetails + import org.springframework.web.bind.annotation.* -import ru.vsu.app.dto.* -import ru.vsu.app.service.UserService +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import kotlin.collections.List +import kotlin.collections.Map + +@SecurityRequirement(name = "Bearer Authentication") @RestController -@RequestMapping("/api/auth") -@Tag(name = "Аутентификация", description = "API для регистрации, входа и управления аккаунтом") -class AuthController(private val userService: UserService) { - - @Operation(summary = "Регистрация нового пользователя") - @ApiResponses(value = [ - SwaggerApiResponse(responseCode = "200", description = "Пользователь успешно зарегистрирован"), - SwaggerApiResponse(responseCode = "400", description = "Некорректные данные или пользователь уже существует") - ]) - @PostMapping("/register") - fun register(@RequestBody request: RegisterRequest): ResponseEntity { - val response = userService.register(request) - return if (response.success) { - ResponseEntity.ok(response) +@Validated +@RequestMapping("\${api.base-path:/api}") +@Tag(name = "Authentication", description = "Регистрация, вход и управление аккаунтом") +class AuthController( + private val authService: AuthService, + private val authMetrics: AuthMetrics +) { + + @Operation( + summary = "Проверка статуса аутентификации", + operationId = "authCheckGet", + description = """Проверяет, авторизован ли пользователь по сессионной cookie. +Возвращает данные пользователя, если сессия активна. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Пользователь авторизован", content = [Content(schema = Schema(implementation = UserDto::class))]), + ApiResponse(responseCode = "401", description = "Пользователь не авторизован", content = [Content(schema = Schema(implementation = AuthCheckGet401Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/auth/check"], + produces = ["application/json"] + ) + fun authCheckGet(): ResponseEntity { + val user = authService.getCurrentUser() + return if (user != null) { + ResponseEntity.ok(user) } else { - ResponseEntity.badRequest().body(response) + ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(AuthCheckGet401Response(false, "Пользователь не авторизован")) } } - @Operation(summary = "Активация аккаунта по коду") - @ApiResponses(value = [ - SwaggerApiResponse(responseCode = "200", description = "Аккаунт успешно активирован"), - SwaggerApiResponse(responseCode = "400", description = "Неверный код активации") - ]) - @PostMapping("/activate") - fun activateAccount(@RequestBody request: ActivateAccountRequest): ResponseEntity { - val response = userService.activateAccount(request) - return if (response.success) { - ResponseEntity.ok(response) - } else { - ResponseEntity.badRequest().body(response) - } + @Operation( + summary = "Выход из системы", + operationId = "authLogoutPost", + description = """Завершает текущую сессию пользователя. +Удаляет сессионную cookie. +""", + responses = [ + ApiResponse(responseCode = "204", description = "Успешный выход"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/auth/logout"], + produces = ["application/json"] + ) + fun authLogoutPost(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @Operation(summary = "Вход в систему") - @ApiResponses(value = [ - SwaggerApiResponse( - responseCode = "200", - description = "Успешная аутентификация, возвращает JWT токен", - content = [Content(schema = Schema(implementation = LoginResponse::class))] - ), - SwaggerApiResponse( - responseCode = "401", - description = "Неверные учетные данные", - content = [Content(schema = Schema(implementation = ApiResponse::class))] - ) - ]) - @PostMapping("/login") - fun login(@RequestBody request: LoginRequest): ResponseEntity { - val response = userService.login(request) - return if (response != null) { - ResponseEntity.ok(response) - } else { - ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body(ApiResponse("Неверные учетные данные или аккаунт не активирован", false)) + @Operation( + summary = "Обновление сессии", + operationId = "authRefreshPost", + description = """Обновляет сессионную cookie. +Требуется валидная существующая сессия. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Сессия успешно обновлена", content = [Content(schema = Schema(implementation = UserDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/auth/refresh"], + produces = ["application/json"] + ) + fun authRefreshPost(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Вход в систему", + operationId = "loginUser", + description = """Аутентификация пользователя по email и паролю. +Если данные введены верно - устанавливает сессионную cookie. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Успешный вход", content = [Content(schema = Schema(implementation = UserDto::class))]), + ApiResponse(responseCode = "400", description = "Неверный формат email", content = [Content(schema = Schema(implementation = LoginUser400Response::class))]), + ApiResponse(responseCode = "401", description = "Неверные учетные данные", content = [Content(schema = Schema(implementation = LoginUser401Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/auth/login"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun loginUser(@Parameter(description = "", required = true) @Valid @RequestBody loginUserRequest: LoginUserRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Регистрация нового пользователя", + operationId = "registerUser", + description = """Принимает email, имя пользователя и пароль. + Если email и пароль валидны, отправляет код подтверждения. + Возвращает временный токен для верификации. + """, + responses = [ + ApiResponse(responseCode = "200", description = "Код подтверждения отправлен", content = [Content(schema = Schema(implementation = RegisterUser200Response::class))]), + ApiResponse(responseCode = "400", description = "Неверный формат данных", content = [Content(schema = Schema(implementation = RegisterUser400Response::class))]), + ApiResponse(responseCode = "409", description = "Пользователь уже существует", content = [Content(schema = Schema(implementation = RegisterUser409Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) + ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/auth/register"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun registerUser( + @Parameter(description = "", required = true) + @Valid @RequestBody registerUserRequest: RegisterUserRequest + ): ResponseEntity { + val start = System.currentTimeMillis() + authMetrics.registerAttempt() + + val response = try { + authService.register(registerUserRequest) + } catch (ex: Exception) { + authMetrics.registerFailure() + authMetrics.registerDuration(System.currentTimeMillis() - start) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex) } + + val duration = System.currentTimeMillis() - start + authMetrics.registerDuration(duration) + + return when (response) { + is RegisterUser200Response -> { + authMetrics.registerSuccess() + ResponseEntity.ok(response) + } + is RegisterUser409Response -> { + authMetrics.registerConflict() + ResponseEntity.status(HttpStatus.CONFLICT).body(response) + } + is RegisterUser400Response -> { + authMetrics.registerValidationError() + ResponseEntity.badRequest().body(response) + } + is InternalServerError -> { + authMetrics.registerFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response) + } + else -> { + authMetrics.registerFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() + } + } + } + + @Operation( + summary = "Запрос кода для сброса пароля", + operationId = "requestPasswordReset", + description = """Отправляет код подтверждения для сброса пароля, если email существует в системе""", + responses = [ + ApiResponse(responseCode = "200", description = "Код подтверждения отправлен на email", content = [Content(schema = Schema(implementation = RequestPasswordReset200Response::class))]), + ApiResponse(responseCode = "400", description = "Неверный формат данных", content = [Content(schema = Schema(implementation = RequestPasswordReset400Response::class))]), + ApiResponse(responseCode = "404", description = "Пользователь с указанным email не найден", content = [Content(schema = Schema(implementation = RequestPasswordReset404Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/auth/forgot-password"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun requestPasswordReset(@Parameter(description = "", required = true) @Valid @RequestBody requestPasswordResetRequest: RequestPasswordResetRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @Operation(summary = "Получение информации о текущем пользователе") - @ApiResponses(value = [ - SwaggerApiResponse( - responseCode = "200", - description = "Информация о пользователе", - content = [Content(schema = Schema(implementation = UserInfoResponse::class))] - ), - SwaggerApiResponse(responseCode = "401", description = "Неавторизованный доступ") - ]) - @GetMapping("/user-info") - fun getUserInfo(@AuthenticationPrincipal userDetails: UserDetails): ResponseEntity { - val response = userService.getUserInfo(userDetails.username) - return ResponseEntity.ok(response) + @Operation( + summary = "Повторная отправка кода", + operationId = "resendVerificationCode", + description = """Отправляет новый код подтверждения""", + responses = [ + ApiResponse(responseCode = "200", description = "Новый код отправлен", content = [Content(schema = Schema(implementation = ResendVerificationCode200Response::class))]), + ApiResponse(responseCode = "400", description = "Неверный формат запроса"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/auth/resend-code"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun resendVerificationCode( + @Parameter(description = "Email пользователя", required = true) + @Valid @RequestBody resendVerificationCodeRequest: ResendVerificationCodeRequest + ): ResponseEntity { + val start = System.currentTimeMillis() + authMetrics.resendVerificationCodeAttempt() + + val response = try { + authService.resendVerificationCode(resendVerificationCodeRequest) + } catch (ex: Exception) { + authMetrics.resendVerificationCodeFailure() + authMetrics.resendVerificationCodeDuration(System.currentTimeMillis() - start) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError(error = ex.message ?: "Internal error")) + } + + val duration = System.currentTimeMillis() - start + authMetrics.resendVerificationCodeDuration(duration) + + return when (response) { + is ResendVerificationCode200Response -> { + authMetrics.resendVerificationCodeSuccess() + ResponseEntity.ok(response) + } + is InternalServerError -> { + authMetrics.resendVerificationCodeFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response) + } + else -> { + authMetrics.resendVerificationCodeFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() + } + } } - @Operation(summary = "Запрос на сброс пароля") - @ApiResponses(value = [ - SwaggerApiResponse( - responseCode = "200", - description = "Код для сброса пароля отправлен на email", - content = [Content(schema = Schema(implementation = ApiResponse::class))] - ) - ]) - @PostMapping("/forgot-password") - fun forgotPassword(@RequestBody request: ForgotPasswordRequest): ResponseEntity { - val response = userService.forgotPassword(request) - return ResponseEntity.ok(response) + @Operation( + summary = "Сброс пароля", + operationId = "resetPassword", + description = """Устанавливает новый пароль после подтверждения кода""", + responses = [ + ApiResponse(responseCode = "200", description = "Пароль изменен", content = [Content(schema = Schema(implementation = ResetPassword200Response::class))]), + ApiResponse(responseCode = "400", description = "Неверный код или токен", content = [Content(schema = Schema(implementation = ResetPassword400Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/auth/reset-password"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun resetPassword(@Parameter(description = "", required = true) @Valid @RequestBody resetPasswordRequest: ResetPasswordRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - - @Operation(summary = "Установка нового пароля по коду из email") - @ApiResponses(value = [ - SwaggerApiResponse( - responseCode = "200", - description = "Пароль успешно изменен", - content = [Content(schema = Schema(implementation = ApiResponse::class))] - ), - SwaggerApiResponse( - responseCode = "400", - description = "Неверный код или код устарел", - content = [Content(schema = Schema(implementation = ApiResponse::class))] - ) - ]) - @PostMapping("/reset-password") - fun resetPassword(@RequestBody request: ResetPasswordRequest): ResponseEntity { - val response = userService.resetPassword(request) - return if (response.success) { - ResponseEntity.ok(response) - } else { - ResponseEntity.badRequest().body(response) + + @Operation( + summary = "Подтверждение email", + operationId = "verifyEmail", + description = """Проверяет код подтверждения. +Если код подтверждения верный - создает аккаунт и возвращает данные пользователя. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Успешная регистрация", content = [Content(schema = Schema(implementation = UserDto::class))]), + ApiResponse(responseCode = "400", description = "Неверный код или токен", content = [Content(schema = Schema(implementation = VerifyEmail400Response::class))]), + ApiResponse(responseCode = "410", description = "Истек срок действия кода", content = [Content(schema = Schema(implementation = VerifyEmail410Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/auth/verify"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun verifyEmail(@Parameter(description = "", required = true) @Valid @RequestBody verifyEmailRequest: VerifyEmailRequest): ResponseEntity { + val start = System.currentTimeMillis() + authMetrics.verifyAttempt() + + val response = try { + authService.verifyEmail(verifyEmailRequest) + } catch (ex: Exception) { + authMetrics.verifyFailure() + authMetrics.verifyDuration(System.currentTimeMillis() - start) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(InternalServerError("Ошибка при подтверждении email", ex.message ?: "Неизвестная ошибка")) + } + + val duration = System.currentTimeMillis() - start + authMetrics.verifyDuration(duration) + + return when (response) { + is UserDto -> { + authMetrics.verifySuccess(response.userID.toString()) + ResponseEntity.ok(response) + } + is VerifyEmail400Response -> { + authMetrics.verifyValidationError() + ResponseEntity.badRequest().body(response) + } + is VerifyEmail410Response -> { + authMetrics.verifyExpired() + ResponseEntity.status(HttpStatus.GONE).body(response) + } + else -> { + authMetrics.verifyFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() + } } } -} \ No newline at end of file +} diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt index 27e197e..11245cf 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt @@ -1,41 +1,86 @@ -package ru.vsu.app.controller +// package ru.vsu.app.controller -import org.springframework.http.ResponseEntity -import org.springframework.security.core.annotation.AuthenticationPrincipal -import org.springframework.web.bind.annotation.* -import ru.vsu.app.dto.CardResponse -import ru.vsu.app.model.User -import ru.vsu.app.service.CardService +// import org.springframework.http.ResponseEntity +// import org.springframework.security.core.annotation.AuthenticationPrincipal +// import org.springframework.web.bind.annotation.* +// import io.swagger.v3.oas.annotations.tags.Tag +// import io.swagger.v3.oas.annotations.Operation +// import io.swagger.v3.oas.annotations.Parameter +// import io.swagger.v3.oas.annotations.media.Content +// import io.swagger.v3.oas.annotations.media.Schema +// import io.swagger.v3.oas.annotations.responses.ApiResponse +// import ru.vsu.app.dto.CardDto +// import ru.vsu.app.model.UserEntity +// import ru.vsu.app.service.InventoryService -@RestController -@RequestMapping("/api/cards") -class CardController( - private val cardService: CardService -) { - @GetMapping("/inventory") - fun getUserInventory( - @AuthenticationPrincipal user: User, - @RequestParam(required = false, defaultValue = "rarity") sortBy: String - ): ResponseEntity> { - val cards = cardService.getUserCards(user, sortBy) - return ResponseEntity.ok(cards) - } +//@SecurityRequirement(name = "Bearer Authentication") +// @RestController +// @RequestMapping("/api/card") +// @Tag(name = "Card", description = "Функции, связанные с карточками пользователя") +// class CardController( +// private val cardService: InventoryService +// ) { - @GetMapping("/{cardId}") - fun getCardDetails( - @PathVariable cardId: Long, - @AuthenticationPrincipal user: User - ): ResponseEntity { - val card = cardService.getCardDetails(cardId, user) - return ResponseEntity.ok(card) - } +// // @Operation( +// // summary = "Получить инвентарь пользователя", +// // description = "Возвращает список карточек, принадлежащих пользователю. Можно указать параметр сортировки (по умолчанию — по редкости).", +// // parameters = [ +// // Parameter(name = "sortBy", description = "Критерий сортировки (rarity, name, и т.д.)", required = false) +// // ], +// // responses = [ +// // ApiResponse(responseCode = "200", description = "Список карточек успешно получен", +// // content = [Content(mediaType = "application/json", schema = Schema(implementation = CardResponse::class))]) +// // ] +// // ) +// // @GetMapping("/inventory") +// // fun getUserInventory( +// // @AuthenticationPrincipal user: UserEntity, +// // @RequestParam(required = false, defaultValue = "rarity") sortBy: String +// // ): ResponseEntity { +// // val cards = cardService.getUserCards(user, sortBy) +// // return ResponseEntity.ok(cards) +// // } - @PostMapping("/{cardId}/disassemble") - fun disassembleCard( - @PathVariable cardId: Long, - @AuthenticationPrincipal user: User - ): ResponseEntity> { - val coinsReceived = cardService.disassembleCard(cardId, user) - return ResponseEntity.ok(mapOf("coinsReceived" to coinsReceived)) - } -} \ No newline at end of file +// @Operation( +// summary = "Получить подробную информацию о карточке", +// description = "Возвращает подробные сведения о конкретной карточке пользователя по её ID.", +// parameters = [ +// Parameter(name = "cardId", description = "ID карточки", required = true) +// ], +// responses = [ +// ApiResponse(responseCode = "200", description = "Карточка успешно получена", +// content = [Content(mediaType = "application/json", schema = Schema(implementation = CardResponse::class))]), +// ApiResponse(responseCode = "404", description = "Карточка не найдена") +// ] +// ) +// @GetMapping("/{cardId}") +// fun getCardDetails( +// @PathVariable cardId: Long, +// @AuthenticationPrincipal user: UserEntity +// ): ResponseEntity { +// val card = cardService.getCardDetails(cardId, user) +// return ResponseEntity.ok(card) +// } + +// @Operation( +// summary = "Разобрать карточку на монеты", +// description = "Позволяет пользователю разобрать карточку и получить за неё внутриигровую валюту.", +// parameters = [ +// Parameter(name = "cardId", description = "ID карточки", required = true) +// ], +// responses = [ +// ApiResponse(responseCode = "200", description = "Карточка успешно разобрана", +// content = [Content(mediaType = "application/json", schema = Schema(example = """{"coinsReceived": 100}"""))]), +// ApiResponse(responseCode = "400", description = "Невозможно разобрать карточку"), +// ApiResponse(responseCode = "404", description = "Карточка не найдена") +// ] +// ) +// @PostMapping("/{cardId}/disassemble") +// fun disassembleCard( +// @PathVariable cardId: Long, +// @AuthenticationPrincipal user: UserEntity +// ): ResponseEntity { +// val coinsReceived = cardService.disassembleCard(cardId, user) +// return ResponseEntity.ok(mapOf("coinsReceived" to coinsReceived)) +// } +// } diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/HomeController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/HomeController.kt new file mode 100644 index 0000000..8e91152 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/controller/HomeController.kt @@ -0,0 +1,272 @@ +package ru.vsu.app.controller + +import ru.vsu.app.dto.responses.home.HomeGenerateCardPost200Response +import ru.vsu.app.dto.responses.home.HomeGenerateCardPost402Response +import ru.vsu.app.dto.requests.HomeGenerateCardPostRequest +import ru.vsu.app.dto.responses.home.HomeNewsGet404Response +import ru.vsu.app.dto.responses.home.HomeNotificationsNotificationIDNavigatePost200Response +import ru.vsu.app.dto.responses.home.HomeNotificationsNotificationIDNavigatePost404Response +import ru.vsu.app.dto.responses.home.HomeQuestsClaimRewardPost200Response +import ru.vsu.app.dto.responses.home.HomeQuestsClaimRewardPost400Response +import ru.vsu.app.dto.requests.HomeQuestsClaimRewardPostRequest +import ru.vsu.app.dto.responses.home.HomeQuestsGet200Response +import ru.vsu.app.dto.responses.home.HomeQuestsQuestIDChangeStatusPost200Response +import ru.vsu.app.dto.responses.home.HomeSearchGet400Response +import ru.vsu.app.dto.responses.home.HomeSearchGet404Response +import ru.vsu.app.dto.NewsDto +import ru.vsu.app.dto.NotificationDto +import ru.vsu.app.dto.responses.common.InternalServerError +import ru.vsu.app.dto.ThemeDto +import ru.vsu.app.dto.UserDto +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + +import kotlin.collections.List +import kotlin.collections.Map + +@SecurityRequirement(name = "Bearer Authentication") +@RestController +@Validated +@RequestMapping("\${api.base-path:/api}") +@Tag(name = "Home", description = "Операции на странице \"Главное меню\"") +class HomeController() { + @Operation( + summary = "Генерация уникальной карты", + operationId = "homeGenerateCardPost", + description = """Генерирует уникальную карту по выбранной теме, если на балансе достаточно средств +""", + responses = [ + ApiResponse(responseCode = "200", description = "Карта успешно сгенерирована", content = [Content(schema = Schema(implementation = HomeGenerateCardPost200Response::class))]), + ApiResponse(responseCode = "402", description = "Недостаточно средств", content = [Content(schema = Schema(implementation = HomeGenerateCardPost402Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/home/generate-card"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun homeGenerateCardPost(@Parameter(description = "", required = true) @Valid @RequestBody homeGenerateCardPostRequest: HomeGenerateCardPostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение списка тем для генерации карт", + operationId = "homeGenerateCardThemesGet", + description = """Возвращает список доступных тем для генерации уникальных карт +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список тем", content = [Content(array = ArraySchema(schema = Schema(implementation = ThemeDto::class)))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/home/generate-card/themes"], + produces = ["application/json"] + ) + fun homeGenerateCardThemesGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение списока новостей", + operationId = "homeNewsGet", + description = """Возвращает список новостей +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список новостей", content = [Content(array = ArraySchema(schema = Schema(implementation = NewsDto::class)))]), + ApiResponse(responseCode = "404", description = "Новостей не найдено", content = [Content(schema = Schema(implementation = HomeNewsGet404Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/home/news"], + produces = ["application/json"] + ) + fun homeNewsGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Просмотр новости", + operationId = "homeNewsNewsIDGet", + description = """Возвращает полную информацию по конкретной новости +""", + responses = [ + ApiResponse(responseCode = "200", description = "Детальная информация о новости", content = [Content(schema = Schema(implementation = NewsDto::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/home/news/{news_ID}"], + produces = ["application/json"] + ) + fun homeNewsNewsIDGet(@Parameter(description = "", required = true) @PathVariable("news_ID") newsID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Просмотр уведомлений", + operationId = "homeNotificationsGet", + description = """Возвращает список уведомлений пользователя +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список уведомлений", content = [Content(array = ArraySchema(schema = Schema(implementation = NotificationDto::class)))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/home/notifications"], + produces = ["application/json"] + ) + fun homeNotificationsGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Просмотр уведомления", + operationId = "homeNotificationsNotificationIDGet", + description = """Возвращает информацию по конкретному уведомлению +""", + responses = [ + ApiResponse(responseCode = "200", description = "Детальная информация об уведомлении", content = [Content(schema = Schema(implementation = NotificationDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/home/notifications/{notification_ID}"], + produces = ["application/json"] + ) + fun homeNotificationsNotificationIDGet(@Parameter(description = "", required = true) @PathVariable("notification_ID") notificationID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Переход по ссылке в уведомлении", + operationId = "homeNotificationsNotificationIDNavigatePost", + description = """Перенаправляет на страницу обмена, связанного с уведомлением +""", + responses = [ + ApiResponse(responseCode = "200", description = "Перенаправление на страницу обмена", content = [Content(schema = Schema(implementation = HomeNotificationsNotificationIDNavigatePost200Response::class))]), + ApiResponse(responseCode = "404", description = "Предложение обмена не найдено", content = [Content(schema = Schema(implementation = HomeNotificationsNotificationIDNavigatePost404Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/home/notifications/{notification_ID}/navigate"], + produces = ["application/json"] + ) + fun homeNotificationsNotificationIDNavigatePost(@Parameter(description = "", required = true) @PathVariable("notification_ID") notificationID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение награды за выполнение квестов", + operationId = "homeQuestsClaimRewardPost", + description = """Выдает награду за выполнение квестов +""", + responses = [ + ApiResponse(responseCode = "200", description = "Награда получена", content = [Content(schema = Schema(implementation = HomeQuestsClaimRewardPost200Response::class))]), + ApiResponse(responseCode = "400", description = "Не все квесты выполнены", content = [Content(schema = Schema(implementation = HomeQuestsClaimRewardPost400Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/home/quests/claim-reward"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun homeQuestsClaimRewardPost(@Parameter(description = "", required = true) @Valid @RequestBody homeQuestsClaimRewardPostRequest: HomeQuestsClaimRewardPostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение списка квестов", + operationId = "homeQuestsGet", + description = """Возвращает список ежедневных и еженедельных квестов с их статусом +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список квестов", content = [Content(schema = Schema(implementation = HomeQuestsGet200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/home/quests"], + produces = ["application/json"] + ) + fun homeQuestsGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Изменение статуса выполнения квеста", + operationId = "homeQuestsQuestIDChangeStatusPost", + description = """Изменение статуса выполнения квеста +""", + responses = [ + ApiResponse(responseCode = "200", description = "Статус квеста успешно изменен", content = [Content(schema = Schema(implementation = HomeQuestsQuestIDChangeStatusPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/home/quests/{quest_ID}/change-status"], + produces = ["application/json"] + ) + fun homeQuestsQuestIDChangeStatusPost(@Parameter(description = "", required = true) @PathVariable("quest_ID") questID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Поиск пользователей", + operationId = "homeSearchGet", + description = """Поиск пользователей по никнейму или ID +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список найденных пользователей", content = [Content(array = ArraySchema(schema = Schema(implementation = UserDto::class)))]), + ApiResponse(responseCode = "400", description = "Неверный запрос", content = [Content(schema = Schema(implementation = HomeSearchGet400Response::class))]), + ApiResponse(responseCode = "404", description = "Пользователь не найден", content = [Content(schema = Schema(implementation = HomeSearchGet404Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/home/search"], + produces = ["application/json"] + ) + fun homeSearchGet( @RequestParam(value = "query", required = false) query: kotlin.String?): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt new file mode 100644 index 0000000..da0982e --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt @@ -0,0 +1,261 @@ +// package ru.vsu.app.controller + +// import ru.vsu.app.dto.responses.InventoryCardCardIDFavoriteGet200Response +// import ru.vsu.app.dto.responses.InventoryCardCardIDQuantityGet200Response +// import ru.vsu.app.dto.responses.InventoryCardCardIDTradeCancelPost200Response +// import ru.vsu.app.dto.responses.InventoryCardCardIDTradeStatusGet200Response +// import ru.vsu.app.dto.responses.InventoryDestroyPost200Response +// import ru.vsu.app.dto.requests.InventoryDestroyPostRequest +// import ru.vsu.app.dto.responses.InventoryFavoritesCountGet200Response +// import ru.vsu.app.dto.responses.InventoryGet200Response +// import ru.vsu.app.dto.responses.OtherProfileCardCardIDInitiateTradePost200Response +// import ru.vsu.app.dto.responses.common.InternalServerError +// import io.swagger.v3.oas.annotations.* +// import io.swagger.v3.oas.annotations.enums.* +// import io.swagger.v3.oas.annotations.media.* +// import io.swagger.v3.oas.annotations.responses.* +// import io.swagger.v3.oas.annotations.security.* +// import io.swagger.v3.oas.annotations.tags.Tag +// import org.springframework.http.HttpStatus +// import org.springframework.http.MediaType +// import org.springframework.http.ResponseEntity + +// import org.springframework.web.bind.annotation.* +// import org.springframework.validation.annotation.Validated +// import org.springframework.web.context.request.NativeWebRequest +// import org.springframework.beans.factory.annotation.Autowired + +// import jakarta.validation.Valid +// import jakarta.validation.constraints.DecimalMax +// import jakarta.validation.constraints.DecimalMin +// import jakarta.validation.constraints.Email +// import jakarta.validation.constraints.Max +// import jakarta.validation.constraints.Min +// import jakarta.validation.constraints.NotNull +// import jakarta.validation.constraints.Pattern +// import jakarta.validation.constraints.Size + +// import org.springframework.security.core.annotation.AuthenticationPrincipal +// import ru.vsu.app.model.UserEntity +// import ru.vsu.app.service.InventoryService +// import ru.vsu.app.dto.responses.CardResponse + +// import kotlin.collections.List +// import kotlin.collections.Map + +// @SecurityRequirement(name = "Bearer Authentication") +// @RestController +// @Validated +// @RequestMapping("\${api.base-path:/api}") +// @Tag(name = "Inventory", description = "Операции на странице \"Инвентарь\"") +// class InventoryController(private val inventoryService: InventoryService) { + +// @Operation( +// summary = "Добавление карты в избранное", +// operationId = "inventoryCardCardIDFavoriteAddPost", +// description = """Добавляет карту в избранное, если: +// - Карта не добавлена в "избранное" +// - Общее количество избранных карт < 5 +// """, +// responses = [ +// ApiResponse(responseCode = "200", description= "Карта успешно добавлена в избранное"), +// ApiResponse(responseCode = "400", description = "Невозможно добавить карту в избранное"), +// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], +// security = [ SecurityRequirement(name = "bearerAuth") ] +// ) +// @RequestMapping( +// method = [RequestMethod.POST], +// value = ["/inventory/card/{card_ID}/favorite-add"], +// produces = ["application/json"] +// ) +// fun inventoryCardCardIDFavoriteAddPost(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { +// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +// } + +// @Operation( +// summary = "Удаление карты из избранного", +// operationId = "inventoryCardCardIDFavoriteDeleteDelete", +// description = """Удаляет карту из избранного. +// """, +// responses = [ +// ApiResponse(responseCode = "200", description = "Карта успешно удалена из избранного"), +// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], +// security = [ SecurityRequirement(name = "bearerAuth") ] +// ) +// @RequestMapping( +// method = [RequestMethod.DELETE], +// value = ["/inventory/card/{card_ID}/favorite-delete"], +// produces = ["application/json"] +// ) +// fun inventoryCardCardIDFavoriteDeleteDelete(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { +// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +// } + +// @Operation( +// summary = "Проверка статуса карты", +// operationId = "inventoryCardCardIDFavoriteGet", +// description = """Проверяет, добавлена ли карта в избранное. +// """, +// responses = [ +// ApiResponse(responseCode = "200", description = "Статус избранного", content = [Content(schema = Schema(implementation = InventoryCardCardIDFavoriteGet200Response::class))]), +// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], +// security = [ SecurityRequirement(name = "bearerAuth") ] +// ) +// @RequestMapping( +// method = [RequestMethod.GET], +// value = ["/inventory/card/{card_ID}/favorite"], +// produces = ["application/json"] +// ) +// fun inventoryCardCardIDFavoriteGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { +// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +// } + +// @Operation( +// summary = "Проверка количества экземпляров карты", +// operationId = "inventoryCardCardIDQuantityGet", +// description = """Возвращает количество экземпляров карты у пользователя. +// Если экземпляров больше 1 - можно разбирать/выставлять на обмен. +// """, +// responses = [ +// ApiResponse(responseCode = "200", description = "Информация о количестве карт", content = [Content(schema = Schema(implementation = InventoryCardCardIDQuantityGet200Response::class))]), +// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], +// security = [ SecurityRequirement(name = "bearerAuth") ] +// ) +// @RequestMapping( +// method = [RequestMethod.GET], +// value = ["/inventory/card/{card_ID}/quantity"], +// produces = ["application/json"] +// ) +// fun inventoryCardCardIDQuantityGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { +// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +// } + +// @Operation( +// summary = "Снятие карты с обмена", +// operationId = "inventoryCardCardIDTradeCancelPost", +// description = """Возвращает карту из раздела "На обмен" в Инвентарь""", +// responses = [ +// ApiResponse(responseCode = "200", description = "Карта успешно снята с обмена", content = [Content(schema = Schema(implementation = InventoryCardCardIDTradeCancelPost200Response::class))]), +// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], +// security = [ SecurityRequirement(name = "bearerAuth") ] +// ) +// @RequestMapping( +// method = [RequestMethod.POST], +// value = ["/inventory/card/{card_ID}/trade-cancel"], +// produces = ["application/json"] +// ) +// fun inventoryCardCardIDTradeCancelPost(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { +// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +// } + +// @Operation( +// summary = "Проверка статуса обмена карты", +// operationId = "inventoryCardCardIDTradeStatusGet", +// description = """Проверяет, выставлена ли карта на обмен. +// Возвращает статус карты. +// """, +// responses = [ +// ApiResponse(responseCode = "200", description = "Статус обмена карты", content = [Content(schema = Schema(implementation = InventoryCardCardIDTradeStatusGet200Response::class))]), +// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], +// security = [ SecurityRequirement(name = "bearerAuth") ] +// ) +// @RequestMapping( +// method = [RequestMethod.GET], +// value = ["/inventory/card/{card_ID}/trade-status"], +// produces = ["application/json"] +// ) +// fun inventoryCardCardIDTradeStatusGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { +// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +// } + +// @Operation( +// summary = "Разбор карты на валюту", +// operationId = "inventoryDestroyPost", +// description = """Разбирает карту на валюту, если: +// - У пользователя есть минимум 2 экземпляра этой карты +// - Добавляет стоимость карты на баланс пользователя +// """, +// responses = [ +// ApiResponse(responseCode = "200", description = "Карта успешно разобрана", content = [Content(schema = Schema(implementation = InventoryDestroyPost200Response::class))]), +// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], +// security = [ SecurityRequirement(name = "bearerAuth") ] +// ) +// @RequestMapping( +// method = [RequestMethod.POST], +// value = ["/inventory/destroy"], +// produces = ["application/json"], +// consumes = ["application/json"] +// ) +// fun inventoryDestroyPost(@Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest): ResponseEntity { +// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +// } + +// @Operation( +// summary = "Получение количества избранных карт", +// operationId = "inventoryFavoritesCountGet", +// description = """Возвращает текущее количество избранных карт. +// Если количество меньше 5 - можно добавлять новые карты в избранное. +// """, +// responses = [ +// ApiResponse(responseCode = "200", description = "Количество избранных карт", content = [Content(schema = Schema(implementation = InventoryFavoritesCountGet200Response::class))]), +// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], +// security = [ SecurityRequirement(name = "bearerAuth") ] +// ) +// @RequestMapping( +// method = [RequestMethod.GET], +// value = ["/inventory/favorites/count"], +// produces = ["application/json"] +// ) +// fun inventoryFavoritesCountGet(): ResponseEntity { +// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +// } + +// @Operation( +// summary = "Получение данных инвентаря", +// operationId = "inventoryGet", +// description = """Загружает данные инвентаря пользователя. +// - Проверяет авторизацию пользователя +// - Если пользователь не авторизован - возвращает ошибку 401 +// - Если авторизован - возвращает список его карт, список избранных карт и баланс +// """, +// responses = [ +// ApiResponse(responseCode = "200", description = "Успешная загрузка инвентаря", content = [Content(schema = Schema(implementation = InventoryGet200Response::class))]), +// ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), +// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], +// security = [ SecurityRequirement(name = "bearerAuth") ] +// ) +// @RequestMapping( +// method = [RequestMethod.GET], +// value = ["/inventory"], +// produces = ["application/json"] +// ) +// fun inventoryGet( +// @AuthenticationPrincipal user: UserEntity +// ): ResponseEntity { +// val inventoryData = inventoryService.getInventoryData(user) +// return ResponseEntity.ok(inventoryData) +// } + +// @Operation( +// summary = "Выставление карты на обмен", +// operationId = "inventoryPutOnTradePost", +// description = """Выставляет карту на обмен, если: +// - У пользователя есть минимум 2 экземпляра этой карты +// - Карта еще не выставлена на обмен +// """, +// responses = [ +// ApiResponse(responseCode = "200", description = "Карта готова к обмену", content = [Content(schema = Schema(implementation = OtherProfileCardCardIDInitiateTradePost200Response::class))]), +// ApiResponse(responseCode = "400", description = "Невозможно выставить карту на обмен"), +// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], +// security = [ SecurityRequirement(name = "bearerAuth") ] +// ) +// @RequestMapping( +// method = [RequestMethod.POST], +// value = ["/inventory/put-on-trade"], +// produces = ["application/json"], +// consumes = ["application/json"] +// ) +// fun inventoryPutOnTradePost(@Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest): ResponseEntity { +// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +// } +// } diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/OtherProfileController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/OtherProfileController.kt new file mode 100644 index 0000000..2bf78bc --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/controller/OtherProfileController.kt @@ -0,0 +1,166 @@ +package ru.vsu.app.controller + +import ru.vsu.app.dto.responses.profile.OtherProfileCardCardIDInitiateTradePost400Response +import ru.vsu.app.dto.responses.profile.OtherProfileCardCardIDInitiateTradePost409Response +import ru.vsu.app.dto.responses.profile.OtherProfileCardCardIDInitiateTradePost200Response +import ru.vsu.app.dto.responses.profile.OtherProfileCardCardIDViewGet200Response +import ru.vsu.app.dto.responses.profile.OtherProfileUserIDGet200Response +import ru.vsu.app.dto.responses.profile.OtherProfileUserIDInventoryGet200Response +import ru.vsu.app.dto.responses.profile.OtherProfileUserIDInventoryGet403Response +import ru.vsu.app.dto.responses.profile.OtherProfileUserIDReportPost200Response +import ru.vsu.app.dto.requests.OtherProfileUserIDReportPostRequest +import ru.vsu.app.dto.responses.profile.ProfileAchievementsGet200Response +import ru.vsu.app.dto.responses.common.InternalServerError +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + +import kotlin.collections.List +import kotlin.collections.Map + +@SecurityRequirement(name = "Bearer Authentication") +@RestController +@Validated +@RequestMapping("\${api.base-path:/api}") +@Tag(name = "OtherProfile", description = "Операции при просмотре профиля другого пользователя") +class OtherProfileController() { + + @Operation( + summary = "Предложение обмена", + operationId = "otherProfileCardCardIDInitiateTradePost", + description = """Создает предложение обмена для указанной карты +и перенаправляет на страницу создания обмена +""", + responses = [ + ApiResponse(responseCode = "200", description = "Перенаправление на создание обмена", content = [Content(schema = Schema(implementation = OtherProfileCardCardIDInitiateTradePost200Response::class))]), + ApiResponse(responseCode = "400", description = "Невозможно предложить обмен", content = [Content(schema = Schema(implementation = OtherProfileCardCardIDInitiateTradePost400Response::class))]), + ApiResponse(responseCode = "409", description = "Конфликт при попытке обмена", content = [Content(schema = Schema(implementation = OtherProfileCardCardIDInitiateTradePost409Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/other-profile/card/{card_ID}/initiate-trade"], + produces = ["application/json"] + ) + fun otherProfileCardCardIDInitiateTradePost(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int, @RequestParam(value = "owner_ID", required = true) ownerID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Просмотр карты другого пользователя", + operationId = "otherProfileCardCardIDViewGet", + description = """Возвращает детальную информацию о карте другого пользователя +и возвращает кнопку для создания предложения обмена +""", + responses = [ + ApiResponse(responseCode = "200", description = "Информация о карте", content = [Content(schema = Schema(implementation = OtherProfileCardCardIDViewGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/other-profile/card/{card_ID}/view"], + produces = ["application/json"] + ) + fun otherProfileCardCardIDViewGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int, @RequestParam(value = "owner_ID", required = true) ownerID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение списка достижений другого пользователя", + operationId = "otherProfileUserIDAchievementsGet", + description = """Возвращает полный список достижений с текущим статусом выполнения +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список достижений", content = [Content(schema = Schema(implementation = ProfileAchievementsGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/other-profile/{user_id}/achievements"], + produces = ["application/json"] + ) + fun otherProfileUserIDAchievementsGet(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение данных профиля другого пользователя", + operationId = "otherProfileUserIDGet", + description = """Загружает данные профиля другого пользователя. +- Возвращает статистику, избранные карты и достижения +- Проверяет настройки приватности инвентаря +""", + responses = [ + ApiResponse(responseCode = "200", description = "Данные профиля", content = [Content(schema = Schema(implementation = OtherProfileUserIDGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/other-profile/{user_id}"], + produces = ["application/json"] + ) + fun otherProfileUserIDGet(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Просмотр инвентаря другого пользователя", + operationId = "otherProfileUserIDInventoryGet", + description = """Возвращает инвентарь пользователя, если он не скрыт в настройках +""", + responses = [ + ApiResponse(responseCode = "200", description = "Инвентарь пользователя", content = [Content(schema = Schema(implementation = OtherProfileUserIDInventoryGet200Response::class))]), + ApiResponse(responseCode = "403", description = "Инвентарь скрыт", content = [Content(schema = Schema(implementation = OtherProfileUserIDInventoryGet403Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/other-profile/{user_id}/inventory"], + produces = ["application/json"] + ) + fun otherProfileUserIDInventoryGet(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Подача жалобы на пользователя", + operationId = "otherProfileUserIDReportPost", + description = """Перенаправляет на страницу создания жалобы. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Перенаправление на страницу создания жалобы", content = [Content(schema = Schema(implementation = OtherProfileUserIDReportPost200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/other-profile/{user_id}/report"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun otherProfileUserIDReportPost(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody otherProfileUserIDReportPostRequest: OtherProfileUserIDReportPostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/ProfileController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/ProfileController.kt new file mode 100644 index 0000000..28dac5a --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/controller/ProfileController.kt @@ -0,0 +1,180 @@ +package ru.vsu.app.controller + +import ru.vsu.app.dto.responses.profile.ProfileAchievementsFavoritesCountGet200Response +import ru.vsu.app.dto.responses.profile.ProfileAchievementsGet200Response +import ru.vsu.app.dto.responses.profile.ProfileGet200Response +import ru.vsu.app.dto.responses.common.InternalServerError +import ru.vsu.app.dto.UserSettingsDto +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + +import kotlin.collections.List +import kotlin.collections.Map + +@SecurityRequirement(name = "Bearer Authentication") +@RestController +@Validated +@RequestMapping("\${api.base-path:/api}") +@Tag(name = "Profile", description = "Операции в профиле пользователя") +class ProfileController() { + + @Operation( + summary = "Добавить достижение в избранное", + operationId = "profileAchievementsAchievementIDFavoriteAddPost", + description = """Добавляет достижение в избранное (максимум 4) +""", + responses = [ + ApiResponse(responseCode = "200", description = "Успешно добавлено в избранное"), + ApiResponse(responseCode = "400", description = "Невозможно добавить в избранное (лимит достигнут)"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/profile/achievements/{achievement_ID}/favorite-add"], + produces = ["application/json"] + ) + fun profileAchievementsAchievementIDFavoriteAddPost(@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Удалить достижение из избранного", + operationId = "profileAchievementsAchievementIDFavoriteDeleteDelete", + description = """Удаляет достижение из избранного +""", + responses = [ + ApiResponse(responseCode = "200", description = "Успешно удалено из избранного"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/profile/achievements/{achievement_ID}/favorite-delete"], + produces = ["application/json"] + ) + fun profileAchievementsAchievementIDFavoriteDeleteDelete(@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Проверка количества избранных достижений", + operationId = "profileAchievementsFavoritesCountGet", + description = """Возвращает текущее количество избранных достижений +""", + responses = [ + ApiResponse(responseCode = "200", description = "Количество избранных", content = [Content(schema = Schema(implementation = ProfileAchievementsFavoritesCountGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/profile/achievements/favorites/count"], + produces = ["application/json"] + ) + fun profileAchievementsFavoritesCountGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение списка достижений", + operationId = "profileAchievementsGet", + description = """Возвращает полный список достижений с текущим статусом выполнения +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список достижений", content = [Content(schema = Schema(implementation = ProfileAchievementsGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/profile/achievements"], + produces = ["application/json"] + ) + fun profileAchievementsGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение данных профиля", + operationId = "profileGet", + description = """Загружает данные профиля пользователя. +- Проверяет авторизацию +- Возвращает статистику, избранные карты и достижения +""", + responses = [ + ApiResponse(responseCode = "200", description = "Данные профиля", content = [Content(schema = Schema(implementation = ProfileGet200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/profile"], + produces = ["application/json"] + ) + fun profileGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Обновление настроек", + operationId = "profileSettingsChangePut", + description = """Обновляет настройки профиля +""", + responses = [ + ApiResponse(responseCode = "200", description = "Настройки обновлены", content = [Content(schema = Schema(implementation = UserSettingsDto::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.PUT], + value = ["/profile/settings-change"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun profileSettingsChangePut(@Parameter(description = "", required = true) @Valid @RequestBody userSettings: UserSettingsDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение настроек", + operationId = "profileSettingsGet", + description = """Возвращает текущие настройки профиля +""", + responses = [ + ApiResponse(responseCode = "200", description = "Настройки профиля", content = [Content(schema = Schema(implementation = UserSettingsDto::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/profile/settings"], + produces = ["application/json"] + ) + fun profileSettingsGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/ShopController (old).kt b/backend/src/main/kotlin/ru/vsu/app/controller/ShopController (old).kt new file mode 100644 index 0000000..258e939 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/controller/ShopController (old).kt @@ -0,0 +1,90 @@ +// package ru.vsu.app.controller + +// import io.swagger.v3.oas.annotations.Operation +// import io.swagger.v3.oas.annotations.Parameter +// import io.swagger.v3.oas.annotations.tags.Tag +// import org.springframework.http.HttpStatus +// import org.springframework.http.ResponseEntity +// import org.springframework.security.core.annotation.AuthenticationPrincipal +// import org.springframework.validation.annotation.Validated +// import org.springframework.web.bind.annotation.* +// import ru.vsu.app.dto.* +// import ru.vsu.app.model.UserEntity +// import ru.vsu.app.service.ShopService +// import jakarta.validation.Valid + +// @RestController +// @RequestMapping("/api/shop") +// @Tag(name = "Shop", description = "API для работы с магазином") +// @Validated +// class ShopController(private val shopService: ShopService) { + +// @Operation(summary = "Получение списка наборов карт", description = "Возвращает список доступных для покупки наборов карт") +// @GetMapping("/packs") +// fun getAllPacks(): ResponseEntity { +// val packs = shopService.getAllPacks() +// return ResponseEntity.ok(packs) +// } + +// @Operation(summary = "Получение информации о наборе карт", description = "Возвращает подробную информацию о наборе и его содержимом") +// @GetMapping("/packs/{packId}") +// fun getPackDetails( +// @Parameter(description = "ID набора карт", required = true) +// @PathVariable packId: Long +// ): ResponseEntity { +// val pack = shopService.getPackDetails(packId) +// return ResponseEntity.ok(pack) +// } + +// @Operation(summary = "Покупка набора карт", description = "Покупает набор карт, если пользователь авторизован и на балансе достаточно средств") +// @PostMapping("/packs/{packId}/buy") +// fun buyPack( +// @Parameter(description = "ID набора карт", required = true) +// @PathVariable packId: Long, +// @AuthenticationPrincipal user: UserEntity +// ): ResponseEntity { +// return ResponseEntity.ok(shopService.buyPack(packId, user)) +// } + +// @Operation(summary = "Получение списка предложений монет", description = "Возвращает список доступных пакетов монет") +// @GetMapping("/coins/offers") +// fun getAllCoinOffers(): ResponseEntity { +// val offers = shopService.getAllCoinOffers() +// return ResponseEntity.ok(offers) +// } + +// @Operation(summary = "Получение информации о предложении монет", description = "Возвращает детализацию конкретного предложения монет") +// @GetMapping("/coins/offers/{offerId}") +// fun getCoinOfferDetails( +// @Parameter(description = "ID предложения монет", required = true) +// @PathVariable offerId: Long +// ): ResponseEntity { +// val offer = shopService.getCoinOfferDetails(offerId) +// return ResponseEntity.ok(offer) +// } + +// data class PurchaseCoinsRequest( +// @Parameter(description = "URL для редиректа после покупки", required = true) +// val redirectUrl: String +// ) + +// @Operation(summary = "Покупка монет", description = "Инициирует процесс покупки монет через платежный шлюз") +// @PostMapping("/coins/offers/{offerId}/purchase") +// fun purchaseCoins( +// @Parameter(description = "ID предложения монет", required = true) +// @PathVariable offerId: Long, +// @Valid @RequestBody request: PurchaseCoinsRequest +// ): ResponseEntity { +// val purchaseResponse = shopService.purchaseCoins(offerId, request.redirectUrl) +// return ResponseEntity.ok(purchaseResponse) +// } + +// @Operation(summary = "Обработка платежа", description = "Обрабатывает результат платежа от платежного шлюза") +// @PostMapping("/payments/process") +// fun processPayment( +// @Valid @RequestBody request: PaymentCallbackRequest +// ): ResponseEntity { +// val success = shopService.processPayment(request) +// return ResponseEntity.ok(mapOf("success" to success)) +// } +// } diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/ShopController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/ShopController.kt index ca961b1..6e4849a 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/ShopController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/ShopController.kt @@ -1,66 +1,204 @@ package ru.vsu.app.controller -import io.swagger.v3.oas.annotations.Operation +import ru.vsu.app.dto.CoinOfferDto +import ru.vsu.app.dto.PackDto +import ru.vsu.app.dto.PaymentCallbackDto +import ru.vsu.app.dto.responses.common.InternalServerError +import ru.vsu.app.dto.responses.shop.ShopCoinsOffersOfferIdPurchasePost200Response +import ru.vsu.app.dto.requests.ShopCoinsOffersOfferIdPurchasePostRequest +import ru.vsu.app.dto.responses.shop.ShopPacksPackIdBuyPost200Response +import ru.vsu.app.dto.responses.shop.ShopPacksPackIdBuyPost402Response +import ru.vsu.app.dto.responses.shop.ShopPacksPackIdOpenPost200Response +import ru.vsu.app.dto.responses.shop.ShopPaymentsProcessPost200Response +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity -import org.springframework.security.core.annotation.AuthenticationPrincipal + import org.springframework.web.bind.annotation.* -import ru.vsu.app.dto.* -import ru.vsu.app.model.User -import ru.vsu.app.service.ShopService +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + +import kotlin.collections.List +import kotlin.collections.Map +@SecurityRequirement(name = "Bearer Authentication") @RestController -@RequestMapping("/api/shop") -@Tag(name = "Shop", description = "API для работы с магазином") -class ShopController(private val shopService: ShopService) { +@Validated +@RequestMapping("\${api.base-path:/api}") +@Tag(name = "Shop", description = "Операции на странице \"Магазин\"") +class ShopController() { + + @Operation( + summary = "Получение предложений по монетам", + operationId = "shopCoinsOffersGet", + description = """Возвращает список доступных пакетов монет +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список предложений", content = [Content(array = ArraySchema(schema = Schema(implementation = CoinOfferDto::class)))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/shop/coins/offers"], + produces = ["application/json"] + ) + fun shopCoinsOffersGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } - @Operation(summary = "Получение списка наборов") - @GetMapping("/packs") - fun getAllPacks(): ResponseEntity> { - return ResponseEntity.ok(shopService.getAllPacks()) + @Operation( + summary = "Просмотр предложения монет", + operationId = "shopCoinsOffersOfferIdGet", + description = """Возвращает детализацию предложения по монетам +""", + responses = [ + ApiResponse(responseCode = "200", description = "Информация о предложении", content = [Content(schema = Schema(implementation = CoinOfferDto::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/shop/coins/offers/{offer_id}"], + produces = ["application/json"] + ) + fun shopCoinsOffersOfferIdGet(@Parameter(description = "", required = true) @PathVariable("offer_id") offerId: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @Operation(summary = "Получение информации о наборе") - @GetMapping("/packs/{packId}") - fun getPackDetails(@PathVariable packId: Long): ResponseEntity { - return ResponseEntity.ok(shopService.getPackDetails(packId)) + @Operation( + summary = "Покупка монет", + operationId = "shopCoinsOffersOfferIdPurchasePost", + description = """Инициирует процесс покупки монет через платежный шлюз +""", + responses = [ + ApiResponse(responseCode = "200", description = "Перенаправление на платежный шлюз", content = [Content(schema = Schema(implementation = ShopCoinsOffersOfferIdPurchasePost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/shop/coins/offers/{offer_id}/purchase"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun shopCoinsOffersOfferIdPurchasePost(@Parameter(description = "", required = true) @PathVariable("offer_id") offerId: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody shopCoinsOffersOfferIdPurchasePostRequest: ShopCoinsOffersOfferIdPurchasePostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @Operation(summary = "Покупка набора") - @PostMapping("/packs/{packId}/buy") - fun buyPack( - @PathVariable packId: Long, - @AuthenticationPrincipal user: User - ): ResponseEntity { - return ResponseEntity.ok(shopService.buyPack(packId, user)) + @Operation( + summary = "Получение списка наборов", + operationId = "shopPacksGet", + description = """Возвращает список доступных для покупки наборов карт +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список наборов", content = [Content(array = ArraySchema(schema = Schema(implementation = PackDto::class)))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/shop/packs"], + produces = ["application/json"] + ) + fun shopPacksGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @Operation(summary = "Получение списка предложений монет") - @GetMapping("/coins/offers") - fun getAllCoinOffers(): ResponseEntity> { - return ResponseEntity.ok(shopService.getAllCoinOffers()) + @Operation( + summary = "Покупка набора карт", + operationId = "shopPacksPackIdBuyPost", + description = """Покупает набор карт, если: +- Пользователь авторизован +- На балансе достаточно средств +""", + responses = [ + ApiResponse(responseCode = "200", description = "Успешная покупка", content = [Content(schema = Schema(implementation = ShopPacksPackIdBuyPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "402", description = "Недостаточно средств", content = [Content(schema = Schema(implementation = ShopPacksPackIdBuyPost402Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/shop/packs/{pack_id}/buy"], + produces = ["application/json"] + ) + fun shopPacksPackIdBuyPost(@Parameter(description = "", required = true) @PathVariable("pack_id") packId: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @Operation(summary = "Получение информации о предложении монет") - @GetMapping("/coins/offers/{offerId}") - fun getCoinOfferDetails(@PathVariable offerId: Long): ResponseEntity { - return ResponseEntity.ok(shopService.getCoinOfferDetails(offerId)) + @Operation( + summary = "Просмотр содержимого набора", + operationId = "shopPacksPackIdGet", + description = """Возвращает подробную информацию о наборе и возможных картах +""", + responses = [ + ApiResponse(responseCode = "200", description = "Информация о наборе", content = [Content(schema = Schema(implementation = PackDto::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/shop/packs/{pack_id}"], + produces = ["application/json"] + ) + fun shopPacksPackIdGet(@Parameter(description = "", required = true) @PathVariable("pack_id") packId: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @Operation(summary = "Покупка монет") - @PostMapping("/coins/offers/{offerId}/purchase") - fun purchaseCoins( - @PathVariable offerId: Long, - @RequestBody request: Map - ): ResponseEntity { - val redirectUrl = request["redirectUrl"] ?: throw IllegalArgumentException("redirectUrl is required") - return ResponseEntity.ok(shopService.purchaseCoins(offerId, redirectUrl)) + @Operation( + summary = "Открытие купленного набора карт", + operationId = "shopPacksPackIdOpenPost", + description = """Открывает ранее купленный набор карт, показывая выпавшие карты +и добавляя их в инвентарь пользователя. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Набор успешно открыт", content = [Content(schema = Schema(implementation = ShopPacksPackIdOpenPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/shop/packs/{pack_id}/open"], + produces = ["application/json"] + ) + fun shopPacksPackIdOpenPost(@Parameter(description = "", required = true) @PathVariable("pack_id") packId: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @Operation(summary = "Обработка платежа") - @PostMapping("/payments/process") - fun processPayment(@RequestBody request: PaymentCallbackRequest): ResponseEntity> { - val success = shopService.processPayment(request) - return ResponseEntity.ok(mapOf("success" to success)) + @Operation( + summary = "Обработка платежа", + operationId = "shopPaymentsProcessPost", + description = """обработка результата платежа от платежного шлюза +""", + responses = [ + ApiResponse(responseCode = "200", description = "Платеж обработан", content = [Content(schema = Schema(implementation = ShopPaymentsProcessPost200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/shop/payments/process"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun shopPaymentsProcessPost(@Parameter(description = "", required = true) @Valid @RequestBody paymentCallback: PaymentCallbackDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } -} \ No newline at end of file +} diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/TradesController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/TradesController.kt new file mode 100644 index 0000000..7e286f3 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/controller/TradesController.kt @@ -0,0 +1,187 @@ +package ru.vsu.app.controller + +import ru.vsu.app.dto.responses.profile.OtherProfileCardCardIDInitiateTradePost400Response +import ru.vsu.app.dto.responses.common.InternalServerError +import ru.vsu.app.dto.TradeDto +import ru.vsu.app.dto.requests.TradesInitiatePostRequest +import ru.vsu.app.dto.responses.trades.TradesTradeIdAcceptPost200Response +import ru.vsu.app.dto.responses.trades.TradesTradeIdAcceptPost403Response +import ru.vsu.app.dto.responses.trades.TradesTradeIdCancelPost200Response +import ru.vsu.app.dto.responses.trades.TradesTradeIdGet404Response +import ru.vsu.app.dto.responses.trades.TradesTradeIdRejectPost200Response +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + +import kotlin.collections.List +import kotlin.collections.Map + +@SecurityRequirement(name = "Bearer Authentication") +@RestController +@Validated +@RequestMapping("\${api.base-path:/api}") +@Tag(name = "Trades", description = "Операции на странице \"Обменник\"") +class TradesController() { + + @Operation( + summary = "Получить все доступные предложения обмена", + operationId = "tradesGet", + description = """Возвращает список всех доступных предложений обмена. +Можно провести поиск по названию карты. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список предложений обмена", content = [Content(array = ArraySchema(schema = Schema(implementation = TradeDto::class)))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/trades"], + produces = ["application/json"] + ) + fun tradesGet( @RequestParam(value = "search", required = false) search: kotlin.String?): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Создать новое предложение обмена", + operationId = "tradesInitiatePost", + description = """Создает новое предложение обмена. +""", + responses = [ + ApiResponse(responseCode = "201", description = "Предложение обмена успешно создано", content = [Content(schema = Schema(implementation = TradeDto::class))]), + ApiResponse(responseCode = "400", description = "Невозможно создать предложение обмена", content = [Content(schema = Schema(implementation = OtherProfileCardCardIDInitiateTradePost400Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/trades/initiate"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun tradesInitiatePost(@Parameter(description = "", required = true) @Valid @RequestBody tradesInitiatePostRequest: TradesInitiatePostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение моих обменов", + operationId = "tradesMyGet", + description = """Возвращает список всех обменов текущего пользователя с их статусами. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список моих обменов", content = [Content(array = ArraySchema(schema = Schema(implementation = TradeDto::class)))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/trades/my"], + produces = ["application/json"] + ) + fun tradesMyGet( @RequestParam(value = "user_id", required = true) userId: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Принять предложение обмена", + operationId = "tradesTradeIdAcceptPost", + description = """Принимает предложение обмена, если текущий пользователь - получатель обмена. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Обмен успешно принят", content = [Content(schema = Schema(implementation = TradesTradeIdAcceptPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Нет прав для принятия этого обмена", content = [Content(schema = Schema(implementation = TradesTradeIdAcceptPost403Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/trades/{trade_id}/accept"], + produces = ["application/json"] + ) + fun tradesTradeIdAcceptPost(@Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Отменить предложение обмена", + operationId = "tradesTradeIdCancelPost", + description = """Отменяет предложение обмена, если текущий пользователь является инициатором. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Обмен успешно отменен", content = [Content(schema = Schema(implementation = TradesTradeIdCancelPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/trades/{trade_id}/cancel"], + produces = ["application/json"] + ) + fun tradesTradeIdCancelPost(@Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получить детали предложения обмена", + operationId = "tradesTradeIdGet", + description = """Возвращает полную информацию о конкретном предложении обмена. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Детали предложения обмена", content = [Content(schema = Schema(implementation = TradeDto::class))]), + ApiResponse(responseCode = "404", description = "Предложение обмена не найдено", content = [Content(schema = Schema(implementation = TradesTradeIdGet404Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/trades/{trade_id}"], + produces = ["application/json"] + ) + fun tradesTradeIdGet(@Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Отклонить предложение обмена", + operationId = "tradesTradeIdRejectPost", + description = """Отклоняет предложение обмена, если текущий пользователь - получатель обмена. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Обмен успешно отклонен", content = [Content(schema = Schema(implementation = TradesTradeIdRejectPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/trades/{trade_id}/reject"], + produces = ["application/json"] + ) + fun tradesTradeIdRejectPost(@Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/AchievementDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/AchievementDto.kt new file mode 100644 index 0000000..3c5a92c --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/AchievementDto.kt @@ -0,0 +1,43 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param achievementID Уникальный идентификатор достижения + * @param name Название достижения + * @param imageURL Ссылка на изображение + * @param description Описание достижения + * @param isUnlocked Статус получения + */ +data class AchievementDto( + + @Schema(example = "101", required = true, description = "Уникальный идентификатор достижения") + @get:JsonProperty("achievement_ID", required = true) val achievementID: kotlin.Int, + + @Schema(example = "Мастер коллекционирования", required = true, description = "Название достижения") + @get:JsonProperty("name", required = true) val name: kotlin.String, + + @Schema(example = "https://example.com/achievements/master.png", required = true, description = "Ссылка на изображение") + @get:JsonProperty("imageURL", required = true) val imageURL: kotlin.String, + + @Schema(example = "Соберите 10 полных коллекций", required = true, description = "Описание достижения") + @get:JsonProperty("description", required = true) val description: kotlin.String, + + @Schema(example = "false", required = true, description = "Статус получения") + @get:JsonProperty("isUnlocked", required = true) val isUnlocked: kotlin.Boolean + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/AuthDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/AuthDto.kt deleted file mode 100644 index ac6a2dc..0000000 --- a/backend/src/main/kotlin/ru/vsu/app/dto/AuthDto.kt +++ /dev/null @@ -1,90 +0,0 @@ -package ru.vsu.app.dto - -import io.swagger.v3.oas.annotations.media.Schema - -@Schema(description = "Запрос на вход в систему") -data class LoginRequest( - @Schema(description = "Email пользователя", example = "user@example.com") - val email: String, - - @Schema(description = "Пароль пользователя", example = "password123") - val password: String -) - -@Schema(description = "Ответ на успешный вход в систему") -data class LoginResponse( - @Schema(description = "JWT токен для авторизации", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") - val token: String, - - @Schema(description = "Имя пользователя", example = "johndoe") - val username: String, - - @Schema(description = "Email пользователя", example = "user@example.com") - val email: String -) - -@Schema(description = "Запрос на регистрацию нового пользователя") -data class RegisterRequest( - @Schema(description = "Email пользователя", example = "user@example.com") - val email: String, - - @Schema(description = "Имя пользователя", example = "johndoe") - val username: String, - - @Schema(description = "Пароль пользователя", example = "password123") - val password: String -) - -@Schema(description = "Ответ на запрос регистрации") -data class RegisterResponse( - @Schema(description = "Сообщение о результате операции", example = "Пользователь успешно зарегистрирован") - val message: String, - - @Schema(description = "Флаг успешности операции", example = "true") - val success: Boolean -) - -@Schema(description = "Запрос на сброс пароля") -data class ForgotPasswordRequest( - @Schema(description = "Email пользователя", example = "user@example.com") - val email: String -) - -@Schema(description = "Запрос на верификацию кода сброса пароля") -data class ResetPasswordRequest( - @Schema(description = "Код подтверждения", example = "123456") - val token: String, - - @Schema(description = "Новый пароль", example = "newPassword123") - val newPassword: String -) - -@Schema(description = "Запрос на активацию аккаунта по коду") -data class ActivateAccountRequest( - @Schema(description = "Email пользователя", example = "user@example.com") - val email: String, - - @Schema(description = "Код активации", example = "123456") - val code: String -) - -@Schema(description = "Информация о пользователе") -data class UserInfoResponse( - @Schema(description = "Идентификатор пользователя", example = "1") - val id: Long, - - @Schema(description = "Имя пользователя", example = "johndoe") - val username: String, - - @Schema(description = "Email пользователя", example = "user@example.com") - val email: String -) - -@Schema(description = "Стандартный ответ API") -data class ApiResponse( - @Schema(description = "Сообщение о результате операции", example = "Операция выполнена успешно") - val message: String, - - @Schema(description = "Флаг успешности операции", example = "true") - val success: Boolean -) \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/CardDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/CardDto.kt index 3e9877e..cccff53 100644 --- a/backend/src/main/kotlin/ru/vsu/app/dto/CardDto.kt +++ b/backend/src/main/kotlin/ru/vsu/app/dto/CardDto.kt @@ -1,12 +1,78 @@ package ru.vsu.app.dto +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param cardID Уникальный идентификатор карты + * @param name Название карты + * @param imageURL Ссылка на изображение + * @param rarity Редкость карты + * @param minPrice Минимальная цена - используется для разбора карточки + * @param isGenerated Флаг показывающий сгенерированная ли карта (true для уникальных, false для остальных) + * @param description Описание карты + * @param theme Тематика, на которую была сгенерирвана карта (только для уникальных карт) + */ data class CardDto( - val id: Long, - val name: String, - val imageUrl: String, - val rarity: Int, - val collection: String, - val description: String, - val type: String, - val disassemblePrice: Int -) \ No newline at end of file + + @Schema(example = "501", required = true, description = "Уникальный идентификатор карты") + @get:JsonProperty("card_ID", required = true) val cardID: kotlin.Int, + + @Schema(example = "Ледяной феникс", required = true, description = "Название карты") + @get:JsonProperty("name", required = true) val name: kotlin.String, + + @Schema(example = "https://example.com/cards/pheonix.png", required = true, description = "Ссылка на изображение") + @get:JsonProperty("imageURL", required = true) val imageURL: kotlin.String, + + @Schema(example = "Редкая", required = true, description = "Редкость карты") + @get:JsonProperty("rarity", required = true) val rarity: CardDto.Rarity, + + @Schema(example = "50", required = true, description = "Минимальная цена - используется для разбора карточки") + @get:JsonProperty("min_price", required = true) val minPrice: kotlin.Int, + + @Schema(example = "false", required = true, description = "Флаг показывающий сгенерированная ли карта (true для уникальных, false для остальных)") + @get:JsonProperty("isGenerated", required = true) val isGenerated: kotlin.Boolean, + + @Schema(example = "Ледяной Феникс — это мифическое существо, воплощающее силу зимы и вечного обновления. Его перья сверкают как морозный утренний иней, а глаза светятся холодным синим светом.", description = "Описание карты") + @get:JsonProperty("description") val description: kotlin.String? = null, + + @Schema(example = "Мифическое существо", description = "Тематика, на которую была сгенерирвана карта (только для уникальных карт)") + @get:JsonProperty("theme") val theme: kotlin.String? = null + ) { + + /** + * Редкость карты + * Values: Обычная,Редкая,Эпическая,Легендарная,Уникальная + */ + enum class Rarity(@get:JsonValue val value: kotlin.String) { + + Обычная("Обычная"), + Редкая("Редкая"), + Эпическая("Эпическая"), + Легендарная("Легендарная"), + Уникальная("Уникальная"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): Rarity { + return values().first{it -> it.value == value} + } + } + } + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/CardResponse.kt b/backend/src/main/kotlin/ru/vsu/app/dto/CardResponse.kt deleted file mode 100644 index 80febdf..0000000 --- a/backend/src/main/kotlin/ru/vsu/app/dto/CardResponse.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ru.vsu.app.dto - -data class CardResponse( - val id: Long, - val name: String, - val imageUrl: String, - val rarity: Int, - val collection: String, - val description: String, - val type: String, - val disassemblePrice: Int -) \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/CoinOfferDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/CoinOfferDto.kt new file mode 100644 index 0000000..53ba6d8 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/CoinOfferDto.kt @@ -0,0 +1,48 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param offerId + * @param name + * @param coinsAmount + * @param price + * @param imageUrl + * @param description + */ +data class CoinOfferDto( + + @Schema(example = "1", description = "") + @get:JsonProperty("offer_id") val offerId: kotlin.Int? = null, + + @Schema(example = "Стартовый набор", description = "") + @get:JsonProperty("name") val name: kotlin.String? = null, + + @Schema(example = "100", description = "") + @get:JsonProperty("coinsAmount") val coinsAmount: kotlin.Int? = null, + + @Schema(example = "99.9", description = "") + @get:JsonProperty("price") val price: kotlin.Double? = null, + + @field:Valid + @Schema(description = "") + @get:JsonProperty("imageUrl") val imageUrl: java.net.URI? = null, + + @Schema(example = "Стартовый набор. Вы можете получить...", description = "") + @get:JsonProperty("description") val description: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/CollectionDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/CollectionDto.kt new file mode 100644 index 0000000..5704329 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/CollectionDto.kt @@ -0,0 +1,41 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.dto.CardDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param collectionID Уникальный идентификатор коллекции + * @param name Название коллекции + * @param cards Карты в коллекции + * @param imageURL Обложка коллекции + */ +data class CollectionDto( + + @Schema(example = "801", required = true, description = "Уникальный идентификатор коллекции") + @get:JsonProperty("collection_ID", required = true) val collectionID: kotlin.Int, + + @Schema(example = "Драконы", required = true, description = "Название коллекции") + @get:JsonProperty("name", required = true) val name: kotlin.String, + + @field:Valid + @Schema(required = true, description = "Карты в коллекции") + @get:JsonProperty("cards", required = true) val cards: kotlin.collections.List, + + @Schema(example = "https://example.com/collections/dragons.png", required = true, description = "Обложка коллекции") + @get:JsonProperty("imageURL", required = true) val imageURL: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/NewsDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/NewsDto.kt new file mode 100644 index 0000000..0629f3e --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/NewsDto.kt @@ -0,0 +1,43 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param newsID Уникальный идентификатор новости + * @param title Заголовок новости + * @param content Содержание новости + * @param datePosted Дата публикации + * @param pictures Ссылки на изображения + */ +data class NewsDto( + + @Schema(example = "1", required = true, description = "Уникальный идентификатор новости") + @get:JsonProperty("news_ID", required = true) val newsID: kotlin.Int, + + @Schema(example = "Новое обновление системы", required = true, description = "Заголовок новости") + @get:JsonProperty("title", required = true) val title: kotlin.String, + + @Schema(example = "Мы добавили новые карты в коллекцию", required = true, description = "Содержание новости") + @get:JsonProperty("content", required = true) val content: kotlin.String, + + @Schema(example = "2024-05-20T14:48Z", required = true, description = "Дата публикации") + @get:JsonProperty("datePosted", required = true) val datePosted: java.time.LocalDateTime, + + @Schema(example = "[\"https://example.com/news1.jpg\",\"https://example.com/news2.jpg\"]", description = "Ссылки на изображения") + @get:JsonProperty("pictures") val pictures: kotlin.collections.List? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/NotificationDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/NotificationDto.kt new file mode 100644 index 0000000..9ac41f7 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/NotificationDto.kt @@ -0,0 +1,43 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param notificationID Уникальный идентификатор уведомления + * @param userID Уникальный идентификатор пользователя + * @param message Текст уведомления + * @param notificationDateTime Дата и время уведомления + * @param links Ссылка на подробности обмена + */ +data class NotificationDto( + + @Schema(example = "201", required = true, description = "Уникальный идентификатор уведомления") + @get:JsonProperty("notification_ID", required = true) val notificationID: kotlin.Int, + + @Schema(example = "1", required = true, description = "Уникальный идентификатор пользователя") + @get:JsonProperty("user_id", required = true) val userID: kotlin.Int, + + @Schema(example = "Ваша карта была успешно обменяна", required = true, description = "Текст уведомления") + @get:JsonProperty("message", required = true) val message: kotlin.String, + + @Schema(example = "2024-05-20T15:30Z", required = true, description = "Дата и время уведомления") + @get:JsonProperty("notificationDateTime", required = true) val notificationDateTime: java.time.LocalDateTime, + + @Schema(example = "[\"https://example.com/exchange-details\"]", description = "Ссылка на подробности обмена") + @get:JsonProperty("links") val links: kotlin.collections.List? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/PackDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/PackDto.kt new file mode 100644 index 0000000..a88261f --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/PackDto.kt @@ -0,0 +1,45 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.dto.CardDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param packID Уникальный идентификатор набора + * @param name Название набора + * @param imageURL Ссылка на изображение + * @param cards Карты в наборе + * @param price Цена + */ +data class PackDto( + + @Schema(example = "401", required = true, description = "Уникальный идентификатор набора") + @get:JsonProperty("pack_ID", required = true) val packID: kotlin.Int, + + @Schema(example = "Стартовый набор", required = true, description = "Название набора") + @get:JsonProperty("name", required = true) val name: kotlin.String, + + @Schema(example = "https://example.com/packs/starter.png", required = true, description = "Ссылка на изображение") + @get:JsonProperty("imageURL", required = true) val imageURL: kotlin.String, + + @field:Valid + @Schema(required = true, description = "Карты в наборе") + @get:JsonProperty("cards", required = true) val cards: kotlin.collections.List, + + @Schema(example = "1000", required = true, description = "Цена") + @get:JsonProperty("price", required = true) val price: kotlin.Int + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/PaymentCallbackDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/PaymentCallbackDto.kt new file mode 100644 index 0000000..0a383e7 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/PaymentCallbackDto.kt @@ -0,0 +1,64 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param transactionId + * @param status + * @param amount + * @param currency + * @param offerId + */ +data class PaymentCallbackDto( + + @Schema(required = true, description = "") + @get:JsonProperty("transactionId", required = true) val transactionId: kotlin.String, + + @Schema(required = true, description = "") + @get:JsonProperty("status", required = true) val status: PaymentCallbackDto.Status, + + @Schema(required = true, description = "") + @get:JsonProperty("amount", required = true) val amount: java.math.BigDecimal, + + @Schema(description = "") + @get:JsonProperty("currency") val currency: kotlin.String? = null, + + @Schema(description = "") + @get:JsonProperty("offerId") val offerId: kotlin.Int? = null + ) { + + /** + * + * Values: Успешно,Ошибка,В_процессе + */ + enum class Status(@get:JsonValue val value: kotlin.String) { + + Успешно("Успешно"), + Ошибка("Ошибка"), + В_процессе("В процессе"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): Status { + return values().first{it -> it.value == value} + } + } + } + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/PaymentDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/PaymentDto.kt new file mode 100644 index 0000000..840edb5 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/PaymentDto.kt @@ -0,0 +1,70 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import ru.vsu.app.dto.UserDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param paymentID Уникальный идентификатор платежа + * @param user + * @param paymentSUM Сумма платежа + * @param paymentStatus Статус платежа + * @param paymentDateTime Дата и время платежа + * @param bankCardData Данные карты (только для записи) + */ +data class PaymentDto( + + @Schema(example = "601", required = true, description = "Уникальный идентификатор платежа") + @get:JsonProperty("payment_ID", required = true) val paymentID: kotlin.Int, + + @field:Valid + @Schema(required = true, description = "") + @get:JsonProperty("user", required = true) val user: UserDto, + + @Schema(example = "199.99", required = true, description = "Сумма платежа") + @get:JsonProperty("paymentSUM", required = true) val paymentSUM: kotlin.Double, + + @Schema(example = "Оплачено", required = true, description = "Статус платежа") + @get:JsonProperty("paymentStatus", required = true) val paymentStatus: PaymentDto.PaymentStatus, + + @Schema(example = "2024-05-20T17:30Z", required = true, description = "Дата и время платежа") + @get:JsonProperty("paymentDateTime", required = true) val paymentDateTime: java.time.LocalDateTime, + + @Schema(description = "Данные карты (только для записи)") + @get:JsonProperty("bankCardData") val bankCardData: kotlin.collections.List? = null + ) { + + /** + * Статус платежа + * Values: В_обработке,Оплачено,Ошибка + */ + enum class PaymentStatus(@get:JsonValue val value: kotlin.String) { + + В_обработке("В обработке"), + Оплачено("Оплачено"), + Ошибка("Ошибка"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): PaymentStatus { + return values().first{it -> it.value == value} + } + } + } + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/QuestDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/QuestDto.kt new file mode 100644 index 0000000..9b51588 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/QuestDto.kt @@ -0,0 +1,61 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.dto.PackDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param questID Уникальный идентификатор квеста + * @param name Название квеста + * @param description Описание квеста + * @param progress Текущий прогресс + * @param target Целевое значение + * @param rewardCoins Награда в монетах + * @param isCompleted Выполнен ли квест + * @param isClaimed Получена ли награда + * @param rewardPacks Награда в наборах + */ +data class QuestDto( + + @Schema(example = "1", required = true, description = "Уникальный идентификатор квеста") + @get:JsonProperty("quest_ID", required = true) val questID: kotlin.Int, + + @Schema(example = "Соберите 5 карт", required = true, description = "Название квеста") + @get:JsonProperty("name", required = true) val name: kotlin.String, + + @Schema(example = "Соберите 5 карт из коллекции Драконы", required = true, description = "Описание квеста") + @get:JsonProperty("description", required = true) val description: kotlin.String, + + @Schema(example = "3", required = true, description = "Текущий прогресс") + @get:JsonProperty("progress", required = true) val progress: kotlin.Int, + + @Schema(example = "5", required = true, description = "Целевое значение") + @get:JsonProperty("target", required = true) val target: kotlin.Int, + + @Schema(example = "200", required = true, description = "Награда в монетах") + @get:JsonProperty("rewardCoins", required = true) val rewardCoins: kotlin.Int, + + @Schema(example = "false", required = true, description = "Выполнен ли квест") + @get:JsonProperty("isCompleted", required = true) val isCompleted: kotlin.Boolean, + + @Schema(example = "false", required = true, description = "Получена ли награда") + @get:JsonProperty("isClaimed", required = true) val isClaimed: kotlin.Boolean, + + @field:Valid + @Schema(description = "Награда в наборах") + @get:JsonProperty("rewardPacks") val rewardPacks: kotlin.collections.List? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/ReportDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/ReportDto.kt new file mode 100644 index 0000000..b0a76d9 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/ReportDto.kt @@ -0,0 +1,71 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import ru.vsu.app.dto.UserDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param reportID Уникальный идентификатор жалобы + * @param reporter + * @param reportedUser + * @param reportDateTime Дата и время жалобы + * @param reason непристойный никнейм + * @param status Статус рассмотрения + */ +data class ReportDto( + + @Schema(example = "301", required = true, description = "Уникальный идентификатор жалобы") + @get:JsonProperty("report_ID", required = true) val reportID: kotlin.Int, + + @field:Valid + @Schema(required = true, description = "") + @get:JsonProperty("reporter", required = true) val reporter: UserDto, + + @field:Valid + @Schema(required = true, description = "") + @get:JsonProperty("reportedUser", required = true) val reportedUser: UserDto, + + @Schema(example = "2024-05-20T16:45Z", required = true, description = "Дата и время жалобы") + @get:JsonProperty("reportDateTime", required = true) val reportDateTime: java.time.LocalDateTime, + + @Schema(example = "Непристойный контент", required = true, description = "непристойный никнейм") + @get:JsonProperty("reason", required = true) val reason: kotlin.String, + + @Schema(example = "На расмотрении", required = true, description = "Статус рассмотрения") + @get:JsonProperty("status", required = true) val status: ReportDto.Status + ) { + + /** + * Статус рассмотрения + * Values: На_рассмотрении,Подтверждено,Отклонено + */ + enum class Status(@get:JsonValue val value: kotlin.String) { + + На_рассмотрении("На рассмотрении"), + Подтверждено("Подтверждено"), + Отклонено("Отклонено"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): Status { + return values().first{it -> it.value == value} + } + } + } + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/ShopDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/ShopDto.kt deleted file mode 100644 index a3b1b3d..0000000 --- a/backend/src/main/kotlin/ru/vsu/app/dto/ShopDto.kt +++ /dev/null @@ -1,35 +0,0 @@ -package ru.vsu.app.dto - -data class PackResponse( - val id: Long, - val name: String, - val imageUrl: String, - val price: Int, - val cards: List -) - -data class CoinOfferResponse( - val id: Long, - val name: String, - val coinsAmount: Int, - val price: Double, - val imageUrl: String, - val description: String? -) - -data class PurchasePackResponse( - val receivedCards: List, - val newBalance: Int -) - -data class PurchaseCoinsResponse( - val paymentUrl: String -) - -data class PaymentCallbackRequest( - val transactionId: String, - val status: String, - val amount: Double, - val currency: String, - val offerId: Long -) \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/ThemeDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/ThemeDto.kt new file mode 100644 index 0000000..a2273ee --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/ThemeDto.kt @@ -0,0 +1,35 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param themeID Уникальный идентификатор темы + * @param name Название темы + * @param description Описание темы + */ +data class ThemeDto( + + @Schema(example = "901", required = true, description = "Уникальный идентификатор темы") + @get:JsonProperty("theme_ID", required = true) val themeID: kotlin.Int, + + @Schema(example = "Мифические существа", required = true, description = "Название темы") + @get:JsonProperty("name", required = true) val name: kotlin.String, + + @Schema(example = "Создайте изображение мифического существа", description = "Описание темы") + @get:JsonProperty("description") val description: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/TradeDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/TradeDto.kt new file mode 100644 index 0000000..6c37a99 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/TradeDto.kt @@ -0,0 +1,57 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.dto.CardDto +import ru.vsu.app.dto.UserDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param tradeID Уникальный идентификатор обмена + * @param offeringUser + * @param offeringCards Предлагаемые карты + * @param receivingUser + * @param receivingCards Запрашиваемые карты + * @param isConfirmed Статус подтверждения + * @param tradeDateTime Дата и время обмена + */ +data class TradeDto( + + @Schema(example = "701", required = true, description = "Уникальный идентификатор обмена") + @get:JsonProperty("trade_ID", required = true) val tradeID: kotlin.Int, + + @field:Valid + @Schema(required = true, description = "") + @get:JsonProperty("offeringUser", required = true) val offeringUser: UserDto, + + @field:Valid + @Schema(required = true, description = "Предлагаемые карты") + @get:JsonProperty("offeringCards", required = true) val offeringCards: kotlin.collections.List, + + @field:Valid + @Schema(required = true, description = "") + @get:JsonProperty("receivingUser", required = true) val receivingUser: UserDto, + + @field:Valid + @Schema(required = true, description = "Запрашиваемые карты") + @get:JsonProperty("receivingCards", required = true) val receivingCards: kotlin.collections.List, + + @Schema(example = "false", required = true, description = "Статус подтверждения") + @get:JsonProperty("isConfirmed", required = true) val isConfirmed: kotlin.Boolean, + + @Schema(example = "2024-05-20T18:00Z", required = true, description = "Дата и время обмена") + @get:JsonProperty("tradeDateTime", required = true) val tradeDateTime: java.time.LocalDateTime + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/UserDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/UserDto.kt new file mode 100644 index 0000000..97eba45 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/UserDto.kt @@ -0,0 +1,79 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.dto.AchievementDto +import ru.vsu.app.dto.CardDto +import ru.vsu.app.dto.NotificationDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param userID Уникальный идентификатор + * @param username Имя пользователя + * @param email Электронная почта + * @param password Пароль + * @param inventoryCards Карты в инвентаре + * @param balance Баланс + * @param achievements Достижения + * @param favoriteCards Избранные карты + * @param onChange Карты на обмен + * @param avatarUrl Аватар пользователя + * @param favoriteAchievements Избранные достижения + * @param notifications Уведомления + */ +data class UserDto( + + @Schema(example = "1", required = true, description = "Уникальный идентификатор") + @get:JsonProperty("user_id", required = true) val userID: kotlin.Int, + + @Schema(example = "Коллекционер_123", required = true, description = "Имя пользователя") + @get:JsonProperty("username", required = true) val username: kotlin.String, + + @get:Email + @Schema(example = "user@example.com", required = true, description = "Электронная почта") + @get:JsonProperty("email", required = true) val email: kotlin.String, + + + @field:Valid + @Schema(required = true, description = "Карты в инвентаре") + @get:JsonProperty("inventoryCards", required = true) val inventoryCards: kotlin.collections.List, + + @Schema(example = "2500", required = true, description = "Баланс") + @get:JsonProperty("balance", required = true) val balance: kotlin.Int, + + @field:Valid + @Schema(required = true, description = "Достижения") + @get:JsonProperty("achievements", required = true) val achievements: kotlin.collections.List, + + @field:Valid + @Schema(description = "Избранные карты") + @get:JsonProperty("favoriteCards") val favoriteCards: kotlin.collections.List? = null, + + @field:Valid + @Schema(description = "Карты на обмен") + @get:JsonProperty("onChange") val onChange: kotlin.collections.List? = null, + + @Schema(example = "https://example.com/avatars/user1.png", description = "Аватар пользователя") + @get:JsonProperty("avatar_url") val avatarUrl: kotlin.String? = null, + + @field:Valid + @Schema(description = "Избранные достижения") + @get:JsonProperty("favoriteAchievements") val favoriteAchievements: kotlin.collections.List? = null, + + @field:Valid + @Schema(description = "Уведомления") + @get:JsonProperty("notifications") val notifications: kotlin.collections.List? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/UserSettingsDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/UserSettingsDto.kt new file mode 100644 index 0000000..8a244b4 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/UserSettingsDto.kt @@ -0,0 +1,35 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param notificationsEnabled Включены ли уведомления + * @param showInventory Виден ли инвентарь другим пользователям + * @param autoDeclineTrades Автоматически отклонять все входящие предложения обмена + */ +data class UserSettingsDto( + + @Schema(example = "true", required = true, description = "Включены ли уведомления") + @get:JsonProperty("notificationsEnabled", required = true) val notificationsEnabled: kotlin.Boolean, + + @Schema(example = "false", required = true, description = "Виден ли инвентарь другим пользователям") + @get:JsonProperty("showInventory", required = true) val showInventory: kotlin.Boolean, + + @Schema(example = "false", required = true, description = "Автоматически отклонять все входящие предложения обмена") + @get:JsonProperty("autoDeclineTrades", required = true) val autoDeclineTrades: kotlin.Boolean + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/UserStatsDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/UserStatsDto.kt new file mode 100644 index 0000000..0445fd3 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/UserStatsDto.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param totalCards + * @param completedCollections + */ +data class UserStatsDto( + + @Schema(example = "150", description = "") + @get:JsonProperty("totalCards") val totalCards: kotlin.Int? = null, + + @Schema(example = "5", description = "") + @get:JsonProperty("completedCollections") val completedCollections: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminReportsReportIDPutRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminReportsReportIDPutRequest.kt new file mode 100644 index 0000000..b509743 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminReportsReportIDPutRequest.kt @@ -0,0 +1,80 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param status + * @param action + * @param banDurationDays Длительность бана в днях (только для temporary_ban) + * @param comment + */ +data class AdminReportsReportIDPutRequest( + + @Schema(example = "resolved", required = true, description = "") + @get:JsonProperty("status", required = true) val status: AdminReportsReportIDPutRequest.Status, + + @Schema(example = "temporary_ban", required = true, description = "") + @get:JsonProperty("action", required = true) val action: AdminReportsReportIDPutRequest.Action, + + @Schema(example = "7", description = "Длительность бана в днях (только для temporary_ban)") + @get:JsonProperty("banDurationDays") val banDurationDays: kotlin.Int? = null, + + @Schema(example = "Пользователь получил временный бан на 7 дней за нарушение правил", description = "") + @get:JsonProperty("comment") val comment: kotlin.String? = null + ) { + + /** + * + * Values: pending,reviewed,resolved + */ + enum class Status(@get:JsonValue val value: kotlin.String) { + + pending("pending"), + reviewed("reviewed"), + resolved("resolved"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): Status { + return values().first{it -> it.value == value} + } + } + } + + /** + * + * Values: none,warning,temporary_ban,permanent_ban + */ + enum class Action(@get:JsonValue val value: kotlin.String) { + + none("none"), + warning("warning"), + temporary_ban("temporary_ban"), + permanent_ban("permanent_ban"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): Action { + return values().first{it -> it.value == value} + } + } + } + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminTradesTradeIDInvalidatePostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminTradesTradeIDInvalidatePostRequest.kt new file mode 100644 index 0000000..1b9cd35 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminTradesTradeIDInvalidatePostRequest.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param reason Причина аннулирования обмена + */ +data class AdminTradesTradeIDInvalidatePostRequest( + + @Schema(required = true, description = "Причина аннулирования обмена") + @get:JsonProperty("reason", required = true) val reason: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminUsersUserIDBanPostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminUsersUserIDBanPostRequest.kt new file mode 100644 index 0000000..a11f553 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminUsersUserIDBanPostRequest.kt @@ -0,0 +1,55 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param banType + * @param reason + * @param durationDays Длительность бана в днях + */ +data class AdminUsersUserIDBanPostRequest( + + @Schema(example = "temporary", required = true, description = "") + @get:JsonProperty("banType", required = true) val banType: AdminUsersUserIDBanPostRequest.BanType, + + @Schema(example = "Нарушение правил сообщества", required = true, description = "") + @get:JsonProperty("reason", required = true) val reason: kotlin.String, + + @Schema(example = "7", description = "Длительность бана в днях") + @get:JsonProperty("durationDays") val durationDays: kotlin.Int? = null + ) { + + /** + * + * Values: temporary,permanent + */ + enum class BanType(@get:JsonValue val value: kotlin.String) { + + temporary("temporary"), + permanent("permanent"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): BanType { + return values().first{it -> it.value == value} + } + } + } + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminUsersUserIDQuestsQuestIDResetPostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminUsersUserIDQuestsQuestIDResetPostRequest.kt new file mode 100644 index 0000000..5df3e81 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminUsersUserIDQuestsQuestIDResetPostRequest.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param reason Причина сброса квеста + * @param removeRewards Нужно ли удалить награды за квест + */ +data class AdminUsersUserIDQuestsQuestIDResetPostRequest( + + @Schema(required = true, description = "Причина сброса квеста") + @get:JsonProperty("reason", required = true) val reason: kotlin.String, + + @Schema(description = "Нужно ли удалить награды за квест") + @get:JsonProperty("removeRewards") val removeRewards: kotlin.Boolean? = false + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/HomeGenerateCardPostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/HomeGenerateCardPostRequest.kt new file mode 100644 index 0000000..0ff782d --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/HomeGenerateCardPostRequest.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param themeID ID выбранной темы + */ +data class HomeGenerateCardPostRequest( + + @Schema(example = "901", required = true, description = "ID выбранной темы") + @get:JsonProperty("theme_ID", required = true) val themeID: kotlin.Int + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/HomeQuestsClaimRewardPostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/HomeQuestsClaimRewardPostRequest.kt new file mode 100644 index 0000000..a0d1901 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/HomeQuestsClaimRewardPostRequest.kt @@ -0,0 +1,47 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param questType + */ +data class HomeQuestsClaimRewardPostRequest( + + @Schema(example = "daily", required = true, description = "") + @get:JsonProperty("questType", required = true) val questType: HomeQuestsClaimRewardPostRequest.QuestType + ) { + + /** + * + * Values: daily,weekly + */ + enum class QuestType(@get:JsonValue val value: kotlin.String) { + + daily("daily"), + weekly("weekly"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): QuestType { + return values().first{it -> it.value == value} + } + } + } + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/InventoryDestroyPostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/InventoryDestroyPostRequest.kt new file mode 100644 index 0000000..0181295 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/InventoryDestroyPostRequest.kt @@ -0,0 +1,28 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param cardID + */ +data class InventoryDestroyPostRequest( + + @get:Min(1) + @Schema(example = "501", required = true, description = "") + @get:JsonProperty("card_ID", required = true) val cardID: kotlin.Int + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/LoginUserRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/LoginUserRequest.kt new file mode 100644 index 0000000..7872234 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/LoginUserRequest.kt @@ -0,0 +1,32 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param email + * @param password + */ +data class LoginUserRequest( + + @get:Email + @Schema(example = "user@example.com", required = true, description = "") + @get:JsonProperty("email", required = true) val email: kotlin.String, + + @Schema(example = "SecurePass123!", required = true, description = "") + @get:JsonProperty("password", required = true) val password: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/NewsRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/NewsRequest.kt new file mode 100644 index 0000000..3c786d3 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/NewsRequest.kt @@ -0,0 +1,22 @@ +package ru.vsu.app.dto.requests + +import com.fasterxml.jackson.annotation.JsonProperty +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import java.time.LocalDateTime + +data class NewsRequest( + @Schema(description = "Заголовок новости", example = "Новое обновление") + @get:JsonProperty("title", required = true) + @field:NotBlank + val title: String, + + @Schema(description = "Содержание новости", example = "Добавлены новые карты") + @get:JsonProperty("content", required = true) + @field:NotBlank + val content: String, + + @Schema(description = "Список изображений") + @get:JsonProperty("pictures") + val pictures: List? = null +) diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/OtherProfileUserIDReportPostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/OtherProfileUserIDReportPostRequest.kt new file mode 100644 index 0000000..a1cece4 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/OtherProfileUserIDReportPostRequest.kt @@ -0,0 +1,53 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param reason Причина жалобы + * @param comment Дополнительные комментарии + */ +data class OtherProfileUserIDReportPostRequest( + + @Schema(example = "Неуместный никнейм", required = true, description = "Причина жалобы") + @get:JsonProperty("reason", required = true) val reason: OtherProfileUserIDReportPostRequest.Reason, + + @get:Size(max=500) + @Schema(example = "Никнейм содержит нецензурные слова", description = "Дополнительные комментарии") + @get:JsonProperty("comment") val comment: kotlin.String? = null + ) { + + /** + * Причина жалобы + * Values: Неуместный_никнейм,Неуместный_аватар,Другое + */ + enum class Reason(@get:JsonValue val value: kotlin.String) { + + Неуместный_никнейм("Неуместный никнейм"), + Неуместный_аватар("Неуместный аватар"), + Другое("Другое"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): Reason { + return values().first{it -> it.value == value} + } + } + } + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/RegisterUserRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/RegisterUserRequest.kt new file mode 100644 index 0000000..194a619 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/RegisterUserRequest.kt @@ -0,0 +1,38 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param email Email пользователя + * @param username Имя пользователя (3-25 символов) + * @param password Пароль (минимум 8 символов) + */ +data class RegisterUserRequest( + + @get:Email + @Schema(example = "user@example.com", required = true, description = "Email пользователя") + @get:JsonProperty("email", required = true) val email: kotlin.String, + + @get:Size(min=3,max=25) + @Schema(example = "Коллекционер_123", required = true, description = "Имя пользователя (3-25 символов)") + @get:JsonProperty("username", required = true) val username: kotlin.String, + + @get:Size(min=8) + @Schema(example = "Password123!", required = true, description = "Пароль (минимум 8 символов)") + @get:JsonProperty("password", required = true) val password: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/RequestPasswordResetRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/RequestPasswordResetRequest.kt new file mode 100644 index 0000000..afa50e6 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/RequestPasswordResetRequest.kt @@ -0,0 +1,28 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param email + */ +data class RequestPasswordResetRequest( + + @get:Email + @Schema(example = "user@example.com", required = true, description = "") + @get:JsonProperty("email", required = true) val email: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/ResendVerificationCodeRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/ResendVerificationCodeRequest.kt new file mode 100644 index 0000000..823aa14 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/ResendVerificationCodeRequest.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param tempToken + */ +data class ResendVerificationCodeRequest( + + @Schema(example = "temp_abc123", required = true, description = "") + @get:JsonProperty("tempToken", required = true) val tempToken: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/ResetPasswordRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/ResetPasswordRequest.kt new file mode 100644 index 0000000..db9f219 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/ResetPasswordRequest.kt @@ -0,0 +1,37 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param resetToken + * @param code + * @param newPassword + */ +data class ResetPasswordRequest( + + @Schema(example = "reset_xyz789", required = true, description = "") + @get:JsonProperty("resetToken", required = true) val resetToken: kotlin.String, + + @get:Pattern(regexp="^\\d{6}$") + @Schema(example = "654321", required = true, description = "") + @get:JsonProperty("code", required = true) val code: kotlin.String, + + @get:Size(min=8) + @Schema(example = "NewSecurePass123!", required = true, description = "") + @get:JsonProperty("newPassword", required = true) val newPassword: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/ShopCoinsOffersOfferIdPurchasePostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/ShopCoinsOffersOfferIdPurchasePostRequest.kt new file mode 100644 index 0000000..5f18896 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/ShopCoinsOffersOfferIdPurchasePostRequest.kt @@ -0,0 +1,28 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param redirectUrl + */ +data class ShopCoinsOffersOfferIdPurchasePostRequest( + + @field:Valid + @Schema(example = "https://cardly.ru/shop/payment-callback", required = true, description = "") + @get:JsonProperty("redirectUrl", required = true) val redirectUrl: java.net.URI + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/TradesInitiatePostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/TradesInitiatePostRequest.kt new file mode 100644 index 0000000..402aebd --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/TradesInitiatePostRequest.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param offeredCardId ID карты, которую предлагает текущий пользователь + * @param requestedCardId ID карт, на которые пользователь готов совершить \"быстрый обмен\" + */ +data class TradesInitiatePostRequest( + + @Schema(example = "401", required = true, description = "ID карты, которую предлагает текущий пользователь") + @get:JsonProperty("offeredCardId", required = true) val offeredCardId: kotlin.Int, + + @Schema(example = "[100,200]", required = true, description = "ID карт, на которые пользователь готов совершить \"быстрый обмен\"") + @get:JsonProperty("requestedCardId", required = true) val requestedCardId: kotlin.collections.List + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/VerifyEmailRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/VerifyEmailRequest.kt new file mode 100644 index 0000000..de91b79 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/VerifyEmailRequest.kt @@ -0,0 +1,32 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param tempToken Временный токен из ответа регистрации + * @param code 6-значный код подтверждения + */ +data class VerifyEmailRequest( + + @Schema(example = "temp_abc123", required = true, description = "Временный токен из ответа регистрации") + @get:JsonProperty("tempToken", required = true) val tempToken: kotlin.String, + + @get:Pattern(regexp="^\\d{6}$") + @Schema(example = "123456", required = true, description = "6-значный код подтверждения") + @get:JsonProperty("code", required = true) val code: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminStatsGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminStatsGet200Response.kt new file mode 100644 index 0000000..b8c78f0 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminStatsGet200Response.kt @@ -0,0 +1,55 @@ +package ru.vsu.app.dto.responses.admin + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param totalUsers + * @param activeUsers + * @param bannedUsers + * @param totalCards + * @param totalTrades + * @param completedTrades + * @param invalidatedTrades + * @param pendingReports + */ +data class AdminStatsGet200Response( + + @Schema(example = "1000", description = "") + @get:JsonProperty("totalUsers") val totalUsers: kotlin.Int? = null, + + @Schema(example = "750", description = "") + @get:JsonProperty("activeUsers") val activeUsers: kotlin.Int? = null, + + @Schema(example = "25", description = "") + @get:JsonProperty("bannedUsers") val bannedUsers: kotlin.Int? = null, + + @Schema(example = "5000", description = "") + @get:JsonProperty("totalCards") val totalCards: kotlin.Int? = null, + + @Schema(example = "1200", description = "") + @get:JsonProperty("totalTrades") val totalTrades: kotlin.Int? = null, + + @Schema(example = "1000", description = "") + @get:JsonProperty("completedTrades") val completedTrades: kotlin.Int? = null, + + @Schema(example = "50", description = "") + @get:JsonProperty("invalidatedTrades") val invalidatedTrades: kotlin.Int? = null, + + @Schema(example = "15", description = "") + @get:JsonProperty("pendingReports") val pendingReports: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminTradesTradeIDInvalidatePost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminTradesTradeIDInvalidatePost200Response.kt new file mode 100644 index 0000000..df120ba --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminTradesTradeIDInvalidatePost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.admin + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class AdminTradesTradeIDInvalidatePost200Response( + + @Schema(example = "Обмен успешно аннулирован", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDBanPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDBanPost200Response.kt new file mode 100644 index 0000000..19fa64c --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDBanPost200Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.admin + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + * @param banEndDate + */ +data class AdminUsersUserIDBanPost200Response( + + @Schema(example = "Пользователь успешно заблокирован", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null, + + @Schema(example = "2024-06-20T12:00Z", description = "") + @get:JsonProperty("banEndDate") val banEndDate: java.time.LocalDateTime? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDQuestsQuestIDResetPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDQuestsQuestIDResetPost200Response.kt new file mode 100644 index 0000000..9cf0466 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDQuestsQuestIDResetPost200Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.admin + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + * @param rewardsRemoved + */ +data class AdminUsersUserIDQuestsQuestIDResetPost200Response( + + @Schema(example = "Квест успешно сброшен", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null, + + @Schema(example = "true", description = "") + @get:JsonProperty("rewardsRemoved") val rewardsRemoved: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDUnbanPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDUnbanPost200Response.kt new file mode 100644 index 0000000..676999c --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDUnbanPost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.admin + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class AdminUsersUserIDUnbanPost200Response( + + @Schema(example = "Пользователь успешно разблокирован", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/AuthCheckGet401Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/AuthCheckGet401Response.kt new file mode 100644 index 0000000..12d8f36 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/AuthCheckGet401Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.auth + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param isAuthenticated + * @param message + */ +data class AuthCheckGet401Response( + + @Schema(example = "false", description = "") + @get:JsonProperty("isAuthenticated") val isAuthenticated: kotlin.Boolean? = null, + + @Schema(example = "Требуется авторизация", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser400Response.kt new file mode 100644 index 0000000..e5a85bb --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser400Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.auth.login + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class LoginUser400Response( + + @Schema(example = "Неверный фортмат email", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser401Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser401Response.kt new file mode 100644 index 0000000..159921e --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser401Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.auth.login + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class LoginUser401Response( + + @Schema(example = "Неверный email или пароль", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser200Response.kt new file mode 100644 index 0000000..7235868 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser200Response.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.dto.responses.auth.register + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param tempToken + * @param message + */ +data class RegisterUser200Response( + + @Schema(example = "temp_abc123", description = "") + @get:JsonProperty("tempToken") val tempToken: kotlin.String? = null, + + @Schema(example = "Код подтверждения отправлен на email", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null +) + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser400Response.kt new file mode 100644 index 0000000..ca7be33 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser400Response.kt @@ -0,0 +1,25 @@ +package ru.vsu.app.dto.responses.auth.register + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class RegisterUser400Response( + + @Schema(example = "Некорректный формат введенных данных", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null +) + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser409Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser409Response.kt new file mode 100644 index 0000000..6ccff31 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser409Response.kt @@ -0,0 +1,24 @@ +package ru.vsu.app.dto.responses.auth.register + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class RegisterUser409Response( + + @Schema(example = "Пользователь с таким email уже существует", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null +) \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset200Response.kt new file mode 100644 index 0000000..bb48a43 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset200Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.auth.requestpassword + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param resetToken + * @param message + */ +data class RequestPasswordReset200Response( + + @Schema(example = "reset_xyz789", description = "") + @get:JsonProperty("resetToken") val resetToken: kotlin.String? = null, + + @Schema(example = "Код подтверждения отправлен на email", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset400Response.kt new file mode 100644 index 0000000..7add1dd --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset400Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.auth.requestpassword + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class RequestPasswordReset400Response( + + @Schema(example = "Некорректный формат emaila", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset404Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset404Response.kt new file mode 100644 index 0000000..30d2585 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset404Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.auth.requestpassword + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class RequestPasswordReset404Response( + + @Schema(example = "Пользователь с таким email не зарегистрирован", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/ResetPassword200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/ResetPassword200Response.kt new file mode 100644 index 0000000..64ebe0d --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/ResetPassword200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.auth.requestpassword + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class ResetPassword200Response( + + @Schema(example = "Пароль успешно изменен", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/ResetPassword400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/ResetPassword400Response.kt new file mode 100644 index 0000000..75d07af --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/ResetPassword400Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.auth.requestpassword + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class ResetPassword400Response( + + @Schema(example = "Неверный код подтверждения", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/ResendVerificationCode200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/ResendVerificationCode200Response.kt new file mode 100644 index 0000000..a465b56 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/ResendVerificationCode200Response.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.dto.responses.auth.verifyemail + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class ResendVerificationCode200Response( + + @Schema(example = "Новый код подтверждения отправлен", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null, + @Schema(example = "Новый временный токен", description = "") + @get:JsonProperty("tempToken") val tempToken: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/VerifyEmail400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/VerifyEmail400Response.kt new file mode 100644 index 0000000..a488df6 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/VerifyEmail400Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.auth.verifyemail + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + * @param canRetry + */ +data class VerifyEmail400Response( + + @Schema(example = "Неверный код подтверждения", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null, + + @Schema(example = "true", description = "") + @get:JsonProperty("canRetry") val canRetry: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/VerifyEmail410Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/VerifyEmail410Response.kt new file mode 100644 index 0000000..9abd6bf --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/VerifyEmail410Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.auth.verifyemail + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class VerifyEmail410Response( + + @Schema(example = "Код устарел, запросите новый", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/common/InternalServerError.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/common/InternalServerError.kt new file mode 100644 index 0000000..5a2c78d --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/common/InternalServerError.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.common + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + * @param code + * @param timestamp + */ +data class InternalServerError( + @Schema(example = "Что-то пошло не так, попробуйте позже", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null, + + @Schema(example = "INTERNAL_SERVER_ERROR", description = "") + @get:JsonProperty("code") val code: kotlin.String? = null, + + @Schema(example = "2024-05-20T12:00Z", description = "") + @get:JsonProperty("timestamp") val timestamp: java.time.LocalDateTime? = null +) diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeGenerateCardPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeGenerateCardPost200Response.kt new file mode 100644 index 0000000..3127e89 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeGenerateCardPost200Response.kt @@ -0,0 +1,33 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.CardEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param card + * @param newBalance + */ +data class HomeGenerateCardPost200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("card") val card: CardEntity? = null, + + @Schema(example = "1500", description = "") + @get:JsonProperty("newBalance") val newBalance: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeGenerateCardPost402Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeGenerateCardPost402Response.kt new file mode 100644 index 0000000..8dcf42a --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeGenerateCardPost402Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + * @param requiredAmount + */ +data class HomeGenerateCardPost402Response( + + @Schema(example = "Недостаточно средств для генерации карты", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null, + + @Schema(example = "1000", description = "") + @get:JsonProperty("requiredAmount") val requiredAmount: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNewsGet404Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNewsGet404Response.kt new file mode 100644 index 0000000..799f8e8 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNewsGet404Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class HomeNewsGet404Response( + + @Schema(example = "Новостей пока нет", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNotificationsNotificationIDNavigatePost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNotificationsNotificationIDNavigatePost200Response.kt new file mode 100644 index 0000000..978560b --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNotificationsNotificationIDNavigatePost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param redirectUrl + */ +data class HomeNotificationsNotificationIDNavigatePost200Response( + + @Schema(example = "/trades/123", description = "") + @get:JsonProperty("redirectUrl") val redirectUrl: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNotificationsNotificationIDNavigatePost404Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNotificationsNotificationIDNavigatePost404Response.kt new file mode 100644 index 0000000..c9325ac --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNotificationsNotificationIDNavigatePost404Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class HomeNotificationsNotificationIDNavigatePost404Response( + + @Schema(example = "Данное предложение обмена уже не существует", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsClaimRewardPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsClaimRewardPost200Response.kt new file mode 100644 index 0000000..d778afb --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsClaimRewardPost200Response.kt @@ -0,0 +1,37 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.PackEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param receivedCoins + * @param receivedPacks + * @param newBalance + */ +data class HomeQuestsClaimRewardPost200Response( + + @Schema(example = "500", description = "") + @get:JsonProperty("receivedCoins") val receivedCoins: kotlin.Int? = null, + + @field:Valid + @Schema(description = "") + @get:JsonProperty("receivedPacks") val receivedPacks: kotlin.collections.List? = null, + + @Schema(example = "2000", description = "") + @get:JsonProperty("newBalance") val newBalance: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsClaimRewardPost400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsClaimRewardPost400Response.kt new file mode 100644 index 0000000..08cac8b --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsClaimRewardPost400Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class HomeQuestsClaimRewardPost400Response( + + @Schema(example = "Выполнены не все квесты, необходимые для получения награды", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsGet200Response.kt new file mode 100644 index 0000000..24e4084 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsGet200Response.kt @@ -0,0 +1,34 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.QuestEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param dailyQuests + * @param weeklyQuests + */ +data class HomeQuestsGet200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("dailyQuests") val dailyQuests: kotlin.collections.List? = null, + + @field:Valid + @Schema(description = "") + @get:JsonProperty("weeklyQuests") val weeklyQuests: kotlin.collections.List? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsQuestIDChangeStatusPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsQuestIDChangeStatusPost200Response.kt new file mode 100644 index 0000000..3356b43 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsQuestIDChangeStatusPost200Response.kt @@ -0,0 +1,33 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.QuestEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param quest + * @param message + */ +data class HomeQuestsQuestIDChangeStatusPost200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("quest") val quest: QuestEntity? = null, + + @Schema(example = "Статус квеста успешно изменен", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeSearchGet400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeSearchGet400Response.kt new file mode 100644 index 0000000..1695715 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeSearchGet400Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class HomeSearchGet400Response( + + @Schema(example = "Введите минимум 3 символа для поиска", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeSearchGet404Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeSearchGet404Response.kt new file mode 100644 index 0000000..feb6302 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeSearchGet404Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class HomeSearchGet404Response( + + @Schema(example = "Пользователь с введеными данными не найден", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/NewsResponse.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/NewsResponse.kt new file mode 100644 index 0000000..2e85a19 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/NewsResponse.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.home + +import com.fasterxml.jackson.annotation.JsonProperty +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +data class NewsResponse( + @Schema(description = "ID новости") + @get:JsonProperty("id") + val id: Int, + + @Schema(description = "Заголовок новости") + @get:JsonProperty("title") + val title: String, + + @Schema(description = "Содержание новости") + @get:JsonProperty("content") + val content: String, + + @Schema(description = "Дата создания") + @get:JsonProperty("createdAt") + val createdAt: LocalDateTime, + + @Schema(description = "Дата обновления") + @get:JsonProperty("updatedAt") + val updatedAt: LocalDateTime, + + @Schema(description = "Список изображений") + @get:JsonProperty("pictures") + val pictures: List? = null +) diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDFavoriteGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDFavoriteGet200Response.kt new file mode 100644 index 0000000..9ddd54e --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDFavoriteGet200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param isFavorite + */ +data class InventoryCardCardIDFavoriteGet200Response( + + @Schema(example = "false", description = "") + @get:JsonProperty("isFavorite") val isFavorite: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDQuantityGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDQuantityGet200Response.kt new file mode 100644 index 0000000..a5668e6 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDQuantityGet200Response.kt @@ -0,0 +1,35 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param count + * @param canDisassemble + * @param canTrade + */ +data class InventoryCardCardIDQuantityGet200Response( + + @Schema(example = "3", description = "") + @get:JsonProperty("count") val count: kotlin.Int? = null, + + @Schema(example = "true", description = "") + @get:JsonProperty("canDisassemble") val canDisassemble: kotlin.Boolean? = null, + + @Schema(example = "true", description = "") + @get:JsonProperty("canTrade") val canTrade: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDTradeCancelPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDTradeCancelPost200Response.kt new file mode 100644 index 0000000..02f6de8 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDTradeCancelPost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class InventoryCardCardIDTradeCancelPost200Response( + + @Schema(example = "Карта успешно снята с обмена", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDTradeStatusGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDTradeStatusGet200Response.kt new file mode 100644 index 0000000..ccea041 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDTradeStatusGet200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param isOnTrade Выставлена ли карта на обмен + */ +data class InventoryCardCardIDTradeStatusGet200Response( + + @Schema(example = "true", description = "Выставлена ли карта на обмен") + @get:JsonProperty("isOnTrade") val isOnTrade: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryDestroyPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryDestroyPost200Response.kt new file mode 100644 index 0000000..cd26fc1 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryDestroyPost200Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param newBalance + * @param destroyedCardId + */ +data class InventoryDestroyPost200Response( + + @Schema(example = "2550", description = "") + @get:JsonProperty("newBalance") val newBalance: kotlin.Int? = null, + + @Schema(example = "501", description = "") + @get:JsonProperty("destroyedCardId") val destroyedCardId: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryFavoritesCountGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryFavoritesCountGet200Response.kt new file mode 100644 index 0000000..59eaddd --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryFavoritesCountGet200Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param count + * @param canAddMore + */ +data class InventoryFavoritesCountGet200Response( + + @Schema(example = "3", description = "") + @get:JsonProperty("count") val count: kotlin.Int? = null, + + @Schema(example = "true", description = "") + @get:JsonProperty("canAddMore") val canAddMore: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet200Response.kt new file mode 100644 index 0000000..962c142 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet200Response.kt @@ -0,0 +1,43 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.CardEntity +import ru.vsu.app.model.CollectionEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param inventory + * @param favoriteCards ID избранных карт + * @param collections + * @param balance + */ +data class InventoryGet200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("inventory") val inventory: kotlin.collections.List? = null, + + @Schema(description = "ID избранных карт") + @get:JsonProperty("favoriteCards") val favoriteCards: kotlin.collections.List? = null, + + @field:Valid + @Schema(description = "") + @get:JsonProperty("collections") val collections: kotlin.collections.List? = null, + + @Schema(example = "2500", description = "") + @get:JsonProperty("balance") val balance: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost200Response.kt new file mode 100644 index 0000000..e45c436 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param redirectUrl + */ +data class OtherProfileCardCardIDInitiateTradePost200Response( + + @Schema(example = "/trades/create?requested_card=123&owner=456", description = "") + @get:JsonProperty("redirectUrl") val redirectUrl: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost400Response.kt new file mode 100644 index 0000000..281c564 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost400Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class OtherProfileCardCardIDInitiateTradePost400Response( + + @Schema(example = "Недостаточно карт для обмена", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost409Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost409Response.kt new file mode 100644 index 0000000..c1377a4 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost409Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + * @param cardQuantity Количество экземпляров карты у владельца + */ +data class OtherProfileCardCardIDInitiateTradePost409Response( + + @Schema(example = "Невозможно предложить обмен - у владельца только один экземпляр этой карты", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null, + + @Schema(example = "1", description = "Количество экземпляров карты у владельца") + @get:JsonProperty("cardQuantity") val cardQuantity: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDViewGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDViewGet200Response.kt new file mode 100644 index 0000000..78a5cdf --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDViewGet200Response.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.CardEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param card + */ +data class OtherProfileCardCardIDViewGet200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("card") val card: CardEntity? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDGet200Response.kt new file mode 100644 index 0000000..8ad479c --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDGet200Response.kt @@ -0,0 +1,55 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.AchievementEntity +import ru.vsu.app.model.CardEntity +import ru.vsu.app.model.UserStatsEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param userStats + * @param favoriteCards + * @param favoriteAchievements + * @param inventoryVisible + * @param id + * @param username + */ +data class OtherProfileUserIDGet200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("userStats") val userStats: UserStatsEntity? = null, + + @field:Valid + @get:Size(max=5) + @Schema(description = "") + @get:JsonProperty("favoriteCards") val favoriteCards: kotlin.collections.List? = null, + + @field:Valid + @get:Size(max=4) + @Schema(description = "") + @get:JsonProperty("favoriteAchievements") val favoriteAchievements: kotlin.collections.List? = null, + + @Schema(example = "true", description = "") + @get:JsonProperty("inventoryVisible") val inventoryVisible: kotlin.Boolean? = null, + + @Schema(example = "456", description = "") + @get:JsonProperty("id") val id: kotlin.Int? = null, + + @Schema(example = "Игрок_456", description = "") + @get:JsonProperty("username") val username: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDInventoryGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDInventoryGet200Response.kt new file mode 100644 index 0000000..50dfc71 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDInventoryGet200Response.kt @@ -0,0 +1,35 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.CardEntity +import ru.vsu.app.model.CollectionEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param cards + * @param collections + */ +data class OtherProfileUserIDInventoryGet200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("cards") val cards: kotlin.collections.List? = null, + + @field:Valid + @Schema(description = "") + @get:JsonProperty("collections") val collections: kotlin.collections.List? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDInventoryGet403Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDInventoryGet403Response.kt new file mode 100644 index 0000000..4028971 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDInventoryGet403Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class OtherProfileUserIDInventoryGet403Response( + + @Schema(example = "Пользователь скрыл свой инвентарь", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDReportPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDReportPost200Response.kt new file mode 100644 index 0000000..3e90200 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDReportPost200Response.kt @@ -0,0 +1,33 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.ReportEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param redirectUrl + * @param reportDraft + */ +data class OtherProfileUserIDReportPost200Response( + + @Schema(example = "/report/create?reported_user=123", description = "") + @get:JsonProperty("redirectUrl") val redirectUrl: kotlin.String? = null, + + @field:Valid + @Schema(description = "") + @get:JsonProperty("reportDraft") val reportDraft: ReportEntity? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileAchievementsFavoritesCountGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileAchievementsFavoritesCountGet200Response.kt new file mode 100644 index 0000000..517a7de --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileAchievementsFavoritesCountGet200Response.kt @@ -0,0 +1,32 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param count + * @param canAddMore + */ +data class ProfileAchievementsFavoritesCountGet200Response( + + @get:Max(4) + @Schema(example = "3", description = "") + @get:JsonProperty("count") val count: kotlin.Int? = null, + + @Schema(example = "true", description = "") + @get:JsonProperty("canAddMore") val canAddMore: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileAchievementsGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileAchievementsGet200Response.kt new file mode 100644 index 0000000..8266696 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileAchievementsGet200Response.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.dto.AchievementDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param achievements + */ +data class ProfileAchievementsGet200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("achievements") val achievements: kotlin.collections.List? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileGet200Response.kt new file mode 100644 index 0000000..1b68e42 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileGet200Response.kt @@ -0,0 +1,44 @@ +package ru.vsu.app.dto.responses.profile + +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.AchievementEntity +import ru.vsu.app.model.CardEntity +import ru.vsu.app.model.UserStatsEntity +import jakarta.validation.Valid +import jakarta.validation.constraints.Size +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param userStats + * @param favoriteCards + * @param favoriteAchievements + * @param userID ID пользователя + * @param username Имя пользователя + * @param avatarUrl Ссылка на аватар пользователя + */ +data class ProfileGet200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("userStats") val userStats: UserStatsEntity? = null, + + @field:Valid + @field:Size(max = 5) + @Schema(description = "") + @get:JsonProperty("favoriteCards") val favoriteCards: List? = null, + + @field:Valid + @field:Size(max = 4) + @Schema(description = "") + @get:JsonProperty("favoriteAchievements") val favoriteAchievements: List? = null, + + @Schema(example = "123", description = "ID пользователя") + @get:JsonProperty("user_id") val userID: Int? = null, + + @Schema(example = "Коллекционер_123", description = "Имя пользователя") + @get:JsonProperty("username") val username: String? = null, + + @Schema(example = "https://example.com/avatars/user123.jpg", description = "Ссылка на аватар пользователя") + @get:JsonProperty("avatar_url") val avatarUrl: String? = null +) diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopCoinsOffersOfferIdPurchasePost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopCoinsOffersOfferIdPurchasePost200Response.kt new file mode 100644 index 0000000..805e559 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopCoinsOffersOfferIdPurchasePost200Response.kt @@ -0,0 +1,28 @@ +package ru.vsu.app.dto.responses.shop + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param paymentUrl + */ +data class ShopCoinsOffersOfferIdPurchasePost200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("paymentUrl") val paymentUrl: java.net.URI? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdBuyPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdBuyPost200Response.kt new file mode 100644 index 0000000..0213755 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdBuyPost200Response.kt @@ -0,0 +1,33 @@ +package ru.vsu.app.dto.responses.shop + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.CardEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param receivedCards + * @param newBalance + */ +data class ShopPacksPackIdBuyPost200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("receivedCards") val receivedCards: kotlin.collections.List? = null, + + @Schema(description = "") + @get:JsonProperty("newBalance") val newBalance: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdBuyPost402Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdBuyPost402Response.kt new file mode 100644 index 0000000..db9b552 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdBuyPost402Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.shop + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + * @param requiredAmount + */ +data class ShopPacksPackIdBuyPost402Response( + + @Schema(example = "Недостаточно средств на балансе", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null, + + @Schema(example = "1000", description = "") + @get:JsonProperty("requiredAmount") val requiredAmount: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdOpenPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdOpenPost200Response.kt new file mode 100644 index 0000000..a5774e0 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdOpenPost200Response.kt @@ -0,0 +1,33 @@ +package ru.vsu.app.dto.responses.shop + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.CardEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param receivedCards Карты, полученные из набора + * @param newCardsAdded Были ли карты успешно добавлены в инвентарь + */ +data class ShopPacksPackIdOpenPost200Response( + + @field:Valid + @Schema(description = "Карты, полученные из набора") + @get:JsonProperty("receivedCards") val receivedCards: kotlin.collections.List? = null, + + @Schema(example = "true", description = "Были ли карты успешно добавлены в инвентарь") + @get:JsonProperty("newCardsAdded") val newCardsAdded: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPaymentsProcessPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPaymentsProcessPost200Response.kt new file mode 100644 index 0000000..24db0f3 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPaymentsProcessPost200Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.shop + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param success + * @param transactionId + */ +data class ShopPaymentsProcessPost200Response( + + @Schema(description = "") + @get:JsonProperty("success") val success: kotlin.Boolean? = null, + + @Schema(description = "") + @get:JsonProperty("transactionId") val transactionId: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdAcceptPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdAcceptPost200Response.kt new file mode 100644 index 0000000..e5e33b7 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdAcceptPost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.trades + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class TradesTradeIdAcceptPost200Response( + + @Schema(example = "Обмен успешно завершен", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdAcceptPost403Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdAcceptPost403Response.kt new file mode 100644 index 0000000..9814af0 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdAcceptPost403Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.trades + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class TradesTradeIdAcceptPost403Response( + + @Schema(example = "Вы не можете принять этот обмен", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdCancelPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdCancelPost200Response.kt new file mode 100644 index 0000000..dbcf305 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdCancelPost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.trades + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class TradesTradeIdCancelPost200Response( + + @Schema(example = "Обмен отменен", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdGet404Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdGet404Response.kt new file mode 100644 index 0000000..ddc8b5c --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdGet404Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.trades + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class TradesTradeIdGet404Response( + + @Schema(example = "Предложение обмена не найдено", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdRejectPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdRejectPost200Response.kt new file mode 100644 index 0000000..40e61fe --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdRejectPost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.trades + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class TradesTradeIdRejectPost200Response( + + @Schema(example = "Обмен отклонен", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/AchievementMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/AchievementMapper.kt new file mode 100644 index 0000000..c14d5d3 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/AchievementMapper.kt @@ -0,0 +1,16 @@ +package ru.vsu.app.mapper + +import org.springframework.stereotype.Component +import ru.vsu.app.dto.AchievementDto +import ru.vsu.app.model.AchievementEntity + +@Component +class AchievementMapper { + fun toDto(entity: AchievementEntity): AchievementDto = AchievementDto( + achievementID = entity.achievementID, + name = entity.name, + imageURL = entity.imageURL, + description = entity.description, + isUnlocked = entity.isUnlocked + ) +} diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/CardMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/CardMapper.kt new file mode 100644 index 0000000..d59c55b --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/CardMapper.kt @@ -0,0 +1,19 @@ +package ru.vsu.app.mapper + +import org.springframework.stereotype.Component +import ru.vsu.app.dto.CardDto +import ru.vsu.app.model.CardEntity + +@Component +class CardMapper { + fun toDto(entity: CardEntity): CardDto = CardDto( + cardID = entity.id, + name = entity.name, + imageURL = entity.imageUrl, + rarity = CardDto.Rarity.forValue(entity.rarity.value), + minPrice = entity.disassemblePrice, + isGenerated = entity.isGenerated, + description = entity.description, + theme = entity.theme + ) +} diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/NotificationMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/NotificationMapper.kt new file mode 100644 index 0000000..91d44b7 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/NotificationMapper.kt @@ -0,0 +1,16 @@ +package ru.vsu.app.mapper + +import org.springframework.stereotype.Component +import ru.vsu.app.dto.NotificationDto +import ru.vsu.app.model.NotificationEntity + +@Component +class NotificationMapper { + fun toDto(entity: NotificationEntity): NotificationDto = NotificationDto( + notificationID = entity.notificationID, + userID = entity.user.userId, + message = entity.message, + notificationDateTime = entity.notificationDateTime, + links = entity.links.takeIf { it.isNotEmpty() } + ) +} diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/UserMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/UserMapper.kt new file mode 100644 index 0000000..e0023bc --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/UserMapper.kt @@ -0,0 +1,28 @@ +package ru.vsu.app.mapper + +import org.springframework.stereotype.Component +import ru.vsu.app.dto.* +import ru.vsu.app.model.* + +@Component +class UserMapper( + private val cardMapper: CardMapper, + private val achievementMapper: AchievementMapper, + private val notificationMapper: NotificationMapper +) { + fun toDto(entity: UserEntity): UserDto { + return UserDto( + userID = entity.userId, + username = entity.username, + email = entity.email, + inventoryCards = entity.inventoryCards.map { cardMapper.toDto(it) }, + balance = entity.balance, + achievements = entity.achievements.map { achievementMapper.toDto(it) }, + favoriteCards = if (entity.favoriteCards.isEmpty()) null else entity.favoriteCards.map { cardMapper.toDto(it) }, + onChange = if (entity.onChange.isEmpty()) null else entity.onChange.map { cardMapper.toDto(it) }, + avatarUrl = entity.avatarUrl, + favoriteAchievements = if (entity.favoriteAchievements.isEmpty()) null else entity.favoriteAchievements.map { achievementMapper.toDto(it) }, + notifications = if (entity.notifications.isEmpty()) null else entity.notifications.map { notificationMapper.toDto(it) } + ) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/metrics/AuthMetrics.kt b/backend/src/main/kotlin/ru/vsu/app/metrics/AuthMetrics.kt new file mode 100644 index 0000000..5667d5c --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/metrics/AuthMetrics.kt @@ -0,0 +1,96 @@ +package ru.vsu.app.metrics + +import org.springframework.stereotype.Service + +@Service +class AuthMetrics(private val metrics: MetricsRegistry) { + + fun loginAttempt() { + metrics.count("auth.login.attempt") + } + + fun loginSuccess(userId: String) { + metrics.count("auth.login.success", listOf(MetricTags.user(userId))) + } + + fun loginFailure() { + metrics.count("auth.login.failure") + } + + fun loginDuration(duration: Long, userId: String) { + metrics.timer("auth.login.duration", duration, listOf(MetricTags.user(userId))) + } + + fun registerAttempt() { + metrics.count("auth.register.attempt") + } + + fun registerSuccess() { + metrics.count("auth.register.success") + } + + fun registerConflict() { + metrics.count("auth.register.conflict") // 409 + } + + fun registerValidationError() { + metrics.count("auth.register.validation_error") // 400 + } + + fun registerFailure() { + metrics.count("auth.register.failure") // 500 или default + } + + fun registerDuration(durationMs: Long) { + metrics.timer("auth.register.duration", durationMs) + } + + fun verifyAttempt() { + metrics.count("auth.verify.attempt") + } + + fun verifySuccess(userId: String) { + metrics.count("auth.verify.success", listOf(MetricTags.user(userId))) + } + + fun verifyValidationError() { + metrics.count("auth.verify.validation_error") // 400 + } + + fun verifyExpired() { + metrics.count("auth.verify.expired") // 410 + } + + fun verifyFailure() { + metrics.count("auth.verify.failure") // 500 или default + } + + fun verifyDuration(durationMs: Long, userId: String? = null) { + if (userId != null) { + metrics.timer("auth.verify.duration", durationMs, listOf(MetricTags.user(userId))) + } else { + metrics.timer("auth.verify.duration", durationMs) + } + } + + fun resendVerificationCodeAttempt() { + metrics.count("auth.resend_code.attempt") + } + + fun resendVerificationCodeSuccess() { + metrics.count("auth.resend_code.success") + } + + fun resendVerificationCodeValidationError() { + metrics.count("auth.resend_code.validation_error") // 400 + } + + fun resendVerificationCodeFailure() { + metrics.count("auth.resend_code.failure") // 500 или default + } + + fun resendVerificationCodeDuration(durationMs: Long) { + metrics.timer("auth.resend_code.duration", durationMs) + } + +} diff --git a/backend/src/main/kotlin/ru/vsu/app/metrics/MetricTags.kt b/backend/src/main/kotlin/ru/vsu/app/metrics/MetricTags.kt new file mode 100644 index 0000000..eea52ca --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/metrics/MetricTags.kt @@ -0,0 +1,6 @@ +package ru.vsu.app.metrics + +object MetricTags { + fun user(id: String) = "userId" to id + fun source(value: String) = "source" to value +} diff --git a/backend/src/main/kotlin/ru/vsu/app/metrics/MetricsRegistry.kt b/backend/src/main/kotlin/ru/vsu/app/metrics/MetricsRegistry.kt new file mode 100644 index 0000000..61210fe --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/metrics/MetricsRegistry.kt @@ -0,0 +1,18 @@ +package ru.vsu.app.metrics + +import io.micrometer.core.instrument.MeterRegistry +import org.springframework.stereotype.Component +import java.util.concurrent.TimeUnit + +@Component +class MetricsRegistry(private val registry: MeterRegistry) { + + fun count(name: String, tags: List> = emptyList()) { + registry.counter(name, *tags.toTypedArray().flatMap { listOf(it.first, it.second) }.toTypedArray()).increment() + } + + fun timer(name: String, durationMillis: Long, tags: List> = emptyList()) { + registry.timer(name, *tags.toTypedArray().flatMap { listOf(it.first, it.second) }.toTypedArray()) + .record(durationMillis, TimeUnit.MILLISECONDS) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/model/AchievementEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/AchievementEntity.kt new file mode 100644 index 0000000..2a873fb --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/AchievementEntity.kt @@ -0,0 +1,26 @@ +package ru.vsu.app.model + +import jakarta.persistence.* +import java.util.* + +@Entity +@Table(name = "achievements") +data class AchievementEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "achievement_id") + val achievementID: Int = 0, + + @Column(name = "name", nullable = false) + val name: String, + + @Column(name = "image_url", nullable = false) + val imageURL: String, + + @Column(name = "description", nullable = false) + val description: String, + + @Column(name = "is_unlocked", nullable = false) + val isUnlocked: Boolean = false +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/Card.kt b/backend/src/main/kotlin/ru/vsu/app/model/Card.kt deleted file mode 100644 index 0e0645a..0000000 --- a/backend/src/main/kotlin/ru/vsu/app/model/Card.kt +++ /dev/null @@ -1,36 +0,0 @@ -package ru.vsu.app.model - -import jakarta.persistence.* - -@Entity -@Table(name = "cards") -data class Card( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long = 0, - - @Column(nullable = false) - val name: String, - - @Column(nullable = false) - val imageUrl: String, - - @Column(nullable = false) - val rarity: Int, - - @Column(nullable = false) - val collection: String, - - @Column(nullable = false, length = 1000) - val description: String, - - @Column(nullable = false) - val type: String, - - @Column(nullable = false) - val disassemblePrice: Int = 150, - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - val owner: User? = null -) \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/model/CardEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/CardEntity.kt new file mode 100644 index 0000000..1c603e6 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/CardEntity.kt @@ -0,0 +1,56 @@ +package ru.vsu.app.model + +import jakarta.persistence.* +import ru.vsu.app.model.UserEntity + +@Entity +@Table(name = "cards") +data class CardEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + val id: Int = 0, + + @Column(name = "name", nullable = false) + val name: String, + + @Column(name = "image_url", nullable = false) + val imageUrl: String, + + @Column(name = "rarity", nullable = false) + @Enumerated(EnumType.STRING) + val rarity: Rarity, + + @Column(name = "disassemble_price", nullable = false) + val disassemblePrice: Int, + + @Column(name = "is_generated", nullable = false) + val isGenerated: Boolean, + + @Column(name = "description") + val description: String? = null, + + @Column(name = "theme") + val theme: String? = null, + + @ManyToOne + @JoinColumn(name = "collection_id") + val collection: CollectionEntity? = null, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "owner_id") + val owner: UserEntity? = null, +) { + enum class Rarity(val value: String) { + Обычная("Обычная"), + Редкая("Редкая"), + Эпическая("Эпическая"), + Легендарная("Легендарная"), + Уникальная("Уникальная"); + + companion object { + fun forValue(value: String): Rarity = + values().first { it.value == value } + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/model/CoinOffer.kt b/backend/src/main/kotlin/ru/vsu/app/model/CoinOfferEntity.kt similarity index 94% rename from backend/src/main/kotlin/ru/vsu/app/model/CoinOffer.kt rename to backend/src/main/kotlin/ru/vsu/app/model/CoinOfferEntity.kt index 9b81ff0..59cd881 100644 --- a/backend/src/main/kotlin/ru/vsu/app/model/CoinOffer.kt +++ b/backend/src/main/kotlin/ru/vsu/app/model/CoinOfferEntity.kt @@ -4,7 +4,7 @@ import jakarta.persistence.* @Entity @Table(name = "coin_offers") -data class CoinOffer( +data class CoinOfferEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0, diff --git a/backend/src/main/kotlin/ru/vsu/app/model/CollectionEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/CollectionEntity.kt new file mode 100644 index 0000000..d76e65f --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/CollectionEntity.kt @@ -0,0 +1,22 @@ +package ru.vsu.app.model + +import jakarta.persistence.* + +@Entity +@Table(name = "collections") +data class CollectionEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "collection_id") + val collectionID: Int = 0, + + @Column(nullable = false) + val name: String, + + @OneToMany(mappedBy = "collection", cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true) + val cards: List = emptyList(), + + @Column(name = "image_url", nullable = false) + val imageURL: String +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/NewsEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/NewsEntity.kt new file mode 100644 index 0000000..f418d5e --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/NewsEntity.kt @@ -0,0 +1,16 @@ +package ru.vsu.app.model + +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +data class NewsEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Int? = null, + + var title: String, + var content: String, + val createdAt: LocalDateTime, + var updatedAt: LocalDateTime +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/NotificationEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/NotificationEntity.kt new file mode 100644 index 0000000..8fa073f --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/NotificationEntity.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.model + +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table(name = "notifications") +data class NotificationEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "notification_ID") + val notificationID: Int = 0, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + val user: UserEntity, + + @Column(nullable = false) + val message: String, + + @Column(nullable = false) + val notificationDateTime: LocalDateTime, + + @ElementCollection + @CollectionTable(name = "notification_links", joinColumns = [JoinColumn(name = "notification_ID")]) + @Column(name = "link") + val links: List = emptyList() +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/Pack.kt b/backend/src/main/kotlin/ru/vsu/app/model/PackEntity.kt similarity index 88% rename from backend/src/main/kotlin/ru/vsu/app/model/Pack.kt rename to backend/src/main/kotlin/ru/vsu/app/model/PackEntity.kt index 02a263f..f24f0f2 100644 --- a/backend/src/main/kotlin/ru/vsu/app/model/Pack.kt +++ b/backend/src/main/kotlin/ru/vsu/app/model/PackEntity.kt @@ -4,7 +4,7 @@ import jakarta.persistence.* @Entity @Table(name = "packs") -data class Pack( +data class PackEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0, @@ -24,5 +24,5 @@ data class Pack( joinColumns = [JoinColumn(name = "pack_id")], inverseJoinColumns = [JoinColumn(name = "card_id")] ) - val cards: List = emptyList() + val cards: List = emptyList() ) \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/model/QuestEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/QuestEntity.kt new file mode 100644 index 0000000..d7dc571 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/QuestEntity.kt @@ -0,0 +1,42 @@ +package ru.vsu.app.model + +import jakarta.persistence.* + +@Entity +@Table(name = "quests") +data class QuestEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "quest_id") + val questID: Int = 0, + + @Column(nullable = false) + val name: String, + + @Column(nullable = false) + val description: String, + + @Column(nullable = false) + val progress: Int, + + @Column(nullable = false) + val target: Int, + + @Column(name = "reward_coins", nullable = false) + val rewardCoins: Int, + + @Column(name = "is_completed", nullable = false) + val isCompleted: Boolean, + + @Column(name = "is_claimed", nullable = false) + val isClaimed: Boolean, + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable( + name = "quest_reward_packs", + joinColumns = [JoinColumn(name = "quest_id")], + inverseJoinColumns = [JoinColumn(name = "pack_id")] + ) + val rewardPacks: List? = null +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/ReportEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/ReportEntity.kt new file mode 100644 index 0000000..5391d0f --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/ReportEntity.kt @@ -0,0 +1,37 @@ +package ru.vsu.app.model + +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table(name = "reports") +data class ReportEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reporter_id", nullable = false) + val reporter: UserEntity, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reported_user_id", nullable = false) + val reportedUser: UserEntity, + + @Column(nullable = false) + val reportDateTime: LocalDateTime, + + @Column(nullable = false) + val reason: String, + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + val status: Status +) { + enum class Status { + На_рассмотрении, + Подтверждено, + Отклонено + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/model/User.kt b/backend/src/main/kotlin/ru/vsu/app/model/User.kt deleted file mode 100644 index f010311..0000000 --- a/backend/src/main/kotlin/ru/vsu/app/model/User.kt +++ /dev/null @@ -1,31 +0,0 @@ -package ru.vsu.app.model - -import jakarta.persistence.* - -@Entity -@Table(name = "users") -data class User( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long = 0, - - @Column(unique = true, nullable = false) - val email: String, - - @Column(unique = true, nullable = false) - val username: String, - - @Column(nullable = false) - val passwordHash: String, - - val isEnabled: Boolean = false, - - val activationToken: String? = null, - - val passwordResetToken: String? = null, - - val passwordResetTokenExpiry: Long? = null, - - @Column(nullable = false) - var coins: Int = 1000 -) \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/model/UserEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/UserEntity.kt new file mode 100644 index 0000000..96d6c60 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/UserEntity.kt @@ -0,0 +1,84 @@ +package ru.vsu.app.model + +import jakarta.persistence.* + +@Entity +@Table(name = "users") +data class UserEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") + val userId: Int = 0, + + @Column(unique = true, nullable = false) + val email: String, + + @Column(unique = true, nullable = false) + val username: String, + + @Column(nullable = false) + val passwordHash: String, + + val isEnabled: Boolean = false, + + val activationToken: String? = null, + + val passwordResetToken: String? = null, + + val passwordResetTokenExpiry: Long? = null, + + @Column(nullable = false) + var balance: Int = 1000, + + @Column(nullable = true, name = "avatar_url") + var avatarUrl: String? = null, + + // Пример связи "один ко многим" — пользователь может иметь много карточек в инвентаре + @OneToMany(targetEntity = CardEntity::class, fetch = FetchType.LAZY) + @JoinTable( + name = "user_inventory_cards", + joinColumns = [JoinColumn(name = "user_id")], + inverseJoinColumns = [JoinColumn(name = "card_id")] + ) + var inventoryCards: List = emptyList(), + + @OneToMany(targetEntity = CardEntity::class, fetch = FetchType.LAZY) + @JoinTable( + name = "user_favorite_cards", + joinColumns = [JoinColumn(name = "user_id")], + inverseJoinColumns = [JoinColumn(name = "card_id")] + ) + var favoriteCards: List = emptyList(), + + @OneToMany(targetEntity = CardEntity::class, fetch = FetchType.LAZY) + @JoinTable( + name = "user_onchange_cards", + joinColumns = [JoinColumn(name = "user_id")], + inverseJoinColumns = [JoinColumn(name = "card_id")] + ) + var onChange: List = emptyList(), + + @OneToMany(targetEntity = AchievementEntity::class, fetch = FetchType.LAZY) + @JoinTable( + name = "user_achievements", + joinColumns = [JoinColumn(name = "user_id")], + inverseJoinColumns = [JoinColumn(name = "achievement_id")] + ) + var achievements: List = emptyList(), + + @OneToMany(targetEntity = AchievementEntity::class, fetch = FetchType.LAZY) + @JoinTable( + name = "user_favorite_achievements", + joinColumns = [JoinColumn(name = "user_id")], + inverseJoinColumns = [JoinColumn(name = "achievement_id")] + ) + var favoriteAchievements: List = emptyList(), + + @OneToMany(targetEntity = NotificationEntity::class, fetch = FetchType.LAZY) + @JoinTable( + name = "user_notifications", + joinColumns = [JoinColumn(name = "user_id")], + inverseJoinColumns = [JoinColumn(name = "notification_id")] + ) + var notifications: List = emptyList() +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/UserStatsEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/UserStatsEntity.kt new file mode 100644 index 0000000..738ff55 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/UserStatsEntity.kt @@ -0,0 +1,22 @@ +package ru.vsu.app.model + +import jakarta.persistence.* + +@Entity +@Table(name = "user_stats") +data class UserStatsEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + + @Column(nullable = false) + val totalCards: Int = 0, + + @Column(nullable = false) + val completedCollections: Int = 0, + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false, unique = true) + val user: UserEntity +) diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/AchievementRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/AchievementRepository.kt new file mode 100644 index 0000000..710ad88 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/AchievementRepository.kt @@ -0,0 +1,10 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import ru.vsu.app.model.AchievementEntity + +@Repository +interface AchievementRepository : JpaRepository { + fun findByIsUnlocked(isUnlocked: Boolean): List +} diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/CardRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/CardRepository.kt index 758f8bd..02a201a 100644 --- a/backend/src/main/kotlin/ru/vsu/app/repository/CardRepository.kt +++ b/backend/src/main/kotlin/ru/vsu/app/repository/CardRepository.kt @@ -2,12 +2,13 @@ package ru.vsu.app.repository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository -import ru.vsu.app.model.Card -import ru.vsu.app.model.User +import ru.vsu.app.model.CardEntity +import ru.vsu.app.model.UserEntity @Repository -interface CardRepository : JpaRepository { - fun findAllByOwner(user: User): List - fun findAllByOwnerOrderByRarityDesc(user: User): List - fun findAllByOwnerOrderByCollectionAsc(user: User): List +interface CardRepository : JpaRepository { + fun findById(id: Int): CardEntity? + fun findAllByOwner(user: UserEntity): List + fun findAllByOwnerOrderByRarityDesc(user: UserEntity): List + fun findAllByOwnerOrderByCollectionAsc(user: UserEntity): List } \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/CoinOfferRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/CoinOfferRepository.kt index 35927bb..216dc7f 100644 --- a/backend/src/main/kotlin/ru/vsu/app/repository/CoinOfferRepository.kt +++ b/backend/src/main/kotlin/ru/vsu/app/repository/CoinOfferRepository.kt @@ -2,7 +2,7 @@ package ru.vsu.app.repository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository -import ru.vsu.app.model.CoinOffer +import ru.vsu.app.model.CoinOfferEntity @Repository -interface CoinOfferRepository : JpaRepository \ No newline at end of file +interface CoinOfferRepository : JpaRepository \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/CollectionRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/CollectionRepository.kt new file mode 100644 index 0000000..f136508 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/CollectionRepository.kt @@ -0,0 +1,10 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import ru.vsu.app.model.CollectionEntity + +@Repository +interface CollectionRepository : JpaRepository { + fun findByNameContainingIgnoreCase(name: String): List +} diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/NewsRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/NewsRepository.kt new file mode 100644 index 0000000..79ca468 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/NewsRepository.kt @@ -0,0 +1,8 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import ru.vsu.app.model.NewsEntity + +@Repository +interface NewsRepository : JpaRepository diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/NotificationRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/NotificationRepository.kt new file mode 100644 index 0000000..9cc2810 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/NotificationRepository.kt @@ -0,0 +1,10 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import ru.vsu.app.model.NotificationEntity + +@Repository +interface NotificationRepository : JpaRepository { + fun findByUserUserId(userId: Int): List +} diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/PackRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/PackRepository.kt index e93b266..f810570 100644 --- a/backend/src/main/kotlin/ru/vsu/app/repository/PackRepository.kt +++ b/backend/src/main/kotlin/ru/vsu/app/repository/PackRepository.kt @@ -2,7 +2,7 @@ package ru.vsu.app.repository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository -import ru.vsu.app.model.Pack +import ru.vsu.app.model.PackEntity @Repository -interface PackRepository : JpaRepository \ No newline at end of file +interface PackRepository : JpaRepository \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/QuestRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/QuestRepository.kt new file mode 100644 index 0000000..3554014 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/QuestRepository.kt @@ -0,0 +1,11 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import ru.vsu.app.model.QuestEntity + +@Repository +interface QuestRepository : JpaRepository { + fun findByIsCompleted(isCompleted: Boolean): List + fun findByIsClaimed(isClaimed: Boolean): List +} diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/ReportRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/ReportRepository.kt new file mode 100644 index 0000000..296e18f --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/ReportRepository.kt @@ -0,0 +1,10 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import ru.vsu.app.model.ReportEntity + +@Repository +interface ReportRepository : JpaRepository { + fun findByStatus(status: ReportEntity.Status): List +} diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/UserRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/UserRepository.kt index 3c4b0e8..06336f8 100644 --- a/backend/src/main/kotlin/ru/vsu/app/repository/UserRepository.kt +++ b/backend/src/main/kotlin/ru/vsu/app/repository/UserRepository.kt @@ -2,15 +2,30 @@ package ru.vsu.app.repository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository -import ru.vsu.app.model.User +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import ru.vsu.app.model.UserEntity import java.util.Optional @Repository -interface UserRepository : JpaRepository { - fun findByEmail(email: String): Optional - fun findByUsername(username: String): Optional - fun findByActivationToken(activationToken: String): Optional - fun findByPasswordResetToken(passwordResetToken: String): Optional +interface UserRepository : JpaRepository { + fun findByEmail(email: String): Optional + fun findByUsername(username: String): Optional + fun findByActivationToken(activationToken: String): Optional + fun findByPasswordResetToken(passwordResetToken: String): Optional fun existsByEmail(email: String): Boolean fun existsByUsername(username: String): Boolean + + @Query(""" + SELECT u FROM UserEntity u + LEFT JOIN FETCH u.inventoryCards + LEFT JOIN FETCH u.favoriteCards + LEFT JOIN FETCH u.onChange + LEFT JOIN FETCH u.achievements + LEFT JOIN FETCH u.favoriteAchievements + LEFT JOIN FETCH u.notifications + WHERE u.userId = :userId + """) + fun findByIdWithRelations(@Param("userId") userId: Int): UserEntity? + } \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/UserStatsRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/UserStatsRepository.kt new file mode 100644 index 0000000..3a97b2b --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/UserStatsRepository.kt @@ -0,0 +1,11 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import ru.vsu.app.model.UserStatsEntity +import ru.vsu.app.model.UserEntity + +@Repository +interface UserStatsRepository : JpaRepository { + fun findByUser(user: UserEntity): UserStatsEntity? +} diff --git a/backend/src/main/kotlin/ru/vsu/app/security/JwtAuthenticationFilter.kt b/backend/src/main/kotlin/ru/vsu/app/security/JwtAuthenticationFilter.kt index f789811..ed59132 100644 --- a/backend/src/main/kotlin/ru/vsu/app/security/JwtAuthenticationFilter.kt +++ b/backend/src/main/kotlin/ru/vsu/app/security/JwtAuthenticationFilter.kt @@ -11,63 +11,56 @@ import org.springframework.stereotype.Component import org.springframework.util.AntPathMatcher import org.springframework.web.filter.OncePerRequestFilter import ru.vsu.app.service.JwtService +import ru.vsu.app.config.PublicEndpointsConfig @Component class JwtAuthenticationFilter( private val jwtService: JwtService, - private val userDetailsService: UserDetailsService + private val userDetailsService: UserDetailsService, + private val publicEndpointsConfig: PublicEndpointsConfig ) : OncePerRequestFilter() { private val pathMatcher = AntPathMatcher() - - private val publicUrls = listOf( - "/api/auth/register", - "/api/auth/login", - "/api/auth/forgot-password", - "/api/auth/activate", - "/v3/api-docs/**", - "/swagger-ui/**", - "/swagger-ui.html", - "/webjars/**" - ) - - override fun shouldNotFilter(request: HttpServletRequest): Boolean { - return publicUrls.any { pattern -> - pathMatcher.match(pattern, request.servletPath) - } - } override fun doFilterInternal( request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain ) { + val servletPath = request.servletPath + + // Public - не обрабатываем токен, просто идем дальше + if (publicEndpointsConfig.endpoints.any { pattern -> pathMatcher.match(pattern, servletPath) }) { + filterChain.doFilter(request, response) + return + } + val authHeader = request.getHeader("Authorization") - if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response) + println("Without authorization") return } - + val jwt = authHeader.substring(7) val userEmail = jwtService.extractUsername(jwt) - + if (SecurityContextHolder.getContext().authentication == null) { + println("Authentication = null") val userDetails = userDetailsService.loadUserByUsername(userEmail) - + if (jwtService.isTokenValid(jwt, userDetails)) { val authToken = UsernamePasswordAuthenticationToken( userDetails, null, userDetails.authorities ) - + authToken.details = WebAuthenticationDetailsSource().buildDetails(request) - SecurityContextHolder.getContext().authentication = authToken } } - + filterChain.doFilter(request, response) } -} \ No newline at end of file +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/AuthService.kt b/backend/src/main/kotlin/ru/vsu/app/service/AuthService.kt new file mode 100644 index 0000000..faa521b --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/AuthService.kt @@ -0,0 +1,165 @@ +package ru.vsu.app.service + +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service +import org.springframework.security.core.context.SecurityContextHolder +import ru.vsu.app.dto.requests.RegisterUserRequest +import ru.vsu.app.dto.requests.ResendVerificationCodeRequest +import ru.vsu.app.dto.responses.auth.register.RegisterUser200Response +import ru.vsu.app.dto.responses.auth.register.RegisterUser400Response +import ru.vsu.app.dto.responses.auth.register.RegisterUser409Response +import ru.vsu.app.dto.responses.auth.verifyemail.VerifyEmail400Response +import ru.vsu.app.dto.responses.auth.verifyemail.VerifyEmail410Response +import ru.vsu.app.dto.responses.auth.verifyemail.ResendVerificationCode200Response +import ru.vsu.app.dto.responses.common.InternalServerError +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import ru.vsu.app.dto.requests.VerifyEmailRequest +import ru.vsu.app.dto.UserDto +import ru.vsu.app.dto.CardDto +import ru.vsu.app.dto.AchievementDto +import ru.vsu.app.model.UserEntity +import ru.vsu.app.repository.UserRepository +import ru.vsu.app.service.JwtService +import ru.vsu.app.service.EmailService +import ru.vsu.app.mapper.UserMapper +import kotlin.random.Random + +@Service +class AuthService( + private val userRepository: UserRepository, + private val userMapper: UserMapper, + private val jwtService: JwtService, + private val emailService: EmailService, + private val passwordEncoder: PasswordEncoder +) { + + fun getCurrentUser(): UserDto? { + println(SecurityContextHolder.getContext()) + val authentication = SecurityContextHolder.getContext().authentication + + if (authentication == null || !authentication.isAuthenticated || authentication.principal == "anonymousUser") { + println("111111111111") + return null + } + + val email = authentication.name + + println(email) + + val user = userRepository.findByEmail(email).orElse(null)?: return null + + return userMapper.toDto(user) + } + + fun register(request: RegisterUserRequest): Any { + val (email, username, password) = request + + if (userRepository.existsByEmail(email)) { + return RegisterUser409Response("Пользователь с таким email уже существует") + } + + if (userRepository.existsByUsername(username)) { + return RegisterUser409Response("Пользователь с таким username уже существует") + } + + val activationCode = generateSixDigitCode() + + try { + emailService.sendActivationEmail(email, activationCode) + } catch (ex: Exception) { + ex.printStackTrace() + return RegisterUser400Response("Ошибка при отправке email. Проверьте корректность адреса") + } + + val newUser = UserEntity( + email = email, + username = username, + passwordHash = passwordEncoder.encode(password), + activationToken = activationCode + ) + + userRepository.save(newUser) + + return RegisterUser200Response( + message = "Пользователь успешно зарегистрирован. Проверьте email для получения кода активации.", + tempToken = jwtService.generateToken(email) + ) + } + + // Функция генерации 6-значного кода + private fun generateSixDigitCode(): String { + return (100000 + Random.nextInt(900000)).toString() + } + + fun verifyEmail(request: VerifyEmailRequest): Any { + val token = request.tempToken + + if (jwtService.isTokenExpired(token)) { + return VerifyEmail410Response("Срок действия токена истёк") + } + + val email = jwtService.extractUsername(token) + val user = userRepository.findByEmail(email).orElse(null) + ?: return VerifyEmail400Response("Пользователь не найден") + + if (user.isEnabled) { + return VerifyEmail400Response("Аккаунт уже подтвержден") + } + + val storedCode = user.activationToken + ?: return VerifyEmail410Response("Срок действия кода истёк или он уже использован") + + + if (storedCode != request.code) { + return VerifyEmail400Response("Неверный код") + } + + // всё валидно - активируем аккаунт + val updatedUser = user.copy( + isEnabled = true, + activationToken = null + ) + + userRepository.save(updatedUser) + + return userMapper.toDto(updatedUser) + } + + fun resendVerificationCode(request: ResendVerificationCodeRequest): Any { + val token = request.tempToken + + val email = jwtService.extractUsernameIgnoreExpiration(token) + ?: return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid token") + + val user = userRepository.findByEmail(email).orElse(null) + ?: return ResponseEntity.status(HttpStatus.BAD_REQUEST) + + if (user.isEnabled) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + } + + val newActivationCode = generateSixDigitCode() + + try { + emailService.sendActivationEmail(email, newActivationCode) + } catch (ex: Exception) { + ex.printStackTrace() + return InternalServerError(error = "Ошибка при отправке email. Проверьте корректность адреса") + } + + val updatedUser = user.copy( + activationToken = newActivationCode + ) + + userRepository.save(updatedUser) + + val newTempToken = jwtService.generateToken(email) + + return ResendVerificationCode200Response( + message = "Новый код подтверждения отправлен", + tempToken = newTempToken + ) + } + +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/CardService.kt b/backend/src/main/kotlin/ru/vsu/app/service/CardService.kt deleted file mode 100644 index a45597b..0000000 --- a/backend/src/main/kotlin/ru/vsu/app/service/CardService.kt +++ /dev/null @@ -1,69 +0,0 @@ -package ru.vsu.app.service - -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import ru.vsu.app.dto.CardResponse -import ru.vsu.app.model.Card -import ru.vsu.app.model.User -import ru.vsu.app.repository.CardRepository -import ru.vsu.app.repository.UserRepository -import jakarta.persistence.EntityNotFoundException - -@Service -class CardService( - private val cardRepository: CardRepository, - private val userRepository: UserRepository -) { - fun getUserCards(user: User, sortBy: String = "rarity"): List { - val cards = when (sortBy) { - "rarity" -> cardRepository.findAllByOwnerOrderByRarityDesc(user) - "collection" -> cardRepository.findAllByOwnerOrderByCollectionAsc(user) - else -> cardRepository.findAllByOwner(user) - } - - return cards.map { card -> mapToCardResponse(card) } - } - - fun getCardDetails(cardId: Long, user: User): CardResponse { - val card = cardRepository.findById(cardId) - .orElseThrow { EntityNotFoundException("Card not found with id: $cardId") } - - if (card.owner?.id != user.id) { - throw IllegalStateException("User does not own this card") - } - - return mapToCardResponse(card) - } - - @Transactional - fun disassembleCard(cardId: Long, user: User): Int { - val card = cardRepository.findById(cardId) - .orElseThrow { EntityNotFoundException("Card not found with id: $cardId") } - - if (card.owner?.id != user.id) { - throw IllegalStateException("User does not own this card") - } - - // Add coins to user's balance - val updatedUser = user.copy(coins = user.coins + card.disassemblePrice) - userRepository.save(updatedUser) - - // Delete the card - cardRepository.delete(card) - - return card.disassemblePrice - } - - private fun mapToCardResponse(card: Card): CardResponse { - return CardResponse( - id = card.id, - name = card.name, - imageUrl = card.imageUrl, - rarity = card.rarity, - collection = card.collection, - description = card.description, - type = card.type, - disassemblePrice = card.disassemblePrice - ) - } -} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/service/EmailService.kt b/backend/src/main/kotlin/ru/vsu/app/service/EmailService.kt index f9700c3..095daa9 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/EmailService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/EmailService.kt @@ -3,13 +3,18 @@ package ru.vsu.app.service import org.springframework.mail.SimpleMailMessage import org.springframework.mail.javamail.JavaMailSender import org.springframework.stereotype.Service +import org.springframework.beans.factory.annotation.Value @Service -class EmailService(private val mailSender: JavaMailSender) { +class EmailService( + private val mailSender: JavaMailSender, + @Value("\${EMAIL_USERNAME}") + private val fromEmail: String) { fun sendActivationEmail(to: String, code: String) { val message = SimpleMailMessage() message.setTo(to) + message.setFrom(fromEmail) message.setSubject("Подтверждение регистрации") message.setText(""" Спасибо за регистрацию! @@ -26,6 +31,7 @@ class EmailService(private val mailSender: JavaMailSender) { fun sendPasswordResetEmail(to: String, code: String) { val message = SimpleMailMessage() message.setTo(to) + message.setFrom(fromEmail) message.setSubject("Сброс пароля") message.setText(""" Вы запросили сброс пароля. diff --git a/backend/src/main/kotlin/ru/vsu/app/service/InventoryService.kt b/backend/src/main/kotlin/ru/vsu/app/service/InventoryService.kt new file mode 100644 index 0000000..3cdfb30 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/InventoryService.kt @@ -0,0 +1,83 @@ +// package ru.vsu.app.service + +// import org.springframework.stereotype.Service +// import org.springframework.transaction.annotation.Transactional +// import ru.vsu.app.dto.responses.CardResponse +// import ru.vsu.app.dto.responses.InventoryGet200Response +// import ru.vsu.app.dto.responses.InventorySortPost200Response +// import ru.vsu.app.dto.requests.OtherProfileInventorySortPostRequest +// import ru.vsu.app.model.CardEntity +// import ru.vsu.app.model.UserEntity +// import ru.vsu.app.repository.InventoryRepository +// import ru.vsu.app.repository.UserRepository +// import jakarta.persistence.EntityNotFoundException + +// @Service +// class InventoryService( +// private val InventoryRepository: InventoryRepository, +// private val userRepository: UserRepository +// ) { +// fun getUserCards(user: UserEntity, sortBy: String = "rarity"): List { +// val cards = when (sortBy) { +// "rarity" -> InventoryRepository.findAllByOwnerOrderByRarityDesc(user) +// "collection" -> InventoryRepository.findAllByOwnerOrderByCollectionAsc(user) +// else -> InventoryRepository.findAllByOwner(user) +// } + +// return cards.map { card -> mapToCardResponse(card) } +// } + +// fun getCardDetails(cardId: Long, user: UserEntity): CardResponse { +// val card = InventoryRepository.findById(cardId) +// .orElseThrow { EntityNotFoundException("Card not found with id: $cardId") } + +// if (card.owner?.id != user.id) { +// throw IllegalStateException("User does not own this card") +// } + +// return mapToCardResponse(card) +// } + +// @Transactional +// fun disassembleCard(cardId: Long, user: UserEntity): Int { +// val card = InventoryRepository.findById(cardId) +// .orElseThrow { EntityNotFoundException("Card not found with id: $cardId") } + +// if (card.owner?.id != user.id) { +// throw IllegalStateException("User does not own this card") +// } + +// // Add coins to user's balance +// val updatedUser = user.copy(coins = user.coins + card.disassemblePrice) +// userRepository.save(updatedUser) + +// // Delete the card +// InventoryRepository.delete(card) + +// return card.disassemblePrice +// } + + +// fun getInventoryData(user: UserEntity): InventoryGet200Response { +// val allCards = InventoryRepository.findAllByOwner(user) +// val favoriteCards = allCards.filter { it.isFavorite == true } +// return InventoryGet200Response( +// cards = allCards.map { mapToCardResponse(it) }, +// favorites = favoriteCards.map { mapToCardResponse(it) }, +// balance = user.coins +// ) +// } + +// private fun mapToCardResponse(card: CardEntity): CardResponse { +// return CardResponse( +// id = card.id, +// name = card.name, +// imageUrl = card.imageUrl, +// rarity = card.rarity, +// collection = card.collection, +// description = card.description, +// type = card.type, +// disassemblePrice = card.disassemblePrice +// ) +// } +// } \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/service/JwtService.kt b/backend/src/main/kotlin/ru/vsu/app/service/JwtService.kt index 2d5ae9f..5fd7127 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/JwtService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/JwtService.kt @@ -4,6 +4,7 @@ import io.jsonwebtoken.Claims import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.ExpiredJwtException import io.jsonwebtoken.security.Keys import org.springframework.beans.factory.annotation.Value import org.springframework.security.core.userdetails.UserDetails @@ -24,6 +25,16 @@ class JwtService { return extractClaim(token) { obj: Claims -> obj.subject } } + fun extractUsernameIgnoreExpiration(token: String): String? { + return try { + extractClaim(token) { it.subject } + } catch (ex: ExpiredJwtException) { + ex.claims.subject // достаём email из протухшего токена + } catch (ex: Exception) { + null + } + } + fun extractClaim(token: String, claimsResolver: (Claims) -> T): T { val claims = extractAllClaims(token) return claimsResolver(claims) @@ -49,7 +60,7 @@ class JwtService { return username == userDetails.username && !isTokenExpired(token) } - private fun isTokenExpired(token: String): Boolean { + fun isTokenExpired(token: String): Boolean { return extractExpiration(token).before(Date()) } diff --git a/backend/src/main/kotlin/ru/vsu/app/service/NewsService.kt b/backend/src/main/kotlin/ru/vsu/app/service/NewsService.kt new file mode 100644 index 0000000..3b99e21 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/NewsService.kt @@ -0,0 +1,49 @@ +package ru.vsu.app.service + +import org.springframework.stereotype.Service +import ru.vsu.app.repository.NewsRepository +import ru.vsu.app.dto.requests.NewsRequest +import ru.vsu.app.dto.responses.home.NewsResponse +import ru.vsu.app.model.NewsEntity +import java.time.LocalDateTime + +@Service +class NewsService( + private var newsRepository: NewsRepository +) { + + fun createNews(request: NewsRequest): NewsResponse { + var news = NewsEntity( + title = request.title, + content = request.content, + createdAt = LocalDateTime.now(), + updatedAt = LocalDateTime.now() + ) + return toResponse(newsRepository.save(news)) + } + + fun updateNews(id: Int, request: NewsRequest): NewsResponse { + var news = newsRepository.findById(id) + .orElseThrow { RuntimeException("Новость не найдена") } + + news.title = request.title + news.content = request.content + news.updatedAt = LocalDateTime.now() + + return toResponse(newsRepository.save(news)) + } + + fun deleteNews(id: Int) { + newsRepository.deleteById(id) + } + + private fun toResponse(news: NewsEntity): NewsResponse { + return NewsResponse( + id = news.id!!, + title = news.title, + content = news.content, + createdAt = news.createdAt, + updatedAt = news.updatedAt + ) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/ShopService.kt b/backend/src/main/kotlin/ru/vsu/app/service/ShopService.kt index 7c37b5d..e461cae 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/ShopService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/ShopService.kt @@ -1,154 +1,156 @@ -package ru.vsu.app.service - -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import ru.vsu.app.dto.* -import ru.vsu.app.model.User -import ru.vsu.app.repository.PackRepository -import ru.vsu.app.repository.CoinOfferRepository -import ru.vsu.app.repository.UserRepository -import ru.vsu.app.repository.CardRepository -import jakarta.persistence.EntityNotFoundException - -@Service -class ShopService( - private val packRepository: PackRepository, - private val coinOfferRepository: CoinOfferRepository, - private val userRepository: UserRepository, - private val cardRepository: CardRepository -) { - fun getAllPacks(): List { - return packRepository.findAll().map { pack -> - PackResponse( - id = pack.id, - name = pack.name, - imageUrl = pack.imageUrl, - price = pack.price, - cards = pack.cards.map { card -> - CardResponse( - id = card.id, - name = card.name, - imageUrl = card.imageUrl, - rarity = card.rarity, - collection = card.collection, - description = card.description, - type = card.type, - disassemblePrice = card.disassemblePrice - ) - } - ) - } - } - - fun getPackDetails(packId: Long): PackResponse { - val pack = packRepository.findById(packId) - .orElseThrow { EntityNotFoundException("Pack not found with id: $packId") } - - return PackResponse( - id = pack.id, - name = pack.name, - imageUrl = pack.imageUrl, - price = pack.price, - cards = pack.cards.map { card -> - CardResponse( - id = card.id, - name = card.name, - imageUrl = card.imageUrl, - rarity = card.rarity, - collection = card.collection, - description = card.description, - type = card.type, - disassemblePrice = card.disassemblePrice - ) - } - ) - } - - @Transactional - fun buyPack(packId: Long, user: User): PurchasePackResponse { - val pack = packRepository.findById(packId) - .orElseThrow { EntityNotFoundException("Pack not found with id: $packId") } - - if (user.coins < pack.price) { - throw IllegalStateException("Insufficient funds") - } - - // Update user's balance - val updatedUser = user.copy(coins = user.coins - pack.price) - userRepository.save(updatedUser) - - // Add cards to user's inventory - val receivedCards = pack.cards.map { card -> - val userCard = card.copy(owner = updatedUser) - cardRepository.save(userCard) - } - - return PurchasePackResponse( - receivedCards = receivedCards.map { card -> - CardResponse( - id = card.id, - name = card.name, - imageUrl = card.imageUrl, - rarity = card.rarity, - collection = card.collection, - description = card.description, - type = card.type, - disassemblePrice = card.disassemblePrice - ) - }, - newBalance = updatedUser.coins - ) - } - - fun getAllCoinOffers(): List { - return coinOfferRepository.findAll().map { offer -> - CoinOfferResponse( - id = offer.id, - name = offer.name, - coinsAmount = offer.coinsAmount, - price = offer.price, - imageUrl = offer.imageUrl, - description = offer.description - ) - } - } - - fun getCoinOfferDetails(offerId: Long): CoinOfferResponse { - val offer = coinOfferRepository.findById(offerId) - .orElseThrow { EntityNotFoundException("Coin offer not found with id: $offerId") } - - return CoinOfferResponse( - id = offer.id, - name = offer.name, - coinsAmount = offer.coinsAmount, - price = offer.price, - imageUrl = offer.imageUrl, - description = offer.description - ) - } - - fun purchaseCoins(offerId: Long, redirectUrl: String): PurchaseCoinsResponse { - val offer = coinOfferRepository.findById(offerId) - .orElseThrow { EntityNotFoundException("Coin offer not found with id: $offerId") } - - // Here you would integrate with your payment gateway - // This is a simplified example - return PurchaseCoinsResponse( - paymentUrl = "$redirectUrl?offer_id=$offerId" - ) - } - - @Transactional - fun processPayment(request: PaymentCallbackRequest): Boolean { - if (request.status != "Успешно") { - return false - } - - val offer = coinOfferRepository.findById(request.offerId) - .orElseThrow { EntityNotFoundException("Coin offer not found with id: ${request.offerId}") } - - // Here you would validate the payment with your payment gateway - // and update the user's balance accordingly - return true - } -} \ No newline at end of file +// package ru.vsu.app.service + +// import org.springframework.stereotype.Service +// import org.springframework.transaction.annotation.Transactional +// import ru.vsu.app.dto.responses.* +// import ru.vsu.app.model.UserEntity +// import ru.vsu.app.model.CardEntity +// import ru.vsu.app.model.PackEntity +// import ru.vsu.app.model.CoinOfferEntity +// import ru.vsu.app.repository.PackRepository +// import ru.vsu.app.repository.CoinOfferRepository +// import ru.vsu.app.repository.UserRepository +// import ru.vsu.app.repository.InventoryRepository +// import jakarta.persistence.EntityNotFoundException + +// @Service +// class ShopService( +// private val packRepository: PackRepository, +// private val coinOfferRepository: CoinOfferRepository, +// private val userRepository: UserRepository, +// private val InventoryRepository: InventoryRepository +// ) { +// fun getAllPacks(): List { +// return packRepository.findAll().map { pack: PackEntity -> +// PackResponse( +// id = pack.id, +// name = pack.name, +// imageUrl = pack.imageUrl, +// price = pack.price, +// cards = pack.cards.map { card: CardEntity -> +// CardResponse( +// id = card.id, +// name = card.name, +// imageUrl = card.imageUrl, +// rarity = card.rarity, +// collection = card.collection, +// description = card.description, +// disassemblePrice = card.disassemblePrice +// ) +// } +// ) +// } +// } + +// fun getPackDetails(packId: Long): PackResponse { +// val pack = packRepository.findById(packId) +// .orElseThrow { EntityNotFoundException("Pack not found with id: $packId") } + +// return PackResponse( +// id = pack.id, +// name = pack.name, +// imageUrl = pack.imageUrl, +// price = pack.price, +// cards = pack.cards.map { card: CardEntity -> +// CardResponse( +// id = card.id, +// name = card.name, +// imageUrl = card.imageUrl, +// rarity = card.rarity, +// collection = card.collection, +// description = card.description, +// type = card.type, +// disassemblePrice = card.disassemblePrice +// ) +// } +// ) +// } + +// @Transactional +// fun buyPack(packId: Long, user: UserEntity): PurchasePackResponse { +// val pack = packRepository.findById(packId) +// .orElseThrow { EntityNotFoundException("Pack not found with id: $packId") } + +// if (user.coins < pack.price) { +// throw IllegalStateException("Insufficient funds") +// } + +// // Update user's balance +// val updatedUser = user.copy(coins = user.coins - pack.price) +// userRepository.save(updatedUser) + +// // Add cards to user's inventory +// val receivedCards = pack.cards.map { card: CardEntity -> +// val userCard = card.copy(owner = updatedUser) +// InventoryRepository.save(userCard) +// } + +// return PurchasePackResponse( +// receivedCards = receivedCards.map { card: CardEntity -> +// CardResponse( +// id = card.id, +// name = card.name, +// imageUrl = card.imageUrl, +// rarity = card.rarity, +// collection = card.collection, +// description = card.description, +// type = card.type, +// disassemblePrice = card.disassemblePrice +// ) +// }, +// newBalance = updatedUser.coins +// ) +// } + +// fun getAllCoinOffers(): List { +// return coinOfferRepository.findAll().map { offer: CoinOfferEntity -> +// CoinOfferResponse( +// id = offer.id, +// name = offer.name, +// coinsAmount = offer.coinsAmount, +// price = offer.price, +// imageUrl = offer.imageUrl, +// description = offer.description +// ) +// } +// } + +// fun getCoinOfferDetails(offerId: Long): CoinOfferResponse { +// val offer = coinOfferRepository.findById(offerId) +// .orElseThrow { EntityNotFoundException("Coin offer not found with id: $offerId") } + +// return CoinOfferResponse( +// id = offer.id, +// name = offer.name, +// coinsAmount = offer.coinsAmount, +// price = offer.price, +// imageUrl = offer.imageUrl, +// description = offer.description +// ) +// } + +// fun purchaseCoins(offerId: Long, redirectUrl: String): PurchaseCoinsResponse { +// coinOfferRepository.findById(offerId) +// .orElseThrow { EntityNotFoundException("Coin offer not found with id: $offerId") } + +// // Here you would integrate with your payment gateway +// // This is a simplified example +// return PurchaseCoinsResponse( +// paymentUrl = "$redirectUrl?offer_id=$offerId" +// ) +// } + +// @Transactional +// fun processPayment(request: PaymentCallbackRequest): Boolean { +// if (request.status != "Успешно") { +// return false +// } + +// coinOfferRepository.findById(request.offerId) +// .orElseThrow { EntityNotFoundException("Coin offer not found with id: ${request.offerId}") } + +// // Here you would validate the payment with your payment gateway +// // and update the user's balance accordingly +// return true +// } +// } \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/service/UserService.kt b/backend/src/main/kotlin/ru/vsu/app/service/UserService.kt index 92d4c35..07dd75f 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/UserService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/UserService.kt @@ -1,184 +1,193 @@ -package ru.vsu.app.service +// package ru.vsu.app.service -import org.springframework.security.crypto.password.PasswordEncoder -import org.springframework.stereotype.Service -import ru.vsu.app.dto.* -import ru.vsu.app.model.User -import ru.vsu.app.repository.UserRepository -import kotlin.random.Random +// import org.springframework.security.crypto.password.PasswordEncoder +// import org.springframework.stereotype.Service +// import ru.vsu.app.dto.responses.RegisterResponse +// import ru.vsu.app.dto.responses.ApiResponse +// import ru.vsu.app.dto.responses.LoginResponse +// import ru.vsu.app.dto.responses.UserInfoResponse +// import ru.vsu.app.dto.requests.RegisterUserRequest +// import ru.vsu.app.dto.requests.ActivateAccountRequest +// import ru.vsu.app.dto.requests.LoginRequest +// import ru.vsu.app.dto.requests.ForgotPasswordRequest +// import ru.vsu.app.dto.requests.ResetPasswordRequest +// import ru.vsu.app.model.UserEntity +// import ru.vsu.app.repository.UserRepository +// import ru.vsu.app.service.JwtService +// import kotlin.random.Random -@Service -class UserService( - private val userRepository: UserRepository, - private val passwordEncoder: PasswordEncoder, - private val emailService: EmailService, - private val jwtService: JwtService -) { +// @Service +// class UserService( +// private val userRepository: UserRepository, +// private val passwordEncoder: PasswordEncoder, +// private val emailService: EmailService, +// private val jwtService: JwtService +// ) { - fun register(request: RegisterRequest): RegisterResponse { - // Проверяем, что пользователь с таким email или username еще не существует - if (userRepository.existsByEmail(request.email)) { - return RegisterResponse("Пользователь с таким email уже существует", false) - } - - if (userRepository.existsByUsername(request.username)) { - return RegisterResponse("Пользователь с таким username уже существует", false) - } - - // Генерируем 6-значный код активации - val activationCode = generateSixDigitCode() - - // Создаем пользователя - val user = User( - email = request.email, - username = request.username, - passwordHash = passwordEncoder.encode(request.password), - isEnabled = false, - activationToken = activationCode - ) - - userRepository.save(user) - - // Отправляем email с кодом подтверждения - emailService.sendActivationEmail(user.email, activationCode) - - return RegisterResponse("Пользователь успешно зарегистрирован. Проверьте email для получения кода активации.", true) - } +// fun register(request: RegisterUserRequest): RegisterResponse { +// // Проверяем, что пользователь с таким email или username еще не существует +// if (userRepository.existsByEmail(request.email)) { +// return RegisterUser409Response("Пользователь с таким email уже существует") +// } + +// if (userRepository.existsByUsername(request.username)) { +// return RegisterUser409Response("Пользователь с таким username уже существует") +// } + +// // Генерируем 6-значный код активации +// val activationCode = generateSixDigitCode() + +// // Создаем пользователя +// val user = UserEntity( +// email = request.email, +// username = request.username, +// passwordHash = passwordEncoder.encode(request.password), +// isEnabled = false, +// activationToken = activationCode +// ) + +// userRepository.save(user) + +// // Отправляем email с кодом подтверждения +// emailService.sendActivationEmail(user.email, activationCode) + +// return RegisterResponse("Пользователь успешно зарегистрирован. Проверьте email для получения кода активации", true) // 200 +// } - fun activateAccount(request: ActivateAccountRequest): ApiResponse { - val userOptional = userRepository.findByEmail(request.email) +// fun activateAccount(request: ActivateAccountRequest): ApiResponse { +// val userOptional = userRepository.findByEmail(request.email) - if (userOptional.isEmpty) { - return ApiResponse("Пользователь с таким email не найден", false) - } +// if (userOptional.isEmpty) { +// return ApiResponse("Пользователь с таким email не найден", false) +// } - val user = userOptional.get() +// val user = userOptional.get() - // Если аккаунт уже активирован - if (user.isEnabled) { - return ApiResponse("Аккаунт уже активирован", true) - } +// // Если аккаунт уже активирован +// if (user.isEnabled) { +// return ApiResponse("Аккаунт уже активирован", true) +// } - // Проверяем код активации - if (user.activationToken != request.code) { - return ApiResponse("Неверный код активации", false) - } +// // Проверяем код активации +// if (user.activationToken != request.code) { +// return ApiResponse("Неверный код активации", false) +// } - // Обновляем пользователя - val updatedUser = user.copy( - isEnabled = true, - activationToken = null - ) +// // Обновляем пользователя +// val updatedUser = user.copy( +// isEnabled = true, +// activationToken = null +// ) - userRepository.save(updatedUser) +// userRepository.save(updatedUser) - return ApiResponse("Аккаунт успешно активирован", true) - } +// return ApiResponse("Аккаунт успешно активирован", true) +// } - fun login(request: LoginRequest): LoginResponse? { - val userOptional = userRepository.findByEmail(request.email) +// fun login(request: LoginRequest): LoginResponse? { +// val userOptional = userRepository.findByEmail(request.email) - if (userOptional.isEmpty) { - return null - } +// if (userOptional.isEmpty) { +// return null +// } - val user = userOptional.get() +// val user = userOptional.get() - // Проверяем, активирован ли аккаунт - if (!user.isEnabled) { - return null - } +// // Проверяем, активирован ли аккаунт +// if (!user.isEnabled) { +// return null +// } - if (!passwordEncoder.matches(request.password, user.passwordHash)) { - return null - } +// if (!passwordEncoder.matches(request.password, user.passwordHash)) { +// return null +// } - val token = jwtService.generateToken(user.email) +// val token = jwtService.generateToken(user.email) - return LoginResponse( - token = token, - username = user.username, - email = user.email - ) - } +// return LoginResponse( +// token = token, +// username = user.username, +// email = user.email +// ) +// } - fun getUserInfo(email: String): UserInfoResponse { - val user = userRepository.findByEmail(email).orElseThrow { - IllegalArgumentException("Пользователь не найден") - } - - return UserInfoResponse( - id = user.id, - username = user.username, - email = user.email - ) - } +// fun getUserInfo(email: String): UserInfoResponse { +// val user = userRepository.findByEmail(email).orElseThrow { +// IllegalArgumentException("Пользователь не найден") +// } + +// return UserInfoResponse( +// id = user.id, +// username = user.username, +// email = user.email +// ) +// } - fun forgotPassword(request: ForgotPasswordRequest): ApiResponse { - val userOptional = userRepository.findByEmail(request.email) +// fun forgotPassword(request: ForgotPasswordRequest): ApiResponse { +// val userOptional = userRepository.findByEmail(request.email) - if (userOptional.isEmpty) { - return ApiResponse("Пользователь с таким email не найден", false) - } +// if (userOptional.isEmpty) { +// return ApiResponse("Пользователь с таким email не найден", false) +// } - val user = userOptional.get() +// val user = userOptional.get() - // Проверяем, активирован ли аккаунт - if (!user.isEnabled) { - return ApiResponse("Аккаунт не активирован", false) - } +// // Проверяем, активирован ли аккаунт +// if (!user.isEnabled) { +// return ApiResponse("Аккаунт не активирован", false) +// } - // Генерируем 6-значный код для сброса пароля - val resetCode = generateSixDigitCode() +// // Генерируем 6-значный код для сброса пароля +// val resetCode = generateSixDigitCode() - // Код будет действителен в течение 15 минут - val expiryTime = System.currentTimeMillis() + (15 * 60 * 1000) // 15 минут +// // Код будет действителен в течение 15 минут +// val expiryTime = System.currentTimeMillis() + (15 * 60 * 1000) // 15 минут - // Обновляем пользователя с кодом сброса пароля - val updatedUser = user.copy( - passwordResetToken = resetCode, - passwordResetTokenExpiry = expiryTime - ) +// // Обновляем пользователя с кодом сброса пароля +// val updatedUser = user.copy( +// passwordResetToken = resetCode, +// passwordResetTokenExpiry = expiryTime +// ) - userRepository.save(updatedUser) +// userRepository.save(updatedUser) - // Отправляем код сброса пароля по email - emailService.sendPasswordResetEmail(user.email, resetCode) +// // Отправляем код сброса пароля по email +// emailService.sendPasswordResetEmail(user.email, resetCode) - return ApiResponse("Код для сброса пароля отправлен на ваш email", true) - } +// return ApiResponse("Код для сброса пароля отправлен на ваш email", true) +// } - fun resetPassword(request: ResetPasswordRequest): ApiResponse { - val userOptional = userRepository.findByPasswordResetToken(request.token) +// fun resetPassword(request: ResetPasswordRequest): ApiResponse { +// val userOptional = userRepository.findByPasswordResetToken(request.resetToken) - if (userOptional.isEmpty) { - return ApiResponse("Неверный код сброса пароля", false) - } +// if (userOptional.isEmpty) { +// return ApiResponse("Неверный код сброса пароля", false) +// } - val user = userOptional.get() +// val user = userOptional.get() - // Сохраняем значение токена в локальную переменную - val tokenExpiry = user.passwordResetTokenExpiry +// // Сохраняем значение токена в локальную переменную +// val tokenExpiry = user.passwordResetTokenExpiry - // Проверяем срок действия кода - if (tokenExpiry == null || System.currentTimeMillis() > tokenExpiry) { - return ApiResponse("Срок действия кода истек", false) - } +// // Проверяем срок действия кода +// if (tokenExpiry == null || System.currentTimeMillis() > tokenExpiry) { +// return ApiResponse("Срок действия кода истек", false) +// } - // Обновляем пользователя с новым паролем - val updatedUser = user.copy( - passwordHash = passwordEncoder.encode(request.newPassword), - passwordResetToken = null, - passwordResetTokenExpiry = null - ) +// // Обновляем пользователя с новым паролем +// val updatedUser = user.copy( +// passwordHash = passwordEncoder.encode(request.newPassword), +// passwordResetToken = null, +// passwordResetTokenExpiry = null +// ) - userRepository.save(updatedUser) +// userRepository.save(updatedUser) - return ApiResponse("Пароль успешно изменен", true) - } +// return ApiResponse("Пароль успешно изменен", true) +// } - // Функция генерации 6-значного кода - private fun generateSixDigitCode(): String { - return (100000 + Random.nextInt(900000)).toString() - } -} \ No newline at end of file +// // Функция генерации 6-значного кода +// private fun generateSixDigitCode(): String { +// return (100000 + Random.nextInt(900000)).toString() +// } +// } \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/util/OptionalExtensions.kt b/backend/src/main/kotlin/ru/vsu/app/util/OptionalExtensions.kt new file mode 100644 index 0000000..d84d61f --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/util/OptionalExtensions.kt @@ -0,0 +1,5 @@ +package ru.vsu.app.util + +import java.util.Optional + +fun Optional.toNullable(): T? = this.orElse(null) diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 8f3f700..4ac81a6 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,30 +1,40 @@ spring.application.name=app -spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:auth_db} -spring.datasource.username=${DB_USER:appuser} -spring.datasource.password=${DB_PASSWORD:password} -spring.datasource.driver-class-name=org.postgresql.Driver +# DB config +spring.datasource.url=${DB_URL} +spring.datasource.username=${DB_USERNAME} +spring.datasource.password=${DB_PASSWORD} +spring.datasource.driver-class-name=${DB_DRIVER} -spring.jpa.hibernate.ddl-auto=update -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.show-sql=true +# JPA config +spring.jpa.hibernate.ddl-auto=${JPA_DDL_AUTO} +spring.jpa.properties.hibernate.dialect=${JPA_DIALECT} +spring.jpa.show-sql=${JPA_SHOW_SQL} -application.security.jwt.secret-key=404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970 -application.security.jwt.expiration=86400000 +# Micrometr config +management.endpoints.web.exposure.include=${MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE} +management.metrics.export.prometheus.enabled=${METRICS_PROMETHEUS_ENABLED} -server.port=8080 +# JWT config +application.security.jwt.secret-key=${JWT_SECRET} +application.security.jwt.expiration=${JWT_EXPIRATION} -logging.level.org.springframework.security=DEBUG +# Server config +server.port=${SERVER_PORT} -spring.mail.host=smtp.gmail.com -spring.mail.port=587 -spring.mail.username=your-email@gmail.com -spring.mail.password=your-email-password -spring.mail.properties.mail.smtp.auth=true -spring.mail.properties.mail.smtp.starttls.enable=true +# Logging config +logging.level.org.springframework.security=${SPRING_SECURITY_LOG_LEVEL} + +# Email config +spring.mail.host=${EMAIL_HOST} +spring.mail.port=${EMAIL_PORT} +spring.mail.username=${EMAIL_USERNAME} +spring.mail.password=${EMAIL_PASSWORD} +spring.mail.properties.mail.smtp.auth=${EMAIL_SMTP_AUTH} +spring.mail.properties.mail.smtp.starttls.enable=${EMAIL_SMTP_STARTTLS_ENABLE} # Настройки Swagger OpenAPI -springdoc.swagger-ui.path=/swagger-ui.html -springdoc.api-docs.path=/v3/api-docs -springdoc.swagger-ui.operationsSorter=method -springdoc.swagger-ui.tagsSorter=alpha +springdoc.swagger-ui.path=${SWAGGER_UI_PATH} +springdoc.api-docs.path=${API_DOCS_PATH} +springdoc.swagger-ui.operationsSorter=${SWAGGER_OPERATIONS_SORTER} +springdoc.swagger-ui.tagsSorter=${SWAGGER_TAGS_SORTER} diff --git a/backend/src/main/resources/openapi.yaml b/backend/src/main/resources/openapi.yaml new file mode 100644 index 0000000..b13bc30 --- /dev/null +++ b/backend/src/main/resources/openapi.yaml @@ -0,0 +1,3573 @@ +openapi: 3.1.0 +info: + title: Cadrly API + version: 0.1.0 + description: Приложение для обмена коллекционными карточками + +servers: + - description: Test server + url: http://localhost:8080/api + +tags: + - name: Authentication + description: Регистрация, вход и управление аккаунтом + - name: Profile + description: Операции в профиле пользователя + - name: OtherProfile + description: Операции при просмотре профиля другого пользователя + - name: Inventory + description: Операции на странице "Инвентарь" + - name: Home + description: Операции на странице "Главное меню" + - name: Trades + description: Операции на странице "Обменник" + - name: Shop + description: Операции на странице "Магазин" + - name: Admin + description: Функции администратора + +paths: + /auth/register: + post: + tags: [Authentication] + summary: Регистрация нового пользователя + description: | + Принимает email, имя пользователя и пароль. + Если email и пароль валидны, отправляет код подтверждения. + Возвращает временный токен для верификации. + operationId: registerUser + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + format: email + example: 'user@example.com' + description: Email пользователя + username: + type: string + minLength: 3 + maxLength: 25 + example: 'Коллекционер_123' + description: 'Имя пользователя (3-25 символов)' + password: + type: string + format: password + minLength: 8 + example: 'Password123!' + description: 'Пароль (минимум 8 символов)' + required: [email, username, password] + responses: + 200: + description: Код подтверждения отправлен + content: + application/json: + schema: + type: object + properties: + tempToken: + type: string + example: 'temp_abc123' + message: + type: string + example: 'Код подтверждения отправлен на email' + 400: + description: Неверный формат данных + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Некорректный формат введенных данных' + 409: + description: Пользователь уже существует + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Пользователь с таким email уже существует' + 500: + $ref: '#/components/responses/ServerError' + + /auth/verify: + post: + tags: [Authentication] + summary: Подтверждение email + description: | + Проверяет код подтверждения. + Если код подтверждения верный - создает аккаунт и возвращает данные пользователя. + operationId: verifyEmail + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + tempToken: + type: string + example: 'temp_abc123' + description: 'Временный токен из ответа регистрации' + code: + type: string + pattern: "^\\d{6}$" + example: '123456' + description: '6-значный код подтверждения' + required: [tempToken, code] + responses: + 200: + description: Успешная регистрация + headers: + Set-Cookie: + schema: + type: string + example: 'session_id=abcde12345; HttpOnly; Path=/; Secure' + content: + application/json: + schema: + $ref: '#/components/schemas/User' + 400: + description: Неверный код или токен + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Неверный код подтверждения' + canRetry: + type: boolean + example: true + 410: + description: Истек срок действия кода + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Код устарел, запросите новый' + 500: + $ref: '#/components/responses/ServerError' + + /auth/resend-code: + post: + tags: [Authentication] + summary: Повторная отправка кода + description: Отправляет новый код подтверждения + operationId: resendVerificationCode + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + tempToken: + type: string + example: 'temp_abc123' + required: [tempToken] + responses: + 200: + description: Новый код отправлен + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Новый код подтверждения отправлен' + 400: + $ref: '#/components/responses/BadRequest' + 500: + $ref: '#/components/responses/ServerError' + + /auth/login: + post: + tags: [Authentication] + summary: Вход в систему + description: | + Аутентификация пользователя по email и паролю. + Если данные введены верно - устанавливает сессионную cookie. + operationId: loginUser + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + format: email + example: 'user@example.com' + password: + type: string + format: password + example: 'SecurePass123!' + required: [email, password] + responses: + 200: + description: Успешный вход + headers: + Set-Cookie: + schema: + type: string + example: 'session_id=abcde12345; HttpOnly; Path=/; Secure' + content: + application/json: + schema: + $ref: '#/components/schemas/User' + 400: + description: Неверный формат email + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Неверный фортмат email' + + 401: + description: Неверные учетные данные + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Неверный email или пароль' + 500: + $ref: '#/components/responses/ServerError' + + /auth/forgot-password: + post: + tags: [Authentication] + summary: Запрос кода для сброса пароля + description: Отправляет код подтверждения для сброса пароля, если email существует в системе + operationId: requestPasswordReset + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + format: email + example: 'user@example.com' + required: [email] + responses: + 200: + description: Код подтверждения отправлен на email + content: + application/json: + schema: + type: object + properties: + resetToken: + type: string + example: 'reset_xyz789' + message: + type: string + example: 'Код подтверждения отправлен на email' + 400: + description: Неверный формат данных + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Некорректный формат emaila' + 404: + description: Пользователь с указанным email не найден + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Пользователь с таким email не зарегистрирован' + 500: + $ref: '#/components/responses/ServerError' + + /auth/reset-password: + post: + tags: [Authentication] + summary: Сброс пароля + description: Устанавливает новый пароль после подтверждения кода + operationId: resetPassword + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + resetToken: + type: string + example: 'reset_xyz789' + code: + type: string + pattern: "^\\d{6}$" + example: '654321' + newPassword: + type: string + format: password + minLength: 8 + example: 'NewSecurePass123!' + required: [resetToken, code, newPassword] + responses: + 200: + description: Пароль изменен + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Пароль успешно изменен' + 400: + description: Неверный код или токен + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Неверный код подтверждения' + 500: + $ref: '#/components/responses/ServerError' + + /auth/check: + get: + tags: [Authentication] + summary: Проверка статуса аутентификации + description: | + Проверяет, авторизован ли пользователь по сессионной cookie. + Возвращает данные пользователя, если сессия активна. + security: + - bearerAuth: [] + responses: + 200: + description: Пользователь авторизован + content: + application/json: + schema: + $ref: '#/components/schemas/User' + 401: + description: Пользователь не авторизован + content: + application/json: + schema: + type: object + properties: + isAuthenticated: + type: boolean + example: false + message: + type: string + example: 'Требуется авторизация' + 500: + $ref: '#/components/responses/ServerError' + + /auth/refresh: + post: + tags: [Authentication] + summary: Обновление сессии + description: | + Обновляет сессионную cookie. + Требуется валидная существующая сессия. + security: + - bearerAuth: [] + responses: + 200: + description: Сессия успешно обновлена + headers: + Set-Cookie: + schema: + type: string + example: 'session_id=new_abcde12345; HttpOnly; Path=/; Secure' + content: + application/json: + schema: + $ref: '#/components/schemas/User' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /auth/logout: + post: + tags: [Authentication] + summary: Выход из системы + description: | + Завершает текущую сессию пользователя. + Удаляет сессионную cookie. + security: + - bearerAuth: [] + responses: + 204: + description: Успешный выход + headers: + Set-Cookie: + schema: + type: string + example: 'session_id=; HttpOnly; Path=/; Secure; Expires=Thu, 01 Jan 1970 00:00:00 GMT' + 500: + $ref: '#/components/responses/ServerError' + + /profile: + get: + tags: [Profile] + summary: Получение данных профиля + description: | + Загружает данные профиля пользователя. + - Проверяет авторизацию + - Возвращает статистику, избранные карты и достижения + security: + - bearerAuth: [] + responses: + 200: + description: Данные профиля + content: + application/json: + schema: + type: object + properties: + userStats: + $ref: '#/components/schemas/UserStats' + favoriteCards: + type: array + items: + $ref: '#/components/schemas/Card' + maxItems: 5 + favoriteAchievements: + type: array + items: + $ref: '#/components/schemas/Achievement' + maxItems: 4 + user_id: + type: integer + example: 123 + description: 'ID пользователя' + username: + type: string + example: 'Коллекционер_123' + description: 'Имя пользователя' + avatar_url: + type: string + format: url + example: 'https://example.com/avatars/user123.jpg' + description: 'Ссылка на аватар пользователя' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /profile/achievements: + get: + tags: [Profile] + summary: Получение списка достижений + description: | + Возвращает полный список достижений с текущим статусом выполнения + security: + - bearerAuth: [] + responses: + 200: + description: Список достижений + content: + application/json: + schema: + type: object + properties: + achievements: + type: array + items: + $ref: '#/components/schemas/Achievement' + 500: + $ref: '#/components/responses/ServerError' + + /profile/achievements/favorites/count: + get: + tags: [Profile] + summary: Проверка количества избранных достижений + description: | + Возвращает текущее количество избранных достижений + security: + - bearerAuth: [] + responses: + 200: + description: Количество избранных + content: + application/json: + schema: + type: object + properties: + count: + type: integer + maximum: 4 + example: 3 + canAddMore: + type: boolean + example: true + 500: + $ref: '#/components/responses/ServerError' + + /profile/achievements/{achievement_ID}/favorite-add: + post: + tags: [Profile] + summary: Добавить достижение в избранное + description: | + Добавляет достижение в избранное (максимум 4) + security: + - bearerAuth: [] + parameters: + - in: path + name: achievement_ID + required: true + schema: + type: integer + responses: + 200: + description: Успешно добавлено в избранное + 400: + description: Невозможно добавить в избранное (лимит достигнут) + 500: + $ref: '#/components/responses/ServerError' + + /profile/achievements/{achievement_ID}/favorite-delete: + delete: + tags: [Profile] + summary: Удалить достижение из избранного + description: | + Удаляет достижение из избранного + security: + - bearerAuth: [] + parameters: + - in: path + name: achievement_ID + required: true + schema: + type: integer + responses: + 200: + description: Успешно удалено из избранного + 500: + $ref: '#/components/responses/ServerError' + + /profile/settings: + get: + tags: [Profile] + summary: Получение настроек + description: | + Возвращает текущие настройки профиля + security: + - bearerAuth: [] + responses: + 200: + description: Настройки профиля + content: + application/json: + schema: + $ref: '#/components/schemas/UserSettings' + 500: + $ref: '#/components/responses/ServerError' + + /profile/settings-change: + put: + tags: [Profile] + summary: Обновление настроек + description: | + Обновляет настройки профиля + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserSettings' + responses: + 200: + description: Настройки обновлены + content: + application/json: + schema: + $ref: '#/components/schemas/UserSettings' + 500: + $ref: '#/components/responses/ServerError' + + /other-profile/{user_id}: + get: + tags: [OtherProfile] + summary: Получение данных профиля другого пользователя + description: | + Загружает данные профиля другого пользователя. + - Возвращает статистику, избранные карты и достижения + - Проверяет настройки приватности инвентаря + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + responses: + 200: + description: Данные профиля + content: + application/json: + schema: + type: object + properties: + userStats: + $ref: '#/components/schemas/UserStats' + favoriteCards: + type: array + items: + $ref: '#/components/schemas/Card' + maxItems: 5 + favoriteAchievements: + type: array + items: + $ref: '#/components/schemas/Achievement' + maxItems: 4 + inventoryVisible: + type: boolean + example: true + id: + type: integer + example: '456' + username: + type: string + example: 'Игрок_456' + 500: + $ref: '#/components/responses/ServerError' + + /other-profile/{user_id}/achievements: + get: + tags: [OtherProfile] + summary: Получение списка достижений другого пользователя + description: | + Возвращает полный список достижений с текущим статусом выполнения + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + responses: + 200: + description: Список достижений + content: + application/json: + schema: + type: object + properties: + achievements: + type: array + items: + $ref: '#/components/schemas/Achievement' + 500: + $ref: '#/components/responses/ServerError' + + /other-profile/{user_id}/inventory: + get: + tags: [OtherProfile] + summary: Просмотр инвентаря другого пользователя + description: | + Возвращает инвентарь пользователя, если он не скрыт в настройках + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + responses: + 200: + description: Инвентарь пользователя + content: + application/json: + schema: + type: object + properties: + cards: + type: array + items: + $ref: '#/components/schemas/Card' + collections: + type: array + items: + $ref: '#/components/schemas/Collection' + 403: + description: Инвентарь скрыт + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Пользователь скрыл свой инвентарь' + 500: + $ref: '#/components/responses/ServerError' + + /other-profile/inventory/sort: + post: + tags: [OtherProfile] + summary: Сортировка карт в инвентаре другого пользователя + description: | + Сортирует карты в инвентаре другого пользователя + (по редкости, по коллекциям, механизм сортировки подробнее описан в инвентаре) + parameters: + - in: query + name: user_id + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + sortType: + type: string + enum: [rarity_desc, rarity_asc, collection, reset] + responses: + 200: + description: Отсортированный список карт + content: + application/json: + schema: + type: object + properties: + inventory: + type: array + items: + $ref: '#/components/schemas/Card' + 500: + $ref: '#/components/responses/ServerError' + + /other-profile/card/{card_ID}/view: + get: + tags: [OtherProfile] + summary: Просмотр карты другого пользователя + description: | + Возвращает детальную информацию о карте другого пользователя + и возвращает кнопку для создания предложения обмена + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + - in: query + name: owner_ID + required: true + schema: + type: integer + responses: + 200: + description: Информация о карте + content: + application/json: + schema: + type: object + properties: + card: + $ref: '#/components/schemas/Card' + 500: + $ref: '#/components/responses/ServerError' + + /other-profile/card/{card_ID}/initiate-trade: + post: + tags: [OtherProfile] + summary: Предложение обмена + description: | + Создает предложение обмена для указанной карты + и перенаправляет на страницу создания обмена + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + - in: query + name: owner_ID + required: true + schema: + type: integer + responses: + 200: + description: Перенаправление на создание обмена + content: + application/json: + schema: + type: object + properties: + redirectUrl: + type: string + example: '/trades/create?requested_card=123&owner=456' + 400: + description: Невозможно предложить обмен + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Недостаточно карт для обмена' + 409: + description: Конфликт при попытке обмена + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Невозможно предложить обмен - у владельца только один экземпляр этой карты' + cardQuantity: + type: integer + example: 1 + description: 'Количество экземпляров карты у владельца' + 500: + $ref: '#/components/responses/ServerError' + + /other-profile/{user_id}/report: + post: + tags: [OtherProfile] + summary: Подача жалобы на пользователя + description: | + Перенаправляет на страницу создания жалобы. + security: + - bearerAuth: [] + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + description: ID пользователя, на которого подается жалоба + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + reason: + type: string + enum: ['Неуместный никнейм', 'Неуместный аватар', 'Другое'] + example: 'Неуместный никнейм' + description: 'Причина жалобы' + comment: + type: string + maxLength: 500 + nullable: true + example: 'Никнейм содержит нецензурные слова' + description: 'Дополнительные комментарии' + required: [reason] + responses: + 200: + description: Перенаправление на страницу создания жалобы + content: + application/json: + schema: + type: object + properties: + redirectUrl: + type: string + example: '/report/create?reported_user=123' + reportDraft: + $ref: '#/components/schemas/Report' + 500: + $ref: '#/components/responses/ServerError' + + /inventory: + get: + tags: [Inventory] + summary: Получение данных инвентаря + description: | + Загружает данные инвентаря пользователя. + - Проверяет авторизацию пользователя + - Если пользователь не авторизован - возвращает ошибку 401 + - Если авторизован - возвращает список его карт, список избранных карт и баланс + security: + - bearerAuth: [] + responses: + 200: + description: Успешная загрузка инвентаря + content: + application/json: + schema: + type: object + properties: + inventory: + type: array + items: + $ref: '#/components/schemas/Card' + favoriteCards: + type: array + items: + type: integer + description: 'ID избранных карт' + collections: + type: array + items: + $ref: '#/components/schemas/Collection' + balance: + type: integer + example: 2500 + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /inventory/card/{card_ID}/quantity: + get: + tags: [Inventory] + summary: Проверка количества экземпляров карты + description: | + Возвращает количество экземпляров карты у пользователя. + Если экземпляров больше 1 - можно разбирать/выставлять на обмен. + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 200: + description: Информация о количестве карт + content: + application/json: + schema: + type: object + properties: + count: + type: integer + example: 3 + canDisassemble: + type: boolean + example: true + canTrade: + type: boolean + example: true + 500: + $ref: '#/components/responses/ServerError' + + /inventory/favorites/count: + get: + tags: [Inventory] + summary: Получение количества избранных карт + description: | + Возвращает текущее количество избранных карт. + Если количество меньше 5 - можно добавлять новые карты в избранное. + security: + - bearerAuth: [] + responses: + 200: + description: Количество избранных карт + content: + application/json: + schema: + type: object + properties: + count: + type: integer + example: 3 + canAddMore: + type: boolean + example: true + 500: + $ref: '#/components/responses/ServerError' + + /inventory/card/{card_ID}/favorite: + get: + tags: [Inventory] + summary: Проверка статуса карты + description: | + Проверяет, добавлена ли карта в избранное. + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 200: + description: Статус избранного + content: + application/json: + schema: + type: object + properties: + isFavorite: + type: boolean + example: false + 500: + $ref: '#/components/responses/ServerError' + /inventory/card/{card_ID}/favorite-add: + post: + tags: [Inventory] + summary: Добавление карты в избранное + description: | + Добавляет карту в избранное, если: + - Карта не добавлена в "избранное" + - Общее количество избранных карт < 5 + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 200: + description: Карта успешно добавлена в избранное + 400: + description: Невозможно добавить карту в избранное + 500: + $ref: '#/components/responses/ServerError' + + /inventory/card/{card_ID}/favorite-delete: + delete: + tags: [Inventory] + summary: Удаление карты из избранного + description: | + Удаляет карту из избранного. + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 200: + description: Карта успешно удалена из избранного + 500: + $ref: '#/components/responses/ServerError' + + /inventory/sort: + post: + tags: [Inventory] + summary: Сортировка карт в инвентаре + description: | + Сортирует карты в инвентаре(по редкости, по коллекциям): + - При выборе по редкости: + - 1 нажатие: по убыванию редкости (Уникальная → Легендарная → Эпическая → Редкая → Обычная) + - 2 нажатие: по возрастанию редкости + - 3 нажатие: сброс сортировки + - При выборе "по коллекциям": сначала идут карты из одной коллекции, коллекции сортируются по ID. + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + sortType: + type: string + enum: [rarity_desc, rarity_asc, collection, reset] + responses: + 200: + description: Отсортированный список карт + content: + application/json: + schema: + type: object + properties: + cards: + type: array + items: + $ref: '#/components/schemas/Card' + 500: + $ref: '#/components/responses/ServerError' + + /inventory/destroy: + post: + tags: [Inventory] + summary: Разбор карты на валюту + description: | + Разбирает карту на валюту, если: + - У пользователя есть минимум 2 экземпляра этой карты + - Добавляет стоимость карты на баланс пользователя + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + card_ID: + type: integer + minimum: 1 + example: 501 + required: [card_ID] + responses: + 200: + description: Карта успешно разобрана + content: + application/json: + schema: + type: object + properties: + newBalance: + type: integer + example: 2550 + destroyedCardId: + type: integer + example: 501 + 500: + $ref: '#/components/responses/ServerError' + /inventory/card/{card_ID}/trade-status: + get: + tags: [Inventory] + summary: Проверка статуса обмена карты + description: | + Проверяет, выставлена ли карта на обмен. + Возвращает статус карты. + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 200: + description: Статус обмена карты + content: + application/json: + schema: + type: object + properties: + isOnTrade: + type: boolean + example: true + description: 'Выставлена ли карта на обмен' + 500: + $ref: '#/components/responses/ServerError' + + /inventory/put-on-trade: + post: + tags: [Inventory] + summary: Выставление карты на обмен + description: | + Выставляет карту на обмен, если: + - У пользователя есть минимум 2 экземпляра этой карты + - Карта еще не выставлена на обмен + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + card_ID: + type: integer + minimum: 1 + example: 501 + required: [card_ID] + responses: + 200: + description: Карта готова к обмену + content: + application/json: + schema: + type: object + properties: + redirectUrl: + type: string + example: '/trades/create?requested_card=123&owner=456' + 400: + description: Невозможно выставить карту на обмен + 500: + $ref: '#/components/responses/ServerError' + + /inventory/card/{card_ID}/trade-cancel: + post: + tags: [Inventory] + summary: Снятие карты с обмена + description: Возвращает карту из раздела "На обмен" в Инвентарь + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 200: + description: Карта успешно снята с обмена + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Карта успешно снята с обмена' + 500: + $ref: '#/components/responses/ServerError' + /home/search: + get: + tags: [Home] + summary: Поиск пользователей + description: | + Поиск пользователей по никнейму или ID + parameters: + - in: query + name: query + description: Никнейм или ID пользователя + schema: + type: string + responses: + 200: + description: Список найденных пользователей + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + 400: + description: Неверный запрос + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Введите минимум 3 символа для поиска' + 404: + description: Пользователь не найден + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Пользователь с введеными данными не найден' + 500: + $ref: '#/components/responses/ServerError' + + /home/notifications: + get: + tags: [Home] + summary: Просмотр уведомлений + description: | + Возвращает список уведомлений пользователя + security: + - bearerAuth: [] + responses: + 200: + description: Список уведомлений + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Notification' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /home/notifications/{notification_ID}: + get: + tags: [Home] + summary: Просмотр уведомления + description: | + Возвращает информацию по конкретному уведомлению + security: + - bearerAuth: [] + parameters: + - in: path + name: notification_ID + required: true + schema: + type: integer + example: 123 + description: 'ID уведомления' + responses: + 200: + description: Детальная информация об уведомлении + content: + application/json: + schema: + $ref: '#/components/schemas/Notification' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /home/notifications/{notification_ID}/navigate: + post: + tags: [Home] + summary: Переход по ссылке в уведомлении + description: | + Перенаправляет на страницу обмена, связанного с уведомлением + security: + - bearerAuth: [] + parameters: + - in: path + name: notification_ID + required: true + schema: + type: integer + responses: + 200: + description: Перенаправление на страницу обмена + content: + application/json: + schema: + type: object + properties: + redirectUrl: + type: string + example: '/trades/123' + 404: + description: Предложение обмена не найдено + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Данное предложение обмена уже не существует' + 500: + $ref: '#/components/responses/ServerError' + /home/generate-card/themes: + get: + tags: [Home] + summary: Получение списка тем для генерации карт + description: | + Возвращает список доступных тем для генерации уникальных карт + security: + - bearerAuth: [] + responses: + 200: + description: Список тем + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Theme' + 500: + $ref: '#/components/responses/ServerError' + /home/generate-card: + post: + tags: [Home] + summary: Генерация уникальной карты + description: | + Генерирует уникальную карту по выбранной теме, если на балансе достаточно средств + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + theme_ID: + type: integer + example: 901 + description: 'ID выбранной темы' + required: [theme_ID] + responses: + 200: + description: Карта успешно сгенерирована + content: + application/json: + schema: + type: object + properties: + card: + $ref: '#/components/schemas/Card' + newBalance: + type: integer + example: 1500 + 402: + description: Недостаточно средств + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Недостаточно средств для генерации карты' + requiredAmount: + type: integer + example: 1000 + 500: + $ref: '#/components/responses/ServerError' + /home/news: + get: + tags: [Home] + summary: Получение списока новостей + description: | + Возвращает список новостей + responses: + 200: + description: Список новостей + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/News' + 404: + description: Новостей не найдено + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Новостей пока нет' + 500: + $ref: '#/components/responses/ServerError' + /home/news/{news_ID}: + get: + tags: [Home] + summary: Просмотр новости + description: | + Возвращает полную информацию по конкретной новости + parameters: + - in: path + name: news_ID + required: true + schema: + type: integer + responses: + 200: + description: Детальная информация о новости + content: + application/json: + schema: + $ref: '#/components/schemas/News' + 500: + $ref: '#/components/responses/ServerError' + /home/quests: + get: + tags: [Home] + summary: Получение списка квестов + description: | + Возвращает список ежедневных и еженедельных квестов с их статусом + security: + - bearerAuth: [] + responses: + 200: + description: Список квестов + content: + application/json: + schema: + type: object + properties: + dailyQuests: + type: array + items: + $ref: '#/components/schemas/Quest' + weeklyQuests: + type: array + items: + $ref: '#/components/schemas/Quest' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + /home/quests/{quest_ID}/change-status: + post: + tags: [Home] + summary: Изменение статуса выполнения квеста + description: | + Изменение статуса выполнения квеста + security: + - bearerAuth: [] + parameters: + - in: path + name: quest_ID + required: true + schema: + type: integer + example: 123 + description: 'ID квеста' + responses: + 200: + description: Статус квеста успешно изменен + content: + application/json: + schema: + type: object + properties: + quest: + $ref: '#/components/schemas/Quest' + message: + type: string + example: 'Статус квеста успешно изменен' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /home/quests/claim-reward: + post: + tags: [Home] + summary: Получение награды за выполнение квестов + description: | + Выдает награду за выполнение квестов + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + questType: + type: string + enum: [daily, weekly] + example: 'daily' + required: [questType] + responses: + 200: + description: Награда получена + content: + application/json: + schema: + type: object + properties: + receivedCoins: + type: integer + example: 500 + receivedPacks: + type: array + items: + $ref: '#/components/schemas/Pack' + newBalance: + type: integer + example: 2000 + 400: + description: Не все квесты выполнены + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Выполнены не все квесты, необходимые для получения награды' + 500: + $ref: '#/components/responses/ServerError' + /trades: + get: + tags: [Trades] + summary: Получить все доступные предложения обмена + description: | + Возвращает список всех доступных предложений обмена. + Можно провести поиск по названию карты. + parameters: + - in: query + name: search + description: Поиск по названию карты + schema: + type: string + responses: + 200: + description: Список предложений обмена + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Trade' + 500: + $ref: '#/components/responses/ServerError' + /trades/{trade_id}: + get: + tags: [Trades] + summary: Получить детали предложения обмена + description: | + Возвращает полную информацию о конкретном предложении обмена. + parameters: + - in: path + name: trade_id + required: true + schema: + type: integer + responses: + 200: + description: Детали предложения обмена + content: + application/json: + schema: + $ref: '#/components/schemas/Trade' + 404: + description: Предложение обмена не найдено + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Предложение обмена не найдено' + 500: + $ref: '#/components/responses/ServerError' + /trades/initiate: + post: + tags: [Trades] + summary: Создать новое предложение обмена + description: | + Создает новое предложение обмена. + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + offeredCardId: + type: integer + example: 401 + description: ID карты, которую предлагает текущий пользователь + requestedCardId: + type: array + items: + type: integer + description: ID карт, на которые пользователь готов совершить "быстрый обмен" + example: [100, 200] + required: [receivingUserId, requestedCardId, offeredCardId] + responses: + 201: + description: Предложение обмена успешно создано + content: + application/json: + schema: + $ref: '#/components/schemas/Trade' + 400: + description: Невозможно создать предложение обмена + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Недостаточно карт для обмена' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + /trades/my: + get: + tags: [Trades] + summary: Получение моих обменов + description: | + Возвращает список всех обменов текущего пользователя с их статусами. + security: + - bearerAuth: [] + parameters: + - in: query + name: user_id + required: true + schema: + type: integer + example: 123 + description: 'ID текущего пользователя' + responses: + 200: + description: Список моих обменов + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Trade' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + /trades/{trade_id}/accept: + post: + tags: [Trades] + summary: Принять предложение обмена + description: | + Принимает предложение обмена, если текущий пользователь - получатель обмена. + security: + - bearerAuth: [] + parameters: + - in: path + name: trade_id + required: true + schema: + type: integer + responses: + 200: + description: Обмен успешно принят + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Обмен успешно завершен' + + 401: + $ref: '#/components/responses/Unauthorized' + 403: + description: Нет прав для принятия этого обмена + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Вы не можете принять этот обмен' + 500: + $ref: '#/components/responses/ServerError' + + /trades/{trade_id}/reject: + post: + tags: [Trades] + summary: Отклонить предложение обмена + description: | + Отклоняет предложение обмена, если текущий пользователь - получатель обмена. + security: + - bearerAuth: [] + parameters: + - in: path + name: trade_id + required: true + schema: + type: integer + responses: + 200: + description: Обмен успешно отклонен + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Обмен отклонен' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /trades/{trade_id}/cancel: + post: + tags: [Trades] + summary: Отменить предложение обмена + description: | + Отменяет предложение обмена, если текущий пользователь является инициатором. + security: + - bearerAuth: [] + parameters: + - in: path + name: trade_id + required: true + schema: + type: integer + responses: + 200: + description: Обмен успешно отменен + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Обмен отменен' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /shop/packs: + get: + tags: [Shop] + summary: Получение списка наборов + description: | + Возвращает список доступных для покупки наборов карт + responses: + 200: + description: Список наборов + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pack' + 500: + $ref: '#/components/responses/ServerError' + + /shop/packs/{pack_id}: + get: + tags: [Shop] + summary: Просмотр содержимого набора + description: | + Возвращает подробную информацию о наборе и возможных картах + parameters: + - in: path + name: pack_id + required: true + schema: + type: integer + responses: + 200: + description: Информация о наборе + content: + application/json: + schema: + $ref: '#/components/schemas/Pack' + 500: + $ref: '#/components/responses/ServerError' + /shop/packs/{pack_id}/buy: + post: + tags: [Shop] + summary: Покупка набора карт + description: | + Покупает набор карт, если: + - Пользователь авторизован + - На балансе достаточно средств + security: + - bearerAuth: [] + parameters: + - in: path + name: pack_id + required: true + schema: + type: integer + responses: + 200: + description: Успешная покупка + content: + application/json: + schema: + type: object + properties: + receivedCards: + type: array + items: + $ref: '#/components/schemas/Card' + newBalance: + type: integer + 401: + $ref: '#/components/responses/Unauthorized' + 402: + description: Недостаточно средств + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Недостаточно средств на балансе' + requiredAmount: + type: integer + example: 1000 + 500: + $ref: '#/components/responses/ServerError' + /shop/packs/{pack_id}/open: + post: + tags: [Shop] + summary: Открытие купленного набора карт + description: | + Открывает ранее купленный набор карт, показывая выпавшие карты + и добавляя их в инвентарь пользователя. + security: + - bearerAuth: [] + parameters: + - in: path + name: pack_id + required: true + schema: + type: integer + example: 1 + description: 'ID набора карт' + responses: + 200: + description: Набор успешно открыт + content: + application/json: + schema: + type: object + properties: + receivedCards: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Карты, полученные из набора' + newCardsAdded: + type: boolean + example: true + description: 'Были ли карты успешно добавлены в инвентарь' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /shop/coins/offers: + get: + tags: [Shop] + summary: Получение предложений по монетам + description: | + Возвращает список доступных пакетов монет + responses: + 200: + description: Список предложений + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CoinOffer' + 500: + $ref: '#/components/responses/ServerError' + /shop/coins/offers/{offer_id}: + get: + tags: [Shop] + summary: Просмотр предложения монет + description: | + Возвращает детализацию предложения по монетам + parameters: + - in: path + name: offer_id + required: true + schema: + type: integer + responses: + 200: + description: Информация о предложении + content: + application/json: + schema: + $ref: '#/components/schemas/CoinOffer' + 500: + $ref: '#/components/responses/ServerError' + /shop/coins/offers/{offer_id}/purchase: + post: + tags: [Shop] + summary: Покупка монет + description: | + Инициирует процесс покупки монет через платежный шлюз + security: + - bearerAuth: [] + parameters: + - in: path + name: offer_id + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + redirectUrl: + type: string + format: uri + example: 'https://cardly.ru/shop/payment-callback' + required: [redirectUrl] + responses: + 200: + description: Перенаправление на платежный шлюз + content: + application/json: + schema: + type: object + properties: + paymentUrl: + type: string + format: uri + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + /shop/payments/process: + post: + tags: [Shop] + summary: Обработка платежа + description: | + обработка результата платежа от платежного шлюза + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PaymentCallback' + responses: + 200: + description: Платеж обработан + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + transactionId: + type: string + 500: + $ref: '#/components/responses/ServerError' + /admin/cards: + post: + tags: [Admin] + summary: Создание новой карты + description: Создает новую карту в системе + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Card' + responses: + 201: + description: Карта успешно создана + content: + application/json: + schema: + $ref: '#/components/schemas/Card' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/cards/{card_ID}: + put: + tags: [Admin] + summary: Обновление карты + description: Обновляет информацию о карте + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Card' + responses: + 200: + description: Карта успешно обновлена + content: + application/json: + schema: + $ref: '#/components/schemas/Card' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + delete: + tags: [Admin] + summary: Удаление карты + description: Удаляет карту из системы + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 204: + description: Карта успешно удалена + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/collections: + post: + tags: [Admin] + summary: Создание новой коллекции + description: Создает новую коллекцию карт + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Collection' + responses: + 201: + description: Коллекция успешно создана + content: + application/json: + schema: + $ref: '#/components/schemas/Collection' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/collections/{collection_ID}: + put: + tags: [Admin] + summary: Обновление коллекции + description: Обновляет информацию о коллекции + security: + - bearerAuth: [] + parameters: + - in: path + name: collection_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Collection' + responses: + 200: + description: Коллекция успешно обновлена + content: + application/json: + schema: + $ref: '#/components/schemas/Collection' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + delete: + tags: [Admin] + summary: Удаление коллекцию + description: Удаляет коллекцию из системы + security: + - bearerAuth: [] + parameters: + - in: path + name: collection_ID + required: true + schema: + type: integer + responses: + 204: + description: Коллекция успешно удалена + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/packs: + post: + tags: [Admin] + summary: Создание нового набора + description: Создает новый набор карт + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pack' + responses: + 201: + description: Набор успешно создан + content: + application/json: + schema: + $ref: '#/components/schemas/Pack' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/packs/{pack_ID}: + put: + tags: [Admin] + summary: Обновление набора + description: Обновляет информацию о наборе + security: + - bearerAuth: [] + parameters: + - in: path + name: pack_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pack' + responses: + 200: + description: Набор успешно обновлен + content: + application/json: + schema: + $ref: '#/components/schemas/Pack' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + delete: + tags: [Admin] + summary: Удаление набор + description: Удаляет набор из системы + security: + - bearerAuth: [] + parameters: + - in: path + name: pack_ID + required: true + schema: + type: integer + responses: + 204: + description: Набор успешно удален + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/coin-offers: + post: + tags: [Admin] + summary: Создание предложения покупки монет + description: Создает новое предложение покупки монет в системе + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CoinOffer' + responses: + 201: + description: Предложение успешно создано + content: + application/json: + schema: + $ref: '#/components/schemas/CoinOffer' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + + get: + tags: [Admin] + summary: Получение списка предложений покупки монет + description: Возвращает список всех предложений покупки монет + security: + - bearerAuth: [] + responses: + 200: + description: Список предложений успешно получен + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CoinOffer' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + + /admin/coin-offers/{offer_id}: + put: + tags: [Admin] + summary: Обновление предложения покупки монет + description: Обновляет информацию о предложении покупки монет + security: + - bearerAuth: [] + parameters: + - in: path + name: offer_id + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CoinOffer' + responses: + 200: + description: Предложение успешно обновлено + content: + application/json: + schema: + $ref: '#/components/schemas/CoinOffer' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + + delete: + tags: [Admin] + summary: Удаление предложения покупки монет + description: Удаляет предложение покупки монет из системы + security: + - bearerAuth: [] + parameters: + - in: path + name: offer_id + required: true + schema: + type: integer + responses: + 204: + description: Предложение успешно удалено + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/news: + post: + tags: [Admin] + summary: Создание новости + description: Создает новую новость в системе + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/News' + responses: + 201: + description: Новость успешно создана + content: + application/json: + schema: + $ref: '#/components/schemas/News' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/news/{news_ID}: + put: + tags: [Admin] + summary: Обновление новости + description: Обновляет информацию о новости + security: + - bearerAuth: [] + parameters: + - in: path + name: news_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/News' + responses: + 200: + description: Новость успешно обновлена + content: + application/json: + schema: + $ref: '#/components/schemas/News' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + delete: + tags: [Admin] + summary: Удаление новости + description: Удаляет новость из системы + security: + - bearerAuth: [] + parameters: + - in: path + name: news_ID + required: true + schema: + type: integer + responses: + 204: + description: Новость успешно удалена + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/achievements: + post: + tags: [Admin] + summary: Создание достижения + description: Создает новое достижение в системе + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Achievement' + responses: + 201: + description: Достижение успешно создано + content: + application/json: + schema: + $ref: '#/components/schemas/Achievement' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/achievements/{achievement_ID}: + put: + tags: [Admin] + summary: Обновление достижения + description: Обновляет информацию о достижении + security: + - bearerAuth: [] + parameters: + - in: path + name: achievement_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Achievement' + responses: + 200: + description: Достижение успешно обновлено + content: + application/json: + schema: + $ref: '#/components/schemas/Achievement' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + delete: + tags: [Admin] + summary: Удаление достижения + description: Удаляет достижение из системы + security: + - bearerAuth: [] + parameters: + - in: path + name: achievement_ID + required: true + schema: + type: integer + responses: + 204: + description: Достижение успешно удалено + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/trades: + get: + tags: [Admin] + summary: Получение списка обменов + description: Возвращает список всех обменов в системе + security: + - bearerAuth: [] + responses: + 200: + description: Список обменов + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Trade' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/trades/{trade_ID}/invalidate: + post: + tags: [Admin] + summary: Признание обмена недействительным + description: | + Помечает обмен как недействительный и возвращает карты участникам + security: + - bearerAuth: [] + parameters: + - in: path + name: trade_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + reason: + type: string + description: Причина аннулирования обмена + required: [reason] + responses: + 200: + description: Обмен аннулирован + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Обмен успешно аннулирован' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/reports: + get: + tags: [Admin] + summary: Просмотр списка жалоб + description: Возвращает список всех жалоб в системе + security: + - bearerAuth: [] + responses: + 200: + description: Список жалоб + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Report' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/reports/{report_ID}: + get: + tags: [Admin] + summary: Просмотр деталей жалобы + description: Возвращает полную информацию о жалобе + security: + - bearerAuth: [] + parameters: + - in: path + name: report_ID + required: true + schema: + type: integer + responses: + 200: + description: Детали жалобы + content: + application/json: + schema: + $ref: '#/components/schemas/Report' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + put: + tags: [Admin] + summary: Обновление статуса жалобы + description: Обновляет статус жалобы и применяет меры к пользователю + security: + - bearerAuth: [] + parameters: + - in: path + name: report_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: [pending, reviewed, resolved] + example: 'resolved' + action: + type: string + enum: [none, warning, temporary_ban, permanent_ban] + example: 'temporary_ban' + banDurationDays: + type: integer + nullable: true + example: 7 + description: 'Длительность бана в днях (только для temporary_ban)' + comment: + type: string + nullable: true + example: 'Пользователь получил временный бан на 7 дней за нарушение правил' + required: [status, action] + responses: + 200: + description: Статус жалобы обновлен + content: + application/json: + schema: + $ref: '#/components/schemas/Report' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + description: Жалоба не найдена + 500: + $ref: '#/components/responses/ServerError' + /admin/users/{user_id}/ban: + post: + tags: [Admin] + summary: Блокировка пользователя + description: Блокирует пользователя на указанный срок или навсегда + security: + - bearerAuth: [] + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + banType: + type: string + enum: [temporary, permanent] + example: 'temporary' + durationDays: + type: integer + nullable: true + example: 7 + description: 'Длительность бана в днях' + reason: + type: string + example: 'Нарушение правил сообщества' + required: [banType, reason] + responses: + 200: + description: Пользователь заблокирован + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Пользователь успешно заблокирован' + banEndDate: + type: string + format: date-time + nullable: true + example: '2024-06-20T12:00:00Z' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + description: Пользователь не найден + 500: + $ref: '#/components/responses/ServerError' + /admin/users/{user_id}/unban: + post: + tags: [Admin] + summary: Разблокировка пользователя + description: Снимает блокировку с пользователя + security: + - bearerAuth: [] + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + responses: + 200: + description: Пользователь разблокирован + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Пользователь успешно разблокирован' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + description: Пользователь не найден + 500: + $ref: '#/components/responses/ServerError' + /admin/users/{user_id}/delete: + delete: + tags: [Admin] + summary: Удаление пользователя + description: Полностью удаляет аккаунт пользователя из системы + security: + - bearerAuth: [] + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + responses: + 204: + description: Пользователь успешно удален + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + description: Пользователь не найден + 500: + $ref: '#/components/responses/ServerError' + /admin/users/{user_id}/inventory/{card_ID}: + delete: + tags: [Admin] + summary: Удаление карты из инвентаря пользователя + description: Удаляет конкретную карту из коллекции пользователя + security: + - bearerAuth: [] + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 204: + description: Карта успешно удалена из инвентаря + 400: + description: Неверный запрос + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/users/{user_id}/achievements/{achievement_ID}: + delete: + tags: [Admin] + summary: Отзыв достижения у пользователя + description: Удаляет достижение из списка полученных пользователем + security: + - bearerAuth: [] + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + - in: path + name: achievement_ID + required: true + schema: + type: integer + responses: + 204: + description: Достижение успешно отозвано + 400: + description: Неверный запрос + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/users/{user_id}/quests/{quest_ID}/reset: + post: + tags: [Admin] + summary: Сброс выполнения квеста + description: Сбрасывает статус выполнения квеста для пользователя + security: + - bearerAuth: [] + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + - in: path + name: quest_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + reason: + type: string + description: Причина сброса квеста + removeRewards: + type: boolean + default: false + description: Нужно ли удалить награды за квест + required: [reason] + responses: + 200: + description: Квест успешно сброшен + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Квест успешно сброшен' + rewardsRemoved: + type: boolean + example: true + 400: + description: Неверный запрос + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/stats: + get: + tags: [Admin] + summary: Получение статистики системы + description: Возвращает статистику по пользователям, картам, обменам и т.д. + security: + - bearerAuth: [] + responses: + 200: + description: Статистика системы + content: + application/json: + schema: + type: object + properties: + totalUsers: + type: integer + example: 1000 + activeUsers: + type: integer + example: 750 + bannedUsers: + type: integer + example: 25 + totalCards: + type: integer + example: 5000 + totalTrades: + type: integer + example: 1200 + completedTrades: + type: integer + example: 1000 + invalidatedTrades: + type: integer + example: 50 + pendingReports: + type: integer + example: 15 + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' +components: + schemas: + News: + type: object + properties: + news_ID: + type: integer + example: 1 + description: 'Уникальный идентификатор новости' + title: + type: string + example: 'Новое обновление системы' + description: 'Заголовок новости' + content: + type: string + example: 'Мы добавили новые карты в коллекцию' + description: 'Содержание новости' + pictures: + type: array + items: + type: string + format: url + example: + ['https://example.com/news1.jpg', 'https://example.com/news2.jpg'] + description: 'Ссылки на изображения' + datePosted: + type: string + format: date-time + example: '2024-05-20T14:48:00Z' + description: 'Дата публикации' + required: + - news_ID + - title + - content + - datePosted + + Achievement: + type: object + properties: + achievement_ID: + type: integer + example: 101 + description: 'Уникальный идентификатор достижения' + name: + type: string + example: 'Мастер коллекционирования' + description: 'Название достижения' + imageURL: + type: string + format: url + example: 'https://example.com/achievements/master.png' + description: 'Ссылка на изображение' + description: + type: string + example: 'Соберите 10 полных коллекций' + description: 'Описание достижения' + isUnlocked: + type: boolean + example: false + description: 'Статус получения' + required: + - achievement_ID + - name + - imageURL + - description + - isUnlocked + + Notification: + type: object + properties: + notification_ID: + type: integer + example: 201 + description: 'Уникальный идентификатор уведомления' + user: + $ref: '#/components/schemas/User' + description: 'Пользователь, которому отправлено уведомление' + message: + type: string + example: 'Ваша карта была успешно обменяна' + description: 'Текст уведомления' + links: + type: array + items: + type: string + format: url + example: ['https://example.com/exchange-details'] + description: 'Ссылка на подробности обмена' + notificationDateTime: + type: string + format: date-time + example: '2024-05-20T15:30:00Z' + description: 'Дата и время уведомления' + required: + - notification_ID + - user + - message + - notificationDateTime + + Report: + type: object + properties: + report_ID: + type: integer + example: 301 + description: 'Уникальный идентификатор жалобы' + reporter: + $ref: '#/components/schemas/User' + description: 'Пользователь, отправивший жалобу' + reportedUser: + $ref: '#/components/schemas/User' + description: 'Пользователь, на которого пожаловались' + reportDateTime: + type: string + format: date-time + example: '2024-05-20T16:45:00Z' + description: 'Дата и время жалобы' + reason: + type: string + example: 'Непристойный контент' + description: 'непристойный никнейм' + status: + type: string + enum: ['На рассмотрении', 'Подтверждено', 'Отклонено'] + example: 'На расмотрении' + description: 'Статус рассмотрения' + required: + - report_ID + - reporter + - reportedUser + - reportDateTime + - reason + - status + + Pack: + type: object + properties: + pack_ID: + type: integer + example: 401 + description: 'Уникальный идентификатор набора' + name: + type: string + example: 'Стартовый набор' + description: 'Название набора' + imageURL: + type: string + format: url + example: 'https://example.com/packs/starter.png' + description: 'Ссылка на изображение' + cards: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Карты в наборе' + price: + type: integer + example: 1000 + description: 'Цена' + required: + - pack_ID + - name + - imageURL + - cards + - price + + Card: + type: object + properties: + card_ID: + type: integer + example: 501 + description: 'Уникальный идентификатор карты' + name: + type: string + example: 'Ледяной феникс' + description: 'Название карты' + imageURL: + type: string + format: url + example: 'https://example.com/cards/pheonix.png' + description: 'Ссылка на изображение' + description: + type: string + example: 'Ледяной Феникс — это мифическое существо, воплощающее силу зимы и вечного обновления. Его перья сверкают как морозный утренний иней, а глаза светятся холодным синим светом.' + description: 'Описание карты' + rarity: + type: string + enum: ['Обычная', 'Редкая', 'Эпическая', 'Легендарная', 'Уникальная'] + example: 'Редкая' + description: 'Редкость карты' + min_price: + type: integer + example: 50 + description: 'Минимальная цена - используется для разбора карточки' + theme: + type: string + nullable: true + example: 'Мифическое существо' + description: 'Тематика, на которую была сгенерирвана карта (только для уникальных карт)' + isGenerated: + type: boolean + example: false + description: 'Флаг показывающий сгенерированная ли карта (true для уникальных, false для остальных)' + required: + - card_ID + - name + - imageURL + - rarity + - min_price + - isGenerated + + User: + type: object + properties: + user_id: + type: integer + example: 1 + description: 'Уникальный идентификатор' + username: + type: string + example: 'Коллекционер_123' + description: 'Имя пользователя' + email: + type: string + format: email + example: 'user@example.com' + description: 'Электронная почта' + password: + type: string + format: password + writeOnly: true + description: 'Пароль' + favoriteCards: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Избранные карты' + onChange: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Карты на обмен' + inventoryCards: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Карты в инвентаре' + balance: + type: integer + example: 2500 + description: 'Баланс' + avatar_url: + type: string + format: url + example: 'https://example.com/avatars/user1.png' + description: 'Аватар пользователя' + achievements: + type: array + items: + $ref: '#/components/schemas/Achievement' + description: 'Достижения' + favoriteAchievements: + type: array + items: + $ref: '#/components/schemas/Achievement' + description: 'Избранные достижения' + notifications: + type: array + items: + $ref: '#/components/schemas/Notification' + description: 'Уведомления' + required: + - user_id + - username + - email + - password + - balance + - inventoryCards + - achievements + + Payment: + type: object + properties: + payment_ID: + type: integer + example: 601 + description: 'Уникальный идентификатор платежа' + user: + $ref: '#/components/schemas/User' + description: 'Пользователь' + paymentSUM: + type: number + format: double + example: 199.99 + description: 'Сумма платежа' + bankCardData: + type: array + items: + type: string + writeOnly: true + description: 'Данные карты (только для записи)' + paymentStatus: + type: string + enum: ['В обработке', 'Оплачено', 'Ошибка'] + example: 'Оплачено' + description: 'Статус платежа' + paymentDateTime: + type: string + format: date-time + example: '2024-05-20T17:30:00Z' + description: 'Дата и время платежа' + required: + - payment_ID + - user + - paymentSUM + - paymentStatus + - paymentDateTime + + Trade: + type: object + properties: + trade_ID: + type: integer + example: 701 + description: 'Уникальный идентификатор обмена' + offeringUser: + $ref: '#/components/schemas/User' + description: 'Пользователь, предлагающий обмен' + offeringCards: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Предлагаемые карты' + receivingUser: + $ref: '#/components/schemas/User' + description: 'Пользователь, получающий предложение' + receivingCards: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Запрашиваемые карты' + isConfirmed: + type: boolean + example: false + description: 'Статус подтверждения' + tradeDateTime: + type: string + format: date-time + example: '2024-05-20T18:00:00Z' + description: 'Дата и время обмена' + required: + - trade_ID + - offeringUser + - offeringCards + - receivingUser + - receivingCards + - isConfirmed + - tradeDateTime + + Collection: + type: object + properties: + collection_ID: + type: integer + example: 801 + description: 'Уникальный идентификатор коллекции' + name: + type: string + example: 'Драконы' + description: 'Название коллекции' + cards: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Карты в коллекции' + imageURL: + type: string + format: url + example: 'https://example.com/collections/dragons.png' + description: 'Обложка коллекции' + required: + - collection_ID + - name + - cards + - imageURL + + Theme: + type: object + properties: + theme_ID: + type: integer + example: 901 + description: 'Уникальный идентификатор темы' + name: + type: string + example: 'Мифические существа' + description: 'Название темы' + description: + type: string + example: 'Создайте изображение мифического существа' + description: 'Описание темы' + required: + - theme_ID + - name + + UserStats: + type: object + properties: + totalCards: + type: integer + example: 150 + completedCollections: + type: integer + example: 5 + + CoinOffer: + type: object + properties: + offer_id: + type: integer + example: 1 + name: + type: string + example: 'Стартовый набор' + coinsAmount: + type: integer + example: 100 + price: + type: number + format: double + example: 99.90 + imageUrl: + type: string + format: uri + description: + type: string + nullable: true + example: 'Стартовый набор. Вы можете получить...' + + PaymentCallback: + type: object + properties: + transactionId: + type: string + status: + type: string + enum: ['Успешно', 'Ошибка', 'В процессе'] + amount: + type: number + currency: + type: string + offerId: + type: integer + required: + - transactionId + - status + - amount + + UserSettings: + type: object + properties: + notificationsEnabled: + type: boolean + example: true + description: 'Включены ли уведомления' + showInventory: + type: boolean + example: false + description: 'Виден ли инвентарь другим пользователям' + autoDeclineTrades: + type: boolean + example: false + description: 'Автоматически отклонять все входящие предложения обмена' + required: + - notificationsEnabled + - showInventory + - autoDeclineTrades + + Quest: + type: object + properties: + quest_ID: + type: integer + example: 1 + description: 'Уникальный идентификатор квеста' + name: + type: string + example: 'Соберите 5 карт' + description: 'Название квеста' + description: + type: string + example: 'Соберите 5 карт из коллекции Драконы' + description: 'Описание квеста' + progress: + type: integer + example: 3 + description: 'Текущий прогресс' + target: + type: integer + example: 5 + description: 'Целевое значение' + rewardCoins: + type: integer + example: 200 + description: 'Награда в монетах' + rewardPacks: + type: array + items: + $ref: '#/components/schemas/Pack' + description: 'Награда в наборах' + isCompleted: + type: boolean + example: false + description: 'Выполнен ли квест' + isClaimed: + type: boolean + example: false + description: 'Получена ли награда' + required: + - quest_ID + - name + - description + - progress + - target + - rewardCoins + - isCompleted + - isClaimed + + responses: + BadRequest: + description: Неверный формат запроса + + Unauthorized: + description: Ошибка аутентификации + + ServerError: + description: Что-то пошло не так + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Что-то пошло не так, попробуйте позже' + code: + type: string + example: 'INTERNAL_SERVER_ERROR' + timestamp: + type: string + format: date-time + example: '2024-05-20T12:00:00Z' + + InvalidToken: + description: Невалидный токен + + Forbidden: + description: Доступ запрещен + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/backend/src/test/kotlin/ru/vsu/app/api/AdminApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/AdminApiTest.kt new file mode 100644 index 0000000..4919a86 --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/AdminApiTest.kt @@ -0,0 +1,470 @@ +package org.openapitools.api + +import org.openapitools.model.Achievement +import org.openapitools.model.AdminReportsReportIDPutRequest +import org.openapitools.model.AdminStatsGet200Response +import org.openapitools.model.AdminTradesTradeIDInvalidatePost200Response +import org.openapitools.model.AdminTradesTradeIDInvalidatePostRequest +import org.openapitools.model.AdminUsersUserIDBanPost200Response +import org.openapitools.model.AdminUsersUserIDBanPostRequest +import org.openapitools.model.AdminUsersUserIDQuestsQuestIDResetPost200Response +import org.openapitools.model.AdminUsersUserIDQuestsQuestIDResetPostRequest +import org.openapitools.model.AdminUsersUserIDUnbanPost200Response +import org.openapitools.model.Card +import org.openapitools.model.CoinOffer +import org.openapitools.model.Collection +import org.openapitools.model.News +import org.openapitools.model.Pack +import org.openapitools.model.InternalServerError +import org.openapitools.model.Report +import org.openapitools.model.Trade +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class AdminApiTest { + + private val api: AdminApiController = AdminApiController() + + /** + * To test AdminApiController.adminAchievementsAchievementIDDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminAchievementsAchievementIDDeleteTest() { + val achievementID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminAchievementsAchievementIDDelete(achievementID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminAchievementsAchievementIDPut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminAchievementsAchievementIDPutTest() { + val achievementID: kotlin.Int = TODO() + val achievement: Achievement = TODO() + val response: ResponseEntity = api.adminAchievementsAchievementIDPut(achievementID, achievement) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminAchievementsPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminAchievementsPostTest() { + val achievement: Achievement = TODO() + val response: ResponseEntity = api.adminAchievementsPost(achievement) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCardsCardIDDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCardsCardIDDeleteTest() { + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminCardsCardIDDelete(cardID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCardsCardIDPut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCardsCardIDPutTest() { + val cardID: kotlin.Int = TODO() + val card: Card = TODO() + val response: ResponseEntity = api.adminCardsCardIDPut(cardID, card) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCardsPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCardsPostTest() { + val card: Card = TODO() + val response: ResponseEntity = api.adminCardsPost(card) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCoinOffersGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCoinOffersGetTest() { + val response: ResponseEntity = api.adminCoinOffersGet() + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCoinOffersOfferIdDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCoinOffersOfferIdDeleteTest() { + val offerId: kotlin.Int = TODO() + val response: ResponseEntity = api.adminCoinOffersOfferIdDelete(offerId) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCoinOffersOfferIdPut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCoinOffersOfferIdPutTest() { + val offerId: kotlin.Int = TODO() + val coinOffer: CoinOffer = TODO() + val response: ResponseEntity = api.adminCoinOffersOfferIdPut(offerId, coinOffer) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCoinOffersPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCoinOffersPostTest() { + val coinOffer: CoinOffer = TODO() + val response: ResponseEntity = api.adminCoinOffersPost(coinOffer) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCollectionsCollectionIDDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCollectionsCollectionIDDeleteTest() { + val collectionID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminCollectionsCollectionIDDelete(collectionID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCollectionsCollectionIDPut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCollectionsCollectionIDPutTest() { + val collectionID: kotlin.Int = TODO() + val collection: Collection = TODO() + val response: ResponseEntity = api.adminCollectionsCollectionIDPut(collectionID, collection) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCollectionsPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCollectionsPostTest() { + val collection: Collection = TODO() + val response: ResponseEntity = api.adminCollectionsPost(collection) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminNewsNewsIDDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminNewsNewsIDDeleteTest() { + val newsID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminNewsNewsIDDelete(newsID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminNewsNewsIDPut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminNewsNewsIDPutTest() { + val newsID: kotlin.Int = TODO() + val news: News = TODO() + val response: ResponseEntity = api.adminNewsNewsIDPut(newsID, news) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminNewsPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminNewsPostTest() { + val news: News = TODO() + val response: ResponseEntity = api.adminNewsPost(news) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminPacksPackIDDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminPacksPackIDDeleteTest() { + val packID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminPacksPackIDDelete(packID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminPacksPackIDPut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminPacksPackIDPutTest() { + val packID: kotlin.Int = TODO() + val pack: Pack = TODO() + val response: ResponseEntity = api.adminPacksPackIDPut(packID, pack) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminPacksPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminPacksPostTest() { + val pack: Pack = TODO() + val response: ResponseEntity = api.adminPacksPost(pack) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminReportsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminReportsGetTest() { + val response: ResponseEntity = api.adminReportsGet() + + // TODO: test validations + } + + /** + * To test AdminApiController.adminReportsReportIDGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminReportsReportIDGetTest() { + val reportID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminReportsReportIDGet(reportID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminReportsReportIDPut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminReportsReportIDPutTest() { + val reportID: kotlin.Int = TODO() + val adminReportsReportIDPutRequest: AdminReportsReportIDPutRequest = TODO() + val response: ResponseEntity = api.adminReportsReportIDPut(reportID, adminReportsReportIDPutRequest) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminStatsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminStatsGetTest() { + val response: ResponseEntity = api.adminStatsGet() + + // TODO: test validations + } + + /** + * To test AdminApiController.adminTradesGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminTradesGetTest() { + val response: ResponseEntity = api.adminTradesGet() + + // TODO: test validations + } + + /** + * To test AdminApiController.adminTradesTradeIDInvalidatePost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminTradesTradeIDInvalidatePostTest() { + val tradeID: kotlin.Int = TODO() + val adminTradesTradeIDInvalidatePostRequest: AdminTradesTradeIDInvalidatePostRequest = TODO() + val response: ResponseEntity = api.adminTradesTradeIDInvalidatePost(tradeID, adminTradesTradeIDInvalidatePostRequest) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminUsersUserIDAchievementsAchievementIDDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminUsersUserIDAchievementsAchievementIDDeleteTest() { + val userID: kotlin.Int = TODO() + val achievementID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminUsersUserIDAchievementsAchievementIDDelete(userID, achievementID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminUsersUserIDBanPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminUsersUserIDBanPostTest() { + val userID: kotlin.Int = TODO() + val adminUsersUserIDBanPostRequest: AdminUsersUserIDBanPostRequest = TODO() + val response: ResponseEntity = api.adminUsersUserIDBanPost(userID, adminUsersUserIDBanPostRequest) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminUsersUserIDDeleteDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminUsersUserIDDeleteDeleteTest() { + val userID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminUsersUserIDDeleteDelete(userID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminUsersUserIDInventoryCardIDDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminUsersUserIDInventoryCardIDDeleteTest() { + val userID: kotlin.Int = TODO() + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminUsersUserIDInventoryCardIDDelete(userID, cardID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminUsersUserIDQuestsQuestIDResetPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminUsersUserIDQuestsQuestIDResetPostTest() { + val userID: kotlin.Int = TODO() + val questID: kotlin.Int = TODO() + val adminUsersUserIDQuestsQuestIDResetPostRequest: AdminUsersUserIDQuestsQuestIDResetPostRequest = TODO() + val response: ResponseEntity = api.adminUsersUserIDQuestsQuestIDResetPost(userID, questID, adminUsersUserIDQuestsQuestIDResetPostRequest) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminUsersUserIDUnbanPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminUsersUserIDUnbanPostTest() { + val userID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminUsersUserIDUnbanPost(userID) + + // TODO: test validations + } +} diff --git a/backend/src/test/kotlin/ru/vsu/app/api/AuthApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/AuthApiTest.kt new file mode 100644 index 0000000..d5b5b90 --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/AuthApiTest.kt @@ -0,0 +1,154 @@ +package org.openapitools.api + +import org.openapitools.model.AuthCheckGet401Response +import org.openapitools.model.LoginUser400Response +import org.openapitools.model.LoginUser401Response +import org.openapitools.model.LoginUserRequest +import org.openapitools.model.RegisterUser200Response +import org.openapitools.model.RegisterUser400Response +import org.openapitools.model.RegisterUser409Response +import org.openapitools.model.InternalServerError +import org.openapitools.model.RegisterUserRequest +import org.openapitools.model.RequestPasswordReset200Response +import org.openapitools.model.RequestPasswordReset400Response +import org.openapitools.model.RequestPasswordReset404Response +import org.openapitools.model.RequestPasswordResetRequest +import org.openapitools.model.ResendVerificationCode200Response +import org.openapitools.model.ResendVerificationCodeRequest +import org.openapitools.model.ResetPassword200Response +import org.openapitools.model.ResetPassword400Response +import org.openapitools.model.ResetPasswordRequest +import org.openapitools.model.User +import org.openapitools.model.VerifyEmail400Response +import org.openapitools.model.VerifyEmail410Response +import org.openapitools.model.VerifyEmailRequest +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class AuthApiTest { + + private val api: AuthApiController = AuthApiController() + + /** + * To test AuthApiController.authCheckGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun authCheckGetTest() { + val response: ResponseEntity = api.authCheckGet() + + // TODO: test validations + } + + /** + * To test AuthApiController.authLogoutPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun authLogoutPostTest() { + val response: ResponseEntity = api.authLogoutPost() + + // TODO: test validations + } + + /** + * To test AuthApiController.authRefreshPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun authRefreshPostTest() { + val response: ResponseEntity = api.authRefreshPost() + + // TODO: test validations + } + + /** + * To test AuthApiController.loginUser + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun loginUserTest() { + val loginUserRequest: LoginUserRequest = TODO() + val response: ResponseEntity = api.loginUser(loginUserRequest) + + // TODO: test validations + } + + /** + * To test AuthApiController.registerUser + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun registerUserTest() { + val registerUserRequest: RegisterUserRequest = TODO() + val response: ResponseEntity = api.registerUser(registerUserRequest) + + // TODO: test validations + } + + /** + * To test AuthApiController.requestPasswordReset + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun requestPasswordResetTest() { + val requestPasswordResetRequest: RequestPasswordResetRequest = TODO() + val response: ResponseEntity = api.requestPasswordReset(requestPasswordResetRequest) + + // TODO: test validations + } + + /** + * To test AuthApiController.resendVerificationCode + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun resendVerificationCodeTest() { + val resendVerificationCodeRequest: ResendVerificationCodeRequest = TODO() + val response: ResponseEntity = api.resendVerificationCode(resendVerificationCodeRequest) + + // TODO: test validations + } + + /** + * To test AuthApiController.resetPassword + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun resetPasswordTest() { + val resetPasswordRequest: ResetPasswordRequest = TODO() + val response: ResponseEntity = api.resetPassword(resetPasswordRequest) + + // TODO: test validations + } + + /** + * To test AuthApiController.verifyEmail + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun verifyEmailTest() { + val verifyEmailRequest: VerifyEmailRequest = TODO() + val response: ResponseEntity = api.verifyEmail(verifyEmailRequest) + + // TODO: test validations + } +} diff --git a/backend/src/test/kotlin/ru/vsu/app/api/HomeApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/HomeApiTest.kt new file mode 100644 index 0000000..1c28168 --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/HomeApiTest.kt @@ -0,0 +1,177 @@ +package org.openapitools.api + +import org.openapitools.model.HomeGenerateCardPost200Response +import org.openapitools.model.HomeGenerateCardPost402Response +import org.openapitools.model.HomeGenerateCardPostRequest +import org.openapitools.model.HomeNewsGet404Response +import org.openapitools.model.HomeNotificationsNotificationIDNavigatePost200Response +import org.openapitools.model.HomeNotificationsNotificationIDNavigatePost404Response +import org.openapitools.model.HomeQuestsClaimRewardPost200Response +import org.openapitools.model.HomeQuestsClaimRewardPost400Response +import org.openapitools.model.HomeQuestsClaimRewardPostRequest +import org.openapitools.model.HomeQuestsGet200Response +import org.openapitools.model.HomeQuestsQuestIDChangeStatusPost200Response +import org.openapitools.model.HomeSearchGet400Response +import org.openapitools.model.HomeSearchGet404Response +import org.openapitools.model.News +import org.openapitools.model.Notification +import org.openapitools.model.InternalServerError +import org.openapitools.model.Theme +import org.openapitools.model.User +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class HomeApiTest { + + private val api: HomeApiController = HomeApiController() + + /** + * To test HomeApiController.homeGenerateCardPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeGenerateCardPostTest() { + val homeGenerateCardPostRequest: HomeGenerateCardPostRequest = TODO() + val response: ResponseEntity = api.homeGenerateCardPost(homeGenerateCardPostRequest) + + // TODO: test validations + } + + /** + * To test HomeApiController.homeGenerateCardThemesGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeGenerateCardThemesGetTest() { + val response: ResponseEntity = api.homeGenerateCardThemesGet() + + // TODO: test validations + } + + /** + * To test HomeApiController.homeNewsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeNewsGetTest() { + val response: ResponseEntity = api.homeNewsGet() + + // TODO: test validations + } + + /** + * To test HomeApiController.homeNewsNewsIDGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeNewsNewsIDGetTest() { + val newsID: kotlin.Int = TODO() + val response: ResponseEntity = api.homeNewsNewsIDGet(newsID) + + // TODO: test validations + } + + /** + * To test HomeApiController.homeNotificationsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeNotificationsGetTest() { + val response: ResponseEntity = api.homeNotificationsGet() + + // TODO: test validations + } + + /** + * To test HomeApiController.homeNotificationsNotificationIDGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeNotificationsNotificationIDGetTest() { + val notificationID: kotlin.Int = TODO() + val response: ResponseEntity = api.homeNotificationsNotificationIDGet(notificationID) + + // TODO: test validations + } + + /** + * To test HomeApiController.homeNotificationsNotificationIDNavigatePost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeNotificationsNotificationIDNavigatePostTest() { + val notificationID: kotlin.Int = TODO() + val response: ResponseEntity = api.homeNotificationsNotificationIDNavigatePost(notificationID) + + // TODO: test validations + } + + /** + * To test HomeApiController.homeQuestsClaimRewardPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeQuestsClaimRewardPostTest() { + val homeQuestsClaimRewardPostRequest: HomeQuestsClaimRewardPostRequest = TODO() + val response: ResponseEntity = api.homeQuestsClaimRewardPost(homeQuestsClaimRewardPostRequest) + + // TODO: test validations + } + + /** + * To test HomeApiController.homeQuestsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeQuestsGetTest() { + val response: ResponseEntity = api.homeQuestsGet() + + // TODO: test validations + } + + /** + * To test HomeApiController.homeQuestsQuestIDChangeStatusPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeQuestsQuestIDChangeStatusPostTest() { + val questID: kotlin.Int = TODO() + val response: ResponseEntity = api.homeQuestsQuestIDChangeStatusPost(questID) + + // TODO: test validations + } + + /** + * To test HomeApiController.homeSearchGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeSearchGetTest() { + val query: kotlin.String? = TODO() + val response: ResponseEntity = api.homeSearchGet(query) + + // TODO: test validations + } +} diff --git a/backend/src/test/kotlin/ru/vsu/app/api/InventoryApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/InventoryApiTest.kt new file mode 100644 index 0000000..35b405b --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/InventoryApiTest.kt @@ -0,0 +1,173 @@ +package org.openapitools.api + +import org.openapitools.model.InventoryCardCardIDFavoriteGet200Response +import org.openapitools.model.InventoryCardCardIDQuantityGet200Response +import org.openapitools.model.InventoryCardCardIDTradeCancelPost200Response +import org.openapitools.model.InventoryCardCardIDTradeStatusGet200Response +import org.openapitools.model.InventoryDestroyPost200Response +import org.openapitools.model.InventoryDestroyPostRequest +import org.openapitools.model.InventoryFavoritesCountGet200Response +import org.openapitools.model.InventoryGet200Response +import org.openapitools.model.InventorySortPost200Response +import org.openapitools.model.OtherProfileCardCardIDInitiateTradePost200Response +import org.openapitools.model.OtherProfileInventorySortPostRequest +import org.openapitools.model.InternalServerError +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class InventoryApiTest { + + private val api: InventoryApiController = InventoryApiController() + + /** + * To test InventoryApiController.inventoryCardCardIDFavoriteAddPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryCardCardIDFavoriteAddPostTest() { + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.inventoryCardCardIDFavoriteAddPost(cardID) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryCardCardIDFavoriteDeleteDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryCardCardIDFavoriteDeleteDeleteTest() { + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.inventoryCardCardIDFavoriteDeleteDelete(cardID) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryCardCardIDFavoriteGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryCardCardIDFavoriteGetTest() { + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.inventoryCardCardIDFavoriteGet(cardID) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryCardCardIDQuantityGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryCardCardIDQuantityGetTest() { + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.inventoryCardCardIDQuantityGet(cardID) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryCardCardIDTradeCancelPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryCardCardIDTradeCancelPostTest() { + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.inventoryCardCardIDTradeCancelPost(cardID) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryCardCardIDTradeStatusGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryCardCardIDTradeStatusGetTest() { + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.inventoryCardCardIDTradeStatusGet(cardID) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryDestroyPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryDestroyPostTest() { + val inventoryDestroyPostRequest: InventoryDestroyPostRequest = TODO() + val response: ResponseEntity = api.inventoryDestroyPost(inventoryDestroyPostRequest) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryFavoritesCountGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryFavoritesCountGetTest() { + val response: ResponseEntity = api.inventoryFavoritesCountGet() + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryGetTest() { + val response: ResponseEntity = api.inventoryGet() + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryPutOnTradePost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryPutOnTradePostTest() { + val inventoryDestroyPostRequest: InventoryDestroyPostRequest = TODO() + val response: ResponseEntity = api.inventoryPutOnTradePost(inventoryDestroyPostRequest) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventorySortPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventorySortPostTest() { + val otherProfileInventorySortPostRequest: OtherProfileInventorySortPostRequest = TODO() + val response: ResponseEntity = api.inventorySortPost(otherProfileInventorySortPostRequest) + + // TODO: test validations + } +} diff --git a/backend/src/test/kotlin/ru/vsu/app/api/OtherProfileApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/OtherProfileApiTest.kt new file mode 100644 index 0000000..3f1515f --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/OtherProfileApiTest.kt @@ -0,0 +1,124 @@ +package org.openapitools.api + +import org.openapitools.model.OtherProfileCardCardIDInitiateTradePost200Response +import org.openapitools.model.OtherProfileCardCardIDInitiateTradePost400Response +import org.openapitools.model.OtherProfileCardCardIDInitiateTradePost409Response +import org.openapitools.model.OtherProfileCardCardIDViewGet200Response +import org.openapitools.model.OtherProfileInventorySortPost200Response +import org.openapitools.model.OtherProfileInventorySortPostRequest +import org.openapitools.model.OtherProfileUserIDGet200Response +import org.openapitools.model.OtherProfileUserIDInventoryGet200Response +import org.openapitools.model.OtherProfileUserIDInventoryGet403Response +import org.openapitools.model.OtherProfileUserIDReportPost200Response +import org.openapitools.model.OtherProfileUserIDReportPostRequest +import org.openapitools.model.ProfileAchievementsGet200Response +import org.openapitools.model.InternalServerError +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class OtherProfileApiTest { + + private val api: OtherProfileApiController = OtherProfileApiController() + + /** + * To test OtherProfileApiController.otherProfileCardCardIDInitiateTradePost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun otherProfileCardCardIDInitiateTradePostTest() { + val cardID: kotlin.Int = TODO() + val ownerID: kotlin.Int = TODO() + val response: ResponseEntity = api.otherProfileCardCardIDInitiateTradePost(cardID, ownerID) + + // TODO: test validations + } + + /** + * To test OtherProfileApiController.otherProfileCardCardIDViewGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun otherProfileCardCardIDViewGetTest() { + val cardID: kotlin.Int = TODO() + val ownerID: kotlin.Int = TODO() + val response: ResponseEntity = api.otherProfileCardCardIDViewGet(cardID, ownerID) + + // TODO: test validations + } + + /** + * To test OtherProfileApiController.otherProfileInventorySortPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun otherProfileInventorySortPostTest() { + val userID: kotlin.Int = TODO() + val otherProfileInventorySortPostRequest: OtherProfileInventorySortPostRequest = TODO() + val response: ResponseEntity = api.otherProfileInventorySortPost(userID, otherProfileInventorySortPostRequest) + + // TODO: test validations + } + + /** + * To test OtherProfileApiController.otherProfileUserIDAchievementsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun otherProfileUserIDAchievementsGetTest() { + val userID: kotlin.Int = TODO() + val response: ResponseEntity = api.otherProfileUserIDAchievementsGet(userID) + + // TODO: test validations + } + + /** + * To test OtherProfileApiController.otherProfileUserIDGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun otherProfileUserIDGetTest() { + val userID: kotlin.Int = TODO() + val response: ResponseEntity = api.otherProfileUserIDGet(userID) + + // TODO: test validations + } + + /** + * To test OtherProfileApiController.otherProfileUserIDInventoryGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun otherProfileUserIDInventoryGetTest() { + val userID: kotlin.Int = TODO() + val response: ResponseEntity = api.otherProfileUserIDInventoryGet(userID) + + // TODO: test validations + } + + /** + * To test OtherProfileApiController.otherProfileUserIDReportPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun otherProfileUserIDReportPostTest() { + val userID: kotlin.Int = TODO() + val otherProfileUserIDReportPostRequest: OtherProfileUserIDReportPostRequest = TODO() + val response: ResponseEntity = api.otherProfileUserIDReportPost(userID, otherProfileUserIDReportPostRequest) + + // TODO: test validations + } +} diff --git a/backend/src/test/kotlin/ru/vsu/app/api/ProfileApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/ProfileApiTest.kt new file mode 100644 index 0000000..c305c87 --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/ProfileApiTest.kt @@ -0,0 +1,108 @@ +package org.openapitools.api + +import org.openapitools.model.ProfileAchievementsFavoritesCountGet200Response +import org.openapitools.model.ProfileAchievementsGet200Response +import org.openapitools.model.ProfileGet200Response +import org.openapitools.model.InternalServerError +import org.openapitools.model.UserSettings +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class ProfileApiTest { + + private val api: ProfileApiController = ProfileApiController() + + /** + * To test ProfileApiController.profileAchievementsAchievementIDFavoriteAddPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun profileAchievementsAchievementIDFavoriteAddPostTest() { + val achievementID: kotlin.Int = TODO() + val response: ResponseEntity = api.profileAchievementsAchievementIDFavoriteAddPost(achievementID) + + // TODO: test validations + } + + /** + * To test ProfileApiController.profileAchievementsAchievementIDFavoriteDeleteDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun profileAchievementsAchievementIDFavoriteDeleteDeleteTest() { + val achievementID: kotlin.Int = TODO() + val response: ResponseEntity = api.profileAchievementsAchievementIDFavoriteDeleteDelete(achievementID) + + // TODO: test validations + } + + /** + * To test ProfileApiController.profileAchievementsFavoritesCountGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun profileAchievementsFavoritesCountGetTest() { + val response: ResponseEntity = api.profileAchievementsFavoritesCountGet() + + // TODO: test validations + } + + /** + * To test ProfileApiController.profileAchievementsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun profileAchievementsGetTest() { + val response: ResponseEntity = api.profileAchievementsGet() + + // TODO: test validations + } + + /** + * To test ProfileApiController.profileGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun profileGetTest() { + val response: ResponseEntity = api.profileGet() + + // TODO: test validations + } + + /** + * To test ProfileApiController.profileSettingsChangePut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun profileSettingsChangePutTest() { + val userSettings: UserSettings = TODO() + val response: ResponseEntity = api.profileSettingsChangePut(userSettings) + + // TODO: test validations + } + + /** + * To test ProfileApiController.profileSettingsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun profileSettingsGetTest() { + val response: ResponseEntity = api.profileSettingsGet() + + // TODO: test validations + } +} diff --git a/backend/src/test/kotlin/ru/vsu/app/api/ShopApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/ShopApiTest.kt new file mode 100644 index 0000000..4e1635f --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/ShopApiTest.kt @@ -0,0 +1,130 @@ +package org.openapitools.api + +import org.openapitools.model.CoinOffer +import org.openapitools.model.Pack +import org.openapitools.model.PaymentCallback +import org.openapitools.model.InternalServerError +import org.openapitools.model.ShopCoinsOffersOfferIdPurchasePost200Response +import org.openapitools.model.ShopCoinsOffersOfferIdPurchasePostRequest +import org.openapitools.model.ShopPacksPackIdBuyPost200Response +import org.openapitools.model.ShopPacksPackIdBuyPost402Response +import org.openapitools.model.ShopPacksPackIdOpenPost200Response +import org.openapitools.model.ShopPaymentsProcessPost200Response +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class ShopApiTest { + + private val api: ShopApiController = ShopApiController() + + /** + * To test ShopApiController.shopCoinsOffersGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopCoinsOffersGetTest() { + val response: ResponseEntity = api.shopCoinsOffersGet() + + // TODO: test validations + } + + /** + * To test ShopApiController.shopCoinsOffersOfferIdGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopCoinsOffersOfferIdGetTest() { + val offerId: kotlin.Int = TODO() + val response: ResponseEntity = api.shopCoinsOffersOfferIdGet(offerId) + + // TODO: test validations + } + + /** + * To test ShopApiController.shopCoinsOffersOfferIdPurchasePost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopCoinsOffersOfferIdPurchasePostTest() { + val offerId: kotlin.Int = TODO() + val shopCoinsOffersOfferIdPurchasePostRequest: ShopCoinsOffersOfferIdPurchasePostRequest = TODO() + val response: ResponseEntity = api.shopCoinsOffersOfferIdPurchasePost(offerId, shopCoinsOffersOfferIdPurchasePostRequest) + + // TODO: test validations + } + + /** + * To test ShopApiController.shopPacksGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopPacksGetTest() { + val response: ResponseEntity = api.shopPacksGet() + + // TODO: test validations + } + + /** + * To test ShopApiController.shopPacksPackIdBuyPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopPacksPackIdBuyPostTest() { + val packId: kotlin.Int = TODO() + val response: ResponseEntity = api.shopPacksPackIdBuyPost(packId) + + // TODO: test validations + } + + /** + * To test ShopApiController.shopPacksPackIdGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopPacksPackIdGetTest() { + val packId: kotlin.Int = TODO() + val response: ResponseEntity = api.shopPacksPackIdGet(packId) + + // TODO: test validations + } + + /** + * To test ShopApiController.shopPacksPackIdOpenPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopPacksPackIdOpenPostTest() { + val packId: kotlin.Int = TODO() + val response: ResponseEntity = api.shopPacksPackIdOpenPost(packId) + + // TODO: test validations + } + + /** + * To test ShopApiController.shopPaymentsProcessPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopPaymentsProcessPostTest() { + val paymentCallback: PaymentCallback = TODO() + val response: ResponseEntity = api.shopPaymentsProcessPost(paymentCallback) + + // TODO: test validations + } +} diff --git a/backend/src/test/kotlin/ru/vsu/app/api/TradesApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/TradesApiTest.kt new file mode 100644 index 0000000..60b7502 --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/TradesApiTest.kt @@ -0,0 +1,116 @@ +package org.openapitools.api + +import org.openapitools.model.OtherProfileCardCardIDInitiateTradePost400Response +import org.openapitools.model.InternalServerError +import org.openapitools.model.Trade +import org.openapitools.model.TradesInitiatePostRequest +import org.openapitools.model.TradesTradeIdAcceptPost200Response +import org.openapitools.model.TradesTradeIdAcceptPost403Response +import org.openapitools.model.TradesTradeIdCancelPost200Response +import org.openapitools.model.TradesTradeIdGet404Response +import org.openapitools.model.TradesTradeIdRejectPost200Response +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class TradesApiTest { + + private val api: TradesApiController = TradesApiController() + + /** + * To test TradesApiController.tradesGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun tradesGetTest() { + val search: kotlin.String? = TODO() + val response: ResponseEntity = api.tradesGet(search) + + // TODO: test validations + } + + /** + * To test TradesApiController.tradesInitiatePost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun tradesInitiatePostTest() { + val tradesInitiatePostRequest: TradesInitiatePostRequest = TODO() + val response: ResponseEntity = api.tradesInitiatePost(tradesInitiatePostRequest) + + // TODO: test validations + } + + /** + * To test TradesApiController.tradesMyGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun tradesMyGetTest() { + val userId: kotlin.Int = TODO() + val response: ResponseEntity = api.tradesMyGet(userId) + + // TODO: test validations + } + + /** + * To test TradesApiController.tradesTradeIdAcceptPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun tradesTradeIdAcceptPostTest() { + val tradeId: kotlin.Int = TODO() + val response: ResponseEntity = api.tradesTradeIdAcceptPost(tradeId) + + // TODO: test validations + } + + /** + * To test TradesApiController.tradesTradeIdCancelPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun tradesTradeIdCancelPostTest() { + val tradeId: kotlin.Int = TODO() + val response: ResponseEntity = api.tradesTradeIdCancelPost(tradeId) + + // TODO: test validations + } + + /** + * To test TradesApiController.tradesTradeIdGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun tradesTradeIdGetTest() { + val tradeId: kotlin.Int = TODO() + val response: ResponseEntity = api.tradesTradeIdGet(tradeId) + + // TODO: test validations + } + + /** + * To test TradesApiController.tradesTradeIdRejectPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun tradesTradeIdRejectPostTest() { + val tradeId: kotlin.Int = TODO() + val response: ResponseEntity = api.tradesTradeIdRejectPost(tradeId) + + // TODO: test validations + } +} From 75349c1cfa56c5f5f23daf30733d181bfd61cee2 Mon Sep 17 00:00:00 2001 From: birbik Date: Mon, 2 Jun 2025 17:07:36 +0300 Subject: [PATCH 40/54] FCCX-149 connect backend and frontend(registration) --- frontend/lib/controllers/auth_controller.dart | 63 ++-- frontend/lib/main.dart | 20 +- frontend/lib/services/api_service.dart | 47 ++- frontend/lib/views/auth_screen.dart | 12 +- .../lib/views/email_verification_screen.dart | 94 ++--- frontend/lib/views/login_screen.dart | 309 --------------- frontend/lib/views/registration_screen.dart | 352 ------------------ frontend/lib/views/splash_screen.dart | 44 +-- 8 files changed, 118 insertions(+), 823 deletions(-) delete mode 100644 frontend/lib/views/login_screen.dart delete mode 100644 frontend/lib/views/registration_screen.dart diff --git a/frontend/lib/controllers/auth_controller.dart b/frontend/lib/controllers/auth_controller.dart index 5b4ee60..9ff7de1 100644 --- a/frontend/lib/controllers/auth_controller.dart +++ b/frontend/lib/controllers/auth_controller.dart @@ -2,18 +2,21 @@ import 'package:flutter/material.dart'; import '../models/user_model.dart'; import '../services/api_service.dart'; import '../views/home_screen.dart'; +import '../views/email_verification_screen.dart'; +import '../main.dart'; class AuthController with ChangeNotifier { UserModel? _currentUser; bool _isLoading = false; String? _token; + String? _tempToken; final ApiService _apiService = ApiService(); UserModel? get currentUser => _currentUser; bool get isLoading => _isLoading; bool get isAuthenticated => _token != null; - static const String _baseUrl = 'http://87.236.23.130:8080/api'; + static const String _baseUrl = 'http://217.114.7.39:8080/api'; static const String _loginUrl = '$_baseUrl/auth/login'; static const String loginError = 'Неверный логин или пароль'; @@ -54,7 +57,7 @@ class AuthController with ChangeNotifier { if (context != null) { Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => const HomeScreen()), + MaterialPageRoute(builder: (context) => const MainScreen()), ); } } catch (e) { @@ -71,21 +74,28 @@ class AuthController with ChangeNotifier { notifyListeners(); try { - final success = await _apiService.register(email, username, password); + final response = await _apiService.register(email, username, password); - if (success) { - _currentUser = UserModel( - email: email, - username: username, - isEmailVerified: false, - ); - } + _tempToken = response['tempToken']; + await _apiService.saveToken(_tempToken!); + _currentUser = UserModel( + email: email, + username: username, + isEmailVerified: false, + ); _isLoading = false; notifyListeners(); - if (context != null && success) { - + if (context != null && _tempToken != null) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => EmailVerificationScreen( + email: email, + tempToken: _tempToken!, + ), + ), + ); } } catch (e) { _isLoading = false; @@ -96,34 +106,35 @@ class AuthController with ChangeNotifier { } } - Future verifyEmail(String email, String code, {BuildContext? context}) async { + Future verifyEmail(String tempToken, String code, {BuildContext? context}) async { _isLoading = true; notifyListeners(); try { - final success = await _apiService.activateAccount(email, code); + final userModel = await _apiService.activateAccount(tempToken, code); - if (success && _currentUser != null) { - _currentUser = UserModel( - email: _currentUser!.email, - username: _currentUser!.username, - token: _currentUser!.token, - isEmailVerified: true, - ); + _currentUser = userModel; + _token = userModel.token; + if (_token != null) { + await _apiService.saveToken(_token!); + } + _tempToken = null; + await _apiService.removeToken(); + if (_token != null) { + await _apiService.saveToken(_token!); } _isLoading = false; notifyListeners(); - if (context != null && success) { - + if (context != null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Email успешно подтвержден')), ); - if (_token == null) { - Navigator.of(context).popUntil((route) => route.isFirst); - } + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => const MainScreen()), + ); } } catch (e) { _isLoading = false; diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 9aa7526..fa86444 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -11,7 +11,8 @@ import 'views/inventory_screen.dart'; import 'views/shop_screen.dart'; import 'views/exchanges_screen.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); runApp(const MyApp()); } @@ -83,7 +84,22 @@ class MyApp extends StatelessWidget { elevation: 0, ), ), - home: const MainScreen(), + home: Builder( + builder: (context) { + return FutureBuilder( + future: Provider.of(context, listen: false).tryAutoLogin(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const SplashScreen(); + } + if (snapshot.hasError) { + return const AuthScreen(); + } + return snapshot.data == true ? const MainScreen() : const AuthScreen(); + }, + ); + }, + ), routes: { '/auth': (context) => const AuthScreen(), '/login': (context) => const AuthScreen(initialTabIndex: 0), diff --git a/frontend/lib/services/api_service.dart b/frontend/lib/services/api_service.dart index b2f91c3..1051a86 100644 --- a/frontend/lib/services/api_service.dart +++ b/frontend/lib/services/api_service.dart @@ -12,11 +12,11 @@ import '../models/notification_model.dart'; import '../models/trade_model.dart'; class ApiService { - static const String baseUrl = 'http://87.236.23.130:8080/api'; + static const String baseUrl = 'http://217.114.7.39:8080/api'; static const String loginUrl = '$baseUrl/auth/login'; static const String registerUrl = '$baseUrl/auth/register'; - static const String activateUrl = '$baseUrl/auth/activate'; - static const String resendActivationUrl = '$baseUrl/auth/resend-activation-code'; + static const String activateUrl = '$baseUrl/auth/verify'; + static const String resendActivationUrl = '$baseUrl/auth/resend-code'; static const String forgotPasswordUrl = '$baseUrl/auth/forgot-password'; static const String resetPasswordUrl = '$baseUrl/auth/reset-password'; @@ -81,7 +81,7 @@ class ApiService { } } - Future register(String email, String username, String password) async { + Future> register(String email, String username, String password) async { try { print('Отправка запроса на регистрацию: $registerUrl'); print('Данные запроса: email=$email, username=$username, password=${password.replaceAll(RegExp(r'.'), '*')}'); @@ -103,11 +103,11 @@ class ApiService { if (response.statusCode == 200 || response.statusCode == 201) { final responseData = json.decode(response.body); - final success = responseData['success'] ?? false; + final tempToken = responseData['tempToken']; final message = responseData['message'] ?? 'Пользователь успешно зарегистрирован'; - print('Результат регистрации: $message (успех: $success)'); - return success; + print('Результат регистрации: $message (tempToken: $tempToken)'); + return responseData; } else { final errorData = json.decode(response.body); final errorMessage = errorData['message'] ?? 'Ошибка регистрации. Попробуйте снова.'; @@ -136,18 +136,19 @@ class ApiService { await prefs.remove('auth_token'); } - Future activateAccount(String email, String code) async { + Future activateAccount(String tempToken, String code) async { try { print('Отправка запроса на активацию аккаунта: $activateUrl'); - print('Данные запроса: email=$email, code=$code'); + print('Данные запроса: tempToken=$tempToken, code=$code'); + + final headers = await getHeaders(); + headers['Content-Type'] = 'application/json'; final response = await http.post( Uri.parse(activateUrl), - headers: { - 'Content-Type': 'application/json', - }, + headers: headers, body: json.encode({ - 'email': email, + 'tempToken': tempToken, 'code': code, }), ); @@ -157,11 +158,16 @@ class ApiService { if (response.statusCode == 200) { final responseData = json.decode(response.body); - final success = responseData['success'] ?? false; - final message = responseData['message'] ?? 'Операция выполнена успешно'; + final userModel = UserModel.fromJson(responseData); - print('Результат активации: $message (успех: $success)'); - return success; + if (userModel.token != null) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('auth_token', userModel.token!); + print('Токен успешно сохранен'); + } + + print('Аккаунт успешно активирован для: ${userModel.email}'); + return userModel; } else { final errorData = json.decode(response.body); final errorMessage = errorData['message'] ?? 'Ошибка активации аккаунта. Попробуйте снова.'; @@ -179,11 +185,12 @@ class ApiService { print('Отправка запроса на повторную отправку кода: $resendActivationUrl'); print('Данные запроса: email=$email'); + final headers = await getHeaders(); + headers['Content-Type'] = 'application/json'; + final response = await http.post( Uri.parse(resendActivationUrl), - headers: { - 'Content-Type': 'application/json', - }, + headers: headers, body: json.encode({ 'email': email, }), diff --git a/frontend/lib/views/auth_screen.dart b/frontend/lib/views/auth_screen.dart index 154bd09..e9c9273 100644 --- a/frontend/lib/views/auth_screen.dart +++ b/frontend/lib/views/auth_screen.dart @@ -82,17 +82,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM context: context, ); - if (!mounted) return; - - // Переход на экран подтверждения email - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EmailVerificationScreen( - email: _registerEmailController.text.trim(), - ), - ), - ); + // Убираем навигацию отсюда, так как она теперь происходит в AuthController } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( diff --git a/frontend/lib/views/email_verification_screen.dart b/frontend/lib/views/email_verification_screen.dart index ef813df..2b98c35 100644 --- a/frontend/lib/views/email_verification_screen.dart +++ b/frontend/lib/views/email_verification_screen.dart @@ -5,10 +5,12 @@ import '../controllers/auth_controller.dart'; class EmailVerificationScreen extends StatefulWidget { final String email; + final String tempToken; const EmailVerificationScreen({ super.key, required this.email, + required this.tempToken, }); @override @@ -55,12 +57,12 @@ class _EmailVerificationScreenState extends State { if (_formKey.currentState!.validate()) { try { await Provider.of(context, listen: false) - .verifyEmail(widget.email, _codeController.text.trim(), context: context); + .verifyEmail(widget.tempToken, _codeController.text.trim(), context: context); } catch (e) { if (!mounted) return; - final errorText = e.toString().replaceAll('', ''); + final errorText = e.toString().replaceAll('Exception: ', ''); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -88,8 +90,7 @@ class _EmailVerificationScreenState extends State { } catch (e) { if (!mounted) return; - // Получаем текст ошибки без "Exception: " - final errorText = e.toString().replaceAll('', ''); + final errorText = e.toString().replaceAll('Exception: ', ''); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -106,6 +107,26 @@ class _EmailVerificationScreenState extends State { } } + String _obscureEmail(String email) { + final parts = email.split('@'); + if (parts.length != 2) return email; + + final name = parts[0]; + final domain = parts[1]; + + final obscuredName = name.length > 2 + ? '${name.substring(0, 2)}${'*' * (name.length - 2)}' + : name; + + return '$obscuredName@$domain'; + } + + String _formatTimeLeft(int seconds) { + final minutes = seconds ~/ 60; + final remainingSeconds = seconds % 60; + return '$minutes:${remainingSeconds.toString().padLeft(2, '0')}'; + } + @override Widget build(BuildContext context) { final authController = Provider.of(context); @@ -113,7 +134,7 @@ class _EmailVerificationScreenState extends State { final obscuredEmail = _obscureEmail(widget.email); return Scaffold( - + backgroundColor: const Color(0xFFFBF6EF), body: Column( children: [ const SizedBox(height: 40), @@ -171,7 +192,7 @@ class _EmailVerificationScreenState extends State { controller: _codeController, decoration: const InputDecoration( filled: true, - fillColor: Color(0xFFEDD6B0), + fillColor: Color(0xFFEAD7C3), border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(8)), @@ -185,6 +206,9 @@ class _EmailVerificationScreenState extends State { if (value == null || value.isEmpty) { return 'Пожалуйста, введите код'; } + if (value.length != 6) { + return 'Код должен состоять из 6 цифр'; + } return null; }, ), @@ -245,71 +269,13 @@ class _EmailVerificationScreenState extends State { ), ), ), - - const SizedBox(height: 16), - - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: const Text( - 'Назад', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - fontFamily: 'Roboto', - ), - ), - ), - ), ], ), ), ), ), - - Container( - width: 100, - height: 4, - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(2), - ), - ), ], ), ); } - - String _obscureEmail(String email) { - if (email.isEmpty) return ''; - - final parts = email.split('@'); - if (parts.length != 2) return email; - - String username = parts[0]; - String domain = parts[1]; - - if (username.length <= 4) { - return '*' * username.length + '@' + domain; - } else { - return username.substring(0, 4) + '*' * (username.length - 4) + '@' + domain; - } - } - - String _formatTimeLeft(int seconds) { - int hours = seconds ~/ 3600; - int minutes = (seconds % 3600) ~/ 60; - int secs = seconds % 60; - return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}'; - } } \ No newline at end of file diff --git a/frontend/lib/views/login_screen.dart b/frontend/lib/views/login_screen.dart deleted file mode 100644 index 68ea4c8..0000000 --- a/frontend/lib/views/login_screen.dart +++ /dev/null @@ -1,309 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:email_validator/email_validator.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'registration_screen.dart'; -import 'forgot_password_screen.dart'; -import '../controllers/auth_controller.dart'; - -class LoginScreen extends StatefulWidget { - const LoginScreen({super.key}); - - @override - State createState() => _LoginScreenState(); -} - -class _LoginScreenState extends State with SingleTickerProviderStateMixin { - final _formKey = GlobalKey(); - final _emailController = TextEditingController(); - final _passwordController = TextEditingController(); - bool _obscurePassword = true; - - late TabController _tabController; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 2, vsync: this); - } - - @override - void dispose() { - _emailController.dispose(); - _passwordController.dispose(); - _tabController.dispose(); - super.dispose(); - } - - Future _login() async { - if (_formKey.currentState!.validate()) { - try { - await Provider.of(context, listen: false).login( - _emailController.text.trim(), - _passwordController.text, - context: context, - ); - } catch (e) { - if (!mounted) return; - - // Получаем текст ошибки без "Exception: " - final errorText = e.toString().replaceAll('', ''); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(errorText), - backgroundColor: Colors.red.shade800, - behavior: SnackBarBehavior.floating, - margin: const EdgeInsets.all(16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ); - } - } - } - - @override - Widget build(BuildContext context) { - final authController = Provider.of(context); - final isLoading = authController.isLoading; - - return Scaffold( - body: SafeArea( - child: Column( - children: [ - const SizedBox(height: 40), - Image.asset( - 'assets/icons/карты.png', - height: 80, - color: const Color(0xFFD9A76A), - ), - const SizedBox(height: 10), - const Text( - 'Cardly', - style: TextStyle( - fontSize: 24, - color: Color(0xFFD9A76A), - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - ), - const SizedBox(height: 30), - - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: TabBar( - controller: _tabController, - indicatorSize: TabBarIndicatorSize.tab, - indicator: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8), - ), - labelColor: Colors.black, - unselectedLabelColor: Colors.black, - labelStyle: const TextStyle( - fontFamily: 'Jost', - ), - unselectedLabelStyle: const TextStyle( - fontFamily: 'Jost', - ), - tabs: const [ - Tab(text: 'Войти'), - Tab(text: 'Создать'), - ], - ), - ), - - const SizedBox(height: 30), - - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Электронная почта', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - fontFamily: 'Jost', - ), - ), - const SizedBox(height: 8), - TextFormField( - controller: _emailController, - decoration: const InputDecoration( - filled: true, - fillColor: Color(0xFFEDD6B0), - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), - ), - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Пожалуйста, введите email'; - } - if (!EmailValidator.validate(value)) { - return 'Введите корректный email'; - } - return null; - }, - ), - - const SizedBox(height: 16), - - const Text( - 'Пароль', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - fontFamily: 'Jost', - ), - ), - const SizedBox(height: 8), - TextFormField( - controller: _passwordController, - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFFEDD6B0), - border: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - suffixIcon: IconButton( - icon: Icon( - _obscurePassword ? Icons.visibility_off : Icons.visibility, - color: Colors.black54, - ), - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - ), - ), - obscureText: _obscurePassword, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Пожалуйста, введите пароль'; - } - if (value.length < 8) { - return 'Пароль должен содержать минимум 8 символов'; - } - return null; - }, - ), - - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const ForgotPasswordScreen(), - ), - ); - }, - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - minimumSize: const Size(50, 30), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: const Text( - 'Забыли пароль?', - style: TextStyle( - fontSize: 14, - color: Colors.black, - fontFamily: 'Jost', - ), - ), - ), - ), - - const SizedBox(height: 40), - - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: isLoading ? null : _login, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - disabledBackgroundColor: const Color(0xFFD6A067).withOpacity(0.7), - ), - child: isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.black, - ), - ) - : const Text( - 'Вход', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - fontFamily: 'Jost', - ), - ), - ), - ), - ], - ), - ), - ), - - GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const RegistrationScreen(), - ), - ); - }, - child: const Center( - child: Text('Нажмите, чтобы создать новый аккаунт'), - ), - ), - ], - ), - ), - - Container( - width: 100, - height: 4, - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(2), - ), - ), - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/frontend/lib/views/registration_screen.dart b/frontend/lib/views/registration_screen.dart deleted file mode 100644 index ec0531f..0000000 --- a/frontend/lib/views/registration_screen.dart +++ /dev/null @@ -1,352 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:email_validator/email_validator.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import '../controllers/auth_controller.dart'; -import 'email_verification_screen.dart'; -import 'login_screen.dart'; - -class RegistrationScreen extends StatefulWidget { - const RegistrationScreen({super.key}); - - @override - State createState() => _RegistrationScreenState(); -} - -class _RegistrationScreenState extends State with SingleTickerProviderStateMixin { - final _formKey = GlobalKey(); - final _nicknameController = TextEditingController(); - final _emailController = TextEditingController(); - final _passwordController = TextEditingController(); - final _confirmPasswordController = TextEditingController(); - bool _obscurePassword = true; - bool _obscureConfirmPassword = true; - - late TabController _tabController; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 2, vsync: this); - _tabController.index = 1; - } - - @override - void dispose() { - _nicknameController.dispose(); - _emailController.dispose(); - _passwordController.dispose(); - _confirmPasswordController.dispose(); - _tabController.dispose(); - super.dispose(); - } - - Future _register() async { - if (_formKey.currentState!.validate()) { - try { - await Provider.of(context, listen: false).register( - _emailController.text.trim(), - _nicknameController.text.trim(), - _passwordController.text, - context: context, - ); - - if (!mounted) return; - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EmailVerificationScreen( - email: _emailController.text.trim(), - ), - ), - ); - } catch (e) { - if (!mounted) return; - - // Получаем текст ошибки без "Exception: " - final errorText = e.toString().replaceAll('', ''); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(errorText), - backgroundColor: Colors.red.shade800, - behavior: SnackBarBehavior.floating, - margin: const EdgeInsets.all(16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ); - } - } - } - - @override - Widget build(BuildContext context) { - final authController = Provider.of(context); - final isLoading = authController.isLoading; - - return Scaffold( - body: SafeArea( - child: Column( - children: [ - const SizedBox(height: 40), - Image.asset( - 'assets/icons/карты.png', - height: 80, - color: const Color(0xFFD9A76A), - ), - const SizedBox(height: 10), - const Text( - 'Cardly', - style: TextStyle( - fontSize: 24, - color: Color(0xFFD9A76A), - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 30), - - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: TabBar( - controller: _tabController, - indicatorSize: TabBarIndicatorSize.tab, - indicator: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8), - ), - labelColor: Colors.black, - unselectedLabelColor: Colors.black, - onTap: (index) { - if (index == 0) { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const LoginScreen()), - ); - } - }, - tabs: const [ - Tab(text: 'Войти'), - Tab(text: 'Создать'), - ], - ), - ), - - const SizedBox(height: 20), - - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Никнейм', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - TextFormField( - controller: _nicknameController, - decoration: const InputDecoration( - filled: true, - fillColor: Color(0xFFEDD6B0), - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Пожалуйста, введите никнейм'; - } - return null; - }, - ), - - const SizedBox(height: 16), - - const Text( - 'Электронная почта', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - TextFormField( - controller: _emailController, - decoration: const InputDecoration( - filled: true, - fillColor: Color(0xFFEDD6B0), - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), - ), - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Пожалуйста, введите email'; - } - if (!EmailValidator.validate(value)) { - return 'Введите корректный email'; - } - return null; - }, - ), - - const SizedBox(height: 16), - - const Text( - 'Пароль', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - TextFormField( - controller: _passwordController, - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFFEDD6B0), - border: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - suffixIcon: IconButton( - icon: Icon( - _obscurePassword ? Icons.visibility_off : Icons.visibility, - color: Colors.black54, - ), - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - ), - ), - obscureText: _obscurePassword, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Пожалуйста, введите пароль'; - } - if (value.length < 8) { - return 'Пароль должен содержать минимум 8 символов'; - } - return null; - }, - ), - - const SizedBox(height: 16), - - const Text( - 'Повторите пароль', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - TextFormField( - controller: _confirmPasswordController, - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFFEDD6B0), - border: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - suffixIcon: IconButton( - icon: Icon( - _obscureConfirmPassword ? Icons.visibility_off : Icons.visibility, - color: Colors.black54, - ), - onPressed: () { - setState(() { - _obscureConfirmPassword = !_obscureConfirmPassword; - }); - }, - ), - ), - obscureText: _obscureConfirmPassword, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Пожалуйста, подтвердите пароль'; - } - if (value != _passwordController.text) { - return 'Пароли не совпадают'; - } - return null; - }, - ), - - const SizedBox(height: 40), - - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: isLoading ? null : _register, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - disabledBackgroundColor: const Color(0xFFD6A067).withOpacity(0.7), - ), - child: isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.black, - ), - ) - : const Text( - 'Регистрация', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - ), - ), - ), - - Container( - width: 100, - height: 4, - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(2), - ), - ), - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/frontend/lib/views/splash_screen.dart b/frontend/lib/views/splash_screen.dart index afcc846..4225eed 100644 --- a/frontend/lib/views/splash_screen.dart +++ b/frontend/lib/views/splash_screen.dart @@ -1,45 +1,8 @@ import 'package:flutter/material.dart'; -import 'dart:async'; -import 'package:provider/provider.dart'; -import '../controllers/auth_controller.dart'; -import 'auth_screen.dart'; -import 'home_screen.dart'; -class SplashScreen extends StatefulWidget { +class SplashScreen extends StatelessWidget { const SplashScreen({super.key}); - @override - State createState() => _SplashScreenState(); -} - -class _SplashScreenState extends State { - @override - void initState() { - super.initState(); - - // Запускаем проверку авторизации с небольшой задержкой для отображения сплеш-скрина - Timer(const Duration(seconds: 2), () { - _checkAuth(); - }); - } - - Future _checkAuth() async { - final authController = Provider.of(context, listen: false); - final isLoggedIn = await authController.tryAutoLogin(); - - if (!mounted) return; - - if (isLoggedIn) { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => const HomeScreen()), - ); - } else { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => const AuthScreen()), - ); - } - } - @override Widget build(BuildContext context) { return Scaffold( @@ -63,7 +26,10 @@ class _SplashScreenState extends State { fontWeight: FontWeight.bold, ), ), - const SizedBox(height: 100), + const SizedBox(height: 20), + const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Color(0xFFD9A76A)), + ), ], ), ), From a9004d57ab7b29586a8c429ed3b264f5df8de719 Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Tue, 3 Jun 2025 04:40:39 +0300 Subject: [PATCH 41/54] FCCX-150 Rewrite auth --- .../ru/vsu/app/controller/AdminController.kt | 2 +- .../app/controller/AuthController (old).kt | 127 ---------- .../ru/vsu/app/controller/AuthController.kt | 221 ++++++++++++++---- .../ru/vsu/app/controller/CardController.kt | 2 +- .../ru/vsu/app/controller/HomeController.kt | 2 +- .../vsu/app/controller/InventoryController.kt | 2 +- .../app/controller/OtherProfileController.kt | 2 +- .../vsu/app/controller/ProfileController.kt | 2 +- .../ru/vsu/app/controller/ShopController.kt | 2 +- .../ru/vsu/app/controller/TradesController.kt | 2 +- .../app/dto/requests/AuthRefreshRequest.kt | 29 +++ .../auth/login/LoginUser200Response.kt | 23 ++ .../AuthRefreshToken200Response.kt | 29 +++ .../AuthRefreshToken401Response.kt | 31 +++ .../RequestPasswordReset400Response.kt | 2 +- .../kotlin/ru/vsu/app/metrics/AuthMetrics.kt | 84 ++++++- .../kotlin/ru/vsu/app/service/AuthService.kt | 154 +++++++++++- .../kotlin/ru/vsu/app/service/JwtService.kt | 34 ++- 18 files changed, 564 insertions(+), 186 deletions(-) delete mode 100644 backend/src/main/kotlin/ru/vsu/app/controller/AuthController (old).kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/requests/AuthRefreshRequest.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/refreshtoken/AuthRefreshToken200Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/refreshtoken/AuthRefreshToken401Response.kt diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt index 6b85ed2..d872bda 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt @@ -46,10 +46,10 @@ import jakarta.validation.constraints.Size import kotlin.collections.List import kotlin.collections.Map -@SecurityRequirement(name = "Bearer Authentication") @RestController @Validated @RequestMapping("\${api.base-path:/api}") +@SecurityRequirement(name = "Bearer Authentication") @Tag(name = "Admin", description = "Функции администратора") class AdminController() { diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/AuthController (old).kt b/backend/src/main/kotlin/ru/vsu/app/controller/AuthController (old).kt deleted file mode 100644 index 393daf1..0000000 --- a/backend/src/main/kotlin/ru/vsu/app/controller/AuthController (old).kt +++ /dev/null @@ -1,127 +0,0 @@ -// package ru.vsu.app.controller - -// import io.swagger.v3.oas.annotations.Operation -// import io.swagger.v3.oas.annotations.media.Content -// import io.swagger.v3.oas.annotations.media.Schema -// import io.swagger.v3.oas.annotations.responses.ApiResponse as SwaggerApiResponse -// import io.swagger.v3.oas.annotations.responses.ApiResponses -// import io.swagger.v3.oas.annotations.tags.Tag -// import org.springframework.http.HttpStatus -// import org.springframework.http.ResponseEntity -// import org.springframework.security.core.annotation.AuthenticationPrincipal -// import org.springframework.security.core.userdetails.UserDetails -// import org.springframework.web.bind.annotation.* -// import ru.vsu.app.dto.* -// import ru.vsu.app.service.UserService - -// @RestController -// @RequestMapping("/api/auth") -// @Tag(name = "Аутентификация", description = "API для регистрации, входа и управления аккаунтом") -// class AuthController(private val userService: UserService) { - -// @Operation(summary = "Регистрация нового пользователя") -// @ApiResponses(value = [ -// SwaggerApiResponse(responseCode = "200", description = "Пользователь успешно зарегистрирован"), -// SwaggerApiResponse(responseCode = "400", description = "Некорректные данные или пользователь уже существует") -// ]) -// @PostMapping("/register") -// fun register(@RequestBody request: RegisterRequest): ResponseEntity { -// val response = userService.register(request) -// return if (response.success) { -// ResponseEntity.ok(response) -// } else { -// ResponseEntity.badRequest().body(response) -// } -// } - -// @Operation(summary = "Активация аккаунта по коду") -// @ApiResponses(value = [ -// SwaggerApiResponse(responseCode = "200", description = "Аккаунт успешно активирован"), -// SwaggerApiResponse(responseCode = "400", description = "Неверный код активации") -// ]) -// @PostMapping("/activate") -// fun activateAccount(@RequestBody request: ActivateAccountRequest): ResponseEntity { -// val response = userService.activateAccount(request) -// return if (response.success) { -// ResponseEntity.ok(response) -// } else { -// ResponseEntity.badRequest().body(response) -// } -// } - -// @Operation(summary = "Вход в систему") -// @ApiResponses(value = [ -// SwaggerApiResponse( -// responseCode = "200", -// description = "Успешная аутентификация, возвращает JWT токен", -// content = [Content(schema = Schema(implementation = LoginResponse::class))] -// ), -// SwaggerApiResponse( -// responseCode = "401", -// description = "Неверные учетные данные", -// content = [Content(schema = Schema(implementation = ApiResponse::class))] -// ) -// ]) -// @PostMapping("/login") -// fun login(@RequestBody request: LoginRequest): ResponseEntity { -// val response = userService.login(request) -// return if (response != null) { -// ResponseEntity.ok(response) -// } else { -// ResponseEntity.status(HttpStatus.UNAUTHORIZED) -// .body(ApiResponse("Неверные учетные данные или аккаунт не активирован", false)) -// } -// } - -// @Operation(summary = "Получение информации о текущем пользователе") -// @ApiResponses(value = [ -// SwaggerApiResponse( -// responseCode = "200", -// description = "Информация о пользователе", -// content = [Content(schema = Schema(implementation = UserInfoResponse::class))] -// ), -// SwaggerApiResponse(responseCode = "401", description = "Неавторизованный доступ") -// ]) -// @GetMapping("/user-info") -// fun getUserInfo(@AuthenticationPrincipal userDetails: UserDetails): ResponseEntity { -// val response = userService.getUserInfo(userDetails.username) -// return ResponseEntity.ok(response) -// } - -// @Operation(summary = "Запрос на сброс пароля") -// @ApiResponses(value = [ -// SwaggerApiResponse( -// responseCode = "200", -// description = "Код для сброса пароля отправлен на email", -// content = [Content(schema = Schema(implementation = ApiResponse::class))] -// ) -// ]) -// @PostMapping("/forgot-password") -// fun forgotPassword(@RequestBody request: ForgotPasswordRequest): ResponseEntity { -// val response = userService.forgotPassword(request) -// return ResponseEntity.ok(response) -// } - -// @Operation(summary = "Установка нового пароля по коду из email") -// @ApiResponses(value = [ -// SwaggerApiResponse( -// responseCode = "200", -// description = "Пароль успешно изменен", -// content = [Content(schema = Schema(implementation = ApiResponse::class))] -// ), -// SwaggerApiResponse( -// responseCode = "400", -// description = "Неверный код или код устарел", -// content = [Content(schema = Schema(implementation = ApiResponse::class))] -// ) -// ]) -// @PostMapping("/reset-password") -// fun resetPassword(@RequestBody request: ResetPasswordRequest): ResponseEntity { -// val response = userService.resetPassword(request) -// return if (response.success) { -// ResponseEntity.ok(response) -// } else { -// ResponseEntity.badRequest().body(response) -// } -// } -// } \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt index 5360144..0688f6e 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt @@ -1,29 +1,43 @@ package ru.vsu.app.controller +import ru.vsu.app.dto.responses.auth.login.LoginUser200Response import ru.vsu.app.dto.responses.auth.login.LoginUser400Response import ru.vsu.app.dto.responses.auth.login.LoginUser401Response import ru.vsu.app.dto.requests.LoginUserRequest -import ru.vsu.app.dto.responses.auth.AuthCheckGet401Response + import ru.vsu.app.dto.responses.auth.register.RegisterUser200Response import ru.vsu.app.dto.responses.auth.register.RegisterUser400Response import ru.vsu.app.dto.responses.auth.register.RegisterUser409Response -import ru.vsu.app.dto.responses.common.InternalServerError import ru.vsu.app.dto.requests.RegisterUserRequest + +import ru.vsu.app.dto.responses.common.InternalServerError + import ru.vsu.app.dto.responses.auth.requestpassword.RequestPasswordReset200Response import ru.vsu.app.dto.responses.auth.requestpassword.RequestPasswordReset400Response import ru.vsu.app.dto.responses.auth.requestpassword.RequestPasswordReset404Response import ru.vsu.app.dto.requests.RequestPasswordResetRequest -import ru.vsu.app.dto.responses.auth.verifyemail.ResendVerificationCode200Response -import ru.vsu.app.dto.requests.ResendVerificationCodeRequest + import ru.vsu.app.dto.responses.auth.requestpassword.ResetPassword200Response import ru.vsu.app.dto.responses.auth.requestpassword.ResetPassword400Response import ru.vsu.app.dto.requests.ResetPasswordRequest -import ru.vsu.app.dto.UserDto + +import ru.vsu.app.dto.responses.auth.verifyemail.ResendVerificationCode200Response +import ru.vsu.app.dto.requests.ResendVerificationCodeRequest + import ru.vsu.app.dto.responses.auth.verifyemail.VerifyEmail400Response import ru.vsu.app.dto.responses.auth.verifyemail.VerifyEmail410Response import ru.vsu.app.dto.requests.VerifyEmailRequest +import ru.vsu.app.dto.responses.auth.AuthCheckGet401Response + +import ru.vsu.app.dto.responses.auth.refreshtoken.AuthRefreshToken200Response +import ru.vsu.app.dto.responses.auth.refreshtoken.AuthRefreshToken401Response +import ru.vsu.app.dto.requests.AuthRefreshRequest + +import ru.vsu.app.dto.UserDto + import ru.vsu.app.service.AuthService +import ru.vsu.app.service.JwtService import ru.vsu.app.metrics.AuthMetrics @@ -55,13 +69,14 @@ import jakarta.validation.constraints.Size import kotlin.collections.List import kotlin.collections.Map -@SecurityRequirement(name = "Bearer Authentication") @RestController @Validated @RequestMapping("\${api.base-path:/api}") +@SecurityRequirement(name = "Bearer Authentication") @Tag(name = "Authentication", description = "Регистрация, вход и управление аккаунтом") class AuthController( private val authService: AuthService, + private val jwtService: JwtService, private val authMetrics: AuthMetrics ) { @@ -83,33 +98,23 @@ class AuthController( produces = ["application/json"] ) fun authCheckGet(): ResponseEntity { + authMetrics.authCheckAttempt() + val startTime = System.currentTimeMillis() + val user = authService.getCurrentUser() - return if (user != null) { + val response: ResponseEntity = if (user != null) { + authMetrics.authCheckAuthorized() ResponseEntity.ok(user) } else { + authMetrics.authCheckUnauthorized() ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(AuthCheckGet401Response(false, "Пользователь не авторизован")) } - } - @Operation( - summary = "Выход из системы", - operationId = "authLogoutPost", - description = """Завершает текущую сессию пользователя. -Удаляет сессионную cookie. -""", - responses = [ - ApiResponse(responseCode = "204", description = "Успешный выход"), - ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], - security = [ SecurityRequirement(name = "bearerAuth") ] - ) - @RequestMapping( - method = [RequestMethod.POST], - value = ["/auth/logout"], - produces = ["application/json"] - ) - fun authLogoutPost(): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + val durationMs = System.currentTimeMillis() - startTime + authMetrics.authCheckDuration(durationMs) + + return response } @Operation( @@ -119,8 +124,8 @@ class AuthController( Требуется валидная существующая сессия. """, responses = [ - ApiResponse(responseCode = "200", description = "Сессия успешно обновлена", content = [Content(schema = Schema(implementation = UserDto::class))]), - ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "200", description = "Сессия успешно обновлена", content = [Content(schema = Schema(implementation = AuthRefreshToken200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации", content = [Content(schema = Schema(implementation = AuthRefreshToken401Response::class))]), ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], security = [ SecurityRequirement(name = "bearerAuth") ] ) @@ -129,21 +134,49 @@ class AuthController( value = ["/auth/refresh"], produces = ["application/json"] ) - fun authRefreshPost(): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun authRefreshPost( + @Parameter(description = "", required = true) + @Valid @RequestBody request: AuthRefreshRequest + ): ResponseEntity { + val start = System.currentTimeMillis() + authMetrics.refreshAttempt() + + val result = authService.refreshSession(request) + + authMetrics.refreshDuration(System.currentTimeMillis() - start) + + return when (result) { + is AuthRefreshToken200Response -> { + authMetrics.refreshSuccess() + ResponseEntity.ok(result) + } + is AuthRefreshToken401Response -> { + authMetrics.refreshFailure() + ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(result) + } + is InternalServerError -> { + authMetrics.refreshFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result) + } + else -> { + authMetrics.refreshFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() + } + } } @Operation( summary = "Вход в систему", operationId = "loginUser", description = """Аутентификация пользователя по email и паролю. -Если данные введены верно - устанавливает сессионную cookie. -""", + Если данные введены верно — создает сессионную cookie и возвращает данные пользователя. + """, responses = [ - ApiResponse(responseCode = "200", description = "Успешный вход", content = [Content(schema = Schema(implementation = UserDto::class))]), - ApiResponse(responseCode = "400", description = "Неверный формат email", content = [Content(schema = Schema(implementation = LoginUser400Response::class))]), - ApiResponse(responseCode = "401", description = "Неверные учетные данные", content = [Content(schema = Schema(implementation = LoginUser401Response::class))]), - ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ApiResponse(responseCode = "200", description = "Успешный вход", content = [Content(schema = Schema(implementation = LoginUser200Response::class))]), + ApiResponse(responseCode = "400", description = "Неверный формат или данные", content = [Content(schema = Schema(implementation = LoginUser400Response::class))]), + ApiResponse(responseCode = "401", description = "Неверные учётные данные", content = [Content(schema = Schema(implementation = LoginUser401Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) + ] ) @RequestMapping( method = [RequestMethod.POST], @@ -151,8 +184,45 @@ class AuthController( produces = ["application/json"], consumes = ["application/json"] ) - fun loginUser(@Parameter(description = "", required = true) @Valid @RequestBody loginUserRequest: LoginUserRequest): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun loginUser( + @Parameter(description = "", required = true) + @Valid @RequestBody loginUserRequest: LoginUserRequest + ): ResponseEntity { + val start = System.currentTimeMillis() + authMetrics.loginAttempt() + + val response = try { + authService.loginUser(loginUserRequest) + } catch (ex: Exception) { + authMetrics.loginFailure() + authMetrics.loginDuration(System.currentTimeMillis() - start) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(InternalServerError("Ошибка при попытке входа", ex.message ?: "Неизвестная ошибка")) + } + + val duration = System.currentTimeMillis() - start + authMetrics.loginDuration(duration) + + return when (response) { + is UserDto -> { + val token = jwtService.generateToken(response.email) + + authMetrics.loginSuccess(response.userID.toString()) + ResponseEntity.ok(LoginUser200Response(user = response, accessToken = token)) + } + is LoginUser400Response -> { + authMetrics.loginFailure() + ResponseEntity.badRequest().body(response) + } + is LoginUser401Response -> { + authMetrics.loginFailure() + ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response) + } + else -> { + authMetrics.loginFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() + } + } } @Operation( @@ -233,8 +303,40 @@ class AuthController( produces = ["application/json"], consumes = ["application/json"] ) - fun requestPasswordReset(@Parameter(description = "", required = true) @Valid @RequestBody requestPasswordResetRequest: RequestPasswordResetRequest): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun requestPasswordReset(@Parameter(description = "", required = true) @Valid @RequestBody request: RequestPasswordResetRequest): ResponseEntity { + val start = System.currentTimeMillis() + authMetrics.passwordResetAttempt() + + return try { + val result = authService.requestPasswordReset(request) + + val duration = System.currentTimeMillis() - start + authMetrics.passwordResetDuration(duration) + + when (result) { + is RequestPasswordReset200Response -> { + authMetrics.passwordResetSuccess() + ResponseEntity.ok(result) + } + is RequestPasswordReset404Response -> { + authMetrics.passwordResetFailure() + ResponseEntity.status(HttpStatus.NOT_FOUND).body(result) + } + is RequestPasswordReset400Response -> { + authMetrics.passwordResetFailure() + ResponseEntity.badRequest().body(result) + } + else -> { + authMetrics.passwordResetFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(InternalServerError(error = "Ошибка: Неожиданный формат ответа")) + } + } + } catch (ex: Exception) { + authMetrics.passwordResetFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(InternalServerError(error = "Ошибка при сбросе пароля: "+ ex.message ?: "Неизвестная ошибка")) + } } @Operation( @@ -301,8 +403,43 @@ class AuthController( produces = ["application/json"], consumes = ["application/json"] ) - fun resetPassword(@Parameter(description = "", required = true) @Valid @RequestBody resetPasswordRequest: ResetPasswordRequest): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun resetPassword( + @Parameter(description = "", required = true) + @Valid @RequestBody resetPasswordRequest: ResetPasswordRequest + ): ResponseEntity { + val start = System.currentTimeMillis() + authMetrics.resetPasswordAttempt() + + val response = try { + authService.resetPassword(resetPasswordRequest) + } catch (ex: Exception) { + authMetrics.resetPasswordFailure() + authMetrics.resetPasswordDuration(System.currentTimeMillis() - start) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(InternalServerError("Ошибка при сбросе пароля", ex.message ?: "Неизвестная ошибка")) + } + + val duration = System.currentTimeMillis() - start + authMetrics.resetPasswordDuration(duration) + + return when (response) { + is ResetPassword200Response -> { + authMetrics.resetPasswordSuccess() + ResponseEntity.ok(response) + } + is ResetPassword400Response -> { + authMetrics.resetPasswordValidationError() + ResponseEntity.badRequest().body(response) + } + is InternalServerError -> { + authMetrics.resetPasswordFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response) + } + else -> { + authMetrics.resetPasswordFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Неизвестная ошибка") + } + } } @Operation( diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt index 11245cf..400139b 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt @@ -13,9 +13,9 @@ // import ru.vsu.app.model.UserEntity // import ru.vsu.app.service.InventoryService -//@SecurityRequirement(name = "Bearer Authentication") // @RestController // @RequestMapping("/api/card") +// @SecurityRequirement(name = "Bearer Authentication") // @Tag(name = "Card", description = "Функции, связанные с карточками пользователя") // class CardController( // private val cardService: InventoryService diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/HomeController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/HomeController.kt index 8e91152..d19712f 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/HomeController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/HomeController.kt @@ -46,10 +46,10 @@ import jakarta.validation.constraints.Size import kotlin.collections.List import kotlin.collections.Map -@SecurityRequirement(name = "Bearer Authentication") @RestController @Validated @RequestMapping("\${api.base-path:/api}") +@SecurityRequirement(name = "Bearer Authentication") @Tag(name = "Home", description = "Операции на странице \"Главное меню\"") class HomeController() { @Operation( diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt index da0982e..2225d84 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt @@ -43,10 +43,10 @@ // import kotlin.collections.List // import kotlin.collections.Map -// @SecurityRequirement(name = "Bearer Authentication") // @RestController // @Validated // @RequestMapping("\${api.base-path:/api}") +// @SecurityRequirement(name = "Bearer Authentication") // @Tag(name = "Inventory", description = "Операции на странице \"Инвентарь\"") // class InventoryController(private val inventoryService: InventoryService) { diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/OtherProfileController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/OtherProfileController.kt index 2bf78bc..820d6a5 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/OtherProfileController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/OtherProfileController.kt @@ -39,10 +39,10 @@ import jakarta.validation.constraints.Size import kotlin.collections.List import kotlin.collections.Map -@SecurityRequirement(name = "Bearer Authentication") @RestController @Validated @RequestMapping("\${api.base-path:/api}") +@SecurityRequirement(name = "Bearer Authentication") @Tag(name = "OtherProfile", description = "Операции при просмотре профиля другого пользователя") class OtherProfileController() { diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/ProfileController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/ProfileController.kt index 28dac5a..6ed257d 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/ProfileController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/ProfileController.kt @@ -33,10 +33,10 @@ import jakarta.validation.constraints.Size import kotlin.collections.List import kotlin.collections.Map -@SecurityRequirement(name = "Bearer Authentication") @RestController @Validated @RequestMapping("\${api.base-path:/api}") +@SecurityRequirement(name = "Bearer Authentication") @Tag(name = "Profile", description = "Операции в профиле пользователя") class ProfileController() { diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/ShopController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/ShopController.kt index 6e4849a..13a33d1 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/ShopController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/ShopController.kt @@ -38,9 +38,9 @@ import jakarta.validation.constraints.Size import kotlin.collections.List import kotlin.collections.Map -@SecurityRequirement(name = "Bearer Authentication") @RestController @Validated +@SecurityRequirement(name = "Bearer Authentication") @RequestMapping("\${api.base-path:/api}") @Tag(name = "Shop", description = "Операции на странице \"Магазин\"") class ShopController() { diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/TradesController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/TradesController.kt index 7e286f3..0e50f8a 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/TradesController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/TradesController.kt @@ -37,9 +37,9 @@ import jakarta.validation.constraints.Size import kotlin.collections.List import kotlin.collections.Map -@SecurityRequirement(name = "Bearer Authentication") @RestController @Validated +@SecurityRequirement(name = "Bearer Authentication") @RequestMapping("\${api.base-path:/api}") @Tag(name = "Trades", description = "Операции на странице \"Обменник\"") class TradesController() { diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/AuthRefreshRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AuthRefreshRequest.kt new file mode 100644 index 0000000..ed86c82 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AuthRefreshRequest.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param accessToken + */ +data class AuthRefreshRequest( + + @field:NotNull + @Schema(description = "Обновлённый JWT токен для доступа", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") + @get:JsonProperty("accessToken") + val accessToken: String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser200Response.kt new file mode 100644 index 0000000..f4e99c8 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser200Response.kt @@ -0,0 +1,23 @@ +package ru.vsu.app.dto.responses.auth.login + +import com.fasterxml.jackson.annotation.JsonProperty +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotNull +import ru.vsu.app.dto.UserDto + +/** + * + * @param user + * @param accessToken + */ +data class LoginUser200Response( + @field:NotNull + @Schema(description = "Данные пользователя", required = true) + @get:JsonProperty("user") + val user: UserDto, + + @field:NotNull + @Schema(description = "JWT токен для доступа", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") + @get:JsonProperty("accessToken") + val accessToken: String +) diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/refreshtoken/AuthRefreshToken200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/refreshtoken/AuthRefreshToken200Response.kt new file mode 100644 index 0000000..9bfe9a5 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/refreshtoken/AuthRefreshToken200Response.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.dto.responses.auth.refreshtoken + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param accessToken + */ +data class AuthRefreshToken200Response( + + @field:NotNull + @Schema(description = "Обновлённый JWT токен для доступа", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") + @get:JsonProperty("accessToken") + val accessToken: String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/refreshtoken/AuthRefreshToken401Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/refreshtoken/AuthRefreshToken401Response.kt new file mode 100644 index 0000000..4fc8d57 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/refreshtoken/AuthRefreshToken401Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.auth.refreshtoken + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param isAuthenticated + * @param message + */ +data class AuthRefreshToken401Response( + + @Schema(example = "false", description = "") + @get:JsonProperty("isAuthenticated") val isAuthenticated: kotlin.Boolean? = null, + + @Schema(example = "Требуется авторизация", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset400Response.kt index 7add1dd..9f4cbc0 100644 --- a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset400Response.kt +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset400Response.kt @@ -19,7 +19,7 @@ import io.swagger.v3.oas.annotations.media.Schema */ data class RequestPasswordReset400Response( - @Schema(example = "Некорректный формат emaila", description = "") + @Schema(example = "Некорректный формат email", description = "") @get:JsonProperty("error") val error: kotlin.String? = null ) { diff --git a/backend/src/main/kotlin/ru/vsu/app/metrics/AuthMetrics.kt b/backend/src/main/kotlin/ru/vsu/app/metrics/AuthMetrics.kt index 5667d5c..1859339 100644 --- a/backend/src/main/kotlin/ru/vsu/app/metrics/AuthMetrics.kt +++ b/backend/src/main/kotlin/ru/vsu/app/metrics/AuthMetrics.kt @@ -5,6 +5,7 @@ import org.springframework.stereotype.Service @Service class AuthMetrics(private val metrics: MetricsRegistry) { + // ===== Авторизация ===== fun loginAttempt() { metrics.count("auth.login.attempt") } @@ -17,10 +18,12 @@ class AuthMetrics(private val metrics: MetricsRegistry) { metrics.count("auth.login.failure") } - fun loginDuration(duration: Long, userId: String) { - metrics.timer("auth.login.duration", duration, listOf(MetricTags.user(userId))) + fun loginDuration(duration: Long) { + metrics.timer("auth.login.duration", duration) } + // ===== Регистрация ===== + fun registerAttempt() { metrics.count("auth.register.attempt") } @@ -45,6 +48,8 @@ class AuthMetrics(private val metrics: MetricsRegistry) { metrics.timer("auth.register.duration", durationMs) } + // ===== Верификация кода Email ===== + fun verifyAttempt() { metrics.count("auth.verify.attempt") } @@ -73,6 +78,8 @@ class AuthMetrics(private val metrics: MetricsRegistry) { } } + // ===== Переотправка кода на Email ===== + fun resendVerificationCodeAttempt() { metrics.count("auth.resend_code.attempt") } @@ -93,4 +100,77 @@ class AuthMetrics(private val metrics: MetricsRegistry) { metrics.timer("auth.resend_code.duration", durationMs) } + // ===== Сброс пароля через токен и код ===== + fun resetPasswordAttempt() { + metrics.count("auth.reset.password.attempt") + } + + + fun resetPasswordDuration(durationMs: Long) { + metrics.timer("auth.reset.password.duration", durationMs) + } + + fun resetPasswordSuccess() { + metrics.count("auth.reset.password.success") + } + + fun resetPasswordValidationError() { + metrics.count("reset.password.validation.error") + } + + fun resetPasswordFailure() { + metrics.count("reset.password.failure") + } + + // ===== Обновление пароля после проверки ===== + fun passwordResetSuccess() { + metrics.count("password.reset.success") + } + + fun passwordResetFailure() { + metrics.count("password.reset.failure") + } + + fun passwordResetDuration(durationMs: Long) { + metrics.timer("password.reset.duration", durationMs) + } + + fun passwordResetAttempt() { + metrics.count("password.reset.attempt") + } + + // ===== Обновление токена (refresh) ===== + fun refreshSuccess() { + metrics.count("auth.refresh.success") + } + + fun refreshFailure() { + metrics.count("auth.refresh.failure") + } + + fun refreshDuration(durationMs: Long) { + metrics.timer("auth.refresh.duration", durationMs) + } + + fun refreshAttempt() { + metrics.count("auth.refresh.attempt") + } + + // ===== Проверка авторизации ===== + fun authCheckAttempt() { + metrics.count("auth.check.attempt") + } + + fun authCheckAuthorized() { + metrics.count("auth.check.authorized") + } + + fun authCheckUnauthorized() { + metrics.count("auth.check.unauthorized") + } + + fun authCheckDuration(durationMs: Long) { + metrics.timer("auth.check.duration", durationMs) + } + } diff --git a/backend/src/main/kotlin/ru/vsu/app/service/AuthService.kt b/backend/src/main/kotlin/ru/vsu/app/service/AuthService.kt index faa521b..88cb84e 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/AuthService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/AuthService.kt @@ -1,30 +1,63 @@ package ru.vsu.app.service +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service import org.springframework.security.core.context.SecurityContextHolder -import ru.vsu.app.dto.requests.RegisterUserRequest + import ru.vsu.app.dto.requests.ResendVerificationCodeRequest + +import ru.vsu.app.dto.responses.auth.login.LoginUser200Response +import ru.vsu.app.dto.responses.auth.login.LoginUser400Response +import ru.vsu.app.dto.responses.auth.login.LoginUser401Response +import ru.vsu.app.dto.requests.LoginUserRequest + import ru.vsu.app.dto.responses.auth.register.RegisterUser200Response import ru.vsu.app.dto.responses.auth.register.RegisterUser400Response import ru.vsu.app.dto.responses.auth.register.RegisterUser409Response +import ru.vsu.app.dto.requests.RegisterUserRequest + import ru.vsu.app.dto.responses.auth.verifyemail.VerifyEmail400Response import ru.vsu.app.dto.responses.auth.verifyemail.VerifyEmail410Response import ru.vsu.app.dto.responses.auth.verifyemail.ResendVerificationCode200Response -import ru.vsu.app.dto.responses.common.InternalServerError -import org.springframework.http.HttpStatus -import org.springframework.http.ResponseEntity import ru.vsu.app.dto.requests.VerifyEmailRequest + +import ru.vsu.app.dto.responses.auth.requestpassword.RequestPasswordReset400Response +import ru.vsu.app.dto.responses.auth.requestpassword.RequestPasswordReset404Response +import ru.vsu.app.dto.responses.auth.requestpassword.RequestPasswordReset200Response +import ru.vsu.app.dto.requests.RequestPasswordResetRequest + +import ru.vsu.app.dto.responses.auth.refreshtoken.AuthRefreshToken200Response +import ru.vsu.app.dto.responses.auth.refreshtoken.AuthRefreshToken401Response +import ru.vsu.app.dto.requests.AuthRefreshRequest + +import ru.vsu.app.dto.responses.auth.requestpassword.ResetPassword200Response +import ru.vsu.app.dto.responses.auth.requestpassword.ResetPassword400Response +import ru.vsu.app.dto.requests.ResetPasswordRequest + +import ru.vsu.app.dto.responses.common.InternalServerError + import ru.vsu.app.dto.UserDto import ru.vsu.app.dto.CardDto import ru.vsu.app.dto.AchievementDto + import ru.vsu.app.model.UserEntity + import ru.vsu.app.repository.UserRepository + import ru.vsu.app.service.JwtService import ru.vsu.app.service.EmailService + import ru.vsu.app.mapper.UserMapper + import kotlin.random.Random +import io.jsonwebtoken.JwtException +import io.jsonwebtoken.ExpiredJwtException + +import java.util.regex.Pattern + @Service class AuthService( private val userRepository: UserRepository, @@ -39,7 +72,6 @@ class AuthService( val authentication = SecurityContextHolder.getContext().authentication if (authentication == null || !authentication.isAuthenticated || authentication.principal == "anonymousUser") { - println("111111111111") return null } @@ -162,4 +194,116 @@ class AuthService( ) } + fun loginUser(request: LoginUserRequest): Any { + val emailPattern = Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$") + if (!emailPattern.matcher(request.email).matches() || request.password.length < 6) { + return LoginUser400Response( + error = "Неверный формат email или пароль слишком короткий" + ) + } + + return try { + val userEntity = userRepository.findByEmail(request.email).orElse(null) + if (userEntity == null) { + return LoginUser401Response( + error = "Неверные учётные данные" + ) + } + + if (!userEntity.isEnabled) { + return LoginUser401Response( + error = "Аккаунт не активирован" + ) + } + + if (!passwordEncoder.matches(request.password, userEntity.passwordHash)) { + return LoginUser401Response( + error = "Неверные учётные данные" + ) + } + + userMapper.toDto(userEntity) + } catch (ex: Exception) { + InternalServerError( + error = "Ошибка при попытке входа: ${ex.message ?: "Неизвестная ошибка"}" + ) + } + } + + fun refreshSession( + request: AuthRefreshRequest + ): Any { + return try { + val isExpired = jwtService.isTokenExpired(request.accessToken) + if (isExpired) { + return AuthRefreshToken401Response(false, "Access token истёк") + } + val accessToken = request.accessToken + + val email = jwtService.extractUsername(accessToken) + + val user = userRepository.findByEmail(email).orElse(null) + ?: return AuthRefreshToken401Response(false, "Пользователь не найден") + + val newAccessToken = jwtService.generateToken(email) + + AuthRefreshToken200Response(accessToken = newAccessToken) + + } catch (ex: JwtException) { + AuthRefreshToken401Response(false, "Невалидный или просроченный access token") + } catch (ex: Exception) { + InternalServerError(error = "Ошибка при обновлении токена: " + ex.message ?: "Неизвестная ошибка") + } + } + + fun requestPasswordReset(request: RequestPasswordResetRequest): Any { + val emailPattern = Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$") + if (!emailPattern.matcher(request.email).matches()) { + return RequestPasswordReset400Response( + error = "Неверный формат email или пароль слишком короткий" + ) + } + + val user = userRepository.findByEmail(request.email) + ?: return RequestPasswordReset404Response("Пользователь с таким email не зарегистрирован") + + val code = generateSixDigitCode() + val resetToken = jwtService.generatePasswordResetToken(request.email, code) + + emailService.sendPasswordResetEmail(request.email, code) + + return RequestPasswordReset200Response( + resetToken = resetToken, + message = "Код подтверждения отправлен на email" + ) + } + + + fun resetPassword(request: ResetPasswordRequest): Any { + try { + val payload = jwtService.parseResetToken(request.resetToken) + + if (request.code != payload.code) { + return ResetPassword400Response("Неверный код подтверждения") + } + + val user = userRepository.findByEmail(payload.email).orElse(null) + ?: return ResetPassword400Response("Пользователь не найден") + + val updatedUser = user.copy(passwordHash = passwordEncoder.encode(request.newPassword)) + userRepository.save(updatedUser) + + return ResetPassword200Response("Пароль успешно изменен") + + } catch (ex: ExpiredJwtException) { + return ResetPassword400Response("Срок действия токена истёк") + } catch (ex: JwtException) { + return ResetPassword400Response("Невалидный токен") + } catch (ex: Exception) { + ex.printStackTrace() + return InternalServerError("Ошибка сброса пароля: ${ex.message}") + } + } + + } diff --git a/backend/src/main/kotlin/ru/vsu/app/service/JwtService.kt b/backend/src/main/kotlin/ru/vsu/app/service/JwtService.kt index 5fd7127..76384e4 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/JwtService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/JwtService.kt @@ -77,8 +77,40 @@ class JwtService { .body } + fun generatePasswordResetToken(email: String, code: String): String { + val now = Date() + val expiration = Date(now.time + 24 * 60 * 60 * 1000) // 24 часа + + return Jwts.builder() + .setSubject(email) + .claim("code", code) + .setIssuedAt(now) + .setExpiration(expiration) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact() + } + + fun parseResetToken(token: String): ResetTokenPayload { + val claims = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .body + + return ResetTokenPayload( + email = claims.subject, + code = claims["code"] as String + ) + } + + private fun getSigningKey(): Key { val keyBytes = Decoders.BASE64.decode(secretKey) return Keys.hmacShaKeyFor(keyBytes) } -} \ No newline at end of file +} + +data class ResetTokenPayload( + val email: String, + val code: String +) \ No newline at end of file From c36fc1b7e70675a8cedc528dd784d9e581d57bf0 Mon Sep 17 00:00:00 2001 From: birbik Date: Tue, 3 Jun 2025 20:09:19 +0300 Subject: [PATCH 42/54] FCCX-151 fix frontend --- frontend/lib/controllers/auth_controller.dart | 17 ++ frontend/lib/main.dart | 91 +++++++- frontend/lib/models/user_model.dart | 4 + frontend/lib/utils/auth_utils.dart | 45 ++++ frontend/lib/views/achievements_screen.dart | 9 +- frontend/lib/views/auth_screen.dart | 59 ++++-- frontend/lib/views/authorization_dialog.dart | 16 +- frontend/lib/views/create_card_screen.dart | 21 +- .../lib/views/create_exchange_screen.dart | 183 +++++++++------- .../lib/views/email_verification_screen.dart | 2 +- .../lib/views/exchange_details_screen.dart | 10 +- .../lib/views/exchange_proposal_screen.dart | 134 +++++++----- frontend/lib/views/exchanges_screen.dart | 66 ++---- frontend/lib/views/home_screen.dart | 197 ++++++++++-------- frontend/lib/views/inventory_screen.dart | 26 ++- frontend/lib/views/profile_screen.dart | 12 ++ frontend/lib/views/quests_screen.dart | 12 ++ frontend/lib/views/settings_screen.dart | 16 +- frontend/lib/views/shop_screen.dart | 17 +- 19 files changed, 618 insertions(+), 319 deletions(-) create mode 100644 frontend/lib/utils/auth_utils.dart diff --git a/frontend/lib/controllers/auth_controller.dart b/frontend/lib/controllers/auth_controller.dart index 9ff7de1..83aa5ec 100644 --- a/frontend/lib/controllers/auth_controller.dart +++ b/frontend/lib/controllers/auth_controller.dart @@ -256,4 +256,21 @@ class AuthController with ChangeNotifier { throw Exception(errorMessage); } } + + Future loginAsGuest({BuildContext? context}) async { + _currentUser = UserModel( + email: 'guest@cardly.com', + username: 'Гость', + isEmailVerified: true, + isGuest: true, + ); + + notifyListeners(); + + if (context != null) { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => const MainScreen()), + ); + } + } } \ No newline at end of file diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index fa86444..d778527 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -10,6 +10,8 @@ import 'views/home_screen.dart'; import 'views/inventory_screen.dart'; import 'views/shop_screen.dart'; import 'views/exchanges_screen.dart'; +import 'utils/auth_utils.dart'; +import 'views/authorization_dialog.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -113,21 +115,75 @@ class MyApp extends StatelessWidget { } class MainScreen extends StatefulWidget { - const MainScreen({super.key}); + final int? initialIndex; + final String? notification; + + const MainScreen({ + super.key, + this.initialIndex, + this.notification, + }); @override State createState() => _MainScreenState(); } class _MainScreenState extends State { - int _currentIndex = 0; + late int _currentIndex; final PageController _pageController = PageController(); - final List _screens = const [ - HomeScreen(), - InventoryScreen(), - ShopScreen(), - ExchangesScreen(), + @override + void initState() { + super.initState(); + _currentIndex = widget.initialIndex ?? 0; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _pageController.jumpToPage(_currentIndex); + if (widget.notification != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Container( + width: 367, + height: 61, + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(15.0), + ), + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), + child: Center( + child: Text( + widget.notification!, + style: const TextStyle( + color: Colors.black, + fontSize: 18.0, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + ), + textAlign: TextAlign.center, + ), + ), + ), + backgroundColor: Colors.transparent, + duration: const Duration(seconds: 1), + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height - 200, + left: 16, + right: 16, + ), + elevation: 0, + ), + ); + } + } + }); + } + + final List _screenNames = [ + 'home_screen', + 'inventory_screen', + 'shop_screen', + 'exchanges_screen', ]; @override @@ -137,6 +193,18 @@ class _MainScreenState extends State { } void _onItemTapped(int index) { + final authController = Provider.of(context, listen: false); + if (authController.currentUser?.isGuest ?? false) { + if (!AuthUtils.isGuestAllowedScreen(_screenNames[index])) { + showDialog( + context: context, + barrierDismissible: true, + builder: (context) => const AuthorizationDialog(), + ); + return; + } + } + setState(() { _currentIndex = index; }); @@ -145,11 +213,18 @@ class _MainScreenState extends State { @override Widget build(BuildContext context) { + final List screens = [ + const HomeScreen(), + const InventoryScreen(), + const ShopScreen(), + ExchangesScreen(initialTabIndex: _currentIndex == 3 ? 0 : null), + ]; + return Scaffold( body: PageView( controller: _pageController, physics: const NeverScrollableScrollPhysics(), - children: _screens, + children: screens, ), bottomNavigationBar: Container( decoration: const BoxDecoration( diff --git a/frontend/lib/models/user_model.dart b/frontend/lib/models/user_model.dart index 2a0a450..4784648 100644 --- a/frontend/lib/models/user_model.dart +++ b/frontend/lib/models/user_model.dart @@ -3,12 +3,14 @@ class UserModel { final String? username; final String? token; final bool isEmailVerified; + final bool isGuest; UserModel({ required this.email, this.username, this.token, this.isEmailVerified = false, + this.isGuest = false, }); factory UserModel.fromJson(Map json) { @@ -17,6 +19,7 @@ class UserModel { username: json['username'], token: json['token'], isEmailVerified: true, // Если пользователь успешно вошел, считаем, что почта подтверждена + isGuest: json['isGuest'] ?? false, ); } @@ -26,6 +29,7 @@ class UserModel { 'username': username, 'token': token, 'isEmailVerified': isEmailVerified, + 'isGuest': isGuest, }; } } \ No newline at end of file diff --git a/frontend/lib/utils/auth_utils.dart b/frontend/lib/utils/auth_utils.dart new file mode 100644 index 0000000..f71e4c4 --- /dev/null +++ b/frontend/lib/utils/auth_utils.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import '../controllers/auth_controller.dart'; +import '../views/authorization_dialog.dart'; +import 'package:provider/provider.dart'; + +class AuthUtils { + static bool isGuestAllowedScreen(String screenName) { + final allowedScreens = { + 'home_screen': true, + 'news_screen': true, + 'news_detail_screen': true, + 'exchanges_screen': true, + 'exchange_details_screen': true, + 'shop_screen': true, + 'search_players_screen': true, + 'card_detail_screen': true, + // Защищенные экраны + 'profile_screen': false, + 'inventory_screen': false, + 'create_exchange_screen': false, + 'my_exchanges_screen': false, + 'create_card_screen': false, + 'achievements_screen': false, + 'settings_screen': false, + 'notifications_screen': false, + 'quests_screen': false, + }; + return allowedScreens[screenName] ?? false; + } + + static bool checkGuestAccess(BuildContext context, String screenName) { + final authController = Provider.of(context, listen: false); + final isGuest = authController.currentUser?.isGuest ?? false; + + if (isGuest && !isGuestAllowedScreen(screenName)) { + showDialog( + context: context, + barrierDismissible: true, + builder: (context) => const AuthorizationDialog(), + ); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/frontend/lib/views/achievements_screen.dart b/frontend/lib/views/achievements_screen.dart index 82943a1..8a68387 100644 --- a/frontend/lib/views/achievements_screen.dart +++ b/frontend/lib/views/achievements_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../models/achievement_model.dart'; import '../services/api_service.dart'; import '../utils/error_formatter.dart'; +import '../utils/auth_utils.dart'; class AchievementsScreen extends StatefulWidget { final bool isOtherUser; @@ -28,7 +29,13 @@ class _AchievementsScreenState extends State { _isLoading = true; _achievements = []; _error = ''; - _loadAchievements(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (AuthUtils.checkGuestAccess(context, 'achievements_screen')) { + _loadAchievements(); + } else { + Navigator.of(context).pop(); + } + }); } Future _loadAchievements() async { diff --git a/frontend/lib/views/auth_screen.dart b/frontend/lib/views/auth_screen.dart index e9c9273..e63ad02 100644 --- a/frontend/lib/views/auth_screen.dart +++ b/frontend/lib/views/auth_screen.dart @@ -102,7 +102,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM body: Column( children: [ // Логотип и название - const SizedBox(height: 40), + const SizedBox(height: 80), Image.asset( 'assets/icons/карты.png', height: 80, @@ -275,31 +275,50 @@ class _AuthScreenState extends State with SingleTickerProviderStateM child: ElevatedButton( onPressed: isLoading ? null : _login, style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFDBAA76), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: const Color(0xFFD6A067), + minimumSize: const Size(double.infinity, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), - disabledBackgroundColor: const Color(0xFFDBAA76).withOpacity(0.7), ), child: isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.black, - ), - ) - : const Text( - 'Вход', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - fontFamily: 'Roboto', + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Text( + 'Войти', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + ), ), - ), + ), + ), + + const SizedBox(height: 16), + + Center( + child: TextButton( + onPressed: isLoading ? null : () { + Provider.of(context, listen: false) + .loginAsGuest(context: context); + }, + child: const Text( + 'Войти как гость', + style: TextStyle( + color: Color.fromARGB(255, 0, 0, 0), + fontSize: 15, + fontFamily: 'Jost', + decoration: TextDecoration.underline, + ), + ), ), ), ], diff --git a/frontend/lib/views/authorization_dialog.dart b/frontend/lib/views/authorization_dialog.dart index 7aaf5bc..d27278c 100644 --- a/frontend/lib/views/authorization_dialog.dart +++ b/frontend/lib/views/authorization_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'auth_screen.dart'; class AuthorizationDialog extends StatelessWidget { const AuthorizationDialog({super.key}); @@ -34,9 +35,12 @@ class AuthorizationDialog extends StatelessWidget { // Button ElevatedButton( onPressed: () { - // TODO: Implement navigation to login screen - // Navigator.push(context, MaterialPageRoute(builder: (context) => LoginScreen())); - // Navigator.pop(context); // Close dialog after navigation + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (context) => const AuthScreen(initialTabIndex: 0), + ), + (route) => false, + ); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFD6A067), // Match button color @@ -50,8 +54,8 @@ class AuthorizationDialog extends StatelessWidget { 'Перейти на вход', style: TextStyle( fontSize: 18.0, // Match text style - fontWeight: FontWeight.bold, - fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), ), @@ -68,7 +72,7 @@ class AuthorizationDialog extends StatelessWidget { width: 36, // Match size from other dialogs height: 36, // Match size decoration: BoxDecoration( - color: Color(0xFFD6A067), // Match color from other dialogs + color: const Color(0xFFD6A067), // Match color from other dialogs borderRadius: BorderRadius.circular(8), // Match radius border: Border.all(color: Colors.black, width: 2), // Match border ), diff --git a/frontend/lib/views/create_card_screen.dart b/frontend/lib/views/create_card_screen.dart index 19c0410..3e6310b 100644 --- a/frontend/lib/views/create_card_screen.dart +++ b/frontend/lib/views/create_card_screen.dart @@ -4,6 +4,7 @@ import 'shop_screen.dart'; import 'exchanges_screen.dart'; import 'inventory_screen.dart'; import 'profile_screen.dart'; +import '../utils/auth_utils.dart'; class CreateCardScreen extends StatefulWidget { const CreateCardScreen({super.key}); @@ -18,6 +19,16 @@ class _CreateCardScreenState extends State { final List _categories = ['Категория 1', 'Категория 2', 'Категория 3', 'Категория 4']; String _selectedCategory = 'Выберите категорию'; + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!AuthUtils.checkGuestAccess(context, 'create_card_screen')) { + Navigator.of(context).pop(); + } + }); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -33,10 +44,12 @@ class _CreateCardScreenState extends State { child: InkWell( borderRadius: BorderRadius.circular(20.0), onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ProfileScreen()), - ); + if (AuthUtils.checkGuestAccess(context, 'profile_screen')) { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ProfileScreen()), + ); + } }, child: Image.asset('assets/icons/профиль.png', height: 22), ), diff --git a/frontend/lib/views/create_exchange_screen.dart b/frontend/lib/views/create_exchange_screen.dart index ee47421..78148a7 100644 --- a/frontend/lib/views/create_exchange_screen.dart +++ b/frontend/lib/views/create_exchange_screen.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; +import 'dart:math' as math; import 'home_screen.dart'; import 'shop_screen.dart'; import 'exchanges_screen.dart'; import 'inventory_screen.dart'; import 'profile_screen.dart'; +import '../utils/auth_utils.dart'; +import '../main.dart'; class CreateExchangeScreen extends StatefulWidget { final ExchangeItem? initialExchangeItem; @@ -27,6 +30,11 @@ class _CreateExchangeScreenState extends State { @override void initState() { super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!AuthUtils.checkGuestAccess(context, 'create_exchange_screen')) { + Navigator.of(context).pop(); + } + }); if (widget.initialExchangeItem != null && widget.initialExchangeItem!.otherUserOfferedCardIds.isNotEmpty) { selectedTopCard = { 'id': widget.initialExchangeItem!.otherUserOfferedCardIds[0], @@ -50,7 +58,7 @@ class _CreateExchangeScreenState extends State { context: context, builder: (BuildContext context) { return Dialog( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), child: Container( width: MediaQuery.of(context).size.width * 0.9, height: MediaQuery.of(context).size.height * 0.8, @@ -141,6 +149,9 @@ class _CreateExchangeScreenState extends State { } void _showTopCardSelectionDialog() { + if (!AuthUtils.checkGuestAccess(context, 'create_exchange_screen')) { + return; + } _showCardSelectionDialog( title: 'Выберите вашу карту', onCardSelected: (card) { @@ -154,6 +165,9 @@ class _CreateExchangeScreenState extends State { } void _showLargeCardSelectionDialog() { + if (!AuthUtils.checkGuestAccess(context, 'create_exchange_screen')) { + return; + } _showCardSelectionDialog( title: 'Выберите карту для обмена', onCardSelected: (card) { @@ -171,7 +185,7 @@ class _CreateExchangeScreenState extends State { return Scaffold( backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, leading: Container( margin: const EdgeInsets.all(8.0), @@ -255,83 +269,102 @@ class _CreateExchangeScreenState extends State { child: _buildExchangeCardsRow(), ), // Кнопка создания обмена - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - onPressed: selectedTopCard == null || ((widget.initialExchangeItem != null || widget.cardId == null) && selectedExchangeCards.isEmpty) - ? null // Делаем кнопку неактивной - : () { - // Если мы дошли до сюда, значит, все необходимые проверки пройдены. - // Показываем успешное сообщение и готовимся к переходу. + Container( + width: double.infinity, + padding: EdgeInsets.only( + left: MediaQuery.of(context).size.width * 0.06, + right: MediaQuery.of(context).size.width * 0.06, + bottom: MediaQuery.of(context).padding.bottom + 12.0, + top: 4.0, + ), + child: LayoutBuilder( + builder: (context, constraints) { + final screenWidth = MediaQuery.of(context).size.width; + final screenHeight = MediaQuery.of(context).size.height; + final diagonal = math.sqrt(screenWidth * screenWidth + screenHeight * screenHeight); + final buttonHeight = diagonal * 0.065; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Container( - width: 367, - height: 61, - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(15.0), - ), - padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), - child: Center( - child: Text( - widget.initialExchangeItem != null ? 'Обмен успешно отправлен' : 'Обмен создан', - style: const TextStyle( - color: Colors.black, - fontSize: 18.0, - fontWeight: FontWeight.w500, - fontFamily: 'Jost', + return SizedBox( + height: buttonHeight, + width: double.infinity, + child: ElevatedButton( + onPressed: selectedTopCard == null || ((widget.initialExchangeItem != null || widget.cardId == null) && selectedExchangeCards.isEmpty) + ? null + : () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Container( + width: 367, + height: 61, + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(15.0), + ), + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), + child: Center( + child: Text( + widget.initialExchangeItem != null ? 'Обмен успешно отправлен' : 'Обмен создан', + style: const TextStyle( + color: Colors.black, + fontSize: 18.0, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + ), + textAlign: TextAlign.center, + ), + ), + ), + backgroundColor: Colors.transparent, + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height - 200, + left: 16, + right: 16, ), - textAlign: TextAlign.center, + elevation: 0, ), - ), - ), - backgroundColor: Colors.transparent, - duration: const Duration(seconds: 2), - behavior: SnackBarBehavior.floating, - margin: EdgeInsets.only( - bottom: MediaQuery.of(context).size.height - 200, - left: 16, - right: 16, - ), - elevation: 0, - ), - ); + ); - // Переходим на экран обменов только после успешного показа уведомления - Future.delayed(const Duration(milliseconds: 500), () { - if (context.mounted) { - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - builder: (context) => ExchangesScreen( - initialTabIndex: widget.initialExchangeItem != null ? 1 : 0, - ), - ), - (route) => false, - ); - } - }); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - disabledBackgroundColor: const Color(0xFFD6A067).withOpacity(0.7), - disabledForegroundColor: Colors.black.withOpacity(0.5), - ), - child: Text( - widget.initialExchangeItem != null ? 'Предложить обмен' : 'Создать обмен', - style: const TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.bold, - fontFamily: 'Roboto', - ), - ), + Future.delayed(const Duration(milliseconds: 500), () { + if (context.mounted) { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => MainScreen( + initialIndex: 3, + notification: widget.initialExchangeItem != null ? 'Обмен успешно отправлен' : 'Обмен создан', + ), + ), + (route) => false, + ); + } + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 16.0), + disabledBackgroundColor: const Color(0xFFD6A067).withOpacity(0.7), + disabledForegroundColor: Colors.black.withOpacity(0.5), + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + widget.initialExchangeItem != null ? 'Предложить обмен' : 'Создать обмен', + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w500, + fontFamily: 'Roboto', + ), + ), + ), + ), + ); + }, ), ), ], diff --git a/frontend/lib/views/email_verification_screen.dart b/frontend/lib/views/email_verification_screen.dart index 2b98c35..ab2d776 100644 --- a/frontend/lib/views/email_verification_screen.dart +++ b/frontend/lib/views/email_verification_screen.dart @@ -137,7 +137,7 @@ class _EmailVerificationScreenState extends State { backgroundColor: const Color(0xFFFBF6EF), body: Column( children: [ - const SizedBox(height: 40), + const SizedBox(height: 80), Image.asset( 'assets/icons/карты.png', height: 80, diff --git a/frontend/lib/views/exchange_details_screen.dart b/frontend/lib/views/exchange_details_screen.dart index b472406..7879cfc 100644 --- a/frontend/lib/views/exchange_details_screen.dart +++ b/frontend/lib/views/exchange_details_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'exchanges_screen.dart'; +import '../main.dart'; class ExchangeDetailsScreen extends StatelessWidget { final ExchangeItem exchangeItem; @@ -163,7 +164,8 @@ class ExchangeDetailsScreen extends StatelessWidget { Navigator.pushAndRemoveUntil( context, MaterialPageRoute( - builder: (context) => ExchangesScreen( + builder: (context) => MainScreen( + initialIndex: 3, notification: 'Обмен принят', ), ), @@ -195,7 +197,8 @@ class ExchangeDetailsScreen extends StatelessWidget { Navigator.pushAndRemoveUntil( context, MaterialPageRoute( - builder: (context) => ExchangesScreen( + builder: (context) => MainScreen( + initialIndex: 3, notification: 'Обмен отклонен', ), ), @@ -232,7 +235,8 @@ class ExchangeDetailsScreen extends StatelessWidget { Navigator.pushAndRemoveUntil( context, MaterialPageRoute( - builder: (context) => ExchangesScreen( + builder: (context) => MainScreen( + initialIndex: 3, notification: 'Обмен отклонен', ), ), diff --git a/frontend/lib/views/exchange_proposal_screen.dart b/frontend/lib/views/exchange_proposal_screen.dart index 64b358d..95fac9c 100644 --- a/frontend/lib/views/exchange_proposal_screen.dart +++ b/frontend/lib/views/exchange_proposal_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:cardly/views/create_exchange_screen.dart'; // Import to reuse _buildExchangeCardVisual import 'package:cardly/views/exchanges_screen.dart'; // Import ExchangeItem +import 'package:cardly/main.dart'; // Import MainScreen class ExchangeProposalScreen extends StatelessWidget { final ExchangeItem exchangeItem; @@ -78,66 +79,85 @@ class ExchangeProposalScreen extends StatelessWidget { ), ), ), - const SizedBox(height: 130.0), // Increased space before buttons - // Buttons - ElevatedButton( - onPressed: () { - // TODO: Implement logic for 'Обменяться картой из списка' - Navigator.pop(context); // Close ExchangeProposalScreen - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - builder: (context) => const ExchangesScreen( - notification: 'Обмен успешно совершен', // Notification message - ), - ), - (route) => false, - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: Text( - exchangeItem.myOfferedCardIds.length == 1 ? 'Обменяться картой из списка' : 'Обменяться картами из списка', - style: const TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.bold, - fontFamily: 'Roboto', - ), + const SizedBox(height: 100.0), // Reduced space before buttons + Padding( + padding: EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width * 0.06, // Reduced padding to make buttons wider ), - ), - const SizedBox(height: 10.0), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CreateExchangeScreen( - initialExchangeItem: exchangeItem, + child: Column( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.065, // Match the height from create exchange screen + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => const MainScreen(initialIndex: 3, notification: 'Обмен успешно совершен'), + ), + (route) => false, + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 16.0), + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + exchangeItem.myOfferedCardIds.length == 1 ? 'Обменяться картой из списка' : 'Обменяться картами из списка', + style: const TextStyle( + fontSize: 18.0, // Slightly smaller font size + fontWeight: FontWeight.w500, // Slightly lighter weight + fontFamily: 'Roboto', + ), + ), + ), ), ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: Text( - exchangeItem.myOfferedCardIds.length == 1 ? 'Предложить другую карту' : 'Предложить другие карты', - style: const TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.bold, - fontFamily: 'Roboto', - ), + const SizedBox(height: 10.0), + SizedBox( + height: MediaQuery.of(context).size.height * 0.065, // Match the height from create exchange screen + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CreateExchangeScreen( + initialExchangeItem: exchangeItem, + ), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 16.0), + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + exchangeItem.myOfferedCardIds.length == 1 ? 'Предложить другую карту' : 'Предложить другие карты', + style: const TextStyle( + fontSize: 18.0, // Slightly smaller font size + fontWeight: FontWeight.w500, // Slightly lighter weight + fontFamily: 'Roboto', + ), + ), + ), + ), + ), + ], ), ), ], diff --git a/frontend/lib/views/exchanges_screen.dart b/frontend/lib/views/exchanges_screen.dart index 5de3b0b..c820fd2 100644 --- a/frontend/lib/views/exchanges_screen.dart +++ b/frontend/lib/views/exchanges_screen.dart @@ -7,11 +7,13 @@ import 'exchange_proposal_screen.dart'; import '../services/api_service.dart'; import '../models/trade_model.dart'; import '../utils/error_formatter.dart'; +import '../utils/auth_utils.dart'; +import 'package:provider/provider.dart'; +import '../controllers/auth_controller.dart'; class ExchangesScreen extends StatefulWidget { - final String? notification; final int? initialTabIndex; - const ExchangesScreen({super.key, this.notification, this.initialTabIndex}); + const ExchangesScreen({super.key, this.initialTabIndex}); @override State createState() => _ExchangesScreenState(); @@ -28,49 +30,20 @@ class _ExchangesScreenState extends State with SingleTickerProv void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this, initialIndex: widget.initialTabIndex ?? 0); - // Показываем Snackbar, если notification не null - WidgetsBinding.instance.addPostFrameCallback((_) { - if (widget.notification != null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Container( - width: 367, - height: 61, - decoration: BoxDecoration( - color: const Color(0xFFEAD7C3), - borderRadius: BorderRadius.circular(15.0), - ), - padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), - child: Center( - child: Text( - widget.notification!, - style: const TextStyle( - color: Colors.black, - fontSize: 18.0, - fontWeight: FontWeight.w500, - fontFamily: 'Jost', - ), - textAlign: TextAlign.center, - ), - ), - ), - backgroundColor: Colors.transparent, - duration: const Duration(seconds: 1), - behavior: SnackBarBehavior.floating, - margin: EdgeInsets.only( - bottom: MediaQuery.of(context).size.height - 200, - left: 16, - right: 16, - ), - elevation: 0, - ), - ); + _tabController.addListener(_handleTabChange); + } + + void _handleTabChange() { + if (_tabController.index == 1) { // Мои обмены + if (!AuthUtils.checkGuestAccess(context, 'my_exchanges_screen')) { + _tabController.animateTo(0); // Возвращаемся на вкладку "Обмен" } - }); + } } @override void dispose() { + _tabController.removeListener(_handleTabChange); _tabController.dispose(); super.dispose(); } @@ -82,6 +55,7 @@ class _ExchangesScreenState extends State with SingleTickerProv appBar: AppBar( backgroundColor: const Color(0xFFFBF6EF), elevation: 0, + automaticallyImplyLeading: false, title: Container( padding: const EdgeInsets.all(8.0), ), @@ -126,10 +100,12 @@ class _ExchangesScreenState extends State with SingleTickerProv children: [ InkWell( onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const CreateExchangeScreen()), - ); + if (AuthUtils.checkGuestAccess(context, 'create_exchange_screen')) { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const CreateExchangeScreen()), + ); + } }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 12.0), @@ -164,7 +140,7 @@ class _ExchangesScreenState extends State with SingleTickerProv Container( child: IconButton( - icon: Image.asset('assets/icons/поиск.png', height: 32), + icon: Image.asset('assets/icons/поиск.png', height: 26), onPressed: () { showModalBottomSheet( context: context, diff --git a/frontend/lib/views/home_screen.dart b/frontend/lib/views/home_screen.dart index a18b483..0e89f2a 100644 --- a/frontend/lib/views/home_screen.dart +++ b/frontend/lib/views/home_screen.dart @@ -6,6 +6,9 @@ import 'news_screen.dart'; import 'quests_screen.dart'; import 'profile_screen.dart'; import 'notifications_modal.dart'; +import '../utils/auth_utils.dart'; +import 'package:provider/provider.dart'; +import '../controllers/auth_controller.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @@ -25,12 +28,14 @@ class HomeScreen extends StatelessWidget { child: InkWell( borderRadius: BorderRadius.circular(20.0), onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ProfileScreen()), - ); + if (AuthUtils.checkGuestAccess(context, 'profile_screen')) { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ProfileScreen()), + ); + } }, - child: Image.asset('assets/icons/профиль.png', height: 22), + child: Image.asset('assets/icons/профиль.png', height: 24), ), ), ), @@ -38,7 +43,7 @@ class HomeScreen extends StatelessWidget { IconButton( icon: Image.asset( 'assets/icons/поиск.png', - height: 32, + height: 26, color: Colors.black, ), onPressed: () { @@ -53,14 +58,16 @@ class HomeScreen extends StatelessWidget { IconButton( icon: Image.asset( 'assets/icons/уведомления.png', - height: 36, + height: 30, color: Colors.black, ), onPressed: () { - showDialog( - context: context, - builder: (context) => const NotificationsModal(), - ); + if (AuthUtils.checkGuestAccess(context, 'notifications_screen')) { + showDialog( + context: context, + builder: (context) => const NotificationsModal(), + ); + } }, ), ], @@ -141,32 +148,40 @@ class HomeScreen extends StatelessWidget { const SizedBox(height: 120.0), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width * 0.06, + ), child: SizedBox( width: double.infinity, + height: MediaQuery.of(context).size.height * 0.065, child: ElevatedButton( onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const CreateCardScreen(), - ), - ); + if (AuthUtils.checkGuestAccess(context, 'create_card_screen')) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CreateCardScreen(), + ), + ); + } }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFD6A067), foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 24.0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.0), ), + padding: const EdgeInsets.symmetric(horizontal: 16.0), ), - child: const Text( - 'Создай свою уникальную карточку', - style: TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.w400, - fontFamily: 'Roboto', + child: const FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'Создай свою уникальную карточку', + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w500, + fontFamily: 'Roboto', + ), ), ), ), @@ -176,40 +191,47 @@ class HomeScreen extends StatelessWidget { const SizedBox(height: 32.0), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width * 0.06, + ), child: Row( children: [ Expanded( - child: ElevatedButton( - onPressed: () { - showQuestsDialog(context); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 24.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - 'assets/icons/квесты.png', - height: 42, - color: Colors.black, + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.085, + child: ElevatedButton( + onPressed: () { + if (AuthUtils.checkGuestAccess(context, 'quests_screen')) { + showQuestsDialog(context); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), ), - const SizedBox(height: 0.0), - const Text( - 'Квесты', - style: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.w400, - fontFamily: 'Roboto', + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/icons/квесты.png', + height: 28, + color: Colors.black, ), - ), - ], + const SizedBox(height: 4.0), + const Text( + 'Квесты', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + fontFamily: 'Roboto', + ), + ), + ], + ), ), ), ), @@ -217,41 +239,44 @@ class HomeScreen extends StatelessWidget { const SizedBox(width: 20.0), Expanded( - child: ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const NewsScreen(), + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.085, + child: ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const NewsScreen(), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 24.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - 'assets/icons/новости.png', - height: 42, - color: Colors.black, - ), - const SizedBox(height: 0.0), - const Text( - 'Новости', - style: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.w400, - fontFamily: 'Roboto', + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/icons/новости.png', + height: 28, + color: Colors.black, ), - ), - ], + const SizedBox(height: 4.0), + const Text( + 'Новости', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + fontFamily: 'Roboto', + ), + ), + ], + ), ), ), ), diff --git a/frontend/lib/views/inventory_screen.dart b/frontend/lib/views/inventory_screen.dart index feb8d5e..094a497 100644 --- a/frontend/lib/views/inventory_screen.dart +++ b/frontend/lib/views/inventory_screen.dart @@ -5,6 +5,9 @@ import 'search_players_screen.dart'; import '../services/api_service.dart'; import '../models/card_model.dart'; import '../utils/error_formatter.dart'; +import '../utils/auth_utils.dart'; +import 'package:provider/provider.dart'; +import '../controllers/auth_controller.dart'; class InventoryScreen extends StatefulWidget { final bool isOtherUser; @@ -93,6 +96,15 @@ class _InventoryScreenState extends State { @override Widget build(BuildContext context) { + if (Provider.of(context).currentUser?.isGuest ?? false) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!AuthUtils.checkGuestAccess(context, 'inventory_screen')) { + Navigator.of(context).pop(); + } + }); + return Container(); // Возвращаем пустой контейнер, пока происходит проверка + } + return Scaffold( backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( @@ -184,12 +196,14 @@ class _InventoryScreenState extends State { child: InkWell( borderRadius: BorderRadius.circular(20.0), onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ProfileScreen()), - ); + if (AuthUtils.checkGuestAccess(context, 'profile_screen')) { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ProfileScreen()), + ); + } }, - child: Image.asset('assets/icons/профиль.png', height: 22), + child: Image.asset('assets/icons/профиль.png', height: 24), ), ), ), @@ -219,7 +233,7 @@ class _InventoryScreenState extends State { ), ), IconButton( - icon: Image.asset('assets/icons/поиск.png', height: 32), + icon: Image.asset('assets/icons/поиск.png', height: 26), onPressed: () { showModalBottomSheet( context: context, diff --git a/frontend/lib/views/profile_screen.dart b/frontend/lib/views/profile_screen.dart index f764296..3f3faa9 100644 --- a/frontend/lib/views/profile_screen.dart +++ b/frontend/lib/views/profile_screen.dart @@ -8,6 +8,9 @@ import 'achievements_screen.dart'; import 'profile_image_dialog.dart'; import '../services/api_service.dart'; import '../utils/error_formatter.dart'; +import '../utils/auth_utils.dart'; +import 'package:provider/provider.dart'; +import '../controllers/auth_controller.dart'; class ProfileScreen extends StatefulWidget { final bool isOtherUser; @@ -108,6 +111,15 @@ class _ProfileScreenState extends State { @override Widget build(BuildContext context) { + if (Provider.of(context).currentUser?.isGuest ?? false) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!AuthUtils.checkGuestAccess(context, 'profile_screen')) { + Navigator.of(context).pop(); + } + }); + return Container(); // Return empty container while checking + } + return Scaffold( backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( diff --git a/frontend/lib/views/quests_screen.dart b/frontend/lib/views/quests_screen.dart index 7690680..fa4f1d5 100644 --- a/frontend/lib/views/quests_screen.dart +++ b/frontend/lib/views/quests_screen.dart @@ -6,6 +6,9 @@ import 'news_screen.dart'; import '../services/api_service.dart'; import '../models/quest_model.dart'; import '../utils/error_formatter.dart'; +import '../utils/auth_utils.dart'; +import 'package:provider/provider.dart'; +import '../controllers/auth_controller.dart'; class QuestsDialogContent extends StatefulWidget { const QuestsDialogContent({super.key}); @@ -287,6 +290,15 @@ class _QuestsScreenState extends State { @override Widget build(BuildContext context) { + if (Provider.of(context).currentUser?.isGuest ?? false) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!AuthUtils.checkGuestAccess(context, 'quests_screen')) { + Navigator.of(context).pop(); + } + }); + return Container(); // Return empty container while checking + } + return Scaffold( appBar: AppBar( title: const Text('Квесты'), diff --git a/frontend/lib/views/settings_screen.dart b/frontend/lib/views/settings_screen.dart index d8d90ab..91e2a74 100644 --- a/frontend/lib/views/settings_screen.dart +++ b/frontend/lib/views/settings_screen.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'home_screen.dart'; +import 'package:provider/provider.dart'; +import '../controllers/auth_controller.dart'; class SettingsDialog extends StatefulWidget { const SettingsDialog({super.key}); @@ -105,7 +107,19 @@ class _SettingsDialogState extends State { child: SizedBox( width: double.infinity, child: ElevatedButton( - onPressed:(){}, + onPressed: () async { + try { + await Provider.of(context, listen: false).logout(); + if (!mounted) return; + Navigator.of(context).pop(); // Закрываем диалог настроек + Navigator.of(context).pushNamedAndRemoveUntil('/auth', (route) => false); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Ошибка при выходе: ${e.toString()}')), + ); + } + }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFD6A067), foregroundColor: Colors.black, diff --git a/frontend/lib/views/shop_screen.dart b/frontend/lib/views/shop_screen.dart index c3ed9b3..f411dd1 100644 --- a/frontend/lib/views/shop_screen.dart +++ b/frontend/lib/views/shop_screen.dart @@ -7,6 +7,9 @@ import '../services/api_service.dart'; import '../models/shop_model.dart'; import '../models/card_model.dart'; import '../utils/error_formatter.dart'; +import '../utils/auth_utils.dart'; +import 'package:provider/provider.dart'; +import '../controllers/auth_controller.dart'; class ShopScreen extends StatefulWidget { const ShopScreen({super.key}); @@ -109,12 +112,14 @@ class _ShopScreenState extends State { child: InkWell( borderRadius: BorderRadius.circular(20.0), onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ProfileScreen()), - ); + if (AuthUtils.checkGuestAccess(context, 'profile_screen')) { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ProfileScreen()), + ); + } }, - child: Image.asset('assets/icons/профиль.png', height: 22), + child: Image.asset('assets/icons/профиль.png', height: 24), ), ), ), @@ -145,7 +150,7 @@ class _ShopScreenState extends State { ), ), IconButton( - icon: Image.asset('assets/icons/поиск.png', height: 32), + icon: Image.asset('assets/icons/поиск.png', height: 26), onPressed: () { showModalBottomSheet( context: context, From a69d8c7d2d5c18463eb0ab9bf615dcae13c938b1 Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Tue, 3 Jun 2025 22:59:36 +0300 Subject: [PATCH 43/54] FCCX-153 add yandex appmetrica and some fixes --- frontend/lib/main.dart | 2 + frontend/lib/services/analytics_service.dart | 39 +++++++++++++++++++ frontend/lib/views/achievements_screen.dart | 2 + frontend/lib/views/create_card_screen.dart | 8 ++-- .../lib/views/create_exchange_screen.dart | 3 ++ frontend/lib/views/exchanges_screen.dart | 4 +- frontend/lib/views/inventory_screen.dart | 2 + frontend/lib/views/news_detail_screen.dart | 2 + frontend/lib/views/news_screen.dart | 2 + frontend/lib/views/profile_screen.dart | 2 + frontend/lib/views/quests_screen.dart | 2 + frontend/lib/views/settings_screen.dart | 2 + frontend/lib/views/shop_screen.dart | 2 + 13 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 frontend/lib/services/analytics_service.dart diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index d778527..96673c1 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'controllers/auth_controller.dart'; +import 'services/analytics_service.dart'; import 'views/splash_screen.dart'; import 'views/auth_screen.dart'; import 'views/email_verification_screen.dart'; @@ -15,6 +16,7 @@ import 'views/authorization_dialog.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await AnalyticsService.initialize(); runApp(const MyApp()); } diff --git a/frontend/lib/services/analytics_service.dart b/frontend/lib/services/analytics_service.dart new file mode 100644 index 0000000..6ca185e --- /dev/null +++ b/frontend/lib/services/analytics_service.dart @@ -0,0 +1,39 @@ +import 'package:appmetrica_plugin/appmetrica_plugin.dart'; + +class AnalyticsService { + static const String _apiKey = '9b0749c4-7f6c-4b03-a593-caef66f15f41'; + + static Future initialize() async { + await AppMetrica.activate(AppMetricaConfig(_apiKey)); + } + + static void trackScreenView(String screenName) { + AppMetrica.reportEvent('screen_view_$screenName'); + } + + static void trackCardGeneration() { + AppMetrica.reportEvent('card_generation_started'); + } + + static void trackCardGenerationComplete() { + AppMetrica.reportEvent('card_generation_complete'); + } + + static void trackExchange() { + AppMetrica.reportEvent('exchange_started'); + } + + static void trackExchangeComplete() { + AppMetrica.reportEvent('exchange_complete'); + } + + static void reportError(String errorName, Object error, [StackTrace? stackTrace]) { + AppMetrica.reportError( + message: errorName, + errorDescription: AppMetricaErrorDescription.fromObjectAndStackTrace( + error, + stackTrace ?? StackTrace.current, + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/views/achievements_screen.dart b/frontend/lib/views/achievements_screen.dart index 8a68387..520bfaf 100644 --- a/frontend/lib/views/achievements_screen.dart +++ b/frontend/lib/views/achievements_screen.dart @@ -3,6 +3,7 @@ import '../models/achievement_model.dart'; import '../services/api_service.dart'; import '../utils/error_formatter.dart'; import '../utils/auth_utils.dart'; +import '../services/analytics_service.dart'; class AchievementsScreen extends StatefulWidget { final bool isOtherUser; @@ -29,6 +30,7 @@ class _AchievementsScreenState extends State { _isLoading = true; _achievements = []; _error = ''; + AnalyticsService.trackScreenView('achievements_screen'); WidgetsBinding.instance.addPostFrameCallback((_) { if (AuthUtils.checkGuestAccess(context, 'achievements_screen')) { _loadAchievements(); diff --git a/frontend/lib/views/create_card_screen.dart b/frontend/lib/views/create_card_screen.dart index 3e6310b..f78c756 100644 --- a/frontend/lib/views/create_card_screen.dart +++ b/frontend/lib/views/create_card_screen.dart @@ -5,6 +5,7 @@ import 'exchanges_screen.dart'; import 'inventory_screen.dart'; import 'profile_screen.dart'; import '../utils/auth_utils.dart'; +import '../services/analytics_service.dart'; class CreateCardScreen extends StatefulWidget { const CreateCardScreen({super.key}); @@ -18,10 +19,11 @@ class _CreateCardScreenState extends State { int _currentIndex = 0; final List _categories = ['Категория 1', 'Категория 2', 'Категория 3', 'Категория 4']; String _selectedCategory = 'Выберите категорию'; - + @override void initState() { super.initState(); + AnalyticsService.trackScreenView('create_card_screen'); WidgetsBinding.instance.addPostFrameCallback((_) { if (!AuthUtils.checkGuestAccess(context, 'create_card_screen')) { Navigator.of(context).pop(); @@ -98,7 +100,7 @@ class _CreateCardScreenState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(8.0), ), child: Row( @@ -140,7 +142,7 @@ class _CreateCardScreenState extends State { child: Container( padding: const EdgeInsets.all(12.0), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0).withOpacity(0.95), + color: const Color(0xFFEAD7C3).withOpacity(0.95), borderRadius: BorderRadius.circular(8.0), ), child: Column( diff --git a/frontend/lib/views/create_exchange_screen.dart b/frontend/lib/views/create_exchange_screen.dart index 78148a7..7097d42 100644 --- a/frontend/lib/views/create_exchange_screen.dart +++ b/frontend/lib/views/create_exchange_screen.dart @@ -7,6 +7,7 @@ import 'inventory_screen.dart'; import 'profile_screen.dart'; import '../utils/auth_utils.dart'; import '../main.dart'; +import '../services/analytics_service.dart'; class CreateExchangeScreen extends StatefulWidget { final ExchangeItem? initialExchangeItem; @@ -27,9 +28,11 @@ class _CreateExchangeScreenState extends State { Map? selectedTopCard; List> selectedExchangeCards = []; + @override void initState() { super.initState(); + AnalyticsService.trackScreenView('create_exchange_screen'); WidgetsBinding.instance.addPostFrameCallback((_) { if (!AuthUtils.checkGuestAccess(context, 'create_exchange_screen')) { Navigator.of(context).pop(); diff --git a/frontend/lib/views/exchanges_screen.dart b/frontend/lib/views/exchanges_screen.dart index c820fd2..79bc304 100644 --- a/frontend/lib/views/exchanges_screen.dart +++ b/frontend/lib/views/exchanges_screen.dart @@ -10,6 +10,7 @@ import '../utils/error_formatter.dart'; import '../utils/auth_utils.dart'; import 'package:provider/provider.dart'; import '../controllers/auth_controller.dart'; +import '../services/analytics_service.dart'; class ExchangesScreen extends StatefulWidget { final int? initialTabIndex; @@ -25,10 +26,11 @@ class _ExchangesScreenState extends State with SingleTickerProv bool _showSortOptions = false; bool _isLoading = false; String? _error; - + @override void initState() { super.initState(); + AnalyticsService.trackScreenView('exchanges_screen'); _tabController = TabController(length: 2, vsync: this, initialIndex: widget.initialTabIndex ?? 0); _tabController.addListener(_handleTabChange); } diff --git a/frontend/lib/views/inventory_screen.dart b/frontend/lib/views/inventory_screen.dart index 094a497..7d9d73b 100644 --- a/frontend/lib/views/inventory_screen.dart +++ b/frontend/lib/views/inventory_screen.dart @@ -8,6 +8,7 @@ import '../utils/error_formatter.dart'; import '../utils/auth_utils.dart'; import 'package:provider/provider.dart'; import '../controllers/auth_controller.dart'; +import '../services/analytics_service.dart'; class InventoryScreen extends StatefulWidget { final bool isOtherUser; @@ -41,6 +42,7 @@ class _InventoryScreenState extends State { _isLoading = true; _cards = []; _error = ''; + AnalyticsService.trackScreenView('inventory_screen'); _loadInventory(); } diff --git a/frontend/lib/views/news_detail_screen.dart b/frontend/lib/views/news_detail_screen.dart index e0021cf..8e0d2aa 100644 --- a/frontend/lib/views/news_detail_screen.dart +++ b/frontend/lib/views/news_detail_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../models/news_model.dart'; import '../utils/error_formatter.dart'; +import '../services/analytics_service.dart'; // Remove unused imports // import 'home_screen.dart'; @@ -19,6 +20,7 @@ class NewsDetailScreen extends StatelessWidget { @override Widget build(BuildContext context) { + AnalyticsService.trackScreenView('news_detail_screen'); return Scaffold( appBar: AppBar( title: Text(news.title), diff --git a/frontend/lib/views/news_screen.dart b/frontend/lib/views/news_screen.dart index c7dd7a0..c78869f 100644 --- a/frontend/lib/views/news_screen.dart +++ b/frontend/lib/views/news_screen.dart @@ -5,6 +5,7 @@ import 'search_players_screen.dart'; import '../services/api_service.dart'; import '../models/news_model.dart'; import '../utils/error_formatter.dart'; +import '../services/analytics_service.dart'; class NewsScreen extends StatefulWidget { const NewsScreen({super.key}); @@ -24,6 +25,7 @@ class _NewsScreenState extends State { _isLoading = true; _news = []; _error = ''; + AnalyticsService.trackScreenView('news_screen'); _loadNews(); } diff --git a/frontend/lib/views/profile_screen.dart b/frontend/lib/views/profile_screen.dart index 3f3faa9..ad0786a 100644 --- a/frontend/lib/views/profile_screen.dart +++ b/frontend/lib/views/profile_screen.dart @@ -11,6 +11,7 @@ import '../utils/error_formatter.dart'; import '../utils/auth_utils.dart'; import 'package:provider/provider.dart'; import '../controllers/auth_controller.dart'; +import '../services/analytics_service.dart'; class ProfileScreen extends StatefulWidget { final bool isOtherUser; @@ -49,6 +50,7 @@ class _ProfileScreenState extends State { _isLoading = true; _error = ''; _settings = {}; + AnalyticsService.trackScreenView('profile_screen'); _loadProfileData(); } diff --git a/frontend/lib/views/quests_screen.dart b/frontend/lib/views/quests_screen.dart index fa4f1d5..a31255d 100644 --- a/frontend/lib/views/quests_screen.dart +++ b/frontend/lib/views/quests_screen.dart @@ -9,6 +9,7 @@ import '../utils/error_formatter.dart'; import '../utils/auth_utils.dart'; import 'package:provider/provider.dart'; import '../controllers/auth_controller.dart'; +import '../services/analytics_service.dart'; class QuestsDialogContent extends StatefulWidget { const QuestsDialogContent({super.key}); @@ -25,6 +26,7 @@ class _QuestsDialogContentState extends State with SingleTi void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this, initialIndex: _selectedTab); + AnalyticsService.trackScreenView('quests_dialog_content'); } @override diff --git a/frontend/lib/views/settings_screen.dart b/frontend/lib/views/settings_screen.dart index 91e2a74..6306da7 100644 --- a/frontend/lib/views/settings_screen.dart +++ b/frontend/lib/views/settings_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'home_screen.dart'; import 'package:provider/provider.dart'; import '../controllers/auth_controller.dart'; +import '../services/analytics_service.dart'; class SettingsDialog extends StatefulWidget { const SettingsDialog({super.key}); @@ -27,6 +28,7 @@ class _SettingsDialogState extends State { @override Widget build(BuildContext context) { + AnalyticsService.trackScreenView('settings_dialog'); return GestureDetector( onTapDown: (_) { if (_showHint) { diff --git a/frontend/lib/views/shop_screen.dart b/frontend/lib/views/shop_screen.dart index f411dd1..f19bf30 100644 --- a/frontend/lib/views/shop_screen.dart +++ b/frontend/lib/views/shop_screen.dart @@ -10,6 +10,7 @@ import '../utils/error_formatter.dart'; import '../utils/auth_utils.dart'; import 'package:provider/provider.dart'; import '../controllers/auth_controller.dart'; +import '../services/analytics_service.dart'; class ShopScreen extends StatefulWidget { const ShopScreen({super.key}); @@ -32,6 +33,7 @@ class _ShopScreenState extends State { _coinOffers = []; _error = ''; _loadShopData(); + AnalyticsService.trackScreenView('shop_screen'); } Future _loadShopData() async { From 6e615ccb8471048760448a6b7d247a6baaee040b Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Tue, 3 Jun 2025 23:12:33 +0300 Subject: [PATCH 44/54] FCCX-153 add yandex appmetrica --- frontend/pubspec.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index bd65de1..2d47809 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -42,6 +42,8 @@ dependencies: http: ^1.2.0 shared_preferences: ^2.2.2 email_validator: ^2.1.17 + # Analytics + appmetrica_plugin: ^3.2.0 dev_dependencies: flutter_test: From a193709e8df621bb769e309597691d8ac85e8a56 Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Tue, 3 Jun 2025 23:19:52 +0300 Subject: [PATCH 45/54] delete .DS_Store --- .DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 20ebb06443b91e37987b0c77472b0b57aca4320b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKK~4iP3>-rxR_dikj`;#Vh^q1eJ^<~)g^HDc-uJ}2cv{DHgs4)uaY19rnZ)rV z${f~f0Jc7iH^2(OKzGE4554)mduJCd;%ITkBVI6$Z@c|=>ax!VoO^{W9En%sHhZ>0#ZNDn=>JbiT1f#Z@UIlG#pZ6a=95}&?H=c~w$ShCp7Tj}<2)!F mq8t;W9CP92_&Sm@ulSt%wc(r?bjE{D)X#wHB9j7tt-ujhVjLL& From 09f3962f03c8f81de83c3a2d732a05f4a9fe1af0 Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Tue, 3 Jun 2025 23:35:05 +0300 Subject: [PATCH 46/54] FCCX-153 add metrics --- frontend/lib/views/create_card_screen.dart | 1 + frontend/lib/views/exchanges_screen.dart | 2 ++ 2 files changed, 3 insertions(+) diff --git a/frontend/lib/views/create_card_screen.dart b/frontend/lib/views/create_card_screen.dart index f78c756..4e058ee 100644 --- a/frontend/lib/views/create_card_screen.dart +++ b/frontend/lib/views/create_card_screen.dart @@ -162,6 +162,7 @@ class _CreateCardScreenState extends State { width: double.infinity, child: ElevatedButton( onPressed: () { + AnalyticsService.trackCardGeneration(); // Логика создания карточки }, style: ElevatedButton.styleFrom( diff --git a/frontend/lib/views/exchanges_screen.dart b/frontend/lib/views/exchanges_screen.dart index 79bc304..9a17a35 100644 --- a/frontend/lib/views/exchanges_screen.dart +++ b/frontend/lib/views/exchanges_screen.dart @@ -103,6 +103,7 @@ class _ExchangesScreenState extends State with SingleTickerProv InkWell( onTap: () { if (AuthUtils.checkGuestAccess(context, 'create_exchange_screen')) { + AnalyticsService.trackExchange(); Navigator.push( context, MaterialPageRoute(builder: (context) => const CreateExchangeScreen()), @@ -651,6 +652,7 @@ class _ExchangesScreenState extends State with SingleTickerProv Future _acceptTrade(Trade trade) async { try { await ApiService().acceptTrade(trade.trade_ID); + AnalyticsService.trackExchangeComplete(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Обмен принят')), ); From 4ed85deaa477f23fa7afa0827fe410bdc625eced Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Wed, 4 Jun 2025 01:15:56 +0300 Subject: [PATCH 47/54] FCCX-153 add some metrics for future --- frontend/lib/main.dart | 4 ++++ frontend/lib/services/analytics_service.dart | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 96673c1..3d119ca 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -141,6 +141,8 @@ class _MainScreenState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _pageController.jumpToPage(_currentIndex); + // Track initial screen view + AnalyticsService.trackScreenView(_screenNames[_currentIndex]); if (widget.notification != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -211,6 +213,8 @@ class _MainScreenState extends State { _currentIndex = index; }); _pageController.jumpToPage(index); + // Track screen view when user navigates + AnalyticsService.trackScreenView(_screenNames[index]); } @override diff --git a/frontend/lib/services/analytics_service.dart b/frontend/lib/services/analytics_service.dart index 6ca185e..3cecbb5 100644 --- a/frontend/lib/services/analytics_service.dart +++ b/frontend/lib/services/analytics_service.dart @@ -36,4 +36,14 @@ class AnalyticsService { ), ); } + + static void trackError(String errorName, String errorMessage) { + AppMetrica.reportError( + message: errorName, + errorDescription: AppMetricaErrorDescription.fromObjectAndStackTrace( + errorMessage, + StackTrace.current, + ), + ); + } } \ No newline at end of file From 1b3a4d9b3046c340939ef004e09448772d4e34a9 Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Wed, 4 Jun 2025 04:59:13 +0300 Subject: [PATCH 48/54] FCCX-80 Created adminpanel --- .../V2__add_completed_collections.sql | 7 + .../db/migration/V3__add_role_attribute.sql | 2 + backend/db/migration/V4__new_tables.sql | 62 ++ .../db/migration/V5__connect_card_theme.sql | 10 + .../ru/vsu/app/config/SecurityConfig.kt | 1 + .../ru/vsu/app/controller/AdminController.kt | 387 +++++++++--- .../ru/vsu/app/controller/AuthController.kt | 60 +- .../ru/vsu/app/controller/CardController.kt | 38 +- .../vsu/app/controller/InventoryController.kt | 583 ++++++++++-------- .../src/main/kotlin/ru/vsu/app/dto/CardDto.kt | 50 +- .../src/main/kotlin/ru/vsu/app/dto/UserDto.kt | 1 - .../inventory/InventoryGet200Response.kt | 10 +- .../inventory/InventoryGet401Response.kt | 34 + .../ru/vsu/app/mapper/AchievementMapper.kt | 16 +- .../kotlin/ru/vsu/app/mapper/CardMapper.kt | 27 +- .../ru/vsu/app/mapper/CollectionMapper.kt | 20 + .../ru/vsu/app/mapper/NotificationMapper.kt | 16 +- .../kotlin/ru/vsu/app/mapper/ThemeMapper.kt | 25 + .../ru/vsu/app/metrics/InventoryMetrics.kt | 89 +++ .../kotlin/ru/vsu/app/model/CardEntity.kt | 9 +- .../ru/vsu/app/model/CollectionEntity.kt | 2 +- .../ru/vsu/app/model/NotificationEntity.kt | 2 +- .../kotlin/ru/vsu/app/model/PaymentEntity.kt | 45 ++ .../kotlin/ru/vsu/app/model/QuestEntity.kt | 2 +- .../kotlin/ru/vsu/app/model/ReportEntity.kt | 4 +- .../kotlin/ru/vsu/app/model/ThemeEntity.kt | 19 + .../kotlin/ru/vsu/app/model/TradeEntity.kt | 44 ++ .../kotlin/ru/vsu/app/model/UserEntity.kt | 30 +- .../ru/vsu/app/model/UserSettingsEntity.kt | 26 + .../ru/vsu/app/model/UserStatsEntity.kt | 2 +- .../ru/vsu/app/repository/CardRepository.kt | 3 +- .../app/repository/CollectionRepository.kt | 2 + .../vsu/app/repository/InventoryRepository.kt | 8 + .../ru/vsu/app/repository/ThemeRepository.kt | 6 + .../ru/vsu/app/repository/UserRepository.kt | 2 +- .../ru/vsu/app/security/CustomUserDetails.kt | 29 + .../app/security/CustomUserDetailsService.kt | 7 +- .../app/security/JwtAuthenticationFilter.kt | 7 +- .../ru/vsu/app/service/AchievementService.kt | 21 + .../kotlin/ru/vsu/app/service/AdminService.kt | 12 + .../ru/vsu/app/service/CardAdminService.kt | 9 + .../kotlin/ru/vsu/app/service/CardService.kt | 21 + .../ru/vsu/app/service/CoinOfferService.kt | 26 + .../vsu/app/service/CollectionAdminService.kt | 9 + .../ru/vsu/app/service/CollectionService.kt | 21 + .../ru/vsu/app/service/InventoryService.kt | 139 ++--- .../kotlin/ru/vsu/app/service/NewsService.kt | 48 +- .../kotlin/ru/vsu/app/service/PackService.kt | 9 + .../ru/vsu/app/service/ReportService.kt | 45 ++ .../kotlin/ru/vsu/app/service/ThemeService.kt | 36 ++ .../ru/vsu/app/service/TradeAdminService.kt | 10 + .../kotlin/ru/vsu/app/service/TradeService.kt | 19 + .../kotlin/ru/vsu/app/service/UserService.kt | 225 ++----- 53 files changed, 1537 insertions(+), 800 deletions(-) create mode 100644 backend/db/migration/V2__add_completed_collections.sql create mode 100644 backend/db/migration/V3__add_role_attribute.sql create mode 100644 backend/db/migration/V4__new_tables.sql create mode 100644 backend/db/migration/V5__connect_card_theme.sql create mode 100644 backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet401Response.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/mapper/CollectionMapper.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/mapper/ThemeMapper.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/metrics/InventoryMetrics.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/model/PaymentEntity.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/model/ThemeEntity.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/model/TradeEntity.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/model/UserSettingsEntity.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/repository/InventoryRepository.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/repository/ThemeRepository.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/security/CustomUserDetails.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/service/AchievementService.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/service/AdminService.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/service/CardAdminService.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/service/CardService.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/service/CoinOfferService.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/service/CollectionAdminService.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/service/CollectionService.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/service/PackService.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/service/ReportService.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/service/ThemeService.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/service/TradeAdminService.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/service/TradeService.kt diff --git a/backend/db/migration/V2__add_completed_collections.sql b/backend/db/migration/V2__add_completed_collections.sql new file mode 100644 index 0000000..dccdedb --- /dev/null +++ b/backend/db/migration/V2__add_completed_collections.sql @@ -0,0 +1,7 @@ +CREATE TABLE user_completed_collections ( + user_id INT NOT NULL, + collection_id INT NOT NULL, + PRIMARY KEY (user_id, collection_id), + FOREIGN KEY (user_id) REFERENCES users(user_id), + FOREIGN KEY (collection_id) REFERENCES collections(collection_id) +); \ No newline at end of file diff --git a/backend/db/migration/V3__add_role_attribute.sql b/backend/db/migration/V3__add_role_attribute.sql new file mode 100644 index 0000000..12fcf4d --- /dev/null +++ b/backend/db/migration/V3__add_role_attribute.sql @@ -0,0 +1,2 @@ +ALTER TABLE users +ADD COLUMN role VARCHAR(50) NOT NULL DEFAULT 'USER'; diff --git a/backend/db/migration/V4__new_tables.sql b/backend/db/migration/V4__new_tables.sql new file mode 100644 index 0000000..5fcf44f --- /dev/null +++ b/backend/db/migration/V4__new_tables.sql @@ -0,0 +1,62 @@ +-- Таблица тем +CREATE TABLE themes ( + theme_id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT +); + +-- Таблица настроек пользователя +CREATE TABLE user_settings ( + settings_id SERIAL PRIMARY KEY, + notifications_enabled BOOLEAN NOT NULL, + show_inventory BOOLEAN NOT NULL, + auto_decline_trades BOOLEAN NOT NULL, + user_id INT NOT NULL UNIQUE, + CONSTRAINT fk_user_settings_user FOREIGN KEY (user_id) REFERENCES users(user_id) +); + +-- Таблица платежей +CREATE TABLE payments ( + payment_id SERIAL PRIMARY KEY, + user_id INT NOT NULL, + payment_sum DOUBLE PRECISION NOT NULL, + payment_status VARCHAR(50) NOT NULL, + payment_datetime TIMESTAMP NOT NULL, + CONSTRAINT fk_payments_user FOREIGN KEY (user_id) REFERENCES users(user_id) +); + +-- Таблица с данными карты (элемент коллекции) +CREATE TABLE payment_card_data ( + payment_id INT NOT NULL, + card_data TEXT, + CONSTRAINT fk_payment_card_data FOREIGN KEY (payment_id) REFERENCES payments(payment_id) +); + +-- Таблица обменов +CREATE TABLE trades ( + trade_id SERIAL PRIMARY KEY, + offering_user_id INT NOT NULL, + receiving_user_id INT NOT NULL, + is_confirmed BOOLEAN NOT NULL, + trade_date_time TIMESTAMP NOT NULL, + CONSTRAINT fk_trades_offering_user FOREIGN KEY (offering_user_id) REFERENCES users(user_id), + CONSTRAINT fk_trades_receiving_user FOREIGN KEY (receiving_user_id) REFERENCES users(user_id) +); + +-- Связь trade -> offeringCards +CREATE TABLE trade_offering_cards ( + trade_id INT NOT NULL, + card_id INT NOT NULL, + PRIMARY KEY (trade_id, card_id), + CONSTRAINT fk_trade_offering_cards_trade FOREIGN KEY (trade_id) REFERENCES trades(trade_id), + CONSTRAINT fk_trade_offering_cards_card FOREIGN KEY (card_id) REFERENCES cards(id) +); + +-- Связь trade -> receivingCards +CREATE TABLE trade_receiving_cards ( + trade_id INT NOT NULL, + card_id INT NOT NULL, + PRIMARY KEY (trade_id, card_id), + CONSTRAINT fk_trade_receiving_cards_trade FOREIGN KEY (trade_id) REFERENCES trades(trade_id), + CONSTRAINT fk_trade_receiving_cards_card FOREIGN KEY (card_id) REFERENCES cards(id) +); diff --git a/backend/db/migration/V5__connect_card_theme.sql b/backend/db/migration/V5__connect_card_theme.sql new file mode 100644 index 0000000..e74ac78 --- /dev/null +++ b/backend/db/migration/V5__connect_card_theme.sql @@ -0,0 +1,10 @@ +ALTER TABLE cards + ADD COLUMN theme_id INT; + +ALTER TABLE cards + ADD CONSTRAINT fk_cards_theme + FOREIGN KEY (theme_id) + REFERENCES themes(theme_id); + +ALTER TABLE cards + DROP COLUMN theme; diff --git a/backend/src/main/kotlin/ru/vsu/app/config/SecurityConfig.kt b/backend/src/main/kotlin/ru/vsu/app/config/SecurityConfig.kt index f967653..db54275 100644 --- a/backend/src/main/kotlin/ru/vsu/app/config/SecurityConfig.kt +++ b/backend/src/main/kotlin/ru/vsu/app/config/SecurityConfig.kt @@ -30,6 +30,7 @@ class SecurityConfig( http .csrf { it.disable() } .authorizeHttpRequests { + it.requestMatchers("/admin/**").hasRole("ADMIN") it.requestMatchers(*publicEndpointsConfig.endpoints.toTypedArray(), *publicEndpointsConfig.passiveTokenEndpoints.toTypedArray()).permitAll() .anyRequest().authenticated() } diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt index d872bda..6f19688 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt @@ -1,6 +1,5 @@ package ru.vsu.app.controller -import ru.vsu.app.dto.AchievementDto import ru.vsu.app.dto.requests.AdminReportsReportIDPutRequest import ru.vsu.app.dto.responses.admin.AdminStatsGet200Response import ru.vsu.app.dto.responses.admin.AdminTradesTradeIDInvalidatePost200Response @@ -10,6 +9,9 @@ import ru.vsu.app.dto.requests.AdminUsersUserIDBanPostRequest import ru.vsu.app.dto.responses.admin.AdminUsersUserIDQuestsQuestIDResetPost200Response import ru.vsu.app.dto.requests.AdminUsersUserIDQuestsQuestIDResetPostRequest import ru.vsu.app.dto.responses.admin.AdminUsersUserIDUnbanPost200Response +import ru.vsu.app.model.ThemeEntity + +import ru.vsu.app.dto.AchievementDto import ru.vsu.app.dto.CardDto import ru.vsu.app.dto.CoinOfferDto import ru.vsu.app.dto.CollectionDto @@ -18,18 +20,33 @@ import ru.vsu.app.dto.PackDto import ru.vsu.app.dto.responses.common.InternalServerError import ru.vsu.app.dto.ReportDto import ru.vsu.app.dto.TradeDto + +import ru.vsu.app.service.AchievementService +import ru.vsu.app.service.CardService +import ru.vsu.app.service.CoinOfferService +import ru.vsu.app.service.CollectionService +import ru.vsu.app.service.NewsService +import ru.vsu.app.service.PackService +import ru.vsu.app.service.ReportService +import ru.vsu.app.service.AdminService +import ru.vsu.app.service.TradeService +import ru.vsu.app.service.ThemeService +import ru.vsu.app.service.UserService + import io.swagger.v3.oas.annotations.* import io.swagger.v3.oas.annotations.enums.* import io.swagger.v3.oas.annotations.media.* import io.swagger.v3.oas.annotations.responses.* import io.swagger.v3.oas.annotations.security.* import io.swagger.v3.oas.annotations.tags.Tag + import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import org.springframework.validation.annotation.Validated +import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.context.request.NativeWebRequest import org.springframework.beans.factory.annotation.Autowired @@ -48,10 +65,23 @@ import kotlin.collections.Map @RestController @Validated +@PreAuthorize("hasRole('ADMIN')") @RequestMapping("\${api.base-path:/api}") @SecurityRequirement(name = "Bearer Authentication") @Tag(name = "Admin", description = "Функции администратора") -class AdminController() { +class AdminController( + private val achievementService: AchievementService, + private val cardService: CardService, + private val coinOfferService: CoinOfferService, + private val collectionService: CollectionService, + private val newsService: NewsService, + private val packService: PackService, + private val reportService: ReportService, + private val adminService: AdminService, + private val tradeService: TradeService, + private val userService: UserService, + private val themeService: ThemeService +) { @Operation( summary = "Удаление достижения", @@ -69,9 +99,14 @@ class AdminController() { value = ["/admin/achievements/{achievement_ID}"], produces = ["application/json"] ) - fun adminAchievementsAchievementIDDelete(@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminAchievementsAchievementIDDelete(@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int): ResponseEntity { + try { + achievementService.deleteAchievement(achievementID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении достижения", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Обновление достижения", @@ -90,9 +125,14 @@ class AdminController() { produces = ["application/json"], consumes = ["application/json"] ) - fun adminAchievementsAchievementIDPut(@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody achievement: AchievementDto): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminAchievementsAchievementIDPut(@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody achievement: AchievementDto): ResponseEntity { + try { + val updatedAchievement = achievementService.updateAchievement(achievementID, achievement) + return ResponseEntity.ok(updatedAchievement) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при обновлении достижения", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Создание достижения", @@ -111,9 +151,14 @@ class AdminController() { produces = ["application/json"], consumes = ["application/json"] ) - fun adminAchievementsPost(@Parameter(description = "", required = true) @Valid @RequestBody achievement: AchievementDto): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminAchievementsPost(@Parameter(description = "", required = true) @Valid @RequestBody achievement: AchievementDto): ResponseEntity { + try { + val createdAchievement = achievementService.createAchievement(achievement) + return ResponseEntity.status(HttpStatus.CREATED).body(createdAchievement) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при создании достижения", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Удаление карты", @@ -131,9 +176,14 @@ class AdminController() { value = ["/admin/cards/{card_ID}"], produces = ["application/json"] ) - fun adminCardsCardIDDelete(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminCardsCardIDDelete(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + try { + cardService.deleteCard(cardID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении карты", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Обновление карты", @@ -152,9 +202,14 @@ class AdminController() { produces = ["application/json"], consumes = ["application/json"] ) - fun adminCardsCardIDPut(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody card: CardDto): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminCardsCardIDPut(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody card: CardDto): ResponseEntity { + try { + val updatedCard = cardService.updateCard(cardID, card) + return ResponseEntity.ok(updatedCard) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при обновлении карты", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Создание новой карты", @@ -173,9 +228,14 @@ class AdminController() { produces = ["application/json"], consumes = ["application/json"] ) - fun adminCardsPost(@Parameter(description = "", required = true) @Valid @RequestBody card: CardDto): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminCardsPost(@Parameter(description = "", required = true) @Valid @RequestBody card: CardDto): ResponseEntity { + try { + val createdCard = cardService.createCard(card) + return ResponseEntity.status(HttpStatus.CREATED).body(createdCard) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при создании карты", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Получение списка предложений покупки монет", @@ -193,9 +253,14 @@ class AdminController() { value = ["/admin/coin-offers"], produces = ["application/json"] ) - fun adminCoinOffersGet(): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminCoinOffersGet(): ResponseEntity { + try { + val coinOffers = coinOfferService.getAllCoinOffers() + return ResponseEntity.ok(coinOffers) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при получении списка предложений покупки монет", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Удаление предложения покупки монет", @@ -213,9 +278,14 @@ class AdminController() { value = ["/admin/coin-offers/{offer_id}"], produces = ["application/json"] ) - fun adminCoinOffersOfferIdDelete(@Parameter(description = "", required = true) @PathVariable("offer_id") offerId: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminCoinOffersOfferIdDelete(@Parameter(description = "", required = true) @PathVariable("offer_id") offerId: kotlin.Int): ResponseEntity { + try { + coinOfferService.deleteCoinOffer(offerId) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении предложения покупки монет", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Обновление предложения покупки монет", @@ -234,9 +304,14 @@ class AdminController() { produces = ["application/json"], consumes = ["application/json"] ) - fun adminCoinOffersOfferIdPut(@Parameter(description = "", required = true) @PathVariable("offer_id") offerId: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody coinOffer: CoinOfferDto): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminCoinOffersOfferIdPut(@Parameter(description = "", required = true) @PathVariable("offer_id") offerId: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody coinOffer: CoinOfferDto): ResponseEntity { + try { + val updatedCoinOffer = coinOfferService.updateCoinOffer(offerId, coinOffer) + return ResponseEntity.ok(updatedCoinOffer) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при обновлении предложения покупки монет", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Создание предложения покупки монет", @@ -255,9 +330,14 @@ class AdminController() { produces = ["application/json"], consumes = ["application/json"] ) - fun adminCoinOffersPost(@Parameter(description = "", required = true) @Valid @RequestBody coinOffer: CoinOfferDto): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminCoinOffersPost(@Parameter(description = "", required = true) @Valid @RequestBody coinOffer: CoinOfferDto): ResponseEntity { + try { + val createdCoinOffer = coinOfferService.createCoinOffer(coinOffer) + return ResponseEntity.status(HttpStatus.CREATED).body(createdCoinOffer) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при создании предложения покупки монет", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Удаление коллекцию", @@ -275,9 +355,14 @@ class AdminController() { value = ["/admin/collections/{collection_ID}"], produces = ["application/json"] ) - fun adminCollectionsCollectionIDDelete(@Parameter(description = "", required = true) @PathVariable("collection_ID") collectionID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminCollectionsCollectionIDDelete(@Parameter(description = "", required = true) @PathVariable("collection_ID") collectionID: kotlin.Int): ResponseEntity { + try { + collectionService.deleteCollection(collectionID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении коллекции", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Обновление коллекции", @@ -296,9 +381,14 @@ class AdminController() { produces = ["application/json"], consumes = ["application/json"] ) - fun adminCollectionsCollectionIDPut(@Parameter(description = "", required = true) @PathVariable("collection_ID") collectionID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody collection: CollectionDto): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminCollectionsCollectionIDPut(@Parameter(description = "", required = true) @PathVariable("collection_ID") collectionID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody collection: CollectionDto): ResponseEntity { + try { + val updatedCollection = collectionService.updateCollection(collectionID, collection) + return ResponseEntity.ok(updatedCollection) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при обновлении коллекции", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Создание новой коллекции", @@ -317,9 +407,14 @@ class AdminController() { produces = ["application/json"], consumes = ["application/json"] ) - fun adminCollectionsPost(@Parameter(description = "", required = true) @Valid @RequestBody collection: CollectionDto): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminCollectionsPost(@Parameter(description = "", required = true) @Valid @RequestBody collection: CollectionDto): ResponseEntity { + try { + val createdCollection = collectionService.createCollection(collection) + return ResponseEntity.status(HttpStatus.CREATED).body(createdCollection) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при создании коллекции", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Удаление новости", @@ -337,9 +432,14 @@ class AdminController() { value = ["/admin/news/{news_ID}"], produces = ["application/json"] ) - fun adminNewsNewsIDDelete(@Parameter(description = "", required = true) @PathVariable("news_ID") newsID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminNewsNewsIDDelete(@Parameter(description = "", required = true) @PathVariable("news_ID") newsID: kotlin.Int): ResponseEntity { + try { + newsService.deleteNews(newsID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении новости", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Обновление новости", @@ -358,9 +458,14 @@ class AdminController() { produces = ["application/json"], consumes = ["application/json"] ) - fun adminNewsNewsIDPut(@Parameter(description = "", required = true) @PathVariable("news_ID") newsID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody news: NewsDto): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminNewsNewsIDPut(@Parameter(description = "", required = true) @PathVariable("news_ID") newsID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody news: NewsDto): ResponseEntity { + try { + val updatedNews = newsService.updateNews(newsID, news) + return ResponseEntity.ok(updatedNews) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при обновлении новости", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Создание новости", @@ -379,9 +484,14 @@ class AdminController() { produces = ["application/json"], consumes = ["application/json"] ) - fun adminNewsPost(@Parameter(description = "", required = true) @Valid @RequestBody news: NewsDto): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminNewsPost(@Parameter(description = "", required = true) @Valid @RequestBody news: NewsDto): ResponseEntity { + try { + val createdNews = newsService.createNews(news) + return ResponseEntity.status(HttpStatus.CREATED).body(createdNews) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при создании новости", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Удаление набор", @@ -399,9 +509,14 @@ class AdminController() { value = ["/admin/packs/{pack_ID}"], produces = ["application/json"] ) - fun adminPacksPackIDDelete(@Parameter(description = "", required = true) @PathVariable("pack_ID") packID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminPacksPackIDDelete(@Parameter(description = "", required = true) @PathVariable("pack_ID") packID: kotlin.Int): ResponseEntity { + try { + packService.deletePack(packID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении набора", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Обновление набора", @@ -420,9 +535,14 @@ class AdminController() { produces = ["application/json"], consumes = ["application/json"] ) - fun adminPacksPackIDPut(@Parameter(description = "", required = true) @PathVariable("pack_ID") packID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody pack: PackDto): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminPacksPackIDPut(@Parameter(description = "", required = true) @PathVariable("pack_ID") packID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody pack: PackDto): ResponseEntity { + try { + val updatedPack = packService.updatePack(packID, pack) + return ResponseEntity.ok(updatedPack) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при обновлении набора", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Создание нового набора", @@ -441,9 +561,14 @@ class AdminController() { produces = ["application/json"], consumes = ["application/json"] ) - fun adminPacksPost(@Parameter(description = "", required = true) @Valid @RequestBody pack: PackDto): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminPacksPost(@Parameter(description = "", required = true) @Valid @RequestBody pack: PackDto): ResponseEntity { + try { + val createdPack = packService.createPack(pack) + return ResponseEntity.status(HttpStatus.CREATED).body(createdPack) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при создании набора", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Просмотр списка жалоб", @@ -461,9 +586,14 @@ class AdminController() { value = ["/admin/reports"], produces = ["application/json"] ) - fun adminReportsGet(): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminReportsGet(): ResponseEntity { + try { + val reports = reportService.getAllReports() + return ResponseEntity.ok(reports) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при получении списка жалоб", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Просмотр деталей жалобы", @@ -481,31 +611,68 @@ class AdminController() { value = ["/admin/reports/{report_ID}"], produces = ["application/json"] ) - fun adminReportsReportIDGet(@Parameter(description = "", required = true) @PathVariable("report_ID") reportID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminReportsReportIDGet(@Parameter(description = "", required = true) @PathVariable("report_ID") reportID: kotlin.Int): ResponseEntity { + try { + val report = reportService.getReportById(reportID) + return ResponseEntity.ok(report) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при получении деталей жалобы", e.message ?: "Неизвестная ошибка")) } +} - @Operation( - summary = "Обновление статуса жалобы", - operationId = "adminReportsReportIDPut", - description = """Обновляет статус жалобы и применяет меры к пользователю""", - responses = [ - ApiResponse(responseCode = "200", description = "Статус жалобы обновлен", content = [Content(schema = Schema(implementation = ReportDto::class))]), - ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), - ApiResponse(responseCode = "403", description = "Доступ запрещен"), - ApiResponse(responseCode = "404", description = "Жалоба не найдена"), - ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], - security = [ SecurityRequirement(name = "bearerAuth") ] - ) - @RequestMapping( - method = [RequestMethod.PUT], - value = ["/admin/reports/{report_ID}"], - produces = ["application/json"], - consumes = ["application/json"] - ) - fun adminReportsReportIDPut(@Parameter(description = "", required = true) @PathVariable("report_ID") reportID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody adminReportsReportIDPutRequest: AdminReportsReportIDPutRequest): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +@Operation( + summary = "Разблокировка пользователя", + operationId = "adminUsersUserIDUnbanPost", + description = """Снимает блокировку с пользователя""", + responses = [ + ApiResponse(responseCode = "200", description = "Пользователь разблокирован", content = [Content(schema = Schema(implementation = AdminUsersUserIDUnbanPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "404", description = "Пользователь не найден"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) + ], + security = [ SecurityRequirement(name = "bearerAuth") ] +) +@RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/users/{user_id}/unban"], + produces = ["application/json"] +) +fun adminUsersUserIDUnbanPost(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int): ResponseEntity { + try { + val result = userService.unbanUser(userID) + return ResponseEntity.ok(result) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при разблокировке пользователя", e.message ?: "Неизвестная ошибка")) + } +} + +@Operation( + summary = "Создание новой темы", + operationId = "adminThemesPost", + description = """Создает новую тему в системе""", + responses = [ + ApiResponse(responseCode = "201", description = "Тема успешно создана", content = [Content(schema = Schema(implementation = ThemeDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) + ], + security = [ SecurityRequirement(name = "bearerAuth") ] +) +@RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/themes"], + produces = ["application/json"], + consumes = ["application/json"] +) +fun adminThemesPost(@Parameter(description = "", required = true) @Valid @RequestBody theme: ThemeDto): ResponseEntity { + try { + val createdTheme = themeService.createTheme(theme) + return ResponseEntity.status(HttpStatus.CREATED).body(createdTheme) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при создании темы", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Получение статистики системы", @@ -523,9 +690,14 @@ class AdminController() { value = ["/admin/stats"], produces = ["application/json"] ) - fun adminStatsGet(): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminStatsGet(): ResponseEntity { + try { + val stats = adminService.getSystemStats() + return ResponseEntity.ok(stats) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при получении статистики системы", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Получение списка обменов", @@ -543,9 +715,14 @@ class AdminController() { value = ["/admin/trades"], produces = ["application/json"] ) - fun adminTradesGet(): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminTradesGet(): ResponseEntity { + try { + val trades = tradeService.getAllTrades() + return ResponseEntity.ok(trades) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при получении списка обменов", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Признание обмена недействительным", @@ -565,9 +742,14 @@ class AdminController() { produces = ["application/json"], consumes = ["application/json"] ) - fun adminTradesTradeIDInvalidatePost(@Parameter(description = "", required = true) @PathVariable("trade_ID") tradeID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody adminTradesTradeIDInvalidatePostRequest: AdminTradesTradeIDInvalidatePostRequest): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminTradesTradeIDInvalidatePost(@Parameter(description = "", required = true) @PathVariable("trade_ID") tradeID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody adminTradesTradeIDInvalidatePostRequest: AdminTradesTradeIDInvalidatePostRequest): ResponseEntity { + try { + val result = tradeService.invalidateTrade(tradeID, adminTradesTradeIDInvalidatePostRequest) + return ResponseEntity.ok(result) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при аннулировании обмена", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Отзыв достижения у пользователя", @@ -586,9 +768,14 @@ class AdminController() { value = ["/admin/users/{user_id}/achievements/{achievement_ID}"], produces = ["application/json"] ) - fun adminUsersUserIDAchievementsAchievementIDDelete(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminUsersUserIDAchievementsAchievementIDDelete(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int): ResponseEntity { + try { + userService.revokeAchievement(userID, achievementID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при отзыве достижения у пользователя", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Блокировка пользователя", @@ -608,9 +795,14 @@ class AdminController() { produces = ["application/json"], consumes = ["application/json"] ) - fun adminUsersUserIDBanPost(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody adminUsersUserIDBanPostRequest: AdminUsersUserIDBanPostRequest): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminUsersUserIDBanPost(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody adminUsersUserIDBanPostRequest: AdminUsersUserIDBanPostRequest): ResponseEntity { + try { + val result = userService.banUser(userID, adminUsersUserIDBanPostRequest) + return ResponseEntity.ok(result) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при блокировке пользователя", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Удаление пользователя", @@ -629,9 +821,14 @@ class AdminController() { value = ["/admin/users/{user_id}/delete"], produces = ["application/json"] ) - fun adminUsersUserIDDeleteDelete(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminUsersUserIDDeleteDelete(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int): ResponseEntity { + try { + userService.deleteUser(userID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении пользователя", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Удаление карты из инвентаря пользователя", @@ -650,9 +847,14 @@ class AdminController() { value = ["/admin/users/{user_id}/inventory/{card_ID}"], produces = ["application/json"] ) - fun adminUsersUserIDInventoryCardIDDelete(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminUsersUserIDInventoryCardIDDelete(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + try { + userService.deleteCardFromInventory(userID, cardID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении карты из инвентаря пользователя", e.message ?: "Неизвестная ошибка")) } +} @Operation( summary = "Сброс выполнения квеста", @@ -672,28 +874,13 @@ class AdminController() { produces = ["application/json"], consumes = ["application/json"] ) - fun adminUsersUserIDQuestsQuestIDResetPost(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @PathVariable("quest_ID") questID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody adminUsersUserIDQuestsQuestIDResetPostRequest: AdminUsersUserIDQuestsQuestIDResetPostRequest): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) +fun adminUsersUserIDQuestsQuestIDResetPost(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @PathVariable("quest_ID") questID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody adminUsersUserIDQuestsQuestIDResetPostRequest: AdminUsersUserIDQuestsQuestIDResetPostRequest): ResponseEntity { + try { + val result = userService.resetQuest(userID, questID, adminUsersUserIDQuestsQuestIDResetPostRequest) + return ResponseEntity.ok(result) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при сбросе выполнения квеста", e.message ?: "Неизвестная ошибка")) } +} - @Operation( - summary = "Разблокировка пользователя", - operationId = "adminUsersUserIDUnbanPost", - description = """Снимает блокировку с пользователя""", - responses = [ - ApiResponse(responseCode = "200", description = "Пользователь разблокирован", content = [Content(schema = Schema(implementation = AdminUsersUserIDUnbanPost200Response::class))]), - ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), - ApiResponse(responseCode = "403", description = "Доступ запрещен"), - ApiResponse(responseCode = "404", description = "Пользователь не найден"), - ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], - security = [ SecurityRequirement(name = "bearerAuth") ] - ) - @RequestMapping( - method = [RequestMethod.POST], - value = ["/admin/users/{user_id}/unban"], - produces = ["application/json"] - ) - fun adminUsersUserIDUnbanPost(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) - } } diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt index 0688f6e..a963133 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt @@ -97,25 +97,25 @@ class AuthController( value = ["/auth/check"], produces = ["application/json"] ) - fun authCheckGet(): ResponseEntity { - authMetrics.authCheckAttempt() - val startTime = System.currentTimeMillis() - - val user = authService.getCurrentUser() - val response: ResponseEntity = if (user != null) { - authMetrics.authCheckAuthorized() - ResponseEntity.ok(user) - } else { - authMetrics.authCheckUnauthorized() - ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body(AuthCheckGet401Response(false, "Пользователь не авторизован")) - } + fun authCheckGet(): ResponseEntity { + authMetrics.authCheckAttempt() + val startTime = System.currentTimeMillis() + + val user = authService.getCurrentUser() + val response: ResponseEntity = if (user != null) { + authMetrics.authCheckAuthorized() + ResponseEntity.ok(user) + } else { + authMetrics.authCheckUnauthorized() + ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(AuthCheckGet401Response(false, "Пользователь не авторизован")) + } - val durationMs = System.currentTimeMillis() - startTime - authMetrics.authCheckDuration(durationMs) + val durationMs = System.currentTimeMillis() - startTime + authMetrics.authCheckDuration(durationMs) - return response - } + return response + } @Operation( summary = "Обновление сессии", @@ -134,22 +134,22 @@ class AuthController( value = ["/auth/refresh"], produces = ["application/json"] ) - fun authRefreshPost( - @Parameter(description = "", required = true) - @Valid @RequestBody request: AuthRefreshRequest - ): ResponseEntity { - val start = System.currentTimeMillis() - authMetrics.refreshAttempt() + fun authRefreshPost( + @Parameter(description = "", required = true) + @Valid @RequestBody request: AuthRefreshRequest + ): ResponseEntity { + val start = System.currentTimeMillis() + authMetrics.refreshAttempt() - val result = authService.refreshSession(request) + val result = authService.refreshSession(request) - authMetrics.refreshDuration(System.currentTimeMillis() - start) + authMetrics.refreshDuration(System.currentTimeMillis() - start) - return when (result) { - is AuthRefreshToken200Response -> { - authMetrics.refreshSuccess() - ResponseEntity.ok(result) - } + return when (result) { + is AuthRefreshToken200Response -> { + authMetrics.refreshSuccess() + ResponseEntity.ok(result) + } is AuthRefreshToken401Response -> { authMetrics.refreshFailure() ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(result) diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt index 400139b..1e3cf77 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt @@ -21,25 +21,25 @@ // private val cardService: InventoryService // ) { -// // @Operation( -// // summary = "Получить инвентарь пользователя", -// // description = "Возвращает список карточек, принадлежащих пользователю. Можно указать параметр сортировки (по умолчанию — по редкости).", -// // parameters = [ -// // Parameter(name = "sortBy", description = "Критерий сортировки (rarity, name, и т.д.)", required = false) -// // ], -// // responses = [ -// // ApiResponse(responseCode = "200", description = "Список карточек успешно получен", -// // content = [Content(mediaType = "application/json", schema = Schema(implementation = CardResponse::class))]) -// // ] -// // ) -// // @GetMapping("/inventory") -// // fun getUserInventory( -// // @AuthenticationPrincipal user: UserEntity, -// // @RequestParam(required = false, defaultValue = "rarity") sortBy: String -// // ): ResponseEntity { -// // val cards = cardService.getUserCards(user, sortBy) -// // return ResponseEntity.ok(cards) -// // } +// @Operation( +// summary = "Получить инвентарь пользователя", +// description = "Возвращает список карточек, принадлежащих пользователю. Можно указать параметр сортировки (по умолчанию — по редкости).", +// parameters = [ +// Parameter(name = "sortBy", description = "Критерий сортировки (rarity, name, и т.д.)", required = false) +// ], +// responses = [ +// ApiResponse(responseCode = "200", description = "Список карточек успешно получен", +// content = [Content(mediaType = "application/json", schema = Schema(implementation = CardResponse::class))]) +// ] +// ) +// @GetMapping("/inventory") +// fun getUserInventory( +// @AuthenticationPrincipal user: UserEntity, +// @RequestParam(required = false, defaultValue = "rarity") sortBy: String +// ): ResponseEntity { +// val cards = cardService.getUserCards(user, sortBy) +// return ResponseEntity.ok(cards) +// } // @Operation( // summary = "Получить подробную информацию о карточке", diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt index 2225d84..62fa5d8 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt @@ -1,261 +1,322 @@ -// package ru.vsu.app.controller - -// import ru.vsu.app.dto.responses.InventoryCardCardIDFavoriteGet200Response -// import ru.vsu.app.dto.responses.InventoryCardCardIDQuantityGet200Response -// import ru.vsu.app.dto.responses.InventoryCardCardIDTradeCancelPost200Response -// import ru.vsu.app.dto.responses.InventoryCardCardIDTradeStatusGet200Response -// import ru.vsu.app.dto.responses.InventoryDestroyPost200Response -// import ru.vsu.app.dto.requests.InventoryDestroyPostRequest -// import ru.vsu.app.dto.responses.InventoryFavoritesCountGet200Response -// import ru.vsu.app.dto.responses.InventoryGet200Response -// import ru.vsu.app.dto.responses.OtherProfileCardCardIDInitiateTradePost200Response -// import ru.vsu.app.dto.responses.common.InternalServerError -// import io.swagger.v3.oas.annotations.* -// import io.swagger.v3.oas.annotations.enums.* -// import io.swagger.v3.oas.annotations.media.* -// import io.swagger.v3.oas.annotations.responses.* -// import io.swagger.v3.oas.annotations.security.* -// import io.swagger.v3.oas.annotations.tags.Tag -// import org.springframework.http.HttpStatus -// import org.springframework.http.MediaType -// import org.springframework.http.ResponseEntity - -// import org.springframework.web.bind.annotation.* -// import org.springframework.validation.annotation.Validated -// import org.springframework.web.context.request.NativeWebRequest -// import org.springframework.beans.factory.annotation.Autowired - -// import jakarta.validation.Valid -// import jakarta.validation.constraints.DecimalMax -// import jakarta.validation.constraints.DecimalMin -// import jakarta.validation.constraints.Email -// import jakarta.validation.constraints.Max -// import jakarta.validation.constraints.Min -// import jakarta.validation.constraints.NotNull -// import jakarta.validation.constraints.Pattern -// import jakarta.validation.constraints.Size - -// import org.springframework.security.core.annotation.AuthenticationPrincipal -// import ru.vsu.app.model.UserEntity -// import ru.vsu.app.service.InventoryService -// import ru.vsu.app.dto.responses.CardResponse - -// import kotlin.collections.List -// import kotlin.collections.Map - -// @RestController -// @Validated -// @RequestMapping("\${api.base-path:/api}") -// @SecurityRequirement(name = "Bearer Authentication") -// @Tag(name = "Inventory", description = "Операции на странице \"Инвентарь\"") -// class InventoryController(private val inventoryService: InventoryService) { - -// @Operation( -// summary = "Добавление карты в избранное", -// operationId = "inventoryCardCardIDFavoriteAddPost", -// description = """Добавляет карту в избранное, если: -// - Карта не добавлена в "избранное" -// - Общее количество избранных карт < 5 -// """, -// responses = [ -// ApiResponse(responseCode = "200", description= "Карта успешно добавлена в избранное"), -// ApiResponse(responseCode = "400", description = "Невозможно добавить карту в избранное"), -// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], -// security = [ SecurityRequirement(name = "bearerAuth") ] -// ) -// @RequestMapping( -// method = [RequestMethod.POST], -// value = ["/inventory/card/{card_ID}/favorite-add"], -// produces = ["application/json"] -// ) -// fun inventoryCardCardIDFavoriteAddPost(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { -// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) -// } - -// @Operation( -// summary = "Удаление карты из избранного", -// operationId = "inventoryCardCardIDFavoriteDeleteDelete", -// description = """Удаляет карту из избранного. -// """, -// responses = [ -// ApiResponse(responseCode = "200", description = "Карта успешно удалена из избранного"), -// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], -// security = [ SecurityRequirement(name = "bearerAuth") ] -// ) -// @RequestMapping( -// method = [RequestMethod.DELETE], -// value = ["/inventory/card/{card_ID}/favorite-delete"], -// produces = ["application/json"] -// ) -// fun inventoryCardCardIDFavoriteDeleteDelete(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { -// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) -// } - -// @Operation( -// summary = "Проверка статуса карты", -// operationId = "inventoryCardCardIDFavoriteGet", -// description = """Проверяет, добавлена ли карта в избранное. -// """, -// responses = [ -// ApiResponse(responseCode = "200", description = "Статус избранного", content = [Content(schema = Schema(implementation = InventoryCardCardIDFavoriteGet200Response::class))]), -// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], -// security = [ SecurityRequirement(name = "bearerAuth") ] -// ) -// @RequestMapping( -// method = [RequestMethod.GET], -// value = ["/inventory/card/{card_ID}/favorite"], -// produces = ["application/json"] -// ) -// fun inventoryCardCardIDFavoriteGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { -// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) -// } - -// @Operation( -// summary = "Проверка количества экземпляров карты", -// operationId = "inventoryCardCardIDQuantityGet", -// description = """Возвращает количество экземпляров карты у пользователя. -// Если экземпляров больше 1 - можно разбирать/выставлять на обмен. -// """, -// responses = [ -// ApiResponse(responseCode = "200", description = "Информация о количестве карт", content = [Content(schema = Schema(implementation = InventoryCardCardIDQuantityGet200Response::class))]), -// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], -// security = [ SecurityRequirement(name = "bearerAuth") ] -// ) -// @RequestMapping( -// method = [RequestMethod.GET], -// value = ["/inventory/card/{card_ID}/quantity"], -// produces = ["application/json"] -// ) -// fun inventoryCardCardIDQuantityGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { -// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) -// } - -// @Operation( -// summary = "Снятие карты с обмена", -// operationId = "inventoryCardCardIDTradeCancelPost", -// description = """Возвращает карту из раздела "На обмен" в Инвентарь""", -// responses = [ -// ApiResponse(responseCode = "200", description = "Карта успешно снята с обмена", content = [Content(schema = Schema(implementation = InventoryCardCardIDTradeCancelPost200Response::class))]), -// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], -// security = [ SecurityRequirement(name = "bearerAuth") ] -// ) -// @RequestMapping( -// method = [RequestMethod.POST], -// value = ["/inventory/card/{card_ID}/trade-cancel"], -// produces = ["application/json"] -// ) -// fun inventoryCardCardIDTradeCancelPost(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { -// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) -// } - -// @Operation( -// summary = "Проверка статуса обмена карты", -// operationId = "inventoryCardCardIDTradeStatusGet", -// description = """Проверяет, выставлена ли карта на обмен. -// Возвращает статус карты. -// """, -// responses = [ -// ApiResponse(responseCode = "200", description = "Статус обмена карты", content = [Content(schema = Schema(implementation = InventoryCardCardIDTradeStatusGet200Response::class))]), -// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], -// security = [ SecurityRequirement(name = "bearerAuth") ] -// ) -// @RequestMapping( -// method = [RequestMethod.GET], -// value = ["/inventory/card/{card_ID}/trade-status"], -// produces = ["application/json"] -// ) -// fun inventoryCardCardIDTradeStatusGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { -// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) -// } - -// @Operation( -// summary = "Разбор карты на валюту", -// operationId = "inventoryDestroyPost", -// description = """Разбирает карту на валюту, если: -// - У пользователя есть минимум 2 экземпляра этой карты -// - Добавляет стоимость карты на баланс пользователя -// """, -// responses = [ -// ApiResponse(responseCode = "200", description = "Карта успешно разобрана", content = [Content(schema = Schema(implementation = InventoryDestroyPost200Response::class))]), -// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], -// security = [ SecurityRequirement(name = "bearerAuth") ] -// ) -// @RequestMapping( -// method = [RequestMethod.POST], -// value = ["/inventory/destroy"], -// produces = ["application/json"], -// consumes = ["application/json"] -// ) -// fun inventoryDestroyPost(@Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest): ResponseEntity { -// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) -// } - -// @Operation( -// summary = "Получение количества избранных карт", -// operationId = "inventoryFavoritesCountGet", -// description = """Возвращает текущее количество избранных карт. -// Если количество меньше 5 - можно добавлять новые карты в избранное. -// """, -// responses = [ -// ApiResponse(responseCode = "200", description = "Количество избранных карт", content = [Content(schema = Schema(implementation = InventoryFavoritesCountGet200Response::class))]), -// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], -// security = [ SecurityRequirement(name = "bearerAuth") ] -// ) -// @RequestMapping( -// method = [RequestMethod.GET], -// value = ["/inventory/favorites/count"], -// produces = ["application/json"] -// ) -// fun inventoryFavoritesCountGet(): ResponseEntity { -// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) -// } - -// @Operation( -// summary = "Получение данных инвентаря", -// operationId = "inventoryGet", -// description = """Загружает данные инвентаря пользователя. -// - Проверяет авторизацию пользователя -// - Если пользователь не авторизован - возвращает ошибку 401 -// - Если авторизован - возвращает список его карт, список избранных карт и баланс -// """, -// responses = [ -// ApiResponse(responseCode = "200", description = "Успешная загрузка инвентаря", content = [Content(schema = Schema(implementation = InventoryGet200Response::class))]), -// ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), -// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], -// security = [ SecurityRequirement(name = "bearerAuth") ] -// ) -// @RequestMapping( -// method = [RequestMethod.GET], -// value = ["/inventory"], -// produces = ["application/json"] -// ) -// fun inventoryGet( -// @AuthenticationPrincipal user: UserEntity -// ): ResponseEntity { -// val inventoryData = inventoryService.getInventoryData(user) -// return ResponseEntity.ok(inventoryData) -// } - -// @Operation( -// summary = "Выставление карты на обмен", -// operationId = "inventoryPutOnTradePost", -// description = """Выставляет карту на обмен, если: -// - У пользователя есть минимум 2 экземпляра этой карты -// - Карта еще не выставлена на обмен -// """, -// responses = [ -// ApiResponse(responseCode = "200", description = "Карта готова к обмену", content = [Content(schema = Schema(implementation = OtherProfileCardCardIDInitiateTradePost200Response::class))]), -// ApiResponse(responseCode = "400", description = "Невозможно выставить карту на обмен"), -// ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], -// security = [ SecurityRequirement(name = "bearerAuth") ] -// ) -// @RequestMapping( -// method = [RequestMethod.POST], -// value = ["/inventory/put-on-trade"], -// produces = ["application/json"], -// consumes = ["application/json"] -// ) -// fun inventoryPutOnTradePost(@Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest): ResponseEntity { -// return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) -// } -// } +package ru.vsu.app.controller + +import ru.vsu.app.dto.responses.inventory.InventoryCardCardIDFavoriteGet200Response + +import ru.vsu.app.dto.responses.inventory.InventoryCardCardIDQuantityGet200Response + +import ru.vsu.app.dto.responses.inventory.InventoryCardCardIDTradeCancelPost200Response +import ru.vsu.app.dto.responses.inventory.InventoryCardCardIDTradeStatusGet200Response + +import ru.vsu.app.dto.responses.inventory.InventoryDestroyPost200Response + +import ru.vsu.app.dto.responses.inventory.InventoryFavoritesCountGet200Response + +import ru.vsu.app.dto.responses.inventory.InventoryGet200Response +import ru.vsu.app.dto.responses.inventory.InventoryGet401Response + +import ru.vsu.app.dto.requests.InventoryDestroyPostRequest + +import ru.vsu.app.dto.responses.profile.OtherProfileCardCardIDInitiateTradePost200Response + +import ru.vsu.app.dto.responses.common.InternalServerError + +import ru.vsu.app.model.UserEntity + +import ru.vsu.app.service.InventoryService + +import ru.vsu.app.metrics.InventoryMetrics + +import ru.vsu.app.security.CustomUserDetails + +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* +import io.swagger.v3.oas.annotations.tags.Tag + +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.server.ResponseStatusException + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + + +import kotlin.collections.List +import kotlin.collections.Map + +@RestController +@Validated +@RequestMapping("\${api.base-path:/api}") +@SecurityRequirement(name = "Bearer Authentication") +@Tag(name = "Inventory", description = "Операции на странице \"Инвентарь\"") +class InventoryController( + private val inventoryService: InventoryService, + private val inventoryMetrics: InventoryMetrics +) { + + @Operation( + summary = "Добавление карты в избранное", + operationId = "inventoryCardCardIDFavoriteAddPost", + description = """Добавляет карту в избранное, если: +- Карта не добавлена в "избранное" +- Общее количество избранных карт < 5 +""", + responses = [ + ApiResponse(responseCode = "200", description= "Карта успешно добавлена в избранное"), + ApiResponse(responseCode = "400", description = "Невозможно добавить карту в избранное"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/inventory/card/{card_ID}/favorite-add"], + produces = ["application/json"] + ) + fun inventoryCardCardIDFavoriteAddPost(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Удаление карты из избранного", + operationId = "inventoryCardCardIDFavoriteDeleteDelete", + description = """Удаляет карту из избранного. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Карта успешно удалена из избранного"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/inventory/card/{card_ID}/favorite-delete"], + produces = ["application/json"] + ) + fun inventoryCardCardIDFavoriteDeleteDelete(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Проверка статуса карты", + operationId = "inventoryCardCardIDFavoriteGet", + description = """Проверяет, добавлена ли карта в избранное. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Статус избранного", content = [Content(schema = Schema(implementation = InventoryCardCardIDFavoriteGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/inventory/card/{card_ID}/favorite"], + produces = ["application/json"] + ) + fun inventoryCardCardIDFavoriteGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Проверка количества экземпляров карты", + operationId = "inventoryCardCardIDQuantityGet", + description = """Возвращает количество экземпляров карты у пользователя. +Если экземпляров больше 1 - можно разбирать/выставлять на обмен. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Информация о количестве карт", content = [Content(schema = Schema(implementation = InventoryCardCardIDQuantityGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/inventory/card/{card_ID}/quantity"], + produces = ["application/json"] + ) + fun inventoryCardCardIDQuantityGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Снятие карты с обмена", + operationId = "inventoryCardCardIDTradeCancelPost", + description = """Возвращает карту из раздела "На обмен" в Инвентарь""", + responses = [ + ApiResponse(responseCode = "200", description = "Карта успешно снята с обмена", content = [Content(schema = Schema(implementation = InventoryCardCardIDTradeCancelPost200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/inventory/card/{card_ID}/trade-cancel"], + produces = ["application/json"] + ) + fun inventoryCardCardIDTradeCancelPost(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Проверка статуса обмена карты", + operationId = "inventoryCardCardIDTradeStatusGet", + description = """Проверяет, выставлена ли карта на обмен. +Возвращает статус карты. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Статус обмена карты", content = [Content(schema = Schema(implementation = InventoryCardCardIDTradeStatusGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/inventory/card/{card_ID}/trade-status"], + produces = ["application/json"] + ) + fun inventoryCardCardIDTradeStatusGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Разбор карты на валюту", + operationId = "inventoryDestroyPost", + description = """Разбирает карту на валюту, если: +- У пользователя есть минимум 2 экземпляра этой карты +- Добавляет стоимость карты на баланс пользователя +""", + responses = [ + ApiResponse(responseCode = "200", description = "Карта успешно разобрана", content = [Content(schema = Schema(implementation = InventoryDestroyPost200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/inventory/destroy"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun inventoryDestroyPost(@Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение количества избранных карт", + operationId = "inventoryFavoritesCountGet", + description = """Возвращает текущее количество избранных карт. +Если количество меньше 5 - можно добавлять новые карты в избранное. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Количество избранных карт", content = [Content(schema = Schema(implementation = InventoryFavoritesCountGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/inventory/favorites/count"], + produces = ["application/json"] + ) + fun inventoryFavoritesCountGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение данных инвентаря", + operationId = "inventoryGet", + description = """ + Загружает данные инвентаря пользователя. + + - Требуется авторизация (JWT) + - Возвращает: + - список карт пользователя, + - список избранных карт, + - текущий баланс пользователя. + """, + responses = [ + ApiResponse( + responseCode = "200", + description = "Инвентарь успешно загружен", + content = [Content(schema = Schema(implementation = InventoryGet200Response::class))] + ), + ApiResponse( + responseCode = "401", + description = "Пользователь не авторизован", + content = [Content(schema = Schema(implementation = InventoryGet401Response::class))] + ), + ApiResponse( + responseCode = "500", + description = "Внутренняя ошибка сервера", + content = [Content(schema = Schema(implementation = InternalServerError::class))] + ) + ], + security = [SecurityRequirement(name = "bearerAuth")] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/inventory"], + produces = ["application/json"] + ) + fun inventoryGet( + @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails + ): ResponseEntity { + val userEntity = user.getUser() + val start = System.currentTimeMillis() + inventoryMetrics.inventoryGetAttempt() + + return try { + val result = inventoryService.getInventoryData(userEntity) + + inventoryMetrics.inventoryGetSuccess() + inventoryMetrics.inventoryGetDuration(System.currentTimeMillis() - start) + + ResponseEntity.ok(result) + } catch (ex: ResponseStatusException) { + if (ex.statusCode == HttpStatus.UNAUTHORIZED) { + inventoryMetrics.inventoryGetFailure() + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( + InventoryGet401Response(false, "Неавторизованный доступ: ${ex.reason}") + ) + } else { + throw ex + } + } catch (ex: Exception) { + inventoryMetrics.inventoryGetFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( + InternalServerError(error = "Ошибка при получении инвентаря: ${ex.message ?: "Неизвестная ошибка"}") + ) + } + } + + @Operation( + summary = "Выставление карты на обмен", + operationId = "inventoryPutOnTradePost", + description = """Выставляет карту на обмен, если: +- У пользователя есть минимум 2 экземпляра этой карты +- Карта еще не выставлена на обмен +""", + responses = [ + ApiResponse(responseCode = "200", description = "Карта готова к обмену", content = [Content(schema = Schema(implementation = OtherProfileCardCardIDInitiateTradePost200Response::class))]), + ApiResponse(responseCode = "400", description = "Невозможно выставить карту на обмен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/inventory/put-on-trade"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun inventoryPutOnTradePost(@Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/CardDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/CardDto.kt index cccff53..44a7b25 100644 --- a/backend/src/main/kotlin/ru/vsu/app/dto/CardDto.kt +++ b/backend/src/main/kotlin/ru/vsu/app/dto/CardDto.kt @@ -1,19 +1,10 @@ package ru.vsu.app.dto -import java.util.Objects import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonValue -import jakarta.validation.constraints.DecimalMax -import jakarta.validation.constraints.DecimalMin -import jakarta.validation.constraints.Email -import jakarta.validation.constraints.Max -import jakarta.validation.constraints.Min -import jakarta.validation.constraints.NotNull -import jakarta.validation.constraints.Pattern -import jakarta.validation.constraints.Size -import jakarta.validation.Valid import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.Valid /** * @@ -29,36 +20,31 @@ import io.swagger.v3.oas.annotations.media.Schema data class CardDto( @Schema(example = "501", required = true, description = "Уникальный идентификатор карты") - @get:JsonProperty("card_ID", required = true) val cardID: kotlin.Int, + @get:JsonProperty("card_ID", required = true) val cardID: Int, @Schema(example = "Ледяной феникс", required = true, description = "Название карты") - @get:JsonProperty("name", required = true) val name: kotlin.String, + @get:JsonProperty("name", required = true) val name: String, @Schema(example = "https://example.com/cards/pheonix.png", required = true, description = "Ссылка на изображение") - @get:JsonProperty("imageURL", required = true) val imageURL: kotlin.String, + @get:JsonProperty("imageURL", required = true) val imageURL: String, @Schema(example = "Редкая", required = true, description = "Редкость карты") - @get:JsonProperty("rarity", required = true) val rarity: CardDto.Rarity, + @get:JsonProperty("rarity", required = true) val rarity: Rarity, @Schema(example = "50", required = true, description = "Минимальная цена - используется для разбора карточки") - @get:JsonProperty("min_price", required = true) val minPrice: kotlin.Int, - - @Schema(example = "false", required = true, description = "Флаг показывающий сгенерированная ли карта (true для уникальных, false для остальных)") - @get:JsonProperty("isGenerated", required = true) val isGenerated: kotlin.Boolean, + @get:JsonProperty("min_price", required = true) val minPrice: Int, - @Schema(example = "Ледяной Феникс — это мифическое существо, воплощающее силу зимы и вечного обновления. Его перья сверкают как морозный утренний иней, а глаза светятся холодным синим светом.", description = "Описание карты") - @get:JsonProperty("description") val description: kotlin.String? = null, + @Schema(example = "false", required = true, description = "Флаг показывающий сгенерированная ли карта") + @get:JsonProperty("isGenerated", required = true) val isGenerated: Boolean, - @Schema(example = "Мифическое существо", description = "Тематика, на которую была сгенерирвана карта (только для уникальных карт)") - @get:JsonProperty("theme") val theme: kotlin.String? = null - ) { - - /** - * Редкость карты - * Values: Обычная,Редкая,Эпическая,Легендарная,Уникальная - */ - enum class Rarity(@get:JsonValue val value: kotlin.String) { + @Schema(example = "Ледяной Феникс — это мифическое существо...", description = "Описание карты") + @get:JsonProperty("description") val description: String? = null, + @field:Valid + @Schema(description = "Тематика, на которую была сгенерирована карта (только для уникальных карт)") + @get:JsonProperty("theme") val theme: ThemeDto? = null +) { + enum class Rarity(@get:JsonValue val value: String) { Обычная("Обычная"), Редкая("Редкая"), Эпическая("Эпическая"), @@ -68,11 +54,9 @@ data class CardDto( companion object { @JvmStatic @JsonCreator - fun forValue(value: kotlin.String): Rarity { - return values().first{it -> it.value == value} + fun forValue(value: String): Rarity { + return values().first { it.value == value } } } } - } - diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/UserDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/UserDto.kt index 97eba45..ed61854 100644 --- a/backend/src/main/kotlin/ru/vsu/app/dto/UserDto.kt +++ b/backend/src/main/kotlin/ru/vsu/app/dto/UserDto.kt @@ -43,7 +43,6 @@ data class UserDto( @Schema(example = "user@example.com", required = true, description = "Электронная почта") @get:JsonProperty("email", required = true) val email: kotlin.String, - @field:Valid @Schema(required = true, description = "Карты в инвентаре") @get:JsonProperty("inventoryCards", required = true) val inventoryCards: kotlin.collections.List, diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet200Response.kt index 962c142..aef4a46 100644 --- a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet200Response.kt +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet200Response.kt @@ -2,8 +2,10 @@ package ru.vsu.app.dto.responses.inventory import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import ru.vsu.app.model.CardEntity -import ru.vsu.app.model.CollectionEntity + +import ru.vsu.app.dto.CardDto +import ru.vsu.app.dto.CollectionDto + import jakarta.validation.constraints.DecimalMax import jakarta.validation.constraints.DecimalMin import jakarta.validation.constraints.Email @@ -26,14 +28,14 @@ data class InventoryGet200Response( @field:Valid @Schema(description = "") - @get:JsonProperty("inventory") val inventory: kotlin.collections.List? = null, + @get:JsonProperty("inventory") val inventory: kotlin.collections.List? = null, @Schema(description = "ID избранных карт") @get:JsonProperty("favoriteCards") val favoriteCards: kotlin.collections.List? = null, @field:Valid @Schema(description = "") - @get:JsonProperty("collections") val collections: kotlin.collections.List? = null, + @get:JsonProperty("collections") val collections: kotlin.collections.List? = null, @Schema(example = "2500", description = "") @get:JsonProperty("balance") val balance: kotlin.Int? = null diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet401Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet401Response.kt new file mode 100644 index 0000000..ad06499 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet401Response.kt @@ -0,0 +1,34 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.CardEntity +import ru.vsu.app.model.CollectionEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param isAuthenticated + * @param message + */ +data class InventoryGet401Response( + + @Schema(example = "false", description = "") + @get:JsonProperty("isAuthenticated") val isAuthenticated: kotlin.Boolean? = null, + + @Schema(example = "Требуется авторизация", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + + diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/AchievementMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/AchievementMapper.kt index c14d5d3..67f114e 100644 --- a/backend/src/main/kotlin/ru/vsu/app/mapper/AchievementMapper.kt +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/AchievementMapper.kt @@ -6,11 +6,13 @@ import ru.vsu.app.model.AchievementEntity @Component class AchievementMapper { - fun toDto(entity: AchievementEntity): AchievementDto = AchievementDto( - achievementID = entity.achievementID, - name = entity.name, - imageURL = entity.imageURL, - description = entity.description, - isUnlocked = entity.isUnlocked - ) + fun toDto(entity: AchievementEntity): AchievementDto { + return AchievementDto( + achievementID = entity.achievementID, + name = entity.name, + imageURL = entity.imageURL, + description = entity.description, + isUnlocked = entity.isUnlocked + ) + } } diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/CardMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/CardMapper.kt index d59c55b..c8b1418 100644 --- a/backend/src/main/kotlin/ru/vsu/app/mapper/CardMapper.kt +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/CardMapper.kt @@ -3,17 +3,22 @@ package ru.vsu.app.mapper import org.springframework.stereotype.Component import ru.vsu.app.dto.CardDto import ru.vsu.app.model.CardEntity +import ru.vsu.app.mapper.ThemeMapper @Component -class CardMapper { - fun toDto(entity: CardEntity): CardDto = CardDto( - cardID = entity.id, - name = entity.name, - imageURL = entity.imageUrl, - rarity = CardDto.Rarity.forValue(entity.rarity.value), - minPrice = entity.disassemblePrice, - isGenerated = entity.isGenerated, - description = entity.description, - theme = entity.theme - ) +class CardMapper( + private val themeMapper: ThemeMapper +) { + fun toDto(cardEntity: CardEntity): CardDto { + return CardDto( + cardID = cardEntity.id, + name = cardEntity.name, + description = cardEntity.description, + rarity = CardDto.Rarity.forValue(cardEntity.rarity.value), + imageURL = cardEntity.imageUrl, + minPrice = cardEntity.disassemblePrice, + isGenerated = cardEntity.isGenerated, + theme = cardEntity.theme?.let { themeMapper.toDto(it) } + ) + } } diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/CollectionMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/CollectionMapper.kt new file mode 100644 index 0000000..a1f1e90 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/CollectionMapper.kt @@ -0,0 +1,20 @@ +package ru.vsu.app.mapper + +import org.springframework.stereotype.Component +import ru.vsu.app.dto.CollectionDto +import ru.vsu.app.model.CollectionEntity +import ru.vsu.app.mapper.CardMapper + +@Component +class CollectionMapper( + private val cardMapper: CardMapper +) { + fun toDto(entity: CollectionEntity): CollectionDto { + return CollectionDto( + collectionID = entity.collectionID, + name = entity.name, + cards = entity.cards.map { cardMapper.toDto(it) }, + imageURL = entity.imageURL + ) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/NotificationMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/NotificationMapper.kt index 91d44b7..dfaa1b9 100644 --- a/backend/src/main/kotlin/ru/vsu/app/mapper/NotificationMapper.kt +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/NotificationMapper.kt @@ -6,11 +6,13 @@ import ru.vsu.app.model.NotificationEntity @Component class NotificationMapper { - fun toDto(entity: NotificationEntity): NotificationDto = NotificationDto( - notificationID = entity.notificationID, - userID = entity.user.userId, - message = entity.message, - notificationDateTime = entity.notificationDateTime, - links = entity.links.takeIf { it.isNotEmpty() } - ) + fun toDto(entity: NotificationEntity): NotificationDto { + return NotificationDto( + notificationID = entity.notificationID, + userID = entity.user.userId, + message = entity.message, + notificationDateTime = entity.notificationDateTime, + links = entity.links.takeIf { it.isNotEmpty() } + ) + } } diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/ThemeMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/ThemeMapper.kt new file mode 100644 index 0000000..34092f7 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/ThemeMapper.kt @@ -0,0 +1,25 @@ +package ru.vsu.app.mapper + +import org.springframework.stereotype.Component +import ru.vsu.app.dto.ThemeDto +import ru.vsu.app.model.ThemeEntity + +@Component +class ThemeMapper { + + fun toDto(entity: ThemeEntity): ThemeDto { + return ThemeDto( + themeID = entity.themeID, + name = entity.name, + description = entity.description + ) + + fun toEntity(dto: ThemeDto): ThemeEntity { + return ThemeEntity( + themeID = dto.themeID, + name = dto.name, + description = dto.description + ) + } +} +} diff --git a/backend/src/main/kotlin/ru/vsu/app/metrics/InventoryMetrics.kt b/backend/src/main/kotlin/ru/vsu/app/metrics/InventoryMetrics.kt new file mode 100644 index 0000000..7fd4dfa --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/metrics/InventoryMetrics.kt @@ -0,0 +1,89 @@ +package ru.vsu.app.metrics + +import org.springframework.stereotype.Service + +@Service +class InventoryMetrics(private val metrics: MetricsRegistry) { + + // ===== Получение инвентаря ===== + + fun inventoryGetAttempt() { + metrics.count("inventory.get.attempt") + } + + fun inventoryGetSuccess() { + metrics.count("inventory.get.success") + } + + fun inventoryGetUnauthorized() { + metrics.count("inventory.get.unauthorized") // 401 + } + + fun inventoryGetValidationError() { + metrics.count("inventory.get.validation_error") // 400 + } + + fun inventoryGetFailure() { + metrics.count("inventory.get.failure") // 500 или unexpected + } + + fun inventoryGetDuration(durationMs: Long, userId: String? = null) { + if (userId != null) { + metrics.timer("inventory.get.duration", durationMs, listOf(MetricTags.user(userId))) + } else { + metrics.timer("inventory.get.duration", durationMs) + } + } + + // ===== Обновление/изменение инвентаря ===== + + fun inventoryUpdateAttempt() { + metrics.count("inventory.update.attempt") + } + + fun inventoryUpdateSuccess(userId: String) { + metrics.count("inventory.update.success", listOf(MetricTags.user(userId))) + } + + fun inventoryUpdateValidationError() { + metrics.count("inventory.update.validation_error") + } + + fun inventoryUpdateFailure() { + metrics.count("inventory.update.failure") + } + + fun inventoryUpdateDuration(durationMs: Long, userId: String? = null) { + if (userId != null) { + metrics.timer("inventory.update.duration", durationMs, listOf(MetricTags.user(userId))) + } else { + metrics.timer("inventory.update.duration", durationMs) + } + } + + // ===== Удаление предмета из инвентаря ===== + + fun inventoryDeleteAttempt() { + metrics.count("inventory.delete.attempt") + } + + fun inventoryDeleteSuccess(userId: String) { + metrics.count("inventory.delete.success", listOf(MetricTags.user(userId))) + } + + fun inventoryDeleteValidationError() { + metrics.count("inventory.delete.validation_error") + } + + fun inventoryDeleteFailure() { + metrics.count("inventory.delete.failure") + } + + fun inventoryDeleteDuration(durationMs: Long, userId: String? = null) { + if (userId != null) { + metrics.timer("inventory.delete.duration", durationMs, listOf(MetricTags.user(userId))) + } else { + metrics.timer("inventory.delete.duration", durationMs) + } + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/model/CardEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/CardEntity.kt index 1c603e6..77569cb 100644 --- a/backend/src/main/kotlin/ru/vsu/app/model/CardEntity.kt +++ b/backend/src/main/kotlin/ru/vsu/app/model/CardEntity.kt @@ -1,7 +1,9 @@ package ru.vsu.app.model import jakarta.persistence.* + import ru.vsu.app.model.UserEntity +import ru.vsu.app.model.ThemeEntity @Entity @Table(name = "cards") @@ -30,14 +32,15 @@ data class CardEntity( @Column(name = "description") val description: String? = null, - @Column(name = "theme") - val theme: String? = null, + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "theme_id") + val theme: ThemeEntity? = null, @ManyToOne @JoinColumn(name = "collection_id") val collection: CollectionEntity? = null, - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "owner_id") val owner: UserEntity? = null, ) { diff --git a/backend/src/main/kotlin/ru/vsu/app/model/CollectionEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/CollectionEntity.kt index d76e65f..ff9b09f 100644 --- a/backend/src/main/kotlin/ru/vsu/app/model/CollectionEntity.kt +++ b/backend/src/main/kotlin/ru/vsu/app/model/CollectionEntity.kt @@ -14,7 +14,7 @@ data class CollectionEntity( @Column(nullable = false) val name: String, - @OneToMany(mappedBy = "collection", cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true) + @OneToMany(mappedBy = "collection", cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) val cards: List = emptyList(), @Column(name = "image_url", nullable = false) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/NotificationEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/NotificationEntity.kt index 8fa073f..203e4a9 100644 --- a/backend/src/main/kotlin/ru/vsu/app/model/NotificationEntity.kt +++ b/backend/src/main/kotlin/ru/vsu/app/model/NotificationEntity.kt @@ -12,7 +12,7 @@ data class NotificationEntity( @Column(name = "notification_ID") val notificationID: Int = 0, - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "user_id", nullable = false) val user: UserEntity, diff --git a/backend/src/main/kotlin/ru/vsu/app/model/PaymentEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/PaymentEntity.kt new file mode 100644 index 0000000..622d141 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/PaymentEntity.kt @@ -0,0 +1,45 @@ +package ru.vsu.app.model + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table(name = "payments") +data class PaymentEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "payment_id") + val paymentID: Int = 0, + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + @JsonIgnore + val user: UserEntity, + + @Column(name = "payment_sum", nullable = false) + val paymentSUM: Double, + + @Enumerated(EnumType.STRING) + @Column(name = "payment_status", nullable = false) + val paymentStatus: PaymentStatus, + + @Column(name = "payment_datetime", nullable = false) + val paymentDateTime: LocalDateTime, + + // Эти данные сохраняются только при создании, поэтому можно не сохранять в БД или сохранять как JSON/строку + @ElementCollection + @CollectionTable(name = "payment_card_data", joinColumns = [JoinColumn(name = "payment_id")]) + @Column(name = "card_data") + val bankCardData: List? = null +) { + + enum class PaymentStatus(val value: String) { + В_обработке("В обработке"), + Оплачено("Оплачено"), + Ошибка("Ошибка"); + + override fun toString(): String = value + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/model/QuestEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/QuestEntity.kt index d7dc571..4b6f8c5 100644 --- a/backend/src/main/kotlin/ru/vsu/app/model/QuestEntity.kt +++ b/backend/src/main/kotlin/ru/vsu/app/model/QuestEntity.kt @@ -32,7 +32,7 @@ data class QuestEntity( @Column(name = "is_claimed", nullable = false) val isClaimed: Boolean, - @ManyToMany(fetch = FetchType.LAZY) + @ManyToMany(fetch = FetchType.EAGER) @JoinTable( name = "quest_reward_packs", joinColumns = [JoinColumn(name = "quest_id")], diff --git a/backend/src/main/kotlin/ru/vsu/app/model/ReportEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/ReportEntity.kt index 5391d0f..e0bc694 100644 --- a/backend/src/main/kotlin/ru/vsu/app/model/ReportEntity.kt +++ b/backend/src/main/kotlin/ru/vsu/app/model/ReportEntity.kt @@ -11,11 +11,11 @@ data class ReportEntity( @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0, - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "reporter_id", nullable = false) val reporter: UserEntity, - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "reported_user_id", nullable = false) val reportedUser: UserEntity, diff --git a/backend/src/main/kotlin/ru/vsu/app/model/ThemeEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/ThemeEntity.kt new file mode 100644 index 0000000..a965275 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/ThemeEntity.kt @@ -0,0 +1,19 @@ +package ru.vsu.app.model + +import jakarta.persistence.* + +@Entity +@Table(name = "themes") +data class ThemeEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "theme_id") + val themeID: Int = 0, + + @Column(name = "name", nullable = false) + val name: String, + + @Column(name = "description") + val description: String? = null +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/TradeEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/TradeEntity.kt new file mode 100644 index 0000000..dcd4b75 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/TradeEntity.kt @@ -0,0 +1,44 @@ +package ru.vsu.app.model + +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table(name = "trades") +data class TradeEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "trade_id") + val tradeID: Int = 0, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "offering_user_id", nullable = false) + val offeringUser: UserEntity, + + @ManyToMany + @JoinTable( + name = "trade_offering_cards", + joinColumns = [JoinColumn(name = "trade_id")], + inverseJoinColumns = [JoinColumn(name = "card_id")] + ) + val offeringCards: List = emptyList(), + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "receiving_user_id", nullable = false) + val receivingUser: UserEntity, + + @ManyToMany + @JoinTable( + name = "trade_receiving_cards", + joinColumns = [JoinColumn(name = "trade_id")], + inverseJoinColumns = [JoinColumn(name = "card_id")] + ) + val receivingCards: List = emptyList(), + + @Column(name = "is_confirmed", nullable = false) + val isConfirmed: Boolean = false, + + @Column(name = "trade_date_time", nullable = false) + val tradeDateTime: LocalDateTime +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/UserEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/UserEntity.kt index 96d6c60..0e92a2e 100644 --- a/backend/src/main/kotlin/ru/vsu/app/model/UserEntity.kt +++ b/backend/src/main/kotlin/ru/vsu/app/model/UserEntity.kt @@ -33,8 +33,7 @@ data class UserEntity( @Column(nullable = true, name = "avatar_url") var avatarUrl: String? = null, - // Пример связи "один ко многим" — пользователь может иметь много карточек в инвентаре - @OneToMany(targetEntity = CardEntity::class, fetch = FetchType.LAZY) + @OneToMany(targetEntity = CardEntity::class, fetch = FetchType.EAGER) @JoinTable( name = "user_inventory_cards", joinColumns = [JoinColumn(name = "user_id")], @@ -42,7 +41,7 @@ data class UserEntity( ) var inventoryCards: List = emptyList(), - @OneToMany(targetEntity = CardEntity::class, fetch = FetchType.LAZY) + @OneToMany(targetEntity = CardEntity::class, fetch = FetchType.EAGER) @JoinTable( name = "user_favorite_cards", joinColumns = [JoinColumn(name = "user_id")], @@ -50,7 +49,7 @@ data class UserEntity( ) var favoriteCards: List = emptyList(), - @OneToMany(targetEntity = CardEntity::class, fetch = FetchType.LAZY) + @OneToMany(targetEntity = CardEntity::class, fetch = FetchType.EAGER) @JoinTable( name = "user_onchange_cards", joinColumns = [JoinColumn(name = "user_id")], @@ -58,7 +57,7 @@ data class UserEntity( ) var onChange: List = emptyList(), - @OneToMany(targetEntity = AchievementEntity::class, fetch = FetchType.LAZY) + @OneToMany(targetEntity = AchievementEntity::class, fetch = FetchType.EAGER) @JoinTable( name = "user_achievements", joinColumns = [JoinColumn(name = "user_id")], @@ -66,7 +65,7 @@ data class UserEntity( ) var achievements: List = emptyList(), - @OneToMany(targetEntity = AchievementEntity::class, fetch = FetchType.LAZY) + @OneToMany(targetEntity = AchievementEntity::class, fetch = FetchType.EAGER) @JoinTable( name = "user_favorite_achievements", joinColumns = [JoinColumn(name = "user_id")], @@ -74,11 +73,26 @@ data class UserEntity( ) var favoriteAchievements: List = emptyList(), - @OneToMany(targetEntity = NotificationEntity::class, fetch = FetchType.LAZY) + @OneToMany(targetEntity = NotificationEntity::class, fetch = FetchType.EAGER) @JoinTable( name = "user_notifications", joinColumns = [JoinColumn(name = "user_id")], inverseJoinColumns = [JoinColumn(name = "notification_id")] ) - var notifications: List = emptyList() + var notifications: List = emptyList(), + + @OneToMany(targetEntity = CollectionEntity::class, fetch = FetchType.EAGER) + @JoinTable( + name = "user_completed_collections", + joinColumns = [JoinColumn(name = "user_id")], + inverseJoinColumns = [JoinColumn(name = "collection_id")] + ) + var completedCollections: List = emptyList(), + + @Enumerated(EnumType.STRING) + var role: Role = Role.USER ) + +enum class Role { + USER, ADMIN +} diff --git a/backend/src/main/kotlin/ru/vsu/app/model/UserSettingsEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/UserSettingsEntity.kt new file mode 100644 index 0000000..a464f8c --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/UserSettingsEntity.kt @@ -0,0 +1,26 @@ +package ru.vsu.app.model + +import jakarta.persistence.* + +@Entity +@Table(name = "user_settings") +data class UserSettingsEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "settings_id") + val id: Int = 0, + + @Column(name = "notifications_enabled", nullable = false) + val notificationsEnabled: Boolean = true, + + @Column(name = "show_inventory", nullable = false) + val showInventory: Boolean = false, + + @Column(name = "auto_decline_trades", nullable = false) + val autoDeclineTrades: Boolean = false, + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", unique = true, nullable = false) + val user: UserEntity +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/UserStatsEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/UserStatsEntity.kt index 738ff55..2fbca9f 100644 --- a/backend/src/main/kotlin/ru/vsu/app/model/UserStatsEntity.kt +++ b/backend/src/main/kotlin/ru/vsu/app/model/UserStatsEntity.kt @@ -16,7 +16,7 @@ data class UserStatsEntity( @Column(nullable = false) val completedCollections: Int = 0, - @OneToOne(fetch = FetchType.LAZY) + @OneToOne(fetch = FetchType.EAGER) @JoinColumn(name = "user_id", nullable = false, unique = true) val user: UserEntity ) diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/CardRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/CardRepository.kt index 02a201a..d0c67ad 100644 --- a/backend/src/main/kotlin/ru/vsu/app/repository/CardRepository.kt +++ b/backend/src/main/kotlin/ru/vsu/app/repository/CardRepository.kt @@ -6,8 +6,7 @@ import ru.vsu.app.model.CardEntity import ru.vsu.app.model.UserEntity @Repository -interface CardRepository : JpaRepository { - fun findById(id: Int): CardEntity? +interface CardRepository : JpaRepository { fun findAllByOwner(user: UserEntity): List fun findAllByOwnerOrderByRarityDesc(user: UserEntity): List fun findAllByOwnerOrderByCollectionAsc(user: UserEntity): List diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/CollectionRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/CollectionRepository.kt index f136508..f97d881 100644 --- a/backend/src/main/kotlin/ru/vsu/app/repository/CollectionRepository.kt +++ b/backend/src/main/kotlin/ru/vsu/app/repository/CollectionRepository.kt @@ -2,7 +2,9 @@ package ru.vsu.app.repository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository + import ru.vsu.app.model.CollectionEntity +import ru.vsu.app.model.UserEntity @Repository interface CollectionRepository : JpaRepository { diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/InventoryRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/InventoryRepository.kt new file mode 100644 index 0000000..0fdd01d --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/InventoryRepository.kt @@ -0,0 +1,8 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import ru.vsu.app.model.CardEntity +import ru.vsu.app.model.UserEntity + +interface InventoryRepository : JpaRepository { +} diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/ThemeRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/ThemeRepository.kt new file mode 100644 index 0000000..c70a23b --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/ThemeRepository.kt @@ -0,0 +1,6 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import ru.vsu.app.model.ThemeEntity + +interface ThemeRepository : JpaRepository diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/UserRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/UserRepository.kt index 06336f8..686ed83 100644 --- a/backend/src/main/kotlin/ru/vsu/app/repository/UserRepository.kt +++ b/backend/src/main/kotlin/ru/vsu/app/repository/UserRepository.kt @@ -8,7 +8,7 @@ import ru.vsu.app.model.UserEntity import java.util.Optional @Repository -interface UserRepository : JpaRepository { +interface UserRepository : JpaRepository { fun findByEmail(email: String): Optional fun findByUsername(username: String): Optional fun findByActivationToken(activationToken: String): Optional diff --git a/backend/src/main/kotlin/ru/vsu/app/security/CustomUserDetails.kt b/backend/src/main/kotlin/ru/vsu/app/security/CustomUserDetails.kt new file mode 100644 index 0000000..5f7d309 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/security/CustomUserDetails.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.security + +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import ru.vsu.app.model.UserEntity + +class CustomUserDetails( + private val user: UserEntity +) : UserDetails { + + override fun getAuthorities(): Collection { + return listOf(SimpleGrantedAuthority("ROLE_${user.role.name}")) + } + + override fun getPassword(): String = user.passwordHash + + override fun getUsername(): String = user.email + + override fun isAccountNonExpired(): Boolean = true + + override fun isAccountNonLocked(): Boolean = true + + override fun isCredentialsNonExpired(): Boolean = true + + override fun isEnabled(): Boolean = user.isEnabled + + fun getUser(): UserEntity = user +} diff --git a/backend/src/main/kotlin/ru/vsu/app/security/CustomUserDetailsService.kt b/backend/src/main/kotlin/ru/vsu/app/security/CustomUserDetailsService.kt index 9e7feff..5a30cf0 100644 --- a/backend/src/main/kotlin/ru/vsu/app/security/CustomUserDetailsService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/security/CustomUserDetailsService.kt @@ -17,11 +17,6 @@ class CustomUserDetailsService( val user = userRepository.findByEmail(username) .orElseThrow { UsernameNotFoundException("Пользователь не найден: $username") } - return User.builder() - .username(user.email) - .password(user.passwordHash) - .authorities(SimpleGrantedAuthority("USER")) - .disabled(!user.isEnabled) - .build() + return CustomUserDetails(user) } } \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/security/JwtAuthenticationFilter.kt b/backend/src/main/kotlin/ru/vsu/app/security/JwtAuthenticationFilter.kt index ed59132..a48198f 100644 --- a/backend/src/main/kotlin/ru/vsu/app/security/JwtAuthenticationFilter.kt +++ b/backend/src/main/kotlin/ru/vsu/app/security/JwtAuthenticationFilter.kt @@ -5,7 +5,6 @@ import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.web.authentication.WebAuthenticationDetailsSource import org.springframework.stereotype.Component import org.springframework.util.AntPathMatcher @@ -13,10 +12,12 @@ import org.springframework.web.filter.OncePerRequestFilter import ru.vsu.app.service.JwtService import ru.vsu.app.config.PublicEndpointsConfig +import ru.vsu.app.security.CustomUserDetailsService + @Component class JwtAuthenticationFilter( private val jwtService: JwtService, - private val userDetailsService: UserDetailsService, + private val userDetailsService: CustomUserDetailsService, private val publicEndpointsConfig: PublicEndpointsConfig ) : OncePerRequestFilter() { @@ -38,7 +39,6 @@ class JwtAuthenticationFilter( val authHeader = request.getHeader("Authorization") if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response) - println("Without authorization") return } @@ -46,7 +46,6 @@ class JwtAuthenticationFilter( val userEmail = jwtService.extractUsername(jwt) if (SecurityContextHolder.getContext().authentication == null) { - println("Authentication = null") val userDetails = userDetailsService.loadUserByUsername(userEmail) if (jwtService.isTokenValid(jwt, userDetails)) { diff --git a/backend/src/main/kotlin/ru/vsu/app/service/AchievementService.kt b/backend/src/main/kotlin/ru/vsu/app/service/AchievementService.kt new file mode 100644 index 0000000..b0350e1 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/AchievementService.kt @@ -0,0 +1,21 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.AchievementDto +import org.springframework.stereotype.Service + +@Service +class AchievementService { + fun deleteAchievement(achievementID: Int) { + // Implementation for deleting an achievement + } + + fun updateAchievement(achievementID: Int, achievement: AchievementDto): AchievementDto { + // Implementation for updating an achievement + return achievement + } + + fun createAchievement(achievement: AchievementDto): AchievementDto { + // Implementation for creating an achievement + return achievement + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/AdminService.kt b/backend/src/main/kotlin/ru/vsu/app/service/AdminService.kt new file mode 100644 index 0000000..36c36b7 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/AdminService.kt @@ -0,0 +1,12 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.responses.admin.AdminStatsGet200Response +import org.springframework.stereotype.Service + +@Service +class AdminService { + fun getSystemStats(): AdminStatsGet200Response { + // Implementation for getting system statistics + return AdminStatsGet200Response() + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/CardAdminService.kt b/backend/src/main/kotlin/ru/vsu/app/service/CardAdminService.kt new file mode 100644 index 0000000..15480b3 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/CardAdminService.kt @@ -0,0 +1,9 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.CardDto + +interface CardAdminService { + fun createCard(card: CardDto): CardDto + fun updateCard(cardID: Int, card: CardDto): CardDto + fun deleteCard(cardID: Int) +} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/service/CardService.kt b/backend/src/main/kotlin/ru/vsu/app/service/CardService.kt new file mode 100644 index 0000000..bf534e2 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/CardService.kt @@ -0,0 +1,21 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.CardDto +import org.springframework.stereotype.Service + +@Service +class CardService { + fun deleteCard(cardID: Int) { + // Implementation for deleting a card + } + + fun updateCard(cardID: Int, card: CardDto): CardDto { + // Implementation for updating a card + return card + } + + fun createCard(card: CardDto): CardDto { + // Implementation for creating a card + return card + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/CoinOfferService.kt b/backend/src/main/kotlin/ru/vsu/app/service/CoinOfferService.kt new file mode 100644 index 0000000..9d341a2 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/CoinOfferService.kt @@ -0,0 +1,26 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.CoinOfferDto +import org.springframework.stereotype.Service + +@Service +class CoinOfferService { + fun getAllCoinOffers(): List { + // Implementation for getting all coin offers + return emptyList() + } + + fun deleteCoinOffer(offerId: Int) { + // Implementation for deleting a coin offer + } + + fun updateCoinOffer(offerId: Int, coinOffer: CoinOfferDto): CoinOfferDto { + // Implementation for updating a coin offer + return coinOffer + } + + fun createCoinOffer(coinOffer: CoinOfferDto): CoinOfferDto { + // Implementation for creating a coin offer + return coinOffer + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/CollectionAdminService.kt b/backend/src/main/kotlin/ru/vsu/app/service/CollectionAdminService.kt new file mode 100644 index 0000000..a4122d3 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/CollectionAdminService.kt @@ -0,0 +1,9 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.CollectionDto + +interface CollectionAdminService { + fun createCollection(collection: CollectionDto): CollectionDto + fun updateCollection(collectionID: Int, collection: CollectionDto): CollectionDto + fun deleteCollection(collectionID: Int) +} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/service/CollectionService.kt b/backend/src/main/kotlin/ru/vsu/app/service/CollectionService.kt new file mode 100644 index 0000000..08247ec --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/CollectionService.kt @@ -0,0 +1,21 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.CollectionDto +import org.springframework.stereotype.Service + +@Service +class CollectionService { + fun deleteCollection(collectionID: Int) { + // Implementation for deleting a collection + } + + fun updateCollection(collectionID: Int, collection: CollectionDto): CollectionDto { + // Implementation for updating a collection + return collection + } + + fun createCollection(collection: CollectionDto): CollectionDto { + // Implementation for creating a collection + return collection + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/InventoryService.kt b/backend/src/main/kotlin/ru/vsu/app/service/InventoryService.kt index 3cdfb30..c5ab915 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/InventoryService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/InventoryService.kt @@ -1,83 +1,84 @@ -// package ru.vsu.app.service +package ru.vsu.app.service -// import org.springframework.stereotype.Service -// import org.springframework.transaction.annotation.Transactional -// import ru.vsu.app.dto.responses.CardResponse -// import ru.vsu.app.dto.responses.InventoryGet200Response -// import ru.vsu.app.dto.responses.InventorySortPost200Response -// import ru.vsu.app.dto.requests.OtherProfileInventorySortPostRequest -// import ru.vsu.app.model.CardEntity -// import ru.vsu.app.model.UserEntity -// import ru.vsu.app.repository.InventoryRepository -// import ru.vsu.app.repository.UserRepository -// import jakarta.persistence.EntityNotFoundException +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional -// @Service -// class InventoryService( -// private val InventoryRepository: InventoryRepository, -// private val userRepository: UserRepository -// ) { -// fun getUserCards(user: UserEntity, sortBy: String = "rarity"): List { -// val cards = when (sortBy) { -// "rarity" -> InventoryRepository.findAllByOwnerOrderByRarityDesc(user) -// "collection" -> InventoryRepository.findAllByOwnerOrderByCollectionAsc(user) -// else -> InventoryRepository.findAllByOwner(user) -// } +import ru.vsu.app.dto.responses.inventory.InventoryGet200Response + +import ru.vsu.app.model.CardEntity +import ru.vsu.app.model.UserEntity + +import ru.vsu.app.repository.InventoryRepository +import ru.vsu.app.repository.CollectionRepository +import ru.vsu.app.repository.UserRepository + +import ru.vsu.app.mapper.CardMapper +import ru.vsu.app.mapper.CollectionMapper + +import jakarta.persistence.EntityNotFoundException + +@Service +class InventoryService( + private val inventoryRepository: InventoryRepository, + private val userRepository: UserRepository, + private val collectionMapper: CollectionMapper, + private val collectionRepository: CollectionRepository, + private val cardMapper: CardMapper +) { + + // fun getUserCards(user: UserEntity, sortBy: String = "rarity"): List { + // val cards = when (sortBy) { + // "rarity" -> InventoryRepository.findAllByOwnerOrderByRarityDesc(user) + // "collection" -> InventoryRepository.findAllByOwnerOrderByCollectionAsc(user) + // else -> InventoryRepository.findAllByOwner(user) + // } -// return cards.map { card -> mapToCardResponse(card) } -// } + // return cards.map { card -> mapToCardResponse(card) } + // } -// fun getCardDetails(cardId: Long, user: UserEntity): CardResponse { -// val card = InventoryRepository.findById(cardId) -// .orElseThrow { EntityNotFoundException("Card not found with id: $cardId") } + // fun getCardDetails(cardId: Long, user: UserEntity): CardResponse { + // val card = InventoryRepository.findById(cardId) + // .orElseThrow { EntityNotFoundException("Card not found with id: $cardId") } -// if (card.owner?.id != user.id) { -// throw IllegalStateException("User does not own this card") -// } + // if (card.owner?.id != user.id) { + // throw IllegalStateException("User does not own this card") + // } -// return mapToCardResponse(card) -// } + // return mapToCardResponse(card) + // } -// @Transactional -// fun disassembleCard(cardId: Long, user: UserEntity): Int { -// val card = InventoryRepository.findById(cardId) -// .orElseThrow { EntityNotFoundException("Card not found with id: $cardId") } + // @Transactional + // fun disassembleCard(cardId: Long, user: UserEntity): Int { + // val card = InventoryRepository.findById(cardId) + // .orElseThrow { EntityNotFoundException("Card not found with id: $cardId") } -// if (card.owner?.id != user.id) { -// throw IllegalStateException("User does not own this card") -// } + // if (card.owner?.id != user.id) { + // throw IllegalStateException("User does not own this card") + // } -// // Add coins to user's balance -// val updatedUser = user.copy(coins = user.coins + card.disassemblePrice) -// userRepository.save(updatedUser) + // // Add coins to user's balance + // val updatedUser = user.copy(coins = user.coins + card.disassemblePrice) + // userRepository.save(updatedUser) -// // Delete the card -// InventoryRepository.delete(card) + // // Delete the card + // InventoryRepository.delete(card) -// return card.disassemblePrice -// } + // return card.disassemblePrice + // } -// fun getInventoryData(user: UserEntity): InventoryGet200Response { -// val allCards = InventoryRepository.findAllByOwner(user) -// val favoriteCards = allCards.filter { it.isFavorite == true } -// return InventoryGet200Response( -// cards = allCards.map { mapToCardResponse(it) }, -// favorites = favoriteCards.map { mapToCardResponse(it) }, -// balance = user.coins -// ) -// } + fun getInventoryData(user: UserEntity): InventoryGet200Response { + val allCards = user.inventoryCards + val favoriteCards = user.favoriteCards + val inventoryDtos = allCards.map { cardMapper.toDto(it) } + val completedColls = user.completedCollections + val collectionDtos = completedColls.map { collectionMapper.toDto(it) } -// private fun mapToCardResponse(card: CardEntity): CardResponse { -// return CardResponse( -// id = card.id, -// name = card.name, -// imageUrl = card.imageUrl, -// rarity = card.rarity, -// collection = card.collection, -// description = card.description, -// type = card.type, -// disassemblePrice = card.disassemblePrice -// ) -// } -// } \ No newline at end of file + return InventoryGet200Response( + inventory = inventoryDtos, + favoriteCards = favoriteCards.map { it.id }, + collections = collectionDtos, + balance = user.balance + ) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/service/NewsService.kt b/backend/src/main/kotlin/ru/vsu/app/service/NewsService.kt index 3b99e21..a326760 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/NewsService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/NewsService.kt @@ -1,49 +1,21 @@ package ru.vsu.app.service +import ru.vsu.app.dto.NewsDto import org.springframework.stereotype.Service -import ru.vsu.app.repository.NewsRepository -import ru.vsu.app.dto.requests.NewsRequest -import ru.vsu.app.dto.responses.home.NewsResponse -import ru.vsu.app.model.NewsEntity -import java.time.LocalDateTime @Service -class NewsService( - private var newsRepository: NewsRepository -) { - - fun createNews(request: NewsRequest): NewsResponse { - var news = NewsEntity( - title = request.title, - content = request.content, - createdAt = LocalDateTime.now(), - updatedAt = LocalDateTime.now() - ) - return toResponse(newsRepository.save(news)) - } - - fun updateNews(id: Int, request: NewsRequest): NewsResponse { - var news = newsRepository.findById(id) - .orElseThrow { RuntimeException("Новость не найдена") } - - news.title = request.title - news.content = request.content - news.updatedAt = LocalDateTime.now() - - return toResponse(newsRepository.save(news)) +class NewsService { + fun deleteNews(newsID: Int) { + // Implementation for deleting news } - fun deleteNews(id: Int) { - newsRepository.deleteById(id) + fun updateNews(newsID: Int, news: NewsDto): NewsDto { + // Implementation for updating news + return news } - private fun toResponse(news: NewsEntity): NewsResponse { - return NewsResponse( - id = news.id!!, - title = news.title, - content = news.content, - createdAt = news.createdAt, - updatedAt = news.updatedAt - ) + fun createNews(news: NewsDto): NewsDto { + // Implementation for creating news + return news } } diff --git a/backend/src/main/kotlin/ru/vsu/app/service/PackService.kt b/backend/src/main/kotlin/ru/vsu/app/service/PackService.kt new file mode 100644 index 0000000..2f9ff32 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/PackService.kt @@ -0,0 +1,9 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.PackDto + +interface PackService { + fun createPack(pack: PackDto): PackDto + fun updatePack(packID: Int, pack: PackDto): PackDto + fun deletePack(packID: Int) +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/ReportService.kt b/backend/src/main/kotlin/ru/vsu/app/service/ReportService.kt new file mode 100644 index 0000000..3240798 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/ReportService.kt @@ -0,0 +1,45 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.ReportDto +import ru.vsu.app.dto.requests.AdminReportsReportIDPutRequest +import org.springframework.stereotype.Service +import ru.vsu.app.dto.UserDto +import java.time.LocalDateTime + +interface ReportService { + fun getAllReports(): List + fun getReportById(reportID: Int): ReportDto? + fun updateReportStatus(reportID: Int, request: AdminReportsReportIDPutRequest): ReportDto +} + +@Service +class ReportServiceImpl : ReportService { + fun getAllReports(): List { + // Implementation for getting all reports + return emptyList() + } + + fun getReportById(reportID: Int): ReportDto { + // Implementation for getting a report by ID + return ReportDto( + reportID = reportID, + reporter = UserDto(userID = 1, username = "Reporter", email = "reporter@example.com", inventoryCards = emptyList(), balance = 0, achievements = emptyList()), + reportedUser = UserDto(userID = 2, username = "ReportedUser", email = "reporteduser@example.com", inventoryCards = emptyList(), balance = 0, achievements = emptyList()), + reportDateTime = LocalDateTime.now(), + reason = "Reason", + status = ReportDto.Status.На_рассмотрении + ) + } + + fun updateReportStatus(reportID: Int, request: AdminReportsReportIDPutRequest): ReportDto { + // Implementation for updating report status + return ReportDto( + reportID = reportID, + reporter = UserDto(userID = 1, username = "Reporter", email = "reporter@example.com", inventoryCards = emptyList(), balance = 0, achievements = emptyList()), + reportedUser = UserDto(userID = 2, username = "ReportedUser", email = "reporteduser@example.com", inventoryCards = emptyList(), balance = 0, achievements = emptyList()), + reportDateTime = LocalDateTime.now(), + reason = "Reason", + status = ReportDto.Status.Подтверждено + ) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/ThemeService.kt b/backend/src/main/kotlin/ru/vsu/app/service/ThemeService.kt new file mode 100644 index 0000000..5518bc8 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/ThemeService.kt @@ -0,0 +1,36 @@ +package ru.vsu.app.service + +import org.springframework.stereotype.Service +import ru.vsu.app.model.ThemeEntity +import ru.vsu.app.repository.ThemeRepository +import ru.vsu.app.dto.ThemeDto + +@Service +class ThemeService(private val themeRepository: ThemeRepository) { + + fun getAllThemes(): List { + return themeRepository.findAll() + } + + fun getThemeById(id: Long): ThemeDto? { + return themeRepository.findById(id).orElse(null) + } + + fun createTheme(theme: ThemeDto): ThemeDto { + return try { + themeRepository.save(theme) + } catch (e: Exception) { + throw RuntimeException("Failed to create theme", e) + } + } + + fun updateTheme(id: Long, theme: ThemeDto): ThemeDto { + val existingTheme = themeRepository.findById(id).orElseThrow { Exception("Theme not found") } + ) + return themeRepository.save(updatedTheme) + } + + fun deleteTheme(id: Long) { + themeRepository.deleteById(id) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/TradeAdminService.kt b/backend/src/main/kotlin/ru/vsu/app/service/TradeAdminService.kt new file mode 100644 index 0000000..0a6f5a0 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/TradeAdminService.kt @@ -0,0 +1,10 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.TradeDto +import ru.vsu.app.dto.requests.AdminTradesTradeIDInvalidatePostRequest +import ru.vsu.app.dto.responses.admin.AdminTradesTradeIDInvalidatePost200Response + +interface TradeAdminService { + fun getAllTrades(): List + fun invalidateTrade(tradeID: Int, request: AdminTradesTradeIDInvalidatePostRequest): AdminTradesTradeIDInvalidatePost200Response +} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/service/TradeService.kt b/backend/src/main/kotlin/ru/vsu/app/service/TradeService.kt new file mode 100644 index 0000000..b9abab3 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/TradeService.kt @@ -0,0 +1,19 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.TradeDto +import ru.vsu.app.dto.requests.AdminTradesTradeIDInvalidatePostRequest +import ru.vsu.app.dto.responses.admin.AdminTradesTradeIDInvalidatePost200Response +import org.springframework.stereotype.Service + +@Service +class TradeService { + fun getAllTrades(): List { + // Implementation for getting all trades + return emptyList() + } + + fun invalidateTrade(tradeID: Int, request: AdminTradesTradeIDInvalidatePostRequest): AdminTradesTradeIDInvalidatePost200Response { + // Implementation for invalidating a trade + return AdminTradesTradeIDInvalidatePost200Response() + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/UserService.kt b/backend/src/main/kotlin/ru/vsu/app/service/UserService.kt index 07dd75f..5f4c4ac 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/UserService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/UserService.kt @@ -1,193 +1,38 @@ -// package ru.vsu.app.service +package ru.vsu.app.service -// import org.springframework.security.crypto.password.PasswordEncoder -// import org.springframework.stereotype.Service -// import ru.vsu.app.dto.responses.RegisterResponse -// import ru.vsu.app.dto.responses.ApiResponse -// import ru.vsu.app.dto.responses.LoginResponse -// import ru.vsu.app.dto.responses.UserInfoResponse -// import ru.vsu.app.dto.requests.RegisterUserRequest -// import ru.vsu.app.dto.requests.ActivateAccountRequest -// import ru.vsu.app.dto.requests.LoginRequest -// import ru.vsu.app.dto.requests.ForgotPasswordRequest -// import ru.vsu.app.dto.requests.ResetPasswordRequest -// import ru.vsu.app.model.UserEntity -// import ru.vsu.app.repository.UserRepository -// import ru.vsu.app.service.JwtService -// import kotlin.random.Random +import ru.vsu.app.dto.requests.AdminUsersUserIDBanPostRequest +import ru.vsu.app.dto.requests.AdminUsersUserIDQuestsQuestIDResetPostRequest +import ru.vsu.app.dto.responses.admin.AdminUsersUserIDBanPost200Response +import ru.vsu.app.dto.responses.admin.AdminUsersUserIDQuestsQuestIDResetPost200Response +import ru.vsu.app.dto.responses.admin.AdminUsersUserIDUnbanPost200Response +import org.springframework.stereotype.Service -// @Service -// class UserService( -// private val userRepository: UserRepository, -// private val passwordEncoder: PasswordEncoder, -// private val emailService: EmailService, -// private val jwtService: JwtService -// ) { +@Service +class UserService { + fun revokeAchievement(userID: Int, achievementID: Int) { + // Implementation for revoking an achievement from a user + } -// fun register(request: RegisterUserRequest): RegisterResponse { -// // Проверяем, что пользователь с таким email или username еще не существует -// if (userRepository.existsByEmail(request.email)) { -// return RegisterUser409Response("Пользователь с таким email уже существует") -// } - -// if (userRepository.existsByUsername(request.username)) { -// return RegisterUser409Response("Пользователь с таким username уже существует") -// } - -// // Генерируем 6-значный код активации -// val activationCode = generateSixDigitCode() - -// // Создаем пользователя -// val user = UserEntity( -// email = request.email, -// username = request.username, -// passwordHash = passwordEncoder.encode(request.password), -// isEnabled = false, -// activationToken = activationCode -// ) - -// userRepository.save(user) - -// // Отправляем email с кодом подтверждения -// emailService.sendActivationEmail(user.email, activationCode) - -// return RegisterResponse("Пользователь успешно зарегистрирован. Проверьте email для получения кода активации", true) // 200 -// } - -// fun activateAccount(request: ActivateAccountRequest): ApiResponse { -// val userOptional = userRepository.findByEmail(request.email) - -// if (userOptional.isEmpty) { -// return ApiResponse("Пользователь с таким email не найден", false) -// } - -// val user = userOptional.get() - -// // Если аккаунт уже активирован -// if (user.isEnabled) { -// return ApiResponse("Аккаунт уже активирован", true) -// } - -// // Проверяем код активации -// if (user.activationToken != request.code) { -// return ApiResponse("Неверный код активации", false) -// } - -// // Обновляем пользователя -// val updatedUser = user.copy( -// isEnabled = true, -// activationToken = null -// ) - -// userRepository.save(updatedUser) - -// return ApiResponse("Аккаунт успешно активирован", true) -// } - -// fun login(request: LoginRequest): LoginResponse? { -// val userOptional = userRepository.findByEmail(request.email) - -// if (userOptional.isEmpty) { -// return null -// } - -// val user = userOptional.get() - -// // Проверяем, активирован ли аккаунт -// if (!user.isEnabled) { -// return null -// } - -// if (!passwordEncoder.matches(request.password, user.passwordHash)) { -// return null -// } - -// val token = jwtService.generateToken(user.email) - -// return LoginResponse( -// token = token, -// username = user.username, -// email = user.email -// ) -// } - -// fun getUserInfo(email: String): UserInfoResponse { -// val user = userRepository.findByEmail(email).orElseThrow { -// IllegalArgumentException("Пользователь не найден") -// } - -// return UserInfoResponse( -// id = user.id, -// username = user.username, -// email = user.email -// ) -// } - -// fun forgotPassword(request: ForgotPasswordRequest): ApiResponse { -// val userOptional = userRepository.findByEmail(request.email) - -// if (userOptional.isEmpty) { -// return ApiResponse("Пользователь с таким email не найден", false) -// } - -// val user = userOptional.get() - -// // Проверяем, активирован ли аккаунт -// if (!user.isEnabled) { -// return ApiResponse("Аккаунт не активирован", false) -// } - -// // Генерируем 6-значный код для сброса пароля -// val resetCode = generateSixDigitCode() - -// // Код будет действителен в течение 15 минут -// val expiryTime = System.currentTimeMillis() + (15 * 60 * 1000) // 15 минут - -// // Обновляем пользователя с кодом сброса пароля -// val updatedUser = user.copy( -// passwordResetToken = resetCode, -// passwordResetTokenExpiry = expiryTime -// ) - -// userRepository.save(updatedUser) - -// // Отправляем код сброса пароля по email -// emailService.sendPasswordResetEmail(user.email, resetCode) - -// return ApiResponse("Код для сброса пароля отправлен на ваш email", true) -// } - -// fun resetPassword(request: ResetPasswordRequest): ApiResponse { -// val userOptional = userRepository.findByPasswordResetToken(request.resetToken) - -// if (userOptional.isEmpty) { -// return ApiResponse("Неверный код сброса пароля", false) -// } - -// val user = userOptional.get() - -// // Сохраняем значение токена в локальную переменную -// val tokenExpiry = user.passwordResetTokenExpiry - -// // Проверяем срок действия кода -// if (tokenExpiry == null || System.currentTimeMillis() > tokenExpiry) { -// return ApiResponse("Срок действия кода истек", false) -// } - -// // Обновляем пользователя с новым паролем -// val updatedUser = user.copy( -// passwordHash = passwordEncoder.encode(request.newPassword), -// passwordResetToken = null, -// passwordResetTokenExpiry = null -// ) - -// userRepository.save(updatedUser) - -// return ApiResponse("Пароль успешно изменен", true) -// } - -// // Функция генерации 6-значного кода -// private fun generateSixDigitCode(): String { -// return (100000 + Random.nextInt(900000)).toString() -// } -// } \ No newline at end of file + fun banUser(userID: Int, request: AdminUsersUserIDBanPostRequest): AdminUsersUserIDBanPost200Response { + // Implementation for banning a user + return AdminUsersUserIDBanPost200Response() + } + + fun deleteUser(userID: Int) { + // Implementation for deleting a user + } + + fun deleteCardFromInventory(userID: Int, cardID: Int) { + // Implementation for deleting a card from a user's inventory + } + + fun resetQuest(userID: Int, questID: Int, request: AdminUsersUserIDQuestsQuestIDResetPostRequest): AdminUsersUserIDQuestsQuestIDResetPost200Response { + // Implementation for resetting a quest for a user + return AdminUsersUserIDQuestsQuestIDResetPost200Response() + } + + fun unbanUser(userID: Int): AdminUsersUserIDUnbanPost200Response { + // Implementation for unbanning a user + return AdminUsersUserIDUnbanPost200Response() + } +} From aa2949c47f5cf61eccce4792a996c3724c1ec347 Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Wed, 4 Jun 2025 06:14:31 +0300 Subject: [PATCH 49/54] fixing some mistakes --- .../kotlin/ru/vsu/app/controller/AdminController.kt | 3 ++- .../main/kotlin/ru/vsu/app/service/ReportService.kt | 6 +++--- .../main/kotlin/ru/vsu/app/service/ThemeService.kt | 11 +++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt index 6f19688..c838658 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt @@ -20,6 +20,7 @@ import ru.vsu.app.dto.PackDto import ru.vsu.app.dto.responses.common.InternalServerError import ru.vsu.app.dto.ReportDto import ru.vsu.app.dto.TradeDto +import ru.vsu.app.dto.ThemeDto import ru.vsu.app.service.AchievementService import ru.vsu.app.service.CardService @@ -665,7 +666,7 @@ fun adminUsersUserIDUnbanPost(@Parameter(description = "", required = true) @Pat produces = ["application/json"], consumes = ["application/json"] ) -fun adminThemesPost(@Parameter(description = "", required = true) @Valid @RequestBody theme: ThemeDto): ResponseEntity { +fun adminThemesPost(@Parameter(description = "", required = true) @Valid @RequestBody theme: ThemeEntity): ResponseEntity { try { val createdTheme = themeService.createTheme(theme) return ResponseEntity.status(HttpStatus.CREATED).body(createdTheme) diff --git a/backend/src/main/kotlin/ru/vsu/app/service/ReportService.kt b/backend/src/main/kotlin/ru/vsu/app/service/ReportService.kt index 3240798..713e1e6 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/ReportService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/ReportService.kt @@ -14,12 +14,12 @@ interface ReportService { @Service class ReportServiceImpl : ReportService { - fun getAllReports(): List { + override fun getAllReports(): List { // Implementation for getting all reports return emptyList() } - fun getReportById(reportID: Int): ReportDto { + override fun getReportById(reportID: Int): ReportDto { // Implementation for getting a report by ID return ReportDto( reportID = reportID, @@ -31,7 +31,7 @@ class ReportServiceImpl : ReportService { ) } - fun updateReportStatus(reportID: Int, request: AdminReportsReportIDPutRequest): ReportDto { + override fun updateReportStatus(reportID: Int, request: AdminReportsReportIDPutRequest): ReportDto { // Implementation for updating report status return ReportDto( reportID = reportID, diff --git a/backend/src/main/kotlin/ru/vsu/app/service/ThemeService.kt b/backend/src/main/kotlin/ru/vsu/app/service/ThemeService.kt index 5518bc8..0a3b402 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/ThemeService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/ThemeService.kt @@ -8,15 +8,15 @@ import ru.vsu.app.dto.ThemeDto @Service class ThemeService(private val themeRepository: ThemeRepository) { - fun getAllThemes(): List { + fun getAllThemes(): List { return themeRepository.findAll() } - fun getThemeById(id: Long): ThemeDto? { + fun getThemeById(id: Long): ThemeEntity? { return themeRepository.findById(id).orElse(null) } - fun createTheme(theme: ThemeDto): ThemeDto { + fun createTheme(theme: ThemeEntity): ThemeEntity { return try { themeRepository.save(theme) } catch (e: Exception) { @@ -24,10 +24,9 @@ class ThemeService(private val themeRepository: ThemeRepository) { } } - fun updateTheme(id: Long, theme: ThemeDto): ThemeDto { + fun updateTheme(id: Long, theme: ThemeEntity): ThemeEntity { val existingTheme = themeRepository.findById(id).orElseThrow { Exception("Theme not found") } - ) - return themeRepository.save(updatedTheme) + return themeRepository.save(existingTheme) } fun deleteTheme(id: Long) { From bbf651df16ba1a63750aa5727007baf7d1688531 Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Wed, 4 Jun 2025 09:38:58 +0300 Subject: [PATCH 50/54] FCCX-94 add trades --- .../ru/vsu/app/controller/TradesController.kt | 60 ++++++-- .../kotlin/ru/vsu/app/mapper/TradeMapper.kt | 23 +++ .../ru/vsu/app/repository/TradeRepository.kt | 12 ++ .../kotlin/ru/vsu/app/service/TradeService.kt | 144 +++++++++++++++++- 4 files changed, 217 insertions(+), 22 deletions(-) create mode 100644 backend/src/main/kotlin/ru/vsu/app/mapper/TradeMapper.kt create mode 100644 backend/src/main/kotlin/ru/vsu/app/repository/TradeRepository.kt diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/TradesController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/TradesController.kt index 0e50f8a..d5bc87b 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/TradesController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/TradesController.kt @@ -9,6 +9,8 @@ import ru.vsu.app.dto.responses.trades.TradesTradeIdAcceptPost403Response import ru.vsu.app.dto.responses.trades.TradesTradeIdCancelPost200Response import ru.vsu.app.dto.responses.trades.TradesTradeIdGet404Response import ru.vsu.app.dto.responses.trades.TradesTradeIdRejectPost200Response +import ru.vsu.app.service.TradeService +import ru.vsu.app.security.CustomUserDetails import io.swagger.v3.oas.annotations.* import io.swagger.v3.oas.annotations.enums.* import io.swagger.v3.oas.annotations.media.* @@ -18,11 +20,12 @@ import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity - import org.springframework.web.bind.annotation.* import org.springframework.validation.annotation.Validated import org.springframework.web.context.request.NativeWebRequest import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.core.userdetails.UserDetails import jakarta.validation.Valid import jakarta.validation.constraints.DecimalMax @@ -42,7 +45,9 @@ import kotlin.collections.Map @SecurityRequirement(name = "Bearer Authentication") @RequestMapping("\${api.base-path:/api}") @Tag(name = "Trades", description = "Операции на странице \"Обменник\"") -class TradesController() { +class TradesController( + private val tradeService: TradeService +) { @Operation( summary = "Получить все доступные предложения обмена", @@ -59,8 +64,8 @@ class TradesController() { value = ["/trades"], produces = ["application/json"] ) - fun tradesGet( @RequestParam(value = "search", required = false) search: kotlin.String?): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun tradesGet(@RequestParam(value = "search", required = false) search: String?): ResponseEntity> { + return ResponseEntity.ok(tradeService.getAllTrades(search)) } @Operation( @@ -81,8 +86,13 @@ class TradesController() { produces = ["application/json"], consumes = ["application/json"] ) - fun tradesInitiatePost(@Parameter(description = "", required = true) @Valid @RequestBody tradesInitiatePostRequest: TradesInitiatePostRequest): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun tradesInitiatePost( + @Parameter(description = "", required = true) @Valid @RequestBody tradesInitiatePostRequest: TradesInitiatePostRequest, + @AuthenticationPrincipal userDetails: UserDetails + ): ResponseEntity { + val customUserDetails = userDetails as CustomUserDetails + val trade = tradeService.initiateTrade(tradesInitiatePostRequest, customUserDetails.getUser().userId) + return ResponseEntity.status(HttpStatus.CREATED).body(trade) } @Operation( @@ -101,8 +111,10 @@ class TradesController() { value = ["/trades/my"], produces = ["application/json"] ) - fun tradesMyGet( @RequestParam(value = "user_id", required = true) userId: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun tradesMyGet(@AuthenticationPrincipal userDetails: UserDetails): ResponseEntity> { + val customUserDetails = userDetails as CustomUserDetails + val trades = tradeService.getUserTrades(customUserDetails.getUser().userId) + return ResponseEntity.ok(trades) } @Operation( @@ -122,8 +134,13 @@ class TradesController() { value = ["/trades/{trade_id}/accept"], produces = ["application/json"] ) - fun tradesTradeIdAcceptPost(@Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun tradesTradeIdAcceptPost( + @Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: Int, + @AuthenticationPrincipal userDetails: UserDetails + ): ResponseEntity { + val customUserDetails = userDetails as CustomUserDetails + val trade = tradeService.acceptTrade(tradeId, customUserDetails.getUser().userId) + return ResponseEntity.ok(trade) } @Operation( @@ -142,8 +159,13 @@ class TradesController() { value = ["/trades/{trade_id}/cancel"], produces = ["application/json"] ) - fun tradesTradeIdCancelPost(@Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun tradesTradeIdCancelPost( + @Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: Int, + @AuthenticationPrincipal userDetails: UserDetails + ): ResponseEntity { + val customUserDetails = userDetails as CustomUserDetails + val trade = tradeService.cancelTrade(tradeId, customUserDetails.getUser().userId) + return ResponseEntity.ok(trade) } @Operation( @@ -161,8 +183,9 @@ class TradesController() { value = ["/trades/{trade_id}"], produces = ["application/json"] ) - fun tradesTradeIdGet(@Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun tradesTradeIdGet(@Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: Int): ResponseEntity { + val trade = tradeService.getTradeById(tradeId) + return ResponseEntity.ok(trade) } @Operation( @@ -181,7 +204,12 @@ class TradesController() { value = ["/trades/{trade_id}/reject"], produces = ["application/json"] ) - fun tradesTradeIdRejectPost(@Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun tradesTradeIdRejectPost( + @Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: Int, + @AuthenticationPrincipal userDetails: UserDetails + ): ResponseEntity { + val customUserDetails = userDetails as CustomUserDetails + val trade = tradeService.rejectTrade(tradeId, customUserDetails.getUser().userId) + return ResponseEntity.ok(trade) } } diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/TradeMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/TradeMapper.kt new file mode 100644 index 0000000..fa6e649 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/TradeMapper.kt @@ -0,0 +1,23 @@ +package ru.vsu.app.mapper + +import ru.vsu.app.dto.TradeDto +import ru.vsu.app.model.TradeEntity +import org.springframework.stereotype.Component + +@Component +class TradeMapper( + private val userMapper: UserMapper, + private val cardMapper: CardMapper +) { + fun toDto(entity: TradeEntity): TradeDto { + return TradeDto( + tradeID = entity.tradeID, + offeringUser = userMapper.toDto(entity.offeringUser), + offeringCards = entity.offeringCards.map { cardMapper.toDto(it) }, + receivingUser = userMapper.toDto(entity.receivingUser), + receivingCards = entity.receivingCards.map { cardMapper.toDto(it) }, + isConfirmed = entity.isConfirmed, + tradeDateTime = entity.tradeDateTime + ) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/TradeRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/TradeRepository.kt new file mode 100644 index 0000000..3044e62 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/TradeRepository.kt @@ -0,0 +1,12 @@ +package ru.vsu.app.repository + +import ru.vsu.app.model.TradeEntity +import ru.vsu.app.model.UserEntity +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface TradeRepository : JpaRepository { + fun findByOfferingUserOrReceivingUser(offeringUser: UserEntity, receivingUser: UserEntity): List + fun findByOfferingCardsNameContainingOrReceivingCardsNameContaining(offeringCardName: String, receivingCardName: String): List +} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/service/TradeService.kt b/backend/src/main/kotlin/ru/vsu/app/service/TradeService.kt index b9abab3..47a59fd 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/TradeService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/TradeService.kt @@ -3,17 +3,149 @@ package ru.vsu.app.service import ru.vsu.app.dto.TradeDto import ru.vsu.app.dto.requests.AdminTradesTradeIDInvalidatePostRequest import ru.vsu.app.dto.responses.admin.AdminTradesTradeIDInvalidatePost200Response +import ru.vsu.app.dto.requests.TradesInitiatePostRequest +import ru.vsu.app.model.TradeEntity +import ru.vsu.app.model.UserEntity +import ru.vsu.app.model.CardEntity +import ru.vsu.app.repository.TradeRepository +import ru.vsu.app.repository.UserRepository +import ru.vsu.app.repository.CardRepository +import ru.vsu.app.mapper.TradeMapper import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.http.HttpStatus +import org.springframework.web.server.ResponseStatusException +import java.time.LocalDateTime @Service -class TradeService { - fun getAllTrades(): List { - // Implementation for getting all trades - return emptyList() +class TradeService( + private val tradeRepository: TradeRepository, + private val userRepository: UserRepository, + private val cardRepository: CardRepository, + private val tradeMapper: TradeMapper +) { + fun getAllTrades(search: String?): List { + val trades = if (search != null) { + tradeRepository.findByOfferingCardsNameContainingOrReceivingCardsNameContaining(search, search) + } else { + tradeRepository.findAll() + } + return trades.map { tradeMapper.toDto(it) } } - fun invalidateTrade(tradeID: Int, request: AdminTradesTradeIDInvalidatePostRequest): AdminTradesTradeIDInvalidatePost200Response { - // Implementation for invalidating a trade + fun getTradeById(tradeId: Int): TradeDto { + val trade = tradeRepository.findById(tradeId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Trade not found") } + return tradeMapper.toDto(trade) + } + + fun getUserTrades(userId: Int): List { + val user = userRepository.findById(userId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "User not found") } + val trades = tradeRepository.findByOfferingUserOrReceivingUser(user, user) + return trades.map { tradeMapper.toDto(it) } + } + + @Transactional + fun initiateTrade(request: TradesInitiatePostRequest, currentUserId: Int): TradeDto { + val offeringUser = userRepository.findById(currentUserId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Offering user not found") } + + val offeredCard = cardRepository.findById(request.offeredCardId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Offered card not found") } + + if (!offeringUser.inventoryCards.contains(offeredCard)) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "User doesn't own the offered card") + } + + val requestedCards = request.requestedCardId.map { cardId -> + cardRepository.findById(cardId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Requested card not found") } + } + + val trade = TradeEntity( + offeringUser = offeringUser, + offeringCards = listOf(offeredCard), + receivingUser = requestedCards.first().owner ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Requested card has no owner"), + receivingCards = requestedCards, + tradeDateTime = LocalDateTime.now() + ) + + val savedTrade = tradeRepository.save(trade) + return tradeMapper.toDto(savedTrade) + } + + @Transactional + fun acceptTrade(tradeId: Int, currentUserId: Int): TradeDto { + val trade = tradeRepository.findById(tradeId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Trade not found") } + + if (trade.receivingUser.userId != currentUserId) { + throw ResponseStatusException(HttpStatus.FORBIDDEN, "User is not authorized to accept this trade") + } + + if (trade.isConfirmed) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Trade is already confirmed") + } + + // Perform the card exchange + val offeringUser = trade.offeringUser + val receivingUser = trade.receivingUser + + offeringUser.inventoryCards = offeringUser.inventoryCards + trade.receivingCards + receivingUser.inventoryCards = receivingUser.inventoryCards + trade.offeringCards + offeringUser.inventoryCards = offeringUser.inventoryCards - trade.offeringCards + receivingUser.inventoryCards = receivingUser.inventoryCards - trade.receivingCards + + val updatedTrade = trade.copy(isConfirmed = true) + val savedTrade = tradeRepository.save(updatedTrade) + return tradeMapper.toDto(savedTrade) + } + + @Transactional + fun rejectTrade(tradeId: Int, currentUserId: Int): TradeDto { + val trade = tradeRepository.findById(tradeId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Trade not found") } + + if (trade.receivingUser.userId != currentUserId) { + throw ResponseStatusException(HttpStatus.FORBIDDEN, "User is not authorized to reject this trade") + } + + val updatedTrade = trade.copy(isConfirmed = false) + val savedTrade = tradeRepository.save(updatedTrade) + return tradeMapper.toDto(savedTrade) + } + + @Transactional + fun cancelTrade(tradeId: Int, currentUserId: Int): TradeDto { + val trade = tradeRepository.findById(tradeId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Trade not found") } + + if (trade.offeringUser.userId != currentUserId) { + throw ResponseStatusException(HttpStatus.FORBIDDEN, "User is not authorized to cancel this trade") + } + + tradeRepository.delete(trade) + return tradeMapper.toDto(trade) + } + + @Transactional + fun invalidateTrade(tradeId: Int, request: AdminTradesTradeIDInvalidatePostRequest): AdminTradesTradeIDInvalidatePost200Response { + val trade = tradeRepository.findById(tradeId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Trade not found") } + + if (trade.isConfirmed) { + // Reverse the trade + val offeringUser = trade.offeringUser + val receivingUser = trade.receivingUser + + offeringUser.inventoryCards = offeringUser.inventoryCards + trade.offeringCards + receivingUser.inventoryCards = receivingUser.inventoryCards + trade.receivingCards + offeringUser.inventoryCards = offeringUser.inventoryCards - trade.receivingCards + receivingUser.inventoryCards = receivingUser.inventoryCards - trade.offeringCards + } + + tradeRepository.delete(trade) return AdminTradesTradeIDInvalidatePost200Response() } } From 7d30c72265a1c6c04a84e962db799a474eab7394 Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Wed, 4 Jun 2025 09:39:18 +0300 Subject: [PATCH 51/54] some fixes --- .../ru/vsu/app/controller/AdminController.kt | 2 +- .../kotlin/ru/vsu/app/service/PackService.kt | 103 +++++++++++++++++- 2 files changed, 100 insertions(+), 5 deletions(-) diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt index c838658..af8577d 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt @@ -718,7 +718,7 @@ fun adminStatsGet(): ResponseEntity { ) fun adminTradesGet(): ResponseEntity { try { - val trades = tradeService.getAllTrades() + val trades = tradeService.getAllTrades(null) return ResponseEntity.ok(trades) } catch (e: Exception) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при получении списка обменов", e.message ?: "Неизвестная ошибка")) diff --git a/backend/src/main/kotlin/ru/vsu/app/service/PackService.kt b/backend/src/main/kotlin/ru/vsu/app/service/PackService.kt index 2f9ff32..11381a5 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/PackService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/PackService.kt @@ -1,9 +1,104 @@ package ru.vsu.app.service +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional import ru.vsu.app.dto.PackDto +import ru.vsu.app.dto.CardDto +import ru.vsu.app.model.PackEntity +import ru.vsu.app.model.CardEntity +import ru.vsu.app.repository.PackRepository +import jakarta.persistence.EntityNotFoundException -interface PackService { - fun createPack(pack: PackDto): PackDto - fun updatePack(packID: Int, pack: PackDto): PackDto - fun deletePack(packID: Int) +@Service +class PackService( + private val packRepository: PackRepository +) { + @Transactional + fun createPack(pack: PackDto): PackDto { + val packEntity = PackEntity( + name = pack.name, + imageUrl = pack.imageURL, + price = pack.price, + cards = pack.cards.map { cardDto -> + CardEntity( + name = cardDto.name, + imageUrl = cardDto.imageURL, + rarity = CardEntity.Rarity.Обычная, // Default value + disassemblePrice = 0, // Default value + isGenerated = false // Default value + ) + }.toList() + ) + + val savedPack = packRepository.save(packEntity) + + return PackDto( + packID = savedPack.id.toInt(), + name = savedPack.name, + imageURL = savedPack.imageUrl, + cards = savedPack.cards.map { cardEntity -> + CardDto( + cardID = cardEntity.id, + name = cardEntity.name, + imageURL = cardEntity.imageUrl, + rarity = CardDto.Rarity.Обычная, + minPrice = cardEntity.disassemblePrice, + isGenerated = cardEntity.isGenerated, + description = cardEntity.description, + theme = null + ) + }.toList(), + price = savedPack.price + ) + } + + @Transactional + fun updatePack(packID: Int, pack: PackDto): PackDto { + val existingPack = packRepository.findById(packID.toLong()) + .orElseThrow { EntityNotFoundException("Pack not found with id: $packID") } + + val updatedPack = existingPack.copy( + name = pack.name, + imageUrl = pack.imageURL, + price = pack.price, + cards = pack.cards.map { cardDto -> + CardEntity( + name = cardDto.name, + imageUrl = cardDto.imageURL, + rarity = CardEntity.Rarity.Обычная, // Default value + disassemblePrice = 0, // Default value + isGenerated = false // Default value + ) + }.toList() + ) + + val savedPack = packRepository.save(updatedPack) + + return PackDto( + packID = savedPack.id.toInt(), + name = savedPack.name, + imageURL = savedPack.imageUrl, + cards = savedPack.cards.map { cardEntity -> + CardDto( + cardID = cardEntity.id, + name = cardEntity.name, + imageURL = cardEntity.imageUrl, + rarity = CardDto.Rarity.Обычная, + minPrice = cardEntity.disassemblePrice, + isGenerated = cardEntity.isGenerated, + description = cardEntity.description, + theme = null + ) + }.toList(), + price = savedPack.price + ) + } + + @Transactional + fun deletePack(packID: Int) { + if (!packRepository.existsById(packID.toLong())) { + throw EntityNotFoundException("Pack not found with id: $packID") + } + packRepository.deleteById(packID.toLong()) + } } From 2c8678e1bb346aa2be1ddd73e0c3c7661c1fbfdb Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Wed, 4 Jun 2025 09:40:50 +0300 Subject: [PATCH 52/54] add inventory more implementations --- .../vsu/app/controller/InventoryController.kt | 202 ++++++++++++++++-- 1 file changed, 183 insertions(+), 19 deletions(-) diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt index 62fa5d8..32b9529 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt @@ -22,6 +22,8 @@ import ru.vsu.app.dto.responses.common.InternalServerError import ru.vsu.app.model.UserEntity +import ru.vsu.app.repository.UserRepository + import ru.vsu.app.service.InventoryService import ru.vsu.app.metrics.InventoryMetrics @@ -67,7 +69,8 @@ import kotlin.collections.Map @Tag(name = "Inventory", description = "Операции на странице \"Инвентарь\"") class InventoryController( private val inventoryService: InventoryService, - private val inventoryMetrics: InventoryMetrics + private val inventoryMetrics: InventoryMetrics, + private val userRepository: UserRepository ) { @Operation( @@ -88,8 +91,32 @@ class InventoryController( value = ["/inventory/card/{card_ID}/favorite-add"], produces = ["application/json"] ) - fun inventoryCardCardIDFavoriteAddPost(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun inventoryCardCardIDFavoriteAddPost( + @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, + @Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int + ): ResponseEntity { + try { + val userEntity = user.getUser() + val card = userEntity.inventoryCards.find { it.id == cardID } + ?: return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Карта не найдена в инвентаре") + + if (userEntity.favoriteCards.size >= 5) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Достигнут лимит избранных карт (5)") + } + + if (userEntity.favoriteCards.any { it.id == cardID }) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Карта уже в избранном") + } + + userEntity.favoriteCards = userEntity.favoriteCards + card + userRepository.save(userEntity) + + return ResponseEntity.ok().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( + InternalServerError("Ошибка при добавлении карты в избранное", e.message ?: "Неизвестная ошибка") + ) + } } @Operation( @@ -107,8 +134,26 @@ class InventoryController( value = ["/inventory/card/{card_ID}/favorite-delete"], produces = ["application/json"] ) - fun inventoryCardCardIDFavoriteDeleteDelete(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun inventoryCardCardIDFavoriteDeleteDelete( + @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, + @Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int + ): ResponseEntity { + try { + val userEntity = user.getUser() + + if (!userEntity.favoriteCards.any { it.id == cardID }) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Карта не найдена в избранном") + } + + userEntity.favoriteCards = userEntity.favoriteCards.filter { it.id != cardID } + userRepository.save(userEntity) + + return ResponseEntity.ok().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( + InternalServerError("Ошибка при удалении карты из избранного", e.message ?: "Неизвестная ошибка") + ) + } } @Operation( @@ -126,8 +171,20 @@ class InventoryController( value = ["/inventory/card/{card_ID}/favorite"], produces = ["application/json"] ) - fun inventoryCardCardIDFavoriteGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun inventoryCardCardIDFavoriteGet( + @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, + @Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int + ): ResponseEntity { + try { + val userEntity = user.getUser() + val isFavorite = userEntity.favoriteCards.any { it.id == cardID } + + return ResponseEntity.ok(InventoryCardCardIDFavoriteGet200Response(isFavorite)) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( + InternalServerError("Ошибка при проверке статуса карты", e.message ?: "Неизвестная ошибка") + ) + } } @Operation( @@ -146,8 +203,20 @@ class InventoryController( value = ["/inventory/card/{card_ID}/quantity"], produces = ["application/json"] ) - fun inventoryCardCardIDQuantityGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun inventoryCardCardIDQuantityGet( + @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, + @Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int + ): ResponseEntity { + try { + val userEntity = user.getUser() + val quantity = userEntity.inventoryCards.count { it.id == cardID } + + return ResponseEntity.ok(InventoryCardCardIDQuantityGet200Response(quantity)) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( + InternalServerError("Ошибка при проверке количества карт", e.message ?: "Неизвестная ошибка") + ) + } } @Operation( @@ -164,8 +233,24 @@ class InventoryController( value = ["/inventory/card/{card_ID}/trade-cancel"], produces = ["application/json"] ) - fun inventoryCardCardIDTradeCancelPost(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun inventoryCardCardIDTradeCancelPost( + @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, + @Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int + ): ResponseEntity { + try { + val userEntity = user.getUser() + val card = userEntity.onChange.find { it.id == cardID } + ?: return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Карта не найдена в разделе 'На обмен'") + + userEntity.onChange = userEntity.onChange.filter { it.id != cardID } + userRepository.save(userEntity) + + return ResponseEntity.ok(InventoryCardCardIDTradeCancelPost200Response("true")) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( + InternalServerError("Ошибка при снятии карты с обмена", e.message ?: "Неизвестная ошибка") + ) + } } @Operation( @@ -184,8 +269,20 @@ class InventoryController( value = ["/inventory/card/{card_ID}/trade-status"], produces = ["application/json"] ) - fun inventoryCardCardIDTradeStatusGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun inventoryCardCardIDTradeStatusGet( + @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, + @Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int + ): ResponseEntity { + try { + val userEntity = user.getUser() + val isOnTrade = userEntity.onChange.any { it.id == cardID } + + return ResponseEntity.ok(InventoryCardCardIDTradeStatusGet200Response(isOnTrade)) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( + InternalServerError("Ошибка при проверке статуса обмена карты", e.message ?: "Неизвестная ошибка") + ) + } } @Operation( @@ -206,8 +303,37 @@ class InventoryController( produces = ["application/json"], consumes = ["application/json"] ) - fun inventoryDestroyPost(@Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun inventoryDestroyPost( + @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, + @Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest + ): ResponseEntity { + try { + val userEntity = user.getUser() + val cardId = inventoryDestroyPostRequest.cardID + + val cardCount = userEntity.inventoryCards.count { it.id == cardId } + if (cardCount < 2) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Недостаточно экземпляров карты для разбора") + } + + val card = userEntity.inventoryCards.find { it.id == cardId } + ?: return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Карта не найдена в инвентаре") + + // Remove one instance of the card + userEntity.inventoryCards = userEntity.inventoryCards.filter { it.id != cardId }.toMutableList().apply { + addAll(userEntity.inventoryCards.filter { it.id == cardId }.drop(1)) + } + + // Add card value to user's balance + userEntity.balance += card.disassemblePrice + userRepository.save(userEntity) + + return ResponseEntity.ok(InventoryDestroyPost200Response(card.disassemblePrice)) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( + InternalServerError("Ошибка при разборе карты", e.message ?: "Неизвестная ошибка") + ) + } } @Operation( @@ -226,8 +352,19 @@ class InventoryController( value = ["/inventory/favorites/count"], produces = ["application/json"] ) - fun inventoryFavoritesCountGet(): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun inventoryFavoritesCountGet( + @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails + ): ResponseEntity { + try { + val userEntity = user.getUser() + val count = userEntity.favoriteCards.size + + return ResponseEntity.ok(InventoryFavoritesCountGet200Response(count)) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( + InternalServerError("Ошибка при получении количества избранных карт", e.message ?: "Неизвестная ошибка") + ) + } } @Operation( @@ -316,7 +453,34 @@ class InventoryController( produces = ["application/json"], consumes = ["application/json"] ) - fun inventoryPutOnTradePost(@Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest): ResponseEntity { - return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + fun inventoryPutOnTradePost( + @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, + @Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest + ): ResponseEntity { + try { + val userEntity = user.getUser() + val cardId = inventoryDestroyPostRequest.cardID + + val cardCount = userEntity.inventoryCards.count { it.id == cardId } + if (cardCount < 2) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Недостаточно экземпляров карты для обмена") + } + + if (userEntity.onChange.any { it.id == cardId }) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Карта уже выставлена на обмен") + } + + val card = userEntity.inventoryCards.find { it.id == cardId } + ?: return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Карта не найдена в инвентаре") + + userEntity.onChange = userEntity.onChange + card + userRepository.save(userEntity) + + return ResponseEntity.ok(OtherProfileCardCardIDInitiateTradePost200Response("true")) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( + InternalServerError("Ошибка при выставлении карты на обмен", e.message ?: "Неизвестная ошибка") + ) + } } } From 308861e2c6c596a6e789348fce66df16627dfb61 Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Wed, 4 Jun 2025 10:31:00 +0300 Subject: [PATCH 53/54] revert changes InventoryController --- .../vsu/app/controller/InventoryController.kt | 204 ++---------------- 1 file changed, 20 insertions(+), 184 deletions(-) diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt index 32b9529..4b1f0e2 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt @@ -22,8 +22,6 @@ import ru.vsu.app.dto.responses.common.InternalServerError import ru.vsu.app.model.UserEntity -import ru.vsu.app.repository.UserRepository - import ru.vsu.app.service.InventoryService import ru.vsu.app.metrics.InventoryMetrics @@ -69,8 +67,7 @@ import kotlin.collections.Map @Tag(name = "Inventory", description = "Операции на странице \"Инвентарь\"") class InventoryController( private val inventoryService: InventoryService, - private val inventoryMetrics: InventoryMetrics, - private val userRepository: UserRepository + private val inventoryMetrics: InventoryMetrics ) { @Operation( @@ -91,32 +88,8 @@ class InventoryController( value = ["/inventory/card/{card_ID}/favorite-add"], produces = ["application/json"] ) - fun inventoryCardCardIDFavoriteAddPost( - @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, - @Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int - ): ResponseEntity { - try { - val userEntity = user.getUser() - val card = userEntity.inventoryCards.find { it.id == cardID } - ?: return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Карта не найдена в инвентаре") - - if (userEntity.favoriteCards.size >= 5) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Достигнут лимит избранных карт (5)") - } - - if (userEntity.favoriteCards.any { it.id == cardID }) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Карта уже в избранном") - } - - userEntity.favoriteCards = userEntity.favoriteCards + card - userRepository.save(userEntity) - - return ResponseEntity.ok().build() - } catch (e: Exception) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( - InternalServerError("Ошибка при добавлении карты в избранное", e.message ?: "Неизвестная ошибка") - ) - } + fun inventoryCardCardIDFavoriteAddPost(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } @Operation( @@ -134,26 +107,8 @@ class InventoryController( value = ["/inventory/card/{card_ID}/favorite-delete"], produces = ["application/json"] ) - fun inventoryCardCardIDFavoriteDeleteDelete( - @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, - @Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int - ): ResponseEntity { - try { - val userEntity = user.getUser() - - if (!userEntity.favoriteCards.any { it.id == cardID }) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Карта не найдена в избранном") - } - - userEntity.favoriteCards = userEntity.favoriteCards.filter { it.id != cardID } - userRepository.save(userEntity) - - return ResponseEntity.ok().build() - } catch (e: Exception) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( - InternalServerError("Ошибка при удалении карты из избранного", e.message ?: "Неизвестная ошибка") - ) - } + fun inventoryCardCardIDFavoriteDeleteDelete(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } @Operation( @@ -171,20 +126,8 @@ class InventoryController( value = ["/inventory/card/{card_ID}/favorite"], produces = ["application/json"] ) - fun inventoryCardCardIDFavoriteGet( - @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, - @Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int - ): ResponseEntity { - try { - val userEntity = user.getUser() - val isFavorite = userEntity.favoriteCards.any { it.id == cardID } - - return ResponseEntity.ok(InventoryCardCardIDFavoriteGet200Response(isFavorite)) - } catch (e: Exception) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( - InternalServerError("Ошибка при проверке статуса карты", e.message ?: "Неизвестная ошибка") - ) - } + fun inventoryCardCardIDFavoriteGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } @Operation( @@ -203,20 +146,8 @@ class InventoryController( value = ["/inventory/card/{card_ID}/quantity"], produces = ["application/json"] ) - fun inventoryCardCardIDQuantityGet( - @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, - @Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int - ): ResponseEntity { - try { - val userEntity = user.getUser() - val quantity = userEntity.inventoryCards.count { it.id == cardID } - - return ResponseEntity.ok(InventoryCardCardIDQuantityGet200Response(quantity)) - } catch (e: Exception) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( - InternalServerError("Ошибка при проверке количества карт", e.message ?: "Неизвестная ошибка") - ) - } + fun inventoryCardCardIDQuantityGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } @Operation( @@ -233,24 +164,8 @@ class InventoryController( value = ["/inventory/card/{card_ID}/trade-cancel"], produces = ["application/json"] ) - fun inventoryCardCardIDTradeCancelPost( - @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, - @Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int - ): ResponseEntity { - try { - val userEntity = user.getUser() - val card = userEntity.onChange.find { it.id == cardID } - ?: return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Карта не найдена в разделе 'На обмен'") - - userEntity.onChange = userEntity.onChange.filter { it.id != cardID } - userRepository.save(userEntity) - - return ResponseEntity.ok(InventoryCardCardIDTradeCancelPost200Response("true")) - } catch (e: Exception) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( - InternalServerError("Ошибка при снятии карты с обмена", e.message ?: "Неизвестная ошибка") - ) - } + fun inventoryCardCardIDTradeCancelPost(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } @Operation( @@ -269,20 +184,8 @@ class InventoryController( value = ["/inventory/card/{card_ID}/trade-status"], produces = ["application/json"] ) - fun inventoryCardCardIDTradeStatusGet( - @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, - @Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int - ): ResponseEntity { - try { - val userEntity = user.getUser() - val isOnTrade = userEntity.onChange.any { it.id == cardID } - - return ResponseEntity.ok(InventoryCardCardIDTradeStatusGet200Response(isOnTrade)) - } catch (e: Exception) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( - InternalServerError("Ошибка при проверке статуса обмена карты", e.message ?: "Неизвестная ошибка") - ) - } + fun inventoryCardCardIDTradeStatusGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } @Operation( @@ -303,37 +206,8 @@ class InventoryController( produces = ["application/json"], consumes = ["application/json"] ) - fun inventoryDestroyPost( - @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, - @Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest - ): ResponseEntity { - try { - val userEntity = user.getUser() - val cardId = inventoryDestroyPostRequest.cardID - - val cardCount = userEntity.inventoryCards.count { it.id == cardId } - if (cardCount < 2) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Недостаточно экземпляров карты для разбора") - } - - val card = userEntity.inventoryCards.find { it.id == cardId } - ?: return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Карта не найдена в инвентаре") - - // Remove one instance of the card - userEntity.inventoryCards = userEntity.inventoryCards.filter { it.id != cardId }.toMutableList().apply { - addAll(userEntity.inventoryCards.filter { it.id == cardId }.drop(1)) - } - - // Add card value to user's balance - userEntity.balance += card.disassemblePrice - userRepository.save(userEntity) - - return ResponseEntity.ok(InventoryDestroyPost200Response(card.disassemblePrice)) - } catch (e: Exception) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( - InternalServerError("Ошибка при разборе карты", e.message ?: "Неизвестная ошибка") - ) - } + fun inventoryDestroyPost(@Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } @Operation( @@ -352,19 +226,8 @@ class InventoryController( value = ["/inventory/favorites/count"], produces = ["application/json"] ) - fun inventoryFavoritesCountGet( - @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails - ): ResponseEntity { - try { - val userEntity = user.getUser() - val count = userEntity.favoriteCards.size - - return ResponseEntity.ok(InventoryFavoritesCountGet200Response(count)) - } catch (e: Exception) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( - InternalServerError("Ошибка при получении количества избранных карт", e.message ?: "Неизвестная ошибка") - ) - } + fun inventoryFavoritesCountGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } @Operation( @@ -453,34 +316,7 @@ class InventoryController( produces = ["application/json"], consumes = ["application/json"] ) - fun inventoryPutOnTradePost( - @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails, - @Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest - ): ResponseEntity { - try { - val userEntity = user.getUser() - val cardId = inventoryDestroyPostRequest.cardID - - val cardCount = userEntity.inventoryCards.count { it.id == cardId } - if (cardCount < 2) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Недостаточно экземпляров карты для обмена") - } - - if (userEntity.onChange.any { it.id == cardId }) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Карта уже выставлена на обмен") - } - - val card = userEntity.inventoryCards.find { it.id == cardId } - ?: return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Карта не найдена в инвентаре") - - userEntity.onChange = userEntity.onChange + card - userRepository.save(userEntity) - - return ResponseEntity.ok(OtherProfileCardCardIDInitiateTradePost200Response("true")) - } catch (e: Exception) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( - InternalServerError("Ошибка при выставлении карты на обмен", e.message ?: "Неизвестная ошибка") - ) - } + fun inventoryPutOnTradePost(@Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } -} +} \ No newline at end of file From eb44e7ffb2b85e4ee4a62cd05b20843664a8bfca Mon Sep 17 00:00:00 2001 From: Nikita Naumov Date: Wed, 4 Jun 2025 22:20:50 +0300 Subject: [PATCH 54/54] update README --- README.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/README.md b/README.md index 550236c..b7bb223 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,63 @@ **Cardly** - это сервис для обмена коллекционными карточками и наборами, включая карточки с изображениями животных, пейзажей и т.д. +## Сборка и развертывание + +### Требования + +#### Для мобильного приложения: +- Flutter SDK (версия указана в pubspec.yaml) +- Android Studio +- JDK 17 или выше +- Android SDK (для Android сборки) + + +#### Для бэкенда: +- JDK 17 или выше +- Kotlin 1.9.25 или выше +- Docker и Docker Compose +- Gradle 8.13 или выше +- Spring Boot 3.4.4 или выше + +### Сборка мобильного приложения + +1. Перейдите в директорию frontend: +```bash +cd frontend +``` + +2. Установите зависимости: +```bash +flutter pub get +``` + +3. Для сборки Android APK: +```bash +flutter build apk --release +``` +APK файл будет находиться в `build/app/outputs/flutter-apk/app-release.apk` + +### Развертывание бэкенда + +1. Перейдите в директорию backend: +```bash +cd backend +``` + +2. Запустите с помощью Docker Compose: +```bash +docker-compose up --build +``` + +### Конфигурация + +#### Бэкенд +- Для изменения порта или других параметров отредактируйте `docker-compose.yml` + +#### Мобильное приложение +- API URL настраивается в `frontend/lib/config/api_config.dart` + + --- ## Команда (ТП 5-1)